Skip to main content

sumchain_primitives/
docclass.rs

1//! SRC-80X Layered Trust Architecture
2//!
3//! This module defines the data structures for the modular trust architecture:
4//! - SRC-801: Subject Standard (DID-like identity anchors)
5//! - SRC-802: Issuer Registry (who may attest)
6//! - SRC-803: Policy Token (verification rules)
7//! - SRC-804: Claim Token (verifiable statements)
8//! - SRC-805: Revocation Standard (privacy-preserving invalidation)
9//! - SRC-806: Proof Envelope (ZK proof containers)
10//! - SRC-81X: Domain-specific claims (academic, professional, etc.)
11//!
12//! Design Principles:
13//! - Identity is not data - it is a cryptographic reference point
14//! - Trust is plural - policies define acceptable trust sources
15//! - Verification rules are code - deterministic on-chain objects
16//! - The chain stores verifiability, not information
17//! - Claims must be revocable without being traceable
18//! - Verifiers trust math and quorum, not institutions
19
20use serde::{Deserialize, Serialize};
21use serde_big_array::BigArray;
22
23use crate::agreement::{EncryptionAlgorithm, EncryptionMeta};
24use crate::{Address, Balance, BlockHeight, Timestamp};
25
26// =============================================================================
27// Type Aliases for New Architecture
28// =============================================================================
29
30/// Subject ID (SRC-801) - alias for identity anchors
31pub type SubjectId = [u8; 32];
32
33/// Policy ID (SRC-803) - hash of policy content
34pub type PolicyId = [u8; 32];
35
36/// Claim ID (SRC-804) - unique claim identifier
37pub type ClaimId = [u8; 32];
38
39/// Proof ID (SRC-806) - unique proof identifier
40pub type ProofId = [u8; 32];
41
42// =============================================================================
43// Claim Types for Policy Matching (SRC-803)
44// =============================================================================
45
46/// High-level claim type categories for policy matching.
47/// Used by policies to specify which types of claims satisfy requirements.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[repr(u8)]
50pub enum ClaimType {
51    /// Identity-related claims (government ID, etc.)
52    Identity = 0,
53    /// Eligibility attestations (citizenship, residency, age)
54    Eligibility = 1,
55    /// Education credentials (transcripts, diplomas)
56    Education = 2,
57    /// Professional licenses and certifications
58    License = 3,
59    /// Employment verification
60    Employment = 4,
61    /// Healthcare credentials
62    Healthcare = 5,
63    /// Financial attestations
64    Financial = 6,
65    /// Custom/other claim types
66    Custom = 255,
67}
68
69impl ClaimType {
70    pub fn from_u8(v: u8) -> Option<Self> {
71        match v {
72            0 => Some(ClaimType::Identity),
73            1 => Some(ClaimType::Eligibility),
74            2 => Some(ClaimType::Education),
75            3 => Some(ClaimType::License),
76            4 => Some(ClaimType::Employment),
77            5 => Some(ClaimType::Healthcare),
78            6 => Some(ClaimType::Financial),
79            255 => Some(ClaimType::Custom),
80            _ => None,
81        }
82    }
83
84    pub fn name(&self) -> &'static str {
85        match self {
86            ClaimType::Identity => "Identity",
87            ClaimType::Eligibility => "Eligibility",
88            ClaimType::Education => "Education",
89            ClaimType::License => "License",
90            ClaimType::Employment => "Employment",
91            ClaimType::Healthcare => "Healthcare",
92            ClaimType::Financial => "Financial",
93            ClaimType::Custom => "Custom",
94        }
95    }
96}
97
98// =============================================================================
99// DocClass Subcodes (Claim Types)
100// =============================================================================
101
102/// DocClass subcode identifying the credential/claim type.
103/// Range 800-899 is reserved for DocClass family.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[repr(u16)]
106pub enum DocSubcode {
107    // SRC-80X Core Standards
108    /// 801: Subject (DID-equivalent) - key binding, controllers
109    Subject = 801,
110    /// 802: Issuer Registry entry
111    IssuerRegistry = 802,
112    /// 803: Policy Token
113    Policy = 803,
114    /// 804: Claim Token
115    Claim = 804,
116    /// 805: Revocation entry
117    Revocation = 805,
118    /// 806: Proof verification log
119    ProofLog = 806,
120
121    // SRC-80X Legacy (backward compat)
122    /// 800: Identity Root (legacy, maps to Subject)
123    IdentityRoot = 800,
124    /// 807: Eligibility Attestation (claim type)
125    EligibilityAttestation = 807,
126
127    // SRC-81X: Domain-Specific Claims
128    /// 810: Academic Transcript Credential
129    AcademicTranscript = 810,
130    /// 811: Diploma / Degree Credential
131    Diploma = 811,
132    /// 812: Enrollment Verification
133    EnrollmentVerification = 812,
134    /// 813: Professional License / Certification
135    ProfessionalLicense = 813,
136    /// 814: Government ID verification
137    GovernmentId = 814,
138    /// 815: Employment verification
139    Employment = 815,
140}
141
142impl DocSubcode {
143    /// Check if this is an SRC-80X core standard (800-809)
144    pub fn is_core_standard(&self) -> bool {
145        (*self as u16) >= 800 && (*self as u16) < 810
146    }
147
148    /// Check if this is an SRC-80X (Identity/Civil) subcode (legacy alias)
149    pub fn is_identity_class(&self) -> bool {
150        self.is_core_standard()
151    }
152
153    /// Check if this is an SRC-81X (Domain-specific claim) subcode
154    pub fn is_domain_claim(&self) -> bool {
155        (*self as u16) >= 810 && (*self as u16) < 820
156    }
157
158    /// Check if this is an SRC-81X (Academic/Professional) subcode (legacy alias)
159    pub fn is_academic_class(&self) -> bool {
160        self.is_domain_claim()
161    }
162
163    /// Get the subcode as u16
164    pub fn as_u16(&self) -> u16 {
165        *self as u16
166    }
167
168    /// Parse from u16
169    pub fn from_u16(code: u16) -> Option<Self> {
170        match code {
171            800 => Some(DocSubcode::IdentityRoot),
172            801 => Some(DocSubcode::Subject),
173            802 => Some(DocSubcode::IssuerRegistry),
174            803 => Some(DocSubcode::Policy),
175            804 => Some(DocSubcode::Claim),
176            805 => Some(DocSubcode::Revocation),
177            806 => Some(DocSubcode::ProofLog),
178            807 => Some(DocSubcode::EligibilityAttestation),
179            810 => Some(DocSubcode::AcademicTranscript),
180            811 => Some(DocSubcode::Diploma),
181            812 => Some(DocSubcode::EnrollmentVerification),
182            813 => Some(DocSubcode::ProfessionalLicense),
183            814 => Some(DocSubcode::GovernmentId),
184            815 => Some(DocSubcode::Employment),
185            _ => None,
186        }
187    }
188
189    /// Get human-readable name
190    pub fn name(&self) -> &'static str {
191        match self {
192            DocSubcode::IdentityRoot => "Identity Root (Legacy)",
193            DocSubcode::Subject => "Subject",
194            DocSubcode::IssuerRegistry => "Issuer Registry",
195            DocSubcode::Policy => "Policy",
196            DocSubcode::Claim => "Claim",
197            DocSubcode::Revocation => "Revocation",
198            DocSubcode::ProofLog => "Proof Log",
199            DocSubcode::EligibilityAttestation => "Eligibility Attestation",
200            DocSubcode::AcademicTranscript => "Academic Transcript",
201            DocSubcode::Diploma => "Diploma/Degree",
202            DocSubcode::EnrollmentVerification => "Enrollment Verification",
203            DocSubcode::ProfessionalLicense => "Professional License",
204            DocSubcode::GovernmentId => "Government ID",
205            DocSubcode::Employment => "Employment",
206        }
207    }
208
209    /// Convert to ClaimType for policy matching
210    pub fn to_claim_type(&self) -> Option<ClaimType> {
211        match self {
212            DocSubcode::EligibilityAttestation => Some(ClaimType::Eligibility),
213            DocSubcode::AcademicTranscript => Some(ClaimType::Education),
214            DocSubcode::Diploma => Some(ClaimType::Education),
215            DocSubcode::EnrollmentVerification => Some(ClaimType::Education),
216            DocSubcode::ProfessionalLicense => Some(ClaimType::License),
217            DocSubcode::GovernmentId => Some(ClaimType::Identity),
218            DocSubcode::Employment => Some(ClaimType::Employment),
219            _ => None,
220        }
221    }
222}
223
224// =============================================================================
225// Credential ID
226// =============================================================================
227
228/// Unique identifier for a DocClass credential.
229/// Derived from: blake3(issuer || subcode || subject_commitment || nonce)
230pub type CredentialId = [u8; 32];
231
232/// Generate a credential ID
233pub fn generate_credential_id(
234    issuer: &Address,
235    subcode: DocSubcode,
236    subject_commitment: &[u8; 32],
237    nonce: u64,
238) -> CredentialId {
239    let mut data = Vec::with_capacity(20 + 2 + 32 + 8);
240    data.extend_from_slice(issuer.as_bytes());
241    data.extend_from_slice(&(subcode.as_u16()).to_be_bytes());
242    data.extend_from_slice(subject_commitment);
243    data.extend_from_slice(&nonce.to_be_bytes());
244    *blake3::hash(&data).as_bytes()
245}
246
247// =============================================================================
248// Commitment Scheme
249// =============================================================================
250
251/// Domain separator for commitment generation
252pub const COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC-8XX-COMMITMENT-v1";
253
254/// Generate a privacy-preserving commitment to credential attributes.
255/// commitment = blake3(domain_sep || schema_hash || canonical_attributes || salt)
256pub fn generate_commitment(
257    schema_hash: &[u8; 32],
258    canonical_attributes: &[u8],
259    salt: &[u8; 32],
260) -> [u8; 32] {
261    let mut hasher = blake3::Hasher::new();
262    hasher.update(COMMITMENT_DOMAIN_SEP);
263    hasher.update(schema_hash);
264    hasher.update(canonical_attributes);
265    hasher.update(salt);
266    *hasher.finalize().as_bytes()
267}
268
269/// Generate a subject commitment (identity binding without revealing identity)
270/// subject_commitment = blake3("SRC-8XX-SUBJECT" || subject_identifier || salt)
271pub fn generate_subject_commitment(subject_identifier: &[u8], salt: &[u8; 32]) -> [u8; 32] {
272    let mut hasher = blake3::Hasher::new();
273    hasher.update(b"SRC-8XX-SUBJECT-v1");
274    hasher.update(subject_identifier);
275    hasher.update(salt);
276    *hasher.finalize().as_bytes()
277}
278
279// =============================================================================
280// SRC-800: Identity Root
281// =============================================================================
282
283/// Identity Root record (DID-equivalent).
284/// Represents a self-sovereign identity with key binding and recovery.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct IdentityRoot {
287    /// Unique identity ID (credential_id for this identity)
288    pub identity_id: CredentialId,
289    /// Subject commitment (privacy-preserving identity binding)
290    pub subject_commitment: [u8; 32],
291    /// Primary controller address (can update keys, add controllers)
292    pub controller: Address,
293    /// Additional controller addresses (recovery, delegates)
294    pub additional_controllers: Vec<Address>,
295    /// Active public keys for this identity
296    pub keys: Vec<IdentityKey>,
297    /// Service endpoints (DID-style services)
298    pub services: Vec<ServiceEndpoint>,
299    /// Creation timestamp
300    pub created_at: Timestamp,
301    /// Last update timestamp
302    pub updated_at: Timestamp,
303    /// Identity status
304    pub status: IdentityStatus,
305    /// Schema hash for the identity structure
306    pub schema_hash: [u8; 32],
307}
308
309/// Public key associated with an identity
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct IdentityKey {
312    /// Key ID (unique within identity)
313    pub key_id: String,
314    /// Key type (Ed25519, X25519, etc.)
315    pub key_type: KeyType,
316    /// Public key bytes
317    pub public_key: [u8; 32],
318    /// Key purpose flags
319    pub purposes: Vec<KeyPurpose>,
320    /// When this key was added
321    pub added_at: Timestamp,
322    /// Optional expiry timestamp (0 = no expiry)
323    pub expires_at: Timestamp,
324    /// Is this key currently active?
325    pub active: bool,
326}
327
328/// Key types supported by identity roots
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330#[repr(u8)]
331pub enum KeyType {
332    /// Ed25519 signing key
333    Ed25519 = 0,
334    /// X25519 key exchange key
335    X25519 = 1,
336    /// Secp256k1 (for Ethereum compatibility)
337    Secp256k1 = 2,
338}
339
340/// Purpose flags for identity keys
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
342#[repr(u8)]
343pub enum KeyPurpose {
344    /// Authentication (prove control of identity)
345    Authentication = 0,
346    /// Assertion (sign credentials/claims)
347    Assertion = 1,
348    /// Key Agreement (encryption key exchange)
349    KeyAgreement = 2,
350    /// Capability Invocation (authorize actions)
351    CapabilityInvocation = 3,
352    /// Capability Delegation (delegate to others)
353    CapabilityDelegation = 4,
354}
355
356/// Service endpoint for identity (DID-style)
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct ServiceEndpoint {
359    /// Service ID (unique within identity)
360    pub service_id: String,
361    /// Service type (e.g., "CredentialRegistry", "Messaging", "Website")
362    pub service_type: String,
363    /// Service endpoint URL or URI
364    pub endpoint: String,
365    /// Optional description
366    pub description: Option<String>,
367}
368
369/// Identity status
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[repr(u8)]
372pub enum IdentityStatus {
373    /// Active and valid
374    Active = 0,
375    /// Temporarily deactivated by controller
376    Deactivated = 1,
377    /// Permanently revoked (cannot be reactivated)
378    Revoked = 2,
379}
380
381// =============================================================================
382// SRC-802: Eligibility Attestation
383// =============================================================================
384
385/// Eligibility attestation types
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
387#[repr(u8)]
388pub enum EligibilityType {
389    /// Citizenship attestation
390    Citizenship = 0,
391    /// Residency attestation
392    Residency = 1,
393    /// Age eligibility (e.g., over 18, over 21)
394    AgeEligibility = 2,
395    /// Voter eligibility
396    VoterEligibility = 3,
397    /// Civil registry attestation (birth, marriage, etc.)
398    CivilRegistry = 4,
399    /// Custom eligibility type (defined by schema)
400    Custom = 255,
401}
402
403impl EligibilityType {
404    pub fn from_u8(v: u8) -> Option<Self> {
405        match v {
406            0 => Some(EligibilityType::Citizenship),
407            1 => Some(EligibilityType::Residency),
408            2 => Some(EligibilityType::AgeEligibility),
409            3 => Some(EligibilityType::VoterEligibility),
410            4 => Some(EligibilityType::CivilRegistry),
411            255 => Some(EligibilityType::Custom),
412            _ => None,
413        }
414    }
415}
416
417/// Eligibility attestation credential (SRC-802)
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct EligibilityAttestation {
420    /// Unique credential ID
421    pub credential_id: CredentialId,
422    /// Subject wallet address (for token ownership)
423    pub subject_address: Address,
424    /// Document subcode (802)
425    pub subcode: DocSubcode,
426    /// Subject commitment (NOT the subject address)
427    pub subject_commitment: [u8; 32],
428    /// Issuer address
429    pub issuer: Address,
430    /// Jurisdiction code (ISO 3166-1/2, e.g., "US", "US-CA", "CA-BC")
431    pub jurisdiction: String,
432    /// Eligibility type
433    pub eligibility_type: EligibilityType,
434    /// Schema hash defining the attestation structure
435    pub schema_hash: [u8; 32],
436    /// Commitment to the credential content
437    pub content_commitment: [u8; 32],
438    /// Issuance timestamp
439    pub issued_at: Timestamp,
440    /// Valid from timestamp
441    pub valid_from: Timestamp,
442    /// Expiry timestamp (0 = no expiry)
443    pub expires_at: Timestamp,
444    /// Optional encrypted payload reference (hash of encrypted blob)
445    pub payload_hash: Option<[u8; 32]>,
446    /// Optional payload hint (e.g., IPFS CID, storage URL)
447    pub payload_hint: Option<String>,
448    /// Optional encryption metadata (algorithm, key commitment, nonce)
449    pub encryption_meta: Option<EncryptionMeta>,
450    /// Issuer signature over the credential
451    #[serde(with = "BigArray")]
452    pub issuer_signature: [u8; 64],
453    /// Key ID used for signing (for key rotation support)
454    pub issuer_key_id: String,
455    /// Revocation status
456    pub revocation_status: RevocationStatus,
457    /// If superseded, the new credential ID
458    pub superseded_by: Option<CredentialId>,
459}
460
461// =============================================================================
462// SRC-805: Revocation / Status Update
463// =============================================================================
464
465/// Revocation status for credentials
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
467#[repr(u8)]
468pub enum RevocationStatus {
469    /// Credential is active and valid
470    Active = 0,
471    /// Credential is suspended (can be reactivated)
472    Suspended = 1,
473    /// Credential is revoked (permanent)
474    Revoked = 2,
475    /// Credential is superseded by a newer version
476    Superseded = 3,
477    /// Credential has expired
478    Expired = 4,
479}
480
481impl RevocationStatus {
482    pub fn from_u8(v: u8) -> Option<Self> {
483        match v {
484            0 => Some(RevocationStatus::Active),
485            1 => Some(RevocationStatus::Suspended),
486            2 => Some(RevocationStatus::Revoked),
487            3 => Some(RevocationStatus::Superseded),
488            4 => Some(RevocationStatus::Expired),
489            _ => None,
490        }
491    }
492
493    pub fn is_valid(&self) -> bool {
494        matches!(self, RevocationStatus::Active)
495    }
496}
497
498/// Revocation reason codes
499#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
500#[repr(u8)]
501pub enum RevocationReason {
502    /// Unspecified reason
503    Unspecified = 0,
504    /// Key compromise
505    KeyCompromise = 1,
506    /// Issuer compromise
507    IssuerCompromise = 2,
508    /// Affiliation changed
509    AffiliationChanged = 3,
510    /// Superseded by new credential
511    Superseded = 4,
512    /// Cessation of operation
513    CessationOfOperation = 5,
514    /// Certificate hold (temporary suspension)
515    CertificateHold = 6,
516    /// Privilege withdrawn
517    PrivilegeWithdrawn = 7,
518    /// Credential expired
519    Expired = 8,
520    /// Fraudulent issuance
521    FraudulentIssuance = 9,
522}
523
524impl RevocationReason {
525    pub fn from_u8(v: u8) -> Option<Self> {
526        match v {
527            0 => Some(RevocationReason::Unspecified),
528            1 => Some(RevocationReason::KeyCompromise),
529            2 => Some(RevocationReason::IssuerCompromise),
530            3 => Some(RevocationReason::AffiliationChanged),
531            4 => Some(RevocationReason::Superseded),
532            5 => Some(RevocationReason::CessationOfOperation),
533            6 => Some(RevocationReason::CertificateHold),
534            7 => Some(RevocationReason::PrivilegeWithdrawn),
535            8 => Some(RevocationReason::Expired),
536            9 => Some(RevocationReason::FraudulentIssuance),
537            _ => None,
538        }
539    }
540}
541
542/// Revocation record (SRC-805)
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544pub struct RevocationRecord {
545    /// The credential being revoked/updated
546    pub credential_id: CredentialId,
547    /// New status
548    pub status: RevocationStatus,
549    /// Reason for revocation/suspension
550    pub reason: RevocationReason,
551    /// Optional reason details
552    pub reason_details: Option<String>,
553    /// Issuer who performed the revocation
554    pub revoker: Address,
555    /// Timestamp of revocation
556    pub revoked_at: Timestamp,
557    /// Block height of revocation
558    pub revoked_at_height: BlockHeight,
559    /// If superseded, the new credential ID
560    pub superseded_by: Option<CredentialId>,
561    /// Signature over the revocation
562    #[serde(with = "BigArray")]
563    pub signature: [u8; 64],
564}
565
566// =============================================================================
567// SRC-81X: Academic & Professional Credentials
568// =============================================================================
569
570/// Base structure for academic/professional credentials
571#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
572pub struct AcademicCredential {
573    /// Unique credential ID
574    pub credential_id: CredentialId,
575    /// Subject wallet address (for token ownership)
576    pub subject_address: Address,
577    /// Document subcode (810, 811, 812, or 813)
578    pub subcode: DocSubcode,
579    /// Subject commitment (NOT the subject's real identity)
580    pub subject_commitment: [u8; 32],
581    /// Issuer address (institution)
582    pub issuer: Address,
583    /// Institution identifier (e.g., "UCLA", "MIT")
584    pub institution_id: String,
585    /// Jurisdiction/country (ISO 3166-1 alpha-2)
586    pub jurisdiction: String,
587    /// Schema hash defining the credential structure
588    pub schema_hash: [u8; 32],
589    /// Commitment to the credential content
590    pub content_commitment: [u8; 32],
591    /// Credential metadata (non-PII, public info)
592    pub metadata: CredentialMetadata,
593    /// Issuance timestamp
594    pub issued_at: Timestamp,
595    /// Valid from timestamp
596    pub valid_from: Timestamp,
597    /// Expiry timestamp (0 = no expiry for most degrees)
598    pub expires_at: Timestamp,
599    /// Optional encrypted payload reference
600    pub payload_hash: Option<[u8; 32]>,
601    /// Optional payload hint (storage location)
602    pub payload_hint: Option<String>,
603    /// Optional encryption metadata (algorithm, key commitment, nonce)
604    pub encryption_meta: Option<EncryptionMeta>,
605    /// Issuer signature
606    #[serde(with = "BigArray")]
607    pub issuer_signature: [u8; 64],
608    /// Key ID used for signing
609    pub issuer_key_id: String,
610    /// Revocation status
611    pub revocation_status: RevocationStatus,
612    /// If superseded, the new credential ID
613    pub superseded_by: Option<CredentialId>,
614}
615
616/// Non-sensitive metadata for credentials
617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
618pub struct CredentialMetadata {
619    /// Credential title (e.g., "Bachelor of Science in Computer Science")
620    pub title: String,
621    /// Credential type within subcode (e.g., "undergraduate_transcript", "doctoral_degree")
622    pub credential_type: String,
623    /// Program/field of study (optional, can be commitment instead)
624    pub program: Option<String>,
625    /// Issue date in human-readable format (e.g., "2024-05-15")
626    pub issue_date: String,
627    /// Optional completion date
628    pub completion_date: Option<String>,
629    /// Additional public attributes (non-PII)
630    pub attributes: Vec<CredentialAttribute>,
631}
632
633/// Public attribute for credentials (non-PII only)
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635pub struct CredentialAttribute {
636    /// Attribute name
637    pub name: String,
638    /// Attribute value (must be non-PII)
639    pub value: String,
640}
641
642// =============================================================================
643// DocClass Operations
644// =============================================================================
645
646/// Operations for DocClass transactions
647#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
648#[repr(u8)]
649pub enum DocClassOperation {
650    // Identity Root operations (800)
651    /// Create a new identity root
652    CreateIdentityRoot = 0,
653    /// Add a key to identity
654    AddKey = 1,
655    /// Remove a key from identity
656    RemoveKey = 2,
657    /// Rotate a key (add new, remove old)
658    RotateKey = 3,
659    /// Add a controller
660    AddController = 4,
661    /// Remove a controller
662    RemoveController = 5,
663    /// Update service endpoint
664    UpdateService = 6,
665    /// Deactivate identity
666    DeactivateIdentity = 7,
667    /// Reactivate identity
668    ReactivateIdentity = 8,
669
670    // Credential operations (802, 810-813)
671    /// Issue a new credential
672    IssueCredential = 10,
673    /// Update credential (metadata only, not content)
674    UpdateCredential = 11,
675
676    // Revocation operations (805)
677    /// Revoke a credential
678    RevokeCredential = 20,
679    /// Suspend a credential
680    SuspendCredential = 21,
681    /// Reactivate a suspended credential
682    ReactivateCredential = 22,
683    /// Supersede credential with new version
684    SupersedeCredential = 23,
685
686    // Issuer registry operations
687    /// Register as issuer
688    RegisterIssuer = 30,
689    /// Update issuer info
690    UpdateIssuer = 31,
691    /// Rotate issuer key
692    RotateIssuerKey = 32,
693    /// Deactivate issuer
694    DeactivateIssuer = 33,
695}
696
697impl DocClassOperation {
698    pub fn from_u8(v: u8) -> Option<Self> {
699        match v {
700            0 => Some(DocClassOperation::CreateIdentityRoot),
701            1 => Some(DocClassOperation::AddKey),
702            2 => Some(DocClassOperation::RemoveKey),
703            3 => Some(DocClassOperation::RotateKey),
704            4 => Some(DocClassOperation::AddController),
705            5 => Some(DocClassOperation::RemoveController),
706            6 => Some(DocClassOperation::UpdateService),
707            7 => Some(DocClassOperation::DeactivateIdentity),
708            8 => Some(DocClassOperation::ReactivateIdentity),
709            10 => Some(DocClassOperation::IssueCredential),
710            11 => Some(DocClassOperation::UpdateCredential),
711            20 => Some(DocClassOperation::RevokeCredential),
712            21 => Some(DocClassOperation::SuspendCredential),
713            22 => Some(DocClassOperation::ReactivateCredential),
714            23 => Some(DocClassOperation::SupersedeCredential),
715            30 => Some(DocClassOperation::RegisterIssuer),
716            31 => Some(DocClassOperation::UpdateIssuer),
717            32 => Some(DocClassOperation::RotateIssuerKey),
718            33 => Some(DocClassOperation::DeactivateIssuer),
719            _ => None,
720        }
721    }
722}
723
724/// Transaction data for DocClass operations
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726pub struct DocClassTxData {
727    /// Operation type
728    pub operation: DocClassOperation,
729    /// Document subcode (for context)
730    pub subcode: DocSubcode,
731    /// Operation-specific data (serialized)
732    pub data: Vec<u8>,
733    /// Token recipient address - the owner of the minted token
734    pub recipient: crate::Address,
735}
736
737// =============================================================================
738// Issuer Registry Types
739// =============================================================================
740
741/// Registered issuer for DocClass credentials
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
743pub struct DocClassIssuer {
744    /// Issuer address
745    pub address: Address,
746    /// Issuer name/organization
747    pub name: String,
748    /// Issuer type
749    pub issuer_type: DocClassIssuerType,
750    /// Jurisdictions the issuer can issue for (ISO 3166-1/2 codes)
751    pub jurisdictions: Vec<String>,
752    /// Document subcodes the issuer is authorized for
753    pub authorized_subcodes: Vec<DocSubcode>,
754    /// Issuer's active public keys
755    pub keys: Vec<IssuerKey>,
756    /// Registration timestamp
757    pub registered_at: Timestamp,
758    /// Last update timestamp
759    pub updated_at: Timestamp,
760    /// Issuer status
761    pub status: DocClassIssuerStatus,
762    /// Optional bond/stake amount (for slashing in future)
763    pub stake_amount: Balance,
764    /// Optional metadata (JSON)
765    pub metadata: Option<String>,
766}
767
768/// Issuer key for signing credentials
769#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
770pub struct IssuerKey {
771    /// Key ID (unique within issuer)
772    pub key_id: String,
773    /// Public key bytes
774    pub public_key: [u8; 32],
775    /// Key type
776    pub key_type: KeyType,
777    /// When this key was added
778    pub added_at: Timestamp,
779    /// Optional expiry (0 = no expiry)
780    pub expires_at: Timestamp,
781    /// Is this key currently active?
782    pub active: bool,
783    /// Is this the primary key?
784    pub is_primary: bool,
785}
786
787/// Types of DocClass issuers
788#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
789#[repr(u8)]
790pub enum DocClassIssuerType {
791    /// Government agency (for 80X credentials)
792    Government = 0,
793    /// Educational institution (for 81X credentials)
794    Educational = 1,
795    /// Professional licensing body
796    Professional = 2,
797    /// Corporate entity
798    Corporate = 3,
799    /// Healthcare provider
800    Healthcare = 4,
801    /// Legal entity
802    Legal = 5,
803    /// Self-sovereign (for identity roots only)
804    SelfSovereign = 6,
805}
806
807impl DocClassIssuerType {
808    pub fn from_u8(v: u8) -> Option<Self> {
809        match v {
810            0 => Some(DocClassIssuerType::Government),
811            1 => Some(DocClassIssuerType::Educational),
812            2 => Some(DocClassIssuerType::Professional),
813            3 => Some(DocClassIssuerType::Corporate),
814            4 => Some(DocClassIssuerType::Healthcare),
815            5 => Some(DocClassIssuerType::Legal),
816            6 => Some(DocClassIssuerType::SelfSovereign),
817            _ => None,
818        }
819    }
820
821    /// Check if this issuer type can issue the given subcode
822    pub fn can_issue(&self, subcode: DocSubcode) -> bool {
823        match self {
824            DocClassIssuerType::Government => {
825                matches!(
826                    subcode,
827                    DocSubcode::EligibilityAttestation
828                        | DocSubcode::GovernmentId
829                        | DocSubcode::Revocation
830                )
831            }
832            DocClassIssuerType::Educational => {
833                matches!(
834                    subcode,
835                    DocSubcode::AcademicTranscript
836                        | DocSubcode::Diploma
837                        | DocSubcode::EnrollmentVerification
838                        | DocSubcode::Revocation
839                )
840            }
841            DocClassIssuerType::Professional => {
842                matches!(
843                    subcode,
844                    DocSubcode::ProfessionalLicense | DocSubcode::Revocation
845                )
846            }
847            DocClassIssuerType::Corporate => {
848                matches!(
849                    subcode,
850                    DocSubcode::Employment | DocSubcode::Revocation
851                )
852            }
853            DocClassIssuerType::SelfSovereign => {
854                matches!(subcode, DocSubcode::IdentityRoot | DocSubcode::Subject)
855            }
856            _ => false,
857        }
858    }
859}
860
861/// Issuer status
862#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
863#[repr(u8)]
864pub enum DocClassIssuerStatus {
865    /// Active and can issue credentials
866    Active = 0,
867    /// Suspended (cannot issue, but existing credentials remain valid)
868    Suspended = 1,
869    /// Revoked (cannot issue, and credentials should be treated with caution)
870    Revoked = 2,
871}
872
873impl DocClassIssuerStatus {
874    pub fn from_u8(v: u8) -> Option<Self> {
875        match v {
876            0 => Some(DocClassIssuerStatus::Active),
877            1 => Some(DocClassIssuerStatus::Suspended),
878            2 => Some(DocClassIssuerStatus::Revoked),
879            _ => None,
880        }
881    }
882
883    pub fn can_issue(&self) -> bool {
884        matches!(self, DocClassIssuerStatus::Active)
885    }
886}
887
888// =============================================================================
889// ZK Proof Inputs (for future voting integration)
890// =============================================================================
891
892/// Data structure for ZK proof inputs.
893/// This defines what fields future ZK circuits will consume to prove
894/// eligibility without revealing identity.
895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
896pub struct ZkProofInputs {
897    /// Credential ID being proven
898    pub credential_id: CredentialId,
899    /// Issuer's public key used for signing
900    pub issuer_public_key: [u8; 32],
901    /// Issuer key ID (for key rotation support)
902    pub issuer_key_id: String,
903    /// Content commitment
904    pub content_commitment: [u8; 32],
905    /// Subject commitment
906    pub subject_commitment: [u8; 32],
907    /// Jurisdiction code
908    pub jurisdiction: String,
909    /// Eligibility type (for 802 credentials)
910    pub eligibility_type: Option<EligibilityType>,
911    /// Valid from timestamp
912    pub valid_from: Timestamp,
913    /// Expiry timestamp
914    pub expires_at: Timestamp,
915    /// Revocation check hook (merkle root or on-chain state root)
916    pub revocation_merkle_root: Option<[u8; 32]>,
917    /// Current block height (for freshness)
918    pub current_block_height: BlockHeight,
919    /// Issuer signature
920    #[serde(with = "BigArray")]
921    pub issuer_signature: [u8; 64],
922}
923
924/// Nullifier for preventing double-use (e.g., double voting)
925/// nullifier = blake3("SRC-8XX-NULLIFIER" || credential_id || context || secret)
926pub fn generate_nullifier(credential_id: &CredentialId, context: &[u8], secret: &[u8; 32]) -> [u8; 32] {
927    let mut hasher = blake3::Hasher::new();
928    hasher.update(b"SRC-8XX-NULLIFIER-v1");
929    hasher.update(credential_id);
930    hasher.update(context);
931    hasher.update(secret);
932    *hasher.finalize().as_bytes()
933}
934
935// =============================================================================
936// Canonical JSON for Commitments
937// =============================================================================
938
939/// Rules for canonical JSON encoding (for deterministic commitments):
940/// 1. Keys are sorted lexicographically (Unicode code points)
941/// 2. No whitespace between elements
942/// 3. UTF-8 encoding
943/// 4. Numbers: integers as-is, floats with minimal representation
944/// 5. Strings: escaped as per JSON spec
945/// 6. Arrays: elements in order, no trailing comma
946/// 7. Objects: sorted keys, no trailing comma
947/// 8. Null values are included (not omitted)
948///
949/// Example: {"age":21,"country":"US","name":"John Doe"}
950pub mod canonical {
951    use serde::Serialize;
952    use std::collections::BTreeMap;
953
954    /// Serialize a value to canonical JSON bytes
955    pub fn to_canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
956        // First serialize to serde_json::Value to normalize
957        let json_value = serde_json::to_value(value)?;
958        // Then serialize with sorted keys and no whitespace
959        let canonical = canonical_json_value(&json_value);
960        Ok(canonical.into_bytes())
961    }
962
963    fn canonical_json_value(value: &serde_json::Value) -> String {
964        match value {
965            serde_json::Value::Null => "null".to_string(),
966            serde_json::Value::Bool(b) => b.to_string(),
967            serde_json::Value::Number(n) => n.to_string(),
968            serde_json::Value::String(s) => serde_json::to_string(s).unwrap(),
969            serde_json::Value::Array(arr) => {
970                let elements: Vec<String> = arr.iter().map(canonical_json_value).collect();
971                format!("[{}]", elements.join(","))
972            }
973            serde_json::Value::Object(obj) => {
974                // Sort keys lexicographically
975                let sorted: BTreeMap<_, _> = obj.iter().collect();
976                let pairs: Vec<String> = sorted
977                    .iter()
978                    .map(|(k, v)| format!("{}:{}", serde_json::to_string(k).unwrap(), canonical_json_value(v)))
979                    .collect();
980                format!("{{{}}}", pairs.join(","))
981            }
982        }
983    }
984}
985
986// =============================================================================
987// Events
988// =============================================================================
989
990/// Events emitted by DocClass operations
991#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
992pub enum DocClassEvent {
993    // Identity Root events
994    IdentityRootCreated {
995        identity_id: CredentialId,
996        controller: Address,
997        subject_commitment: [u8; 32],
998    },
999    KeyAdded {
1000        identity_id: CredentialId,
1001        key_id: String,
1002        key_type: KeyType,
1003    },
1004    KeyRemoved {
1005        identity_id: CredentialId,
1006        key_id: String,
1007    },
1008    KeyRotated {
1009        identity_id: CredentialId,
1010        old_key_id: String,
1011        new_key_id: String,
1012    },
1013    ControllerAdded {
1014        identity_id: CredentialId,
1015        controller: Address,
1016    },
1017    ControllerRemoved {
1018        identity_id: CredentialId,
1019        controller: Address,
1020    },
1021    ServiceUpdated {
1022        identity_id: CredentialId,
1023        service_id: String,
1024    },
1025    IdentityStatusChanged {
1026        identity_id: CredentialId,
1027        new_status: IdentityStatus,
1028    },
1029
1030    // Credential events
1031    CredentialIssued {
1032        credential_id: CredentialId,
1033        subcode: DocSubcode,
1034        issuer: Address,
1035        jurisdiction: String,
1036        subject_commitment: [u8; 32],
1037        schema_hash: [u8; 32],
1038        expires_at: Timestamp,
1039    },
1040    CredentialRevoked {
1041        credential_id: CredentialId,
1042        issuer: Address,
1043        reason: RevocationReason,
1044        timestamp: Timestamp,
1045    },
1046    CredentialSuspended {
1047        credential_id: CredentialId,
1048        issuer: Address,
1049        reason: RevocationReason,
1050        timestamp: Timestamp,
1051    },
1052    CredentialReactivated {
1053        credential_id: CredentialId,
1054        issuer: Address,
1055        timestamp: Timestamp,
1056    },
1057    CredentialSuperseded {
1058        old_credential_id: CredentialId,
1059        new_credential_id: CredentialId,
1060        issuer: Address,
1061        timestamp: Timestamp,
1062    },
1063
1064    // Issuer registry events
1065    IssuerRegistered {
1066        issuer: Address,
1067        issuer_type: DocClassIssuerType,
1068        jurisdictions: Vec<String>,
1069        subcodes: Vec<DocSubcode>,
1070    },
1071    IssuerUpdated {
1072        issuer: Address,
1073    },
1074    IssuerKeyRotated {
1075        issuer: Address,
1076        old_key_id: String,
1077        new_key_id: String,
1078    },
1079    IssuerStatusChanged {
1080        issuer: Address,
1081        new_status: DocClassIssuerStatus,
1082    },
1083}
1084
1085// =============================================================================
1086// Schema Registry (predefined schemas)
1087// =============================================================================
1088
1089/// Predefined schema hashes for common credential types.
1090/// In production, these would be registered in a schema registry.
1091pub mod schemas {
1092    /// Schema for citizenship eligibility attestation
1093    pub const CITIZENSHIP_SCHEMA: [u8; 32] = [
1094        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1095        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1096        0x43, 0x49, 0x54, 0x49, 0x5a, 0x45, 0x4e, 0x53,
1097        0x48, 0x49, 0x50, 0x5f, 0x56, 0x31, 0x00, 0x00,
1098    ];
1099
1100    /// Schema for residency eligibility attestation
1101    pub const RESIDENCY_SCHEMA: [u8; 32] = [
1102        0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1103        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1104        0x52, 0x45, 0x53, 0x49, 0x44, 0x45, 0x4e, 0x43,
1105        0x59, 0x5f, 0x56, 0x31, 0x00, 0x00, 0x00, 0x00,
1106    ];
1107
1108    /// Schema for age eligibility attestation
1109    pub const AGE_ELIGIBILITY_SCHEMA: [u8; 32] = [
1110        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1111        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1112        0x41, 0x47, 0x45, 0x5f, 0x45, 0x4c, 0x49, 0x47,
1113        0x5f, 0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
1114    ];
1115
1116    /// Schema for academic transcript
1117    pub const TRANSCRIPT_SCHEMA: [u8; 32] = [
1118        0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1119        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1120        0x54, 0x52, 0x41, 0x4e, 0x53, 0x43, 0x52, 0x49,
1121        0x50, 0x54, 0x5f, 0x56, 0x31, 0x00, 0x00, 0x00,
1122    ];
1123
1124    /// Schema for diploma/degree
1125    pub const DIPLOMA_SCHEMA: [u8; 32] = [
1126        0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1127        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1128        0x44, 0x49, 0x50, 0x4c, 0x4f, 0x4d, 0x41, 0x5f,
1129        0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1130    ];
1131
1132    /// Schema for professional license
1133    pub const LICENSE_SCHEMA: [u8; 32] = [
1134        0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1135        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1136        0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x5f,
1137        0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1138    ];
1139
1140    /// Schema for identity root
1141    pub const IDENTITY_ROOT_SCHEMA: [u8; 32] = [
1142        0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1143        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1144        0x49, 0x44, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f,
1145        0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1146    ];
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151    use super::*;
1152
1153    #[test]
1154    fn test_subcode_classification() {
1155        assert!(DocSubcode::IdentityRoot.is_identity_class());
1156        assert!(DocSubcode::EligibilityAttestation.is_identity_class());
1157        assert!(!DocSubcode::AcademicTranscript.is_identity_class());
1158
1159        assert!(!DocSubcode::IdentityRoot.is_academic_class());
1160        assert!(DocSubcode::AcademicTranscript.is_academic_class());
1161        assert!(DocSubcode::Diploma.is_academic_class());
1162        assert!(DocSubcode::ProfessionalLicense.is_academic_class());
1163    }
1164
1165    #[test]
1166    fn test_commitment_determinism() {
1167        let schema_hash = [1u8; 32];
1168        let attributes = b"test_attributes";
1169        let salt = [2u8; 32];
1170
1171        let c1 = generate_commitment(&schema_hash, attributes, &salt);
1172        let c2 = generate_commitment(&schema_hash, attributes, &salt);
1173        assert_eq!(c1, c2);
1174
1175        // Different salt should produce different commitment
1176        let different_salt = [3u8; 32];
1177        let c3 = generate_commitment(&schema_hash, attributes, &different_salt);
1178        assert_ne!(c1, c3);
1179    }
1180
1181    #[test]
1182    fn test_credential_id_generation() {
1183        let issuer = Address::new([1u8; 20]);
1184        let subcode = DocSubcode::EligibilityAttestation;
1185        let subject_commitment = [2u8; 32];
1186        let nonce = 12345u64;
1187
1188        let id1 = generate_credential_id(&issuer, subcode, &subject_commitment, nonce);
1189        let id2 = generate_credential_id(&issuer, subcode, &subject_commitment, nonce);
1190        assert_eq!(id1, id2);
1191
1192        // Different nonce should produce different ID
1193        let id3 = generate_credential_id(&issuer, subcode, &subject_commitment, nonce + 1);
1194        assert_ne!(id1, id3);
1195    }
1196
1197    #[test]
1198    fn test_nullifier_generation() {
1199        let credential_id = [1u8; 32];
1200        let context = b"election_2024";
1201        let secret = [2u8; 32];
1202
1203        let n1 = generate_nullifier(&credential_id, context, &secret);
1204        let n2 = generate_nullifier(&credential_id, context, &secret);
1205        assert_eq!(n1, n2);
1206
1207        // Different context should produce different nullifier
1208        let n3 = generate_nullifier(&credential_id, b"election_2025", &secret);
1209        assert_ne!(n1, n3);
1210    }
1211
1212    #[test]
1213    fn test_canonical_json() {
1214        use serde_json::json;
1215
1216        let value = json!({
1217            "name": "John",
1218            "age": 21,
1219            "country": "US"
1220        });
1221
1222        let canonical = canonical::to_canonical_json(&value).unwrap();
1223        let canonical_str = String::from_utf8(canonical).unwrap();
1224
1225        // Keys should be sorted alphabetically
1226        assert_eq!(canonical_str, r#"{"age":21,"country":"US","name":"John"}"#);
1227    }
1228
1229    #[test]
1230    fn test_issuer_type_permissions() {
1231        assert!(DocClassIssuerType::Government.can_issue(DocSubcode::EligibilityAttestation));
1232        assert!(!DocClassIssuerType::Government.can_issue(DocSubcode::AcademicTranscript));
1233
1234        assert!(DocClassIssuerType::Educational.can_issue(DocSubcode::Diploma));
1235        assert!(DocClassIssuerType::Educational.can_issue(DocSubcode::AcademicTranscript));
1236        assert!(!DocClassIssuerType::Educational.can_issue(DocSubcode::EligibilityAttestation));
1237
1238        assert!(DocClassIssuerType::Professional.can_issue(DocSubcode::ProfessionalLicense));
1239        assert!(!DocClassIssuerType::Professional.can_issue(DocSubcode::Diploma));
1240
1241        assert!(DocClassIssuerType::SelfSovereign.can_issue(DocSubcode::IdentityRoot));
1242        assert!(!DocClassIssuerType::SelfSovereign.can_issue(DocSubcode::EligibilityAttestation));
1243    }
1244
1245    #[test]
1246    fn test_revocation_status() {
1247        assert!(RevocationStatus::Active.is_valid());
1248        assert!(!RevocationStatus::Revoked.is_valid());
1249        assert!(!RevocationStatus::Suspended.is_valid());
1250        assert!(!RevocationStatus::Superseded.is_valid());
1251        assert!(!RevocationStatus::Expired.is_valid());
1252    }
1253}