Skip to main content

cpop_protocol/war/profiles/
vc.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! W3C Verifiable Credential profile — projects an EAR token into a VC 2.0.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8type Result<T> = std::result::Result<T, String>;
9use crate::war::ear::{Ar4siStatus, EarToken};
10
11/// W3C Verifiable Credential 2.0 structure.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct VerifiableCredential {
14    #[serde(rename = "@context")]
15    pub context: Vec<String>,
16    #[serde(rename = "type")]
17    pub vc_type: Vec<String>,
18    pub issuer: String,
19    #[serde(rename = "validFrom")]
20    pub valid_from: String,
21    #[serde(rename = "credentialSubject")]
22    pub credential_subject: CredentialSubject,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub evidence: Option<Vec<VcEvidence>>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub proof: Option<VcProof>,
27}
28
29/// The credential subject — the author and their attestation.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CredentialSubject {
32    pub id: String,
33    #[serde(rename = "type")]
34    pub subject_type: String,
35    #[serde(rename = "processAttestation")]
36    pub process_attestation: ProcessAttestation,
37}
38
39/// Process attestation claims embedded in the credential subject.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ProcessAttestation {
42    pub status: String,
43    #[serde(rename = "trustVector", skip_serializing_if = "Option::is_none")]
44    pub trust_vector: Option<TrustVectorVc>,
45    #[serde(rename = "documentRef", skip_serializing_if = "Option::is_none")]
46    pub document_ref: Option<String>,
47    #[serde(rename = "chainDuration", skip_serializing_if = "Option::is_none")]
48    pub chain_duration: Option<String>,
49    #[serde(rename = "attestationTier", skip_serializing_if = "Option::is_none")]
50    pub attestation_tier: Option<String>,
51}
52
53/// Trust vector in VC JSON-LD format.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct TrustVectorVc {
56    pub instance_identity: i8,
57    pub configuration: i8,
58    pub executables: i8,
59    pub file_system: i8,
60    pub hardware: i8,
61    pub runtime_opaque: i8,
62    pub storage_opaque: i8,
63    pub sourced_data: i8,
64}
65
66/// Evidence entry in the VC.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct VcEvidence {
69    #[serde(rename = "type")]
70    pub evidence_type: String,
71    pub verifier: String,
72    #[serde(rename = "sealHash", skip_serializing_if = "Option::is_none")]
73    pub seal_hash: Option<String>,
74}
75
76/// Data integrity proof on the VC.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct VcProof {
79    #[serde(rename = "type")]
80    pub proof_type: String,
81    pub cryptosuite: String,
82    #[serde(rename = "verificationMethod")]
83    pub verification_method: String,
84    #[serde(rename = "proofPurpose")]
85    pub proof_purpose: String,
86    #[serde(rename = "proofValue")]
87    pub proof_value: String,
88}
89
90/// Produce a W3C Verifiable Credential 2.0 from an EAR token.
91pub fn to_verifiable_credential(ear: &EarToken, author_did: &str) -> Result<VerifiableCredential> {
92    let appr = ear
93        .pop_appraisal()
94        .ok_or_else(|| String::from("EAR token missing 'pop' submodule"))?;
95
96    let tv_vc = appr
97        .ear_trustworthiness_vector
98        .as_ref()
99        .map(|tv| TrustVectorVc {
100            instance_identity: tv.instance_identity,
101            configuration: tv.configuration,
102            executables: tv.executables,
103            file_system: tv.file_system,
104            hardware: tv.hardware,
105            runtime_opaque: tv.runtime_opaque,
106            storage_opaque: tv.storage_opaque,
107            sourced_data: tv.sourced_data,
108        });
109
110    let document_ref = appr.pop_evidence_ref.as_ref().map(hex::encode);
111
112    let chain_duration = appr.pop_chain_duration.map(|secs| {
113        let hours = secs / 3600;
114        let minutes = (secs % 3600) / 60;
115        let remaining_secs = secs % 60;
116        if hours > 0 {
117            format!("PT{}H{}M{}S", hours, minutes, remaining_secs)
118        } else if minutes > 0 {
119            format!("PT{}M{}S", minutes, remaining_secs)
120        } else {
121            format!("PT{}S", remaining_secs)
122        }
123    });
124
125    let tier_str = appr
126        .ear_trustworthiness_vector
127        .as_ref()
128        .map(|tv| {
129            if tv.hardware >= Ar4siStatus::Affirming as i8 {
130                "hardware_bound"
131            } else if tv.hardware >= Ar4siStatus::Warning as i8 {
132                "attested_software"
133            } else {
134                "software_only"
135            }
136        })
137        .map(String::from);
138
139    let valid_from: DateTime<Utc> = DateTime::from_timestamp(ear.iat, 0).unwrap_or_else(Utc::now);
140
141    let seal_hash = appr.pop_seal.as_ref().map(|s| hex::encode(s.h3));
142
143    let evidence = vec![VcEvidence {
144        evidence_type: "ProofOfProcessEvidence".to_string(),
145        verifier: ear.ear_verifier_id.build.clone(),
146        seal_hash,
147    }];
148
149    // Proof placeholder — actual signing happens at a higher layer
150    let proof = VcProof {
151        proof_type: "DataIntegrityProof".to_string(),
152        cryptosuite: "eddsa-rdfc-2022".to_string(),
153        verification_method: format!("{}#key-1", author_did),
154        proof_purpose: "assertionMethod".to_string(),
155        proof_value: String::new(),
156    };
157
158    Ok(VerifiableCredential {
159        context: vec![
160            "https://www.w3.org/ns/credentials/v2".to_string(),
161            "https://writerslogic.com/ns/pop/v1".to_string(),
162        ],
163        vc_type: vec![
164            "VerifiableCredential".to_string(),
165            "ProcessAttestationCredential".to_string(),
166        ],
167        issuer: "did:web:writerslogic.com".to_string(),
168        valid_from: valid_from.to_rfc3339(),
169        credential_subject: CredentialSubject {
170            id: author_did.to_string(),
171            subject_type: "Author".to_string(),
172            process_attestation: ProcessAttestation {
173                status: appr.ear_status.as_str().to_string(),
174                trust_vector: tv_vc,
175                document_ref,
176                chain_duration,
177                attestation_tier: tier_str,
178            },
179        },
180        evidence: Some(evidence),
181        proof: Some(proof),
182    })
183}