1use std::collections::{BTreeMap, BTreeSet};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6
7use crate::extraction_input_digest;
8
9use super::{
10 DeliveryDecision, DeliveryEffects, DeliveryIssue, DeliveryIssueCode, ReleaseSignature,
11 ServiceRelease, issue, service_release_integrity_is_valid,
12};
13
14pub const RELEASE_TRUST_EVIDENCE_PROTOCOL: &str = "lenso.release-trust-evidence.v1";
15
16#[derive(
17 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
18)]
19#[serde(rename_all = "snake_case")]
20pub enum ReleaseSignerStatus {
21 Trusted,
22 Untrusted,
23 Revoked,
24 Invalid,
25}
26
27pub trait ReleaseTrustProvider: std::fmt::Debug + Send + Sync {
28 fn sign(&self, signer: &str, subject_digest: &str) -> Option<String>;
29
30 fn verify(&self, signer: &str, subject_digest: &str, signature: &str) -> ReleaseSignerStatus;
31}
32
33#[derive(Debug, Clone, Default)]
34pub struct DeterministicTrustProvider {
35 trusted_signers: BTreeMap<String, String>,
36 revoked_signers: BTreeSet<String>,
37}
38
39impl DeterministicTrustProvider {
40 #[must_use]
41 pub fn new<I, K, V>(trusted_signers: I) -> Self
42 where
43 I: IntoIterator<Item = (K, V)>,
44 K: Into<String>,
45 V: Into<String>,
46 {
47 Self {
48 trusted_signers: trusted_signers
49 .into_iter()
50 .map(|(signer, key)| (signer.into(), key.into()))
51 .collect(),
52 revoked_signers: BTreeSet::new(),
53 }
54 }
55
56 #[must_use]
57 pub fn with_revoked(mut self, signer: impl Into<String>) -> Self {
58 self.revoked_signers.insert(signer.into());
59 self
60 }
61
62 fn expected_signature(&self, signer: &str, subject_digest: &str) -> Option<String> {
63 let key = self.trusted_signers.get(signer)?;
64 Some(extraction_input_digest(
65 format!("{key}\0{subject_digest}").as_bytes(),
66 ))
67 }
68}
69
70impl ReleaseTrustProvider for DeterministicTrustProvider {
71 fn sign(&self, signer: &str, subject_digest: &str) -> Option<String> {
72 if self.revoked_signers.contains(signer) {
73 return None;
74 }
75 self.expected_signature(signer, subject_digest)
76 }
77
78 fn verify(&self, signer: &str, subject_digest: &str, signature: &str) -> ReleaseSignerStatus {
79 if self.revoked_signers.contains(signer) {
80 return ReleaseSignerStatus::Revoked;
81 }
82 let Some(expected) = self.expected_signature(signer, subject_digest) else {
83 return ReleaseSignerStatus::Untrusted;
84 };
85 if signature == expected {
86 ReleaseSignerStatus::Trusted
87 } else {
88 ReleaseSignerStatus::Invalid
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
94#[serde(rename_all = "camelCase")]
95pub struct WorkloadTrustEvidence {
96 pub workload_id: String,
97 pub artifact_digest: String,
98 pub sbom_reference: String,
99 pub provenance_reference: String,
100 pub provenance_subject_matches: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
104#[serde(rename_all = "camelCase")]
105pub struct SignatureTrustEvidence {
106 pub signer: String,
107 pub subject_digest: String,
108 pub status: ReleaseSignerStatus,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
112#[serde(rename_all = "camelCase")]
113pub struct ReleaseTrustEvidence {
114 pub protocol: String,
115 pub release_id: String,
116 pub release_digest: String,
117 pub decision: DeliveryDecision,
118 pub evidence_digest: String,
119 pub workloads: Vec<WorkloadTrustEvidence>,
120 pub signatures: Vec<SignatureTrustEvidence>,
121 pub issues: Vec<DeliveryIssue>,
122 pub effects: DeliveryEffects,
123}
124
125pub fn attach_service_release_signature(
126 release: &mut ServiceRelease,
127 provider: &dyn ReleaseTrustProvider,
128 signer: &str,
129) -> Result<(), DeliveryIssue> {
130 if !service_release_integrity_is_valid(release) {
131 return Err(issue(
132 DeliveryIssueCode::ReleaseTampered,
133 "The Service Release content no longer matches its canonical identity.",
134 "Discard the changed release and assemble a new immutable release.",
135 "Assemble the release again before signing.",
136 ));
137 }
138 let Some(signature) = provider.sign(signer, &release.release_digest) else {
139 return Err(issue(
140 DeliveryIssueCode::SignerUntrusted,
141 format!("Signer `{signer}` is not available through the selected trust provider."),
142 "Select an authorized signing identity without exposing its signing material.",
143 "Configure the signer and retry signing.",
144 ));
145 };
146 release.signatures.retain(|item| item.signer != signer);
147 release.signatures.push(ReleaseSignature {
148 signer: signer.to_owned(),
149 subject_digest: release.release_digest.clone(),
150 signature,
151 });
152 release.signatures.sort_by(|left, right| {
153 (&left.signer, &left.subject_digest).cmp(&(&right.signer, &right.subject_digest))
154 });
155 Ok(())
156}
157
158#[must_use]
159pub fn verify_service_release_trust(
160 release: &ServiceRelease,
161 provider: &dyn ReleaseTrustProvider,
162) -> ReleaseTrustEvidence {
163 let mut issues = Vec::new();
164 if !service_release_integrity_is_valid(release) {
165 issues.push(issue(
166 DeliveryIssueCode::ReleaseTampered,
167 "The Service Release content no longer matches its signed canonical identity.",
168 "Discard the changed content and assemble and sign a new release.",
169 "Reassemble the release before any protected action.",
170 ));
171 return trust_evidence(release, Vec::new(), Vec::new(), issues);
172 }
173
174 let workloads = release
175 .workloads
176 .iter()
177 .map(|workload| {
178 let provenance_subject_matches = workload
179 .provenance
180 .subject_digests
181 .iter()
182 .any(|digest| digest == &workload.artifact_digest);
183 if !provenance_subject_matches {
184 issues.push(DeliveryIssue {
185 code: DeliveryIssueCode::ProvenanceSubjectMismatch,
186 message: format!(
187 "Provenance for Workload `{}` does not name its exact artifact digest.",
188 workload.workload_id
189 ),
190 evidence_references: vec![workload.provenance.reference.clone()],
191 remediation: "Regenerate provenance whose subjects include the exact Workload artifact digest.".to_owned(),
192 next_actions: vec!["Attach matching provenance and verify the release again.".to_owned()],
193 });
194 }
195 WorkloadTrustEvidence {
196 workload_id: workload.workload_id.clone(),
197 artifact_digest: workload.artifact_digest.clone(),
198 sbom_reference: workload.sbom.reference.clone(),
199 provenance_reference: workload.provenance.reference.clone(),
200 provenance_subject_matches,
201 }
202 })
203 .collect::<Vec<_>>();
204
205 if release.signatures.is_empty() {
206 issues.push(issue(
207 DeliveryIssueCode::SignatureMissing,
208 "The Service Release has no signature over its canonical identity.",
209 "Sign the release digest with a trusted release identity.",
210 "Sign the release and verify it again.",
211 ));
212 }
213 let signatures = release
214 .signatures
215 .iter()
216 .map(|signature| {
217 let status = if signature.subject_digest != release.release_digest {
218 ReleaseSignerStatus::Invalid
219 } else {
220 provider.verify(
221 &signature.signer,
222 &signature.subject_digest,
223 &signature.signature,
224 )
225 };
226 if status != ReleaseSignerStatus::Trusted {
227 let (code, message) = match status {
228 ReleaseSignerStatus::Untrusted => (
229 DeliveryIssueCode::SignerUntrusted,
230 format!("Release signer `{}` is not trusted.", signature.signer),
231 ),
232 ReleaseSignerStatus::Revoked => (
233 DeliveryIssueCode::SignerRevoked,
234 format!("Release signer `{}` is revoked.", signature.signer),
235 ),
236 ReleaseSignerStatus::Invalid => (
237 DeliveryIssueCode::SignatureInvalid,
238 format!("Release signature from `{}` is invalid.", signature.signer),
239 ),
240 ReleaseSignerStatus::Trusted => unreachable!(),
241 };
242 issues.push(issue(
243 code,
244 message,
245 "Use an authorized, non-revoked signer and an exact release digest subject.",
246 "Correct the signature and verify the release again.",
247 ));
248 }
249 SignatureTrustEvidence {
250 signer: signature.signer.clone(),
251 subject_digest: signature.subject_digest.clone(),
252 status,
253 }
254 })
255 .collect::<Vec<_>>();
256
257 trust_evidence(release, workloads, signatures, issues)
258}
259
260fn trust_evidence(
261 release: &ServiceRelease,
262 workloads: Vec<WorkloadTrustEvidence>,
263 signatures: Vec<SignatureTrustEvidence>,
264 issues: Vec<DeliveryIssue>,
265) -> ReleaseTrustEvidence {
266 #[derive(Serialize)]
267 #[serde(rename_all = "camelCase")]
268 struct EvidenceContent<'a> {
269 protocol: &'a str,
270 release_id: &'a str,
271 release_digest: &'a str,
272 workloads: &'a [WorkloadTrustEvidence],
273 signatures: &'a [SignatureTrustEvidence],
274 issues: &'a [DeliveryIssue],
275 }
276
277 let content = EvidenceContent {
278 protocol: RELEASE_TRUST_EVIDENCE_PROTOCOL,
279 release_id: &release.release_id,
280 release_digest: &release.release_digest,
281 workloads: &workloads,
282 signatures: &signatures,
283 issues: &issues,
284 };
285 let evidence_digest = extraction_input_digest(
286 serde_json::to_vec(&content).expect("trust evidence must serialize"),
287 );
288 ReleaseTrustEvidence {
289 protocol: RELEASE_TRUST_EVIDENCE_PROTOCOL.to_owned(),
290 release_id: release.release_id.clone(),
291 release_digest: release.release_digest.clone(),
292 decision: if issues.is_empty() {
293 DeliveryDecision::Passed
294 } else {
295 DeliveryDecision::Blocked
296 },
297 evidence_digest,
298 workloads,
299 signatures,
300 issues,
301 effects: DeliveryEffects::default(),
302 }
303}
304
305#[must_use]
306pub fn release_trust_evidence_integrity_is_valid(
307 evidence: &ReleaseTrustEvidence,
308 release: &ServiceRelease,
309 provider: &dyn ReleaseTrustProvider,
310) -> bool {
311 service_release_integrity_is_valid(release)
312 && evidence == &verify_service_release_trust(release, provider)
313}