Skip to main content

attestation_exchange/
lib.rs

1//! Typed attestation exchange surface crate for vendor and trust-root artifacts.
2//!
3//! The crate keeps the compatibility name, but it publishes typed exchange
4//! contracts and bounded profile helpers rather than a transport runtime.
5//!
6//! ## Integration Points
7//!
8//! - **forge-pilot observe phase:** `governance_gate.rs` (`#[cfg(feature = "governance")]`) reads
9//!   attestation envelope validity and trust root set state from semantic-memory projections.
10//! - **forge-pilot act phase:** transparency receipt admissibility judgments gate whether
11//!   artifact provenance is considered trustworthy for execution.
12//! - **verification-control:** attestation envelope and trust root set case types are affected;
13//!   transparency receipts feed into provenance verification cases.
14//! - **Stack Arena scenarios:** governed lane exercises governance observation of attestation
15//!   envelope integrity and trust root validity.
16//!
17//! ## Artifact Families
18//!
19//! | Artifact | Schema Version | Owner |
20//! |----------|---------------|-------|
21//! | [`AttestationEnvelopeV1`] | `attestation_envelope_v1` | this crate |
22//! | [`TrustRootSetV1`] | `trust_root_set_v1` | this crate |
23//! | [`TransparencyReceiptV1`] | `transparency_receipt_v1` | this crate |
24
25pub mod error;
26pub mod profile_p6_vendor;
27
28pub use error::*;
29pub use profile_p6_vendor::*;
30
31use crate::error::{require_non_empty, require_non_empty_slice};
32use schemars::JsonSchema;
33use serde::{Deserialize, Serialize};
34use stack_ids::{
35    ArtifactAdmissionPolicyId, AttestationEnvelopeId, ContentDigest, DisclosurePolicyId,
36    TransparencyReceiptId, TrustRootSetId,
37};
38use std::fmt::{Display, Formatter, Result as FmtResult};
39
40pub const ATTESTATION_ENVELOPE_V1_SCHEMA: &str = "attestation_envelope_v1";
41pub const TRUST_ROOT_SET_V1_SCHEMA: &str = "trust_root_set_v1";
42pub const TRANSPARENCY_RECEIPT_V1_SCHEMA: &str = "transparency_receipt_v1";
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45#[serde(rename_all = "snake_case")]
46pub enum AttestationReplayabilityClassV1 {
47    Replayable,
48    ReplayableWithTicket,
49}
50
51impl Display for AttestationReplayabilityClassV1 {
52    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
53        let value = match self {
54            Self::Replayable => "replayable",
55            Self::ReplayableWithTicket => "replayable_with_ticket",
56        };
57        f.write_str(value)
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
62#[serde(rename_all = "snake_case")]
63pub enum TrustRootExpirationPolicyV1 {
64    #[serde(rename = "365d")]
65    Days365,
66    #[serde(rename = "rotate_every_90_days")]
67    RotateEvery90Days,
68}
69
70impl Display for TrustRootExpirationPolicyV1 {
71    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
72        let value = match self {
73            Self::Days365 => "365d",
74            Self::RotateEvery90Days => "rotate_every_90_days",
75        };
76        f.write_str(value)
77    }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
81#[serde(rename_all = "snake_case")]
82pub enum TrustRootRotationPolicyV1 {
83    PublishedKeyset,
84    Overlap14DaysAndEmitSupersession,
85}
86
87impl Display for TrustRootRotationPolicyV1 {
88    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
89        let value = match self {
90            Self::PublishedKeyset => "published_keyset",
91            Self::Overlap14DaysAndEmitSupersession => "overlap_14_days_and_emit_supersession",
92        };
93        f.write_str(value)
94    }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
98#[serde(rename_all = "snake_case")]
99pub enum TransparencyAdmissibilityJudgmentV1 {
100    Admitted,
101    LoggedAndAdmissible,
102    AdmissibleWithNotedCaveat,
103}
104
105impl Display for TransparencyAdmissibilityJudgmentV1 {
106    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
107        let value = match self {
108            Self::Admitted => "admitted",
109            Self::LoggedAndAdmissible => "logged_and_admissible",
110            Self::AdmissibleWithNotedCaveat => "admissible_with_noted_caveat",
111        };
112        f.write_str(value)
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117pub struct AttestationEnvelopeV1 {
118    pub schema_version: String,
119    pub attestation_envelope_id: AttestationEnvelopeId,
120    pub artifact_family: String,
121    pub artifact_version: String,
122    pub content_digest: ContentDigest,
123    pub schema_identity: String,
124    pub signer_identity: String,
125    pub signing_time: String,
126    pub trust_root_set_id: TrustRootSetId,
127    pub provenance_summary: String,
128    pub disclosure_policy_id: DisclosurePolicyId,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub artifact_admission_policy_id: Option<ArtifactAdmissionPolicyId>,
131    pub replayability_class: AttestationReplayabilityClassV1,
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub revocation_refs: Vec<String>,
134    #[serde(default, skip_serializing_if = "Vec::is_empty")]
135    pub supersession_refs: Vec<String>,
136}
137
138impl AttestationEnvelopeV1 {
139    #[allow(clippy::too_many_arguments)]
140    pub fn new(
141        attestation_envelope_id: AttestationEnvelopeId,
142        artifact_family: impl Into<String>,
143        artifact_version: impl Into<String>,
144        content_digest: ContentDigest,
145        schema_identity: impl Into<String>,
146        signer_identity: impl Into<String>,
147        signing_time: impl Into<String>,
148        trust_root_set_id: TrustRootSetId,
149        provenance_summary: impl Into<String>,
150        disclosure_policy_id: DisclosurePolicyId,
151        artifact_admission_policy_id: Option<ArtifactAdmissionPolicyId>,
152        replayability_class: AttestationReplayabilityClassV1,
153        revocation_refs: Vec<String>,
154        supersession_refs: Vec<String>,
155    ) -> Result<Self, AttestationValidationError> {
156        let value = Self {
157            schema_version: ATTESTATION_ENVELOPE_V1_SCHEMA.to_string(),
158            attestation_envelope_id,
159            artifact_family: artifact_family.into(),
160            artifact_version: artifact_version.into(),
161            content_digest,
162            schema_identity: schema_identity.into(),
163            signer_identity: signer_identity.into(),
164            signing_time: signing_time.into(),
165            trust_root_set_id,
166            provenance_summary: provenance_summary.into(),
167            disclosure_policy_id,
168            artifact_admission_policy_id,
169            replayability_class,
170            revocation_refs,
171            supersession_refs,
172        };
173        value.validate()?;
174        Ok(value)
175    }
176
177    pub fn validate(&self) -> AttestationValidationResult {
178        require_non_empty(
179            self.attestation_envelope_id.as_str(),
180            "attestation_envelope_id",
181        )?;
182        require_non_empty(&self.artifact_family, "artifact_family")?;
183        require_non_empty(&self.artifact_version, "artifact_version")?;
184        require_non_empty(&self.content_digest.0, "content_digest")?;
185        require_non_empty(&self.schema_identity, "schema_identity")?;
186        require_non_empty(&self.signer_identity, "signer_identity")?;
187        require_non_empty(&self.signing_time, "signing_time")?;
188        require_non_empty(self.trust_root_set_id.as_str(), "trust_root_set_id")?;
189        require_non_empty(&self.provenance_summary, "provenance_summary")?;
190        require_non_empty(self.disclosure_policy_id.as_str(), "disclosure_policy_id")?;
191        Ok(())
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
196pub struct TrustRootSetV1 {
197    pub schema_version: String,
198    pub trust_root_set_id: TrustRootSetId,
199    #[serde(default, skip_serializing_if = "Vec::is_empty")]
200    pub trust_root_identities: Vec<String>,
201    #[serde(default, skip_serializing_if = "Vec::is_empty")]
202    pub allowed_signer_classes: Vec<String>,
203    pub expiration_policy: TrustRootExpirationPolicyV1,
204    pub rotation_policy: TrustRootRotationPolicyV1,
205    #[serde(default, skip_serializing_if = "Vec::is_empty")]
206    pub allowed_artifact_families: Vec<String>,
207    #[serde(default, skip_serializing_if = "Vec::is_empty")]
208    pub revocation_sources: Vec<String>,
209    #[serde(default, skip_serializing_if = "Vec::is_empty")]
210    pub policy_owner_refs: Vec<String>,
211}
212
213impl TrustRootSetV1 {
214    #[allow(clippy::too_many_arguments)]
215    pub fn new(
216        trust_root_set_id: TrustRootSetId,
217        trust_root_identities: Vec<String>,
218        allowed_signer_classes: Vec<String>,
219        expiration_policy: TrustRootExpirationPolicyV1,
220        rotation_policy: TrustRootRotationPolicyV1,
221        allowed_artifact_families: Vec<String>,
222        revocation_sources: Vec<String>,
223        policy_owner_refs: Vec<String>,
224    ) -> Result<Self, AttestationValidationError> {
225        let value = Self {
226            schema_version: TRUST_ROOT_SET_V1_SCHEMA.to_string(),
227            trust_root_set_id,
228            trust_root_identities,
229            allowed_signer_classes,
230            expiration_policy,
231            rotation_policy,
232            allowed_artifact_families,
233            revocation_sources,
234            policy_owner_refs,
235        };
236        value.validate()?;
237        Ok(value)
238    }
239
240    pub fn validate(&self) -> AttestationValidationResult {
241        require_non_empty(self.trust_root_set_id.as_str(), "trust_root_set_id")?;
242        require_non_empty_slice(&self.trust_root_identities, "trust_root_identities")?;
243        require_non_empty_slice(&self.allowed_signer_classes, "allowed_signer_classes")?;
244        require_non_empty_slice(&self.allowed_artifact_families, "allowed_artifact_families")?;
245        require_non_empty_slice(&self.revocation_sources, "revocation_sources")?;
246        require_non_empty_slice(&self.policy_owner_refs, "policy_owner_refs")?;
247        Ok(())
248    }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
252pub struct TransparencyReceiptV1 {
253    pub schema_version: String,
254    pub transparency_receipt_id: TransparencyReceiptId,
255    pub attestation_envelope_id: AttestationEnvelopeId,
256    pub registry_identity: String,
257    pub inclusion_material: String,
258    pub recorded_time: String,
259    pub admissibility_judgment: TransparencyAdmissibilityJudgmentV1,
260}
261
262impl TransparencyReceiptV1 {
263    pub fn new(
264        transparency_receipt_id: TransparencyReceiptId,
265        attestation_envelope_id: AttestationEnvelopeId,
266        registry_identity: impl Into<String>,
267        inclusion_material: impl Into<String>,
268        recorded_time: impl Into<String>,
269        admissibility_judgment: TransparencyAdmissibilityJudgmentV1,
270    ) -> Result<Self, AttestationValidationError> {
271        let value = Self {
272            schema_version: TRANSPARENCY_RECEIPT_V1_SCHEMA.to_string(),
273            transparency_receipt_id,
274            attestation_envelope_id,
275            registry_identity: registry_identity.into(),
276            inclusion_material: inclusion_material.into(),
277            recorded_time: recorded_time.into(),
278            admissibility_judgment,
279        };
280        value.validate()?;
281        Ok(value)
282    }
283
284    pub fn validate(&self) -> AttestationValidationResult {
285        require_non_empty(
286            self.transparency_receipt_id.as_str(),
287            "transparency_receipt_id",
288        )?;
289        require_non_empty(
290            self.attestation_envelope_id.as_str(),
291            "attestation_envelope_id",
292        )?;
293        require_non_empty(&self.registry_identity, "registry_identity")?;
294        require_non_empty(&self.inclusion_material, "inclusion_material")?;
295        require_non_empty(&self.recorded_time, "recorded_time")?;
296        Ok(())
297    }
298}