1pub fn ed25519_multibase_pubkey(public_key_bytes: &[u8; 32]) -> String {
3 let mut buf = Vec::with_capacity(34);
4 buf.extend_from_slice(&[0xed, 0x01]);
5 buf.extend_from_slice(public_key_bytes);
6 multibase::encode(multibase::Base::Base58Btc, &buf)
7}
8
9const ED25519_PUB_CODEC: [u8; 2] = [0xed, 0x01];
11
12pub fn decode_ed25519_public_key_multibase(mb: &str) -> Result<[u8; 32], DidKeyError> {
16 let (_, raw) = multibase::decode(mb).map_err(|e| DidKeyError::Multibase(e.to_string()))?;
17 let key_bytes = if raw.len() >= 2 && [raw[0], raw[1]] == ED25519_PUB_CODEC {
18 &raw[2..]
19 } else {
20 &raw[..]
21 };
22 key_bytes
23 .try_into()
24 .map_err(|_| DidKeyError::InvalidSeedLength)
25}
26
27const ED25519_PRIV_CODEC: [u8; 2] = [0x80, 0x26]; const X25519_PRIV_CODEC: [u8; 2] = [0x82, 0x26]; const P256_PRIV_CODEC: [u8; 2] = [0x86, 0x26]; pub fn decode_private_key_multibase(mb: &str) -> Result<[u8; 32], DidKeyError> {
41 let (_, raw) = multibase::decode(mb).map_err(|e| DidKeyError::Multibase(e.to_string()))?;
42 let key_bytes = if raw.len() >= 2 {
43 match [raw[0], raw[1]] {
44 ED25519_PRIV_CODEC | X25519_PRIV_CODEC | P256_PRIV_CODEC => &raw[2..],
45 _ => &raw[..],
46 }
47 } else {
48 &raw[..]
49 };
50 key_bytes
51 .try_into()
52 .map_err(|_| DidKeyError::InvalidSeedLength)
53}
54
55#[cfg(feature = "didcomm")]
57pub struct DidKeySecrets {
58 pub signing: affinidi_tdk::secrets_resolver::secrets::Secret,
59 pub key_agreement: affinidi_tdk::secrets_resolver::secrets::Secret,
60}
61
62#[cfg(feature = "didcomm")]
67pub fn secrets_from_did_key(did: &str, seed: &[u8; 32]) -> Result<DidKeySecrets, DidKeyError> {
68 use affinidi_tdk::secrets_resolver::secrets::Secret;
69
70 let ed_pub_mb = did
71 .strip_prefix("did:key:")
72 .ok_or(DidKeyError::InvalidDidKey)?;
73
74 let mut signing = Secret::generate_ed25519(None, Some(seed));
76 signing.id = format!("{did}#{ed_pub_mb}");
77
78 let mut key_agreement = signing
80 .to_x25519()
81 .map_err(|e| DidKeyError::X25519Conversion(e.to_string()))?;
82 let x_pub_mb = key_agreement
83 .get_public_keymultibase()
84 .map_err(|e| DidKeyError::X25519Conversion(e.to_string()))?;
85 key_agreement.id = format!("{did}#{x_pub_mb}");
86
87 Ok(DidKeySecrets {
88 signing,
89 key_agreement,
90 })
91}
92
93#[cfg(feature = "didcomm")]
120pub fn secrets_from_bundle(
121 bundle: &crate::did_secrets::DidSecretsBundle,
122) -> Result<Vec<affinidi_tdk::secrets_resolver::secrets::Secret>, DidKeyError> {
123 use affinidi_tdk::secrets_resolver::secrets::Secret;
124
125 let mut secrets = Vec::with_capacity(bundle.secrets.len());
126 for entry in &bundle.secrets {
127 decode_private_key_multibase(&entry.private_key_multibase)?;
131
132 let secret = Secret::from_multibase(&entry.private_key_multibase, Some(&entry.key_id))
137 .map_err(|e| DidKeyError::SecretConstruction(e.to_string()))?;
138 secrets.push(secret);
139 }
140 Ok(secrets)
141}
142
143#[derive(Debug)]
144pub enum DidKeyError {
145 Multibase(String),
146 InvalidSeedLength,
147 InvalidDidKey,
148 #[cfg(feature = "didcomm")]
149 X25519Conversion(String),
150 #[cfg(feature = "didcomm")]
151 SecretConstruction(String),
152}
153
154impl std::fmt::Display for DidKeyError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 Self::Multibase(e) => write!(f, "invalid private key multibase: {e}"),
158 Self::InvalidSeedLength => write!(f, "private key seed must be 32 bytes"),
159 Self::InvalidDidKey => write!(f, "invalid did:key format"),
160 #[cfg(feature = "didcomm")]
161 Self::X25519Conversion(e) => write!(f, "X25519 conversion failed: {e}"),
162 #[cfg(feature = "didcomm")]
163 Self::SecretConstruction(e) => write!(f, "failed to construct secret from bundle: {e}"),
164 }
165 }
166}
167
168impl std::error::Error for DidKeyError {}
169
170#[cfg(feature = "client")]
177pub fn secret_from_key_response(
178 resp: &crate::client::GetKeySecretResponse,
179) -> Result<affinidi_tdk::secrets_resolver::secrets::Secret, DidKeyError> {
180 affinidi_tdk::secrets_resolver::secrets::Secret::from_multibase(
181 &resp.private_key_multibase,
182 None,
183 )
184 .map_err(|e| DidKeyError::Multibase(e.to_string()))
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_ed25519_multibase_pubkey_format() {
193 let key = [0u8; 32];
194 let result = ed25519_multibase_pubkey(&key);
195 assert!(result.starts_with('z'));
197
198 let (_, decoded) = multibase::decode(&result).unwrap();
199 assert_eq!(decoded.len(), 34);
200 assert_eq!(decoded[0], 0xed);
201 assert_eq!(decoded[1], 0x01);
202 assert_eq!(&decoded[2..], &key);
203 }
204
205 #[test]
206 fn test_decode_private_key_multibase_roundtrip() {
207 let seed = [42u8; 32];
208 let encoded = multibase::encode(multibase::Base::Base58Btc, seed);
209 let decoded = decode_private_key_multibase(&encoded).unwrap();
210 assert_eq!(decoded, seed);
211 }
212
213 #[test]
214 fn test_decode_private_key_multibase_with_codec_prefix() {
215 let seed = [42u8; 32];
216 let mut prefixed = Vec::with_capacity(34);
217 prefixed.extend_from_slice(&ED25519_PRIV_CODEC);
218 prefixed.extend_from_slice(&seed);
219 let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed);
220 let decoded = decode_private_key_multibase(&encoded).unwrap();
221 assert_eq!(decoded, seed);
222 }
223
224 #[test]
225 fn test_decode_private_key_multibase_invalid() {
226 let result = decode_private_key_multibase("!!!bad!!!");
227 assert!(result.is_err());
228 }
229
230 #[test]
231 fn test_decode_private_key_multibase_wrong_length() {
232 let encoded = multibase::encode(multibase::Base::Base58Btc, [1u8; 16]);
233 let result = decode_private_key_multibase(&encoded);
234 assert!(matches!(result, Err(DidKeyError::InvalidSeedLength)));
235 }
236
237 #[cfg(feature = "didcomm")]
248 #[test]
249 fn test_secrets_from_did_key_uses_multibase_fragment_ids() {
250 use affinidi_tdk::secrets_resolver::secrets::Secret;
251
252 let seed = [42u8; 32];
253 let ed_secret = Secret::generate_ed25519(None, Some(&seed));
254 let ed_pub_mb = ed_secret.get_public_keymultibase().unwrap();
255 let did = format!("did:key:{ed_pub_mb}");
256
257 let secrets = secrets_from_did_key(&did, &seed).expect("did:key secrets");
258
259 assert_eq!(secrets.signing.id, format!("{did}#{ed_pub_mb}"));
262 assert_ne!(secrets.signing.id, format!("{did}#key-0"));
263
264 assert!(
268 secrets.key_agreement.id.starts_with(&format!("{did}#z")),
269 "key_agreement.id should start with `{did}#z`, got: {}",
270 secrets.key_agreement.id
271 );
272 assert_ne!(secrets.key_agreement.id, format!("{did}#key-1"));
273 assert_ne!(secrets.key_agreement.id, secrets.signing.id);
274 }
275
276 #[cfg(feature = "didcomm")]
281 #[test]
282 fn test_secrets_from_did_key_is_deterministic() {
283 use affinidi_tdk::secrets_resolver::secrets::Secret;
284
285 let seed = [7u8; 32];
286 let ed_secret = Secret::generate_ed25519(None, Some(&seed));
287 let did = format!("did:key:{}", ed_secret.get_public_keymultibase().unwrap());
288
289 let a = secrets_from_did_key(&did, &seed).unwrap();
290 let b = secrets_from_did_key(&did, &seed).unwrap();
291 assert_eq!(a.signing.id, b.signing.id);
292 assert_eq!(a.key_agreement.id, b.key_agreement.id);
293 }
294
295 #[cfg(feature = "didcomm")]
296 #[test]
297 fn test_secrets_from_did_key_rejects_non_did_key() {
298 let seed = [1u8; 32];
299 let result = secrets_from_did_key("did:webvh:abc:example.com:vta", &seed);
300 assert!(matches!(result, Err(DidKeyError::InvalidDidKey)));
301 }
302
303 #[cfg(feature = "didcomm")]
310 #[test]
311 fn test_secrets_from_bundle_ed25519_and_x25519() {
312 use crate::did_secrets::{DidSecretsBundle, SecretEntry};
313 use crate::keys::KeyType;
314 use affinidi_tdk::secrets_resolver::secrets::{KeyType as ResolverKeyType, Secret};
315
316 let did = "did:webvh:QmAbc:example.com:agent";
317
318 let ed_seed = [11u8; 32];
320 let mut ed_prefixed = Vec::with_capacity(34);
321 ed_prefixed.extend_from_slice(&ED25519_PRIV_CODEC);
322 ed_prefixed.extend_from_slice(&ed_seed);
323 let signing_mb = multibase::encode(multibase::Base::Base58Btc, &ed_prefixed);
324
325 let x_scalar = [22u8; 32];
327 let mut x_prefixed = Vec::with_capacity(34);
328 x_prefixed.extend_from_slice(&X25519_PRIV_CODEC);
329 x_prefixed.extend_from_slice(&x_scalar);
330 let ka_mb = multibase::encode(multibase::Base::Base58Btc, &x_prefixed);
331
332 let bundle = DidSecretsBundle {
333 did: did.to_string(),
334 secrets: vec![
335 SecretEntry {
336 key_id: format!("{did}#key-0"),
337 key_type: KeyType::Ed25519,
338 private_key_multibase: signing_mb.clone(),
339 },
340 SecretEntry {
341 key_id: format!("{did}#key-1"),
342 key_type: KeyType::X25519,
343 private_key_multibase: ka_mb,
344 },
345 ],
346 };
347
348 let secrets = secrets_from_bundle(&bundle).expect("bundle secrets");
349 assert_eq!(secrets.len(), 2, "signing + key-agreement");
350
351 assert_eq!(secrets[0].id, format!("{did}#key-0"));
353 assert_eq!(secrets[1].id, format!("{did}#key-1"));
354
355 assert_eq!(secrets[0].get_key_type(), ResolverKeyType::Ed25519);
357 assert_eq!(secrets[1].get_key_type(), ResolverKeyType::X25519);
358
359 let mut x_prefixed2 = Vec::with_capacity(34);
364 x_prefixed2.extend_from_slice(&X25519_PRIV_CODEC);
365 x_prefixed2.extend_from_slice(&x_scalar);
366 let same = Secret::from_multibase(
367 &multibase::encode(multibase::Base::Base58Btc, &x_prefixed2),
368 None,
369 )
370 .unwrap();
371 assert_eq!(
372 secrets[1].get_public_keymultibase().unwrap(),
373 same.get_public_keymultibase().unwrap(),
374 );
375 }
376
377 #[cfg(feature = "didcomm")]
381 #[test]
382 fn test_secrets_from_bundle_roundtrips_multibase() {
383 use crate::did_secrets::{DidSecretsBundle, SecretEntry};
384 use crate::keys::KeyType;
385
386 let scalar = [7u8; 32];
387 let mut prefixed = Vec::with_capacity(34);
388 prefixed.extend_from_slice(&X25519_PRIV_CODEC);
389 prefixed.extend_from_slice(&scalar);
390 let mb = multibase::encode(multibase::Base::Base58Btc, &prefixed);
391
392 assert_eq!(decode_private_key_multibase(&mb).unwrap(), scalar);
394
395 let bundle = DidSecretsBundle {
396 did: "did:webvh:x:example.com:a".to_string(),
397 secrets: vec![SecretEntry {
398 key_id: "did:webvh:x:example.com:a#key-1".to_string(),
399 key_type: KeyType::X25519,
400 private_key_multibase: mb,
401 }],
402 };
403 let secrets = secrets_from_bundle(&bundle).unwrap();
404 assert_eq!(secrets.len(), 1);
405 assert_eq!(secrets[0].id, "did:webvh:x:example.com:a#key-1");
406 }
407
408 #[cfg(feature = "didcomm")]
411 #[test]
412 fn test_secrets_from_bundle_rejects_bad_multibase() {
413 use crate::did_secrets::{DidSecretsBundle, SecretEntry};
414 use crate::keys::KeyType;
415
416 let bundle = DidSecretsBundle {
417 did: "did:webvh:x:example.com:a".to_string(),
418 secrets: vec![SecretEntry {
419 key_id: "did:webvh:x:example.com:a#key-0".to_string(),
420 key_type: KeyType::Ed25519,
421 private_key_multibase: "!!!bad!!!".to_string(),
422 }],
423 };
424 assert!(matches!(
425 secrets_from_bundle(&bundle),
426 Err(DidKeyError::Multibase(_))
427 ));
428 }
429}