Skip to main content

a3s_runtime/
attestation.rs

1use crate::contract::{
2    ArtifactRef, IsolationLevel, RuntimeEvidence, RuntimeObservation, RuntimeUnitClass,
3    RuntimeUnitSpec, RuntimeUnitState,
4};
5use serde::Serialize;
6use sha2::{Digest, Sha256};
7
8/// Exact provider-neutral proof that one provider resource observed one
9/// identity-attached Runtime Unit generation.
10///
11/// Product policy remains outside Runtime. The attachment digest is opaque;
12/// this projection only proves that the same immutable digest entered the
13/// specification, provider evidence, and attested provider observation.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct RuntimeAttestationBinding {
16    pub unit_id: String,
17    pub generation: u64,
18    pub class: RuntimeUnitClass,
19    pub isolation: IsolationLevel,
20    pub state: RuntimeUnitState,
21    pub spec_digest: String,
22    pub identity_attachment_digest: String,
23    pub provider_resource_id: String,
24    pub provider_build: String,
25    pub observed_at_ms: u64,
26    pub evidence: RuntimeEvidence,
27    pub provider_attestation: ArtifactRef,
28}
29
30impl RuntimeAttestationBinding {
31    /// Project a closed binding from an exact specification and observation.
32    /// Missing, stale-generation, drifted, or unattested evidence fails
33    /// closed. Freshness and product policy admission remain caller-owned.
34    pub fn from_observation(
35        spec: &RuntimeUnitSpec,
36        observation: &RuntimeObservation,
37    ) -> Result<Self, String> {
38        observation.validate_against(spec)?;
39        let identity_attachment_digest = spec
40            .identity_attachment_digest
41            .as_ref()
42            .ok_or_else(|| "Runtime specification has no identity attachment".to_string())?;
43        let provider_resource_id = observation
44            .provider_resource_id
45            .as_ref()
46            .ok_or_else(|| "Runtime attestation has no provider resource identity".to_string())?;
47        let provider_build = observation
48            .provider_build
49            .as_ref()
50            .ok_or_else(|| "Runtime attestation has no provider build identity".to_string())?;
51        let evidence = observation
52            .evidence
53            .as_ref()
54            .ok_or_else(|| "Runtime attestation has no provider evidence".to_string())?;
55        if evidence.provider_build != *provider_build {
56            return Err("Runtime attestation provider build evidence drifted".into());
57        }
58        if evidence.identity_attachment_digest.as_ref() != Some(identity_attachment_digest) {
59            return Err("Runtime attestation identity attachment evidence drifted".into());
60        }
61        let provider_attestation = observation
62            .provider_attestation
63            .as_ref()
64            .ok_or_else(|| "Runtime observation has no provider attestation".to_string())?;
65        if observation.observed_at_ms == 0 {
66            return Err("Runtime attestation observation time must be positive".into());
67        }
68        let value = Self {
69            unit_id: spec.unit_id.clone(),
70            generation: spec.generation,
71            class: spec.class,
72            isolation: spec.isolation,
73            state: observation.state,
74            spec_digest: observation.spec_digest.clone(),
75            identity_attachment_digest: identity_attachment_digest.clone(),
76            provider_resource_id: provider_resource_id.clone(),
77            provider_build: provider_build.clone(),
78            observed_at_ms: observation.observed_at_ms,
79            evidence: evidence.clone(),
80            provider_attestation: provider_attestation.clone(),
81        };
82        value.validate()?;
83        Ok(value)
84    }
85
86    pub fn validate(&self) -> Result<(), String> {
87        crate::contract::validate_id("unit_id", &self.unit_id, 512)?;
88        if self.generation == 0 || self.observed_at_ms == 0 {
89            return Err(
90                "Runtime attestation generation and observation time must be positive".into(),
91            );
92        }
93        crate::contract::validate_digest(&self.spec_digest)?;
94        crate::contract::validate_digest(&self.identity_attachment_digest)?;
95        crate::contract::validate_nonempty(
96            "provider_resource_id",
97            &self.provider_resource_id,
98            1024,
99        )?;
100        crate::contract::validate_nonempty("provider_build", &self.provider_build, 255)?;
101        self.evidence.validate()?;
102        self.provider_attestation.validate()?;
103        if self.evidence.provider_build != self.provider_build
104            || self.evidence.spec_digest != self.spec_digest
105            || self.evidence.identity_attachment_digest.as_ref()
106                != Some(&self.identity_attachment_digest)
107        {
108            return Err("Runtime attestation evidence does not match its binding".into());
109        }
110        Ok(())
111    }
112
113    /// Stable digest for a caller-owned durable admission record.
114    pub fn digest(&self) -> Result<String, String> {
115        self.validate()?;
116        #[derive(Serialize)]
117        struct CanonicalBinding<'a> {
118            unit_id: &'a str,
119            generation: u64,
120            class: RuntimeUnitClass,
121            isolation: IsolationLevel,
122            state: RuntimeUnitState,
123            spec_digest: &'a str,
124            identity_attachment_digest: &'a str,
125            provider_resource_id: &'a str,
126            provider_build: &'a str,
127            observed_at_ms: u64,
128            evidence: &'a RuntimeEvidence,
129            provider_attestation: &'a ArtifactRef,
130        }
131        let bytes = serde_json::to_vec(&CanonicalBinding {
132            unit_id: &self.unit_id,
133            generation: self.generation,
134            class: self.class,
135            isolation: self.isolation,
136            state: self.state,
137            spec_digest: &self.spec_digest,
138            identity_attachment_digest: &self.identity_attachment_digest,
139            provider_resource_id: &self.provider_resource_id,
140            provider_build: &self.provider_build,
141            observed_at_ms: self.observed_at_ms,
142            evidence: &self.evidence,
143            provider_attestation: &self.provider_attestation,
144        })
145        .map_err(|error| format!("could not encode Runtime attestation binding: {error}"))?;
146        Ok(format!("sha256:{:x}", Sha256::digest(bytes)))
147    }
148}