use alloc::{
borrow::ToOwned,
string::{String, ToString},
vec::Vec,
};
use serde::{Deserialize, Serialize};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crate::{Error, Result, sha256_prefixed, validate_sha256};
pub const SPEC_VERSION: &str = "wilder.pser/0.5";
pub const SPEC_VERSION_06: &str = "wilder.pser/0.6";
#[must_use]
pub fn is_supported_spec(spec: &str) -> bool {
spec == SPEC_VERSION || spec == SPEC_VERSION_06
}
const BINDING_MODE_DIRECT_WITNESS: &str = "DIRECT_WITNESS";
const BINDING_MODE_DELEGATED_WITNESS: &str = "DELEGATED_WITNESS";
const ACK_PROVENANCE_THIRD_PARTY: &str = "THIRD_PARTY";
const ACK_PROVENANCE_ISSUER_ASSERTED: &str = "ISSUER_ASSERTED";
const ACK_PROVENANCE_NONE: &str = "NONE";
const ISSUER_AFFILIATION_AFFILIATED: &str = "AFFILIATED";
const ISSUER_AFFILIATION_INDEPENDENT: &str = "INDEPENDENT";
const ISSUER_AFFILIATION_NOT_DISCLOSED: &str = "NOT_DISCLOSED";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Site {
id: String,
class: SiteClass,
envelope: SiteEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum SiteClass {
Residential,
Industrial,
Healthcare,
Infra,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SiteEnvelope {
id: String,
digest: String,
#[serde(deserialize_with = "deserialize_required_option")]
geobounds: Option<String>,
temporal: Temporal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Temporal {
#[serde(deserialize_with = "deserialize_required_option")]
starts: Option<String>,
#[serde(deserialize_with = "deserialize_required_option")]
ends: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Actor {
id: String,
class: ActorClass,
operator: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum ActorClass {
Autonomous,
SemiAutonomous,
Human,
Crew,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Engagement {
id: String,
window: Window,
r#type: String,
outcome_class: OutcomeClass,
envelope_conformance: EnvelopeConformance,
evidence_digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Window {
start: String,
end: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum OutcomeClass {
Completed,
Aborted,
Refused,
Errored,
ObservedOnly,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum EnvelopeConformance {
Within,
ExceededTemporal,
ExceededGeo,
ExceededActor,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Attestation {
binding_mode: BindingMode,
tee_class: String,
measured_boot: MeasuredBoot,
platform_evidence: PlatformEvidence,
sealed_evidence: SealedEvidence,
witness_key: String,
validity: Validity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct MeasuredBoot {
chain: String,
components: Vec<MeasuredBootComponent>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct MeasuredBootComponent {
name: String,
digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct PlatformEvidence {
encoding: String,
digest: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Validity {
not_before: String,
not_after: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct SealedEvidence {
digest: String,
size_bytes: u64,
encoding: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Adapter {
system: String,
endpoint: String,
posted_at: String,
ack_digest: String,
ack_provenance: AckProvenance,
mode: AdapterMode,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
enum AdapterMode {
#[serde(rename = "WRITE_ONLY")]
WriteOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AckProvenance {
ThirdParty,
IssuerAsserted,
NoAcknowledgement,
Unrecognized(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindingMode {
DirectWitness,
DelegatedWitness,
Unrecognized(String),
}
impl BindingMode {
#[must_use]
pub fn as_wire_str(&self) -> &str {
match self {
Self::DirectWitness => BINDING_MODE_DIRECT_WITNESS,
Self::DelegatedWitness => BINDING_MODE_DELEGATED_WITNESS,
Self::Unrecognized(raw) => raw,
}
}
#[must_use]
pub const fn is_unrecognized(&self) -> bool {
matches!(self, Self::Unrecognized(_))
}
}
impl Serialize for BindingMode {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> core::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_wire_str())
}
}
impl<'de> Deserialize<'de> for BindingMode {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> core::result::Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
Ok(match raw.as_str() {
BINDING_MODE_DIRECT_WITNESS => Self::DirectWitness,
BINDING_MODE_DELEGATED_WITNESS => Self::DelegatedWitness,
_ => Self::Unrecognized(raw),
})
}
}
impl AckProvenance {
#[must_use]
pub fn as_wire_str(&self) -> &str {
match self {
Self::ThirdParty => ACK_PROVENANCE_THIRD_PARTY,
Self::IssuerAsserted => ACK_PROVENANCE_ISSUER_ASSERTED,
Self::NoAcknowledgement => ACK_PROVENANCE_NONE,
Self::Unrecognized(raw) => raw,
}
}
#[must_use]
pub const fn is_unrecognized(&self) -> bool {
matches!(self, Self::Unrecognized(_))
}
}
impl Serialize for AckProvenance {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> core::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_wire_str())
}
}
impl<'de> Deserialize<'de> for AckProvenance {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> core::result::Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
Ok(match raw.as_str() {
ACK_PROVENANCE_THIRD_PARTY => Self::ThirdParty,
ACK_PROVENANCE_ISSUER_ASSERTED => Self::IssuerAsserted,
ACK_PROVENANCE_NONE => Self::NoAcknowledgement,
_ => Self::Unrecognized(raw),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IssuerAffiliation {
Affiliated,
Independent,
NotDisclosed,
Unrecognized(String),
}
impl IssuerAffiliation {
#[must_use]
pub fn as_wire_str(&self) -> &str {
match self {
Self::Affiliated => ISSUER_AFFILIATION_AFFILIATED,
Self::Independent => ISSUER_AFFILIATION_INDEPENDENT,
Self::NotDisclosed => ISSUER_AFFILIATION_NOT_DISCLOSED,
Self::Unrecognized(raw) => raw,
}
}
#[must_use]
pub const fn is_unrecognized(&self) -> bool {
matches!(self, Self::Unrecognized(_))
}
#[must_use]
pub const fn is_not_disclosed(&self) -> bool {
matches!(self, Self::NotDisclosed)
}
}
impl Serialize for IssuerAffiliation {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> core::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_wire_str())
}
}
impl<'de> Deserialize<'de> for IssuerAffiliation {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> core::result::Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
Ok(match raw.as_str() {
ISSUER_AFFILIATION_AFFILIATED => Self::Affiliated,
ISSUER_AFFILIATION_INDEPENDENT => Self::Independent,
ISSUER_AFFILIATION_NOT_DISCLOSED => Self::NotDisclosed,
_ => Self::Unrecognized(raw),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Chain {
seq: u64,
#[serde(deserialize_with = "deserialize_required_option")]
prev_hash: Option<String>,
hash: String,
}
fn check_spec_version(bytes: &[u8]) -> Result<()> {
#[derive(Deserialize)]
struct SpecOnly {
spec: String,
}
if let Ok(probe) = serde_json::from_slice::<SpecOnly>(bytes)
&& !is_supported_spec(&probe.spec)
{
return Err(Error::Validation("unsupported spec version"));
}
Ok(())
}
fn deserialize_required_option<'de, D, T>(
deserializer: D,
) -> core::result::Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(deserializer)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Payload {
spec: String,
id: String,
ts: String,
issuer_affiliation: IssuerAffiliation,
site: Site,
actor: Actor,
engagement: Engagement,
attestation: Attestation,
adapter: Adapter,
chain: Chain,
}
impl Payload {
pub fn from_json(bytes: &[u8]) -> Result<Self> {
check_spec_version(bytes)?;
let payload: Self =
serde_json::from_slice(bytes).map_err(|error| Error::Json(error.to_string()))?;
payload.validate()?;
Ok(payload)
}
pub fn from_json_for_production(bytes: &[u8]) -> Result<Self> {
check_spec_version(bytes)?;
let mut payload: Self =
serde_json::from_slice(bytes).map_err(|error| Error::Json(error.to_string()))?;
payload.update_chain_hash()?;
payload.validate()?;
Ok(payload)
}
pub fn to_jcs(&self) -> Result<Vec<u8>> {
let value = serde_json::to_value(self).map_err(|error| Error::Json(error.to_string()))?;
canonicalize_value(&value)
}
#[must_use]
pub fn spec(&self) -> &str {
&self.spec
}
#[must_use]
pub fn site_id(&self) -> &str {
&self.site.id
}
#[must_use]
pub const fn attestation_binding_mode(&self) -> &BindingMode {
&self.attestation.binding_mode
}
pub fn witness_key(&self) -> &str {
&self.attestation.witness_key
}
#[must_use]
pub fn chain_hash(&self) -> &str {
&self.chain.hash
}
#[must_use]
pub fn chain_seq(&self) -> u64 {
self.chain.seq
}
#[must_use]
pub fn chain_prev_hash(&self) -> Option<&str> {
self.chain.prev_hash.as_deref()
}
#[must_use]
pub const fn issuer_affiliation(&self) -> &IssuerAffiliation {
&self.issuer_affiliation
}
#[must_use]
pub const fn adapter_ack_provenance(&self) -> &AckProvenance {
&self.adapter.ack_provenance
}
#[must_use]
pub fn actor_id(&self) -> &str {
&self.actor.id
}
#[must_use]
pub fn actor_operator(&self) -> &str {
&self.actor.operator
}
#[must_use]
pub fn engagement_id(&self) -> &str {
&self.engagement.id
}
#[must_use]
pub fn engagement_type(&self) -> &str {
&self.engagement.r#type
}
#[must_use]
pub fn engagement_window_start(&self) -> &str {
&self.engagement.window.start
}
#[must_use]
pub fn engagement_window_end(&self) -> &str {
&self.engagement.window.end
}
#[must_use]
pub fn engagement_evidence_digest(&self) -> &str {
&self.engagement.evidence_digest
}
#[must_use]
pub fn attestation_tee_class(&self) -> &str {
&self.attestation.tee_class
}
#[must_use]
pub fn sealed_evidence_digest(&self) -> &str {
&self.attestation.sealed_evidence.digest
}
#[must_use]
pub fn sealed_evidence_size_bytes(&self) -> u64 {
self.attestation.sealed_evidence.size_bytes
}
#[must_use]
pub fn sealed_evidence_encoding(&self) -> &str {
&self.attestation.sealed_evidence.encoding
}
#[must_use]
pub fn adapter_system(&self) -> &str {
&self.adapter.system
}
#[must_use]
pub fn adapter_endpoint(&self) -> &str {
&self.adapter.endpoint
}
#[must_use]
pub fn adapter_posted_at(&self) -> &str {
&self.adapter.posted_at
}
#[must_use]
pub fn adapter_ack_digest(&self) -> &str {
&self.adapter.ack_digest
}
#[must_use]
pub fn adapter_is_write_only(&self) -> bool {
matches!(self.adapter.mode, AdapterMode::WriteOnly)
}
pub(crate) fn parse_canonical(bytes: &[u8]) -> Result<Self> {
let payload = Self::from_json(bytes)?;
if payload.to_jcs()?.as_slice() != bytes {
return Err(Error::NonCanonicalPayload);
}
Ok(payload)
}
fn update_chain_hash(&mut self) -> Result<()> {
self.chain.hash = self.expected_chain_hash()?;
Ok(())
}
fn expected_chain_hash(&self) -> Result<String> {
let mut value =
serde_json::to_value(self).map_err(|error| Error::Json(error.to_string()))?;
let chain = value
.get_mut("chain")
.and_then(serde_json::Value::as_object_mut)
.ok_or(Error::Validation("chain must be an object"))?;
chain.remove("hash");
let canonical = canonicalize_value(&value)?;
Ok(sha256_prefixed(&canonical))
}
fn validate(&self) -> Result<()> {
if !is_supported_spec(&self.spec) {
return Err(Error::Validation("unsupported spec version"));
}
if self.attestation.binding_mode.is_unrecognized() {
return Err(Error::Validation("unrecognized attestation binding mode"));
}
for value in [
&self.id,
&self.site.id,
&self.site.envelope.id,
&self.actor.id,
&self.actor.operator,
&self.engagement.id,
&self.engagement.r#type,
&self.attestation.tee_class,
&self.attestation.platform_evidence.encoding,
&self.attestation.sealed_evidence.encoding,
&self.attestation.witness_key,
&self.adapter.system,
&self.adapter.endpoint,
] {
if value.is_empty() {
return Err(Error::Validation("required string must not be empty"));
}
}
validate_utc(&self.ts)?;
validate_optional_window(
self.site.envelope.temporal.starts.as_deref(),
self.site.envelope.temporal.ends.as_deref(),
)?;
let start = validate_utc(&self.engagement.window.start)?;
let end = validate_utc(&self.engagement.window.end)?;
if end < start {
return Err(Error::Validation(
"engagement window end precedes its start",
));
}
validate_utc(&self.adapter.posted_at)?;
validate_sha256(&self.site.envelope.digest)?;
validate_sha256(&self.engagement.evidence_digest)?;
validate_sha256(&self.attestation.measured_boot.chain)?;
validate_sha256(&self.attestation.platform_evidence.digest)?;
validate_sha256(&self.attestation.sealed_evidence.digest)?;
for component in &self.attestation.measured_boot.components {
if component.name.is_empty() {
return Err(Error::Validation(
"measured-boot component name must not be empty",
));
}
validate_sha256(&component.digest)?;
}
let validity_start = validate_utc(&self.attestation.validity.not_before)?;
let validity_end = validate_utc(&self.attestation.validity.not_after)?;
if validity_end <= validity_start {
return Err(Error::Validation(
"attestation validity notAfter must be strictly later than notBefore",
));
}
if self.spec == SPEC_VERSION_06 {
let ts = validate_utc(&self.ts)?;
if ts < validity_start || ts > validity_end {
return Err(Error::Validation(
"receipt-issuance timestamp is outside the attestation validity interval",
));
}
}
validate_sha256(&self.adapter.ack_digest)?;
match (self.chain.seq, self.chain.prev_hash.as_deref()) {
(0, None) => {}
(0, Some(_)) => {
return Err(Error::Validation("sequence zero must have null prevHash"));
}
(_, Some(previous)) => validate_sha256(previous)?,
(_, None) => {
return Err(Error::Validation("nonzero sequence must include prevHash"));
}
}
validate_sha256(&self.chain.hash)?;
if self.chain.hash != self.expected_chain_hash()? {
return Err(Error::Validation("chain.hash does not match payload"));
}
Ok(())
}
}
pub fn canonicalize_json(bytes: &[u8]) -> Result<Vec<u8>> {
let value: serde_json::Value =
serde_json::from_slice(bytes).map_err(|error| Error::Json(error.to_string()))?;
canonicalize_value(&value)
}
fn canonicalize_value(value: &serde_json::Value) -> Result<Vec<u8>> {
let mut output = Vec::new();
write_canonical(value, &mut output)?;
Ok(output)
}
fn write_canonical(value: &serde_json::Value, output: &mut Vec<u8>) -> Result<()> {
match value {
serde_json::Value::Null => output.extend_from_slice(b"null"),
serde_json::Value::Bool(true) => output.extend_from_slice(b"true"),
serde_json::Value::Bool(false) => output.extend_from_slice(b"false"),
serde_json::Value::Number(number) => {
let text = if let Some(integer) = number.as_i64() {
integer.to_string()
} else if let Some(integer) = number.as_u64() {
integer.to_string()
} else {
let number = number
.as_f64()
.ok_or(Error::Jcs("number is not representable as f64".to_owned()))?;
if !number.is_finite() {
return Err(Error::Jcs("JCS numbers must be finite".to_owned()));
}
ryu_js::Buffer::new().format_finite(number).to_owned()
};
output.extend_from_slice(text.as_bytes());
}
serde_json::Value::String(string) => {
let escaped =
serde_json::to_string(string).map_err(|error| Error::Jcs(error.to_string()))?;
output.extend_from_slice(escaped.as_bytes());
}
serde_json::Value::Array(values) => {
output.push(b'[');
for (index, item) in values.iter().enumerate() {
if index != 0 {
output.push(b',');
}
write_canonical(item, output)?;
}
output.push(b']');
}
serde_json::Value::Object(object) => {
let mut entries: Vec<_> = object.iter().collect();
entries.sort_by(|(left, _), (right, _)| left.encode_utf16().cmp(right.encode_utf16()));
output.push(b'{');
for (index, (key, item)) in entries.into_iter().enumerate() {
if index != 0 {
output.push(b',');
}
let escaped =
serde_json::to_string(key).map_err(|error| Error::Jcs(error.to_string()))?;
output.extend_from_slice(escaped.as_bytes());
output.push(b':');
write_canonical(item, output)?;
}
output.push(b'}');
}
}
Ok(())
}
fn validate_utc(value: &str) -> Result<OffsetDateTime> {
if !value.ends_with('Z') {
return Err(Error::Validation("timestamp must use UTC Z notation"));
}
OffsetDateTime::parse(value, &Rfc3339)
.map_err(|_| Error::Validation("timestamp must be valid RFC 3339"))
}
fn validate_optional_window(starts: Option<&str>, ends: Option<&str>) -> Result<()> {
let starts = starts.map(validate_utc).transpose()?;
let ends = ends.map(validate_utc).transpose()?;
if let (Some(start), Some(end)) = (starts, ends)
&& end < start
{
return Err(Error::Validation("temporal end precedes its start"));
}
Ok(())
}