Skip to main content

auths_crypto/
did_key.rs

1//! DID:key encoding and decoding for Ed25519 and P-256 public keys.
2//!
3//! Centralizes all `did:key` ↔ public key byte conversions in one place.
4//! The `did:key` method encodes a public key directly in the DID string
5//! using multicodec + base58btc, per the [did:key spec](https://w3c-ccg.github.io/did-method-key/).
6// allow during curve-agnostic refactor
7#![allow(clippy::disallowed_methods)]
8
9/// Ed25519 multicodec prefix (varint-encoded `0xED`).
10const ED25519_MULTICODEC: [u8; 2] = [0xED, 0x01];
11
12/// P-256 (secp256r1) compressed multicodec prefix (varint-encoded `0x1200`).
13const P256_MULTICODEC: [u8; 2] = [0x80, 0x24];
14
15/// Errors from parsing or encoding `did:key` strings.
16#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum DidKeyError {
19    #[error("DID must start with 'did:key:z', got: {0}")]
20    InvalidPrefix(String),
21
22    #[error("Base58 decoding failed: {0}")]
23    Base58DecodeFailed(String),
24
25    #[error("Unsupported multicodec: expected Ed25519 [0xED, 0x01] or P-256 [0x80, 0x24]")]
26    UnsupportedMulticodec,
27
28    #[error("Invalid key length: got {0} bytes")]
29    InvalidKeyLength(usize),
30}
31
32impl crate::AuthsErrorInfo for DidKeyError {
33    fn error_code(&self) -> &'static str {
34        match self {
35            Self::InvalidPrefix(_) => "AUTHS-E1101",
36            Self::Base58DecodeFailed(_) => "AUTHS-E1102",
37            Self::UnsupportedMulticodec => "AUTHS-E1103",
38            Self::InvalidKeyLength(_) => "AUTHS-E1104",
39        }
40    }
41
42    fn suggestion(&self) -> Option<&'static str> {
43        match self {
44            Self::InvalidPrefix(_) => Some("DID must start with 'did:key:z'"),
45            Self::UnsupportedMulticodec => Some("Supported key types: Ed25519, P-256 (secp256r1)"),
46            _ => None,
47        }
48    }
49}
50
51/// Encode a raw public key as a `did:keri:` string (base58-encoded).
52///
53/// Args:
54/// * `pk`: Raw public key bytes.
55pub fn ed25519_pubkey_to_did_keri(pk: &[u8]) -> String {
56    format!("did:keri:{}", bs58::encode(pk).into_string())
57}
58
59/// Encode a raw public key as a `did:key:z…` string (multicodec-tagged,
60/// base58btc) — the self-describing inverse of [`did_key_decode`]. The curve is
61/// carried in the multicodec prefix, never inferred from length.
62///
63/// Args:
64/// * `curve`: The key's curve.
65/// * `public_key`: 32 raw bytes (Ed25519) or 33 compressed SEC1 bytes (P-256).
66///
67/// Usage:
68/// ```ignore
69/// let did = did_key_encode(CurveType::P256, &compressed_point);
70/// ```
71pub fn did_key_encode(curve: crate::CurveType, public_key: &[u8]) -> String {
72    let prefix = match curve {
73        crate::CurveType::Ed25519 => ED25519_MULTICODEC,
74        crate::CurveType::P256 => P256_MULTICODEC,
75    };
76    let mut bytes = Vec::with_capacity(2 + public_key.len());
77    bytes.extend_from_slice(&prefix);
78    bytes.extend_from_slice(public_key);
79    format!("did:key:z{}", bs58::encode(bytes).into_string())
80}
81
82/// Decode a `did:key:z...` string to a 33-byte compressed P-256 public key.
83///
84/// Args:
85/// * `did`: A DID string in `did:key:z<base58btc>` format with P-256 multicodec.
86///
87/// Usage:
88/// ```ignore
89/// let pk: Vec<u8> = did_key_to_p256("did:key:zDn...")?;
90/// assert_eq!(pk.len(), 33);
91/// ```
92pub fn did_key_to_p256(did: &str) -> Result<Vec<u8>, DidKeyError> {
93    let encoded = strip_did_key_prefix(did)?;
94    let decoded = decode_base58(encoded)?;
95    if decoded.len() < 2 {
96        return Err(DidKeyError::InvalidKeyLength(decoded.len()));
97    }
98    if decoded[0] != P256_MULTICODEC[0] || decoded[1] != P256_MULTICODEC[1] {
99        return Err(DidKeyError::UnsupportedMulticodec);
100    }
101    let key = decoded[2..].to_vec();
102    if key.len() != 33 {
103        return Err(DidKeyError::InvalidKeyLength(key.len()));
104    }
105    Ok(key)
106}
107
108/// Decoded public key from a `did:key` string, with curve identification.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum DecodedDidKey {
111    /// Ed25519 public key (32 bytes).
112    Ed25519([u8; 32]),
113    /// P-256 compressed public key (33 bytes).
114    P256(Vec<u8>),
115}
116
117impl DecodedDidKey {
118    /// Returns the curve of the decoded key.
119    pub fn curve(&self) -> crate::CurveType {
120        match self {
121            Self::Ed25519(_) => crate::CurveType::Ed25519,
122            Self::P256(_) => crate::CurveType::P256,
123        }
124    }
125
126    /// Returns the raw public key bytes.
127    pub fn bytes(&self) -> &[u8] {
128        match self {
129            Self::Ed25519(b) => b,
130            Self::P256(b) => b,
131        }
132    }
133}
134
135/// Decode a `did:key:z...` string, auto-detecting the curve from the multicodec.
136///
137/// Usage:
138/// ```ignore
139/// match did_key_decode("did:key:z...")? {
140///     DecodedDidKey::Ed25519(pk) => { /* 32 bytes */ }
141///     DecodedDidKey::P256(pk) => { /* 33 bytes */ }
142/// }
143/// ```
144pub fn did_key_decode(did: &str) -> Result<DecodedDidKey, DidKeyError> {
145    let encoded = strip_did_key_prefix(did)?;
146    let decoded = decode_base58(encoded)?;
147    if decoded.len() < 2 {
148        return Err(DidKeyError::InvalidKeyLength(decoded.len()));
149    }
150    if decoded[0] == ED25519_MULTICODEC[0] && decoded[1] == ED25519_MULTICODEC[1] {
151        let key = validate_multicodec_and_extract(&decoded)?;
152        Ok(DecodedDidKey::Ed25519(key))
153    } else if decoded[0] == P256_MULTICODEC[0] && decoded[1] == P256_MULTICODEC[1] {
154        let key = decoded[2..].to_vec();
155        if key.len() != 33 {
156            return Err(DidKeyError::InvalidKeyLength(key.len()));
157        }
158        Ok(DecodedDidKey::P256(key))
159    } else {
160        Err(DidKeyError::UnsupportedMulticodec)
161    }
162}
163
164fn strip_did_key_prefix(did: &str) -> Result<&str, DidKeyError> {
165    did.strip_prefix("did:key:z")
166        .ok_or_else(|| DidKeyError::InvalidPrefix(did.to_string()))
167}
168
169fn decode_base58(encoded: &str) -> Result<Vec<u8>, DidKeyError> {
170    bs58::decode(encoded)
171        .into_vec()
172        .map_err(|e| DidKeyError::Base58DecodeFailed(e.to_string()))
173}
174
175fn validate_multicodec_and_extract(decoded: &[u8]) -> Result<[u8; 32], DidKeyError> {
176    if decoded.len() != 34
177        || decoded[0] != ED25519_MULTICODEC[0]
178        || decoded[1] != ED25519_MULTICODEC[1]
179    {
180        if decoded.len() != 34 {
181            return Err(DidKeyError::InvalidKeyLength(
182                decoded.len().saturating_sub(2),
183            ));
184        }
185        return Err(DidKeyError::UnsupportedMulticodec);
186    }
187
188    let mut key = [0u8; 32];
189    key.copy_from_slice(&decoded[2..]);
190    Ok(key)
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn rejects_invalid_prefix() {
199        let err = did_key_decode("did:web:example.com").unwrap_err();
200        assert!(matches!(err, DidKeyError::InvalidPrefix(_)));
201    }
202
203    #[test]
204    fn rejects_invalid_base58() {
205        let err = did_key_decode("did:key:z0OOO").unwrap_err();
206        assert!(matches!(err, DidKeyError::Base58DecodeFailed(_)));
207    }
208}