Skip to main content

solid_pod_rs_didkey/
pubkey.rs

1//! Public-key representation for `did:key`.
2//!
3//! Each variant is tagged with the W3C multicodec code used in the
4//! `did:key:z…` form (after varint encoding):
5//!
6//! | Variant    | Codec   | Serialised pubkey size |
7//! |------------|---------|------------------------|
8//! | Ed25519    | `0xed`  | 32 bytes (raw)         |
9//! | P-256      | `0x1200`| 33 bytes (SEC1 compressed) |
10//! | Secp256k1  | `0xe7`  | 33 bytes (SEC1 compressed) |
11//!
12//! Reference:
13//! <https://github.com/multiformats/multicodec/blob/master/table.csv>
14
15use crate::error::DidKeyError;
16
17/// Multicodec code for Ed25519 public keys.
18pub const CODEC_ED25519: u64 = 0xed;
19
20/// Multicodec code for secp256k1 public keys (SEC1 compressed).
21pub const CODEC_SECP256K1: u64 = 0xe7;
22
23/// Multicodec code for NIST P-256 public keys (SEC1 compressed).
24pub const CODEC_P256: u64 = 0x1200;
25
26/// Length of an Ed25519 raw public key.
27pub const ED25519_LEN: usize = 32;
28
29/// Length of a SEC1-compressed P-256 / secp256k1 public key.
30pub const SEC1_COMPRESSED_LEN: usize = 33;
31
32/// W3C `did:key` public-key payload.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum DidKeyPubkey {
35    /// 32-byte raw Ed25519 public key (RFC 8032).
36    Ed25519([u8; ED25519_LEN]),
37
38    /// 33-byte SEC1-compressed NIST P-256 public key.
39    P256(Vec<u8>),
40
41    /// 33-byte SEC1-compressed secp256k1 public key.
42    Secp256k1(Vec<u8>),
43}
44
45impl DidKeyPubkey {
46    /// Human-readable name for diagnostics.
47    pub fn codec_name(&self) -> &'static str {
48        match self {
49            DidKeyPubkey::Ed25519(_) => "ed25519",
50            DidKeyPubkey::P256(_) => "p-256",
51            DidKeyPubkey::Secp256k1(_) => "secp256k1",
52        }
53    }
54
55    /// Multicodec integer code corresponding to this variant.
56    pub fn codec_code(&self) -> u64 {
57        match self {
58            DidKeyPubkey::Ed25519(_) => CODEC_ED25519,
59            DidKeyPubkey::P256(_) => CODEC_P256,
60            DidKeyPubkey::Secp256k1(_) => CODEC_SECP256K1,
61        }
62    }
63
64    /// JWS `alg` value required for a signature produced by the paired
65    /// private key, per RFC 8037 (EdDSA) / RFC 7518 §3.4 (ES256 /
66    /// ES256K).
67    pub fn jws_alg(&self) -> &'static str {
68        match self {
69            DidKeyPubkey::Ed25519(_) => "EdDSA",
70            DidKeyPubkey::P256(_) => "ES256",
71            DidKeyPubkey::Secp256k1(_) => "ES256K",
72        }
73    }
74
75    /// Raw key bytes (no multicodec prefix).
76    pub fn as_bytes(&self) -> &[u8] {
77        match self {
78            DidKeyPubkey::Ed25519(b) => b.as_slice(),
79            DidKeyPubkey::P256(v) => v.as_slice(),
80            DidKeyPubkey::Secp256k1(v) => v.as_slice(),
81        }
82    }
83
84    /// Multicodec payload: `varint(codec) || key_bytes`. Used by the
85    /// `did:key` encoder and by callers that want a raw identifier
86    /// (e.g. CBOR identity tokens).
87    pub fn to_multicodec_bytes(&self) -> Vec<u8> {
88        let mut buf = [0u8; 10]; // unsigned-varint max for u64 is 10.
89        let varint = unsigned_varint::encode::u64(self.codec_code(), &mut buf);
90        let mut out = Vec::with_capacity(varint.len() + self.as_bytes().len());
91        out.extend_from_slice(varint);
92        out.extend_from_slice(self.as_bytes());
93        out
94    }
95
96    /// Parse a multicodec payload into a [`DidKeyPubkey`]. Returns an
97    /// error for unknown codecs or length mismatches.
98    pub fn from_multicodec_bytes(bytes: &[u8]) -> Result<Self, DidKeyError> {
99        let (code, rest) = unsigned_varint::decode::u64(bytes)
100            .map_err(|e| DidKeyError::InvalidMultibase(format!("varint: {e}")))?;
101        match code {
102            CODEC_ED25519 => {
103                if rest.len() != ED25519_LEN {
104                    return Err(DidKeyError::InvalidKeyLength {
105                        codec: "ed25519",
106                        expected: ED25519_LEN,
107                        actual: rest.len(),
108                    });
109                }
110                let mut arr = [0u8; ED25519_LEN];
111                arr.copy_from_slice(rest);
112                Ok(DidKeyPubkey::Ed25519(arr))
113            }
114            CODEC_P256 => {
115                if rest.len() != SEC1_COMPRESSED_LEN {
116                    return Err(DidKeyError::InvalidKeyLength {
117                        codec: "p-256",
118                        expected: SEC1_COMPRESSED_LEN,
119                        actual: rest.len(),
120                    });
121                }
122                Ok(DidKeyPubkey::P256(rest.to_vec()))
123            }
124            CODEC_SECP256K1 => {
125                if rest.len() != SEC1_COMPRESSED_LEN {
126                    return Err(DidKeyError::InvalidKeyLength {
127                        codec: "secp256k1",
128                        expected: SEC1_COMPRESSED_LEN,
129                        actual: rest.len(),
130                    });
131                }
132                Ok(DidKeyPubkey::Secp256k1(rest.to_vec()))
133            }
134            other => Err(DidKeyError::UnknownCodec(other)),
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn ed25519_roundtrip_multicodec() {
145        let k = DidKeyPubkey::Ed25519([7u8; ED25519_LEN]);
146        let bytes = k.to_multicodec_bytes();
147        let out = DidKeyPubkey::from_multicodec_bytes(&bytes).unwrap();
148        assert_eq!(k, out);
149        assert_eq!(k.jws_alg(), "EdDSA");
150    }
151
152    #[test]
153    fn p256_roundtrip_multicodec() {
154        let mut sec1 = vec![0x02; SEC1_COMPRESSED_LEN];
155        sec1[0] = 0x02; // compressed prefix
156        let k = DidKeyPubkey::P256(sec1);
157        let bytes = k.to_multicodec_bytes();
158        let out = DidKeyPubkey::from_multicodec_bytes(&bytes).unwrap();
159        assert_eq!(k, out);
160        assert_eq!(k.jws_alg(), "ES256");
161    }
162
163    #[test]
164    fn secp256k1_roundtrip_multicodec() {
165        let mut sec1 = vec![0x02; SEC1_COMPRESSED_LEN];
166        sec1[0] = 0x03;
167        let k = DidKeyPubkey::Secp256k1(sec1);
168        let bytes = k.to_multicodec_bytes();
169        let out = DidKeyPubkey::from_multicodec_bytes(&bytes).unwrap();
170        assert_eq!(k, out);
171        assert_eq!(k.jws_alg(), "ES256K");
172    }
173
174    #[test]
175    fn rejects_unknown_codec() {
176        // 0xFFFF unused in the multicodec table as of 2026-04.
177        let payload = vec![0xff, 0xff, 0x03, 1, 2, 3];
178        let err = DidKeyPubkey::from_multicodec_bytes(&payload).unwrap_err();
179        assert!(matches!(err, DidKeyError::UnknownCodec(_)));
180    }
181
182    #[test]
183    fn rejects_wrong_ed25519_length() {
184        let mut buf = [0u8; 10];
185        let varint = unsigned_varint::encode::u64(CODEC_ED25519, &mut buf);
186        let mut payload = varint.to_vec();
187        payload.extend_from_slice(&[0u8; 16]); // wrong length
188        let err = DidKeyPubkey::from_multicodec_bytes(&payload).unwrap_err();
189        assert!(matches!(err, DidKeyError::InvalidKeyLength { .. }));
190    }
191}