Skip to main content

aph_core/crypto/
did_key.rs

1//! `did:key` — self-describing notary identifiers.
2//!
3//! `did:key` is the first and preferred key-discovery mechanism (spec §8.4.6)
4//! because it needs no network at all: the public key IS the identifier. A
5//! verifier that receives `issuer: "did:key:z6Mk..."` can decode the key from
6//! that string and check the signature entirely offline.
7//!
8//! The identifier is a multicodec-prefixed public key in multibase base58btc
9//! (spec §8.4.3). The prefixes APH uses are `0xed01` for Ed25519 and `0x1200`
10//! for P-256, both varint-encoded — `0x1200` becomes the two bytes
11//! `0x80 0x24`, which is why the constants below are not simply the codec
12//! numbers written out.
13
14/// Multicodec varint prefix for an Ed25519 public key (`0xed01`).
15const ED25519_MULTICODEC: [u8; 2] = [0xed, 0x01];
16/// Multicodec varint prefix for a P-256 public key (`0x1200`).
17const P256_MULTICODEC: [u8; 2] = [0x80, 0x24];
18
19/// The `did:key:` URI scheme prefix.
20const DID_KEY_PREFIX: &str = "did:key:";
21
22/// A public key decoded from a `did:key` identifier.
23#[derive(std::fmt::Debug, std::clone::Clone, std::cmp::PartialEq, std::cmp::Eq)]
24pub enum DecodedDidKey {
25  /// Ed25519 verifying key — the `eddsa-jcs-2022` cryptosuite.
26  Ed25519(std::boxed::Box<ed25519_dalek::VerifyingKey>),
27  /// Compressed P-256 point — the `ecdsa-jcs-2019` cryptosuite. Returned as
28  /// raw bytes so callers that do not need ES256 pay nothing to parse it.
29  P256(std::vec::Vec<u8>),
30}
31
32/// Builds the `did:key` identifier for an Ed25519 public key.
33///
34/// This is how a notary derives the DID it publishes as `issuer`, so it must
35/// agree exactly with what verifiers decode.
36pub fn encode_ed25519(key: &ed25519_dalek::VerifyingKey) -> String {
37  let mut payload = std::vec::Vec::with_capacity(2 + 32);
38  payload.extend_from_slice(&ED25519_MULTICODEC);
39  payload.extend_from_slice(key.as_bytes());
40  std::format!("{}{}", DID_KEY_PREFIX, super::multibase::base58btc_encode(&payload))
41}
42
43/// Decodes a `did:key` identifier into the public key it names.
44///
45/// Every failure path returns `APH_E001`: a verifier that cannot decode the
46/// issuer's key must refuse the envelope, never fall through to accepting it.
47pub fn decode(did: &str) -> std::result::Result<DecodedDidKey, crate::errors::AphError> {
48  let encoded = match did.strip_prefix(DID_KEY_PREFIX) {
49    std::option::Option::Some(rest) => rest,
50    std::option::Option::None => {
51      return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
52    }
53  };
54  decode_multibase_key(encoded)
55}
56
57/// Decodes a multicodec-prefixed public key in multibase base58btc.
58///
59/// This is the payload half of a `did:key` identifier, and is also exactly
60/// what a DID Document's `publicKeyMultibase` field carries — so `did:web`
61/// discovery reuses this decoder rather than reimplementing it.
62pub fn decode_multibase_key(
63  encoded: &str,
64) -> std::result::Result<DecodedDidKey, crate::errors::AphError> {
65  let bytes = super::multibase::base58btc_decode(encoded)?;
66  if bytes.len() < 3 {
67    return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
68  }
69  let (prefix, key_bytes) = bytes.split_at(2);
70
71  if prefix == ED25519_MULTICODEC {
72    let arr: [u8; 32] = match key_bytes.try_into() {
73      std::result::Result::Ok(a) => a,
74      // Wrong length for the declared codec: the identifier is malformed.
75      std::result::Result::Err(_) => {
76        return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
77      }
78    };
79    return match ed25519_dalek::VerifyingKey::from_bytes(&arr) {
80      std::result::Result::Ok(k) => {
81        std::result::Result::Ok(DecodedDidKey::Ed25519(std::boxed::Box::new(k)))
82      }
83      // Well-formed length but not a valid curve point.
84      std::result::Result::Err(_) => {
85        std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature)
86      }
87    };
88  }
89
90  if prefix == P256_MULTICODEC {
91    // Compressed SEC1 point: 33 bytes with a 0x02/0x03 parity byte.
92    if key_bytes.len() != 33 {
93      return std::result::Result::Err(crate::errors::AphError::InvalidEnvelopeSignature);
94    }
95    return std::result::Result::Ok(DecodedDidKey::P256(key_bytes.to_vec()));
96  }
97
98  // A codec APH does not define. Refuse rather than guess at the algorithm.
99  std::result::Result::Err(crate::errors::AphError::unsupported_algorithm(std::format!(
100    "did:key multicodec 0x{:02x}{:02x}",
101    prefix[0],
102    prefix[1]
103  )))
104}
105
106#[cfg(test)]
107mod tests {
108  fn sample_key() -> ed25519_dalek::VerifyingKey {
109    let sk = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
110    sk.verifying_key()
111  }
112
113  #[test]
114  fn encode_decode_round_trip() {
115    // A notary derives its issuer DID from its key; a verifier reverses that
116    // derivation. If the two disagree, no self-issued envelope verifies.
117    let key = sample_key();
118    let did = super::encode_ed25519(&key);
119    match super::decode(&did).unwrap() {
120      super::DecodedDidKey::Ed25519(decoded) => std::assert_eq!(*decoded, key),
121      other => std::panic!("expected Ed25519, got {:?}", other),
122    }
123  }
124
125  #[test]
126  fn ed25519_dids_use_the_z6mk_prefix() {
127    // The 0xed01 multicodec plus base58btc always yields "z6Mk..." for a
128    // 32-byte key. Pinning it catches a wrong multicodec prefix, which would
129    // otherwise produce identifiers no other implementation can resolve.
130    let did = super::encode_ed25519(&sample_key());
131    std::assert!(did.starts_with("did:key:z6Mk"), "got {}", did);
132  }
133
134  #[test]
135  fn published_example_issuer_decodes() {
136    // The issuer DID from the repository's published example envelopes must
137    // decode with this implementation — otherwise the examples we hand to
138    // implementers cannot be verified by our own reference code.
139    let did = "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV";
140    std::assert!(matches!(
141      super::decode(did).unwrap(),
142      super::DecodedDidKey::Ed25519(_)
143    ));
144  }
145
146  #[test]
147  fn non_did_key_input_is_rejected() {
148    // did:web and DNS discovery are different mechanisms with their own
149    // resolvers; this decoder must not silently accept their identifiers.
150    std::assert!(super::decode("did:web:notary.example").is_err());
151    std::assert!(super::decode("z6MkfAkf").is_err());
152  }
153
154  #[test]
155  fn unknown_multicodec_reports_unsupported_algorithm() {
156    // A key type APH does not define must surface as APH_E010 rather than a
157    // generic signature failure, so an operator can tell the two apart.
158    let payload = super::super::multibase::base58btc_encode(&[0x99u8, 0x01, 1, 2, 3]);
159    let err = super::decode(&std::format!("did:key:{}", payload)).unwrap_err();
160    std::assert_eq!(err.code(), "APH_E010");
161  }
162
163  #[test]
164  fn truncated_key_is_rejected_not_panicking() {
165    // Attacker-controlled input: a correct multicodec with a short key must
166    // error rather than panic on the fixed-size array conversion.
167    let payload = super::super::multibase::base58btc_encode(&[0xed, 0x01, 1, 2, 3]);
168    std::assert!(super::decode(&std::format!("did:key:{}", payload)).is_err());
169  }
170}