use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Audience {
Public,
LegitimateInterest,
Authority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Disclosure {
Public,
Restricted,
Conformity,
Individual,
}
pub const PASSPORT_FIELD_DISCLOSURE: &[(&str, Disclosure)] = &[
("batchId", Disclosure::Restricted),
("lintResult", Disclosure::Restricted),
("jwsSignature", Disclosure::Conformity),
("retentionLocked", Disclosure::Conformity),
];
impl Audience {
#[must_use]
pub const fn may_see(self, disclosure: Disclosure) -> bool {
matches!(
(self, disclosure),
(Self::Public, Disclosure::Public)
| (
Self::LegitimateInterest,
Disclosure::Public | Disclosure::Restricted | Disclosure::Individual
)
| (
Self::Authority,
Disclosure::Public | Disclosure::Restricted | Disclosure::Conformity
)
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PassportCredential {
#[serde(rename = "@context")]
pub context: Vec<String>,
#[serde(rename = "type")]
pub credential_type: Vec<String>,
pub id: String,
pub issuer: String,
pub valid_from: DateTime<Utc>,
pub credential_subject: PassportCredentialSubject,
}
impl PassportCredential {
pub const VC_BASE_CONTEXT: &'static str = "https://www.w3.org/ns/credentials/v2";
pub const PASSPORT_CONTEXT: &'static str =
"https://schema.odal-node.io/credentials/dpp-passport/v1";
#[must_use]
pub fn new(issuer: String, credential_subject: PassportCredentialSubject) -> Self {
Self {
context: vec![Self::VC_BASE_CONTEXT.into(), Self::PASSPORT_CONTEXT.into()],
credential_type: vec![
"VerifiableCredential".into(),
"DppPassportCredential".into(),
],
id: format!("urn:uuid:{}", uuid::Uuid::now_v7()),
issuer,
valid_from: Utc::now(),
credential_subject,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PassportCredentialSubject {
pub id: String,
pub payload_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedCredential {
pub credential: PassportCredential,
pub jws: String,
pub issuer_did: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn authorities_do_not_see_individual_item_data() {
assert!(!Audience::Authority.may_see(Disclosure::Individual));
assert!(Audience::LegitimateInterest.may_see(Disclosure::Individual));
}
#[test]
fn legitimate_interest_does_not_see_conformity_evidence() {
assert!(!Audience::LegitimateInterest.may_see(Disclosure::Conformity));
assert!(Audience::Authority.may_see(Disclosure::Conformity));
}
#[test]
fn neither_non_public_audience_contains_the_other() {
let all = [
Disclosure::Public,
Disclosure::Restricted,
Disclosure::Conformity,
Disclosure::Individual,
];
let authority_only = all
.iter()
.any(|d| Audience::Authority.may_see(*d) && !Audience::LegitimateInterest.may_see(*d));
let interest_only = all
.iter()
.any(|d| Audience::LegitimateInterest.may_see(*d) && !Audience::Authority.may_see(*d));
assert!(authority_only && interest_only);
}
#[test]
fn point_two_is_shared_and_public_is_universal() {
for audience in [
Audience::Public,
Audience::LegitimateInterest,
Audience::Authority,
] {
assert!(audience.may_see(Disclosure::Public));
}
assert!(Audience::LegitimateInterest.may_see(Disclosure::Restricted));
assert!(Audience::Authority.may_see(Disclosure::Restricted));
assert!(!Audience::Public.may_see(Disclosure::Restricted));
}
#[test]
fn public_sees_nothing_restricted() {
for class in [
Disclosure::Restricted,
Disclosure::Conformity,
Disclosure::Individual,
] {
assert!(!Audience::Public.may_see(class));
}
}
#[test]
fn new_passport_credential_guarantees_vc_base_context_and_type() {
let vc = PassportCredential::new(
"did:web:issuer.example.com".into(),
PassportCredentialSubject {
id: "urn:uuid:00000000-0000-0000-0000-000000000000".into(),
payload_hash: "deadbeef".into(),
},
);
assert_eq!(
vc.context.first().map(String::as_str),
Some(PassportCredential::VC_BASE_CONTEXT)
);
assert!(
vc.credential_type
.contains(&"VerifiableCredential".to_string())
);
assert!(vc.id.starts_with("urn:uuid:"));
}
}