actpub_activitystreams/multikey.rs
1//! [FEP-521a Multikey][fep521a] verification-method type.
2//!
3//! FEP-521a defines a uniform way to publish per-actor verification
4//! keys via the W3C [Controlled Identifiers][cid] vocabulary. Each
5//! `assertionMethod` (or `authentication`) entry is either a bare URL
6//! reference to a remote key document or an inlined [`Multikey`] block
7//! carrying a `multibase`-encoded public key.
8//!
9//! The `publicKeyMultibase` payload uses the `multicodec` envelope to
10//! describe the key type:
11//!
12//! | Codec prefix (varint) | Algorithm |
13//! |-----------------------|-----------|
14//! | `0xed01` | Ed25519 |
15//! | `0x1200` | secp256r1 (P-256) |
16//! | `0x1205` | RSA |
17//!
18//! This crate models the wire form only. Encoding/decoding the
19//! multibase payload back to raw key bytes is handled by
20//! `actpub-httpsig::Multikey`, keeping cryptographic primitives in the
21//! crypto crate.
22//!
23//! [fep521a]: https://codeberg.org/fediverse/fep/src/branch/main/fep/521a/fep-521a.md
24//! [cid]: https://www.w3.org/TR/cid-1.0/
25
26use serde::{Deserialize, Serialize};
27use url::Url;
28
29use crate::value::UrlOr;
30
31/// FEP-521a `Multikey` block.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct Multikey {
35 /// Globally unique identifier for this key, typically the actor URL
36 /// suffixed with a `#key-N` fragment.
37 pub id: Url,
38
39 /// Type discriminator. MUST be `"Multikey"` per FEP-521a §3.
40 #[serde(rename = "type")]
41 pub kind: String,
42
43 /// Actor that owns and rotates this key.
44 pub controller: Url,
45
46 /// Multibase-encoded multicodec public key payload (e.g.
47 /// `z6Mk…` for Ed25519, `zDn…` for P-256).
48 pub public_key_multibase: String,
49}
50
51impl Multikey {
52 /// The fixed type discriminator value mandated by FEP-521a.
53 pub const TYPE: &'static str = "Multikey";
54
55 /// Builds a [`Multikey`] with the canonical `"Multikey"` type.
56 #[must_use]
57 pub fn new(id: Url, controller: Url, public_key_multibase: impl Into<String>) -> Self {
58 Self {
59 id,
60 kind: Self::TYPE.to_owned(),
61 controller,
62 public_key_multibase: public_key_multibase.into(),
63 }
64 }
65}
66
67/// A reference to a verification method, either as a bare URL or an
68/// inlined [`Multikey`] document.
69///
70/// This is the value type shared by `ActivityPub` `assertionMethod` and
71/// `authentication` arrays per FEP-521a §4 and the W3C Controlled
72/// Identifiers spec.
73pub type AssertionMethod = UrlOr<Multikey>;
74
75#[cfg(test)]
76mod tests {
77 use pretty_assertions::assert_eq;
78 use serde_json::json;
79
80 use super::*;
81
82 #[test]
83 fn inline_multikey_roundtrips() {
84 let raw = json!({
85 "id": "https://example.com/users/alice#ed25519-key",
86 "type": "Multikey",
87 "controller": "https://example.com/users/alice",
88 "publicKeyMultibase": "z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2"
89 });
90 let key: Multikey = serde_json::from_value(raw.clone()).unwrap();
91 assert_eq!(key.kind, Multikey::TYPE);
92 assert!(key.public_key_multibase.starts_with('z'));
93 let back = serde_json::to_value(&key).unwrap();
94 assert_eq!(back, raw);
95 }
96
97 #[test]
98 fn assertion_method_accepts_bare_url_form() {
99 let raw = json!("https://example.com/users/alice#main-key");
100 let am: AssertionMethod = serde_json::from_value(raw.clone()).unwrap();
101 assert!(matches!(am, AssertionMethod::Url(_)));
102 let back = serde_json::to_value(&am).unwrap();
103 assert_eq!(back, raw);
104 }
105
106 #[test]
107 fn assertion_method_accepts_inlined_multikey_form() {
108 let raw = json!({
109 "id": "https://example.com/users/alice#main-key",
110 "type": "Multikey",
111 "controller": "https://example.com/users/alice",
112 "publicKeyMultibase": "z6Mk…"
113 });
114 let am: AssertionMethod = serde_json::from_value(raw.clone()).unwrap();
115 assert!(matches!(am, AssertionMethod::Object(_)));
116 let back = serde_json::to_value(&am).unwrap();
117 assert_eq!(back, raw);
118 }
119}