lenso-service 0.1.24

Public contracts for Lenso Providers and Autonomous Services.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::extraction_input_digest;

use super::{
    DeliveryDecision, DeliveryEffects, DeliveryIssue, DeliveryIssueCode, ReleaseSignature,
    ServiceRelease, issue, service_release_integrity_is_valid,
};

pub const RELEASE_TRUST_EVIDENCE_PROTOCOL: &str = "lenso.release-trust-evidence.v1";

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ReleaseSignerStatus {
    Trusted,
    Untrusted,
    Revoked,
    Invalid,
}

pub trait ReleaseTrustProvider: std::fmt::Debug + Send + Sync {
    fn sign(&self, signer: &str, subject_digest: &str) -> Option<String>;

    fn verify(&self, signer: &str, subject_digest: &str, signature: &str) -> ReleaseSignerStatus;
}

#[derive(Debug, Clone, Default)]
pub struct DeterministicTrustProvider {
    trusted_signers: BTreeMap<String, String>,
    revoked_signers: BTreeSet<String>,
}

impl DeterministicTrustProvider {
    #[must_use]
    pub fn new<I, K, V>(trusted_signers: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<String>,
    {
        Self {
            trusted_signers: trusted_signers
                .into_iter()
                .map(|(signer, key)| (signer.into(), key.into()))
                .collect(),
            revoked_signers: BTreeSet::new(),
        }
    }

    #[must_use]
    pub fn with_revoked(mut self, signer: impl Into<String>) -> Self {
        self.revoked_signers.insert(signer.into());
        self
    }

    fn expected_signature(&self, signer: &str, subject_digest: &str) -> Option<String> {
        let key = self.trusted_signers.get(signer)?;
        Some(extraction_input_digest(
            format!("{key}\0{subject_digest}").as_bytes(),
        ))
    }
}

impl ReleaseTrustProvider for DeterministicTrustProvider {
    fn sign(&self, signer: &str, subject_digest: &str) -> Option<String> {
        if self.revoked_signers.contains(signer) {
            return None;
        }
        self.expected_signature(signer, subject_digest)
    }

    fn verify(&self, signer: &str, subject_digest: &str, signature: &str) -> ReleaseSignerStatus {
        if self.revoked_signers.contains(signer) {
            return ReleaseSignerStatus::Revoked;
        }
        let Some(expected) = self.expected_signature(signer, subject_digest) else {
            return ReleaseSignerStatus::Untrusted;
        };
        if signature == expected {
            ReleaseSignerStatus::Trusted
        } else {
            ReleaseSignerStatus::Invalid
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkloadTrustEvidence {
    pub workload_id: String,
    pub artifact_digest: String,
    pub sbom_reference: String,
    pub provenance_reference: String,
    pub provenance_subject_matches: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SignatureTrustEvidence {
    pub signer: String,
    pub subject_digest: String,
    pub status: ReleaseSignerStatus,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseTrustEvidence {
    pub protocol: String,
    pub release_id: String,
    pub release_digest: String,
    pub decision: DeliveryDecision,
    pub evidence_digest: String,
    pub workloads: Vec<WorkloadTrustEvidence>,
    pub signatures: Vec<SignatureTrustEvidence>,
    pub issues: Vec<DeliveryIssue>,
    pub effects: DeliveryEffects,
}

pub fn attach_service_release_signature(
    release: &mut ServiceRelease,
    provider: &dyn ReleaseTrustProvider,
    signer: &str,
) -> Result<(), DeliveryIssue> {
    if !service_release_integrity_is_valid(release) {
        return Err(issue(
            DeliveryIssueCode::ReleaseTampered,
            "The Service Release content no longer matches its canonical identity.",
            "Discard the changed release and assemble a new immutable release.",
            "Assemble the release again before signing.",
        ));
    }
    let Some(signature) = provider.sign(signer, &release.release_digest) else {
        return Err(issue(
            DeliveryIssueCode::SignerUntrusted,
            format!("Signer `{signer}` is not available through the selected trust provider."),
            "Select an authorized signing identity without exposing its signing material.",
            "Configure the signer and retry signing.",
        ));
    };
    release.signatures.retain(|item| item.signer != signer);
    release.signatures.push(ReleaseSignature {
        signer: signer.to_owned(),
        subject_digest: release.release_digest.clone(),
        signature,
    });
    release.signatures.sort_by(|left, right| {
        (&left.signer, &left.subject_digest).cmp(&(&right.signer, &right.subject_digest))
    });
    Ok(())
}

#[must_use]
pub fn verify_service_release_trust(
    release: &ServiceRelease,
    provider: &dyn ReleaseTrustProvider,
) -> ReleaseTrustEvidence {
    let mut issues = Vec::new();
    if !service_release_integrity_is_valid(release) {
        issues.push(issue(
            DeliveryIssueCode::ReleaseTampered,
            "The Service Release content no longer matches its signed canonical identity.",
            "Discard the changed content and assemble and sign a new release.",
            "Reassemble the release before any protected action.",
        ));
        return trust_evidence(release, Vec::new(), Vec::new(), issues);
    }

    let workloads = release
        .workloads
        .iter()
        .map(|workload| {
            let provenance_subject_matches = workload
                .provenance
                .subject_digests
                .iter()
                .any(|digest| digest == &workload.artifact_digest);
            if !provenance_subject_matches {
                issues.push(DeliveryIssue {
                    code: DeliveryIssueCode::ProvenanceSubjectMismatch,
                    message: format!(
                        "Provenance for Workload `{}` does not name its exact artifact digest.",
                        workload.workload_id
                    ),
                    evidence_references: vec![workload.provenance.reference.clone()],
                    remediation: "Regenerate provenance whose subjects include the exact Workload artifact digest.".to_owned(),
                    next_actions: vec!["Attach matching provenance and verify the release again.".to_owned()],
                });
            }
            WorkloadTrustEvidence {
                workload_id: workload.workload_id.clone(),
                artifact_digest: workload.artifact_digest.clone(),
                sbom_reference: workload.sbom.reference.clone(),
                provenance_reference: workload.provenance.reference.clone(),
                provenance_subject_matches,
            }
        })
        .collect::<Vec<_>>();

    if release.signatures.is_empty() {
        issues.push(issue(
            DeliveryIssueCode::SignatureMissing,
            "The Service Release has no signature over its canonical identity.",
            "Sign the release digest with a trusted release identity.",
            "Sign the release and verify it again.",
        ));
    }
    let signatures = release
        .signatures
        .iter()
        .map(|signature| {
            let status = if signature.subject_digest != release.release_digest {
                ReleaseSignerStatus::Invalid
            } else {
                provider.verify(
                    &signature.signer,
                    &signature.subject_digest,
                    &signature.signature,
                )
            };
            if status != ReleaseSignerStatus::Trusted {
                let (code, message) = match status {
                    ReleaseSignerStatus::Untrusted => (
                        DeliveryIssueCode::SignerUntrusted,
                        format!("Release signer `{}` is not trusted.", signature.signer),
                    ),
                    ReleaseSignerStatus::Revoked => (
                        DeliveryIssueCode::SignerRevoked,
                        format!("Release signer `{}` is revoked.", signature.signer),
                    ),
                    ReleaseSignerStatus::Invalid => (
                        DeliveryIssueCode::SignatureInvalid,
                        format!("Release signature from `{}` is invalid.", signature.signer),
                    ),
                    ReleaseSignerStatus::Trusted => unreachable!(),
                };
                issues.push(issue(
                    code,
                    message,
                    "Use an authorized, non-revoked signer and an exact release digest subject.",
                    "Correct the signature and verify the release again.",
                ));
            }
            SignatureTrustEvidence {
                signer: signature.signer.clone(),
                subject_digest: signature.subject_digest.clone(),
                status,
            }
        })
        .collect::<Vec<_>>();

    trust_evidence(release, workloads, signatures, issues)
}

fn trust_evidence(
    release: &ServiceRelease,
    workloads: Vec<WorkloadTrustEvidence>,
    signatures: Vec<SignatureTrustEvidence>,
    issues: Vec<DeliveryIssue>,
) -> ReleaseTrustEvidence {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct EvidenceContent<'a> {
        protocol: &'a str,
        release_id: &'a str,
        release_digest: &'a str,
        workloads: &'a [WorkloadTrustEvidence],
        signatures: &'a [SignatureTrustEvidence],
        issues: &'a [DeliveryIssue],
    }

    let content = EvidenceContent {
        protocol: RELEASE_TRUST_EVIDENCE_PROTOCOL,
        release_id: &release.release_id,
        release_digest: &release.release_digest,
        workloads: &workloads,
        signatures: &signatures,
        issues: &issues,
    };
    let evidence_digest = extraction_input_digest(
        serde_json::to_vec(&content).expect("trust evidence must serialize"),
    );
    ReleaseTrustEvidence {
        protocol: RELEASE_TRUST_EVIDENCE_PROTOCOL.to_owned(),
        release_id: release.release_id.clone(),
        release_digest: release.release_digest.clone(),
        decision: if issues.is_empty() {
            DeliveryDecision::Passed
        } else {
            DeliveryDecision::Blocked
        },
        evidence_digest,
        workloads,
        signatures,
        issues,
        effects: DeliveryEffects::default(),
    }
}

#[must_use]
pub fn release_trust_evidence_integrity_is_valid(
    evidence: &ReleaseTrustEvidence,
    release: &ServiceRelease,
    provider: &dyn ReleaseTrustProvider,
) -> bool {
    service_release_integrity_is_valid(release)
        && evidence == &verify_service_release_trust(release, provider)
}