actpub_httpsig/key/
multikey.rs1use multibase::Base;
15use unsigned_varint::{decode, encode};
16
17use crate::error::Error;
18use crate::key::ed25519::{ED25519_PUBLIC_KEY_LEN, Ed25519PublicKey};
19
20pub(crate) const ED25519_MULTICODEC: u64 = 0xed;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Multikey {
32 pub encoded: String,
34 pub key: Ed25519PublicKey,
36}
37
38impl Multikey {
39 #[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 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 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]); 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]); 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 #[test]
140 fn decodes_known_good_fixture() {
141 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 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}