cellstate_core/
credentials.rs

1//! Verifiable Credentials for Agent Identity
2//!
3//! Implements a subset of the W3C Verifiable Credentials Data Model v2.0
4//! for establishing cryptographic proof of agent identity and capabilities.
5//!
6//! ## Identity Model
7//!
8//! Each agent receives a DID (Decentralized Identifier) using the `did:web` method:
9//! ```text
10//! did:web:cellstate.batterypack.dev:agents:{agent-uuid}
11//! ```
12//!
13//! Agents can hold Verifiable Credentials that attest to:
14//! - Identity (who is this agent?)
15//! - Capabilities (what can this agent do?)
16//! - Provenance (who created this agent?)
17//! - Compliance (EU AI Act attestation)
18
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::fmt;
23use uuid::Uuid;
24
25// ============================================================================
26// DECENTRALIZED IDENTIFIER (DID)
27// ============================================================================
28
29/// A Decentralized Identifier (DID) for an agent.
30///
31/// DIDs follow the W3C DID Core specification. For CELLSTATE agents,
32/// we use the `did:web` method which resolves via HTTPS.
33///
34/// Format: `did:web:{domain}:agents:{agent-uuid}`
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct Did(pub String);
37
38impl Did {
39    /// Create a `did:web` identifier for an agent.
40    ///
41    /// # Arguments
42    /// * `domain` - The domain hosting the DID document (e.g., "cellstate.batterypack.dev")
43    /// * `agent_id` - The agent's UUID
44    ///
45    /// # Examples
46    /// ```
47    /// # use uuid::Uuid;
48    /// # use cellstate_core::credentials::Did;
49    /// let id = Uuid::nil();
50    /// let did = Did::web("cellstate.batterypack.dev", id);
51    /// assert_eq!(did.0, "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000");
52    /// ```
53    pub fn web(domain: &str, agent_id: Uuid) -> Self {
54        Self(format!("did:web:{}:agents:{}", domain, agent_id))
55    }
56
57    /// Parse a DID string, returning `Some(Did)` if it begins with `did:`.
58    pub fn parse(s: &str) -> Option<Self> {
59        if s.starts_with("did:") {
60            Some(Self(s.to_string()))
61        } else {
62            None
63        }
64    }
65
66    /// Extract the agent UUID from a `did:web` identifier.
67    ///
68    /// Returns `None` if the DID is not a `did:web` with the expected
69    /// `:agents:{uuid}` suffix.
70    pub fn agent_id(&self) -> Option<Uuid> {
71        let parts: Vec<&str> = self.0.split(':').collect();
72        // did:web:domain:agents:uuid  -> 5 parts minimum
73        if parts.len() < 5 || parts[1] != "web" {
74            return None;
75        }
76        // Find the "agents" segment and take the next one as the UUID
77        let mut iter = parts.iter();
78        while let Some(&segment) = iter.next() {
79            if segment == "agents" {
80                if let Some(&uuid_str) = iter.next() {
81                    return Uuid::parse_str(uuid_str).ok();
82                }
83            }
84        }
85        None
86    }
87
88    /// Get the DID method (e.g., "web", "key", "pkh").
89    ///
90    /// Returns the method portion of the DID string (`did:{method}:...`).
91    pub fn method(&self) -> &str {
92        self.0
93            .strip_prefix("did:")
94            .and_then(|rest| rest.split(':').next())
95            .unwrap_or("")
96    }
97}
98
99impl fmt::Display for Did {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "{}", self.0)
102    }
103}
104
105// ============================================================================
106// DID DOCUMENT
107// ============================================================================
108
109/// A DID Document containing the agent's public keys and service endpoints.
110///
111/// Follows the W3C DID Core specification. The document is resolvable via
112/// the `did:web` method (HTTPS GET to `/.well-known/did.json`).
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct DidDocument {
115    /// JSON-LD context URIs
116    #[serde(rename = "@context")]
117    pub context: Vec<String>,
118    /// The DID this document describes
119    pub id: Did,
120    /// The DID of the controller (typically the tenant)
121    pub controller: Did,
122    /// Public keys and verification methods
123    pub verification_method: Vec<VerificationMethod>,
124    /// DIDs or key IDs that can authenticate as this DID subject
125    pub authentication: Vec<String>,
126    /// DIDs or key IDs that can issue assertions (sign credentials)
127    pub assertion_method: Vec<String>,
128    /// Service endpoints (API URLs, messaging endpoints, etc.)
129    pub service: Vec<ServiceEndpoint>,
130    /// When this document was created
131    pub created: DateTime<Utc>,
132    /// When this document was last updated
133    pub updated: DateTime<Utc>,
134}
135
136impl DidDocument {
137    /// Create a DID Document for an agent.
138    ///
139    /// This generates a standard document structure with a single Ed25519
140    /// verification method and the CELLSTATE API service endpoint.
141    pub fn for_agent(domain: &str, agent_id: Uuid, tenant_id: Uuid) -> Self {
142        let agent_did = Did::web(domain, agent_id);
143        let tenant_did = Did::web(domain, tenant_id);
144        let now = Utc::now();
145
146        let key_id = format!("{}#key-1", agent_did.0);
147
148        Self {
149            context: vec![
150                "https://www.w3.org/ns/did/v1".to_string(),
151                "https://w3id.org/security/suites/ed25519-2020/v1".to_string(),
152            ],
153            id: agent_did.clone(),
154            controller: tenant_did,
155            verification_method: vec![VerificationMethod {
156                id: key_id.clone(),
157                method_type: "Ed25519VerificationKey2020".to_string(),
158                controller: agent_did.0.clone(),
159                public_key_multibase: None, // Populated when keys are generated
160            }],
161            authentication: vec![key_id.clone()],
162            assertion_method: vec![key_id],
163            service: vec![ServiceEndpoint {
164                id: format!("{}#cellstate-api", agent_did.0),
165                service_type: "CellstateAgent".to_string(),
166                service_endpoint: format!("https://{}/api/v1/agents/{}", domain, agent_id),
167            }],
168            created: now,
169            updated: now,
170        }
171    }
172}
173
174/// A verification method (public key) in a DID Document.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct VerificationMethod {
177    /// Unique identifier for this key (e.g., "did:web:...#key-1")
178    pub id: String,
179    /// The type of key (e.g., "Ed25519VerificationKey2020")
180    #[serde(rename = "type")]
181    pub method_type: String,
182    /// The DID of the key controller
183    pub controller: String,
184    /// The public key encoded in multibase format
185    pub public_key_multibase: Option<String>,
186}
187
188/// A service endpoint in a DID Document.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ServiceEndpoint {
191    /// Unique identifier for this service
192    pub id: String,
193    /// The type of service (e.g., "CellstateAgent", "LinkedDomains")
194    #[serde(rename = "type")]
195    pub service_type: String,
196    /// The URL of the service endpoint
197    #[serde(rename = "serviceEndpoint")]
198    pub service_endpoint: String,
199}
200
201// ============================================================================
202// VERIFIABLE CREDENTIAL
203// ============================================================================
204
205/// A W3C Verifiable Credential.
206///
207/// This structure follows the W3C Verifiable Credentials Data Model v2.0.
208/// Credentials attest to claims about a subject (an agent) made by an issuer
209/// (typically the CELLSTATE platform or tenant).
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct VerifiableCredential {
212    /// JSON-LD context URIs
213    #[serde(rename = "@context")]
214    pub context: Vec<String>,
215    /// Unique identifier for this credential (URN or URL)
216    pub id: String,
217    /// The types of this credential (always includes "VerifiableCredential")
218    #[serde(rename = "type")]
219    pub credential_type: Vec<String>,
220    /// Who issued this credential
221    pub issuer: CredentialIssuer,
222    /// When this credential was issued
223    pub issuance_date: DateTime<Utc>,
224    /// When this credential expires (if applicable)
225    pub expiration_date: Option<DateTime<Utc>>,
226    /// The subject and claims of this credential
227    pub credential_subject: CredentialSubject,
228    /// Cryptographic proof (if signed)
229    pub proof: Option<CredentialProof>,
230}
231
232/// The issuer of a credential, either a simple DID string or an object with metadata.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(untagged)]
235pub enum CredentialIssuer {
236    /// Just a DID string
237    Simple(String),
238    /// An object with id and optional name
239    Object {
240        /// The issuer's DID
241        id: String,
242        /// Human-readable name
243        name: Option<String>,
244    },
245}
246
247impl CredentialIssuer {
248    /// Get the issuer ID regardless of variant.
249    pub fn id(&self) -> &str {
250        match self {
251            CredentialIssuer::Simple(id) => id,
252            CredentialIssuer::Object { id, .. } => id,
253        }
254    }
255}
256
257/// The subject of a credential, including the subject's DID and claims.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct CredentialSubject {
260    /// The DID of the subject
261    pub id: String,
262    /// Key-value claims about the subject
263    #[serde(flatten)]
264    pub claims: HashMap<String, serde_json::Value>,
265}
266
267/// A cryptographic proof attached to a credential.
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct CredentialProof {
270    /// The type of proof (e.g., "Ed25519Signature2020")
271    #[serde(rename = "type")]
272    pub proof_type: String,
273    /// When the proof was created
274    pub created: DateTime<Utc>,
275    /// The verification method (key ID) used to create the proof
276    pub verification_method: String,
277    /// The purpose of the proof (e.g., "assertionMethod")
278    pub proof_purpose: String,
279    /// The proof value (signature, encoded)
280    pub proof_value: String,
281}
282
283// ============================================================================
284// AGENT CREDENTIAL TYPES
285// ============================================================================
286
287/// Credential types that can be issued to agents.
288#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum AgentCredentialType {
291    /// Identity credential -- proves who the agent is
292    Identity,
293    /// Capability credential -- proves what the agent can do
294    Capability,
295    /// Provenance credential -- proves who created the agent
296    Provenance,
297    /// Compliance credential -- EU AI Act attestation
298    Compliance,
299}
300
301impl AgentCredentialType {
302    /// Returns the W3C VC type string for this credential type.
303    pub fn vc_type(&self) -> &'static str {
304        match self {
305            AgentCredentialType::Identity => "AgentIdentityCredential",
306            AgentCredentialType::Capability => "AgentCapabilityCredential",
307            AgentCredentialType::Provenance => "AgentProvenanceCredential",
308            AgentCredentialType::Compliance => "AgentComplianceCredential",
309        }
310    }
311}
312
313impl fmt::Display for AgentCredentialType {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "{}", self.vc_type())
316    }
317}
318
319// ============================================================================
320// CREDENTIAL BUILDER
321// ============================================================================
322
323/// Builder for creating Verifiable Credentials.
324///
325/// Uses the builder pattern to construct well-formed credentials with
326/// required and optional fields.
327///
328/// # Example
329/// ```
330/// # use cellstate_core::credentials::*;
331/// # use chrono::Utc;
332/// let credential = CredentialBuilder::new()
333///     .issuer("did:web:cellstate.batterypack.dev")
334///     .subject("did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000")
335///     .credential_type(AgentCredentialType::Identity)
336///     .claim("name", serde_json::json!("test-agent"))
337///     .build()
338///     .expect("credential should build");
339/// ```
340pub struct CredentialBuilder {
341    issuer: Option<String>,
342    subject: Option<String>,
343    credential_type: Option<AgentCredentialType>,
344    claims: HashMap<String, serde_json::Value>,
345    expiration: Option<DateTime<Utc>>,
346}
347
348impl CredentialBuilder {
349    /// Create a new empty credential builder.
350    pub fn new() -> Self {
351        Self {
352            issuer: None,
353            subject: None,
354            credential_type: None,
355            claims: HashMap::new(),
356            expiration: None,
357        }
358    }
359
360    /// Set the issuer DID.
361    pub fn issuer(mut self, issuer: &str) -> Self {
362        self.issuer = Some(issuer.to_string());
363        self
364    }
365
366    /// Set the subject DID.
367    pub fn subject(mut self, subject_did: &str) -> Self {
368        self.subject = Some(subject_did.to_string());
369        self
370    }
371
372    /// Set the credential type.
373    pub fn credential_type(mut self, cred_type: AgentCredentialType) -> Self {
374        self.credential_type = Some(cred_type);
375        self
376    }
377
378    /// Add a claim to the credential subject.
379    pub fn claim(mut self, key: &str, value: serde_json::Value) -> Self {
380        self.claims.insert(key.to_string(), value);
381        self
382    }
383
384    /// Set the expiration date.
385    pub fn expiration(mut self, expires: DateTime<Utc>) -> Self {
386        self.expiration = Some(expires);
387        self
388    }
389
390    /// Build the Verifiable Credential.
391    ///
392    /// Returns `Err` if required fields (issuer, subject) are missing.
393    pub fn build(self) -> Result<VerifiableCredential, CredentialError> {
394        let issuer = self.issuer.ok_or(CredentialError::MissingIssuer)?;
395        let subject = self.subject.ok_or(CredentialError::MissingSubject)?;
396
397        let mut types = vec!["VerifiableCredential".to_string()];
398        if let Some(ref cred_type) = self.credential_type {
399            types.push(cred_type.vc_type().to_string());
400        }
401
402        let credential_id = format!("urn:uuid:{}", Uuid::now_v7());
403
404        Ok(VerifiableCredential {
405            context: vec![
406                "https://www.w3.org/2018/credentials/v1".to_string(),
407                "https://cellstate.batterypack.dev/credentials/v1".to_string(),
408            ],
409            id: credential_id,
410            credential_type: types,
411            issuer: CredentialIssuer::Simple(issuer),
412            issuance_date: Utc::now(),
413            expiration_date: self.expiration,
414            credential_subject: CredentialSubject {
415                id: subject,
416                claims: self.claims,
417            },
418            proof: None,
419        })
420    }
421}
422
423impl Default for CredentialBuilder {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429// ============================================================================
430// CREDENTIAL ERRORS
431// ============================================================================
432
433/// Errors that can occur during credential operations.
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub enum CredentialError {
436    /// The credential is missing an issuer
437    MissingIssuer,
438    /// The credential is missing a subject
439    MissingSubject,
440    /// The DID string is malformed
441    InvalidDid(String),
442    /// The credential has expired
443    Expired,
444    /// The proof is invalid or cannot be verified
445    InvalidProof,
446    /// The proof type is not supported
447    UnsupportedProofType(String),
448}
449
450impl fmt::Display for CredentialError {
451    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452        match self {
453            CredentialError::MissingIssuer => write!(f, "Credential is missing an issuer"),
454            CredentialError::MissingSubject => write!(f, "Credential is missing a subject"),
455            CredentialError::InvalidDid(did) => write!(f, "Invalid DID: {}", did),
456            CredentialError::Expired => write!(f, "Credential has expired"),
457            CredentialError::InvalidProof => write!(f, "Credential proof is invalid"),
458            CredentialError::UnsupportedProofType(t) => {
459                write!(f, "Unsupported proof type: {}", t)
460            }
461        }
462    }
463}
464
465impl std::error::Error for CredentialError {}
466
467// ============================================================================
468// CREDENTIAL VERIFIER
469// ============================================================================
470
471/// Verifier for Verifiable Credentials.
472///
473/// Performs structural validation, expiry checks, and subject matching.
474/// Cryptographic proof verification requires the issuer's public key,
475/// which must be resolved via the DID Document.
476pub struct CredentialVerifier;
477
478impl CredentialVerifier {
479    /// Verify a credential's structure and expiry.
480    ///
481    /// Checks:
482    /// - The credential has a non-empty `id`
483    /// - The type array includes "VerifiableCredential"
484    /// - The subject has a non-empty `id`
485    /// - The credential has not expired
486    ///
487    /// This does NOT verify the cryptographic proof (that requires the
488    /// issuer's public key resolved from their DID Document).
489    pub fn verify_structure(credential: &VerifiableCredential) -> Result<(), CredentialError> {
490        // Check credential ID
491        if credential.id.is_empty() {
492            return Err(CredentialError::InvalidDid(
493                "Credential ID is empty".to_string(),
494            ));
495        }
496
497        // Check type includes base type
498        if !credential
499            .credential_type
500            .iter()
501            .any(|t| t == "VerifiableCredential")
502        {
503            return Err(CredentialError::InvalidDid(
504                "Missing VerifiableCredential base type".to_string(),
505            ));
506        }
507
508        // Check subject ID
509        if credential.credential_subject.id.is_empty() {
510            return Err(CredentialError::MissingSubject);
511        }
512
513        // Check expiry
514        if Self::is_expired(credential) {
515            return Err(CredentialError::Expired);
516        }
517
518        Ok(())
519    }
520
521    /// Check if a credential has expired.
522    ///
523    /// Returns `false` if the credential has no expiration date.
524    pub fn is_expired(credential: &VerifiableCredential) -> bool {
525        if let Some(expiration) = credential.expiration_date {
526            Utc::now() > expiration
527        } else {
528            false
529        }
530    }
531
532    /// Verify the credential was issued for the given subject DID.
533    pub fn verify_subject(credential: &VerifiableCredential, subject_did: &str) -> bool {
534        credential.credential_subject.id == subject_did
535    }
536}
537
538// ============================================================================
539// TESTS
540// ============================================================================
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use serde_json::json;
546
547    // ------------------------------------------------------------------------
548    // DID Tests
549    // ------------------------------------------------------------------------
550
551    #[test]
552    fn test_did_web_creation() {
553        let agent_id = Uuid::nil();
554        let did = Did::web("cellstate.batterypack.dev", agent_id);
555        assert_eq!(
556            did.0,
557            "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
558        );
559    }
560
561    #[test]
562    fn test_did_web_with_real_uuid() {
563        let agent_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
564        let did = Did::web("example.com", agent_id);
565        assert_eq!(
566            did.0,
567            "did:web:example.com:agents:550e8400-e29b-41d4-a716-446655440000"
568        );
569    }
570
571    #[test]
572    fn test_did_parse_valid() {
573        let did = Did::parse(
574            "did:web:cellstate.batterypack.dev:agents:550e8400-e29b-41d4-a716-446655440000",
575        );
576        assert!(did.is_some());
577        assert_eq!(
578            did.unwrap().0,
579            "did:web:cellstate.batterypack.dev:agents:550e8400-e29b-41d4-a716-446655440000"
580        );
581    }
582
583    #[test]
584    fn test_did_parse_other_method() {
585        let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
586        assert!(did.is_some());
587    }
588
589    #[test]
590    fn test_did_parse_invalid() {
591        assert!(Did::parse("not-a-did").is_none());
592        assert!(Did::parse("").is_none());
593        assert!(Did::parse("http://example.com").is_none());
594    }
595
596    #[test]
597    fn test_did_agent_id_extraction() {
598        let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
599        let did = Did::web("cellstate.batterypack.dev", uuid);
600        assert_eq!(did.agent_id(), Some(uuid));
601    }
602
603    #[test]
604    fn test_did_agent_id_nil() {
605        let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
606        assert_eq!(did.agent_id(), Some(Uuid::nil()));
607    }
608
609    #[test]
610    fn test_did_agent_id_non_web() {
611        let did = Did("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string());
612        assert!(did.agent_id().is_none());
613    }
614
615    #[test]
616    fn test_did_agent_id_malformed() {
617        let did = Did("did:web:cellstate.batterypack.dev".to_string());
618        assert!(did.agent_id().is_none());
619    }
620
621    #[test]
622    fn test_did_agent_id_bad_uuid() {
623        let did = Did("did:web:cellstate.batterypack.dev:agents:not-a-uuid".to_string());
624        assert!(did.agent_id().is_none());
625    }
626
627    #[test]
628    fn test_did_method_web() {
629        let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
630        assert_eq!(did.method(), "web");
631    }
632
633    #[test]
634    fn test_did_method_key() {
635        let did = Did("did:key:z6Mk...".to_string());
636        assert_eq!(did.method(), "key");
637    }
638
639    #[test]
640    fn test_did_method_empty() {
641        let did = Did("not-a-did".to_string());
642        assert_eq!(did.method(), "");
643    }
644
645    #[test]
646    fn test_did_display() {
647        let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
648        let displayed = format!("{}", did);
649        assert_eq!(
650            displayed,
651            "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
652        );
653    }
654
655    #[test]
656    fn test_did_serde_roundtrip() {
657        let did = Did::web("cellstate.batterypack.dev", Uuid::nil());
658        let json = serde_json::to_string(&did).unwrap();
659        let deserialized: Did = serde_json::from_str(&json).unwrap();
660        assert_eq!(did, deserialized);
661    }
662
663    #[test]
664    fn test_did_equality_and_hash() {
665        let id = Uuid::nil();
666        let did1 = Did::web("cellstate.batterypack.dev", id);
667        let did2 = Did::web("cellstate.batterypack.dev", id);
668        let did3 = Did::web("other.com", id);
669
670        assert_eq!(did1, did2);
671        assert_ne!(did1, did3);
672
673        // Test as HashMap key
674        let mut map = HashMap::new();
675        map.insert(did1.clone(), "value");
676        assert_eq!(map.get(&did2), Some(&"value"));
677        assert_eq!(map.get(&did3), None);
678    }
679
680    // ------------------------------------------------------------------------
681    // DID Document Tests
682    // ------------------------------------------------------------------------
683
684    #[test]
685    fn test_did_document_for_agent() {
686        let agent_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
687        let tenant_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440000").unwrap();
688        let doc = DidDocument::for_agent("cellstate.batterypack.dev", agent_id, tenant_id);
689
690        // Check context
691        assert_eq!(doc.context.len(), 2);
692        assert!(doc.context[0].contains("did/v1"));
693
694        // Check id
695        assert_eq!(doc.id, Did::web("cellstate.batterypack.dev", agent_id));
696
697        // Check controller is the tenant
698        assert_eq!(
699            doc.controller,
700            Did::web("cellstate.batterypack.dev", tenant_id)
701        );
702
703        // Check verification method
704        assert_eq!(doc.verification_method.len(), 1);
705        assert_eq!(
706            doc.verification_method[0].method_type,
707            "Ed25519VerificationKey2020"
708        );
709        assert!(doc.verification_method[0].id.contains("#key-1"));
710
711        // Check authentication and assertion_method reference the key
712        assert_eq!(doc.authentication.len(), 1);
713        assert!(doc.authentication[0].contains("#key-1"));
714        assert_eq!(doc.assertion_method.len(), 1);
715
716        // Check service endpoint
717        assert_eq!(doc.service.len(), 1);
718        assert_eq!(doc.service[0].service_type, "CellstateAgent");
719        assert!(doc.service[0]
720            .service_endpoint
721            .contains(&agent_id.to_string()));
722    }
723
724    #[test]
725    fn test_did_document_serde_roundtrip() {
726        let doc = DidDocument::for_agent("cellstate.batterypack.dev", Uuid::nil(), Uuid::nil());
727        let json = serde_json::to_string(&doc).unwrap();
728        let deserialized: DidDocument = serde_json::from_str(&json).unwrap();
729        assert_eq!(deserialized.id, doc.id);
730        assert_eq!(deserialized.controller, doc.controller);
731        assert_eq!(deserialized.verification_method.len(), 1);
732        assert_eq!(deserialized.service.len(), 1);
733    }
734
735    #[test]
736    fn test_did_document_json_ld_context() {
737        let doc = DidDocument::for_agent("cellstate.batterypack.dev", Uuid::nil(), Uuid::nil());
738        let json_value: serde_json::Value = serde_json::to_value(&doc).unwrap();
739        // @context should be serialized correctly
740        assert!(json_value.get("@context").is_some());
741        let ctx = json_value["@context"].as_array().unwrap();
742        assert_eq!(ctx.len(), 2);
743    }
744
745    // ------------------------------------------------------------------------
746    // Credential Builder Tests
747    // ------------------------------------------------------------------------
748
749    #[test]
750    fn test_credential_builder_identity() {
751        let cred = CredentialBuilder::new()
752            .issuer("did:web:cellstate.batterypack.dev")
753            .subject(
754                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
755            )
756            .credential_type(AgentCredentialType::Identity)
757            .claim("name", json!("test-agent"))
758            .claim("agentType", json!("coder"))
759            .build()
760            .unwrap();
761
762        assert!(cred.id.starts_with("urn:uuid:"));
763        assert!(cred
764            .credential_type
765            .contains(&"VerifiableCredential".to_string()));
766        assert!(cred
767            .credential_type
768            .contains(&"AgentIdentityCredential".to_string()));
769        assert_eq!(cred.credential_subject.claims["name"], json!("test-agent"));
770        assert_eq!(cred.credential_subject.claims["agentType"], json!("coder"));
771        assert!(cred.expiration_date.is_none());
772        assert!(cred.proof.is_none());
773    }
774
775    #[test]
776    fn test_credential_builder_capability() {
777        let cred = CredentialBuilder::new()
778            .issuer("did:web:cellstate.batterypack.dev")
779            .subject(
780                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
781            )
782            .credential_type(AgentCredentialType::Capability)
783            .claim("capabilities", json!(["read", "write", "execute"]))
784            .build()
785            .unwrap();
786
787        assert!(cred
788            .credential_type
789            .contains(&"AgentCapabilityCredential".to_string()));
790        assert_eq!(
791            cred.credential_subject.claims["capabilities"],
792            json!(["read", "write", "execute"])
793        );
794    }
795
796    #[test]
797    fn test_credential_builder_provenance() {
798        let cred = CredentialBuilder::new()
799            .issuer("did:web:cellstate.batterypack.dev")
800            .subject(
801                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
802            )
803            .credential_type(AgentCredentialType::Provenance)
804            .claim("creator", json!("tenant-admin"))
805            .claim("createdAt", json!("2025-01-01T00:00:00Z"))
806            .build()
807            .unwrap();
808
809        assert!(cred
810            .credential_type
811            .contains(&"AgentProvenanceCredential".to_string()));
812    }
813
814    #[test]
815    fn test_credential_builder_compliance() {
816        let cred = CredentialBuilder::new()
817            .issuer("did:web:cellstate.batterypack.dev")
818            .subject(
819                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
820            )
821            .credential_type(AgentCredentialType::Compliance)
822            .claim("regulation", json!("EU AI Act"))
823            .claim("riskCategory", json!("limited"))
824            .claim("transparencyProvided", json!(true))
825            .build()
826            .unwrap();
827
828        assert!(cred
829            .credential_type
830            .contains(&"AgentComplianceCredential".to_string()));
831        assert_eq!(
832            cred.credential_subject.claims["regulation"],
833            json!("EU AI Act")
834        );
835    }
836
837    #[test]
838    fn test_credential_builder_with_expiration() {
839        let expires = Utc::now() + chrono::Duration::days(365);
840        let cred = CredentialBuilder::new()
841            .issuer("did:web:cellstate.batterypack.dev")
842            .subject(
843                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
844            )
845            .credential_type(AgentCredentialType::Identity)
846            .expiration(expires)
847            .build()
848            .unwrap();
849
850        assert_eq!(cred.expiration_date, Some(expires));
851    }
852
853    #[test]
854    fn test_credential_builder_no_type() {
855        let cred = CredentialBuilder::new()
856            .issuer("did:web:cellstate.batterypack.dev")
857            .subject(
858                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
859            )
860            .build()
861            .unwrap();
862
863        // Should still have base type
864        assert_eq!(cred.credential_type, vec!["VerifiableCredential"]);
865    }
866
867    #[test]
868    fn test_credential_builder_missing_issuer() {
869        let result = CredentialBuilder::new()
870            .subject(
871                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
872            )
873            .build();
874
875        assert_eq!(result.unwrap_err(), CredentialError::MissingIssuer);
876    }
877
878    #[test]
879    fn test_credential_builder_missing_subject() {
880        let result = CredentialBuilder::new()
881            .issuer("did:web:cellstate.batterypack.dev")
882            .build();
883
884        assert_eq!(result.unwrap_err(), CredentialError::MissingSubject);
885    }
886
887    #[test]
888    fn test_credential_builder_missing_both() {
889        let result = CredentialBuilder::new().build();
890        // Issuer is checked first
891        assert_eq!(result.unwrap_err(), CredentialError::MissingIssuer);
892    }
893
894    #[test]
895    fn test_credential_builder_default() {
896        let builder = CredentialBuilder::default();
897        // Default builder should fail to build (no issuer/subject)
898        assert!(builder.build().is_err());
899    }
900
901    // ------------------------------------------------------------------------
902    // Credential Verifier Tests
903    // ------------------------------------------------------------------------
904
905    fn make_valid_credential() -> VerifiableCredential {
906        CredentialBuilder::new()
907            .issuer("did:web:cellstate.batterypack.dev")
908            .subject(
909                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
910            )
911            .credential_type(AgentCredentialType::Identity)
912            .claim("name", json!("test-agent"))
913            .build()
914            .unwrap()
915    }
916
917    #[test]
918    fn test_verify_structure_valid() {
919        let cred = make_valid_credential();
920        assert!(CredentialVerifier::verify_structure(&cred).is_ok());
921    }
922
923    #[test]
924    fn test_verify_structure_empty_id() {
925        let mut cred = make_valid_credential();
926        cred.id = String::new();
927        let result = CredentialVerifier::verify_structure(&cred);
928        assert!(matches!(result, Err(CredentialError::InvalidDid(_))));
929    }
930
931    #[test]
932    fn test_verify_structure_missing_base_type() {
933        let mut cred = make_valid_credential();
934        cred.credential_type = vec!["AgentIdentityCredential".to_string()];
935        let result = CredentialVerifier::verify_structure(&cred);
936        assert!(matches!(result, Err(CredentialError::InvalidDid(_))));
937    }
938
939    #[test]
940    fn test_verify_structure_empty_subject() {
941        let mut cred = make_valid_credential();
942        cred.credential_subject.id = String::new();
943        let result = CredentialVerifier::verify_structure(&cred);
944        assert_eq!(result.unwrap_err(), CredentialError::MissingSubject);
945    }
946
947    #[test]
948    fn test_verify_structure_expired() {
949        let mut cred = make_valid_credential();
950        cred.expiration_date = Some(Utc::now() - chrono::Duration::hours(1));
951        let result = CredentialVerifier::verify_structure(&cred);
952        assert_eq!(result.unwrap_err(), CredentialError::Expired);
953    }
954
955    #[test]
956    fn test_is_expired_no_expiration() {
957        let cred = make_valid_credential();
958        assert!(!CredentialVerifier::is_expired(&cred));
959    }
960
961    #[test]
962    fn test_is_expired_future() {
963        let mut cred = make_valid_credential();
964        cred.expiration_date = Some(Utc::now() + chrono::Duration::days(365));
965        assert!(!CredentialVerifier::is_expired(&cred));
966    }
967
968    #[test]
969    fn test_is_expired_past() {
970        let mut cred = make_valid_credential();
971        cred.expiration_date = Some(Utc::now() - chrono::Duration::seconds(1));
972        assert!(CredentialVerifier::is_expired(&cred));
973    }
974
975    #[test]
976    fn test_verify_subject_match() {
977        let cred = make_valid_credential();
978        assert!(CredentialVerifier::verify_subject(
979            &cred,
980            "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
981        ));
982    }
983
984    #[test]
985    fn test_verify_subject_mismatch() {
986        let cred = make_valid_credential();
987        assert!(!CredentialVerifier::verify_subject(
988            &cred,
989            "did:web:cellstate.batterypack.dev:agents:11111111-1111-1111-1111-111111111111"
990        ));
991    }
992
993    #[test]
994    fn test_verify_subject_empty() {
995        let cred = make_valid_credential();
996        assert!(!CredentialVerifier::verify_subject(&cred, ""));
997    }
998
999    // ------------------------------------------------------------------------
1000    // Credential Issuer Tests
1001    // ------------------------------------------------------------------------
1002
1003    #[test]
1004    fn test_credential_issuer_simple_id() {
1005        let issuer = CredentialIssuer::Simple("did:web:cellstate.batterypack.dev".to_string());
1006        assert_eq!(issuer.id(), "did:web:cellstate.batterypack.dev");
1007    }
1008
1009    #[test]
1010    fn test_credential_issuer_object_id() {
1011        let issuer = CredentialIssuer::Object {
1012            id: "did:web:cellstate.batterypack.dev".to_string(),
1013            name: Some("CELLSTATE Platform".to_string()),
1014        };
1015        assert_eq!(issuer.id(), "did:web:cellstate.batterypack.dev");
1016    }
1017
1018    #[test]
1019    fn test_credential_issuer_serde_simple() {
1020        let issuer = CredentialIssuer::Simple("did:web:cellstate.batterypack.dev".to_string());
1021        let json = serde_json::to_string(&issuer).unwrap();
1022        assert_eq!(json, "\"did:web:cellstate.batterypack.dev\"");
1023    }
1024
1025    #[test]
1026    fn test_credential_issuer_serde_object() {
1027        let issuer = CredentialIssuer::Object {
1028            id: "did:web:cellstate.batterypack.dev".to_string(),
1029            name: Some("CELLSTATE".to_string()),
1030        };
1031        let json = serde_json::to_value(&issuer).unwrap();
1032        assert_eq!(json["id"], "did:web:cellstate.batterypack.dev");
1033        assert_eq!(json["name"], "CELLSTATE");
1034    }
1035
1036    // ------------------------------------------------------------------------
1037    // Credential Serde Tests
1038    // ------------------------------------------------------------------------
1039
1040    #[test]
1041    fn test_credential_serde_roundtrip() {
1042        let cred = CredentialBuilder::new()
1043            .issuer("did:web:cellstate.batterypack.dev")
1044            .subject(
1045                "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000",
1046            )
1047            .credential_type(AgentCredentialType::Identity)
1048            .claim("name", json!("agent-1"))
1049            .claim("version", json!(1))
1050            .build()
1051            .unwrap();
1052
1053        let json = serde_json::to_string(&cred).unwrap();
1054        let deserialized: VerifiableCredential = serde_json::from_str(&json).unwrap();
1055
1056        assert_eq!(deserialized.id, cred.id);
1057        assert_eq!(deserialized.credential_type, cred.credential_type);
1058        assert_eq!(
1059            deserialized.credential_subject.id,
1060            cred.credential_subject.id
1061        );
1062        assert_eq!(
1063            deserialized.credential_subject.claims["name"],
1064            json!("agent-1")
1065        );
1066    }
1067
1068    #[test]
1069    fn test_credential_json_structure() {
1070        let cred = make_valid_credential();
1071        let json_value: serde_json::Value = serde_json::to_value(&cred).unwrap();
1072
1073        // Verify JSON-LD @context is present
1074        assert!(json_value.get("@context").is_some());
1075        // Verify type field
1076        assert!(json_value.get("type").is_some());
1077        let types = json_value["type"].as_array().unwrap();
1078        assert!(types.iter().any(|t| t == "VerifiableCredential"));
1079    }
1080
1081    // ------------------------------------------------------------------------
1082    // Credential Error Tests
1083    // ------------------------------------------------------------------------
1084
1085    #[test]
1086    fn test_credential_error_display() {
1087        assert_eq!(
1088            format!("{}", CredentialError::MissingIssuer),
1089            "Credential is missing an issuer"
1090        );
1091        assert_eq!(
1092            format!("{}", CredentialError::MissingSubject),
1093            "Credential is missing a subject"
1094        );
1095        assert_eq!(
1096            format!("{}", CredentialError::InvalidDid("bad".to_string())),
1097            "Invalid DID: bad"
1098        );
1099        assert_eq!(
1100            format!("{}", CredentialError::Expired),
1101            "Credential has expired"
1102        );
1103        assert_eq!(
1104            format!("{}", CredentialError::InvalidProof),
1105            "Credential proof is invalid"
1106        );
1107        assert_eq!(
1108            format!(
1109                "{}",
1110                CredentialError::UnsupportedProofType("RSA".to_string())
1111            ),
1112            "Unsupported proof type: RSA"
1113        );
1114    }
1115
1116    #[test]
1117    fn test_credential_error_is_error() {
1118        let err: Box<dyn std::error::Error> = Box::new(CredentialError::MissingIssuer);
1119        assert_eq!(err.to_string(), "Credential is missing an issuer");
1120    }
1121
1122    // ------------------------------------------------------------------------
1123    // AgentCredentialType Tests
1124    // ------------------------------------------------------------------------
1125
1126    #[test]
1127    fn test_agent_credential_type_vc_type() {
1128        assert_eq!(
1129            AgentCredentialType::Identity.vc_type(),
1130            "AgentIdentityCredential"
1131        );
1132        assert_eq!(
1133            AgentCredentialType::Capability.vc_type(),
1134            "AgentCapabilityCredential"
1135        );
1136        assert_eq!(
1137            AgentCredentialType::Provenance.vc_type(),
1138            "AgentProvenanceCredential"
1139        );
1140        assert_eq!(
1141            AgentCredentialType::Compliance.vc_type(),
1142            "AgentComplianceCredential"
1143        );
1144    }
1145
1146    #[test]
1147    fn test_agent_credential_type_display() {
1148        assert_eq!(
1149            format!("{}", AgentCredentialType::Identity),
1150            "AgentIdentityCredential"
1151        );
1152    }
1153
1154    #[test]
1155    fn test_agent_credential_type_equality() {
1156        assert_eq!(AgentCredentialType::Identity, AgentCredentialType::Identity);
1157        assert_ne!(
1158            AgentCredentialType::Identity,
1159            AgentCredentialType::Capability
1160        );
1161    }
1162
1163    #[test]
1164    fn test_agent_credential_type_serde_roundtrip() {
1165        let cred_type = AgentCredentialType::Compliance;
1166        let json = serde_json::to_string(&cred_type).unwrap();
1167        let deserialized: AgentCredentialType = serde_json::from_str(&json).unwrap();
1168        assert_eq!(cred_type, deserialized);
1169    }
1170
1171    // ------------------------------------------------------------------------
1172    // CredentialProof Tests
1173    // ------------------------------------------------------------------------
1174
1175    #[test]
1176    fn test_credential_proof_serde() {
1177        let proof = CredentialProof {
1178            proof_type: "Ed25519Signature2020".to_string(),
1179            created: Utc::now(),
1180            verification_method: "did:web:cellstate.batterypack.dev#key-1".to_string(),
1181            proof_purpose: "assertionMethod".to_string(),
1182            proof_value: "z58DAdFfa9SkqZMVPxAQpic7ndTn".to_string(),
1183        };
1184
1185        let json = serde_json::to_string(&proof).unwrap();
1186        let deserialized: CredentialProof = serde_json::from_str(&json).unwrap();
1187        assert_eq!(deserialized.proof_type, "Ed25519Signature2020");
1188        assert_eq!(
1189            deserialized.verification_method,
1190            "did:web:cellstate.batterypack.dev#key-1"
1191        );
1192        assert_eq!(deserialized.proof_purpose, "assertionMethod");
1193    }
1194
1195    // ------------------------------------------------------------------------
1196    // Integration-style Tests
1197    // ------------------------------------------------------------------------
1198
1199    #[test]
1200    fn test_full_credential_lifecycle() {
1201        let agent_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1202        let tenant_id = Uuid::parse_str("660e8400-e29b-41d4-a716-446655440000").unwrap();
1203        let domain = "cellstate.batterypack.dev";
1204
1205        // Step 1: Create DID Document
1206        let doc = DidDocument::for_agent(domain, agent_id, tenant_id);
1207        assert_eq!(doc.id, Did::web(domain, agent_id));
1208
1209        // Step 2: Build identity credential
1210        let cred = CredentialBuilder::new()
1211            .issuer(&doc.controller.0)
1212            .subject(&doc.id.0)
1213            .credential_type(AgentCredentialType::Identity)
1214            .claim("name", json!("agent-alpha"))
1215            .claim("agentType", json!("coder"))
1216            .claim("tenantId", json!(tenant_id.to_string()))
1217            .build()
1218            .unwrap();
1219
1220        // Step 3: Verify structure
1221        assert!(CredentialVerifier::verify_structure(&cred).is_ok());
1222
1223        // Step 4: Verify subject
1224        assert!(CredentialVerifier::verify_subject(&cred, &doc.id.0));
1225
1226        // Step 5: Not expired
1227        assert!(!CredentialVerifier::is_expired(&cred));
1228
1229        // Step 6: Extract agent_id from DID
1230        assert_eq!(doc.id.agent_id(), Some(agent_id));
1231    }
1232
1233    #[test]
1234    fn test_multiple_credential_types_for_agent() {
1235        let agent_did =
1236            "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000";
1237        let issuer = "did:web:cellstate.batterypack.dev";
1238
1239        let identity = CredentialBuilder::new()
1240            .issuer(issuer)
1241            .subject(agent_did)
1242            .credential_type(AgentCredentialType::Identity)
1243            .claim("name", json!("agent-1"))
1244            .build()
1245            .unwrap();
1246
1247        let capability = CredentialBuilder::new()
1248            .issuer(issuer)
1249            .subject(agent_did)
1250            .credential_type(AgentCredentialType::Capability)
1251            .claim("capabilities", json!(["read", "write"]))
1252            .build()
1253            .unwrap();
1254
1255        let provenance = CredentialBuilder::new()
1256            .issuer(issuer)
1257            .subject(agent_did)
1258            .credential_type(AgentCredentialType::Provenance)
1259            .claim("creator", json!("admin"))
1260            .build()
1261            .unwrap();
1262
1263        let compliance = CredentialBuilder::new()
1264            .issuer(issuer)
1265            .subject(agent_did)
1266            .credential_type(AgentCredentialType::Compliance)
1267            .claim("regulation", json!("EU AI Act"))
1268            .build()
1269            .unwrap();
1270
1271        // All should be valid
1272        for cred in [&identity, &capability, &provenance, &compliance] {
1273            assert!(CredentialVerifier::verify_structure(cred).is_ok());
1274            assert!(CredentialVerifier::verify_subject(cred, agent_did));
1275        }
1276
1277        // All should have unique IDs
1278        let ids: Vec<&str> = [&identity, &capability, &provenance, &compliance]
1279            .iter()
1280            .map(|c| c.id.as_str())
1281            .collect();
1282        let unique: std::collections::HashSet<&&str> = ids.iter().collect();
1283        assert_eq!(unique.len(), 4);
1284    }
1285
1286    #[test]
1287    fn test_verification_method_serde() {
1288        let vm = VerificationMethod {
1289            id: "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000#key-1".to_string(),
1290            method_type: "Ed25519VerificationKey2020".to_string(),
1291            controller: "did:web:cellstate.batterypack.dev:agents:00000000-0000-0000-0000-000000000000"
1292                .to_string(),
1293            public_key_multibase: Some("z6MkhaXgBZDvotDk".to_string()),
1294        };
1295
1296        let json = serde_json::to_value(&vm).unwrap();
1297        assert_eq!(json["type"], "Ed25519VerificationKey2020");
1298        assert!(
1299            json.get("publicKeyMultibase").is_some() || json.get("public_key_multibase").is_some()
1300        );
1301    }
1302
1303    #[test]
1304    fn test_service_endpoint_serde() {
1305        let ep = ServiceEndpoint {
1306            id: "did:web:cellstate.batterypack.dev#api".to_string(),
1307            service_type: "CellstateAgent".to_string(),
1308            service_endpoint: "https://cellstate.batterypack.dev/api/v1/agents/test".to_string(),
1309        };
1310
1311        let json = serde_json::to_value(&ep).unwrap();
1312        assert_eq!(json["type"], "CellstateAgent");
1313        assert_eq!(
1314            json["serviceEndpoint"],
1315            "https://cellstate.batterypack.dev/api/v1/agents/test"
1316        );
1317    }
1318}