Skip to main content

lenso_service/production_delivery/
trust.rs

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}