Skip to main content

plecto_host/
trust.rs

1//! Provenance verification (ADR 000006): the set of keys a `Host` trusts to sign filters, and
2//! the signed artifact material [`Host::load`] verifies before ever touching component bytes.
3
4use anyhow::Result;
5use sigstore::crypto::{CosignVerificationKey, Signature};
6
7use crate::errors::{LoadError, sbom_binds_component};
8
9/// The set of public keys the operator trusts to sign filters (ADR 000006 provenance). A
10/// filter loads only if a trusted key verifies BOTH its component signature and its SBOM
11/// signature (keyed cosign, offline — no Fulcio / Rekor / network). An **empty** policy
12/// trusts no one, so nothing loads: deny-by-default / fail-closed, with no "allow unsigned"
13/// escape hatch in the production API. The keys live on the `Host`, not on each `load` call,
14/// so the operator manages one trust root.
15///
16/// This gates *whether a filter may load at all*. It deliberately does NOT pick the filter's
17/// `Isolation` (trusted/untrusted lifecycle) — a valid signature from a third party's key is
18/// still untrusted code. Mapping signer identity to isolation is left to the declarative
19/// manifest (ADR 000007); here, isolation stays the caller's explicit `LoadOptions` choice.
20pub struct TrustPolicy {
21    keys: Vec<CosignVerificationKey>,
22}
23
24impl TrustPolicy {
25    /// Trust the given public keys (SPKI PEM). The key type is auto-detected — cosign's
26    /// default is ECDSA P-256; P-256 / Ed25519 / RSA cosign keys are all accepted.
27    pub fn from_pem_keys<I, B>(pems: I) -> Result<Self>
28    where
29        I: IntoIterator<Item = B>,
30        B: AsRef<[u8]>,
31    {
32        let keys = pems
33            .into_iter()
34            .map(|pem| {
35                CosignVerificationKey::try_from_pem(pem.as_ref())
36                    .map_err(|e| anyhow::anyhow!("invalid trusted public key (PEM): {e}"))
37            })
38            .collect::<Result<Vec<_>>>()?;
39        Ok(Self { keys })
40    }
41
42    /// An explicitly empty policy — trusts no one, so every load fails closed. Useful to
43    /// assert the fail-closed default.
44    pub fn empty() -> Self {
45        Self { keys: Vec::new() }
46    }
47
48    /// Does ANY trusted key verify this raw (DER) signature over `msg`? cosign ECDSA
49    /// signatures are ASN.1 DER; verification hashes `msg` internally (do not pre-hash).
50    pub(crate) fn verifies(&self, signature_der: &[u8], msg: &[u8]) -> bool {
51        self.keys.iter().any(|k| {
52            k.verify_signature(Signature::Raw(signature_der), msg)
53                .is_ok()
54        })
55    }
56
57    /// The whole ADR 000006 provenance gate over a resolved artifact — SBOM present, both
58    /// signatures trusted, SBOM subject bound to THIS component — with no wasmtime involved.
59    /// [`Host::load`] runs exactly this before ever compiling the bytes; `plecto validate
60    /// --resolve` runs it standalone so CI can prove a manifest + layout pair would pass the
61    /// load gate without starting a server (field report §3.5).
62    ///
63    /// [`Host::load`]: crate::Host::load
64    pub fn verify_artifact(&self, artifact: &SignedArtifact<'_>) -> Result<(), LoadError> {
65        if artifact.sbom.is_empty() {
66            return Err(LoadError::MissingSbom);
67        }
68        if !self.verifies(artifact.component_signature, artifact.component_bytes) {
69            return Err(LoadError::UnverifiedComponentSignature);
70        }
71        if !self.verifies(artifact.sbom_signature, artifact.sbom) {
72            return Err(LoadError::UnverifiedSbomSignature);
73        }
74        sbom_binds_component(artifact.sbom, artifact.component_bytes)
75    }
76}
77
78/// The material the host verifies before instantiating a filter (ADR 000006). The component
79/// bytes plus a keyed cosign signature over them, and a **mandatory** SBOM with its own
80/// signature. Signatures are RAW DER ECDSA bytes: decoding cosign's base64 `.sig` and
81/// fetching the artifact from an OCI registry is the ADR 000007 / `wkg` boundary, kept out
82/// of the host so ADR 000006 (verify) and ADR 000007 (distribute) stay decoupled.
83pub struct SignedArtifact<'a> {
84    /// The WASM component bytes.
85    pub component_bytes: &'a [u8],
86    /// Raw DER signature over `component_bytes` (cosign `sign-blob`).
87    pub component_signature: &'a [u8],
88    /// The SBOM as an in-toto-style statement whose `subject[].digest.sha256` binds it to
89    /// `component_bytes` (verified at load, review f000003 #1). The predicate (the SBOM body)
90    /// stays opaque in v0.1 — content policy (CVE / license scanning) is deferred.
91    pub sbom: &'a [u8],
92    /// Raw DER signature over `sbom`.
93    pub sbom_signature: &'a [u8],
94}