Skip to main content

claim_ledger/
envelope.rs

1//! Signed artifact envelope with staged trust verification.
2
3use chrono::{DateTime, Utc};
4use ring::signature::{Ed25519KeyPair, KeyPair, UnparsedPublicKey, ED25519};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::collections::{BTreeMap, BTreeSet};
8
9const ARTIFACT_DIGEST_DOMAIN: &[u8] = b"recursiveintell:artifact-envelope:digest:v1\0";
10const SIGNATURE_DOMAIN: &[u8] = b"recursiveintell:artifact-envelope:signature:v1\0";
11
12/// Policy decision carried by an artifact envelope.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct PolicyAdmission {
15    /// Stable policy identifier.
16    pub policy_id: String,
17    /// Whether the producing policy admitted the artifact.
18    pub admitted: bool,
19}
20
21impl PolicyAdmission {
22    /// Creates an admitted policy decision.
23    pub fn admitted(policy_id: impl Into<String>) -> Self {
24        Self {
25            policy_id: policy_id.into(),
26            admitted: true,
27        }
28    }
29
30    /// Creates a rejected policy decision.
31    pub fn rejected(policy_id: impl Into<String>) -> Self {
32        Self {
33            policy_id: policy_id.into(),
34            admitted: false,
35        }
36    }
37}
38
39/// Version-one envelope binding content, signer, trusted time, and policy.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ArtifactEnvelopeV1 {
42    /// Domain-separated SHA-256 digest of the artifact bytes.
43    pub artifact_digest: String,
44    /// Ed25519 signature over the envelope identity fields, if present.
45    pub signature: Option<Vec<u8>>,
46    /// Stable signer identity.
47    pub signer_id: String,
48    /// Time asserted by the trusted-time source.
49    pub trusted_timestamp: DateTime<Utc>,
50    /// Policy admission asserted by the producer.
51    pub policy_admission: PolicyAdmission,
52    /// Ed25519 public key declared by the producer.
53    pub signer_public_key: Option<[u8; 32]>,
54}
55
56impl ArtifactEnvelopeV1 {
57    /// Creates an unsigned envelope whose artifact digest is already bound.
58    pub fn unsigned(
59        artifact: &[u8],
60        signer_id: impl Into<String>,
61        trusted_timestamp: DateTime<Utc>,
62        policy_admission: PolicyAdmission,
63    ) -> Self {
64        Self {
65            artifact_digest: artifact_digest(artifact),
66            signature: None,
67            signer_id: signer_id.into(),
68            trusted_timestamp,
69            policy_admission,
70            signer_public_key: None,
71        }
72    }
73
74    /// Signs all trust-relevant envelope identity fields with Ed25519.
75    pub fn sign_ed25519(&mut self, secret_key: &[u8; 32]) -> Result<(), EnvelopeError> {
76        let signing_key = Ed25519KeyPair::from_seed_unchecked(secret_key)
77            .map_err(|_| EnvelopeError::InvalidSigningKey)?;
78        let preimage = self.signature_preimage()?;
79        self.signature = Some(signing_key.sign(&preimage).as_ref().to_vec());
80        self.signer_public_key = Some(
81            signing_key
82                .public_key()
83                .as_ref()
84                .try_into()
85                .map_err(|_| EnvelopeError::InvalidSigningKey)?,
86        );
87        Ok(())
88    }
89
90    /// Returns the producer-declared Ed25519 public key.
91    pub fn signer_public_key(&self) -> Option<[u8; 32]> {
92        self.signer_public_key
93    }
94
95    /// Verifies each trust stage without collapsing distinct failure states.
96    pub fn verify(
97        &self,
98        artifact: &[u8],
99        context: &EnvelopeVerificationContext,
100    ) -> EnvelopeVerificationReport {
101        let digest_valid = self.artifact_digest == artifact_digest(artifact);
102        if !digest_valid {
103            return EnvelopeVerificationReport::at(
104                false,
105                false,
106                false,
107                false,
108                false,
109                EnvelopeVerificationStatus::DigestInvalid,
110            );
111        }
112
113        let signature_present = self.signature.is_some();
114        let signature_valid = self.verify_signature(context);
115        if !signature_present {
116            return EnvelopeVerificationReport::at(
117                true,
118                false,
119                false,
120                false,
121                false,
122                EnvelopeVerificationStatus::DigestValidOnly,
123            );
124        }
125        if !signature_valid {
126            return EnvelopeVerificationReport::at(
127                true,
128                false,
129                false,
130                false,
131                false,
132                EnvelopeVerificationStatus::SignatureInvalid,
133            );
134        }
135
136        let signer_authorized = context.authorized_signers.contains(&self.signer_id);
137        if !signer_authorized {
138            return EnvelopeVerificationReport::at(
139                true,
140                true,
141                false,
142                false,
143                false,
144                EnvelopeVerificationStatus::SignatureValidSignerUnauthorized,
145            );
146        }
147
148        let time_valid = self.trusted_timestamp >= context.not_before
149            && self.trusted_timestamp <= context.not_after;
150        if !time_valid {
151            return EnvelopeVerificationReport::at(
152                true,
153                true,
154                true,
155                false,
156                false,
157                EnvelopeVerificationStatus::SignerAuthorizedTimeInvalid,
158            );
159        }
160
161        let policy_admitted = self.policy_admission.admitted
162            && context
163                .admitted_policies
164                .contains(&self.policy_admission.policy_id);
165        let status = if policy_admitted {
166            EnvelopeVerificationStatus::FullyVerified
167        } else {
168            EnvelopeVerificationStatus::TimeValidPolicyRejected
169        };
170        EnvelopeVerificationReport::at(true, true, true, true, policy_admitted, status)
171    }
172
173    fn verify_signature(&self, context: &EnvelopeVerificationContext) -> bool {
174        let Some(signature_bytes) = self.signature.as_deref() else {
175            return false;
176        };
177        let Some(key_bytes) = context.signer_keys.get(&self.signer_id) else {
178            return false;
179        };
180        let Ok(preimage) = self.signature_preimage() else {
181            return false;
182        };
183        UnparsedPublicKey::new(&ED25519, key_bytes)
184            .verify(&preimage, signature_bytes)
185            .is_ok()
186    }
187
188    fn signature_preimage(&self) -> Result<Vec<u8>, EnvelopeError> {
189        let timestamp = self
190            .trusted_timestamp
191            .timestamp_nanos_opt()
192            .ok_or(EnvelopeError::TimestampOutOfRange)?;
193        let mut preimage = SIGNATURE_DOMAIN.to_vec();
194        for field in [
195            self.artifact_digest.as_bytes(),
196            self.signer_id.as_bytes(),
197            &timestamp.to_be_bytes(),
198            self.policy_admission.policy_id.as_bytes(),
199            &[u8::from(self.policy_admission.admitted)],
200        ] {
201            preimage.extend_from_slice(&(field.len() as u64).to_be_bytes());
202            preimage.extend_from_slice(field);
203        }
204        Ok(preimage)
205    }
206}
207
208fn artifact_digest(artifact: &[u8]) -> String {
209    let mut digest = Sha256::new();
210    digest.update(ARTIFACT_DIGEST_DOMAIN);
211    digest.update((artifact.len() as u64).to_be_bytes());
212    digest.update(artifact);
213    hex::encode(digest.finalize())
214}
215
216/// Verifier-owned trust roots, authorization, time window, and policy set.
217#[derive(Debug, Clone)]
218pub struct EnvelopeVerificationContext {
219    signer_keys: BTreeMap<String, [u8; 32]>,
220    authorized_signers: BTreeSet<String>,
221    admitted_policies: BTreeSet<String>,
222    not_before: DateTime<Utc>,
223    not_after: DateTime<Utc>,
224}
225
226impl EnvelopeVerificationContext {
227    /// Creates a context with an inclusive trusted-time window.
228    pub fn new(not_before: DateTime<Utc>, not_after: DateTime<Utc>) -> Self {
229        Self {
230            signer_keys: BTreeMap::new(),
231            authorized_signers: BTreeSet::new(),
232            admitted_policies: BTreeSet::new(),
233            not_before,
234            not_after,
235        }
236    }
237
238    /// Registers a verification key without authorizing the signer.
239    pub fn with_signer_key(mut self, signer_id: impl Into<String>, key: [u8; 32]) -> Self {
240        self.signer_keys.insert(signer_id.into(), key);
241        self
242    }
243
244    /// Authorizes a registered signer identity.
245    pub fn authorize_signer(mut self, signer_id: impl Into<String>) -> Self {
246        self.authorized_signers.insert(signer_id.into());
247        self
248    }
249
250    /// Admits an envelope policy identifier.
251    pub fn admit_policy(mut self, policy_id: impl Into<String>) -> Self {
252        self.admitted_policies.insert(policy_id.into());
253        self
254    }
255}
256
257/// Highest verification stage reached by an envelope.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "snake_case")]
260pub enum EnvelopeVerificationStatus {
261    /// Artifact bytes do not match the envelope digest.
262    DigestInvalid,
263    /// Digest matches, but no signature is present.
264    DigestValidOnly,
265    /// A signature is present but invalid or unverifiable.
266    SignatureInvalid,
267    /// Signature is valid, but signer identity lacks authorization.
268    SignatureValidSignerUnauthorized,
269    /// Signer is authorized, but trusted time is outside the accepted window.
270    SignerAuthorizedTimeInvalid,
271    /// Time is valid, but policy admission is rejected.
272    TimeValidPolicyRejected,
273    /// Every verification stage passed.
274    FullyVerified,
275}
276
277/// Independent verification-stage results plus the highest reached status.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct EnvelopeVerificationReport {
280    /// Artifact digest matches.
281    pub digest_valid: bool,
282    /// Ed25519 signature verifies.
283    pub signature_valid: bool,
284    /// Signer is authorized by verifier policy.
285    pub signer_authorized: bool,
286    /// Trusted timestamp is inside the verifier window.
287    pub time_valid: bool,
288    /// Producer and verifier both admit the policy.
289    pub policy_admitted: bool,
290    /// Highest verification stage reached.
291    pub status: EnvelopeVerificationStatus,
292}
293
294impl EnvelopeVerificationReport {
295    fn at(
296        digest_valid: bool,
297        signature_valid: bool,
298        signer_authorized: bool,
299        time_valid: bool,
300        policy_admitted: bool,
301        status: EnvelopeVerificationStatus,
302    ) -> Self {
303        Self {
304            digest_valid,
305            signature_valid,
306            signer_authorized,
307            time_valid,
308            policy_admitted,
309            status,
310        }
311    }
312}
313
314/// Artifact-envelope construction errors.
315#[derive(Debug, thiserror::Error)]
316pub enum EnvelopeError {
317    /// Timestamp cannot be represented at nanosecond precision.
318    #[error("trusted timestamp is outside the nanosecond range")]
319    TimestampOutOfRange,
320    /// The provided Ed25519 seed could not create a signing key.
321    #[error("invalid Ed25519 signing key")]
322    InvalidSigningKey,
323}