Skip to main content

auths_crypto/
did_key.rs

1//! DID:key encoding and decoding for Ed25519 public keys.
2//!
3//! Centralizes all `did:key` ↔ Ed25519 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
7/// Ed25519 multicodec prefix (varint-encoded `0xED`).
8const ED25519_MULTICODEC: [u8; 2] = [0xED, 0x01];
9
10/// Errors from parsing or encoding `did:key` strings.
11#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum DidKeyError {
14    #[error("DID must start with 'did:key:z', got: {0}")]
15    InvalidPrefix(String),
16
17    #[error("Base58 decoding failed: {0}")]
18    Base58DecodeFailed(String),
19
20    #[error("Unsupported or malformed multicodec: expected Ed25519 [0xED, 0x01]")]
21    UnsupportedMulticodec,
22
23    #[error("Invalid Ed25519 key length: expected 32 bytes, got {0}")]
24    InvalidKeyLength(usize),
25}
26
27impl crate::AuthsErrorInfo for DidKeyError {
28    fn error_code(&self) -> &'static str {
29        match self {
30            Self::InvalidPrefix(_) => "AUTHS-E1101",
31            Self::Base58DecodeFailed(_) => "AUTHS-E1102",
32            Self::UnsupportedMulticodec => "AUTHS-E1103",
33            Self::InvalidKeyLength(_) => "AUTHS-E1104",
34        }
35    }
36
37    fn suggestion(&self) -> Option<&'static str> {
38        match self {
39            Self::InvalidPrefix(_) => Some("DID must start with 'did:key:z'"),
40            Self::UnsupportedMulticodec => Some("Only Ed25519 keys are supported"),
41            _ => None,
42        }
43    }
44}
45
46/// Decode a `did:key:z...` string into a 32-byte Ed25519 public key.
47///
48/// Args:
49/// * `did`: A DID string in `did:key:z<base58btc>` format.
50///
51/// Usage:
52/// ```ignore
53/// let pk: [u8; 32] = did_key_to_ed25519("did:key:z6Mkf...")?;
54/// ```
55pub fn did_key_to_ed25519(did: &str) -> Result<[u8; 32], DidKeyError> {
56    let encoded = strip_did_key_prefix(did)?;
57    let decoded = decode_base58(encoded)?;
58    validate_multicodec_and_extract(&decoded)
59}
60
61/// Encode a 32-byte Ed25519 public key as a `did:key:z...` string.
62///
63/// Args:
64/// * `public_key`: A 32-byte Ed25519 public key.
65///
66/// Usage:
67/// ```ignore
68/// let did = ed25519_pubkey_to_did_key(&key_bytes);
69/// assert!(did.starts_with("did:key:z"));
70/// ```
71pub fn ed25519_pubkey_to_did_key(public_key: &[u8; 32]) -> String {
72    let mut prefixed = vec![0xED, 0x01];
73    prefixed.extend_from_slice(public_key);
74    let encoded = bs58::encode(prefixed).into_string();
75    format!("did:key:z{encoded}")
76}
77
78/// Encode a raw public key as a `did:keri:` string (base58-encoded).
79///
80/// Args:
81/// * `pk`: Raw public key bytes.
82pub fn ed25519_pubkey_to_did_keri(pk: &[u8]) -> String {
83    format!("did:keri:{}", bs58::encode(pk).into_string())
84}
85
86fn strip_did_key_prefix(did: &str) -> Result<&str, DidKeyError> {
87    did.strip_prefix("did:key:z")
88        .ok_or_else(|| DidKeyError::InvalidPrefix(did.to_string()))
89}
90
91fn decode_base58(encoded: &str) -> Result<Vec<u8>, DidKeyError> {
92    bs58::decode(encoded)
93        .into_vec()
94        .map_err(|e| DidKeyError::Base58DecodeFailed(e.to_string()))
95}
96
97fn validate_multicodec_and_extract(decoded: &[u8]) -> Result<[u8; 32], DidKeyError> {
98    if decoded.len() != 34
99        || decoded[0] != ED25519_MULTICODEC[0]
100        || decoded[1] != ED25519_MULTICODEC[1]
101    {
102        if decoded.len() != 34 {
103            return Err(DidKeyError::InvalidKeyLength(
104                decoded.len().saturating_sub(2),
105            ));
106        }
107        return Err(DidKeyError::UnsupportedMulticodec);
108    }
109
110    let mut key = [0u8; 32];
111    key.copy_from_slice(&decoded[2..]);
112    Ok(key)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn roundtrip_encode_decode() {
121        let original = [42u8; 32];
122        let did = ed25519_pubkey_to_did_key(&original);
123        assert!(did.starts_with("did:key:z"));
124        let decoded = did_key_to_ed25519(&did).unwrap();
125        assert_eq!(decoded, original);
126    }
127
128    #[test]
129    fn rejects_invalid_prefix() {
130        let err = did_key_to_ed25519("did:web:example.com").unwrap_err();
131        assert!(matches!(err, DidKeyError::InvalidPrefix(_)));
132    }
133
134    #[test]
135    fn rejects_invalid_base58() {
136        let err = did_key_to_ed25519("did:key:z0OOO").unwrap_err();
137        assert!(matches!(err, DidKeyError::Base58DecodeFailed(_)));
138    }
139}