Skip to main content

aex_identity/
did_key.rs

1//! `did:key` identity provider.
2//!
3//! A `did:key` identifier encodes an Ed25519 public key directly inside
4//! the DID string — no network call is ever required to resolve it.
5//! The encoding (W3C `did:key` Method Specification, §2.1) is:
6//!
7//! ```text
8//! did:key:z<base58btc(<multicodec-varint-prefix> || <raw-pubkey-bytes>)>
9//! ```
10//!
11//! For Ed25519, the multicodec prefix is `0xed01` (two bytes,
12//! `varint(0xed)` = `[0xed, 0x01]`). The provider currently supports
13//! only Ed25519 (`did:key:z6Mk...`); other key types would be additive
14//! variants.
15//!
16//! # Use case
17//!
18//! `did:key` is the canonical *offline* identity in AEX v2: tests, CI,
19//! device-local agents that intentionally do not publish a card. Per
20//! ADR-0047 it ships in v2.0 GA alongside `did:spize`, `did:web`, and
21//! `did:ethr`.
22
23use std::sync::Arc;
24
25use aex_core::{AgentId, Error, IdScheme, IdentityProvider, Result, Signature, SignatureAlgorithm};
26use async_trait::async_trait;
27use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
28use tokio::sync::RwLock;
29
30/// Multicodec prefix bytes for Ed25519 public keys.
31///
32/// Source: W3C DID Method `did:key` §2.1 + the multicodec table —
33/// `ed25519-pub` is decimal 237 (`0xED`); when encoded as a varint
34/// (single byte under 0x80 would be `0xED`, but the spec uses the
35/// 2-byte form `0xED 0x01`).
36const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
37
38/// Length of a raw Ed25519 public key in bytes.
39const ED25519_PUBKEY_LEN: usize = 32;
40
41/// Provider for `did:key` identities.
42///
43/// Holds the agent's own signing key (so it can `sign()`) and an
44/// in-memory cache of peer Ed25519 keys decoded from `did:key`
45/// strings. Cache exists only to avoid re-running base58 decode on
46/// every verify; cache misses fall back to decoding the input
47/// agent_id on the spot.
48pub struct DidKeyProvider {
49    agent_id: AgentId,
50    signing_key: SigningKey,
51    peers: Arc<RwLock<std::collections::HashMap<AgentId, VerifyingKey>>>,
52}
53
54impl DidKeyProvider {
55    /// Build a provider from an existing Ed25519 signing key. The
56    /// agent_id is derived deterministically from the corresponding
57    /// public key.
58    pub fn from_signing_key(signing_key: SigningKey) -> Result<Self> {
59        let verifying = signing_key.verifying_key();
60        let id_str = encode_did_key(&verifying);
61        let agent_id = AgentId::new(id_str)?;
62        Ok(Self {
63            agent_id,
64            signing_key,
65            peers: Arc::new(RwLock::new(Default::default())),
66        })
67    }
68
69    /// Generate a fresh `did:key` identity from system entropy.
70    pub fn generate() -> Result<Self> {
71        let signing_key = SigningKey::generate(&mut rand::rngs::OsRng);
72        Self::from_signing_key(signing_key)
73    }
74
75    /// Decode a `did:key:z...` string into its Ed25519 verifying key.
76    ///
77    /// Returns an error if the input is not a `did:key`, if the
78    /// multibase prefix is not `z` (base58btc), if the multicodec
79    /// prefix is not Ed25519, or if the key length is wrong.
80    pub fn decode_pubkey(agent_id: &AgentId) -> Result<VerifyingKey> {
81        if agent_id.scheme() != IdScheme::DidKey {
82            return Err(Error::InvalidAgentId(format!(
83                "not a did:key agent_id: {}",
84                agent_id.as_str()
85            )));
86        }
87        let uri = agent_id.as_did_uri().ok_or_else(|| {
88            Error::InvalidAgentId(format!(
89                "did:key id is not a valid DID URI: {}",
90                agent_id.as_str()
91            ))
92        })?;
93        let msi = uri.method_specific_id;
94        // method_specific_id starts with the multibase prefix character;
95        // `z` = base58btc per W3C did:key §2.1.
96        let after_z = msi
97            .strip_prefix('z')
98            .ok_or_else(|| Error::InvalidAgentId("did:key must use base58btc (z prefix)".into()))?;
99
100        let bytes = bs58::decode(after_z)
101            .into_vec()
102            .map_err(|e| Error::InvalidAgentId(format!("base58 decode failed: {}", e)))?;
103
104        if bytes.len() != ED25519_MULTICODEC_PREFIX.len() + ED25519_PUBKEY_LEN {
105            return Err(Error::InvalidAgentId(format!(
106                "did:key length mismatch: got {} bytes, expected {}",
107                bytes.len(),
108                ED25519_MULTICODEC_PREFIX.len() + ED25519_PUBKEY_LEN
109            )));
110        }
111        if bytes[..2] != ED25519_MULTICODEC_PREFIX {
112            return Err(Error::InvalidAgentId(format!(
113                "did:key multicodec prefix mismatch: got {:02x?}, expected {:02x?} (Ed25519)",
114                &bytes[..2],
115                ED25519_MULTICODEC_PREFIX
116            )));
117        }
118        let pubkey_bytes: [u8; ED25519_PUBKEY_LEN] =
119            bytes[2..].try_into().expect("length checked just above");
120        VerifyingKey::from_bytes(&pubkey_bytes)
121            .map_err(|e| Error::InvalidAgentId(format!("invalid Ed25519 public key: {}", e)))
122    }
123
124    /// Register a peer's public key. Useful when the caller has already
125    /// decoded a peer's `did:key` and wants to avoid repeated decoding.
126    pub async fn register_peer(&self, peer_id: AgentId, pubkey: VerifyingKey) {
127        self.peers.write().await.insert(peer_id, pubkey);
128    }
129}
130
131/// Encode an Ed25519 verifying key as a `did:key:z...` string.
132fn encode_did_key(vk: &VerifyingKey) -> String {
133    let mut buf = Vec::with_capacity(ED25519_MULTICODEC_PREFIX.len() + ED25519_PUBKEY_LEN);
134    buf.extend_from_slice(&ED25519_MULTICODEC_PREFIX);
135    buf.extend_from_slice(vk.as_bytes());
136    format!("did:key:z{}", bs58::encode(buf).into_string())
137}
138
139#[async_trait]
140impl IdentityProvider for DidKeyProvider {
141    fn agent_id(&self) -> &AgentId {
142        &self.agent_id
143    }
144
145    async fn sign(&self, message: &[u8]) -> Result<Signature> {
146        let sig = self.signing_key.sign(message);
147        Ok(Signature {
148            algorithm: SignatureAlgorithm::Ed25519,
149            bytes: sig.to_bytes().to_vec(),
150        })
151    }
152
153    async fn verify_peer(
154        &self,
155        peer_id: &AgentId,
156        message: &[u8],
157        signature: &Signature,
158    ) -> Result<()> {
159        if signature.algorithm != SignatureAlgorithm::Ed25519 {
160            return Err(Error::SignatureFormat(format!(
161                "did:key requires Ed25519 signature, got {:?}",
162                signature.algorithm
163            )));
164        }
165
166        // Cached path: peer key already registered.
167        let cached = self.peers.read().await.get(peer_id).copied();
168        let pubkey = match cached {
169            Some(k) => k,
170            None => {
171                // Fallback: decode the agent_id itself. did:key is
172                // self-certifying so we always have the key in-line.
173                let pk = Self::decode_pubkey(peer_id)?;
174                self.peers.write().await.insert(peer_id.clone(), pk);
175                pk
176            }
177        };
178
179        use ed25519_dalek::Verifier;
180        let sig_bytes: [u8; 64] = signature.bytes.as_slice().try_into().map_err(|_| {
181            Error::SignatureFormat(format!(
182                "Ed25519 signature must be 64 bytes, got {}",
183                signature.bytes.len()
184            ))
185        })?;
186        let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
187        pubkey
188            .verify(message, &sig)
189            .map_err(|_| Error::SignatureInvalid)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn generate_yields_did_key_scheme() {
199        let p = DidKeyProvider::generate().unwrap();
200        assert_eq!(p.agent_id().scheme(), IdScheme::DidKey);
201        assert!(p.agent_id().as_str().starts_with("did:key:z"));
202    }
203
204    #[test]
205    fn from_signing_key_deterministic() {
206        let sk1 = SigningKey::from_bytes(&[7u8; 32]);
207        let sk2 = SigningKey::from_bytes(&[7u8; 32]);
208        let p1 = DidKeyProvider::from_signing_key(sk1).unwrap();
209        let p2 = DidKeyProvider::from_signing_key(sk2).unwrap();
210        assert_eq!(p1.agent_id(), p2.agent_id());
211    }
212
213    #[test]
214    fn roundtrip_encode_decode() {
215        let sk = SigningKey::from_bytes(&[42u8; 32]);
216        let original_vk = sk.verifying_key();
217        let p = DidKeyProvider::from_signing_key(sk).unwrap();
218        let decoded = DidKeyProvider::decode_pubkey(p.agent_id()).unwrap();
219        assert_eq!(decoded.as_bytes(), original_vk.as_bytes());
220    }
221
222    #[test]
223    fn reject_non_did_key_id() {
224        let id = AgentId::new("did:web:acme.com#agent").unwrap();
225        let err = DidKeyProvider::decode_pubkey(&id).unwrap_err();
226        assert!(matches!(err, Error::InvalidAgentId(_)));
227    }
228
229    #[test]
230    fn reject_wrong_multibase_prefix() {
231        // 'f' = base16, not the 'z' we require.
232        let id = AgentId::new("did:key:fab12cd").unwrap();
233        let err = DidKeyProvider::decode_pubkey(&id).unwrap_err();
234        assert!(matches!(err, Error::InvalidAgentId(_)));
235    }
236
237    #[test]
238    fn reject_truncated_id() {
239        // 'z' + few base58 chars → too short after decoding.
240        let id = AgentId::new("did:key:zabc").unwrap();
241        let err = DidKeyProvider::decode_pubkey(&id).unwrap_err();
242        assert!(matches!(err, Error::InvalidAgentId(_)));
243    }
244
245    #[test]
246    fn reject_wrong_multicodec_prefix() {
247        // Encode bytes with a NON-Ed25519 multicodec prefix (0x12 = sha2-256).
248        let mut buf: Vec<u8> = vec![0x12, 0x20];
249        buf.extend_from_slice(&[0u8; 32]);
250        let s = format!("did:key:z{}", bs58::encode(buf).into_string());
251        let id = AgentId::new(s).unwrap();
252        let err = DidKeyProvider::decode_pubkey(&id).unwrap_err();
253        assert!(matches!(err, Error::InvalidAgentId(_)));
254    }
255
256    #[tokio::test]
257    async fn sign_and_verify_self() {
258        let p = DidKeyProvider::generate().unwrap();
259        let msg = b"hello did:key";
260        let sig = p.sign(msg).await.unwrap();
261        p.verify_peer(p.agent_id(), msg, &sig).await.unwrap();
262    }
263
264    #[tokio::test]
265    async fn verify_peer_decodes_inline_on_cache_miss() {
266        let alice = DidKeyProvider::generate().unwrap();
267        let bob = DidKeyProvider::generate().unwrap();
268        let msg = b"from bob to alice";
269        let sig = bob.sign(msg).await.unwrap();
270        // Alice has never seen Bob — decode from did:key on the fly.
271        alice
272            .verify_peer(bob.agent_id(), msg, &sig)
273            .await
274            .expect("did:key peer verifies without prior registration");
275    }
276
277    #[tokio::test]
278    async fn rejects_wrong_signature_algorithm() {
279        let p = DidKeyProvider::generate().unwrap();
280        let bogus = Signature {
281            algorithm: SignatureAlgorithm::EcdsaSecp256k1,
282            bytes: vec![0u8; 64],
283        };
284        let err = p.verify_peer(p.agent_id(), b"x", &bogus).await.unwrap_err();
285        assert!(matches!(err, Error::SignatureFormat(_)));
286    }
287
288    #[tokio::test]
289    async fn rejects_tampered_signature() {
290        let p = DidKeyProvider::generate().unwrap();
291        let msg = b"x";
292        let mut sig = p.sign(msg).await.unwrap();
293        sig.bytes[0] ^= 0xff;
294        let err = p.verify_peer(p.agent_id(), msg, &sig).await.unwrap_err();
295        assert!(matches!(err, Error::SignatureInvalid));
296    }
297
298    #[tokio::test]
299    async fn rejects_wrong_signature_length() {
300        let p = DidKeyProvider::generate().unwrap();
301        let short = Signature {
302            algorithm: SignatureAlgorithm::Ed25519,
303            bytes: vec![0u8; 32], // not 64
304        };
305        let err = p.verify_peer(p.agent_id(), b"x", &short).await.unwrap_err();
306        assert!(matches!(err, Error::SignatureFormat(_)));
307    }
308}