use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SealMode {
ProviderSeal,
OperatorSeal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum SealFormat {
Jades,
Pades,
Cades,
Xades,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum SealConformanceLevel {
BaselineB,
BaselineT,
BaselineLt,
BaselineLta,
}
impl SealConformanceLevel {
pub const ALL: &'static [Self] = &[
Self::BaselineB,
Self::BaselineT,
Self::BaselineLt,
Self::BaselineLta,
];
#[must_use]
pub const fn survives_certificate_expiry(self) -> bool {
matches!(self, Self::BaselineLt | Self::BaselineLta)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum SealEnvelope {
Detached,
Enveloping,
Attached,
Parallel,
Enveloped,
Certification,
Revision,
}
impl SealEnvelope {
pub const ALL: &'static [Self] = &[
Self::Detached,
Self::Enveloping,
Self::Attached,
Self::Parallel,
Self::Enveloped,
Self::Certification,
Self::Revision,
];
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SealCredentialRef {
pub qtsp_id: String,
pub credential_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SealRequest {
pub payload_hash: String,
pub mode: SealMode,
pub key_ref: SealCredentialRef,
pub sig_format: SealFormat,
#[serde(default = "default_conformance_level")]
pub conformance_level: SealConformanceLevel,
#[serde(default = "default_envelope")]
pub envelope: SealEnvelope,
}
fn default_conformance_level() -> SealConformanceLevel {
SealConformanceLevel::BaselineLt
}
fn default_envelope() -> SealEnvelope {
SealEnvelope::Detached
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SealedEnvelope {
pub format: SealFormat,
pub seal_value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signing_cert_ref: Option<String>,
pub sealed_at: DateTime<Utc>,
pub placeholder: bool,
}
impl SealFormat {
pub const ALL: &'static [Self] = &[Self::Jades, Self::Pades, Self::Cades, Self::Xades];
#[must_use]
pub const fn envelopes(&self) -> &'static [SealEnvelope] {
match self {
Self::Jades | Self::Cades => &[
SealEnvelope::Attached,
SealEnvelope::Detached,
SealEnvelope::Parallel,
],
Self::Xades => &[
SealEnvelope::Enveloped,
SealEnvelope::Enveloping,
SealEnvelope::Detached,
],
Self::Pades => &[SealEnvelope::Certification, SealEnvelope::Revision],
}
}
#[must_use]
pub fn admits(&self, envelope: SealEnvelope) -> bool {
self.envelopes().contains(&envelope)
}
}
impl SealMode {
pub const ALL: &'static [Self] = &[Self::ProviderSeal, Self::OperatorSeal];
}
#[derive(Debug, Clone)]
pub struct SealCapabilities {
pub supported_formats: Vec<SealFormat>,
pub supported_modes: Vec<SealMode>,
pub supported_levels: Vec<SealConformanceLevel>,
pub supported_envelopes: Vec<SealEnvelope>,
}
impl SealCapabilities {
pub fn can_produce(&self, req: &SealRequest) -> bool {
self.supported_formats.contains(&req.sig_format)
&& self.supported_modes.contains(&req.mode)
&& self.supported_levels.contains(&req.conformance_level)
&& self.supported_envelopes.contains(&req.envelope)
&& req.sig_format.admits(req.envelope)
}
#[must_use]
pub fn can_outlive_certificate_expiry(&self) -> bool {
self.supported_levels
.iter()
.any(|l| l.survives_certificate_expiry())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SealIndication {
TotalPassed,
TotalFailed(String),
Indeterminate(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SealChecks {
None,
SignatureOnly,
FullValidation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SealVerification {
pub indication: SealIndication,
pub checks: SealChecks,
pub placeholder: bool,
}
impl SealVerification {
#[must_use]
pub fn passed(checks: SealChecks) -> Self {
Self {
indication: SealIndication::TotalPassed,
checks,
placeholder: false,
}
}
#[must_use]
pub fn failed(checks: SealChecks, reason: impl Into<String>) -> Self {
Self {
indication: SealIndication::TotalFailed(reason.into()),
checks,
placeholder: false,
}
}
#[must_use]
pub fn indeterminate(checks: SealChecks, reason: impl Into<String>) -> Self {
Self {
indication: SealIndication::Indeterminate(reason.into()),
checks,
placeholder: false,
}
}
#[must_use]
pub fn placeholder(reason: impl Into<String>) -> Self {
Self {
indication: SealIndication::Indeterminate(reason.into()),
checks: SealChecks::None,
placeholder: true,
}
}
#[must_use]
pub fn is_qualified_pass(&self) -> bool {
!self.placeholder
&& self.checks == SealChecks::FullValidation
&& self.indication == SealIndication::TotalPassed
}
#[must_use]
pub fn is_coherent(&self) -> bool {
!(self.indication == SealIndication::TotalPassed && self.checks == SealChecks::None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seal_format_serde_round_trips() {
for fmt in [
SealFormat::Jades,
SealFormat::Pades,
SealFormat::Cades,
SealFormat::Xades,
] {
let json = serde_json::to_string(&fmt).unwrap();
let back: SealFormat = serde_json::from_str(&json).unwrap();
assert_eq!(fmt, back);
}
}
#[test]
fn a_signature_check_is_not_a_qualified_pass() {
let signature_only = SealVerification {
indication: SealIndication::TotalPassed,
checks: SealChecks::SignatureOnly,
placeholder: false,
};
assert!(
!signature_only.is_qualified_pass(),
"a signature check says nothing about the certificate behind it"
);
let full = SealVerification {
checks: SealChecks::FullValidation,
..signature_only.clone()
};
assert!(full.is_qualified_pass());
let placeholder = SealVerification {
placeholder: true,
..full.clone()
};
assert!(!placeholder.is_qualified_pass());
}
#[test]
fn indeterminate_is_not_a_pass() {
let unresolved = SealVerification {
indication: SealIndication::Indeterminate("revocation data unreachable".into()),
checks: SealChecks::FullValidation,
placeholder: false,
};
assert!(!unresolved.is_qualified_pass());
assert_ne!(unresolved.indication, SealIndication::TotalPassed);
}
#[test]
fn seal_mode_serde_round_trips() {
for mode in [SealMode::ProviderSeal, SealMode::OperatorSeal] {
let json = serde_json::to_string(&mode).unwrap();
let back: SealMode = serde_json::from_str(&json).unwrap();
assert_eq!(mode, back);
}
}
#[test]
fn seal_envelope_serde_round_trips() {
for envelope in SealEnvelope::ALL {
let json = serde_json::to_string(envelope).unwrap();
let back: SealEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(*envelope, back);
}
}
#[test]
fn every_format_and_packaging_is_reachable() {
for format in SealFormat::ALL {
assert!(
!format.envelopes().is_empty(),
"{format:?} defines no packaging, so no request for it is well-formed"
);
}
for envelope in SealEnvelope::ALL {
assert!(
SealFormat::ALL.iter().any(|f| f.admits(*envelope)),
"{envelope:?} belongs to no format, so nothing can ask for it"
);
}
}
#[test]
fn packagings_are_scoped_to_the_formats_that_define_them() {
assert!(SealFormat::Xades.admits(SealEnvelope::Enveloping));
assert!(!SealFormat::Jades.admits(SealEnvelope::Enveloping));
assert!(!SealFormat::Cades.admits(SealEnvelope::Enveloping));
assert!(SealFormat::Pades.admits(SealEnvelope::Certification));
assert!(!SealFormat::Pades.admits(SealEnvelope::Detached));
assert!(!SealFormat::Jades.admits(SealEnvelope::Certification));
assert!(SealFormat::Jades.admits(SealEnvelope::Parallel));
assert!(SealFormat::Cades.admits(SealEnvelope::Parallel));
assert!(!SealFormat::Xades.admits(SealEnvelope::Parallel));
}
#[test]
fn a_defaulted_pades_request_cannot_be_satisfied() {
let everything = SealCapabilities {
supported_formats: SealFormat::ALL.to_vec(),
supported_modes: SealMode::ALL.to_vec(),
supported_levels: SealConformanceLevel::ALL.to_vec(),
supported_envelopes: SealEnvelope::ALL.to_vec(),
};
let wire = r#"{
"payloadHash": "abababababababababababababababababababababababababababababababab",
"mode": "provider_seal",
"keyRef": { "qtspId": "q", "credentialId": "c" },
"sigFormat": "PADES"
}"#;
let defaulted: SealRequest = serde_json::from_str(wire).expect("defaults fill the rest");
assert_eq!(
defaulted.envelope,
SealEnvelope::Detached,
"the default is format-blind, which is the premise of this test"
);
assert!(
!everything.can_produce(&defaulted),
"PAdES does not define Detached, so nothing can produce this request"
);
let named = SealRequest {
envelope: SealEnvelope::Certification,
..defaulted
};
assert!(everything.can_produce(&named));
}
#[test]
fn can_produce_refuses_a_pair_no_format_defines() {
let capabilities = SealCapabilities {
supported_formats: vec![SealFormat::Jades, SealFormat::Xades],
supported_modes: vec![SealMode::ProviderSeal],
supported_levels: vec![SealConformanceLevel::BaselineLt],
supported_envelopes: vec![SealEnvelope::Enveloping, SealEnvelope::Detached],
};
let request = |sig_format: SealFormat, envelope: SealEnvelope| SealRequest {
payload_hash: "ab".repeat(32),
mode: SealMode::ProviderSeal,
key_ref: SealCredentialRef {
qtsp_id: "q".into(),
credential_id: "c".into(),
},
sig_format,
conformance_level: SealConformanceLevel::BaselineLt,
envelope,
};
assert!(!capabilities.can_produce(&request(SealFormat::Jades, SealEnvelope::Enveloping)));
assert!(capabilities.can_produce(&request(SealFormat::Xades, SealEnvelope::Enveloping)));
assert!(capabilities.can_produce(&request(SealFormat::Jades, SealEnvelope::Detached)));
}
}