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