Skip to main content

attestix/
didkey.rs

1//! `did:key` (Ed25519) decoding, matching `attestix/auth/crypto.py`.
2//!
3//! `did:key:z` + base58btc(`0xed 0x01` ‖ 32-byte raw Ed25519 public key).
4//! The `0xed01` prefix is the unsigned-varint multicodec code for `ed25519-pub`.
5
6use crate::error::VerifyError;
7
8/// Multicodec prefix for an Ed25519 public key (`ed25519-pub`).
9pub const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
10
11/// Decode a full `did:key:z…` string into the raw 32-byte Ed25519 public key.
12///
13/// Strips the `did:key:z` prefix, base58btc-decodes, asserts the first two bytes
14/// are `0xed 0x01`, and returns the remaining 32 bytes.
15pub fn decode_did_key(did: &str) -> Result<[u8; 32], VerifyError> {
16    let multibase = did
17        .strip_prefix("did:key:")
18        .ok_or(VerifyError::DidKey("missing 'did:key:' prefix"))?;
19    decode_multibase(multibase)
20}
21
22/// Decode a `z…` multibase body (the part after `did:key:`) into the raw key.
23pub fn decode_multibase(multibase: &str) -> Result<[u8; 32], VerifyError> {
24    let b58 = multibase
25        .strip_prefix('z')
26        .ok_or(VerifyError::DidKey("missing 'z' multibase prefix"))?;
27    let decoded = bs58::decode(b58)
28        .into_vec()
29        .map_err(|_| VerifyError::DidKey("invalid base58btc"))?;
30    if decoded.len() != 2 + 32 {
31        return Err(VerifyError::DidKey("unexpected multicodec length"));
32    }
33    if decoded[0..2] != ED25519_MULTICODEC_PREFIX {
34        return Err(VerifyError::DidKey("not an ed25519-pub multicodec (expected 0xed01)"));
35    }
36    let mut key = [0u8; 32];
37    key.copy_from_slice(&decoded[2..]);
38    Ok(key)
39}
40
41/// Encode a raw 32-byte Ed25519 public key as the `z…` multibase body.
42pub fn encode_multibase(raw: &[u8; 32]) -> String {
43    let mut buf = Vec::with_capacity(2 + 32);
44    buf.extend_from_slice(&ED25519_MULTICODEC_PREFIX);
45    buf.extend_from_slice(raw);
46    format!("z{}", bs58::encode(buf).into_string())
47}
48
49/// Encode a raw 32-byte Ed25519 public key as a full `did:key:z…` string.
50pub fn encode_did_key(raw: &[u8; 32]) -> String {
51    format!("did:key:{}", encode_multibase(raw))
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    const DID: &str = "did:key:z6Mko5TBPGKHkCxSgmf3aC6p6SGj2auwCfRmBydXJFEwL4ev";
59    const RAW_HEX: &str = "8022fe847be6106443a4030d74d390c8d9a91319b9f51526bc7c3d88a27c9b7b";
60
61    #[test]
62    fn decode_roundtrip() {
63        let key = decode_did_key(DID).unwrap();
64        assert_eq!(hex::encode(key), RAW_HEX);
65        assert_eq!(encode_did_key(&key), DID);
66    }
67
68    #[test]
69    fn rejects_non_ed25519() {
70        // 0x12 0x00 is not the ed25519 multicodec.
71        let mut bad = vec![0x12u8, 0x00];
72        bad.extend_from_slice(&[0u8; 32]);
73        let mb = format!("z{}", bs58::encode(bad).into_string());
74        assert!(decode_multibase(&mb).is_err());
75    }
76}