1use blake3::Hasher;
2use ed25519_dalek::{Verifier, VerifyingKey};
3use serde::{Deserialize, Serialize};
4
5use crate::error::A1Error;
6use crate::identity::Signer;
7
8const DOMAIN_VC_SIGN: &str = "a1::dyolo::vc::sign::v2.8.0";
9const DID_METHOD: &str = "a1";
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct AgentDid(String);
37
38impl AgentDid {
39 pub fn from_key(vk: &VerifyingKey) -> Self {
41 Self(format!("did:{}:{}", DID_METHOD, hex::encode(vk.as_bytes())))
42 }
43
44 pub fn parse(did: &str) -> Result<Self, A1Error> {
46 let mut parts = did.splitn(3, ':');
47 let scheme = parts.next().unwrap_or("");
48 let method = parts.next().unwrap_or("");
49 let id = parts.next().unwrap_or("");
50
51 if scheme != "did" || method != DID_METHOD || id.is_empty() {
52 return Err(A1Error::WireFormatError(format!(
53 "expected did:a1:<hex>, got: {did}"
54 )));
55 }
56 let bytes = hex::decode(id)
57 .map_err(|_| A1Error::WireFormatError("DID identifier must be hex".into()))?;
58 if bytes.len() != 32 {
59 return Err(A1Error::WireFormatError(
60 "DID identifier must be 32 bytes (Ed25519 key)".into(),
61 ));
62 }
63 Ok(Self(did.to_owned()))
64 }
65
66 pub fn verifying_key(&self) -> Result<VerifyingKey, A1Error> {
68 let hex_part = self.0.splitn(3, ':').nth(2).unwrap_or("");
69 let bytes = hex::decode(hex_part)
70 .map_err(|_| A1Error::WireFormatError("invalid DID hex".into()))?;
71 let arr: [u8; 32] = bytes
72 .try_into()
73 .map_err(|_| A1Error::WireFormatError("DID key must be 32 bytes".into()))?;
74 VerifyingKey::from_bytes(&arr)
75 .map_err(|e| A1Error::WireFormatError(format!("invalid DID key: {e}")))
76 }
77
78 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81
82 pub fn key_id(&self) -> String {
84 format!("{}#key-0", self.0)
85 }
86}
87
88impl std::fmt::Display for AgentDid {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.write_str(&self.0)
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct DidDocument {
112 #[serde(rename = "@context")]
113 pub context: Vec<String>,
114 pub id: String,
115 #[serde(rename = "verificationMethod")]
116 pub verification_method: Vec<VerificationMethod>,
117 pub authentication: Vec<String>,
118 #[serde(rename = "assertionMethod")]
119 pub assertion_method: Vec<String>,
120 #[serde(rename = "capabilityDelegation")]
121 pub capability_delegation: Vec<String>,
122 #[serde(
123 rename = "a1PassportNamespace",
124 skip_serializing_if = "Option::is_none"
125 )]
126 pub passport_namespace: Option<String>,
127 #[serde(
128 rename = "a1CapabilityMaskHex",
129 skip_serializing_if = "Option::is_none"
130 )]
131 pub capability_mask_hex: Option<String>,
132 #[serde(rename = "a1Version")]
133 pub a1_version: String,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct VerificationMethod {
138 pub id: String,
139 #[serde(rename = "type")]
140 pub method_type: String,
141 pub controller: String,
142 #[serde(rename = "publicKeyHex")]
143 pub public_key_hex: String,
144}
145
146impl DidDocument {
147 pub fn for_identity(vk: &VerifyingKey) -> Self {
149 let did = AgentDid::from_key(vk);
150 let key_id = did.key_id();
151 Self {
152 context: vec![
153 "https://www.w3.org/ns/did/v1".into(),
154 "https://w3id.org/security/suites/ed25519-2020/v1".into(),
155 "https://a1.dev/contexts/v1".into(),
156 ],
157 id: did.to_string(),
158 verification_method: vec![VerificationMethod {
159 id: key_id.clone(),
160 method_type: "Ed25519VerificationKey2020".into(),
161 controller: did.to_string(),
162 public_key_hex: hex::encode(vk.as_bytes()),
163 }],
164 authentication: vec![key_id.clone()],
165 assertion_method: vec![key_id.clone()],
166 capability_delegation: vec![key_id],
167 passport_namespace: None,
168 capability_mask_hex: None,
169 a1_version: "2.8.0".into(),
170 }
171 }
172
173 pub fn with_passport_metadata(
178 mut self,
179 namespace: impl Into<String>,
180 mask_hex: impl Into<String>,
181 ) -> Self {
182 self.passport_namespace = Some(namespace.into());
183 self.capability_mask_hex = Some(mask_hex.into());
184 self
185 }
186
187 pub fn to_json(&self) -> Result<String, A1Error> {
189 serde_json::to_string_pretty(self).map_err(|e| A1Error::WireFormatError(e.to_string()))
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct VerifiableCredential {
228 #[serde(rename = "@context")]
229 pub context: Vec<String>,
230 #[serde(rename = "type")]
231 pub vc_type: Vec<String>,
232 pub id: String,
233 pub issuer: String,
234 #[serde(rename = "issuanceDate")]
235 pub issuance_date: String,
236 #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
237 pub expiration_date: Option<String>,
238 #[serde(rename = "credentialSubject")]
239 pub credential_subject: CredentialSubject,
240 pub proof: VcProof,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct CredentialSubject {
245 pub id: String,
246 #[serde(rename = "a1PassportNamespace")]
247 pub passport_namespace: String,
248 #[serde(rename = "a1Capabilities")]
249 pub capabilities: Vec<String>,
250 #[serde(rename = "a1ChainFingerprint")]
251 pub chain_fingerprint: String,
252 #[serde(rename = "a1Version")]
253 pub a1_version: String,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct VcProof {
258 #[serde(rename = "type")]
259 pub proof_type: String,
260 pub created: String,
261 #[serde(rename = "verificationMethod")]
262 pub verification_method: String,
263 #[serde(rename = "proofPurpose")]
264 pub proof_purpose: String,
265 #[serde(rename = "proofValue")]
266 pub proof_value: String,
267}
268
269impl VerifiableCredential {
270 pub fn issue_capability(
279 issuer: &dyn Signer,
280 subject_did: &AgentDid,
281 passport_namespace: &str,
282 capabilities: &[&str],
283 issued_at_unix: u64,
284 expiry_unix: u64,
285 chain_fingerprint: &[u8; 32],
286 ) -> Result<Self, A1Error> {
287 let issuer_vk = issuer.verifying_key();
288 let issuer_did = AgentDid::from_key(&issuer_vk);
289
290 let cred_id = format!("urn:a1:cred:{}", hex::encode(&chain_fingerprint[..16]));
291 let issuance = unix_to_iso8601(issued_at_unix);
292 let expiry = unix_to_iso8601(expiry_unix);
293
294 let subject = CredentialSubject {
295 id: subject_did.to_string(),
296 passport_namespace: passport_namespace.to_owned(),
297 capabilities: capabilities.iter().map(|s| s.to_string()).collect(),
298 chain_fingerprint: hex::encode(chain_fingerprint),
299 a1_version: "2.8.0".into(),
300 };
301
302 let signable =
303 vc_signable_bytes(&cred_id, issuer_did.as_str(), &issuance, &expiry, &subject);
304 let sig = issuer.sign_message(&signable);
305
306 Ok(Self {
307 context: vec![
308 "https://www.w3.org/2018/credentials/v1".into(),
309 "https://a1.dev/contexts/v1".into(),
310 ],
311 vc_type: vec![
312 "VerifiableCredential".into(),
313 "A1CapabilityCredential".into(),
314 ],
315 id: cred_id.clone(),
316 issuer: issuer_did.to_string(),
317 issuance_date: issuance.clone(),
318 expiration_date: Some(expiry.clone()),
319 credential_subject: subject,
320 proof: VcProof {
321 proof_type: "Ed25519Signature2020".into(),
322 created: issuance,
323 verification_method: issuer_did.key_id(),
324 proof_purpose: "assertionMethod".into(),
325 proof_value: hex::encode(sig.to_bytes()),
326 },
327 })
328 }
329
330 pub fn verify(&self) -> Result<(), A1Error> {
335 let issuer_did = AgentDid::parse(&self.issuer)?;
336 let vk = issuer_did.verifying_key()?;
337
338 let expiry = self.expiration_date.as_deref().unwrap_or("");
339 let signable = vc_signable_bytes(
340 &self.id,
341 &self.issuer,
342 &self.issuance_date,
343 expiry,
344 &self.credential_subject,
345 );
346
347 let sig_bytes = hex::decode(&self.proof.proof_value)
348 .map_err(|_| A1Error::WireFormatError("invalid proof_value hex".into()))?;
349 let sig_arr: [u8; 64] = sig_bytes
350 .try_into()
351 .map_err(|_| A1Error::WireFormatError("signature must be 64 bytes".into()))?;
352 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
353
354 vk.verify(&signable, &sig)
355 .map_err(|_| A1Error::HybridSignatureInvalid {
356 component: "vc-ed25519",
357 })
358 }
359
360 pub fn to_json(&self) -> Result<String, A1Error> {
362 serde_json::to_string_pretty(self).map_err(|e| A1Error::WireFormatError(e.to_string()))
363 }
364}
365
366fn vc_signable_bytes(
369 id: &str,
370 issuer: &str,
371 issuance: &str,
372 expiry: &str,
373 subject: &CredentialSubject,
374) -> Vec<u8> {
375 let mut h = Hasher::new_derive_key(DOMAIN_VC_SIGN);
376 h.update(&(id.len() as u64).to_le_bytes());
377 h.update(id.as_bytes());
378 h.update(&(issuer.len() as u64).to_le_bytes());
379 h.update(issuer.as_bytes());
380 h.update(&(issuance.len() as u64).to_le_bytes());
381 h.update(issuance.as_bytes());
382 h.update(&(expiry.len() as u64).to_le_bytes());
383 h.update(expiry.as_bytes());
384 h.update(&(subject.passport_namespace.len() as u64).to_le_bytes());
385 h.update(subject.passport_namespace.as_bytes());
386 h.update(&(subject.capabilities.len() as u64).to_le_bytes());
387 for cap in &subject.capabilities {
388 h.update(&(cap.len() as u64).to_le_bytes());
389 h.update(cap.as_bytes());
390 }
391 h.update(subject.chain_fingerprint.as_bytes());
392 h.finalize().as_bytes().to_vec()
393}
394
395fn unix_to_iso8601(unix: u64) -> String {
398 let s = unix % 60;
399 let m = (unix / 60) % 60;
400 let h = (unix / 3600) % 24;
401 let days = unix / 86400;
402 let (year, month, day) = days_to_ymd(days);
403 format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}Z")
404}
405
406fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
407 let mut year = 1970u64;
408 loop {
409 let y_days = if is_leap(year) { 366 } else { 365 };
410 if days < y_days {
411 break;
412 }
413 days -= y_days;
414 year += 1;
415 }
416 let month_days: [u64; 12] = if is_leap(year) {
417 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
418 } else {
419 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
420 };
421 let mut month = 1u64;
422 for mlen in month_days {
423 if days < mlen {
424 break;
425 }
426 days -= mlen;
427 month += 1;
428 }
429 (year, month, days + 1)
430}
431
432fn is_leap(y: u64) -> bool {
433 (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
434}
435
436#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::identity::DyoloIdentity;
442
443 #[test]
444 fn did_roundtrip() {
445 let id = DyoloIdentity::generate();
446 let did = AgentDid::from_key(&id.verifying_key());
447 assert!(did.as_str().starts_with("did:a1:"));
448 let recovered = did.verifying_key().unwrap();
449 assert_eq!(id.verifying_key().as_bytes(), recovered.as_bytes());
450 }
451
452 #[test]
453 fn did_parse_rejects_malformed() {
454 assert!(AgentDid::parse("did:key:abc").is_err());
455 assert!(AgentDid::parse("did:a1:notvalidhex!").is_err());
456 assert!(AgentDid::parse("notadid").is_err());
457 assert!(AgentDid::parse("did:a1:deadbeef").is_err()); }
459
460 #[test]
461 fn did_document_structure() {
462 let id = DyoloIdentity::generate();
463 let doc = DidDocument::for_identity(&id.verifying_key());
464 assert!(doc.id.starts_with("did:a1:"));
465 assert_eq!(doc.verification_method.len(), 1);
466 assert_eq!(
467 doc.verification_method[0].method_type,
468 "Ed25519VerificationKey2020"
469 );
470 assert_eq!(doc.a1_version, "2.8.0");
471 assert!(doc.passport_namespace.is_none());
472 }
473
474 #[test]
475 fn did_document_with_passport_metadata() {
476 let id = DyoloIdentity::generate();
477 let doc = DidDocument::for_identity(&id.verifying_key())
478 .with_passport_metadata("acme-bot", "ff00ff00");
479 assert_eq!(doc.passport_namespace.as_deref(), Some("acme-bot"));
480 assert_eq!(doc.capability_mask_hex.as_deref(), Some("ff00ff00"));
481 }
482
483 #[test]
484 fn vc_issue_and_verify() {
485 let issuer = DyoloIdentity::generate();
486 let agent = DyoloIdentity::generate();
487 let agent_did = AgentDid::from_key(&agent.verifying_key());
488 let fp = [7u8; 32];
489 let now = 1_700_000_000u64;
490
491 let vc = VerifiableCredential::issue_capability(
492 &issuer,
493 &agent_did,
494 "acme-trading-bot",
495 &["trade.equity", "portfolio.read"],
496 now,
497 now + 86400,
498 &fp,
499 )
500 .unwrap();
501
502 assert!(vc.verify().is_ok());
503 assert_eq!(vc.vc_type[1], "A1CapabilityCredential");
504 assert_eq!(
505 vc.credential_subject.capabilities,
506 ["trade.equity", "portfolio.read"]
507 );
508 }
509
510 #[test]
511 fn tampered_capabilities_fail_verify() {
512 let issuer = DyoloIdentity::generate();
513 let agent = DyoloIdentity::generate();
514 let agent_did = AgentDid::from_key(&agent.verifying_key());
515 let fp = [1u8; 32];
516 let now = 1_700_000_000u64;
517
518 let mut vc = VerifiableCredential::issue_capability(
519 &issuer,
520 &agent_did,
521 "acme-trading-bot",
522 &["trade.equity"],
523 now,
524 now + 86400,
525 &fp,
526 )
527 .unwrap();
528
529 vc.credential_subject
530 .capabilities
531 .push("admin.everything".into());
532 assert!(vc.verify().is_err());
533 }
534
535 #[test]
536 fn tampered_proof_fails_verify() {
537 let issuer = DyoloIdentity::generate();
538 let agent = DyoloIdentity::generate();
539 let agent_did = AgentDid::from_key(&agent.verifying_key());
540 let fp = [2u8; 32];
541 let now = 1_700_000_000u64;
542
543 let mut vc = VerifiableCredential::issue_capability(
544 &issuer,
545 &agent_did,
546 "acme-bot",
547 &["read"],
548 now,
549 now + 3600,
550 &fp,
551 )
552 .unwrap();
553
554 let mut bad = vc.proof.proof_value.clone().into_bytes();
555 bad[0] ^= 0xFF;
556 vc.proof.proof_value = String::from_utf8(bad).unwrap_or_default();
557 assert!(vc.verify().is_err());
558 }
559
560 #[test]
561 fn iso8601_epoch() {
562 assert_eq!(unix_to_iso8601(0), "1970-01-01T00:00:00Z");
563 }
564
565 #[test]
566 fn iso8601_known_date() {
567 let s = unix_to_iso8601(1_700_000_000);
568 assert!(s.starts_with("2023-"));
569 }
570}