dpp_domain/domain/identity.rs
1//! Identity and access-tier types: `AccessTier`, `SignedCredential`, and `PassportCredential`.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// ESPR access tier levels for DPP data gating.
7///
8/// The EU ESPR mandates three tiers of data access:
9/// - **Public**: No credential required. Visible to any consumer.
10/// - **Professional**: Requires a W3C Verifiable Credential proving the
11/// holder's role (repairer, recycler, remanufacturer, etc.).
12/// - **Confidential**: Requires an institutional DID (market surveillance
13/// authority, customs, notified body).
14///
15/// This is the canonical definition used across all dpp-core crates.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum AccessTier {
20 Public = 0,
21 Professional = 1,
22 Confidential = 2,
23}
24
25/// A W3C Verifiable Credential 2.0 envelope binding a DPP passport to its signed payload.
26///
27/// The cryptographic proof is in [`SignedCredential::jws`]; this struct provides
28/// the structured VC context required for EUDI/EBSI interoperability.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct PassportCredential {
32 #[serde(rename = "@context")]
33 pub context: Vec<String>,
34 #[serde(rename = "type")]
35 pub credential_type: Vec<String>,
36 /// Unique credential ID (`urn:uuid:…`) — generated fresh per signing call.
37 pub id: String,
38 /// DID of the signing issuer (`did:web:…`).
39 pub issuer: String,
40 /// Credential issuance timestamp (W3C VC 2.0 `validFrom`).
41 pub valid_from: DateTime<Utc>,
42 pub credential_subject: PassportCredentialSubject,
43}
44
45impl PassportCredential {
46 /// W3C VCDM v2 base context — MUST be the first `@context` entry.
47 pub const VC_BASE_CONTEXT: &'static str = "https://www.w3.org/ns/credentials/v2";
48 /// Project-specific JSON-LD context for DPP passport credentials.
49 pub const PASSPORT_CONTEXT: &'static str =
50 "https://schema.odal-node.io/credentials/dpp-passport/v1";
51
52 /// Construct a passport credential with the VCDM v2 base context and the
53 /// `VerifiableCredential` base type guaranteed present, so a caller cannot
54 /// emit a VC missing `https://www.w3.org/ns/credentials/v2`. `id`
55 /// (`urn:uuid:` v7) and `valid_from` are generated fresh.
56 #[must_use]
57 pub fn new(issuer: String, credential_subject: PassportCredentialSubject) -> Self {
58 Self {
59 context: vec![Self::VC_BASE_CONTEXT.into(), Self::PASSPORT_CONTEXT.into()],
60 credential_type: vec![
61 "VerifiableCredential".into(),
62 "DppPassportCredential".into(),
63 ],
64 id: format!("urn:uuid:{}", uuid::Uuid::now_v7()),
65 issuer,
66 valid_from: Utc::now(),
67 credential_subject,
68 }
69 }
70}
71
72/// Claims about the DPP passport being attested.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct PassportCredentialSubject {
76 /// `urn:uuid:{passport_id}` — the DPP passport being attested.
77 pub id: String,
78 /// SHA-256 hex digest of the RFC 8785 canonical payload bytes.
79 pub payload_hash: String,
80}
81
82/// A DPP Verifiable Credential with its JWS proof signature.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct SignedCredential {
86 /// Structured W3C VC 2.0 passport credential.
87 pub credential: PassportCredential,
88 /// Compact JWS signature string (header.payload.signature).
89 pub jws: String,
90 /// The DID of the issuer (manufacturer or Odal on their behalf).
91 pub issuer_did: String,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn new_passport_credential_guarantees_vc_base_context_and_type() {
100 let vc = PassportCredential::new(
101 "did:web:issuer.example.com".into(),
102 PassportCredentialSubject {
103 id: "urn:uuid:00000000-0000-0000-0000-000000000000".into(),
104 payload_hash: "deadbeef".into(),
105 },
106 );
107 // VCDM v2 requires the base context to be the first @context entry.
108 assert_eq!(
109 vc.context.first().map(String::as_str),
110 Some(PassportCredential::VC_BASE_CONTEXT)
111 );
112 assert!(
113 vc.credential_type
114 .contains(&"VerifiableCredential".to_string())
115 );
116 assert!(vc.id.starts_with("urn:uuid:"));
117 }
118}