Skip to main content

affinidi_encoding/
multicodec.rs

1//! Multicodec encoding/decoding
2//!
3//! Multicodec is a self-describing format that prefixes data with a varint
4//! indicating the type of data that follows.
5//!
6//! See: <https://github.com/multiformats/multicodec>
7
8use crate::EncodingError;
9use serde::{Deserialize, Serialize};
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12// ****************************************************************************
13// Codec Magic Numbers
14// See: https://github.com/multiformats/multicodec/blob/master/table.csv
15// ****************************************************************************
16pub const ED25519_PUB: u64 = 0xed;
17pub const ED25519_PRIV: u64 = 0x1300;
18pub const X25519_PUB: u64 = 0xec;
19pub const X25519_PRIV: u64 = 0x1302;
20pub const SECP256K1_PUB: u64 = 0xe7;
21pub const SECP256K1_PRIV: u64 = 0x1301;
22pub const P256_PUB: u64 = 0x1200;
23pub const P256_PRIV: u64 = 0x1306;
24pub const P384_PUB: u64 = 0x1201;
25pub const P384_PRIV: u64 = 0x1307;
26pub const P521_PUB: u64 = 0x1202;
27pub const P521_PRIV: u64 = 0x1308;
28// BLS12-381 public keys (used by BBS+ issuers). Compressed group elements:
29// G1 = 48 bytes, G2 = 96 bytes. A BBS issuer's verification key is a G2 point.
30pub const BLS12381_G1_PUB: u64 = 0xea;
31pub const BLS12381_G2_PUB: u64 = 0xeb;
32
33// Post-quantum codecs — draft entries from the official multicodec table.
34// We store ML-DSA private keys as the 32-byte seed, so we use the
35// `-priv-seed` codes (0x131a–0x131c), not the 2560/4032/4896-byte
36// expanded-private codes (0x1317–0x1319).
37//
38// SLH-DSA has no private-key codec registered; `Secret::from_multibase`
39// and `get_private_keymultibase` return an error for SLH-DSA keys.
40pub const ML_DSA_44_PUB: u64 = 0x1210;
41pub const ML_DSA_44_PRIV_SEED: u64 = 0x131a;
42pub const ML_DSA_65_PUB: u64 = 0x1211;
43pub const ML_DSA_65_PRIV_SEED: u64 = 0x131b;
44pub const ML_DSA_87_PUB: u64 = 0x1212;
45pub const ML_DSA_87_PRIV_SEED: u64 = 0x131c;
46pub const SLH_DSA_SHA2_128S_PUB: u64 = 0x1220;
47
48/// Known codec types
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum Codec {
51    Ed25519Pub,
52    Ed25519Priv,
53    X25519Pub,
54    X25519Priv,
55    Secp256k1Pub,
56    Secp256k1Priv,
57    P256Pub,
58    P256Priv,
59    P384Pub,
60    P384Priv,
61    P521Pub,
62    P521Priv,
63    Bls12381G1Pub,
64    Bls12381G2Pub,
65    MlDsa44Pub,
66    MlDsa44PrivSeed,
67    MlDsa65Pub,
68    MlDsa65PrivSeed,
69    MlDsa87Pub,
70    MlDsa87PrivSeed,
71    SlhDsaSha2_128sPub,
72    Unknown(u64),
73}
74
75impl Codec {
76    /// Convert a raw codec value to a Codec enum
77    pub fn from_u64(value: u64) -> Self {
78        match value {
79            ED25519_PUB => Codec::Ed25519Pub,
80            ED25519_PRIV => Codec::Ed25519Priv,
81            X25519_PUB => Codec::X25519Pub,
82            X25519_PRIV => Codec::X25519Priv,
83            SECP256K1_PUB => Codec::Secp256k1Pub,
84            SECP256K1_PRIV => Codec::Secp256k1Priv,
85            P256_PUB => Codec::P256Pub,
86            P256_PRIV => Codec::P256Priv,
87            P384_PUB => Codec::P384Pub,
88            P384_PRIV => Codec::P384Priv,
89            P521_PUB => Codec::P521Pub,
90            P521_PRIV => Codec::P521Priv,
91            BLS12381_G1_PUB => Codec::Bls12381G1Pub,
92            BLS12381_G2_PUB => Codec::Bls12381G2Pub,
93            ML_DSA_44_PUB => Codec::MlDsa44Pub,
94            ML_DSA_44_PRIV_SEED => Codec::MlDsa44PrivSeed,
95            ML_DSA_65_PUB => Codec::MlDsa65Pub,
96            ML_DSA_65_PRIV_SEED => Codec::MlDsa65PrivSeed,
97            ML_DSA_87_PUB => Codec::MlDsa87Pub,
98            ML_DSA_87_PRIV_SEED => Codec::MlDsa87PrivSeed,
99            SLH_DSA_SHA2_128S_PUB => Codec::SlhDsaSha2_128sPub,
100            other => Codec::Unknown(other),
101        }
102    }
103
104    /// Convert to raw u64 value
105    pub fn to_u64(self) -> u64 {
106        match self {
107            Codec::Ed25519Pub => ED25519_PUB,
108            Codec::Ed25519Priv => ED25519_PRIV,
109            Codec::X25519Pub => X25519_PUB,
110            Codec::X25519Priv => X25519_PRIV,
111            Codec::Secp256k1Pub => SECP256K1_PUB,
112            Codec::Secp256k1Priv => SECP256K1_PRIV,
113            Codec::P256Pub => P256_PUB,
114            Codec::P256Priv => P256_PRIV,
115            Codec::P384Pub => P384_PUB,
116            Codec::P384Priv => P384_PRIV,
117            Codec::P521Pub => P521_PUB,
118            Codec::P521Priv => P521_PRIV,
119            Codec::Bls12381G1Pub => BLS12381_G1_PUB,
120            Codec::Bls12381G2Pub => BLS12381_G2_PUB,
121            Codec::MlDsa44Pub => ML_DSA_44_PUB,
122            Codec::MlDsa44PrivSeed => ML_DSA_44_PRIV_SEED,
123            Codec::MlDsa65Pub => ML_DSA_65_PUB,
124            Codec::MlDsa65PrivSeed => ML_DSA_65_PRIV_SEED,
125            Codec::MlDsa87Pub => ML_DSA_87_PUB,
126            Codec::MlDsa87PrivSeed => ML_DSA_87_PRIV_SEED,
127            Codec::SlhDsaSha2_128sPub => SLH_DSA_SHA2_128S_PUB,
128            Codec::Unknown(v) => v,
129        }
130    }
131
132    /// Returns true if this is a public key codec
133    pub fn is_public(&self) -> bool {
134        matches!(
135            self,
136            Codec::Ed25519Pub
137                | Codec::X25519Pub
138                | Codec::Secp256k1Pub
139                | Codec::P256Pub
140                | Codec::P384Pub
141                | Codec::P521Pub
142                | Codec::Bls12381G1Pub
143                | Codec::Bls12381G2Pub
144                | Codec::MlDsa44Pub
145                | Codec::MlDsa65Pub
146                | Codec::MlDsa87Pub
147                | Codec::SlhDsaSha2_128sPub // SLH-DSA has no private codec registered
148        )
149    }
150
151    /// Returns the expected key length for this codec, if known
152    pub fn expected_key_length(&self) -> Option<usize> {
153        match self {
154            Codec::Ed25519Pub | Codec::Ed25519Priv => Some(32),
155            Codec::X25519Pub | Codec::X25519Priv => Some(32),
156            Codec::Secp256k1Pub => Some(33), // compressed
157            Codec::P256Pub => Some(33),      // compressed
158            Codec::P384Pub => Some(49),      // compressed
159            Codec::P521Pub => Some(67),      // compressed
160            // BLS12-381 compressed group elements.
161            Codec::Bls12381G1Pub => Some(48),
162            Codec::Bls12381G2Pub => Some(96),
163            // ML-DSA public keys: FIPS 204 fixed sizes
164            Codec::MlDsa44Pub => Some(1312),
165            Codec::MlDsa65Pub => Some(1952),
166            Codec::MlDsa87Pub => Some(2592),
167            // ML-DSA priv-seed codec (0x131a–0x131c): 32-byte seed (xi)
168            Codec::MlDsa44PrivSeed | Codec::MlDsa65PrivSeed | Codec::MlDsa87PrivSeed => Some(32),
169            // SLH-DSA-SHA2-128s public key: FIPS 205 (32 bytes)
170            Codec::SlhDsaSha2_128sPub => Some(32),
171            _ => None,
172        }
173    }
174}
175
176/// A multicodec-encoded byte slice (borrowed).
177///
178/// `#[repr(transparent)]` guarantees the same memory layout as `[u8]`,
179/// which `MultiEncoded::new` relies on when reinterpreting a borrowed
180/// byte slice as this DST.
181#[derive(Zeroize, ZeroizeOnDrop)]
182#[repr(transparent)]
183pub struct MultiEncoded([u8]);
184
185impl MultiEncoded {
186    /// Create a new multiencoded byte slice, validating the varint
187    /// prefix.
188    pub fn new(bytes: &[u8]) -> Result<&Self, EncodingError> {
189        unsigned_varint::decode::u64(bytes)
190            .map_err(|e| EncodingError::InvalidMulticodec(format!("varint decode: {e}")))?;
191
192        // SAFETY: `MultiEncoded` is a `#[repr(transparent)]` wrapper
193        // around `[u8]`, so `&[u8]` and `&MultiEncoded` have identical
194        // memory layout (including DST metadata). The varint prefix has
195        // been validated above, so every subsequent call into `parts()`
196        // will see well-formed bytes.
197        Ok(unsafe { &*(bytes as *const [u8] as *const MultiEncoded) })
198    }
199
200    /// Size of the byte array (including codec prefix)
201    pub fn len(&self) -> usize {
202        self.0.len()
203    }
204
205    /// Returns true if empty
206    pub fn is_empty(&self) -> bool {
207        self.0.is_empty()
208    }
209
210    /// Separates the codec and the data
211    pub fn parts(&self) -> (u64, &[u8]) {
212        unsigned_varint::decode::u64(&self.0).unwrap()
213    }
214
215    /// Raw codec value (u64)
216    pub fn codec(&self) -> u64 {
217        self.parts().0
218    }
219
220    /// Codec as typed enum
221    pub fn codec_type(&self) -> Codec {
222        Codec::from_u64(self.codec())
223    }
224
225    /// Data bytes (without codec prefix)
226    pub fn data(&self) -> &[u8] {
227        self.parts().1
228    }
229
230    /// Returns the raw bytes, including the codec prefix
231    pub fn as_bytes(&self) -> &[u8] {
232        &self.0
233    }
234}
235
236/// A multicodec-encoded byte buffer (owned)
237#[derive(Clone, Zeroize, ZeroizeOnDrop)]
238pub struct MultiEncodedBuf(Vec<u8>);
239
240impl MultiEncodedBuf {
241    /// Parse an existing multicodec-encoded buffer
242    pub fn new(bytes: Vec<u8>) -> Result<Self, EncodingError> {
243        unsigned_varint::decode::u64(&bytes)
244            .map_err(|e| EncodingError::InvalidMulticodec(format!("varint decode: {e}")))?;
245        Ok(Self(bytes))
246    }
247
248    /// Encode bytes with the given codec
249    pub fn encode(codec: Codec, bytes: &[u8]) -> Self {
250        Self::encode_raw(codec.to_u64(), bytes)
251    }
252
253    /// Encode bytes with a raw codec value (backwards-compatible alias)
254    pub fn encode_bytes(codec: u64, bytes: &[u8]) -> Self {
255        Self::encode_raw(codec, bytes)
256    }
257
258    /// Encode bytes with a raw codec value
259    pub fn encode_raw(codec: u64, bytes: &[u8]) -> Self {
260        let mut codec_buffer = [0u8; 10];
261        let encoded_codec = unsigned_varint::encode::u64(codec, &mut codec_buffer);
262        let mut result = Vec::with_capacity(encoded_codec.len() + bytes.len());
263        result.extend(encoded_codec);
264        result.extend(bytes);
265        Self(result)
266    }
267
268    /// Returns the raw bytes, including the codec prefix
269    /// Note: clones due to ZeroizeOnDrop
270    pub fn into_bytes(self) -> Vec<u8> {
271        self.0.clone()
272    }
273
274    /// Returns a reference to the raw bytes
275    pub fn as_bytes(&self) -> &[u8] {
276        &self.0
277    }
278
279    /// Borrow as MultiEncoded slice.
280    pub fn as_multi_encoded(&self) -> &MultiEncoded {
281        // SAFETY: `MultiEncoded` is `#[repr(transparent)]` over `[u8]`.
282        // The varint prefix was validated when this `MultiEncodedBuf`
283        // was constructed (see `MultiEncodedBuf::new` / `encode_raw`).
284        unsafe { &*(self.0.as_slice() as *const [u8] as *const MultiEncoded) }
285    }
286}
287
288impl AsRef<MultiEncoded> for MultiEncodedBuf {
289    fn as_ref(&self) -> &MultiEncoded {
290        self.as_multi_encoded()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_encode_decode_ed25519() {
300        let key_bytes = [0u8; 32];
301        let encoded = MultiEncodedBuf::encode(Codec::Ed25519Pub, &key_bytes);
302
303        let decoded = MultiEncoded::new(encoded.as_bytes()).unwrap();
304        assert_eq!(decoded.codec(), ED25519_PUB);
305        assert_eq!(decoded.codec_type(), Codec::Ed25519Pub);
306        assert_eq!(decoded.data(), &key_bytes);
307    }
308
309    #[test]
310    fn test_codec_roundtrip() {
311        for codec in [
312            Codec::Ed25519Pub,
313            Codec::X25519Pub,
314            Codec::P256Pub,
315            Codec::P384Pub,
316        ] {
317            let raw = codec.to_u64();
318            assert_eq!(Codec::from_u64(raw), codec);
319        }
320    }
321}