1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct PolicyAdmission {
15 pub policy_id: String,
17 pub admitted: bool,
19}
20
21impl PolicyAdmission {
22 pub fn admitted(policy_id: impl Into<String>) -> Self {
24 Self {
25 policy_id: policy_id.into(),
26 admitted: true,
27 }
28 }
29
30 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ArtifactEnvelopeV1 {
42 pub artifact_digest: String,
44 pub signature: Option<Vec<u8>>,
46 pub signer_id: String,
48 pub trusted_timestamp: DateTime<Utc>,
50 pub policy_admission: PolicyAdmission,
52 pub signer_public_key: Option<[u8; 32]>,
54}
55
56impl ArtifactEnvelopeV1 {
57 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 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 pub fn signer_public_key(&self) -> Option<[u8; 32]> {
92 self.signer_public_key
93 }
94
95 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 ×tamp.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#[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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "snake_case")]
260pub enum EnvelopeVerificationStatus {
261 DigestInvalid,
263 DigestValidOnly,
265 SignatureInvalid,
267 SignatureValidSignerUnauthorized,
269 SignerAuthorizedTimeInvalid,
271 TimeValidPolicyRejected,
273 FullyVerified,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct EnvelopeVerificationReport {
280 pub digest_valid: bool,
282 pub signature_valid: bool,
284 pub signer_authorized: bool,
286 pub time_valid: bool,
288 pub policy_admitted: bool,
290 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#[derive(Debug, thiserror::Error)]
316pub enum EnvelopeError {
317 #[error("trusted timestamp is outside the nanosecond range")]
319 TimestampOutOfRange,
320 #[error("invalid Ed25519 signing key")]
322 InvalidSigningKey,
323}