1use serde::{Deserialize, Serialize};
21use serde_big_array::BigArray;
22
23use crate::agreement::{EncryptionAlgorithm, EncryptionMeta};
24use crate::{Address, Balance, BlockHeight, Timestamp};
25
26pub type SubjectId = [u8; 32];
32
33pub type PolicyId = [u8; 32];
35
36pub type ClaimId = [u8; 32];
38
39pub type ProofId = [u8; 32];
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[repr(u8)]
50pub enum ClaimType {
51 Identity = 0,
53 Eligibility = 1,
55 Education = 2,
57 License = 3,
59 Employment = 4,
61 Healthcare = 5,
63 Financial = 6,
65 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[repr(u16)]
106pub enum DocSubcode {
107 Subject = 801,
110 IssuerRegistry = 802,
112 Policy = 803,
114 Claim = 804,
116 Revocation = 805,
118 ProofLog = 806,
120
121 IdentityRoot = 800,
124 EligibilityAttestation = 807,
126
127 AcademicTranscript = 810,
130 Diploma = 811,
132 EnrollmentVerification = 812,
134 ProfessionalLicense = 813,
136 GovernmentId = 814,
138 Employment = 815,
140}
141
142impl DocSubcode {
143 pub fn is_core_standard(&self) -> bool {
145 (*self as u16) >= 800 && (*self as u16) < 810
146 }
147
148 pub fn is_identity_class(&self) -> bool {
150 self.is_core_standard()
151 }
152
153 pub fn is_domain_claim(&self) -> bool {
155 (*self as u16) >= 810 && (*self as u16) < 820
156 }
157
158 pub fn is_academic_class(&self) -> bool {
160 self.is_domain_claim()
161 }
162
163 pub fn as_u16(&self) -> u16 {
165 *self as u16
166 }
167
168 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 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 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
224pub type CredentialId = [u8; 32];
231
232pub 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
247pub const COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC-8XX-COMMITMENT-v1";
253
254pub 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
269pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct IdentityRoot {
287 pub identity_id: CredentialId,
289 pub subject_commitment: [u8; 32],
291 pub controller: Address,
293 pub additional_controllers: Vec<Address>,
295 pub keys: Vec<IdentityKey>,
297 pub services: Vec<ServiceEndpoint>,
299 pub created_at: Timestamp,
301 pub updated_at: Timestamp,
303 pub status: IdentityStatus,
305 pub schema_hash: [u8; 32],
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct IdentityKey {
312 pub key_id: String,
314 pub key_type: KeyType,
316 pub public_key: [u8; 32],
318 pub purposes: Vec<KeyPurpose>,
320 pub added_at: Timestamp,
322 pub expires_at: Timestamp,
324 pub active: bool,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330#[repr(u8)]
331pub enum KeyType {
332 Ed25519 = 0,
334 X25519 = 1,
336 Secp256k1 = 2,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
342#[repr(u8)]
343pub enum KeyPurpose {
344 Authentication = 0,
346 Assertion = 1,
348 KeyAgreement = 2,
350 CapabilityInvocation = 3,
352 CapabilityDelegation = 4,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct ServiceEndpoint {
359 pub service_id: String,
361 pub service_type: String,
363 pub endpoint: String,
365 pub description: Option<String>,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[repr(u8)]
372pub enum IdentityStatus {
373 Active = 0,
375 Deactivated = 1,
377 Revoked = 2,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
387#[repr(u8)]
388pub enum EligibilityType {
389 Citizenship = 0,
391 Residency = 1,
393 AgeEligibility = 2,
395 VoterEligibility = 3,
397 CivilRegistry = 4,
399 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct EligibilityAttestation {
420 pub credential_id: CredentialId,
422 pub subject_address: Address,
424 pub subcode: DocSubcode,
426 pub subject_commitment: [u8; 32],
428 pub issuer: Address,
430 pub jurisdiction: String,
432 pub eligibility_type: EligibilityType,
434 pub schema_hash: [u8; 32],
436 pub content_commitment: [u8; 32],
438 pub issued_at: Timestamp,
440 pub valid_from: Timestamp,
442 pub expires_at: Timestamp,
444 pub payload_hash: Option<[u8; 32]>,
446 pub payload_hint: Option<String>,
448 pub encryption_meta: Option<EncryptionMeta>,
450 #[serde(with = "BigArray")]
452 pub issuer_signature: [u8; 64],
453 pub issuer_key_id: String,
455 pub revocation_status: RevocationStatus,
457 pub superseded_by: Option<CredentialId>,
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
467#[repr(u8)]
468pub enum RevocationStatus {
469 Active = 0,
471 Suspended = 1,
473 Revoked = 2,
475 Superseded = 3,
477 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
500#[repr(u8)]
501pub enum RevocationReason {
502 Unspecified = 0,
504 KeyCompromise = 1,
506 IssuerCompromise = 2,
508 AffiliationChanged = 3,
510 Superseded = 4,
512 CessationOfOperation = 5,
514 CertificateHold = 6,
516 PrivilegeWithdrawn = 7,
518 Expired = 8,
520 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
544pub struct RevocationRecord {
545 pub credential_id: CredentialId,
547 pub status: RevocationStatus,
549 pub reason: RevocationReason,
551 pub reason_details: Option<String>,
553 pub revoker: Address,
555 pub revoked_at: Timestamp,
557 pub revoked_at_height: BlockHeight,
559 pub superseded_by: Option<CredentialId>,
561 #[serde(with = "BigArray")]
563 pub signature: [u8; 64],
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
572pub struct AcademicCredential {
573 pub credential_id: CredentialId,
575 pub subject_address: Address,
577 pub subcode: DocSubcode,
579 pub subject_commitment: [u8; 32],
581 pub issuer: Address,
583 pub institution_id: String,
585 pub jurisdiction: String,
587 pub schema_hash: [u8; 32],
589 pub content_commitment: [u8; 32],
591 pub metadata: CredentialMetadata,
593 pub issued_at: Timestamp,
595 pub valid_from: Timestamp,
597 pub expires_at: Timestamp,
599 pub payload_hash: Option<[u8; 32]>,
601 pub payload_hint: Option<String>,
603 pub encryption_meta: Option<EncryptionMeta>,
605 #[serde(with = "BigArray")]
607 pub issuer_signature: [u8; 64],
608 pub issuer_key_id: String,
610 pub revocation_status: RevocationStatus,
612 pub superseded_by: Option<CredentialId>,
614}
615
616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
618pub struct CredentialMetadata {
619 pub title: String,
621 pub credential_type: String,
623 pub program: Option<String>,
625 pub issue_date: String,
627 pub completion_date: Option<String>,
629 pub attributes: Vec<CredentialAttribute>,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635pub struct CredentialAttribute {
636 pub name: String,
638 pub value: String,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
648#[repr(u8)]
649pub enum DocClassOperation {
650 CreateIdentityRoot = 0,
653 AddKey = 1,
655 RemoveKey = 2,
657 RotateKey = 3,
659 AddController = 4,
661 RemoveController = 5,
663 UpdateService = 6,
665 DeactivateIdentity = 7,
667 ReactivateIdentity = 8,
669
670 IssueCredential = 10,
673 UpdateCredential = 11,
675
676 RevokeCredential = 20,
679 SuspendCredential = 21,
681 ReactivateCredential = 22,
683 SupersedeCredential = 23,
685
686 RegisterIssuer = 30,
689 UpdateIssuer = 31,
691 RotateIssuerKey = 32,
693 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726pub struct DocClassTxData {
727 pub operation: DocClassOperation,
729 pub subcode: DocSubcode,
731 pub data: Vec<u8>,
733 pub recipient: crate::Address,
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
743pub struct DocClassIssuer {
744 pub address: Address,
746 pub name: String,
748 pub issuer_type: DocClassIssuerType,
750 pub jurisdictions: Vec<String>,
752 pub authorized_subcodes: Vec<DocSubcode>,
754 pub keys: Vec<IssuerKey>,
756 pub registered_at: Timestamp,
758 pub updated_at: Timestamp,
760 pub status: DocClassIssuerStatus,
762 pub stake_amount: Balance,
764 pub metadata: Option<String>,
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
770pub struct IssuerKey {
771 pub key_id: String,
773 pub public_key: [u8; 32],
775 pub key_type: KeyType,
777 pub added_at: Timestamp,
779 pub expires_at: Timestamp,
781 pub active: bool,
783 pub is_primary: bool,
785}
786
787#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
789#[repr(u8)]
790pub enum DocClassIssuerType {
791 Government = 0,
793 Educational = 1,
795 Professional = 2,
797 Corporate = 3,
799 Healthcare = 4,
801 Legal = 5,
803 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
863#[repr(u8)]
864pub enum DocClassIssuerStatus {
865 Active = 0,
867 Suspended = 1,
869 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
896pub struct ZkProofInputs {
897 pub credential_id: CredentialId,
899 pub issuer_public_key: [u8; 32],
901 pub issuer_key_id: String,
903 pub content_commitment: [u8; 32],
905 pub subject_commitment: [u8; 32],
907 pub jurisdiction: String,
909 pub eligibility_type: Option<EligibilityType>,
911 pub valid_from: Timestamp,
913 pub expires_at: Timestamp,
915 pub revocation_merkle_root: Option<[u8; 32]>,
917 pub current_block_height: BlockHeight,
919 #[serde(with = "BigArray")]
921 pub issuer_signature: [u8; 64],
922}
923
924pub 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
935pub mod canonical {
951 use serde::Serialize;
952 use std::collections::BTreeMap;
953
954 pub fn to_canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
956 let json_value = serde_json::to_value(value)?;
958 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
992pub enum DocClassEvent {
993 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 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 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
1085pub mod schemas {
1092 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 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 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 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 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 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 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 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 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 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 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}