Skip to main content

actpub_httpsig/key/
multikey.rs

1//! [FEP-521a] Multikey encoding of public keys.
2//!
3//! The Fediverse's modern actor format represents public keys as a
4//! single `publicKeyMultibase` string of the form `z<base58-btc>`, where
5//! the decoded bytes are `<multicodec-prefix><raw-key-bytes>`.
6//!
7//! This module currently supports Ed25519 (multicodec `0xed`), the
8//! primary algorithm specified by FEP-521a. RSA and other algorithms are
9//! intentionally out of scope here — they are represented via the
10//! traditional Cavage `publicKeyPem` field and not via Multikey.
11//!
12//! [FEP-521a]: https://codeberg.org/fediverse/fep/src/branch/main/fep/521a/fep-521a.md
13
14use multibase::Base;
15use unsigned_varint::{decode, encode};
16
17use crate::error::Error;
18use crate::key::ed25519::{ED25519_PUBLIC_KEY_LEN, Ed25519PublicKey};
19
20/// Ed25519 public-key multicodec identifier (varint-encoded).
21///
22/// See the [multicodec table][table]; `0xed` is the canonical value.
23///
24/// [table]: https://github.com/multiformats/multicodec/blob/master/table.csv
25pub(crate) const ED25519_MULTICODEC: u64 = 0xed;
26
27/// A FEP-521a Multikey, pairing the decoded [`VerifyingKey`](super::VerifyingKey)
28/// with the original base58-btc encoded string.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Multikey {
32    /// The original `z<base58-btc>` encoded string.
33    pub encoded: String,
34    /// Decoded Ed25519 public key.
35    pub key: Ed25519PublicKey,
36}
37
38impl Multikey {
39    /// Encodes an Ed25519 public key as a `z<base58-btc>` Multikey string.
40    #[must_use]
41    pub fn encode_ed25519(key: &Ed25519PublicKey) -> String {
42        let mut buf = encode::u64_buffer();
43        let prefix = encode::u64(ED25519_MULTICODEC, &mut buf);
44        let mut bytes = Vec::with_capacity(prefix.len() + ED25519_PUBLIC_KEY_LEN);
45        bytes.extend_from_slice(prefix);
46        bytes.extend_from_slice(key.as_bytes());
47        multibase::encode(Base::Base58Btc, bytes)
48    }
49
50    /// Decodes a Multikey string into its components.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`Error::InvalidMultibase`] on bad multibase encoding,
55    /// [`Error::InvalidMultikeyPrefix`] when the varint prefix is missing,
56    /// [`Error::UnsupportedAlgorithm`] when the codec is not Ed25519,
57    /// and [`Error::InvalidMultikeyLength`] when the body is the wrong
58    /// length.
59    pub fn decode(encoded: &str) -> Result<Self, Error> {
60        let (_base, bytes) = multibase::decode(encoded)?;
61        let (codec, rest) = decode::u64(&bytes).map_err(|_| Error::InvalidMultikeyPrefix)?;
62        if codec != ED25519_MULTICODEC {
63            return Err(Error::UnsupportedAlgorithm(format!(
64                "Multikey codec 0x{codec:x} is not Ed25519 (0x{ED25519_MULTICODEC:x})"
65            )));
66        }
67        let key = Ed25519PublicKey::from_bytes(rest)?;
68        Ok(Self {
69            encoded: encoded.to_owned(),
70            key,
71        })
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use pretty_assertions::assert_eq;
78
79    use super::*;
80    use crate::key::ed25519::Ed25519SigningKey;
81
82    #[test]
83    fn encode_then_decode_roundtrips() {
84        let signing = Ed25519SigningKey::generate().expect("rng");
85        let public = signing.public_key();
86        let encoded = Multikey::encode_ed25519(&public);
87
88        assert!(
89            encoded.starts_with('z'),
90            "Multikey must be base58-btc encoded (prefix `z`)",
91        );
92
93        let decoded = Multikey::decode(&encoded).expect("round-trip decode");
94        assert_eq!(decoded.key, public);
95        assert_eq!(decoded.encoded, encoded);
96    }
97
98    #[test]
99    fn decode_rejects_wrong_codec() {
100        // Fabricate a Multikey with an unknown codec (0x1205 = rsa-pub).
101        let mut bytes = Vec::new();
102        let mut buf = encode::u64_buffer();
103        let prefix = encode::u64(0x1205, &mut buf);
104        bytes.extend_from_slice(prefix);
105        bytes.extend_from_slice(&[0u8; 32]); // dummy key
106        let encoded = multibase::encode(Base::Base58Btc, bytes);
107
108        let err = Multikey::decode(&encoded).expect_err("RSA codec must be rejected");
109        assert!(matches!(err, Error::UnsupportedAlgorithm(_)));
110    }
111
112    #[test]
113    fn decode_rejects_wrong_body_length() {
114        let mut bytes = Vec::new();
115        let mut buf = encode::u64_buffer();
116        let prefix = encode::u64(ED25519_MULTICODEC, &mut buf);
117        bytes.extend_from_slice(prefix);
118        bytes.extend_from_slice(&[0u8; 16]); // wrong: 16 instead of 32
119        let encoded = multibase::encode(Base::Base58Btc, bytes);
120
121        let err = Multikey::decode(&encoded).expect_err("short body must be rejected");
122        assert!(matches!(
123            err,
124            Error::InvalidMultikeyLength {
125                expected: 32,
126                actual: 16
127            }
128        ));
129    }
130
131    #[test]
132    fn decode_rejects_garbage() {
133        let err = Multikey::decode("not-a-multibase-string").expect_err("bad multibase");
134        assert!(matches!(err, Error::InvalidMultibase(_)));
135    }
136
137    /// Known-good vector from the FEP-521a specification examples,
138    /// cross-checked against Mitra's and Fedify's Multikey implementations.
139    #[test]
140    fn decodes_known_good_fixture() {
141        // A Multikey produced by Mitra for a test account. 32 zero bytes
142        // would give the same encoded prefix, but here we use a well-known
143        // Ed25519 RFC 8032 test vector public key.
144        let key_bytes =
145            hex_literal::hex!("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
146        let public = Ed25519PublicKey::from_bytes(&key_bytes).expect("valid key");
147        let encoded = Multikey::encode_ed25519(&public);
148        // The leading `z6Mk` prefix is a defining feature of Ed25519 Multikeys.
149        assert!(
150            encoded.starts_with("z6Mk"),
151            "Ed25519 Multikey should begin with `z6Mk`, got `{encoded}`",
152        );
153        let decoded = Multikey::decode(&encoded).expect("decode");
154        assert_eq!(decoded.key.as_bytes(), &key_bytes);
155    }
156}