Skip to main content

ojak/portable/
mod.rs

1//! Portable objects (FEP-ef61): verifying them, and signing them.
2//!
3//! A portable object is authentic when an FEP-8b32 integrity proof on it was
4//! made with a key under the DID of its `id`, and by nothing else: not where
5//! it was fetched from, and not the HTTP signature it arrived under.
6//! *docs/guide/portable.md* explains it.
7//!
8//! `did:key` is resolved here, since the DID is the key. Any other method
9//! needs a [`DidResolver`] from the application.
10
11mod hashlink;
12mod uri;
13
14pub use hashlink::{Hashlink, Media};
15pub use uri::*;
16
17use crate::sig::did::{did_key, did_key_method, resolve_did_key};
18use crate::sig::integrity::{self, PublicKey};
19use serde_json::Value;
20use std::fmt;
21use std::future::Future;
22use std::pin::Pin;
23use std::sync::Arc;
24
25/// The most gateways asked for one object, or tried for one delivery. The
26/// hints in an `ap` URI are its writer's to choose, and so are an actor's
27/// gateways: unbounded, one reference could send a server to any number of
28/// hosts of someone else's choosing.
29pub const MAX_GATEWAYS: usize = 5;
30
31/// An error from the application's resolver or signer.
32pub type Error = Box<dyn std::error::Error + Send + Sync>;
33
34type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
35
36/// Resolves the DID URLs of methods other than `did:key` to keys.
37pub trait DidResolver: Send + Sync + 'static {
38    /// The key `did_url` names, or `None` when this resolver does not know
39    /// it.
40    fn resolve<'a>(&'a self, did_url: &'a str) -> BoxFuture<'a, Result<Option<PublicKey>, Error>>;
41}
42
43/// Why a portable document is not authentic.
44#[derive(Debug)]
45pub enum PortableError {
46    /// Its `id` is not an `ap` URI, or it has none.
47    NotPortable,
48    /// It has no usable integrity proof.
49    NoProof,
50    /// The proof's verification method is not under the DID it has to be.
51    ForeignMethod { method: String, did: String },
52    /// The verification method did not resolve to a key.
53    Unresolved { method: String, why: String },
54    /// The proof does not verify.
55    Invalid(String),
56    /// It is an actor and lists no gateway.
57    NoGateways,
58    /// It is an actor and lists something that is not a gateway: every
59    /// gateway has to be an `http` or `https` origin.
60    InvalidGateway(String),
61}
62
63impl fmt::Display for PortableError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::NotPortable => f.write_str("not a portable object"),
67            Self::NoProof => f.write_str("no usable integrity proof"),
68            Self::ForeignMethod { method, did } => {
69                write!(f, "proof key {method} is not under {did}")
70            }
71            Self::Unresolved { method, why } => write!(f, "{method} did not resolve: {why}"),
72            Self::Invalid(why) => write!(f, "proof does not verify: {why}"),
73            Self::NoGateways => f.write_str("portable actor lists no gateways"),
74            Self::InvalidGateway(gateway) => {
75                write!(f, "portable actor lists {gateway}, which is not an origin")
76            }
77        }
78    }
79}
80
81impl std::error::Error for PortableError {}
82
83/// The key `did_url` names: a `did:key`'s own, or what `resolver` says.
84///
85/// # Errors
86///
87/// When it does not resolve.
88pub async fn resolve(
89    did_url: &str,
90    resolver: Option<&dyn DidResolver>,
91) -> Result<PublicKey, PortableError> {
92    let unresolved = |why: String| PortableError::Unresolved {
93        method: did_url.to_owned(),
94        why,
95    };
96    if did_url.starts_with("did:key:") {
97        return resolve_did_key(did_url).map_err(|error| unresolved(error.to_string()));
98    }
99    let resolver = resolver.ok_or_else(|| unresolved("no resolver for its method".into()))?;
100    match resolver.resolve(did_url).await {
101        Ok(Some(key)) => Ok(key),
102        Ok(None) => Err(unresolved("unknown to the resolver".into())),
103        Err(error) => Err(unresolved(error.to_string())),
104    }
105}
106
107/// Verify that `document` carries a proof made with a key under `did`: any
108/// of its proofs, tried in order.
109///
110/// # Errors
111///
112/// When none is, with why the last one tried was not.
113pub async fn verify_by(
114    document: &Value,
115    did: &str,
116    resolver: Option<&dyn DidResolver>,
117) -> Result<(), PortableError> {
118    let mut last = PortableError::NoProof;
119    for (proof, _, method) in integrity::integrity_proofs(document, chrono::Utc::now().timestamp())
120    {
121        if did_of(&method) != Some(did) {
122            last = PortableError::ForeignMethod {
123                method,
124                did: did.to_owned(),
125            };
126            continue;
127        }
128        let key = match resolve(&method, resolver).await {
129            Ok(key) => key,
130            Err(error) => {
131                last = error;
132                continue;
133            }
134        };
135        match integrity::verify_object_integrity_proof(document, &proof, &key) {
136            Ok(()) => return Ok(()),
137            Err(error) => last = PortableError::Invalid(error.to_string()),
138        }
139    }
140    Err(last)
141}
142
143/// Verify a portable document: its `id` is an `ap` URI, a proof by the
144/// `id`'s DID verifies, and an actor lists at least one gateway and nothing
145/// else. Returns the `id`.
146///
147/// # Errors
148///
149/// When any of that does not hold.
150pub async fn verify(
151    document: &Value,
152    resolver: Option<&dyn DidResolver>,
153) -> Result<ApUri, PortableError> {
154    let id = document
155        .get("id")
156        .and_then(Value::as_str)
157        .and_then(ApUri::parse)
158        .ok_or(PortableError::NotPortable)?;
159    if is_actor(document) {
160        // Checked before the proof, which costs more: an actor with a list
161        // that is not all gateways is refused whole, since leaving out what
162        // is not one would change which gateway is its first.
163        let listed = listed_gateways(document);
164        if let Some(invalid) = listed
165            .iter()
166            .find(|gateway| gateway.as_str().is_none_or(|gateway| !is_gateway(gateway)))
167        {
168            return Err(PortableError::InvalidGateway(match invalid {
169                Value::String(gateway) => gateway.clone(),
170                other => other.to_string(),
171            }));
172        }
173        if listed.is_empty() {
174            return Err(PortableError::NoGateways);
175        }
176    }
177    verify_by(document, id.did(), resolver).await?;
178    Ok(id)
179}
180
181/// The gateways an actor document lists, in order, as origins: without the
182/// `/` of an empty path, which some gateways refuse. What is not a gateway
183/// is left out; [`verify`] refuses an actor that lists one.
184///
185/// `gateways` is read as it is written, whether or not the document's
186/// contexts map it: tootik's actors use it without the FEP-ef61 context, and
187/// the proof covers the document as written.
188#[must_use]
189pub fn gateways(actor: &Value) -> Vec<String> {
190    let mut gateways: Vec<String> = Vec::new();
191    for gateway in listed_gateways(actor).into_iter().filter_map(Value::as_str) {
192        let gateway = gateway.strip_suffix('/').unwrap_or(gateway);
193        if is_gateway(gateway) && !gateways.iter().any(|seen| seen == gateway) {
194            gateways.push(gateway.to_owned());
195        }
196    }
197    gateways
198}
199
200fn listed_gateways(actor: &Value) -> Vec<&Value> {
201    match actor.get("gateways") {
202        Some(Value::Array(items)) => items.iter().collect(),
203        Some(Value::Object(list)) => match list.get("@list") {
204            Some(Value::Array(items)) => items.iter().collect(),
205            _ => Vec::new(),
206        },
207        Some(item) => vec![item],
208        None => Vec::new(),
209    }
210}
211
212fn is_actor(document: &Value) -> bool {
213    const ACTORS: [&str; 5] = ["Application", "Group", "Organization", "Person", "Service"];
214    has_type(document, &ACTORS)
215}
216
217/// Whether `document`'s `type` is, or includes, one of `types`.
218pub(crate) fn has_type(document: &Value, types: &[&str]) -> bool {
219    match document.get("type") {
220        Some(Value::String(kind)) => types.contains(&kind.as_str()),
221        Some(Value::Array(kinds)) => kinds
222            .iter()
223            .filter_map(Value::as_str)
224            .any(|kind| types.contains(&kind)),
225        _ => false,
226    }
227}
228
229/// Where a proof comes from: a key the server holds, or one it reaches
230/// through a signing service or the user's device. What is signed is a whole
231/// document, and what comes back is that document with its proof.
232pub trait ProofSigner: Send + Sync + 'static {
233    /// The DID URL of the key, which a proof names as its
234    /// `verificationMethod`.
235    fn verification_method(&self) -> &str;
236
237    /// `document` with an integrity proof attached.
238    fn prove<'a>(&'a self, document: &'a Value) -> BoxFuture<'a, Result<Value, Error>>;
239}
240
241impl<S: ProofSigner + ?Sized> ProofSigner for Arc<S> {
242    fn verification_method(&self) -> &str {
243        (**self).verification_method()
244    }
245
246    fn prove<'a>(&'a self, document: &'a Value) -> BoxFuture<'a, Result<Value, Error>> {
247        (**self).prove(document)
248    }
249}
250
251/// An Ed25519 key held in memory, signing as its `did:key`.
252#[derive(Clone)]
253pub struct Ed25519Signer {
254    seed: [u8; 32],
255    did: String,
256    method: String,
257}
258
259impl fmt::Debug for Ed25519Signer {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.debug_struct("Ed25519Signer")
262            .field("did", &self.did)
263            .finish_non_exhaustive()
264    }
265}
266
267impl Ed25519Signer {
268    /// A signer for the raw 32-byte seed `seed`.
269    #[must_use]
270    pub fn new(seed: [u8; 32]) -> Self {
271        let did = did_key(&integrity::ed25519_public_key(&seed));
272        let method = did_key_method(&did);
273        Self { seed, did, method }
274    }
275
276    /// A signer with a fresh key.
277    #[must_use]
278    pub fn generate() -> Self {
279        Self::new(integrity::generate_ed25519_seed(&mut rand_core::UnwrapErr(
280            getrandom::SysRng,
281        )))
282    }
283
284    /// A signer for a PKCS#8 PEM Ed25519 private key.
285    ///
286    /// # Errors
287    ///
288    /// When `pem` is not one.
289    pub fn from_pem(pem: &str) -> Result<Self, Error> {
290        let (seed, _) = integrity::parse_ed25519_key(pem).map_err(|error| error.to_string())?;
291        Ok(Self::new(seed))
292    }
293
294    /// The `did:key` the key is: the authority of the `ap` URIs it controls.
295    #[must_use]
296    pub fn did(&self) -> &str {
297        &self.did
298    }
299}
300
301impl ProofSigner for Ed25519Signer {
302    fn verification_method(&self) -> &str {
303        &self.method
304    }
305
306    fn prove<'a>(&'a self, document: &'a Value) -> BoxFuture<'a, Result<Value, Error>> {
307        Box::pin(async move {
308            integrity::sign_object_integrity_proof(
309                document,
310                &self.method,
311                &self.seed,
312                chrono::Utc::now().timestamp(),
313            )
314            .map_err(|error| error.to_string().into())
315        })
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use serde_json::json;
323
324    fn actor(signer: &Ed25519Signer) -> Value {
325        json!({
326            "@context": [
327                "https://www.w3.org/ns/activitystreams",
328                "https://w3id.org/security/data-integrity/v1",
329                "https://w3id.org/fep/ef61"
330            ],
331            "type": "Person",
332            "id": format!("ap://{}/actor", signer.did()),
333            "inbox": format!("ap://{}/actor/inbox", signer.did()),
334            "gateways": ["https://server1.example"]
335        })
336    }
337
338    #[tokio::test]
339    async fn what_its_key_signed_is_authentic() {
340        let signer = Ed25519Signer::generate();
341        let signed = signer.prove(&actor(&signer)).await.unwrap();
342        let id = verify(&signed, None).await.unwrap();
343        assert_eq!(id.did(), signer.did());
344        assert_eq!(gateways(&signed), ["https://server1.example"]);
345    }
346
347    #[tokio::test]
348    async fn a_proof_that_verifies_is_found_among_ones_that_do_not() {
349        let owner = Ed25519Signer::generate();
350        let other = Ed25519Signer::generate();
351        let unsigned = actor(&owner);
352        let by_owner = owner.prove(&unsigned).await.unwrap();
353        let by_other = other.prove(&unsigned).await.unwrap();
354        let mut tampered = by_owner["proof"].clone();
355        tampered["created"] = json!("2000-01-01T00:00:00Z");
356        let mut signed = unsigned;
357        signed["proof"] = json!([by_other["proof"], tampered, by_owner["proof"]]);
358        assert_eq!(
359            integrity::integrity_proofs(&signed, chrono::Utc::now().timestamp()).len(),
360            3
361        );
362
363        let id = verify(&signed, None).await.unwrap();
364        assert_eq!(id.did(), owner.did());
365
366        signed["proof"] = json!([by_other["proof"], tampered]);
367        assert!(matches!(
368            verify(&signed, None).await,
369            Err(PortableError::Invalid(_))
370        ));
371    }
372
373    #[tokio::test]
374    async fn another_key_does_not_vouch_for_it() {
375        let owner = Ed25519Signer::generate();
376        let other = Ed25519Signer::generate();
377        let signed = other.prove(&actor(&owner)).await.unwrap();
378        assert!(matches!(
379            verify(&signed, None).await,
380            Err(PortableError::ForeignMethod { .. })
381        ));
382    }
383
384    #[tokio::test]
385    async fn an_actor_lists_only_gateways() {
386        let signer = Ed25519Signer::generate();
387        let mut unsigned = actor(&signer);
388        unsigned["gateways"] = json!(["https://server1.example/", "https://server2.example"]);
389        let signed = signer.prove(&unsigned).await.unwrap();
390        verify(&signed, None).await.unwrap();
391        assert_eq!(
392            gateways(&signed),
393            ["https://server1.example", "https://server2.example"]
394        );
395
396        for listed in [
397            json!(["https://server1.example", "https://server2.example/path"]),
398            json!(["https://server1.example", 1]),
399            json!("ap://did:key:z6Mk/actor"),
400        ] {
401            unsigned["gateways"] = listed;
402            let signed = signer.prove(&unsigned).await.unwrap();
403            assert!(matches!(
404                verify(&signed, None).await,
405                Err(PortableError::InvalidGateway(_))
406            ));
407        }
408        unsigned["gateways"] = json!([]);
409        let signed = signer.prove(&unsigned).await.unwrap();
410        assert!(matches!(
411            verify(&signed, None).await,
412            Err(PortableError::NoGateways)
413        ));
414    }
415
416    #[tokio::test]
417    async fn a_changed_document_does_not_verify() {
418        let signer = Ed25519Signer::generate();
419        let mut signed = signer.prove(&actor(&signer)).await.unwrap();
420        signed["gateways"] = json!(["https://attacker.example"]);
421        assert!(matches!(
422            verify(&signed, None).await,
423            Err(PortableError::Invalid(_))
424        ));
425    }
426
427    #[tokio::test]
428    async fn an_actor_needs_a_gateway_and_a_proof() {
429        let signer = Ed25519Signer::generate();
430        let mut unsigned = actor(&signer);
431        assert!(matches!(
432            verify(&unsigned, None).await,
433            Err(PortableError::NoProof)
434        ));
435        unsigned.as_object_mut().unwrap().remove("gateways");
436        let signed = signer.prove(&unsigned).await.unwrap();
437        assert!(matches!(
438            verify(&signed, None).await,
439            Err(PortableError::NoGateways)
440        ));
441        let https = json!({"id": "https://a.example/users/alice", "type": "Person"});
442        assert!(matches!(
443            verify(&https, None).await,
444            Err(PortableError::NotPortable)
445        ));
446    }
447
448    #[tokio::test]
449    async fn other_methods_need_a_resolver() {
450        struct Web(PublicKeyBytes);
451        struct PublicKeyBytes([u8; 32]);
452        impl DidResolver for Web {
453            fn resolve<'a>(
454                &'a self,
455                did_url: &'a str,
456            ) -> BoxFuture<'a, Result<Option<PublicKey>, Error>> {
457                let key = (did_url == "did:web:a.example#key")
458                    .then(|| PublicKey::Ed25519(Box::new(self.0.0)));
459                Box::pin(async move { Ok(key) })
460            }
461        }
462
463        let seed = [9u8; 32];
464        let public = integrity::ed25519_public_key(&seed);
465        let document = json!({
466            "@context": "https://www.w3.org/ns/activitystreams",
467            "id": "ap://did:web:a.example/objects/1",
468            "type": "Note"
469        });
470        let signed = integrity::sign_object_integrity_proof(
471            &document,
472            "did:web:a.example#key",
473            &seed,
474            chrono::Utc::now().timestamp(),
475        )
476        .unwrap();
477        assert!(matches!(
478            verify(&signed, None).await,
479            Err(PortableError::Unresolved { .. })
480        ));
481        let resolver = Web(PublicKeyBytes(public));
482        verify(&signed, Some(&resolver)).await.unwrap();
483    }
484}