From 7bea70955dc6ac0ce730966f547f82b67ba564ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Mon, 26 Oct 2026 17:05:25 +0100 Subject: [PATCH 1/5] feat(cosign): verify keyless signatures with sigstore-verify Keyless cosign signatures (a Fulcio certificate, a Rekor entry and an RFC 3161 timestamp in a v0.3 Sigstore bundle) are verified through the sigstore org's sigstore-verify crate against a Sigstore trusted root the new --cosign-trusted-root flag names. The crate establishes the signing time from the bundle's RFC 3161 timestamps and its Rekor entry's signed entry timestamp, verifies the inclusion proof and checkpoint, chains the certificate at that time and checks the signature; the runtime then requires the statement to be a cosign signature of the pinned digest and the signer to be trusted for the module's repository by the operator policy's new trustSigner rules (context.issuer, context.identity over the Repository hierarchy, default-deny). The sigstore 0.14 crate is replaced: key-based signatures keep their contract (the keys are the trust root, no transparency-log entry is required or read) on sigstore-crypto's VerificationKey, because sigstore-verify's public-key policy refuses a bundle with neither a log entry nor a timestamp, which is what cosign writes with --tlog-upload=false. The operator policy is shared with the verifier as an Arc. reqwest's query and json features, which the removed crate had enabled transitively, are declared. The fixture under testdata/cosign is the real keyless signature this repository's release workflow attached to the published pdb-addon example; the tests verify it offline against the trusted root embedded in sigstore-trust-root and refuse it for another digest, under another trusted root, tampered, and when no trustSigner rule trusts the signer. Spike for #116. --- Cargo.lock | 1469 +++-------------- crates/function/Cargo.toml | 8 +- crates/function/src/authz.rs | 72 +- crates/function/src/cosign.rs | 485 +++++- crates/function/src/main.rs | 42 +- crates/function/src/runner.rs | 21 +- crates/function/src/validate.rs | 33 +- crates/function/testdata/cosign/README.md | 16 + .../pdb-addon-v0.1.0-keyless.bundle.json | 1 + .../testdata/cosign/pdb-addon-v0.1.0.digest | 1 + crates/function/tests/guests.rs | 2 +- 11 files changed, 793 insertions(+), 1357 deletions(-) create mode 100644 crates/function/testdata/cosign/README.md create mode 100644 crates/function/testdata/cosign/pdb-addon-v0.1.0-keyless.bundle.json create mode 100644 crates/function/testdata/cosign/pdb-addon-v0.1.0.digest diff --git a/Cargo.lock b/Cargo.lock index b5fbb93c..6d9f32a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,27 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "ahash" version = "0.8.12" @@ -197,28 +176,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-fips-sys" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118303cd75f63d1933a90c2ceb7e697281ac6acbdbcc490b46419f25a527ab90" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", - "regex", -] - [[package]] name = "aws-lc-rs" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ - "aws-lc-fips-sys", "aws-lc-sys", "untrusted 0.7.1", "zeroize", @@ -230,7 +193,6 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ - "bindgen", "cc", "cmake", "dunce", @@ -281,24 +243,12 @@ dependencies = [ "tower-service", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base16ct" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -317,26 +267,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.10.5", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn 2.0.119", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -382,15 +312,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "borrow-or-share" version = "0.2.4" @@ -437,15 +358,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "cc" version = "1.4.4" @@ -455,7 +367,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", + "shlex", ] [[package]] @@ -522,15 +434,6 @@ dependencies = [ "smol_str", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -551,7 +454,7 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -561,46 +464,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout 0.1.4", - "zeroize", -] - -[[package]] -name = "cipher" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" -dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", - "inout 0.2.2", -] - -[[package]] -name = "clang-sys" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.6.7" @@ -656,6 +524,18 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "x509-cert", +] + [[package]] name = "cobs" version = "0.3.0" @@ -710,27 +590,6 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -961,18 +820,6 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-bigint" version = "0.7.5" @@ -984,7 +831,7 @@ dependencies = [ "getrandom 0.4.3", "hybrid-array", "num-traits", - "rand_core 0.10.1", + "rand_core", "subtle", "zeroize", ] @@ -1007,22 +854,7 @@ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "getrandom 0.4.3", "hybrid-array", - "rand_core 0.10.1", -] - -[[package]] -name = "crypto_secretbox" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" -dependencies = [ - "aead", - "cipher 0.4.4", - "generic-array", - "poly1305", - "salsa20 0.10.2", - "subtle", - "zeroize", + "rand_core", ] [[package]] @@ -1035,65 +867,14 @@ dependencies = [ "subtle", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", + "darling_core", + "darling_macro", ] [[package]] @@ -1109,24 +890,13 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.119", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.119", ] @@ -1221,37 +991,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.119", -] - [[package]] name = "digest" version = "0.10.7" @@ -1259,9 +998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.7", - "subtle", ] [[package]] @@ -1297,16 +1034,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.7" @@ -1336,20 +1063,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der 0.7.10", - "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", -] - [[package]] name = "ecdsa" version = "0.17.0" @@ -1358,37 +1071,13 @@ checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der 0.8.1", "digest 0.11.3", - "elliptic-curve 0.14.1", - "rfc6979 0.6.0", - "signature 3.0.0", + "elliptic-curve", + "rfc6979", + "signature", "spki 0.8.0", "zeroize", ] -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - [[package]] name = "educe" version = "0.8.1" @@ -1407,44 +1096,23 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", - "digest 0.10.7", - "ff 0.13.1", - "generic-array", - "group 0.13.0", - "hkdf", - "pem-rfc7468 0.7.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ - "base16ct 1.0.0", - "crypto-bigint 0.7.5", + "base16ct", + "crypto-bigint", "crypto-common 0.2.2", "digest 0.11.3", - "ff 0.14.0", - "group 0.14.0", + "ff", + "group", "hybrid-array", "pem-rfc7468 1.0.0", - "pkcs8 0.11.0", - "rand_core 0.10.1", - "sec1 0.8.1", + "pkcs8", + "rand_core", + "sec1", "subtle", "zeroize", ] @@ -1541,32 +1209,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "ff" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.10.1", + "rand_core", "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "filetime" version = "0.2.29" @@ -1712,16 +1364,18 @@ dependencies = [ "http", "http-body", "jsonschema", - "p256 0.14.0", + "p256", "pbjson-types", - "pem 4.0.0", + "pem", "prost", "reqwest", "serde", "serde_json", "serde_yaml", "sha2 0.11.1", - "sigstore", + "sigstore-trust-root", + "sigstore-types", + "sigstore-verify", "tar", "tempfile", "tokio", @@ -1798,18 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "futures-sink" +name = "futures-sink" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" @@ -1829,7 +1472,6 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1859,7 +1501,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -1899,21 +1540,10 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", - "rand_core 0.10.1", + "rand_core", "wasm-bindgen", ] -[[package]] -name = "getset" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "gimli" version = "0.33.0" @@ -1926,31 +1556,14 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff 0.13.1", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "group" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ - "ff 0.14.0", - "rand_core 0.10.1", + "ff", + "rand_core", "subtle", ] @@ -2037,30 +1650,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0" @@ -2080,15 +1669,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-auth" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" -dependencies = [ - "memchr", -] - [[package]] name = "http-body" version = "1.1.0" @@ -2390,25 +1970,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "hybrid-array", -] - [[package]] name = "ipnet" version = "2.12.1" @@ -2421,15 +1982,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2599,9 +2151,9 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.60.2" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42a11b47492e6a2cc76e9c7bcc8077776cb31dac444e0795d7f61bcc6441c08" +checksum = "65de752665c4a06528af6759955eecf60ba73ae7442d35349963a09c8031cd71" dependencies = [ "ahash", "bytecount", @@ -2623,25 +2175,25 @@ dependencies = [ "rustls", "serde", "serde_json", - "strum 0.28.0", + "strum", "unicode-general-category", "uuid-simd", ] [[package]] name = "jsonschema-regex" -version = "0.60.2" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107aeb6ff14b8deb5610096a8ce5c554ec89806b521147eaeb3cae05b07edb7a" +checksum = "6f8a848331614144c03e815fb931f238c9e342bd7ab501b29e35d647e7cd5688" dependencies = [ "regex-syntax", ] [[package]] name = "jsonschema-value" -version = "0.60.2" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281d6b177bb4834f2ffd6f92aa0443cffe29c996b251c8d94eb6f456186feba" +checksum = "5a5a947ef798eeacacb43edf29b1d0ab64bdf5e2c9fbcaad10ee8663d922b544" dependencies = [ "ahash", "bytecount", @@ -2654,22 +2206,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "jsonwebtoken" -version = "10.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" -dependencies = [ - "aws-lc-rs", - "base64 0.22.1", - "getrandom 0.2.17", - "js-sys", - "serde", - "serde_json", - "signature 2.2.0", - "zeroize", -] - [[package]] name = "keccak" version = "0.1.6" @@ -2679,21 +2215,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - [[package]] name = "lalrpop" version = "0.22.2" @@ -2731,9 +2252,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -2747,16 +2265,6 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - [[package]] name = "libm" version = "0.2.16" @@ -2926,22 +2434,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.9.1" @@ -2969,28 +2461,12 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nonempty" version = "0.12.0" @@ -3033,22 +2509,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.8", - "smallvec", - "zeroize", -] - [[package]] name = "num-cmp" version = "0.1.0" @@ -3107,74 +2567,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", -] - -[[package]] -name = "oauth2" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" -dependencies = [ - "base64 0.21.7", - "chrono", - "getrandom 0.2.17", - "http", - "rand 0.8.8", - "serde", - "serde_json", - "serde_path_to_error", - "sha2 0.10.9", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", ] [[package]] @@ -3198,61 +2590,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oci-client" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5261a7fb43d9c53b8e63e6d5e86860719dad253d015d022066c72d585125aed8" -dependencies = [ - "bytes", - "chrono", - "futures-util", - "hex", - "http", - "http-auth", - "jsonwebtoken", - "lazy_static", - "oci-spec", - "olpc-cjson", - "regex", - "reqwest", - "serde", - "serde_json", - "sha2 0.11.1", - "thiserror 2.0.21", - "tokio", - "tracing", - "unicase", -] - -[[package]] -name = "oci-spec" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8445a2631507cec628a15fdd6154b54a3ab3f20ed4fe9d73a3b8b7a4e1ba03a" -dependencies = [ - "const_format", - "derive_builder", - "getset", - "regex", - "serde", - "serde_json", - "strum 0.27.2", - "strum_macros 0.27.2", - "thiserror 2.0.21", -] - -[[package]] -name = "olpc-cjson" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" -dependencies = [ - "serde", - "serde_json", - "unicode-normalization", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -3265,101 +2602,31 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openidconnect" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" -dependencies = [ - "base64 0.21.7", - "chrono", - "dyn-clone", - "ed25519-dalek", - "hmac 0.12.1", - "http", - "itertools 0.10.5", - "log", - "oauth2", - "p256 0.13.2", - "p384", - "rand 0.8.8", - "rsa", - "serde", - "serde-value", - "serde_json", - "serde_path_to_error", - "serde_plain", - "serde_with", - "sha2 0.10.9", - "subtle", - "thiserror 1.0.69", - "url", -] - [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - [[package]] name = "p256" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ - "ecdsa 0.17.0", - "elliptic-curve 0.14.1", + "ecdsa", + "elliptic-curve", "primefield", - "primeorder 0.14.0", + "primeorder", "sha2 0.11.1", ] -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -3383,15 +2650,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "password-hash" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" -dependencies = [ - "phc", -] - [[package]] name = "pbjson" version = "0.9.0" @@ -3429,36 +2687,6 @@ dependencies = [ "serde", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - -[[package]] -name = "pbkdf2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" -dependencies = [ - "digest 0.11.3", - "hmac 0.13.0", -] - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - [[package]] name = "pem" version = "4.0.0" @@ -3524,16 +2752,6 @@ dependencies = [ "indexmap 2.14.0", ] -[[package]] -name = "phc" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" -dependencies = [ - "base64ct", - "ctutils", -] - [[package]] name = "phf_shared" version = "0.11.3" @@ -3575,44 +2793,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes", - "cbc", - "der 0.7.10", - "pbkdf2 0.12.2", - "scrypt 0.11.0", - "sha2 0.10.9", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "pkcs5", - "rand_core 0.6.4", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.11.0" @@ -3629,17 +2809,6 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "portable-atomic" version = "1.15.0" @@ -3682,15 +2851,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" @@ -3724,30 +2884,21 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ - "crypto-bigint 0.7.5", + "crypto-bigint", "crypto-common 0.2.2", - "ff 0.14.0", - "rand_core 0.10.1", + "ff", + "rand_core", "subtle", "zeroize", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve 0.13.8", -] - [[package]] name = "primeorder" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ - "elliptic-curve 0.14.1", + "elliptic-curve", "once_cell", "primefield", "serdect", @@ -3828,41 +2979,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "prost-reflect" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b80ea363c31af2de2b92e3c07ed1156628f7838c4afb4df75ee78a37fedbd1" -dependencies = [ - "base64 0.22.1", - "prost", - "prost-reflect-derive", - "prost-types", - "serde", - "serde-value", -] - -[[package]] -name = "prost-reflect-build" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a9e8261adf6617d5dc2a5a9e75cce5ab9d546a007f6f870f809a1ad25386b6" -dependencies = [ - "prost-build", - "prost-reflect", -] - -[[package]] -name = "prost-reflect-derive" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30320eb03b43b7dfcaf9b361f808a4f1adad1e718ad219df1d7e4283e34e73f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "prost-types" version = "0.14.4" @@ -3935,7 +3051,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand 0.10.2", + "rand", "rand_pcg", "ring", "rustc-hash", @@ -3983,17 +3099,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.10.2" @@ -4002,26 +3107,7 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -4036,7 +3122,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -4101,9 +3187,9 @@ dependencies = [ [[package]] name = "referencing" -version = "0.60.2" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f42818036d3b37cda6bd4c494543158a36e431cb717015ef7c93acc6ba214d3b" +checksum = "922268812b6f9e20b4ab0f1efe8e07036958eed7346ceddb31c777b5531fd11f" dependencies = [ "ahash", "fluent-uri", @@ -4180,7 +3266,6 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime_guess", "percent-encoding", "pin-project-lite", "quinn", @@ -4193,25 +3278,13 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac 0.12.1", - "subtle", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -4220,8 +3293,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "crypto-bigint 0.7.5", - "hmac 0.13.0", + "crypto-bigint", + "hmac", ] [[package]] @@ -4238,26 +3311,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "rustc-demangle" version = "0.1.28" @@ -4402,25 +3455,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "salsa20" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" -dependencies = [ - "cfg-if", - "cipher 0.5.2", -] - [[package]] name = "same-file" version = "1.0.6" @@ -4469,51 +3503,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2 0.12.2", - "salsa20 0.10.2", - "sha2 0.10.9", -] - -[[package]] -name = "scrypt" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" -dependencies = [ - "cfg-if", - "password-hash", - "pbkdf2 0.13.0", - "salsa20 0.11.0", - "sha2 0.11.1", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", - "generic-array", - "pkcs8 0.10.2", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ - "base16ct 1.0.0", + "base16ct", "ctutils", "der 0.8.1", "hybrid-array", @@ -4564,16 +3560,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_core" version = "1.0.229" @@ -4611,46 +3597,15 @@ dependencies = [ [[package]] name = "serde_json_canonicalizer" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe52319a927259afbfa5180c5157cd8167edfd3e8c254f9558c7fef44c5649f2" +checksum = "87f7bb64f8c7e9f783fdfe883b5fe93a90bee1597c63705baa91842c5f698620" dependencies = [ "ryu-js", "serde", "serde_json", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_plain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "serde_spanned" version = "1.1.1" @@ -4699,7 +3654,7 @@ version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.119", @@ -4724,21 +3679,10 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" dependencies = [ - "base16ct 1.0.0", + "base16ct", "serde", ] -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha2" version = "0.10.9" @@ -4780,12 +3724,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" @@ -4804,94 +3742,146 @@ dependencies = [ [[package]] name = "signature" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", + "digest 0.11.3", + "rand_core", ] [[package]] -name = "signature" -version = "3.0.0" +name = "sigstore-bundle" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +checksum = "442bc4f672eef040397e65d4ae34ca1b1dc8140b1a35b1246e0b219ce5f4e571" dependencies = [ - "digest 0.11.3", - "rand_core 0.10.1", + "jiff", + "sigstore-types", + "thiserror 2.0.21", ] [[package]] -name = "sigstore" -version = "0.14.0" +name = "sigstore-crypto" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4df549c175469befae9f95dcfa52cda15b11a4a72ade1f43a36e90cd90ba88a" +checksum = "045039ae523e86a8b856470a6fc8e9a4567bb61e723ec08a4d23fa8ce40167e9" dependencies = [ - "async-trait", "aws-lc-rs", - "base64 0.22.1", - "cfg-if", - "chrono", "const-oid 0.9.6", - "crypto_secretbox", - "digest 0.10.7", - "hex", - "hex-literal", - "oci-client", - "openidconnect", - "pem 3.0.6", - "pkcs8 0.10.2", - "regex", - "reqwest", + "der 0.7.10", + "futures-io", + "jiff", "rustls-pki-types", - "rustls-webpki", - "scrypt 0.12.0", + "sigstore-types", + "spki 0.7.3", + "thiserror 2.0.21", + "x509-cert", +] + +[[package]] +name = "sigstore-merkle" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22923d7f1a03e21a3313a4a2e6a6485959327a2a084df09b4fff45810e46f08a" +dependencies = [ + "sigstore-crypto", + "sigstore-types", + "thiserror 2.0.21", +] + +[[package]] +name = "sigstore-rekor" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a46ca1954e5e92b8a35eeefb745f55a1eff2d82c6b80f946e733671b33508540" +dependencies = [ + "base64 0.23.1", + "hex", + "jiff", "serde", "serde_json", - "serde_json_canonicalizer", - "serde_repr", - "serde_with", - "sha2 0.10.9", - "sigstore_protobuf_specs", + "sigstore-merkle", + "sigstore-types", + "thiserror 2.0.21", +] + +[[package]] +name = "sigstore-trust-root" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a49e390a5f52e0b05f1a17820be5ac747a3890bb9f54156d8a1147508420a7" +dependencies = [ + "jiff", + "serde", + "serde_json", + "sigstore-crypto", + "sigstore-types", + "thiserror 2.0.21", +] + +[[package]] +name = "sigstore-tsa" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36aa9c47f3480420ccb90753c5f3aae07823c489ab20762f0c96070ba35cd5cb" +dependencies = [ + "cms", + "const-oid 0.9.6", + "der 0.7.10", + "hex", + "jiff", + "rustls-pki-types", + "rustls-webpki", + "sigstore-crypto", + "sigstore-types", "thiserror 2.0.21", - "tls_codec", - "tokio", - "tokio-util", "tracing", - "url", - "webbrowser", "x509-cert", - "zeroize", ] [[package]] -name = "sigstore-protobuf-specs-derive" -version = "0.0.1" +name = "sigstore-types" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80baa401f274093f7bb27d7a69d6139cbc11f1b97624e9a61a9b3ea32c776a35" +checksum = "eec330f1cdb4c7ec6628ef43165c42978480d4213af70567e67e2891ec9072d8" dependencies = [ - "quote", - "syn 2.0.119", + "base64 0.23.1", + "hex", + "jiff", + "pem", + "serde", + "serde_json", + "thiserror 2.0.21", ] [[package]] -name = "sigstore_protobuf_specs" -version = "0.5.2" +name = "sigstore-verify" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "423fc484041de8195cf21dc8888463e74e8cbb3487e36c14373abeaef1f96125" +checksum = "465f37d73c80fc6a2fa519d1337befab2026fce47f490181f72842aa56d401e1" dependencies = [ - "anyhow", - "glob", - "prost", - "prost-build", - "prost-reflect", - "prost-reflect-build", - "prost-types", + "base64 0.23.1", + "const-oid 0.9.6", + "futures-io", + "hex", + "jiff", + "rustls-pki-types", + "rustls-webpki", "serde", "serde_json", - "sigstore-protobuf-specs-derive", - "which", + "serde_json_canonicalizer", + "sigstore-bundle", + "sigstore-crypto", + "sigstore-merkle", + "sigstore-rekor", + "sigstore-trust-root", + "sigstore-tsa", + "sigstore-types", + "thiserror 2.0.21", + "tls_codec", + "tracing", + "x509-cert", ] [[package]] @@ -4957,12 +3947,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" - [[package]] name = "spki" version = "0.7.3" @@ -5020,31 +4004,13 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" - [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "strum_macros 0.28.0", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", + "strum_macros", ] [[package]] @@ -5131,7 +4097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5384,9 +4350,9 @@ checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" -version = "0.14.7" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ea7e687c08008d22518056608ad0c1cc721204d30ed48c8439a88ae5b9fc73" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", @@ -5406,7 +4372,6 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-stream", - "tokio-util", "tower", "tower-layer", "tower-service", @@ -5415,9 +4380,9 @@ dependencies = [ [[package]] name = "tonic-health" -version = "0.14.7" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa3153e1b4e67e5e94cf2c76f29f38dc47bcdc1410909a970b0655b61695da4e" +checksum = "fcfab99db777fba2802f0dfa861d1628d1ae916fb199d29819941f139ae85082" dependencies = [ "prost", "tokio", @@ -5428,9 +4393,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.7" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe9efdab8bb668f5ef526e21c247c8cb658853a754983cb70f1dac3fc4f462a" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -5439,9 +4404,9 @@ dependencies = [ [[package]] name = "tonic-reflection" -version = "0.14.7" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa5136cc28a9c88e06476a2b3c7510473914082a48835a172c05ba8077d328b" +checksum = "acccd136a4bf19810a1fde9c74edc6129b42a66b44d0c1c8aaa67aeb49a146a7" dependencies = [ "prost", "prost-types", @@ -5511,7 +4476,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5598,12 +4562,6 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-general-category" version = "1.1.0" @@ -5659,16 +4617,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -5697,7 +4645,6 @@ dependencies = [ "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] @@ -5915,19 +4862,6 @@ dependencies = [ "wasmparser 0.261.0", ] -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasmparser" version = "0.258.3" @@ -6221,7 +5155,7 @@ dependencies = [ "bitflags 2.13.1", "bytes", "futures", - "rand 0.10.2", + "rand", "rustix", "rustix-linux-procfs", "thiserror 2.0.21", @@ -6311,22 +5245,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webbrowser" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" -dependencies = [ - "jni", - "log", - "ndk-context", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "url", - "web-sys", -] - [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -6336,15 +5254,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "8.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" -dependencies = [ - "libc", -] - [[package]] name = "winapi" version = "0.3.9" @@ -6611,8 +5520,8 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" dependencies = [ - "ff 0.14.0", - "group 0.14.0", + "ff", + "group", "hybrid-array", ] @@ -6630,8 +5539,6 @@ checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", "der 0.7.10", - "sha1", - "signature 2.2.0", "spki 0.7.3", "tls_codec", ] diff --git a/crates/function/Cargo.toml b/crates/function/Cargo.toml index 74fbdc4e..7b5bcfa4 100644 --- a/crates/function/Cargo.toml +++ b/crates/function/Cargo.toml @@ -26,14 +26,15 @@ jsonschema = "0.60" tempfile = "3" pbjson-types = "0.9" prost = "0.14" -reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls", "gzip"] } +# query and json: the registry client's token request and response; the +# sigstore 0.14 crate used to enable them transitively. +reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls", "gzip", "query", "json"] } serde.workspace = true serde_json.workspace = true pem = "4" # Signs Sigstore bundles in cosign::testutil (feature testutil) only. p256 = { version = "0.14", features = ["ecdsa", "pem"], optional = true } serde_yaml = "0.9" -sigstore = { version = "0.14", default-features = false, features = ["cosign", "rustls-tls"] } sha2 = "0.11" tar = "0.4" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } @@ -46,6 +47,9 @@ tracing.workspace = true # env-filter: the --debug filter in logging.rs is scoped by crate; json: the # pod's line format (the feature function-sdk-rust already enables). tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +sigstore-verify = { version = "0.15.1", default-features = false, features = ["aws-lc-rs"] } +sigstore-trust-root = { version = "0.15.1", default-features = false, features = ["aws-lc-rs"] } +sigstore-types = "0.15.1" [dev-dependencies] # The crate's own integration tests use the test registry. diff --git a/crates/function/src/authz.rs b/crates/function/src/authz.rs index ccd2495f..e2f115b4 100644 --- a/crates/function/src/authz.rs +++ b/crates/function/src/authz.rs @@ -33,6 +33,7 @@ pub const ACTION_GRANT_EGRESS: &str = "grantEgress"; pub const ACTION_USE_PRIVATE_TMP: &str = "usePrivateTmp"; pub const ACTION_SET_ENV: &str = "setEnv"; const ACTION_REQUIRE_SIGNATURE: &str = "requireSignature"; +const ACTION_TRUST_SIGNER: &str = "trustSigner"; const ACTION_DIAL_ADDRESS: &str = "dialAddress"; const COMPOSITION_ACTIONS: &[&str] = &[ @@ -213,6 +214,12 @@ pub struct OperatorPolicy { policy: PolicySet, } +impl crate::cosign::SignerPolicy for OperatorPolicy { + fn trusts_signer(&self, repository: &str, signer: &crate::cosign::Signer) -> bool { + self.permits_signer(repository, &signer.issuer, &signer.identity) + } +} + impl OperatorPolicy { /// Reads and compiles a Cedar grant policy from a file. pub fn load(path: &std::path::Path) -> Result { @@ -305,14 +312,40 @@ impl OperatorPolicy { /// Whether the operator policy contains any requireSignature rule, so /// --cosign-key without one warns loudly rather than lapse silently. - /// (Used once --cosign-key is ported.) - #[allow(dead_code)] pub fn has_signature_rules(&self) -> bool { self.policy .policies() .any(|p| policy_scopes_action(p, ACTION_REQUIRE_SIGNATURE)) } + /// Whether the operator policy trusts a keyless cosign signature by + /// identity, authenticated by the OIDC issuer, for a module at location: + /// a trustSigner permit over the boundary-correct Repository hierarchy + /// whose condition reads context.issuer and context.identity (Cedar's + /// `like` matches a workflow's tag pattern). Default-deny: no rule, no + /// trusted signer. Caller-independent, so the principal is a placeholder. + pub fn permits_signer(&self, location: &str, issuer: &str, identity: &str) -> bool { + let (resource, entities) = repository_entities(location); + decide_raw( + &self.policy, + uid("Module", "module"), + None, + ACTION_TRUST_SIGNER, + resource, + entities, + json!({ "issuer": issuer, "identity": identity }), + ) + } + + /// Whether the operator policy contains any trustSigner rule, so + /// --cosign-trusted-root without one warns loudly rather than lapse + /// silently. + pub fn has_signer_rules(&self) -> bool { + self.policy + .policies() + .any(|p| policy_scopes_action(p, ACTION_TRUST_SIGNER)) + } + /// Compiles the policy's Action::"dialAddress" rules into the SSRF /// decision table egress evaluates: forbids to blocked, permits to /// allowed, so no Cedar runs on the dial hot path. The dial path is @@ -859,6 +892,41 @@ mod tests { assert!(p.requires_signature("ghcr.io/secure/greeter")); assert!(!p.requires_signature("ghcr.io/open/greeter")); assert!(p.has_signature_rules()); + assert!(!p.has_signer_rules()); + } + + #[test] + fn operator_policy_trusts_signers_per_repository() { + const ISSUER: &str = "https://token.actions.githubusercontent.com"; + const RELEASE: &str = "https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/tags/v0.6.0"; + let p = OperatorPolicy::new( + "test", + r#"permit (principal, action == Action::"trustSigner", resource in Repository::"ghcr.io/jonasz-lasut/wasmfn/examples") + when { context.issuer == "https://token.actions.githubusercontent.com" + && context.identity like "https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/tags/v*" };"#, + ) + .expect("compile"); + let module = "ghcr.io/jonasz-lasut/wasmfn/examples/pdb-addon"; + assert!(p.permits_signer(module, ISSUER, RELEASE)); + // Another branch, another issuer, another repository: refused. + assert!(!p.permits_signer( + module, + ISSUER, + "https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/heads/main" + )); + assert!(!p.permits_signer(module, "https://accounts.google.com", RELEASE)); + assert!(!p.permits_signer("ghcr.io/jonasz-lasut/other/pdb-addon", ISSUER, RELEASE)); + assert!(p.has_signer_rules()); + assert!(!p.has_signature_rules()); + // The trait the verifier consumes reads the same decision. + use crate::cosign::{Signer, SignerPolicy}; + assert!(p.trusts_signer( + module, + &Signer { + issuer: ISSUER.to_string(), + identity: RELEASE.to_string() + } + )); } #[test] diff --git a/crates/function/src/cosign.rs b/crates/function/src/cosign.rs index 6e79eb0e..29621332 100644 --- a/crates/function/src/cosign.rs +++ b/crates/function/src/cosign.rs @@ -1,25 +1,37 @@ -//! Cosign signature verification over cosign 3's Sigstore bundles, with the -//! crypto from sigstore-rs - key-based only: the "cosign sign --key" -//! workflow. Keyless verification is not implemented yet (#116); a keyless -//! bundle simply matches no configured key. +//! Cosign signature verification over cosign 3's Sigstore bundles: key-based +//! (the "cosign sign --key" workflow) with sigstore-crypto's keys, and +//! keyless (a Fulcio certificate, a Rekor entry, an RFC 3161 timestamp) +//! through sigstore-verify against a Sigstore trusted root (#116). //! //! cosign 3 stores a signature as an OCI 1.1 referrer of the signed //! manifest: a manifest whose subject is that manifest and whose layer is a //! Sigstore bundle (v0.3) holding a DSSE envelope around an in-toto //! statement that names the manifest digest. The referrers are listed and //! fetched through the runtime's own registry client, so they travel the -//! same authenticated path as the module pull; sigstore-rs contributes the -//! verification keys (algorithm auto-detected from the PEM's SPKI) and the -//! signature check. The configured keys are the trust root: the bundle's -//! verification material (a key hint, a certificate, transparency-log -//! entries) is never consulted, as the Rekor entry of a legacy signature -//! never was. cosign 2's legacy `sha256-.sig` signatures are not read. +//! same authenticated path as the module pull. +//! +//! A bundle carrying a key hint is checked against the configured keys +//! (--cosign-key): the keys are the trust root, and the bundle's +//! transparency-log entries are not consulted, as the Rekor entry of a +//! legacy signature never was. A bundle carrying a certificate is keyless: +//! sigstore-verify establishes the signing time from the bundle's RFC 3161 +//! timestamps and its Rekor entry's signed entry timestamp, verifies the +//! inclusion proof and checkpoint, chains the certificate to the trusted +//! root's Fulcio authorities at that time and checks the signature; the +//! signer the certificate names (its OIDC issuer and SAN identity) is then +//! held against the operator policy's trustSigner rules for the module's +//! repository, default-deny. cosign 2's legacy `sha256-.sig` +//! signatures are not read. use std::collections::{BTreeMap, BTreeSet, HashSet}; -use std::sync::Mutex; +use std::path::Path; +use std::sync::{Arc, Mutex}; use serde::Deserialize; -use sigstore::crypto::{CosignVerificationKey, Signature}; +use sigstore_trust_root::TrustedRoot; +use sigstore_types::{DerPublicKey, Sha256Hash, SignatureBytes}; +use sigstore_verify::crypto::VerificationKey; +use sigstore_verify::{VerificationPolicy, Verifier as BundleVerifier}; use crate::location::OciReference; use crate::oci::{Descriptor, OCI_MANIFEST_TYPE, RegistryClient}; @@ -46,17 +58,82 @@ const MAX_MANIFEST_SIZE: usize = 4 << 20; /// costs bounded requests. const MAX_REFERRERS: usize = 32; -/// Checks cosign signatures made with a fixed set of public keys. Results -/// are remembered per manifest digest for the life of the process; +/// A keyless signature's signer, as its Fulcio certificate names it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signer { + /// The OIDC issuer that authenticated the signer (Fulcio's issuer + /// extension), such as https://token.actions.githubusercontent.com. + pub issuer: String, + /// The certificate's subject alternative name: a workflow URI or an + /// email address. + pub identity: String, +} + +/// Decides whether a keyless signature's signer may vouch for a module in +/// repository - the operator policy's trustSigner rules. No policy, no +/// trusted signer. +pub trait SignerPolicy: Send + Sync { + fn trusts_signer(&self, repository: &str, signer: &Signer) -> bool; +} + +impl SignerPolicy for F +where + F: Fn(&str, &Signer) -> bool + Send + Sync, +{ + fn trusts_signer(&self, repository: &str, signer: &Signer) -> bool { + self(repository, signer) + } +} + +/// Checks cosign signatures: key-based ones against a fixed set of public +/// keys, keyless ones against a Sigstore trusted root and a signer policy. +/// Results are remembered per manifest digest for the life of the process; /// manifests are immutable by digest. pub struct Verifier { - keys: Vec, + keys: Vec, + keyless: Option, verified: Mutex>, } +/// The keyless half: sigstore-verify over the trusted root, and the policy +/// that decides which signers a repository may trust. +struct Keyless { + verifier: BundleVerifier, + policy: Option>, +} + impl Verifier { + /// Builds the verifier the flags describe: the PEM keys of --cosign-key, + /// the trusted root of --cosign-trusted-root (its signers decided by + /// policy), or both. None when neither flag is set. + pub fn configure( + keys: Option<&Path>, + trusted_root: Option<&Path>, + policy: Option>, + ) -> Result, String> { + let mut verifier = match keys { + Some(path) => Self::load(path)?, + None => Self::empty(), + }; + if let Some(path) = trusted_root { + verifier = verifier.with_trusted_root(load_trusted_root(path)?, policy)?; + } + if verifier.keys.is_empty() && verifier.keyless.is_none() { + return Ok(None); + } + Ok(Some(verifier)) + } + + fn empty() -> Verifier { + Verifier { + keys: Vec::new(), + keyless: None, + verified: Mutex::new(HashSet::new()), + } + } + /// Reads one or more PEM public keys (as cosign.pub is written). - pub fn load(path: &std::path::Path) -> Result { + pub fn load(path: &Path) -> Result { let raw = std::fs::read(path).map_err(|e| { format!( "cannot read cosign public key: {}", @@ -73,7 +150,7 @@ impl Verifier { .map_err(|e| format!("cannot parse cosign public key: {e}"))?; let mut keys = Vec::new(); for block in blocks { - let key = CosignVerificationKey::try_from_der(block.contents()) + let key = VerificationKey::from_spki(&DerPublicKey::new(block.contents().to_vec())) .map_err(|e| format!("cannot parse cosign public key: {e}"))?; keys.push(key); } @@ -82,14 +159,30 @@ impl Verifier { } Ok(Verifier { keys, - verified: Mutex::new(HashSet::new()), + ..Self::empty() }) } - /// Checks that the pinned manifest carries a cosign signature by one of - /// the keys: some Sigstore bundle among its referrers verifies. The - /// referrers are read through client - the same authenticated path the - /// module pull uses. Blocking: run it off the async executor. + /// Adds keyless verification against root: Fulcio authorities, Rekor + /// keys, certificate-transparency keys and timestamp authorities. A + /// signer the root vouches for is admitted only where policy trusts it. + pub fn with_trusted_root( + mut self, + root: TrustedRoot, + policy: Option>, + ) -> Result { + let verifier = BundleVerifier::new(&root) + .map_err(|e| format!("cannot use the Sigstore trusted root: {e}"))?; + self.keyless = Some(Keyless { verifier, policy }); + Ok(self) + } + + /// Checks that the pinned manifest carries a cosign signature this + /// verifier accepts: some Sigstore bundle among its referrers verifies + /// with one of the keys, or keyless against the trusted root by a signer + /// the policy trusts for the repository. The referrers are read through + /// client - the same authenticated path the module pull uses. Blocking: + /// run it off the async executor. pub fn verify(&self, client: &RegistryClient, reference: &OciReference) -> Result<(), String> { if self .verified @@ -99,6 +192,7 @@ impl Verifier { { return Ok(()); } + let repository = format!("{}/{}", reference.registry, reference.repository); let referrers = client.referrers(&reference.digest, MAX_MANIFEST_SIZE)?; // Only an image manifest can carry a bundle layer. The listing's // artifactType does not say which do: cosign 3.0 sets none, so its @@ -136,7 +230,7 @@ impl Verifier { .filter(|l| l.media_type == BUNDLE_MEDIA_TYPE) { bundles += 1; - match self.check_bundle(client, layer, &reference.digest) { + match self.check_bundle(client, layer, &reference.digest, &repository) { Ok(()) => { self.verified .lock() @@ -156,10 +250,7 @@ impl Verifier { manifests.len() )); } - let name = format!( - "{}/{}@{}", - reference.registry, reference.repository, reference.digest - ); + let name = format!("{repository}@{}", reference.digest); if bundles == 0 && reasons.is_empty() { return Err(format!( "{name} carries no cosign signature (no Sigstore bundle among its referrers)" @@ -178,6 +269,7 @@ impl Verifier { client: &RegistryClient, layer: &Descriptor, digest: &str, + repository: &str, ) -> Result<(), String> { if !(0..=MAX_BUNDLE_SIZE as i64).contains(&layer.size) { return Err(format!( @@ -186,13 +278,18 @@ impl Verifier { )); } let raw = client.verified_blob(&layer.digest, MAX_BUNDLE_SIZE, "Sigstore bundle")?; - self.check_bundle_content(&raw, digest) + self.check_bundle_content(&raw, digest, repository) } - /// Checks a bundle: a DSSE envelope whose signature verifies with one - /// of the keys, around an in-toto statement that is a cosign signature - /// of digest. - fn check_bundle_content(&self, raw: &[u8], digest: &str) -> Result<(), String> { + /// Checks a bundle: a DSSE envelope around an in-toto statement that is + /// a cosign signature of digest, signed by one of the keys or, when the + /// bundle carries a certificate, keyless by a trusted signer. + fn check_bundle_content( + &self, + raw: &[u8], + digest: &str, + repository: &str, + ) -> Result<(), String> { let bundle: Bundle = serde_json::from_slice(raw) .map_err(|e| format!("Sigstore bundle is not valid JSON: {e}"))?; if bundle.media_type != BUNDLE_MEDIA_TYPE { @@ -216,15 +313,31 @@ impl Verifier { )); } let payload = decode_base64(&envelope.payload).ok_or("DSSE payload is not base64")?; + if bundle.verification_material.certificate.is_some() { + let Some(keyless) = &self.keyless else { + return Err( + "keyless signature cannot be verified: the runtime has no --cosign-trusted-root" + .to_string(), + ); + }; + return self.check_keyless(keyless, raw, &payload, digest, repository); + } + if self.keys.is_empty() { + return Err( + "key-based signature cannot be verified: the runtime has no --cosign-key" + .to_string(), + ); + } let signed = pae(&envelope.payload_type, &payload); let verifies = envelope .signatures .iter() .filter_map(|s| decode_base64(&s.sig)) .any(|sig| { + let sig = SignatureBytes::from_bytes(&sig); self.keys .iter() - .any(|key| key.verify_signature(Signature::Raw(&sig), &signed).is_ok()) + .any(|key| key.verify(&signed, &sig).is_ok()) }); if !verifies { return Err("signature does not verify with the configured keys".to_string()); @@ -232,18 +345,102 @@ impl Verifier { // Only a payload a key vouched for is read any further. check_statement(&payload, digest) } + + /// Verifies a keyless bundle: sigstore-verify binds the statement's + /// subject to digest, establishes the signing time, checks the Rekor + /// entry and chains the certificate; this runtime then requires the + /// statement to be a cosign signature and the signer to be trusted for + /// repository. + fn check_keyless( + &self, + keyless: &Keyless, + raw: &[u8], + payload: &[u8], + digest: &str, + repository: &str, + ) -> Result<(), String> { + let text = + std::str::from_utf8(raw).map_err(|_| "Sigstore bundle is not UTF-8".to_string())?; + let bundle = sigstore_types::Bundle::from_json(text) + .map_err(|e| format!("Sigstore bundle does not parse as a Sigstore bundle: {e}"))?; + let hex = digest.strip_prefix("sha256:").unwrap_or(digest); + let subject = Sha256Hash::from_hex(hex) + .map_err(|e| format!("module digest {digest:?} is not a sha256 digest: {e}"))?; + let result = keyless + .verifier + .verify(subject, &bundle, &VerificationPolicy::any_identity()) + .map_err(|e| { + format!("keyless signature does not verify against the trusted root: {e}") + })?; + // The verifier bound the subject to digest; that the statement is a + // signature and not an attestation is this runtime's rule. + check_statement(payload, digest)?; + let (Some(identity), Some(issuer)) = (result.identity(), result.issuer()) else { + return Err( + "keyless signature's certificate names no signer identity or OIDC issuer" + .to_string(), + ); + }; + let signer = Signer { + issuer: issuer.to_string(), + identity: identity.as_str().to_string(), + }; + let trusted = keyless + .policy + .as_ref() + .is_some_and(|p| p.trusts_signer(repository, &signer)); + if !trusted { + return Err(format!( + "keyless signature by {} (issuer {}) is not trusted for {repository}: no trustSigner rule in the operator policy permits it", + signer.identity, signer.issuer + )); + } + tracing::debug!( + digest, + repository, + identity = signer.identity, + issuer = signer.issuer, + "keyless cosign signature verified" + ); + Ok(()) + } } -/// A Sigstore bundle, as far as a key-based check reads it: the -/// verification material is deliberately not modelled. +/// Reads a Sigstore trusted root (trusted_root.json, as `cosign +/// trusted-root` writes it): Fulcio authorities, Rekor and +/// certificate-transparency keys and timestamp authorities with their +/// validity windows. +pub fn load_trusted_root(path: &Path) -> Result { + let raw = std::fs::read_to_string(path).map_err(|e| { + format!( + "cannot read Sigstore trusted root: {}", + crate::resolver::go_io_error("open", path, &e) + ) + })?; + TrustedRoot::from_json(&raw).map_err(|e| format!("cannot parse Sigstore trusted root: {e}")) +} + +/// A Sigstore bundle, as far as this module reads it before handing a +/// keyless one to sigstore-verify: enough to tell a key hint from a +/// certificate and to check a key-based signature. #[derive(Deserialize)] struct Bundle { #[serde(default, rename = "mediaType")] media_type: String, + #[serde(default, rename = "verificationMaterial")] + verification_material: Material, #[serde(default, rename = "dsseEnvelope")] dsse_envelope: Option, } +#[derive(Deserialize, Default)] +struct Material { + /// Present in a keyless bundle: the Fulcio certificate. A key-based + /// bundle carries a publicKey hint instead. + #[serde(default)] + certificate: Option, +} + #[derive(Deserialize)] struct Envelope { #[serde(default)] @@ -478,10 +675,25 @@ mod tests { use super::*; use crate::location::parse_oci_reference; use crate::oci::testregistry::{TestRegistry, digest_of, serve}; + use sigstore_trust_root::SigstoreInstance; use std::collections::HashMap; const SLSA_PROVENANCE: &str = "https://slsa.dev/provenance/v1"; + /// A real keyless signature: the cosign bundle this repository's release + /// workflow attached to ghcr.io/jonasz-lasut/wasmfn/examples/pdb-addon + /// v0.1.0 on 2026-10-07 (see testdata/cosign/README.md). + const KEYLESS_BUNDLE: &[u8] = + include_bytes!("../testdata/cosign/pdb-addon-v0.1.0-keyless.bundle.json"); + /// The manifest digest that bundle signs. + const KEYLESS_DIGEST: &str = include_str!("../testdata/cosign/pdb-addon-v0.1.0.digest"); + const GITHUB_ACTIONS_ISSUER: &str = "https://token.actions.githubusercontent.com"; + const RELEASE_WORKFLOW: &str = "https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/tags/v0.6.0"; + + fn keyless_digest() -> String { + KEYLESS_DIGEST.trim().to_string() + } + /// A registry holding one module artifact, and its manifest digest. fn module_registry(referrers_api: bool) -> (TestRegistry, String) { let wasm = b"fake wasm"; @@ -507,19 +719,55 @@ mod tests { (registry, digest) } - /// Serves registry and verifies the module digest with keys; the + /// A registry with no module manifest: the verifier reads referrers + /// only, so a bundle attached to any subject digest can be checked. + fn bare_registry() -> TestRegistry { + TestRegistry { + manifests: HashMap::new(), + blobs: HashMap::new(), + bearer: false, + referrers_api: false, + basic: false, + } + } + + /// Serves registry and verifies the module digest with verifier; the /// registry's address reads in a refusal. - fn verify(registry: TestRegistry, digest: &str, keys: &[&TestKey]) -> Result<(), String> { + fn verify_with(registry: TestRegistry, digest: &str, verifier: Verifier) -> Result<(), String> { let addr = serve(registry); let reference = parse_oci_reference(&format!("{addr}/example/greeter@{digest}")).expect("reference"); let client = RegistryClient::new(&reference, None); - let pem: String = keys.iter().map(|k| k.public_pem.as_str()).collect(); - Verifier::new(pem.as_bytes())? + verifier .verify(&client, &reference) .map_err(|e| e.replace(&addr, "")) } + /// Serves registry and verifies the module digest with keys. + fn verify(registry: TestRegistry, digest: &str, keys: &[&TestKey]) -> Result<(), String> { + let pem: String = keys.iter().map(|k| k.public_pem.as_str()).collect(); + verify_with(registry, digest, Verifier::new(pem.as_bytes())?) + } + + /// A verifier with no keys: the embedded trusted root of instance and + /// a policy trusting this repository's release workflow for the test + /// repository, or no policy at all. + fn keyless_verifier(instance: SigstoreInstance, trusting: bool) -> Verifier { + let policy: Option> = trusting.then(|| { + Arc::new(|repository: &str, signer: &Signer| { + repository.ends_with("/example/greeter") + && signer.issuer == GITHUB_ACTIONS_ISSUER + && signer.identity == RELEASE_WORKFLOW + }) as Arc + }); + Verifier::empty() + .with_trusted_root( + TrustedRoot::from_embedded(instance).expect("embedded trusted root"), + policy, + ) + .expect("verifier") + } + fn unsigned(digest: &str) -> String { format!( "/example/greeter@{digest} carries no cosign signature (no Sigstore bundle among its referrers)" @@ -713,11 +961,168 @@ mod tests { verify(registry, &digest, &[&key]), Err(invalid( &digest, - "signature does not verify with the configured keys" + "keyless signature cannot be verified: the runtime has no --cosign-trusted-root" )) ); } + #[test] + fn a_key_bundle_is_refused_by_a_verifier_without_keys() { + let key = TestKey::from_seed(7); + let (mut registry, digest) = module_registry(false); + attach( + &mut registry, + &digest, + &key.bundle(&digest, COSIGN_SIGN_PREDICATE), + Shape::ArtifactType, + ); + assert_eq!( + verify_with( + registry, + &digest, + keyless_verifier(SigstoreInstance::PublicGood, true) + ), + Err(invalid( + &digest, + "key-based signature cannot be verified: the runtime has no --cosign-key" + )) + ); + } + + #[test] + fn a_keyless_signature_by_a_trusted_signer_verifies_against_the_trusted_root() { + // Offline: the embedded public-good trusted root carries the Fulcio + // authority, the Rekor key and the timestamp authority that signed + // the fixture's certificate, log entry and RFC 3161 timestamp; the + // certificate expired ten minutes after signing, so the verdict + // rests on the authenticated signing time, not the clock. + let digest = keyless_digest(); + let mut registry = bare_registry(); + attach( + &mut registry, + &digest, + KEYLESS_BUNDLE, + Shape::AnnotationsOnly, + ); + assert_eq!( + verify_with( + registry, + &digest, + keyless_verifier(SigstoreInstance::PublicGood, true) + ), + Ok(()) + ); + } + + #[test] + fn a_keyless_signature_nobody_trusts_is_refused() { + let digest = keyless_digest(); + let mut registry = bare_registry(); + attach( + &mut registry, + &digest, + KEYLESS_BUNDLE, + Shape::AnnotationsOnly, + ); + assert_eq!( + verify_with( + registry, + &digest, + keyless_verifier(SigstoreInstance::PublicGood, false) + ), + Err(invalid( + &digest, + &format!( + "keyless signature by {RELEASE_WORKFLOW} (issuer {GITHUB_ACTIONS_ISSUER}) is not trusted for /example/greeter: no trustSigner rule in the operator policy permits it" + ) + )) + ); + } + + /// Asserts a keyless refusal from sigstore-verify, whose detail is the + /// crate's wording: only the runtime's prefix and the category are + /// pinned. + fn assert_keyless_refusal(result: Result<(), String>, digest: &str, category: &str) { + let prefix = invalid( + digest, + "keyless signature does not verify against the trusted root: ", + ); + match result { + Err(e) if e.starts_with(&prefix) && e[prefix.len()..].starts_with(category) => {} + other => panic!("want a refusal starting {prefix:?}{category:?}, got {other:?}"), + } + } + + #[test] + fn a_keyless_signature_for_another_digest_is_refused() { + let other = format!("sha256:{}", "9".repeat(64)); + let mut registry = bare_registry(); + attach( + &mut registry, + &other, + KEYLESS_BUNDLE, + Shape::AnnotationsOnly, + ); + assert_keyless_refusal( + verify_with( + registry, + &other, + keyless_verifier(SigstoreInstance::PublicGood, true), + ), + &other, + "artifact mismatch", + ); + } + + #[test] + fn a_keyless_signature_against_another_trusted_root_is_refused() { + // Staging's authorities never issued the fixture's certificate. + let digest = keyless_digest(); + let mut registry = bare_registry(); + attach( + &mut registry, + &digest, + KEYLESS_BUNDLE, + Shape::AnnotationsOnly, + ); + assert_keyless_refusal( + verify_with( + registry, + &digest, + keyless_verifier(SigstoreInstance::Staging, true), + ), + &digest, + "", + ); + } + + #[test] + fn a_tampered_keyless_signature_is_refused() { + let digest = keyless_digest(); + let mut bundle: serde_json::Value = serde_json::from_slice(KEYLESS_BUNDLE).expect("bundle"); + let sig = bundle["dsseEnvelope"]["signatures"][0]["sig"] + .as_str() + .expect("signature") + .to_string(); + // Flip one base64 symbol well inside the signature. + let mut chars: Vec = sig.chars().collect(); + chars[10] = if chars[10] == 'A' { 'B' } else { 'A' }; + bundle["dsseEnvelope"]["signatures"][0]["sig"] = + serde_json::json!(chars.into_iter().collect::()); + let tampered = serde_json::to_vec(&bundle).expect("bundle"); + let mut registry = bare_registry(); + attach(&mut registry, &digest, &tampered, Shape::AnnotationsOnly); + assert_keyless_refusal( + verify_with( + registry, + &digest, + keyless_verifier(SigstoreInstance::PublicGood, true), + ), + &digest, + "", + ); + } + #[test] fn a_legacy_signature_alone_is_unsigned() { // cosign 2's default: a simple-signing payload under the diff --git a/crates/function/src/main.rs b/crates/function/src/main.rs index 09b8c352..0b084bf2 100644 --- a/crates/function/src/main.rs +++ b/crates/function/src/main.rs @@ -75,6 +75,14 @@ struct ServeArgs { #[arg(long, env = "COSIGN_KEY")] cosign_key: Option, + /// Sigstore trusted root (trusted_root.json: Fulcio authorities, Rekor + /// and certificate-transparency keys, timestamp authorities) that + /// verifies keyless cosign signatures. A keyless signature admits a + /// module only where the operator policy's trustSigner rules trust its + /// signer for the repository. + #[arg(long, env = "COSIGN_TRUSTED_ROOT")] + cosign_trusted_root: Option, + /// Keep compiled modules in memory between requests. Off, every request /// maps the module's compiled artifact from disk and releases it after. #[arg(long, default_value_t = true, env = "ENABLE_MEMORY_CACHE", action = clap::ArgAction::Set)] @@ -165,16 +173,6 @@ fn main() -> ExitCode { async fn serve_main(args: ServeArgs) -> Result<(), String> { logging::configure(args.sdk.debug); - let verifier = match &args.cosign_key { - None => None, - Some(path) => match cosign::Verifier::load(path) { - Ok(v) => Some(Arc::new(v)), - Err(e) => { - eprintln!("{e}"); - std::process::exit(1); - } - }, - }; if args.egress_rate_limit_per_minute < 0.0 || args.egress_rate_limit_burst < 0 { eprintln!( "--egress-rate-limit-per-minute and --egress-rate-limit-burst must not be negative" @@ -203,6 +201,20 @@ async fn serve_main(args: ServeArgs) -> Result<(), String> { args.egress_rate_limit_per_minute, args.egress_rate_limit_burst, )); + // The policy is shared with the verifier: its trustSigner rules decide + // which keyless signers a repository may trust. + let policy = policy.map(Arc::new); + let verifier = match cosign::Verifier::configure( + args.cosign_key.as_deref(), + args.cosign_trusted_root.as_deref(), + policy.clone().map(|p| p as Arc), + ) { + Ok(v) => v.map(Arc::new), + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + }; // A key without a policy is the all-or-nothing regime; a key beside a // policy with no requireSignature rule verifies nothing - warn loudly // rather than let --cosign-key lapse silently. @@ -214,11 +226,19 @@ async fn serve_main(args: ServeArgs) -> Result<(), String> { "--cosign-key is set but the operator policy has no requireSignature rule: no module will be signature-verified" ); } + // A trusted root admits a keyless signature only through a trustSigner + // rule: without one, every keyless signature is refused. + if args.cosign_trusted_root.is_some() && !policy.as_ref().is_some_and(|p| p.has_signer_rules()) + { + tracing::warn!( + "--cosign-trusted-root is set but the operator policy has no trustSigner rule: no keyless signature will be trusted" + ); + } // The $TMPDIR probe runs once before serving, only when the policy can // grant a private /tmp: a misconfigured $TMPDIR stops the runtime here, // not every request. if policy - .as_ref() + .as_deref() .is_some_and(authz::OperatorPolicy::has_private_tmp_rules) { match tempfile::Builder::new() diff --git a/crates/function/src/runner.rs b/crates/function/src/runner.rs index f33f4cfd..9db8bec4 100644 --- a/crates/function/src/runner.rs +++ b/crates/function/src/runner.rs @@ -39,7 +39,7 @@ pub struct WasmFunction { /// The operator's grant policy (--sandbox-policy-file), the sole /// authority that enables a sandbox capability; None refuses every /// sandbox grant, so the runtime offers only the default sandbox. - pub policy: Option, + pub policy: Option>, /// The egress mechanism: the SSRF block list, fixed budgets, the /// operator's Cedar CIDR rules and rate limit. Always built; whether a /// run may use it is the policy layers' grantEgress decision. @@ -175,15 +175,18 @@ impl WasmFunction { // before it is resolved: the legacy all-or-nothing --cosign-key // requires every module (and refuses non-OCI), while an operator // policy requires it per repository. - let signature_required = - crate::from::signature_required(self.policy.as_ref(), self.verifier.is_some(), &source); + let signature_required = crate::from::signature_required( + self.policy.as_deref(), + self.verifier.is_some(), + &source, + ); if signature_required && source.r#type != "OCI" { return Ok(raw_rsp(self.fatal( rsp, OUTCOME_REFUSED, format!( "cannot resolve module: {}", - crate::from::non_oci_signature_refusal(self.policy.as_ref(), &source) + crate::from::non_oci_signature_refusal(self.policy.as_deref(), &source) ), ))); } @@ -366,7 +369,7 @@ impl WasmFunction { let caps = match admission::admit_requires( manifest.as_ref().and_then(|m| m.requires.as_ref()), Some(&self.egress), - self.policy.as_ref(), + self.policy.as_deref(), admitted.composition.as_deref(), &principal, ) { @@ -723,13 +726,13 @@ mod tests { let (digest, addr) = crate::oci::testregistry::wasm_artifact(&wasm, Some(module_manifest), true); let mut f = function(None); - f.policy = Some( + f.policy = Some(Arc::new( crate::authz::OperatorPolicy::new( "test", r#"permit (principal, action == Action::"usePrivateTmp", resource);"#, ) .expect("policy"), - ); + )); let rsp = run( &f, input(serde_json::json!({ @@ -1033,14 +1036,14 @@ mod tests { fn granting_function(dir: Option<&std::path::Path>) -> WasmFunction { let mut f = function(dir.map(std::path::Path::to_owned)); - f.policy = Some( + f.policy = Some(Arc::new( crate::authz::OperatorPolicy::new( "test", r#"permit (principal, action == Action::"setEnv", resource); permit (principal, action == Action::"spendCredential", resource);"#, ) .expect("policy"), - ); + )); f } diff --git a/crates/function/src/validate.rs b/crates/function/src/validate.rs index 9045263a..c0264faf 100644 --- a/crates/function/src/validate.rs +++ b/crates/function/src/validate.rs @@ -79,6 +79,12 @@ pub struct ValidateArgs { #[arg(long, env = "COSIGN_KEY")] cosign_key: Option, + /// Sigstore trusted root (trusted_root.json) that verifies keyless + /// cosign signatures; the operator policy's trustSigner rules decide + /// which signers a repository trusts. + #[arg(long, env = "COSIGN_TRUSTED_ROOT")] + cosign_trusted_root: Option, + /// Cedar document with the operator's grant policy. #[arg(long, env = "SANDBOX_POLICY_FILE")] sandbox_policy_file: Option, @@ -190,10 +196,15 @@ fn run_inner(args: &ValidateArgs) -> Result { args.egress_rate_limit_per_minute, args.egress_rate_limit_burst, )); - let verifier = match &args.cosign_key { - None => None, - Some(path) => Some(std::sync::Arc::new(crate::cosign::Verifier::load(path)?)), - }; + let policy = policy.map(std::sync::Arc::new); + let verifier = crate::cosign::Verifier::configure( + args.cosign_key.as_deref(), + args.cosign_trusted_root.as_deref(), + policy + .clone() + .map(|p| p as std::sync::Arc), + )? + .map(std::sync::Arc::new); let ceilings = Config { timeout: args.module_timeout, memory_limit: args.module_memory_limit << 20, @@ -231,7 +242,7 @@ fn run_inner(args: &ValidateArgs) -> Result { xr, policy, egress, - cosign_key: args.cosign_key.is_some(), + verifier_configured: verifier.is_some(), verifier, }; let mut refused = false; @@ -437,9 +448,9 @@ struct Validator { engine: Option, resolver: Option, xr: Option, - policy: Option, + policy: Option>, egress: std::sync::Arc, - cosign_key: bool, + verifier_configured: bool, verifier: Option>, } @@ -537,11 +548,11 @@ impl Validator { // carry one, a required OCI module is verified (or refused when no // --cosign-key can check it). let signature_required = - crate::from::signature_required(self.policy.as_ref(), self.verifier.is_some(), &src); + crate::from::signature_required(self.policy.as_deref(), self.verifier.is_some(), &src); if signature_required && src.r#type != "OCI" { refuse!(format!( "cannot resolve module: {}", - crate::from::non_oci_signature_refusal(self.policy.as_ref(), &src) + crate::from::non_oci_signature_refusal(self.policy.as_deref(), &src) )); } let resolved = match resolver.resolve(&src, None) { @@ -628,7 +639,7 @@ impl Validator { .as_ref() .and_then(|q| q.egress.as_ref()) .is_some_and(|e| !e.http.is_empty()) - && !self.cosign_key + && !self.verifier_configured { r.warnings.push( "the module requires egress but is not signature-verified: no --cosign-key was given" @@ -643,7 +654,7 @@ impl Validator { let caps = match admission::admit_requires( m.requires.as_ref(), Some(&self.egress), - self.policy.as_ref(), + self.policy.as_deref(), comp, &principal, ) { diff --git a/crates/function/testdata/cosign/README.md b/crates/function/testdata/cosign/README.md new file mode 100644 index 00000000..e21461bf --- /dev/null +++ b/crates/function/testdata/cosign/README.md @@ -0,0 +1,16 @@ +# cosign fixtures + +`pdb-addon-v0.1.0-keyless.bundle.json` is a real keyless cosign signature: +the Sigstore bundle (v0.3) that this repository's release workflow, +`publish-pkg.yml` on tag `v0.6.0`, attached on 2026-10-07 to +`ghcr.io/jonasz-lasut/wasmfn/examples/pdb-addon:v0.1.0` (the manifest digest +in `pdb-addon-v0.1.0.digest`), downloaded from GHCR on 2026-10-26: the +referrer annotated with predicateType `https://sigstore.dev/cosign/sign/v1`, +its one layer. It carries a Fulcio certificate (SAN +`https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/tags/v0.6.0`, +issuer `https://token.actions.githubusercontent.com`, valid 11:15:35 to +11:25:35 UTC), a Rekor v1 `dsse/0.0.1` entry (log index 3129974832) with its +signed entry timestamp, inclusion proof and checkpoint, and one RFC 3161 +timestamp. The certificate has long expired: verifying it proves the +signing time is established from the bundle, not the clock. The tests verify +it offline against the trusted root embedded in sigstore-trust-root. diff --git a/crates/function/testdata/cosign/pdb-addon-v0.1.0-keyless.bundle.json b/crates/function/testdata/cosign/pdb-addon-v0.1.0-keyless.bundle.json new file mode 100644 index 00000000..1ff6aaa1 --- /dev/null +++ b/crates/function/testdata/cosign/pdb-addon-v0.1.0-keyless.bundle.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial":{"certificate":{"rawBytes":"MIIHVjCCBtugAwIBAgIUMX42PxPrF6V8/G1muMVCArWHT/4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMDA3MTExNTM1WhcNMjYxMDA3MTEyNTM1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalxQ/a9OPxkm9CIO+GcDIeUXLELX2G3G4bT8pe1LbO7aMm1eN4A+HRSxjL8fd1DbR/6Cr8WeVxzkuRDl1kvgcaOCBfowggX2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhkPzBuBgNVHREBAf8EZDBihmBodHRwczovL2dpdGh1Yi5jb20vam9uYXN6LWxhc3V0L2Z1bmN0aW9uLXdhc20vLmdpdGh1Yi93b3JrZmxvd3MvcHVibGlzaC1wa2cueW1sQHJlZnMvdGFncy92MC42LjAwOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBCgwNzVkYmE0ZDJhYzliOWZlZjhlODE1YjlhZmM3YjJkOTEyZWMxNjU3MCYGCisGAQQBg78wAQQEGFB1Ymxpc2ggRnVuY3Rpb24gUGFja2FnZTAoBgorBgEEAYO/MAEFBBpqb25hc3otbGFzdXQvZnVuY3Rpb24td2FzbTAeBgorBgEEAYO/MAEGBBByZWZzL3RhZ3MvdjAuNi4wMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTBwBgorBgEEAYO/MAEJBGIMYGh0dHBzOi8vZ2l0aHViLmNvbS9qb25hc3otbGFzdXQvZnVuY3Rpb24td2FzbS8uZ2l0aHViL3dvcmtmbG93cy9wdWJsaXNoLXBrZy55bWxAcmVmcy90YWdzL3YwLjYuMDA4BgorBgEEAYO/MAEKBCoMKDA3NWRiYTRkMmFjOWI5ZmVmOGU4MTViOWFmYzdiMmQ5MTJlYzE2NTcwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMD0GCisGAQQBg78wAQwELwwtaHR0cHM6Ly9naXRodWIuY29tL2pvbmFzei1sYXN1dC9mdW5jdGlvbi13YXNtMDgGCisGAQQBg78wAQ0EKgwoMDc1ZGJhNGQyYWM5YjlmZWY4ZTgxNWI5YWZjN2IyZDkxMmVjMTY1NzAgBgorBgEEAYO/MAEOBBIMEHJlZnMvdGFncy92MC42LjAwGgYKKwYBBAGDvzABDwQMDAoxMzM1ODUwMzYyMC8GCisGAQQBg78wARAEIQwfaHR0cHM6Ly9naXRodWIuY29tL2pvbmFzei1sYXN1dDAYBgorBgEEAYO/MAERBAoMCDkzMjgxOTMyMHAGCisGAQQBg78wARIEYgxgaHR0cHM6Ly9naXRodWIuY29tL2pvbmFzei1sYXN1dC9mdW5jdGlvbi13YXNtLy5naXRodWIvd29ya2Zsb3dzL3B1Ymxpc2gtcGtnLnltbEByZWZzL3RhZ3MvdjAuNi4wMDgGCisGAQQBg78wARMEKgwoMDc1ZGJhNGQyYWM5YjlmZWY4ZTgxNWI5YWZjN2IyZDkxMmVjMTY1NzAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMGEGCisGAQQBg78wARUEUwxRaHR0cHM6Ly9naXRodWIuY29tL2pvbmFzei1sYXN1dC9mdW5jdGlvbi13YXNtL2FjdGlvbnMvcnVucy8zNzYxMDU3ODAxNS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMFgGCisGAQQBg78wARgESgxIcmVwbzpqb25hc3otbGFzdXRAOTMyODE5MzIvZnVuY3Rpb24td2FzbUAxMzM1ODUwMzYyOnJlZjpyZWZzL3RhZ3MvdjAuNi4wMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGhFhNymQAABAMARjBEAiBKNgi/ZbL6fD/Nsklyi+QN8zM9m/htICgR7ezLsKoJewIgE1CyEmGhRDcYFPqJ2e/ihiOUHbvTDehvf4zpBHi1xr0wCgYIKoZIzj0EAwMDaQAwZgIxAMe42zGCI645n2lMwTuGqDqypNqjaY/TgfdytxZc2dI4Qf9Fy8F1JNrnLOjR1Dc4cgIxALh3rj9Z65XvPD8fPz5qB80D7aCAlYUCPswpIvg/wmRqDrn1Av1Nd72Dn0FgzOiImA=="}, "tlogEntries":[{"logIndex":"3129974832", "logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion":{"kind":"dsse", "version":"0.0.1"}, "integratedTime":"1791371736", "inclusionPromise":{"signedEntryTimestamp":"MEYCIQDLeRPpMnEpqdjk4mAUCX5l84oHYxjJEln2D/MMMhqDmAIhAORNIZEjqwwVV/3iuqj2WPKc2K4UvC/dpn4ODOrheNmL"}, "inclusionProof":{"logIndex":"3008070570", "rootHash":"lfSRWu6iWV6ccWYofmM/XHaXFb3DHxRyIbjbNi+nt94=", "treeSize":"3008070599", "hashes":["hFZxQ2fa6bDtvHUxQcEf4gnyXJXCd/6lDOl7xiUzqfc=", "3OMPoRbdpJc7cyfx7f9swXVP3NYrF4ctqXTFPqOxAZQ=", "yIyLArQN8SXsJIUyKTRwQedB+1MStcTBnTXGGpRnHPk=", "8oR+omV2Rvhuuf6nHsz0Pr4aXYXKk8/wyFFUDm3LlQk=", "CiY+wKAly3S028xXc8uJ4YgJGWO79CQuBw7R9DTOjXI=", "uzdvJ8bf4PQrrSSYqVx3XexRkFIdyyQsYDylRnLL+Tc=", "qz5vnc0OvHQJVmtugOrpLGpBzk4tP6sMPMESIeHjqZE=", "X4syTwRAq0sb+8pClvECH0MAX+DL+WLAP4vlxy1Q9jU=", "ZFEy/cckenMyS25K9SlfG1GQftpZ8w3wYBJjxH/YqMA=", "oD4DxluHi/YX2jymBB4FLP+lE+g8yz9LtKH733DL1KM=", "jR74jqLUnIA4MRWHlmYSYBWmPQZDpVF1rcjosiM2uqI=", "XkDDP9/tU4M06lI8NPKcAV9mJ+Ph0Ex34zoPykAGKZ8=", "fOSWJMwMV79xO90NPvr23tVW1i9UjlgU8iQm1+v1Bt8=", "JdTvqaY5OwzX26fcFI56qVBH+oddrfD+Wudbt3bXxX0=", "RV3sjcHrGKiF/VUe34vahONCRPotjitxq2XpsslF16g=", "3fRFZuDepJD9y0NjNA9nqInlZuCxNImKQJJfr//qImA=", "x9rXqkSu8+Vm4yqD2XeAwn305JZ4ZykdOwcD90FjP8o=", "NlOclldmKPA1tMKIskqHAwuQDYHp31YwpK+OOMrMccc=", "qxzHanAzz57SDdmJe0B7bJK72NTIBbwEMGBBKvDOROw=", "xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY="], "checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n3008070599\nlfSRWu6iWV6ccWYofmM/XHaXFb3DHxRyIbjbNi+nt94=\n\n— rekor.sigstore.dev wNI9ajBFAiEA4CQDGPbha66WNAMYqTuK520QC9GlrvPl0CthSvrSdJECICjubR3hh/RVqzp7CYvnFiBEauNitkXKOy17qMuk3no9\n"}}, "canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiMWI4YzcwMGM2OGFiMGIxNDE2Y2UzMzBjMDRiYjlhMWM1ZDdlMDg1MmY1OTgwYzQ1ZDg5M2U2ZTdmNDZlNDA4YyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImJmNmRiODQ5YzhiYjBmNTZkMDM4YjhjMWViYjEzOGZjNGU5MzdlYjdiZTk1OTBhZDAyYzdhYzdhZWIwMzMxMjMifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVZQ0lRQ2Q1NXh3cmZjcGtaeWIrcmNCVVloOUNLaWRmR01XcXNseldWSjJON2VoZmdJaEFLQmluYmdwRTh3S1ZHa0ZRN0N6bENJUE5HZUFqUHZHK3p3ZmJLeGFlZGErIiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VoV2FrTkRRblIxWjBGM1NVSkJaMGxWVFZnME1sQjRVSEpHTmxZNEwwY3hiWFZOVmtOQmNsZElWQzgwZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwWmVFMUVRVE5OVkVWNFRsUk5NVmRvWTA1TmFsbDRUVVJCTTAxVVJYbE9WRTB4VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVmhiSGhSTDJFNVQxQjRhMjA1UTBsUEswZGpSRWxsVlZoTVJVeFlNa2N6UnpSaVZEZ0tjR1V4VEdKUE4yRk5iVEZsVGpSQkswaFNVM2hxVERobVpERkVZbEl2TmtOeU9GZGxWbmg2YTNWU1JHd3hhM1puWTJGUFEwSm1iM2RuWjFneVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdaQ1owNVdTRk5OUlVkRVFWZG5RbFJtQ2pBcmJsQldhVkZTYkhadGJ6SlBhMjlXWVV4SFRHaG9hMUI2UW5WQ1owNVdTRkpGUWtGbU9FVmFSRUpwYUcxQ2IyUklVbmRqZW05MlRESmtjR1JIYURFS1dXazFhbUl5TUhaaGJUbDFXVmhPTmt4WGVHaGpNMVl3VERKYU1XSnRUakJoVnpsMVRGaGthR015TUhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRncGtNMDEyWTBoV2FXSkhiSHBoUXpGM1lUSmpkV1ZYTVhOUlNFcHNXbTVOZG1SSFJtNWplVGt5VFVNME1reHFRWGRQVVZsTFMzZFpRa0pCUjBSMmVrRkNDa0ZSVVhKaFNGSXdZMGhOTmt4NU9UQmlNblJzWW1rMWFGa3pVbkJpTWpWNlRHMWtjR1JIYURGWmJsWjZXbGhLYW1JeU5UQmFWelV3VEcxT2RtSlVRV1lLUW1kdmNrSm5SVVZCV1U4dlRVRkZRMEpDUmpOaU0wcHlXbTE0ZG1ReE9XdGhXRTUzV1ZoU2FtRkVRVEpDWjI5eVFtZEZSVUZaVHk5TlFVVkVRa05uZHdwT2VsWnJXVzFGTUZwRVNtaFplbXhwVDFkYWJGcHFhR3hQUkVVeFdXcHNhRnB0VFROWmFrcHJUMVJGZVZwWFRYaE9hbFV6VFVOWlIwTnBjMGRCVVZGQ0NtYzNPSGRCVVZGRlIwWkNNVmx0ZUhCak1tZG5VbTVXZFZrelVuQmlNalJuVlVkR2FtRXlSbTVhVkVGdlFtZHZja0puUlVWQldVOHZUVUZGUmtKQ2NIRUtZakkxYUdNemIzUmlSMFo2WkZoUmRscHVWblZaTTFKd1lqSTBkR1F5Um5waVZFRmxRbWR2Y2tKblJVVkJXVTh2VFVGRlIwSkNRbmxhVjFwNlRETlNhQXBhTTAxMlpHcEJkVTVwTkhkTlJITkhRMmx6UjBGUlVVSm5OemgzUVZGblJVeFJkM0poU0ZJd1kwaE5Oa3g1T1RCaU1uUnNZbWsxYUZrelVuQmlNalY2Q2t4dFpIQmtSMmd4V1c1V2VscFlTbXBpTWpVd1dsYzFNRXh0VG5aaVZFSjNRbWR2Y2tKblJVVkJXVTh2VFVGRlNrSkhTVTFaUjJnd1pFaENlazlwT0hZS1dqSnNNR0ZJVm1sTWJVNTJZbE01Y1dJeU5XaGpNMjkwWWtkR2VtUllVWFphYmxaMVdUTlNjR0l5TkhSa01rWjZZbE00ZFZveWJEQmhTRlpwVEROa2RncGpiWFJ0WWtjNU0yTjVPWGRrVjBwellWaE9iMHhZUW5KYWVUVTFZbGQ0UVdOdFZtMWplVGt3V1Zka2Vrd3pXWGRNYWxsMVRVUkJORUpuYjNKQ1owVkZDa0ZaVHk5TlFVVkxRa052VFV0RVFUTk9WMUpwV1ZSU2EwMXRSbXBQVjBrMVdtMVdiVTlIVlRSTlZGWnBUMWRHYlZsNlpHbE5iVkUxVFZSS2JGbDZSVElLVGxSamQwaFJXVXRMZDFsQ1FrRkhSSFo2UVVKRGQxRlFSRUV4Ym1GWVVtOWtWMGwwWVVjNWVtUkhWbXROUkRCSFEybHpSMEZSVVVKbk56aDNRVkYzUlFwTWQzZDBZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESndkbUp0Um5wbGFURnpXVmhPTVdSRE9XMWtWelZxWkVkc2RtSnBNVE5aV0U1MENrMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVUQkZTMmQzYjAxRVl6RmFSMHBvVGtkUmVWbFhUVFZaYW14dFdsZFpORnBVWjNoT1YwazFXVmRhYWs0eVNYa0tXa1JyZUUxdFZtcE5WRmt4VG5wQlowSm5iM0pDWjBWRlFWbFBMMDFCUlU5Q1FrbE5SVWhLYkZwdVRYWmtSMFp1WTNrNU1rMUROREpNYWtGM1IyZFpTd3BMZDFsQ1FrRkhSSFo2UVVKRWQxRk5SRUZ2ZUUxNlRURlBSRlYzVFhwWmVVMURPRWREYVhOSFFWRlJRbWMzT0hkQlVrRkZTVkYzWm1GSVVqQmpTRTAyQ2t4NU9XNWhXRkp2WkZkSmRWa3lPWFJNTW5CMlltMUdlbVZwTVhOWldFNHhaRVJCV1VKbmIzSkNaMFZGUVZsUEwwMUJSVkpDUVc5TlEwUnJlazFxWjNnS1QxUk5lVTFJUVVkRGFYTkhRVkZSUW1jM09IZEJVa2xGV1dkNFoyRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3eWNIWmliVVo2WldreGN3cFpXRTR4WkVNNWJXUlhOV3BrUjJ4Mllta3hNMWxZVG5STWVUVnVZVmhTYjJSWFNYWmtNamw1WVRKYWMySXpaSHBNTTBJeFdXMTRjR015WjNSalIzUnVDa3h1YkhSaVJVSjVXbGRhZWt3elVtaGFNMDEyWkdwQmRVNXBOSGROUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5SR014V2tkS2FFNUhVWGtLV1ZkTk5WbHFiRzFhVjFrMFdsUm5lRTVYU1RWWlYxcHFUakpKZVZwRWEzaE5iVlpxVFZSWk1VNTZRV2hDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0pOVFFwRldHUjJZMjEwYldKSE9UTllNbEp3WXpOQ2FHUkhUbTlOUjBWSFEybHpSMEZSVVVKbk56aDNRVkpWUlZWM2VGSmhTRkl3WTBoTk5reDVPVzVoV0ZKdkNtUlhTWFZaTWpsMFRESndkbUp0Um5wbGFURnpXVmhPTVdSRE9XMWtWelZxWkVkc2RtSnBNVE5aV0U1MFRESkdhbVJIYkhaaWJrMTJZMjVXZFdONU9Ib0tUbnBaZUUxRVZUTlBSRUY0VGxNNWFHUklVbXhpV0VJd1kzazRlRTFDV1VkRGFYTkhRVkZSUW1jM09IZEJVbGxGUTBGM1IyTklWbWxpUjJ4cVRVWm5Sd3BEYVhOSFFWRlJRbWMzT0hkQlVtZEZVMmQ0U1dOdFZuZGllbkJ4WWpJMWFHTXpiM1JpUjBaNlpGaFNRVTlVVFhsUFJFVTFUWHBKZGxwdVZuVlpNMUp3Q21JeU5IUmtNa1o2WWxWQmVFMTZUVEZQUkZWM1RYcFplVTl1U214YWFuQjVXbGRhZWt3elVtaGFNMDEyWkdwQmRVNXBOSGROU1VkS1FtZHZja0puUlVVS1FXUmFOVUZuVVVOQ1NITkZaVkZDTTBGSVZVRXpWREIzWVhOaVNFVlVTbXBIVWpSamJWZGpNMEZ4U2t0WWNtcGxVRXN6TDJnMGNIbG5Remh3TjI4MFFRcEJRVWRvUm1oT2VXMVJRVUZDUVUxQlVtcENSVUZwUWt0T1oya3ZXbUpNTm1aRUwwNXphMng1YVN0UlRqaDZUVGx0TDJoMFNVTm5VamRsZWt4elMyOUtDbVYzU1dkRk1VTjVSVzFIYUZKRVkxbEdVSEZLTW1VdmFXaHBUMVZJWW5aVVJHVm9kbVkwZW5CQ1NHa3hlSEl3ZDBObldVbExiMXBKZW1vd1JVRjNUVVFLWVZGQmQxcG5TWGhCVFdVME1ucEhRMGsyTkRWdU1teE5kMVIxUjNGRWNYbHdUbkZxWVZrdlZHZG1aSGwwZUZwak1tUkpORkZtT1VaNU9FWXhTazV5YmdwTVQycFNNVVJqTkdOblNYaEJUR2d6Y21vNVdqWTFXSFpRUkRobVVIbzFjVUk0TUVRM1lVTkJiRmxWUTFCemQzQkpkbWN2ZDIxU2NVUnliakZCZGpGT0NtUTNNa1J1TUVabmVrOXBTVzFCUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9XX19"}], "timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyzADAgEAMIICwgYJKoZIhvcNAQcCoIICszCCAq8CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg+l7euAKFq+tKqbCIx5dY9hzPg3Oba3qbket+O7clU6cCFQDR1P9JXf3dr6h6HJmogm08dbhYHhgPMjAyNjEwMDcxMTE1MzVaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHcMIIB2AIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTAwNzExMTUzNVowLwYJKoZIhvcNAQkEMSIEIFUPzIOi1CDf1J+Gn20fTPg5aL6YaPLn11kmV7SgzszgMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRoMGYCMQCW7cYB1Gr1tiBSyU/5KyRGaxSl4pBgKjGJ3N1Ix6T+RNNQq8foUMUPDakadV9rOd8CMQDeRDDjk7RCNW8lsSnz7wPCYSLZT9li3XsDoHOcqYRSD+2TXaBVk05hBe1lMfb3CrM="}]}}, "dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCAic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6ImY4MGE4MzY0YTYxNTU1ZjAyZDE0MjlhMTdhZjlmZWFmMGU5ZTg1MDVjOGZiYjBkNjQ1ODlmODRiMDViMTQzNDYifSwgImFubm90YXRpb25zIjp7fX1dLCAicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2lnc3RvcmUuZGV2L2Nvc2lnbi9zaWduL3YxIiwgInByZWRpY2F0ZSI6e319", "payloadType":"application/vnd.in-toto+json", "signatures":[{"sig":"MEYCIQCd55xwrfcpkZyb+rcBUYh9CKidfGMWqslzWVJ2N7ehfgIhAKBinbgpE8wKVGkFQ7CzlCIPNGeAjPvG+zwfbKxaeda+"}]}} \ No newline at end of file diff --git a/crates/function/testdata/cosign/pdb-addon-v0.1.0.digest b/crates/function/testdata/cosign/pdb-addon-v0.1.0.digest new file mode 100644 index 00000000..951c130d --- /dev/null +++ b/crates/function/testdata/cosign/pdb-addon-v0.1.0.digest @@ -0,0 +1 @@ +sha256:f80a8364a61555f02d1429a17af9feaf0e9e8505c8fbb0d64589f84b05b14346 diff --git a/crates/function/tests/guests.rs b/crates/function/tests/guests.rs index 1d3f4328..ceefe60e 100644 --- a/crates/function/tests/guests.rs +++ b/crates/function/tests/guests.rs @@ -482,7 +482,7 @@ fn run_guest(guest: &str) { engine, resolver: Arc::new(Resolver::new(Some(dir.path().to_owned()), 128 << 20, None)), ttl: std::time::Duration::from_secs(60), - policy: Some(permissive_policy()), + policy: Some(std::sync::Arc::new(permissive_policy())), egress: Arc::new(function_wasm::egress::Egress::new(rules, 0.0, 0)), step_slots: Arc::new(function_wasm_engine::concurrency::StepSlots::new()), verifier: None, From 2d78b5eff456e128aec9a4d1d638ae7706452187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Mon, 26 Oct 2026 17:10:20 +0100 Subject: [PATCH 2/5] docs: document keyless cosign verification The signatures page gains a keyless section (--cosign-trusted-root, how to obtain a trusted root, what is verified and the trustSigner rule), the flags table its row, the Cedar reference the eighth action, the troubleshooting page the four new refusals, and the trust-model and use-case one-pagers move their revisions. AGENTS.md follows the code: the cosign.rs entry, the Signatures section and the sigstore-rust crates in Key Dependencies in place of sigstore-rs. Part of #116. --- AGENTS.md | 16 ++++--- docs/one-pager-trust-model.md | 31 ++++++++----- docs/one-pager-use-case-examples.md | 14 +++--- docs/operators/cedar-policy.md | 63 +++++++++++++++++++------- docs/operators/runtime-flags.md | 3 +- docs/operators/signatures.md | 69 ++++++++++++++++++++++++----- docs/operators/troubleshooting.md | 7 ++- 7 files changed, 152 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 50353f97..f686173e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,10 +141,14 @@ crates/function/ the runtime crate: a library (everything below) + th referrers in tests src/location.rs go-containerregistry-compatible reference normalization: pinned refs (the runtime), any refs with tag/digest (guestfn), http locations - src/cosign.rs cosign 3 Sigstore bundle verification, key-based (bundles found among - the manifest's referrers over the runtime's own registry client, DSSE - + in-toto parsed here, sigstore-rs crypto); legacy .sig not read, - keyless not yet (#116); testutil signs cosign-3-shaped bundles + src/cosign.rs cosign 3 Sigstore bundle verification (bundles found among the + manifest's referrers over the runtime's own registry client): key-based + with sigstore-crypto keys (DSSE + in-toto parsed here, the keys the + trust root), keyless through sigstore-verify against + --cosign-trusted-root (signing time from RFC 3161 + Rekor SET, + inclusion proof, chain), the signer then held to the operator policy's + trustSigner rules via the SignerPolicy trait (#116); legacy .sig not + read; testutil signs cosign-3-shaped bundles src/cache.rs the module cache: memory (idle TTL, LRU bound) over the compiled- artifact store, single-flight loads with compile slots; cache events src/store.rs the content-addressed disk stores (modules, compiled/, @@ -484,7 +488,7 @@ Readiness is answered twice — gRPC health on the function port and plain-HTTP ### Signatures -`--cosign-key` loads PEM public keys into `cosign::Verifier` (sigstore-rs crypto, key-based only); verification runs **before** the caches, once per manifest digest per process, over the runtime's own registry client (same auth path as the pull); non-OCI sources are refused when a signature is required. The format is cosign 3's: a Sigstore bundle (v0.3) in an OCI 1.1 referrer of the module's manifest, discovered through the referrers API or, on a 404 (GHCR, `registry:3`), the `sha256-` referrers tag index; bundles are recognised by the layer media type, never by `artifactType` (cosign 3.0 sets none). A referrer counts only if its `subject` is the pinned digest; the bundle is fetched bounded and verified against its digest; a DSSE envelope signature over the PAE must verify with a configured key, and only then is the in-toto statement read: `Statement/v1`, `predicateType` exactly `https://sigstore.dev/cosign/sign/v1` (SLSA provenance and other attestations never count, whoever signed them) and a subject with the manifest digest. The keys are the trust root - the bundle's verification material (key hint, certificate, tlog entries) is ignored. Refusal reasons are sorted and deduplicated so the wording is deterministic whatever order a registry lists referrers in. cosign 2's legacy `.sig` signatures are not read (no lookup, no hint). Keyless (Fulcio/Rekor) is not implemented yet (#116, waiting on sigstore-rs): a keyless bundle matches no key and never fails the check for a key bundle beside it. `cosign::testutil` (feature `testutil`) signs cosign-3-shaped bundles and attaches them to `oci::testregistry`. +`--cosign-key` loads PEM public keys into `cosign::Verifier` (sigstore-crypto's `VerificationKey`) and `--cosign-trusted-root` a Sigstore trusted root for keyless bundles (sigstore-verify); verification runs **before** the caches, once per manifest digest per process, over the runtime's own registry client (same auth path as the pull); non-OCI sources are refused when a signature is required. The format is cosign 3's: a Sigstore bundle (v0.3) in an OCI 1.1 referrer of the module's manifest, discovered through the referrers API or, on a 404 (GHCR, `registry:3`), the `sha256-` referrers tag index; bundles are recognised by the layer media type, never by `artifactType` (cosign 3.0 sets none). A referrer counts only if its `subject` is the pinned digest; the bundle is fetched bounded and verified against its digest; a DSSE envelope signature over the PAE must verify with a configured key, and only then is the in-toto statement read: `Statement/v1`, `predicateType` exactly `https://sigstore.dev/cosign/sign/v1` (SLSA provenance and other attestations never count, whoever signed them) and a subject with the manifest digest. For a key-based bundle the keys are the trust root - its verification material (key hint, tlog entries) is ignored. Refusal reasons are sorted and deduplicated so the wording is deterministic whatever order a registry lists referrers in. cosign 2's legacy `.sig` signatures are not read (no lookup, no hint). A bundle carrying a certificate is keyless (#116): `sigstore_verify::Verifier::verify` (by the manifest digest, `VerificationPolicy::any_identity()`) establishes the signing time from its RFC 3161 timestamps and its Rekor entry's SET (a present-but-failing source is fatal, no fallback to the clock), verifies the inclusion proof and checkpoint, chains the certificate to the root's Fulcio authorities at that time and checks the signature; `cosign.rs` then requires the cosign predicate and asks the operator policy's `trustSigner` rules (`OperatorPolicy::permits_signer`: `context.issuer` and `context.identity` over the Repository hierarchy, default-deny, reached through the `cosign::SignerPolicy` trait) whether the signer is trusted for the module's repository. Without `--cosign-trusted-root` a keyless bundle is refused with a message naming the flag and never fails the check for a key bundle beside it. Key-based bundles stay on the key path on purpose: sigstore-verify's public-key policy refuses a bundle with neither a tlog entry nor a timestamp, which is what `cosign sign --key --tlog-upload=false` writes. The trusted root is a file read at startup (`cosign initialize` fetches it); a TUF refresh is not implemented. `cosign::testutil` (feature `testutil`) signs cosign-3-shaped bundles and attaches them to `oci::testregistry`. ### One-pagers @@ -602,7 +606,7 @@ By hand: `cargo run -p function-wasm -- --insecure --debug --module-dir=examples - `wit-component` / `wit-parser` / `wasi-preview1-component-adapter-provider`: `guestfn build`'s componentizing (`engine::componentize`): the wrap of a core module carrying wit-bindgen's component-type section, the embed of that section from a Go guest's `wit/` (`embed_world`), and the wasip1 reactor adapter it links in. Pinned in `crates/engine` beside wasmtime: the adapter is published at wasmtime's version and must be the one the engine's wasmtime-wasi was released with, and wit-component and wit-parser (with wasmparser) are the versions wasmtime's own bindgen already pulls, so no second copy is compiled; Renovate moves the adapter with the wasmtime group and ignores wit-component, wasmparser and wit-parser, which are bumped by hand in that PR to what `cargo tree -i wit-component` shows wasmtime pulling. - `function-sdk-rust` — the gRPC/protobuf types (prost), `request`/`response`/`resource` helpers and the CLI `Args`; the generated FunctionRunnerService *client* types serve tests, while serving goes through the raw codec. - `cedar-policy` — both policy layers. -- `sigstore` (features `cosign`, `rustls-tls`, no default features): cosign key verification crypto only; fetching stays on the runtime's own registry client and the bundle, DSSE and in-toto shapes are parsed by `cosign.rs`'s own serde structs (no `bundle`/`verify` features). +- `sigstore-verify`, `sigstore-trust-root`, `sigstore-types` (sigstore/sigstore-rust 0.15, `default-features = false` plus `aws-lc-rs`, which rustls already brings; no `tuf` feature, so no network): keyless bundle verification and `TrustedRoot::from_json`, with `sigstore_verify::crypto::VerificationKey` for the key path. Fetching stays on the runtime's own registry client, and `cosign.rs` keeps its own serde structs for the key path and the key-or-certificate check. Pre-1.0: minor bumps are breaking, so a Renovate PR for them is a code change. The `sigstore` crate (sigstore-rs) is no longer used. - `reqwest` (blocking, rustls) — the egress client and the registry client. - `prometheus-client` — the metrics registry and OpenMetrics encoder (the official OpenMetrics-native client; the classic text format is derived from its output in `metrics.rs`). - `clap` — both CLIs. diff --git a/docs/one-pager-trust-model.md b/docs/one-pager-trust-model.md index 3516dee3..91f02be6 100644 --- a/docs/one-pager-trust-model.md +++ b/docs/one-pager-trust-model.md @@ -2,7 +2,7 @@ * Owner: Jonasz Małecki (@jonasz-lasut) * Reviewers: Function WASM Maintainers -* Status: Implemented, revision 1.6 +* Status: Implemented, revision 1.7 Who decides what code runs, what that code can see and reach, and what the runtime guarantees to each of them. Written after the 2026-08-16 review, @@ -85,18 +85,29 @@ verifies with a configured key and the statement it signs is a cosign signature (`predicateType` `https://sigstore.dev/cosign/sign/v1`) whose subject is the manifest digest; the statement is read only after the signature verifies. Other attestations, such as SLSA provenance, never -count, whichever key signed them. The configured keys are the trust root: -the bundle's verification material (key hint, certificate, Rekor entries) -is not consulted, as the legacy path never consulted Rekor. +count, whichever key signed them. For a key-based bundle the configured +keys are the trust root: its verification material (key hint, Rekor +entries) is not consulted, as the legacy path never consulted Rekor. cosign 2's legacy signature, a simple-signing payload under the `sha256-.sig` tag, is not read: a module signed only that way is -unsigned to this runtime and must be re-signed with cosign 3. Keyless -(Fulcio/Rekor) verification is not implemented yet (#116, waiting on -sigstore-rs): a keyless bundle matches no configured key, so it never -admits a module and never fails the check for a key-based bundle beside it. -Until then an operator admits a keyless-signed module by verifying it with -cosign and countersigning it with their own key. +unsigned to this runtime and must be re-signed with cosign 3. + +Keyless (Fulcio/Rekor) signatures (#116) are verified through +sigstore-verify, the sigstore org's second Rust implementation, against the +Sigstore trusted root `--cosign-trusted-root` names: the signing time comes +from the bundle's RFC 3161 timestamps and its Rekor entry's signed entry +timestamp (a source that is present but fails is fatal, and there is no +fallback to the clock), the inclusion proof and checkpoint are verified, +the certificate chains to a Fulcio authority at that time, and only then is +the statement read as above. The signer the certificate names is a further +party in this model: the operator decides per repository, through +`trustSigner` rules in the same policy file that requires the signature, +which OIDC issuer and identity may vouch for a module - default-deny, so a +trusted root alone admits nothing. Key-based bundles keep the key path: a +bundle with a key hint never reaches the trusted root, and a keyless bundle +met without one is refused by name, never mistaken for a key failure. The +root is a file read at startup; a TUF refresh is the open half of #116. An operator `--sandbox-policy-file` makes the requirement per-repository: a `requireSignature` rule demands a signature for the repositories it names, diff --git a/docs/one-pager-use-case-examples.md b/docs/one-pager-use-case-examples.md index 4ed9dc4a..f6d7c043 100644 --- a/docs/one-pager-use-case-examples.md +++ b/docs/one-pager-use-case-examples.md @@ -2,7 +2,7 @@ * Owner: Jonasz Małecki (@jonasz-lasut) * Reviewers: Function WASM Maintainers -* Status: Implemented, revision 1.4 +* Status: Implemented, revision 1.5 * Tracking: https://github.com/jonasz-lasut/function-wasm/issues/112 ## Context @@ -160,11 +160,13 @@ with their module manifests, by `publish-pkg.yml`'s `publish-examples` job: - **When:** at a release, so a published example only ever targets a released runtime. - **Signing:** keyless cosign plus SLSA provenance from the release workflow, - with no key in the repository. The runtime verifies key-based signatures - only (keyless waits on sigstore-rs, - [#116](https://github.com/jonasz-lasut/function-wasm/issues/116)); until then - an operator who requires signatures verifies the keyless one, copies the - module and countersigns it with its own key. + with no key in the repository. The runtime verifies the keyless signature + against a Sigstore trusted root (`--cosign-trusted-root`) and admits it + where the operator's `trustSigner` rule names this repository's release + workflow on a version tag + ([#116](https://github.com/jonasz-lasut/function-wasm/issues/116); the + signatures page has the rule); an operator may still countersign with its + own key instead. - **References:** a release asset, `function-wasm-examples-.yaml`, lists every published example's reference pinned by digest (`repo:v@sha256:…`: the tag for people, the digest for the runtime). diff --git a/docs/operators/cedar-policy.md b/docs/operators/cedar-policy.md index 844dcfcd..eaaec9cf 100644 --- a/docs/operators/cedar-policy.md +++ b/docs/operators/cedar-policy.md @@ -13,11 +13,11 @@ drawn there; this page is the reference for writing the two documents. | layer | where it lives | written by | default | actions it may use | |---|---|---|---|---| -| operator policy | the file `--sandbox-policy-file` names, compiled once at startup and immutable for the process | the operator | deny: a capability no `permit` matches is refused, and no file means no capability at all | `usePrivateTmp`, `setEnv`, `spendCredential`, `grantEgress`, `requireSignature`, `dialAddress` | +| operator policy | the file `--sandbox-policy-file` names, compiled once at startup and immutable for the process | the operator | deny: a capability no `permit` matches is refused, and no file means no capability at all | `usePrivateTmp`, `setEnv`, `spendCredential`, `grantEgress`, `requireSignature`, `trustSigner`, `dialAddress` | | composition policy | the Input's `compositionPolicy` string, compiled per Composition (cached by content hash) | the composition author | deny for the `module.from` fences (`pullModule`, and `spendCredential` for an XR-chosen pull credential); scoped default-permit for the sandbox actions (below) | `pullModule`, `spendCredential`, `usePrivateTmp`, `setEnv`, `grantEgress` | -`requireSignature` and `dialAddress` are the operator's alone: a -`compositionPolicy` rule for either is never evaluated. `pullModule` is the +`requireSignature`, `trustSigner` and `dialAddress` are the operator's +alone: a `compositionPolicy` rule for any of them is never evaluated. `pullModule` is the composition author's alone: the operator policy has no say in where an XR-chosen module comes from, only in what it is granted once chosen. Where `compositionPolicy` sits in the Input, and that it is read from the Input @@ -41,22 +41,22 @@ safe because both layers only narrow: a rule that cannot match grants nothing. `function validate` builds the principal from `--xr`, and uses the zero principal without it. -`requireSignature` is the one action evaluated with a placeholder principal -(`Module::"module"`) that has no attributes: the requirement is -caller-independent, and a `requireSignature` rule that conditions on -`principal` never matches. +`requireSignature` and `trustSigner` are evaluated with a placeholder +principal (`Module::"module"`) that has no attributes: a signature +requirement and a signer's trust are caller-independent, and a rule for +either that conditions on `principal` never matches. ## Entity types | entity type | id | attributes | hierarchy (`in`) | used by | |---|---|---|---|---| -| `Request` | `self` | `namespace`, `xrKind`, `composition` | none | the principal of every action but `requireSignature` | -| `Repository` | a module's normalized location: `registry/repository` for an OCI reference, without its tag or digest and normalized as go-containerregistry does (`docker.io/x` is `index.docker.io/x`); `scheme://host/path` for an HTTP URL, host lowercased, no query | none | every path-boundary prefix, with and without its trailing slash: `ghcr.io/team` and `ghcr.io/team/` are ancestors of `ghcr.io/team/mod`, `ghcr.io/team-other` and `ghcr.io/teamsuffix` are not. `in` is reflexive, so an entity is `in` itself | `pullModule` (resource), `requireSignature` (resource), `spendCredential` (`context.repository`) | +| `Request` | `self` | `namespace`, `xrKind`, `composition` | none | the principal of every action but `requireSignature` and `trustSigner` | +| `Repository` | a module's normalized location: `registry/repository` for an OCI reference, without its tag or digest and normalized as go-containerregistry does (`docker.io/x` is `index.docker.io/x`); `scheme://host/path` for an HTTP URL, host lowercased, no query | none | every path-boundary prefix, with and without its trailing slash: `ghcr.io/team` and `ghcr.io/team/` are ancestors of `ghcr.io/team/mod`, `ghcr.io/team-other` and `ghcr.io/teamsuffix` are not. `in` is reflexive, so an entity is `in` itself | `pullModule` (resource), `requireSignature` and `trustSigner` (resource), `spendCredential` (`context.repository`) | | `HostPattern` | an egress rule's `host`, lowercased with any trailing dot dropped, or its `hostPattern` as written (`*.example.com`) | `host`: the same string, for `like` | the DNS label suffixes: `api.example.com` is `in HostPattern::"example.com"` and `in HostPattern::"com"`; the pattern `*.example.com` is `in HostPattern::"example.com"` too. `example.com.attacker.net` and `badexample.com` are not | `grantEgress` (resource) | | `Capability` | `privateTmp` or `env` | none | none | `usePrivateTmp` and `setEnv` (resource) | | `Credential` | the name of a pipeline-step credential | none | none | `spendCredential` (resource) | -| `Module` | `module` | none | none | the placeholder principal of `requireSignature` | -| `Action` | one of the seven below | | | the action of every rule | +| `Module` | `module` | none | none | the placeholder principal of `requireSignature` and `trustSigner` | +| `Action` | one of the eight below | | | the action of every rule | ## Actions @@ -67,7 +67,8 @@ caller-independent, and a `requireSignature` rule that conditions on | `usePrivateTmp` | both | `Capability::"privateTmp"` | none | the private `/tmp` the manifest requires | | `setEnv` | both | `Capability::"env"` | `keys`: the set of every environment variable name the manifest binds, decided once for all of them | the env bindings as a whole; each binding's credential is then decided by `spendCredential` | | `grantEgress` | both | `HostPattern::""` | `method` (upper case) and `path` (the rule's `pathPrefix`, empty when it has none); one decision per rule and per method it lists | that rule's (host, method) | -| `requireSignature` | operator | `Repository::""` | none (placeholder principal) | nothing: it demands that a module at that location carry a cosign signature, verified with `--cosign-key` before any cache is read. The [signatures](signatures.md) page has the precedence with `--cosign-key` alone | +| `requireSignature` | operator | `Repository::""` | none (placeholder principal) | nothing: it demands that a module at that location carry a cosign signature, verified with `--cosign-key` or, keyless, with `--cosign-trusted-root` and a `trustSigner` rule, before any cache is read. The [signatures](signatures.md) page has the precedence with `--cosign-key` alone | +| `trustSigner` | operator | `Repository::""` | `issuer`, the OIDC issuer of a keyless signature's Fulcio certificate, and `identity`, its subject alternative name (a workflow URI or an email), both strings for `==` and `like` (placeholder principal) | nothing by itself: a keyless cosign signature by that signer, verified against `--cosign-trusted-root`, counts as the module's signature for `requireSignature`. See [signatures](signatures.md) | | `dialAddress` | operator | none: the rule must leave `principal` and `resource` unconstrained | `ip`, the resolved address, tested with `isInRange(ip("CIDR"))` or `isLoopback()` | nothing at request time: the rules compile at load into the egress block list, a `forbid` adding a blocked prefix and a `permit` a hole in the default list | ## How each layer decides @@ -204,7 +205,8 @@ when { resource in Repository::"ghcr.io/secure" }; ``` Every module pulled from under `ghcr.io/secure` must carry a cosign -signature that one of the `--cosign-key` keys verifies; a module from +signature that one of the `--cosign-key` keys verifies, or a keyless one +by a signer a `trustSigner` rule trusts; a module from `ghcr.io/open/greeter` runs unsigned. With the rule but no `--cosign-key`, a required module is refused: `cannot verify module oci ghcr.io/secure/greeter@sha256:…: the operator policy requires a cosign @@ -214,6 +216,35 @@ signature: `module.http "https://example.com/fn.wasm" requires a cosign signature (operator policy), but only OCI modules can be signature-verified`. A `Path` source is never required by a rule. +### `trustSigner` + +```cedar +permit (principal, action == Action::"requireSignature", resource) +when { resource in Repository::"ghcr.io/jonasz-lasut/wasmfn/examples" }; + +permit (principal, action == Action::"trustSigner", resource) +when { + resource in Repository::"ghcr.io/jonasz-lasut/wasmfn/examples" && + context.issuer == "https://token.actions.githubusercontent.com" && + context.identity like "https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/tags/v*" +}; +``` + +A keyless signature on a module under `ghcr.io/jonasz-lasut/wasmfn/examples` +counts only when its Fulcio certificate was issued to this repository's +release workflow running on a version tag, authenticated by GitHub Actions. +`context.identity` is the certificate's subject alternative name as Fulcio +wrote it (`https://github.com///.github/workflows/@` +for a GitHub Actions run, an email for a human signer) and `context.issuer` +its OIDC issuer; `like` takes `*` as a wildcard. The signature is verified +first, against `--cosign-trusted-root`; the rule only decides whether a +verified signer is trusted for the repository, so a rule with no `when` +trusts every identity the root's authorities ever certified, which is +rarely meant. A verified signature no rule trusts is refused: `cannot +verify module oci …: no valid cosign signature for : keyless signature +by (issuer ) is not trusted for : no +trustSigner rule in the operator policy permits it`. + ### `dialAddress` ```cedar @@ -301,14 +332,16 @@ prints the error and exits 1, `function validate` prints it prefixed takes context.ip and no argument`, `isInRange takes one ip("CIDR") literal`, or `"" is not an ip literal: ` -Two more checks run at the runtime's startup, not in `function validate`: +Three more checks run at the runtime's startup, not in `function validate`: when the policy carries any `usePrivateTmp` rule the runtime makes one temporary directory under `TMPDIR` and exits 1 if it cannot (`the operator policy grants a private /tmp (usePrivateTmp), but the runtime cannot create one under : `), and when `--cosign-key` is set but the policy has no `requireSignature` rule it logs a warning, since that combination verifies nothing: `--cosign-key is set but the operator policy has no -requireSignature rule: no module will be signature-verified`. +requireSignature rule: no module will be signature-verified`; likewise +`--cosign-trusted-root is set but the operator policy has no trustSigner +rule: no keyless signature will be trusted`. ## Testing a policy diff --git a/docs/operators/runtime-flags.md b/docs/operators/runtime-flags.md index 603d2341..940f50b5 100644 --- a/docs/operators/runtime-flags.md +++ b/docs/operators/runtime-flags.md @@ -18,7 +18,8 @@ flags would admit. | `--max-cached-modules` | `MAX_CACHED_MODULES` | `0` (unbounded) | most compiled modules resident at once; the least recently used is dropped beyond it (freed once its last run ends). Artifacts are mapped from disk, so a resident Go module costs ~90 MB of file-backed memory | | `--max-concurrent-compiles` | `MAX_CONCURRENT_COMPILES` | `1` | modules compiled at once. One compile already uses every core (~25 CPU-seconds and ~1 GB for a large Go module); further first requests wait their turn instead of multiplying that | | `--max-cache-size` | `MAX_CACHE_SIZE` | `0` (unbounded) | MB the two on-disk caches may hold together; past it the least recently used entries (fetched modules and artifacts alike, ~230 MB per Go module version) are removed, at startup and every ten minutes. Size the volume, or set this below its size | -| `--cosign-key` | `COSIGN_KEY` | unset | PEM file of cosign public key(s); on its own, all-or-nothing: only OCI modules carrying a matching key-based cosign 3 signature (`cosign sign --key`: a Sigstore bundle attached as an OCI 1.1 referrer, found through the referrers API or the `sha256-` tag fallback) run and `http`/`path` sources are refused. cosign 2's legacy `.sig` signatures are not read, and keyless signatures are not verified yet ([#116](https://github.com/jonasz-lasut/function-wasm/issues/116)) - see [signing](signatures.md). With a `--sandbox-policy-file`, it supplies the keys while the policy's `requireSignature` rules decide which repositories must be signed (a repository no rule names runs unsigned) | +| `--cosign-key` | `COSIGN_KEY` | unset | PEM file of cosign public key(s); on its own, all-or-nothing: only OCI modules carrying a matching key-based cosign 3 signature (`cosign sign --key`: a Sigstore bundle attached as an OCI 1.1 referrer, found through the referrers API or the `sha256-` tag fallback) run and `http`/`path` sources are refused. cosign 2's legacy `.sig` signatures are not read; keyless signatures are `--cosign-trusted-root`'s - see [signing](signatures.md). With a `--sandbox-policy-file`, it supplies the keys while the policy's `requireSignature` rules decide which repositories must be signed (a repository no rule names runs unsigned) | +| `--cosign-trusted-root` | `COSIGN_TRUSTED_ROOT` | unset | Sigstore `trusted_root.json` (Fulcio authorities, Rekor and certificate-transparency keys, timestamp authorities, each with its validity window; `cosign initialize` fetches the public-good instance's into `~/.sigstore/root/`) that verifies keyless cosign signatures offline: the signing time comes from the bundle's RFC 3161 timestamp and its Rekor entry's signed entry timestamp, never from the clock. A keyless signature admits a module only where the operator policy's `trustSigner` rules trust its signer for the repository, so the flag wants a `--sandbox-policy-file` with one (the runtime warns at startup otherwise). Combines with `--cosign-key`: a bundle with a key hint goes to the keys, one with a certificate to the root - see [signing](signatures.md) | | `--max-concurrent-runs` | `MAX_CONCURRENT_RUNS` | `0` (unbounded) | module runs executing at once; a further request waits for a slot under its own deadline and, if that passes first, is a fatal result (`waiting for a run slot: context deadline exceeded`) without having run. Unbounded, concurrency is the caller's — Crossplane's reconcile workers | | `--max-total-run-memory` | `MAX_TOTAL_RUN_MEMORY` | `0` (unbounded) | total linear-memory budget in MB across all running modules; a run reserves its module's initial linear memory from the pool before it starts (waiting under its deadline when the pool is full) and each growth beyond it as its guest actually grows - so the pool holds what runs use, not their worst-case ceilings. A growth the pool cannot serve before the run's deadline is denied: the guest sees `memory.grow` fail, counted in `function_wasm_module_memory_denials_total` | | `--warm-modules` | `WARM_MODULES` | unset | modules loaded before the health service reports Serving — resolved, verified (`--cosign-key` applies), then compiled or mapped through the same caches a request uses: OCI references pinned to their manifest digest (`repo[:tag]@sha256:…`, pulled with the runtime's Docker config) and, with `--module-dir`, `path:` entries. Repeatable or comma-separated. An entry that fails to load is logged with the reason and does not stop the pod from serving; that module is loaded on its first request as usual | diff --git a/docs/operators/signatures.md b/docs/operators/signatures.md index 1a98c3b7..38b9bb58 100644 --- a/docs/operators/signatures.md +++ b/docs/operators/signatures.md @@ -18,26 +18,73 @@ one bundle's signature verifies with a configured key and its statement is a cosign signature (predicate type `https://sigstore.dev/cosign/sign/v1`) of that digest. Other attestations among the referrers, such as SLSA provenance from `actions/attest`, are not signatures and never admit a -module, even when signed with a configured key. The keys are the trust -root: a bundle's key hint, certificate and transparency-log entries are +module, even when signed with a configured key. For a key-based bundle +the keys are the trust root: its key hint and transparency-log entries are not consulted. cosign 2's legacy signatures (the `sha256-.sig` tag, which cosign 3 still writes with `--new-bundle-format=false`) are not read: a module signed only that way is refused as unsigned, so re-sign it with cosign 3. -Keyless (Fulcio/Rekor) signatures are not verified yet -([#116](https://github.com/jonasz-lasut/function-wasm/issues/116)): a -keyless bundle matches no configured key, so it neither admits a module -nor gets in the way of a key-based bundle beside it. Until then, admit a -module signed keyless upstream by verifying it, copying it if you serve -from your own registry, and countersigning it with your key: + +## Keyless signatures + +A keyless signature (`cosign sign ` under an OIDC identity, which is +how this repository's release workflow signs the examples it publishes) +carries a Fulcio certificate valid for ten minutes, a Rekor +transparency-log entry and an RFC 3161 timestamp instead of a key hint. +The runtime verifies it with `--cosign-trusted-root trusted_root.json` +(`COSIGN_TRUSTED_ROOT`), a Sigstore trusted root: the Fulcio authorities, +the Rekor and certificate-transparency keys and the timestamp authorities, +each with its validity window. `cosign initialize` fetches the public-good +instance's over TUF: ```shell -cosign verify --certificate-identity --certificate-oidc-issuer -crane copy # optional; keeps the manifest digest -cosign sign --key cosign.key # pinned by digest, repo@sha256:… +cosign initialize +cp ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/targets/trusted_root.json . ``` +Verification is offline and rests on the signing time, not the clock: +every RFC 3161 timestamp in the bundle must verify with a timestamp +authority of the root and the Rekor entry's signed entry timestamp with a +Rekor key, the certificate must chain to a Fulcio authority and be valid +at each of those times, the entry's inclusion proof and signed checkpoint +are verified, and then the signature itself. A bundle with no verifiable +signing time is refused, so a signing key taken from a CI run after its +certificate's ten minutes signs nothing the runtime accepts. + +A trusted root alone admits nothing. The signer the certificate names (its +OIDC issuer and its subject alternative name: a workflow URI, or an email) +must be trusted for the module's repository by a `trustSigner` rule in the +operator policy, default-deny, and the repository still needs its +`requireSignature` rule: + +```cedar +permit (principal, action == Action::"requireSignature", resource) +when { resource in Repository::"ghcr.io/jonasz-lasut/wasmfn/examples" }; + +permit (principal, action == Action::"trustSigner", resource) +when { + resource in Repository::"ghcr.io/jonasz-lasut/wasmfn/examples" && + context.issuer == "https://token.actions.githubusercontent.com" && + context.identity like "https://github.com/jonasz-lasut/function-wasm/.github/workflows/publish-pkg.yml@refs/tags/v*" +}; +``` + +With that policy and the trusted root, the examples this repository +publishes run on the identity of its release workflow, with no +countersignature. A keyless signature by any other signer is refused with +`keyless signature by (issuer ) is not trusted for +: no trustSigner rule in the operator policy permits it`, and +a keyless bundle met by a runtime without the flag with `keyless signature +cannot be verified: the runtime has no --cosign-trusted-root`. +`--cosign-key` and `--cosign-trusted-root` combine: a bundle with a key +hint is checked against the keys, one with a certificate against the root, +and a module is signed when any one bundle verifies. The trusted root is +read once at startup; refreshing it over TUF while the runtime runs is not +implemented yet ([#116](https://github.com/jonasz-lasut/function-wasm/issues/116)). + +## Requiring signatures per repository + `--cosign-key` on its own is all-or-nothing: with it, every module must be signed. An operator grant policy (`--sandbox-policy-file`) can instead require a signature **per repository** through a `requireSignature` rule over the same diff --git a/docs/operators/troubleshooting.md b/docs/operators/troubleshooting.md index 9e7a0176..55a28814 100644 --- a/docs/operators/troubleshooting.md +++ b/docs/operators/troubleshooting.md @@ -24,9 +24,12 @@ Each entry is a message the runtime, `function validate` or `guestfn` prints, or - **`module … requires a private /tmp (requires.filesystem.privateTmp), but the runtime has no --sandbox-policy-file, which is required to grant sandbox capabilities`** (and the env/egress forms): mount a Cedar `--sandbox-policy-file` with a matching permit or use a module that requires nothing. - **`the operator policy grants a private /tmp (usePrivateTmp), but the runtime cannot create one under …`** at startup: point `TMPDIR` at a writable directory (an `emptyDir`; tmpfs with `sizeLimit` bounds what a module may write). - **Guest gets `EPERM` under `/tmp`**: its path left the private `/tmp`; there is no other directory to reach. -- **`cannot verify module oci …: the operator policy requires a cosign signature, but the runtime has no --cosign-key to verify it`**: add `--cosign-key`, or narrow the `requireSignature` rule. The runtime warns loudly at startup when `--cosign-key` is set but the policy requires nothing. +- **`cannot verify module oci …: the operator policy requires a cosign signature, but the runtime has no --cosign-key to verify it`**: add `--cosign-key` (or, for keyless signatures, `--cosign-trusted-root` and a `trustSigner` rule), or narrow the `requireSignature` rule. The runtime warns loudly at startup when `--cosign-key` is set but the policy requires nothing. - **`cannot verify module oci …: carries no cosign signature (no Sigstore bundle among its referrers)`**: nothing among the manifest's OCI 1.1 referrers (the referrers API, or the `sha256-` tag index where the registry has no API) carries a Sigstore bundle naming this digest. Sign it with cosign 3 (`cosign sign --key cosign.key @sha256:…`). A module signed only with cosign 2 or `--new-bundle-format=false` (a `sha256-.sig` tag) lands here too: the legacy format is not read - re-sign it. -- **`cannot verify module oci …: no valid cosign signature for : signature does not verify with the configured keys`**: a bundle is there but no `--cosign-key` key signed it - a different key, or a keyless signature (not verified yet, #116; countersign it with your key). Other reasons in the same list: `signed statement's predicate type is "https://slsa.dev/provenance/v1", not https://sigstore.dev/cosign/sign/v1` (an attestation, not a signature), `signed statement is for another digest` (a bundle copied from another manifest), and fetch, size or digest failures of a bundle. +- **`cannot verify module oci …: no valid cosign signature for : signature does not verify with the configured keys`**: a bundle is there but no `--cosign-key` key signed it. Other reasons in the same list: `signed statement's predicate type is "https://slsa.dev/provenance/v1", not https://sigstore.dev/cosign/sign/v1` (an attestation, not a signature), `signed statement is for another digest` (a bundle copied from another manifest), and fetch, size or digest failures of a bundle. +- **`cannot verify module oci …: no valid cosign signature for : keyless signature cannot be verified: the runtime has no --cosign-trusted-root`**: the module's signature is keyless (a Fulcio certificate instead of a key hint). Add `--cosign-trusted-root` and a `trustSigner` rule trusting its signer, or countersign the module with your key. The mirror image, `key-based signature cannot be verified: the runtime has no --cosign-key`, is a key-based bundle met by a runtime with only a trusted root. +- **`cannot verify module oci …: no valid cosign signature for : keyless signature does not verify against the trusted root: `**: sigstore-verify refused the bundle: `artifact mismatch` (a bundle copied from another manifest), a certificate that does not chain to the root's Fulcio authorities (another Sigstore instance; GitHub's own needs its trusted root), an invalid signature or signed entry timestamp, or no verifiable signing time (a bundle with neither a Rekor entry nor an RFC 3161 timestamp). `cosign verify --certificate-identity … --certificate-oidc-issuer … ` against the same root tells which. +- **`cannot verify module oci …: no valid cosign signature for : keyless signature by (issuer ) is not trusted for : no trustSigner rule in the operator policy permits it`**: the signature verifies, but the operator policy trusts no such signer for that repository. Add or widen a `trustSigner` rule naming that identity (Cedar's `like` for a tag pattern) and issuer, or leave the module refused. The runtime warns at startup when `--cosign-trusted-root` is set and the policy has no `trustSigner` rule at all. - **`operator policy …: dialAddress rule "…" must scope the action as == Action::"dialAddress"`** (or another Cedar/ip-rule load error): the `--sandbox-policy-file` is malformed and refused at load. A `dialAddress` condition accepts only `context.ip.isInRange(ip("CIDR"))`, `context.ip.isLoopback()`, or a `||` of them. - **A guest's request fails with `sandbox.egress: resolves to an address the egress policy blocks`**: the host refuses private, loopback, link-local and cluster ranges by default; the address and block-list entry stay operator-side in the audit line. Add the range to the policy's `allowedCIDRs` to permit an in-cluster service. - **A guest's request fails with `wasmfn: sandbox.egress: HTTP egress is not granted to this module`**: the module calls the HTTP helper but its manifest requires no egress; the import always exists, the grant decides. From b69c29a8a3bea1998c7dd54e4a55c0f397092b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Tue, 27 Oct 2026 08:22:17 +0100 Subject: [PATCH 3/5] feat(cosign): reword the no-verifier refusals A required module met by a runtime that can verify nothing is refused with "the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it", and function validate's egress warning reads "neither --cosign-key nor --cosign-trusted-root was given"; the all-or-nothing refusal of a non-OCI source names both flags too. Both sentences named --cosign-key alone, which since the keyless path landed understated the operator's options. Three conformance goldens are re-recorded deliberately for those two sentences and nothing else: SignatureRequired, ManifestNoPolicy-egress and ManifestPermissive-egress. The wording is pinned contract from 1.0.0 on; this lands before it. Part of #116. --- crates/function/src/from.rs | 2 +- crates/function/src/runner.rs | 2 +- crates/function/src/validate.rs | 4 ++-- .../testdata/conformance/ManifestNoPolicy-egress.golden | 2 +- .../testdata/conformance/ManifestPermissive-egress.golden | 2 +- .../function/testdata/conformance/SignatureRequired.golden | 2 +- docs/one-pager-admission-tooling.md | 2 +- docs/operators/cedar-policy.md | 7 ++++--- docs/operators/signatures.md | 4 ++-- docs/operators/troubleshooting.md | 2 +- 10 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/function/src/from.rs b/crates/function/src/from.rs index f4a8d0af..4bbc08d2 100644 --- a/crates/function/src/from.rs +++ b/crates/function/src/from.rs @@ -280,7 +280,7 @@ pub fn non_oci_signature_refusal( src: &ModuleSource, ) -> String { if policy.is_none() { - return "only cosign-signed oci modules are accepted (--cosign-key is set); http and path sources are refused".to_string(); + return "only cosign-signed oci modules are accepted (--cosign-key or --cosign-trusted-root is set); http and path sources are refused".to_string(); } let (field, location) = match src.r#type.as_str() { "HTTP" => ( diff --git a/crates/function/src/runner.rs b/crates/function/src/runner.rs index 9db8bec4..7948b03e 100644 --- a/crates/function/src/runner.rs +++ b/crates/function/src/runner.rs @@ -262,7 +262,7 @@ impl WasmFunction { rsp, OUTCOME_REFUSED, format!( - "cannot verify module {}: the operator policy requires a cosign signature, but the runtime has no --cosign-key to verify it", + "cannot verify module {}: the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it", resolved.description ), ))); diff --git a/crates/function/src/validate.rs b/crates/function/src/validate.rs index c0264faf..2c4f73e6 100644 --- a/crates/function/src/validate.rs +++ b/crates/function/src/validate.rs @@ -562,7 +562,7 @@ impl Validator { if signature_required { let Some(verifier) = &self.verifier else { refuse!(format!( - "cannot verify module {}: the operator policy requires a cosign signature, but the runtime has no --cosign-key to verify it", + "cannot verify module {}: the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it", resolved.description )); }; @@ -642,7 +642,7 @@ impl Validator { && !self.verifier_configured { r.warnings.push( - "the module requires egress but is not signature-verified: no --cosign-key was given" + "the module requires egress but is not signature-verified: neither --cosign-key nor --cosign-trusted-root was given" .to_string(), ); } diff --git a/crates/function/testdata/conformance/ManifestNoPolicy-egress.golden b/crates/function/testdata/conformance/ManifestNoPolicy-egress.golden index 95e91582..28483f2e 100644 --- a/crates/function/testdata/conformance/ManifestNoPolicy-egress.golden +++ b/crates/function/testdata/conformance/ManifestNoPolicy-egress.golden @@ -3,5 +3,5 @@ exit 1 /egress.yaml: Composition/egress pipeline[0] egress: refused: module module file fn.wasm requires egress (requires.egress.http), but the runtime has no --sandbox-policy-file, which is required to grant egress (grantEgress) module: sha256:09e6ac0cbcc0abcea78afa7df96114b5e4c3bf4020eb97a9aa188e17473d9714, 255 B, ABI v2; manifest: greeter 0.1.0, requires egress api.example.com warning: module.type Path names a file under the runtime's --module-dir and carries no digest; a cluster Composition should pin an OCI or HTTP source by digest - warning: the module requires egress but is not signature-verified: no --cosign-key was given + warning: the module requires egress but is not signature-verified: neither --cosign-key nor --cosign-trusted-root was given --- stderr diff --git a/crates/function/testdata/conformance/ManifestPermissive-egress.golden b/crates/function/testdata/conformance/ManifestPermissive-egress.golden index d3b3ddc0..9f0a574a 100644 --- a/crates/function/testdata/conformance/ManifestPermissive-egress.golden +++ b/crates/function/testdata/conformance/ManifestPermissive-egress.golden @@ -3,5 +3,5 @@ exit 0 /egress.yaml: Composition/egress pipeline[0] egress: OK (path fn.wasm) module: sha256:09e6ac0cbcc0abcea78afa7df96114b5e4c3bf4020eb97a9aa188e17473d9714, 255 B, ABI v2; manifest: greeter 0.1.0, requires egress api.example.com warning: module.type Path names a file under the runtime's --module-dir and carries no digest; a cluster Composition should pin an OCI or HTTP source by digest - warning: the module requires egress but is not signature-verified: no --cosign-key was given + warning: the module requires egress but is not signature-verified: neither --cosign-key nor --cosign-trusted-root was given --- stderr diff --git a/crates/function/testdata/conformance/SignatureRequired.golden b/crates/function/testdata/conformance/SignatureRequired.golden index 9e2b7c18..81089b4a 100644 --- a/crates/function/testdata/conformance/SignatureRequired.golden +++ b/crates/function/testdata/conformance/SignatureRequired.golden @@ -1,4 +1,4 @@ exit 1 --- stdout -testdata/validate/signature.yaml: Composition/signature pipeline[0] secure: refused: cannot verify module oci ghcr.io/secure/greeter@sha256:3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a: the operator policy requires a cosign signature, but the runtime has no --cosign-key to verify it +testdata/validate/signature.yaml: Composition/signature pipeline[0] secure: refused: cannot verify module oci ghcr.io/secure/greeter@sha256:3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a3f2a: the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it --- stderr diff --git a/docs/one-pager-admission-tooling.md b/docs/one-pager-admission-tooling.md index 41c80840..231b45ed 100644 --- a/docs/one-pager-admission-tooling.md +++ b/docs/one-pager-admission-tooling.md @@ -78,7 +78,7 @@ reported as chosen by the XR and checked for the rule the runtime enforces ``` composition.yaml: Composition/hello pipeline[0] greeter: OK (oci ghcr.io/example/greeter:v1@sha256:3f2a…, limits timeout 5s memory 128Mi, compositionPolicy) - warning: the module requires egress but is not signature-verified: no --cosign-key was given + warning: the module requires egress but is not signature-verified: neither --cosign-key nor --cosign-trusted-root was given composition.yaml: Composition/hello pipeline[1] labeler: refused: module oci ghcr.io/example/labeler@sha256:9d1c… requires egress GET to host "evil.example.com" (requires.egress.http[0]), which the operator policy (--sandbox-policy-file) does not permit ``` diff --git a/docs/operators/cedar-policy.md b/docs/operators/cedar-policy.md index eaaec9cf..527390af 100644 --- a/docs/operators/cedar-policy.md +++ b/docs/operators/cedar-policy.md @@ -210,7 +210,8 @@ by a signer a `trustSigner` rule trusts; a module from `ghcr.io/open/greeter` runs unsigned. With the rule but no `--cosign-key`, a required module is refused: `cannot verify module oci ghcr.io/secure/greeter@sha256:…: the operator policy requires a cosign -signature, but the runtime has no --cosign-key to verify it`. A required +signature, but the runtime has neither --cosign-key nor --cosign-trusted-root +to verify it`. A required HTTP source is refused before any fetch, since only OCI artifacts carry a signature: `module.http "https://example.com/fn.wasm" requires a cosign signature (operator policy), but only OCI modules can be @@ -365,8 +366,8 @@ is the zero principal, which matches no per-tenant condition, and a its fence checked only for the policy's presence. Exit codes: 0 when every step is admitted, 1 when at least one is refused, 2 when the tool itself failed. A module that requires egress on a run without `--cosign-key` gets -the warning `the module requires egress but is not signature-verified: no ---cosign-key was given`. +the warning `the module requires egress but is not signature-verified: +neither --cosign-key nor --cosign-trusted-root was given`. ## Further reading diff --git a/docs/operators/signatures.md b/docs/operators/signatures.md index 38b9bb58..5669cd96 100644 --- a/docs/operators/signatures.md +++ b/docs/operators/signatures.md @@ -98,8 +98,8 @@ when { resource in Repository::"ghcr.io/acme/prod" }; The crypto is unchanged: `--cosign-key` still provides the keys and performs the check; Cedar only decides *whether* a given repository must be signed, and -the refusal (a required module that is unsigned, or that the runtime has no -`--cosign-key` to verify) happens before any cache, exactly as the +the refusal (a required module that is unsigned, or that the runtime has +neither flag to verify it with) happens before any cache, exactly as the all-or-nothing check does. Precedence: **without** `--sandbox-policy-file`, `--cosign-key` keeps its all-or-nothing meaning unchanged; **with** a `--sandbox-policy-file`, the per-repository `requireSignature` decision governs which modules must be signed diff --git a/docs/operators/troubleshooting.md b/docs/operators/troubleshooting.md index 55a28814..2610e2ed 100644 --- a/docs/operators/troubleshooting.md +++ b/docs/operators/troubleshooting.md @@ -24,7 +24,7 @@ Each entry is a message the runtime, `function validate` or `guestfn` prints, or - **`module … requires a private /tmp (requires.filesystem.privateTmp), but the runtime has no --sandbox-policy-file, which is required to grant sandbox capabilities`** (and the env/egress forms): mount a Cedar `--sandbox-policy-file` with a matching permit or use a module that requires nothing. - **`the operator policy grants a private /tmp (usePrivateTmp), but the runtime cannot create one under …`** at startup: point `TMPDIR` at a writable directory (an `emptyDir`; tmpfs with `sizeLimit` bounds what a module may write). - **Guest gets `EPERM` under `/tmp`**: its path left the private `/tmp`; there is no other directory to reach. -- **`cannot verify module oci …: the operator policy requires a cosign signature, but the runtime has no --cosign-key to verify it`**: add `--cosign-key` (or, for keyless signatures, `--cosign-trusted-root` and a `trustSigner` rule), or narrow the `requireSignature` rule. The runtime warns loudly at startup when `--cosign-key` is set but the policy requires nothing. +- **`cannot verify module oci …: the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it`**: add `--cosign-key` (or, for keyless signatures, `--cosign-trusted-root` and a `trustSigner` rule), or narrow the `requireSignature` rule. The runtime warns loudly at startup when `--cosign-key` is set but the policy requires nothing. - **`cannot verify module oci …: carries no cosign signature (no Sigstore bundle among its referrers)`**: nothing among the manifest's OCI 1.1 referrers (the referrers API, or the `sha256-` tag index where the registry has no API) carries a Sigstore bundle naming this digest. Sign it with cosign 3 (`cosign sign --key cosign.key @sha256:…`). A module signed only with cosign 2 or `--new-bundle-format=false` (a `sha256-.sig` tag) lands here too: the legacy format is not read - re-sign it. - **`cannot verify module oci …: no valid cosign signature for : signature does not verify with the configured keys`**: a bundle is there but no `--cosign-key` key signed it. Other reasons in the same list: `signed statement's predicate type is "https://slsa.dev/provenance/v1", not https://sigstore.dev/cosign/sign/v1` (an attestation, not a signature), `signed statement is for another digest` (a bundle copied from another manifest), and fetch, size or digest failures of a bundle. - **`cannot verify module oci …: no valid cosign signature for : keyless signature cannot be verified: the runtime has no --cosign-trusted-root`**: the module's signature is keyless (a Fulcio certificate instead of a key hint). Add `--cosign-trusted-root` and a `trustSigner` rule trusting its signer, or countersign the module with your key. The mirror image, `key-based signature cannot be verified: the runtime has no --cosign-key`, is a key-based bundle met by a runtime with only a trusted root. From 63a1c45d674424de2ab2a328717ac4e94c3ff003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Tue, 27 Oct 2026 08:22:17 +0100 Subject: [PATCH 4/5] test(cosign): keyless cases over a test Sigstore testsigstore::Instance (feature testutil) is an in-memory Sigstore instance: a Fulcio certificate authority, a certificate-transparency log and a Rekor log, P-256 keys from fixed seeds, that issues keyless cosign bundles at a fixed past time and publishes the trusted root that verifies them. A bundle carries a ten-minute certificate with the signer's identity, Fulcio's issuer extension and an embedded SCT over the precertificate, and a Rekor dsse 0.0.1 entry with its signed entry timestamp, a one-leaf inclusion proof and a signed checkpoint; no RFC 3161 timestamp. Every certificate it issues has expired, so a signature verifies only because its signing time is authenticated. Unit tests verify and refuse against it beside the real GHCR fixture, and five conformance cases run function validate --resolve against a registry it signed into, under the new signer-policy fixtures: a trusted signer admitted, an untrusted one refused, another instance's root refused (that golden quotes sigstore-verify's wording), a keyless module met by --cosign-key alone, a key-signed module met by --cosign-trusted-root alone. rcgen, x509-cert and time join the testutil feature and the dev-dependencies. Part of #116. --- AGENTS.md | 4 +- Cargo.lock | 143 ++++++- crates/function/Cargo.toml | 10 +- crates/function/src/cosign.rs | 107 ++++- crates/function/src/lib.rs | 2 + crates/function/src/testsigstore.rs | 385 ++++++++++++++++++ .../conformance/Cosign-KeyedWithoutKey.golden | 4 + .../Cosign-KeylessAgainstAnotherRoot.golden | 4 + .../conformance/Cosign-KeylessTrusted.golden | 5 + .../Cosign-KeylessUntrusted.golden | 4 + .../Cosign-KeylessWithoutTrustedRoot.golden | 4 + .../validate/signer-policy-untrusting.cedar | 9 + .../testdata/validate/signer-policy.cedar | 12 + crates/function/tests/conformance.rs | 212 ++++++++-- 14 files changed, 869 insertions(+), 36 deletions(-) create mode 100644 crates/function/src/testsigstore.rs create mode 100644 crates/function/testdata/conformance/Cosign-KeyedWithoutKey.golden create mode 100644 crates/function/testdata/conformance/Cosign-KeylessAgainstAnotherRoot.golden create mode 100644 crates/function/testdata/conformance/Cosign-KeylessTrusted.golden create mode 100644 crates/function/testdata/conformance/Cosign-KeylessUntrusted.golden create mode 100644 crates/function/testdata/conformance/Cosign-KeylessWithoutTrustedRoot.golden create mode 100644 crates/function/testdata/validate/signer-policy-untrusting.cedar create mode 100644 crates/function/testdata/validate/signer-policy.cedar diff --git a/AGENTS.md b/AGENTS.md index f686173e..09ec35d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -488,7 +488,7 @@ Readiness is answered twice — gRPC health on the function port and plain-HTTP ### Signatures -`--cosign-key` loads PEM public keys into `cosign::Verifier` (sigstore-crypto's `VerificationKey`) and `--cosign-trusted-root` a Sigstore trusted root for keyless bundles (sigstore-verify); verification runs **before** the caches, once per manifest digest per process, over the runtime's own registry client (same auth path as the pull); non-OCI sources are refused when a signature is required. The format is cosign 3's: a Sigstore bundle (v0.3) in an OCI 1.1 referrer of the module's manifest, discovered through the referrers API or, on a 404 (GHCR, `registry:3`), the `sha256-` referrers tag index; bundles are recognised by the layer media type, never by `artifactType` (cosign 3.0 sets none). A referrer counts only if its `subject` is the pinned digest; the bundle is fetched bounded and verified against its digest; a DSSE envelope signature over the PAE must verify with a configured key, and only then is the in-toto statement read: `Statement/v1`, `predicateType` exactly `https://sigstore.dev/cosign/sign/v1` (SLSA provenance and other attestations never count, whoever signed them) and a subject with the manifest digest. For a key-based bundle the keys are the trust root - its verification material (key hint, tlog entries) is ignored. Refusal reasons are sorted and deduplicated so the wording is deterministic whatever order a registry lists referrers in. cosign 2's legacy `.sig` signatures are not read (no lookup, no hint). A bundle carrying a certificate is keyless (#116): `sigstore_verify::Verifier::verify` (by the manifest digest, `VerificationPolicy::any_identity()`) establishes the signing time from its RFC 3161 timestamps and its Rekor entry's SET (a present-but-failing source is fatal, no fallback to the clock), verifies the inclusion proof and checkpoint, chains the certificate to the root's Fulcio authorities at that time and checks the signature; `cosign.rs` then requires the cosign predicate and asks the operator policy's `trustSigner` rules (`OperatorPolicy::permits_signer`: `context.issuer` and `context.identity` over the Repository hierarchy, default-deny, reached through the `cosign::SignerPolicy` trait) whether the signer is trusted for the module's repository. Without `--cosign-trusted-root` a keyless bundle is refused with a message naming the flag and never fails the check for a key bundle beside it. Key-based bundles stay on the key path on purpose: sigstore-verify's public-key policy refuses a bundle with neither a tlog entry nor a timestamp, which is what `cosign sign --key --tlog-upload=false` writes. The trusted root is a file read at startup (`cosign initialize` fetches it); a TUF refresh is not implemented. `cosign::testutil` (feature `testutil`) signs cosign-3-shaped bundles and attaches them to `oci::testregistry`. +`--cosign-key` loads PEM public keys into `cosign::Verifier` (sigstore-crypto's `VerificationKey`) and `--cosign-trusted-root` a Sigstore trusted root for keyless bundles (sigstore-verify); verification runs **before** the caches, once per manifest digest per process, over the runtime's own registry client (same auth path as the pull); non-OCI sources are refused when a signature is required. The format is cosign 3's: a Sigstore bundle (v0.3) in an OCI 1.1 referrer of the module's manifest, discovered through the referrers API or, on a 404 (GHCR, `registry:3`), the `sha256-` referrers tag index; bundles are recognised by the layer media type, never by `artifactType` (cosign 3.0 sets none). A referrer counts only if its `subject` is the pinned digest; the bundle is fetched bounded and verified against its digest; a DSSE envelope signature over the PAE must verify with a configured key, and only then is the in-toto statement read: `Statement/v1`, `predicateType` exactly `https://sigstore.dev/cosign/sign/v1` (SLSA provenance and other attestations never count, whoever signed them) and a subject with the manifest digest. For a key-based bundle the keys are the trust root - its verification material (key hint, tlog entries) is ignored. Refusal reasons are sorted and deduplicated so the wording is deterministic whatever order a registry lists referrers in. cosign 2's legacy `.sig` signatures are not read (no lookup, no hint). A bundle carrying a certificate is keyless (#116): `sigstore_verify::Verifier::verify` (by the manifest digest, `VerificationPolicy::any_identity()`) establishes the signing time from its RFC 3161 timestamps and its Rekor entry's SET (a present-but-failing source is fatal, no fallback to the clock), verifies the inclusion proof and checkpoint, chains the certificate to the root's Fulcio authorities at that time and checks the signature; `cosign.rs` then requires the cosign predicate and asks the operator policy's `trustSigner` rules (`OperatorPolicy::permits_signer`: `context.issuer` and `context.identity` over the Repository hierarchy, default-deny, reached through the `cosign::SignerPolicy` trait) whether the signer is trusted for the module's repository. Without `--cosign-trusted-root` a keyless bundle is refused with a message naming the flag and never fails the check for a key bundle beside it. Key-based bundles stay on the key path on purpose: sigstore-verify's public-key policy refuses a bundle with neither a tlog entry nor a timestamp, which is what `cosign sign --key --tlog-upload=false` writes. The trusted root is a file read at startup (`cosign initialize` fetches it); a TUF refresh is not implemented. `cosign::testutil` (feature `testutil`) signs cosign-3-shaped key-based bundles and attaches them to `oci::testregistry`; `testsigstore::Instance` signs keyless ones against its own trusted root. A required module met by a runtime with neither flag is refused naming both: `the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it`. ### One-pagers @@ -536,7 +536,7 @@ CI runs lint and the toolchain-free workspace tests on every push and PR (`ci.ym ### Test Patterns -Unit tests live in `#[cfg(test)] mod tests` blocks beside the code; integration suites under `tests/`. Guest modules for tests are WAT fixtures assembled with the `wat` crate: components implementing the world with a sync-lifted `run` (the engine's tests carry fixtures that misbehave in one way each; the sandbox is tested through WASI 0.2 interfaces lowered by hand - `wasi:cli/environment`, `wasi:filesystem/preopens`, `wasi:cli/exit`, `wasi:clocks/monotonic-clock` - against a `$libc` core instance whose memory the main module imports; the one core-module fixture proves the ABI v1 refusal). Registry-backed tests use `oci::testregistry` (feature `testutil`): an in-memory distribution registry that serves and accepts artifacts, optionally behind the Bearer token flow, with OCI 1.1 referrers through the referrers API (`referrers_api: true`) or the tag schema (`push_referrer` maintains the `sha256-` index); `cosign::testutil` signs cosign-3-shaped Sigstore bundles into it. Expected `RunFunctionResponse`s are constructed whole and compared with `assert_eq!` on the prost types, including fatal cases, whose per-guest message wording is blanked before the comparison. +Unit tests live in `#[cfg(test)] mod tests` blocks beside the code; integration suites under `tests/`. Guest modules for tests are WAT fixtures assembled with the `wat` crate: components implementing the world with a sync-lifted `run` (the engine's tests carry fixtures that misbehave in one way each; the sandbox is tested through WASI 0.2 interfaces lowered by hand - `wasi:cli/environment`, `wasi:filesystem/preopens`, `wasi:cli/exit`, `wasi:clocks/monotonic-clock` - against a `$libc` core instance whose memory the main module imports; the one core-module fixture proves the ABI v1 refusal). Registry-backed tests use `oci::testregistry` (feature `testutil`): an in-memory distribution registry that serves and accepts artifacts, optionally behind the Bearer token flow, with OCI 1.1 referrers through the referrers API (`referrers_api: true`) or the tag schema (`push_referrer` maintains the `sha256-` index); `cosign::testutil` signs cosign-3-shaped key-based Sigstore bundles into it, and `testsigstore::Instance` (feature `testutil`; rcgen, x509-cert and time are its dependencies) is an in-memory Sigstore instance - a Fulcio CA, a CT log and a Rekor log from fixed seeds - that issues keyless bundles (ten-minute certificate with an embedded SCT, Rekor dsse 0.0.1 entry with SET, inclusion proof and checkpoint) at a fixed past time and publishes the trusted root that verifies them, so the keyless path is covered offline and deterministically (`validate_resolve_keyless_cosign_matches_the_goldens`). Expected `RunFunctionResponse`s are constructed whole and compared with `assert_eq!` on the prost types, including fatal cases, whose per-guest message wording is blanked before the comparison. ### Linting diff --git a/Cargo.lock b/Cargo.lock index 6d9f32a4..0960cc0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,45 @@ dependencies = [ "term", ] +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-compression" version = "0.4.43" @@ -273,7 +312,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] [[package]] @@ -282,6 +321,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -971,6 +1019,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der_derive" version = "0.7.3" @@ -1368,6 +1430,7 @@ dependencies = [ "pbjson-types", "pem", "prost", + "rcgen", "reqwest", "serde", "serde_json", @@ -1378,6 +1441,7 @@ dependencies = [ "sigstore-verify", "tar", "tempfile", + "time", "tokio", "tonic", "tonic-health", @@ -1387,6 +1451,7 @@ dependencies = [ "tracing", "tracing-subscriber", "wat", + "x509-cert", ] [[package]] @@ -2434,6 +2499,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.9.1" @@ -2467,6 +2538,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonempty" version = "0.12.0" @@ -2590,6 +2671,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3145,6 +3235,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3338,6 +3442,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -5543,6 +5656,24 @@ dependencies = [ "tls_codec", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -5553,6 +5684,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec 0.9.1", + "time", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/crates/function/Cargo.toml b/crates/function/Cargo.toml index 7b5bcfa4..b4008a11 100644 --- a/crates/function/Cargo.toml +++ b/crates/function/Cargo.toml @@ -50,13 +50,21 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } sigstore-verify = { version = "0.15.1", default-features = false, features = ["aws-lc-rs"] } sigstore-trust-root = { version = "0.15.1", default-features = false, features = ["aws-lc-rs"] } sigstore-types = "0.15.1" +rcgen = { version = "0.14.10", default-features = false, features = ["aws_lc_rs", "pem", "x509-parser"], optional = true } +x509-cert = { version = "0.2", optional = true } +time = { version = "0.3", features = ["formatting"], optional = true } [dev-dependencies] # The crate's own integration tests use the test registry. function-wasm = { path = ".", features = ["testutil"] } p256 = { version = "0.14", features = ["ecdsa", "pem"] } +rcgen = { version = "0.14.10", default-features = false, features = ["aws_lc_rs", "pem", "x509-parser"] } +x509-cert = "0.2" tempfile = "3" wat = "1" +time = { version = "0.3", features = ["formatting"] } [features] -testutil = ["dep:p256"] +# rcgen and x509-cert: cosign::testutil::sigstore, the in-memory Sigstore +# instance (Fulcio CA, CT log, Rekor) the keyless tests sign against. +testutil = ["dep:p256", "dep:rcgen", "dep:x509-cert", "dep:time"] diff --git a/crates/function/src/cosign.rs b/crates/function/src/cosign.rs index 29621332..acc24abb 100644 --- a/crates/function/src/cosign.rs +++ b/crates/function/src/cosign.rs @@ -460,7 +460,7 @@ struct EnvelopeSignature { /// DSSE's pre-authentication encoding - what the envelope's signatures /// sign: "DSSEv1 ", lengths in /// ASCII decimal bytes. -fn pae(payload_type: &str, payload: &[u8]) -> Vec { +pub(crate) fn pae(payload_type: &str, payload: &[u8]) -> Vec { let mut out = format!( "DSSEv1 {} {payload_type} {} ", payload_type.len(), @@ -753,6 +753,14 @@ mod tests { /// a policy trusting this repository's release workflow for the test /// repository, or no policy at all. fn keyless_verifier(instance: SigstoreInstance, trusting: bool) -> Verifier { + keyless_verifier_with( + TrustedRoot::from_embedded(instance).expect("embedded trusted root"), + trusting, + ) + } + + /// A verifier with no keys over root, and the same policy choice. + fn keyless_verifier_with(root: TrustedRoot, trusting: bool) -> Verifier { let policy: Option> = trusting.then(|| { Arc::new(|repository: &str, signer: &Signer| { repository.ends_with("/example/greeter") @@ -761,13 +769,17 @@ mod tests { }) as Arc }); Verifier::empty() - .with_trusted_root( - TrustedRoot::from_embedded(instance).expect("embedded trusted root"), - policy, - ) + .with_trusted_root(root, policy) .expect("verifier") } + fn release_signer() -> Signer { + Signer { + issuer: GITHUB_ACTIONS_ISSUER.to_string(), + identity: RELEASE_WORKFLOW.to_string(), + } + } + fn unsigned(digest: &str) -> String { format!( "/example/greeter@{digest} carries no cosign signature (no Sigstore bundle among its referrers)" @@ -1259,6 +1271,91 @@ mod tests { ); } + #[test] + fn an_in_memory_keyless_signature_verifies_against_its_own_trusted_root() { + // The stand-in signs at a fixed time in the past with a ten-minute + // certificate: only the signing time its Rekor entry authenticates + // admits the signature, as with the real fixture above. + let instance = crate::testsigstore::Instance::new(); + assert!( + crate::testsigstore::Instance::signing_time() + time::Duration::minutes(5) + < time::OffsetDateTime::now_utc(), + "the stand-in's certificates must have expired" + ); + let (mut registry, digest) = module_registry(false); + attach( + &mut registry, + &digest, + &instance.sign(&digest, COSIGN_SIGN_PREDICATE, &release_signer()), + Shape::AnnotationsOnly, + ); + assert_eq!( + verify_with( + registry, + &digest, + keyless_verifier_with(instance.trusted_root(), true) + ), + Ok(()) + ); + } + + #[test] + fn an_in_memory_keyless_signature_is_refused_where_it_should_be() { + let instance = crate::testsigstore::Instance::new(); + let bundle = instance.sign( + &module_registry(false).1, + COSIGN_SIGN_PREDICATE, + &release_signer(), + ); + // Nobody trusts the signer. + let (mut registry, digest) = module_registry(false); + attach(&mut registry, &digest, &bundle, Shape::AnnotationsOnly); + assert_eq!( + verify_with( + registry, + &digest, + keyless_verifier_with(instance.trusted_root(), false) + ), + Err(invalid( + &digest, + &format!( + "keyless signature by {RELEASE_WORKFLOW} (issuer {GITHUB_ACTIONS_ISSUER}) is not trusted for /example/greeter: no trustSigner rule in the operator policy permits it" + ) + )) + ); + // Another instance's trusted root vouches for none of it. + let (mut registry, digest) = module_registry(false); + attach(&mut registry, &digest, &bundle, Shape::AnnotationsOnly); + assert_keyless_refusal( + verify_with( + registry, + &digest, + keyless_verifier_with(crate::testsigstore::Instance::other().trusted_root(), true), + ), + &digest, + "", + ); + // The provenance it also signs is not a signature. + let (mut registry, digest) = module_registry(false); + attach( + &mut registry, + &digest, + &instance.sign(&digest, SLSA_PROVENANCE, &release_signer()), + Shape::AnnotationsOnly, + ); + assert_eq!( + verify_with( + registry, + &digest, + keyless_verifier_with(instance.trusted_root(), true) + ), + Err(invalid( + &digest, + "signed statement's predicate type is \"https://slsa.dev/provenance/v1\", not https://sigstore.dev/cosign/sign/v1" + )) + ); + } + #[test] fn pae_matches_the_dsse_spec() { assert_eq!( diff --git a/crates/function/src/lib.rs b/crates/function/src/lib.rs index d9537d70..566cd702 100644 --- a/crates/function/src/lib.rs +++ b/crates/function/src/lib.rs @@ -24,4 +24,6 @@ pub mod resolver; pub mod runner; pub mod sandboxenv; pub mod store; +#[cfg(any(test, feature = "testutil"))] +pub mod testsigstore; pub mod validate; diff --git a/crates/function/src/testsigstore.rs b/crates/function/src/testsigstore.rs new file mode 100644 index 00000000..076b0169 --- /dev/null +++ b/crates/function/src/testsigstore.rs @@ -0,0 +1,385 @@ +//! An in-memory Sigstore instance for tests: a Fulcio certificate authority, +//! a certificate-transparency log and a Rekor log, each a P-256 key from a +//! fixed seed, that issues keyless cosign signatures and publishes the +//! trusted root that verifies them. The keyless path is then covered +//! offline, deterministically, without the public-good instance's rotating +//! material. The signing time is fixed in the past and the certificate is +//! valid for ten minutes around it, as Fulcio issues them, so a signature +//! verifies only because its signing time is authenticated by the Rekor +//! entry, never because the clock happens to fall inside the certificate. + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; +use p256::pkcs8::EncodePrivateKey as _; +use rcgen::{ + BasicConstraints, CertificateParams, CustomExtension, DistinguishedName, DnType, + ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, PublicKeyData as _, SanType, + SerialNumber, SigningKey as _, +}; +use sha2::Digest as _; +use sigstore_trust_root::TrustedRoot; +use time::format_description::well_known::Rfc3339; +use time::{Date, Duration, Month, OffsetDateTime}; +use x509_cert::der::asn1::ObjectIdentifier; +use x509_cert::der::{Decode as _, Encode as _}; + +use crate::cosign::{BUNDLE_MEDIA_TYPE, IN_TOTO_PAYLOAD_TYPE, IN_TOTO_STATEMENT, Signer}; + +/// Fulcio's OIDC issuer extension, the current form: a DER UTF8String. +const FULCIO_ISSUER_V2: &[u64] = &[1, 3, 6, 1, 4, 1, 57264, 1, 8]; +/// RFC 6962's embedded signed-certificate-timestamp list. +const CT_PRECERT_SCTS: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 4, 2]; +const CT_PRECERT_SCTS_OID: &str = "1.3.6.1.4.1.11129.2.4.2"; + +/// The instance: three keys and the certificate authority they anchor. +pub struct Instance { + fulcio: KeyPair, + fulcio_params: CertificateParams, + fulcio_der: Vec, + ct: KeyPair, + rekor: KeyPair, + /// When every authority became valid. + since: OffsetDateTime, + name: String, +} + +impl Default for Instance { + fn default() -> Self { + Self::new() + } +} + +impl Instance { + /// The instance the tests sign with. + pub fn new() -> Instance { + Self::with_seeds(21) + } + + /// A second instance whose trusted root vouches for nothing the first + /// signed. + pub fn other() -> Instance { + Self::with_seeds(31) + } + + /// An instance whose keys derive from base, base + 1 and base + 2. + pub fn with_seeds(base: u8) -> Instance { + let fulcio = key_from_seed(base); + let since = utc(2026, Month::January, 1); + let name = format!("sigstore-{base}.test"); + let mut params = CertificateParams::default(); + let mut dn = DistinguishedName::new(); + dn.push(DnType::OrganizationName, "function-wasm tests"); + dn.push(DnType::CommonName, format!("fulcio.{name}")); + params.distinguished_name = dn; + params.not_before = since; + params.not_after = utc(2036, Month::January, 1); + params.serial_number = Some(SerialNumber::from(vec![0x7f, base])); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.key_usages = vec![ + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::CrlSign, + KeyUsagePurpose::DigitalSignature, + ]; + let fulcio_der = params.self_signed(&fulcio).expect("ca").der().to_vec(); + Instance { + fulcio, + fulcio_params: params, + fulcio_der, + ct: key_from_seed(base + 1), + rekor: key_from_seed(base + 2), + since, + name, + } + } + + /// When the instance signs: fixed, in the past, so every verified time + /// is deterministic and every certificate it issues has expired. + pub fn signing_time() -> OffsetDateTime { + utc(2026, Month::October, 1) + Duration::hours(12) + } + + /// The trusted root a verifier of this instance's signatures reads: the + /// Rekor key, the Fulcio chain and the CT key, each valid since the + /// instance started. + pub fn trusted_root_json(&self) -> Vec { + let since = rfc3339(self.since); + let log = |key: &KeyPair, url: String| { + serde_json::json!({ + "baseUrl": url, + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": BASE64.encode(key.subject_public_key_info()), + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": {"start": since}, + }, + "logId": {"keyId": BASE64.encode(key_id(key))}, + }) + }; + serde_json::to_vec(&serde_json::json!({ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [log(&self.rekor, format!("https://rekor.{}", self.name))], + "certificateAuthorities": [{ + "subject": {"organization": "function-wasm tests", "commonName": format!("fulcio.{}", self.name)}, + "uri": format!("https://fulcio.{}", self.name), + "certChain": {"certificates": [{"rawBytes": BASE64.encode(&self.fulcio_der)}]}, + "validFor": {"start": since}, + }], + "ctlogs": [log(&self.ct, format!("https://ctfe.{}", self.name))], + "timestampAuthorities": [], + })) + .expect("trusted root") + } + + /// The trusted root, parsed. + pub fn trusted_root(&self) -> TrustedRoot { + TrustedRoot::from_json(std::str::from_utf8(&self.trusted_root_json()).expect("utf-8")) + .expect("trusted root parses") + } + + /// The keyless cosign bundle for a signature of subject (a sha256 + /// manifest digest) with predicate_type, by signer, made at + /// [`Instance::signing_time`]: a fresh signing key, a ten-minute Fulcio + /// certificate naming the signer with an embedded SCT, a Rekor dsse + /// 0.0.1 entry with its signed entry timestamp, inclusion proof and + /// checkpoint, no RFC 3161 timestamp. + pub fn sign(&self, subject: &str, predicate_type: &str, signer: &Signer) -> Vec { + let signed_at = Self::signing_time(); + let hex = subject.strip_prefix("sha256:").expect("a sha256 digest"); + let payload = serde_json::to_vec(&serde_json::json!({ + "_type": IN_TOTO_STATEMENT, + "subject": [{"digest": {"sha256": hex}, "annotations": {}}], + "predicateType": predicate_type, + "predicate": {}, + })) + .expect("statement"); + let leaf_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).expect("leaf key"); + let signature = leaf_key + .sign(&crate::cosign::pae(IN_TOTO_PAYLOAD_TYPE, &payload)) + .expect("sign"); + + // The certificate, twice: the SCT signs the TBS without its own + // extension, so a first pass with a placeholder yields those bytes, + // the CT log signs them, and the second pass carries the SCT. + let issuer = Issuer::from_params(&self.fulcio_params, &self.fulcio); + let not_before = signed_at - Duration::minutes(5); + let not_after = signed_at + Duration::minutes(5); + let precert = leaf_params(signer, not_before, not_after, vec![0, 0]) + .signed_by(&leaf_key, &issuer) + .expect("precert") + .der() + .to_vec(); + let tbs = tbs_without_sct(&precert); + let sct = self.sct(&tbs, &issuer_spki(&self.fulcio_der), unix_millis(signed_at)); + let leaf = leaf_params(signer, not_before, not_after, sct) + .signed_by(&leaf_key, &issuer) + .expect("leaf") + .der() + .to_vec(); + assert_eq!( + tbs_without_sct(&leaf), + tbs, + "the certificate's TBS must encode identically with and without the SCT" + ); + + // The Rekor entry: the dsse 0.0.1 body, its signed entry timestamp + // and a one-leaf tree whose checkpoint the log signs. + let envelope = serde_json::json!({ + "payload": BASE64.encode(&payload), + "payloadType": IN_TOTO_PAYLOAD_TYPE, + "signatures": [{"sig": BASE64.encode(&signature)}], + }); + let envelope_bytes = serde_json::to_vec(&envelope).expect("envelope"); + let leaf_pem = pem::encode(&pem::Pem::new("CERTIFICATE", leaf.clone())); + let body = serde_json::to_vec(&serde_json::json!({ + "apiVersion": "0.0.1", + "kind": "dsse", + "spec": { + "envelopeHash": {"algorithm": "sha256", "value": hex::encode(sha256(&envelope_bytes))}, + "payloadHash": {"algorithm": "sha256", "value": hex::encode(sha256(&payload))}, + "signatures": [{"signature": BASE64.encode(&signature), "verifier": BASE64.encode(leaf_pem.as_bytes())}], + }, + })) + .expect("body"); + let rekor_id = key_id(&self.rekor); + let integrated_time = signed_at.unix_timestamp(); + // Keys in the canonical (sorted) order Rekor's SET payload uses. + let set_payload = serde_json::to_vec(&serde_json::json!({ + "body": BASE64.encode(&body), + "integratedTime": integrated_time, + "logID": hex::encode(rekor_id), + "logIndex": 0, + })) + .expect("set payload"); + let set = self.rekor.sign(&set_payload).expect("set"); + let mut leaf_hash_input = vec![0u8]; + leaf_hash_input.extend_from_slice(&body); + let root = sha256(&leaf_hash_input); + let origin = format!("rekor.{} - 1", self.name); + let note_body = format!("{origin}\n1\n{}", BASE64.encode(root)); + let checkpoint_signature = self + .rekor + .sign(format!("{note_body}\n").as_bytes()) + .expect("checkpoint"); + let mut hinted = rekor_id[..4].to_vec(); + hinted.extend_from_slice(&checkpoint_signature); + let envelope_text = format!( + "{note_body}\n\n\u{2014} rekor.{} {}\n", + self.name, + BASE64.encode(hinted) + ); + + serde_json::to_vec(&serde_json::json!({ + "mediaType": BUNDLE_MEDIA_TYPE, + "verificationMaterial": { + "certificate": {"rawBytes": BASE64.encode(&leaf)}, + "tlogEntries": [{ + "logIndex": "0", + "logId": {"keyId": BASE64.encode(rekor_id)}, + "kindVersion": {"kind": "dsse", "version": "0.0.1"}, + "integratedTime": integrated_time.to_string(), + "inclusionPromise": {"signedEntryTimestamp": BASE64.encode(&set)}, + "inclusionProof": { + "logIndex": "0", + "rootHash": BASE64.encode(root), + "treeSize": "1", + "hashes": [], + "checkpoint": {"envelope": envelope_text}, + }, + "canonicalizedBody": BASE64.encode(&body), + }], + "timestampVerificationData": {"rfc3161Timestamps": []}, + }, + "dsseEnvelope": envelope, + })) + .expect("bundle") + } + + /// RFC 6962's SignedCertificateTimestampList with one SCT by the CT log + /// over the precertificate: the TBS without the SCT extension, bound to + /// the issuer's key. + fn sct(&self, tbs: &[u8], issuer_spki: &[u8], timestamp_ms: u64) -> Vec { + let mut signed = vec![0u8, 0u8]; // v1, certificate_timestamp + signed.extend_from_slice(×tamp_ms.to_be_bytes()); + signed.extend_from_slice(&[0, 1]); // precert_entry + signed.extend_from_slice(&sha256(issuer_spki)); + let len = u32::try_from(tbs.len()).expect("tbs length"); + signed.extend_from_slice(&len.to_be_bytes()[1..]); + signed.extend_from_slice(tbs); + signed.extend_from_slice(&[0, 0]); // no extensions + let signature = self.ct.sign(&signed).expect("sct"); + + let mut sct = vec![0u8]; // v1 + sct.extend_from_slice(&key_id(&self.ct)); + sct.extend_from_slice(×tamp_ms.to_be_bytes()); + sct.extend_from_slice(&[0, 0]); // no extensions + sct.extend_from_slice(&[4, 3]); // sha256, ecdsa + sct.extend_from_slice(&u16::try_from(signature.len()).expect("len").to_be_bytes()); + sct.extend_from_slice(&signature); + let mut list = Vec::new(); + list.extend_from_slice(&u16::try_from(sct.len() + 2).expect("len").to_be_bytes()); + list.extend_from_slice(&u16::try_from(sct.len()).expect("len").to_be_bytes()); + list.extend_from_slice(&sct); + list + } +} + +/// A P-256 key from a fixed seed, so an instance is the same every run. +fn key_from_seed(seed: u8) -> KeyPair { + let signing = p256::ecdsa::SigningKey::from_bytes((&[seed; 32]).into()).expect("key"); + let pem = signing + .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) + .expect("pkcs8"); + KeyPair::from_pem(&pem).expect("rcgen key") +} + +/// The Fulcio-shaped leaf: empty subject, the signer's identity as a +/// critical SAN, code signing, the OIDC issuer extension and the SCT list. +fn leaf_params( + signer: &Signer, + not_before: OffsetDateTime, + not_after: OffsetDateTime, + sct_list: Vec, +) -> CertificateParams { + let mut params = CertificateParams::default(); + params.distinguished_name = DistinguishedName::new(); + params.not_before = not_before; + params.not_after = not_after; + params.serial_number = Some(SerialNumber::from(vec![0x12, 0x34, 0x56])); + // rcgen's IA5 string type is private; the SAN variant's field infers it. + params.subject_alt_names = vec![if signer.identity.contains("://") { + SanType::URI(signer.identity.as_str().try_into().expect("ia5 identity")) + } else { + SanType::Rfc822Name(signer.identity.as_str().try_into().expect("ia5 identity")) + }]; + params.key_usages = vec![KeyUsagePurpose::DigitalSignature]; + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning]; + params.use_authority_key_identifier_extension = true; + let issuer = + CustomExtension::from_oid_content(FULCIO_ISSUER_V2, der(0x0c, signer.issuer.as_bytes())); + let sct = CustomExtension::from_oid_content(CT_PRECERT_SCTS, der(0x04, &sct_list)); + params.custom_extensions = vec![issuer, sct]; + params +} + +/// The certificate's TBS re-encoded without the SCT extension: what the CT +/// log signed, and what a verifier reconstructs. +fn tbs_without_sct(cert_der: &[u8]) -> Vec { + let cert = x509_cert::Certificate::from_der(cert_der).expect("certificate"); + let sct_oid = ObjectIdentifier::new_unwrap(CT_PRECERT_SCTS_OID); + let mut tbs = cert.tbs_certificate; + tbs.extensions = tbs + .extensions + .map(|exts| exts.into_iter().filter(|e| e.extn_id != sct_oid).collect()); + let mut out = Vec::new(); + tbs.encode_to_vec(&mut out).expect("tbs"); + out +} + +/// The issuer's SubjectPublicKeyInfo as its certificate encodes it. +fn issuer_spki(ca_der: &[u8]) -> Vec { + x509_cert::Certificate::from_der(ca_der) + .expect("ca certificate") + .tbs_certificate + .subject_public_key_info + .to_der() + .expect("spki") +} + +/// A log's key id: SHA-256 of its SubjectPublicKeyInfo. +fn key_id(key: &KeyPair) -> [u8; 32] { + sha256(&key.subject_public_key_info()) +} + +fn sha256(bytes: &[u8]) -> [u8; 32] { + sha2::Sha256::digest(bytes).into() +} + +/// One DER value of the given tag over content. +fn der(tag: u8, content: &[u8]) -> Vec { + let mut out = vec![tag]; + let len = content.len(); + if len < 0x80 { + out.push(len as u8); + } else if len < 0x100 { + out.extend_from_slice(&[0x81, len as u8]); + } else { + out.extend_from_slice(&[0x82, (len >> 8) as u8, len as u8]); + } + out.extend_from_slice(content); + out +} + +fn utc(year: i32, month: Month, day: u8) -> OffsetDateTime { + Date::from_calendar_date(year, month, day) + .expect("date") + .midnight() + .assume_utc() +} + +fn rfc3339(t: OffsetDateTime) -> String { + t.format(&Rfc3339).expect("rfc 3339") +} + +fn unix_millis(t: OffsetDateTime) -> u64 { + u64::try_from(t.unix_timestamp_nanos() / 1_000_000).expect("millis") +} diff --git a/crates/function/testdata/conformance/Cosign-KeyedWithoutKey.golden b/crates/function/testdata/conformance/Cosign-KeyedWithoutKey.golden new file mode 100644 index 00000000..67456b6f --- /dev/null +++ b/crates/function/testdata/conformance/Cosign-KeyedWithoutKey.golden @@ -0,0 +1,4 @@ +exit 1 +--- stdout +/KeyedWithoutKey.yaml: Composition/KeyedWithoutKey pipeline[0] KeyedWithoutKey: refused: cannot verify module oci /example/greeter@sha256:d5b02bee92d9172ffa64118cd26b0357ba5ac9788c7b0e84dd6ac0c664cf7759: no valid cosign signature for /example/greeter@sha256:d5b02bee92d9172ffa64118cd26b0357ba5ac9788c7b0e84dd6ac0c664cf7759: key-based signature cannot be verified: the runtime has no --cosign-key +--- stderr diff --git a/crates/function/testdata/conformance/Cosign-KeylessAgainstAnotherRoot.golden b/crates/function/testdata/conformance/Cosign-KeylessAgainstAnotherRoot.golden new file mode 100644 index 00000000..84ef6f26 --- /dev/null +++ b/crates/function/testdata/conformance/Cosign-KeylessAgainstAnotherRoot.golden @@ -0,0 +1,4 @@ +exit 1 +--- stdout +/KeylessAgainstAnotherRoot.yaml: Composition/KeylessAgainstAnotherRoot pipeline[0] KeylessAgainstAnotherRoot: refused: cannot verify module oci /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b: no valid cosign signature for /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b: keyless signature does not verify against the trusted root: invalid inclusion promise: No log key valid at integrated time 2026-10-01T12:00:00Z for log ID Rr2obBt5iPMXO1mFF1oFGLU4KyN3IUoqdIyQSu8q0HY= +--- stderr diff --git a/crates/function/testdata/conformance/Cosign-KeylessTrusted.golden b/crates/function/testdata/conformance/Cosign-KeylessTrusted.golden new file mode 100644 index 00000000..61381c2f --- /dev/null +++ b/crates/function/testdata/conformance/Cosign-KeylessTrusted.golden @@ -0,0 +1,5 @@ +exit 0 +--- stdout +/KeylessTrusted.yaml: Composition/KeylessTrusted pipeline[0] KeylessTrusted: OK (oci /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b) + module: sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b, 255 B, ABI v2 +--- stderr diff --git a/crates/function/testdata/conformance/Cosign-KeylessUntrusted.golden b/crates/function/testdata/conformance/Cosign-KeylessUntrusted.golden new file mode 100644 index 00000000..ef9b1d44 --- /dev/null +++ b/crates/function/testdata/conformance/Cosign-KeylessUntrusted.golden @@ -0,0 +1,4 @@ +exit 1 +--- stdout +/KeylessUntrusted.yaml: Composition/KeylessUntrusted pipeline[0] KeylessUntrusted: refused: cannot verify module oci /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b: no valid cosign signature for /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b: keyless signature by https://github.com/example/greeter/.github/workflows/release.yml@refs/tags/v1.0.0 (issuer https://token.actions.githubusercontent.com) is not trusted for /example/greeter: no trustSigner rule in the operator policy permits it +--- stderr diff --git a/crates/function/testdata/conformance/Cosign-KeylessWithoutTrustedRoot.golden b/crates/function/testdata/conformance/Cosign-KeylessWithoutTrustedRoot.golden new file mode 100644 index 00000000..d6408bbc --- /dev/null +++ b/crates/function/testdata/conformance/Cosign-KeylessWithoutTrustedRoot.golden @@ -0,0 +1,4 @@ +exit 1 +--- stdout +/KeylessWithoutTrustedRoot.yaml: Composition/KeylessWithoutTrustedRoot pipeline[0] KeylessWithoutTrustedRoot: refused: cannot verify module oci /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b: no valid cosign signature for /example/greeter@sha256:5e3c56b32296415a6fa9e13aad07e5636a6615866df94738b88bb9d9010dfa0b: keyless signature cannot be verified: the runtime has no --cosign-trusted-root +--- stderr diff --git a/crates/function/testdata/validate/signer-policy-untrusting.cedar b/crates/function/testdata/validate/signer-policy-untrusting.cedar new file mode 100644 index 00000000..01646040 --- /dev/null +++ b/crates/function/testdata/validate/signer-policy-untrusting.cedar @@ -0,0 +1,9 @@ +// Operator signer policy fixture that trusts only main-branch runs of the +// release workflow: a signature made on a version tag is refused. +permit (principal, action == Action::"requireSignature", resource); + +permit (principal, action == Action::"trustSigner", resource) +when { + context.issuer == "https://token.actions.githubusercontent.com" && + context.identity like "https://github.com/example/greeter/.github/workflows/release.yml@refs/heads/main" +}; diff --git a/crates/function/testdata/validate/signer-policy.cedar b/crates/function/testdata/validate/signer-policy.cedar new file mode 100644 index 00000000..14c07614 --- /dev/null +++ b/crates/function/testdata/validate/signer-policy.cedar @@ -0,0 +1,12 @@ +// Operator signer policy fixture: every module must be signed, and a keyless +// signature counts only when this organisation's release workflow made it on +// a version tag, authenticated by GitHub Actions. The resource is left +// unconstrained because the conformance registry's address is not known in +// advance; a real policy fences both rules to a Repository. +permit (principal, action == Action::"requireSignature", resource); + +permit (principal, action == Action::"trustSigner", resource) +when { + context.issuer == "https://token.actions.githubusercontent.com" && + context.identity like "https://github.com/example/greeter/.github/workflows/release.yml@refs/tags/v*" +}; diff --git a/crates/function/tests/conformance.rs b/crates/function/tests/conformance.rs index bc40c109..f2bab117 100644 --- a/crates/function/tests/conformance.rs +++ b/crates/function/tests/conformance.rs @@ -809,22 +809,10 @@ fn validate_resolve_oci_source_matches_the_goldens() { assert!(failures.is_empty(), "\n{}", failures.join("\n\n")); } -/// Cosign verification under the all-or-nothing --cosign-key: the harness -/// signs an artifact with a P-256 key the way cosign 3's key-based flow -/// does (a Sigstore bundle - DSSE over an in-toto statement - attached as -/// an OCI 1.1 referrer, listed by the referrers tag schema as on GHCR and -/// registry:3) and leaves another unsigned. The Go runtime read cosign 2's -/// legacy .sig signatures instead, so the unsigned refusal's wording is -/// this runtime's own. -#[test] -fn validate_resolve_cosign_matches_the_goldens() { - use function_wasm::cosign::testutil::{COSIGN_SIGN_PREDICATE, Shape, TestKey, attach}; - use function_wasm::oci::testregistry::{TestRegistry, digest_of, serve}; - - let rust = Path::new(env!("CARGO_BIN_EXE_function")); - let cwd = crate_dir(); - - let wasm = wat::parse_str( +/// The smallest ABI v2 component: a sync-lifted run that returns an empty +/// response, enough for `function validate --resolve` to inspect. +fn tiny_component() -> Vec { + wat::parse_str( r#"(component (core module $m (memory (export "memory") 1) @@ -839,18 +827,41 @@ fn validate_resolve_cosign_matches_the_goldens() { (canon lift (core func $i "run") (memory (core memory $i "memory")) (realloc (core func $i "cabi_realloc")))) )"#, ) - .expect("wat"); + .expect("wat") +} + +/// An OCI manifest of one wasm layer and the empty config, titled name so +/// two modules of the same bytes get distinct digests. +fn module_manifest(wasm: &[u8], name: &str) -> Vec { + use function_wasm::oci::testregistry::digest_of; let config = br#"{}"#; - let module_manifest = |name: &str| { - serde_json::to_vec(&serde_json::json!({ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": {"mediaType": "application/vnd.oci.empty.v1+json", "digest": digest_of(config), "size": config.len()}, - "layers": [{"mediaType": "application/wasm", "digest": digest_of(&wasm), "size": wasm.len()}], - "annotations": {"org.opencontainers.image.title": name}, - })) - .expect("manifest json") - }; + serde_json::to_vec(&serde_json::json!({ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": {"mediaType": "application/vnd.oci.empty.v1+json", "digest": digest_of(config), "size": config.len()}, + "layers": [{"mediaType": "application/wasm", "digest": digest_of(wasm), "size": wasm.len()}], + "annotations": {"org.opencontainers.image.title": name}, + })) + .expect("manifest json") +} + +/// Cosign verification under the all-or-nothing --cosign-key: the harness +/// signs an artifact with a P-256 key the way cosign 3's key-based flow +/// does (a Sigstore bundle - DSSE over an in-toto statement - attached as +/// an OCI 1.1 referrer, listed by the referrers tag schema as on GHCR and +/// registry:3) and leaves another unsigned. The Go runtime read cosign 2's +/// legacy .sig signatures instead, so the unsigned refusal's wording is +/// this runtime's own. +#[test] +fn validate_resolve_cosign_matches_the_goldens() { + use function_wasm::cosign::testutil::{COSIGN_SIGN_PREDICATE, Shape, TestKey, attach}; + use function_wasm::oci::testregistry::{TestRegistry, digest_of, serve}; + + let rust = Path::new(env!("CARGO_BIN_EXE_function")); + let cwd = crate_dir(); + + let wasm = tiny_component(); + let module_manifest = |name: &str| module_manifest(&wasm, name); let signed_manifest = module_manifest("signed"); let unsigned_manifest = module_manifest("unsigned"); let (signed_digest, unsigned_digest) = @@ -967,3 +978,150 @@ fn validate_resolve_cosign_matches_the_goldens() { } assert!(failures.is_empty(), "\n{}", failures.join("\n\n")); } + +/// Keyless cosign verification against an in-memory Sigstore instance +/// (`function_wasm::testsigstore`): a module signed keyless by a release +/// workflow identity, verified with the instance's trusted root under a +/// policy that trusts that identity, one that trusts another branch, and +/// another instance's root; a keyless module met by a key-only runtime, and +/// a key-signed module met by a trusted-root-only one. The refusal that +/// quotes sigstore-verify's own wording (`KeylessAgainstAnotherRoot`) pins +/// that crate's words too, so a bump of it may re-record that golden. +#[test] +fn validate_resolve_keyless_cosign_matches_the_goldens() { + use function_wasm::cosign::Signer; + use function_wasm::cosign::testutil::{COSIGN_SIGN_PREDICATE, Shape, TestKey, attach}; + use function_wasm::oci::testregistry::{TestRegistry, digest_of, serve}; + use function_wasm::testsigstore::Instance; + + let rust = Path::new(env!("CARGO_BIN_EXE_function")); + let cwd = crate_dir(); + let wasm = tiny_component(); + let keyless_manifest = module_manifest(&wasm, "keyless"); + let keyed_manifest = module_manifest(&wasm, "keyed"); + let (keyless_digest, keyed_digest) = (digest_of(&keyless_manifest), digest_of(&keyed_manifest)); + + let mut registry = TestRegistry { + manifests: [ + (keyless_digest.clone(), keyless_manifest), + (keyed_digest.clone(), keyed_manifest), + ] + .into(), + blobs: [(digest_of(&wasm), wasm.clone())].into(), + bearer: false, + referrers_api: false, + basic: false, + }; + let instance = Instance::new(); + let signer = Signer { + issuer: "https://token.actions.githubusercontent.com".to_string(), + identity: + "https://github.com/example/greeter/.github/workflows/release.yml@refs/tags/v1.0.0" + .to_string(), + }; + attach( + &mut registry, + &keyless_digest, + &instance.sign(&keyless_digest, COSIGN_SIGN_PREDICATE, &signer), + Shape::AnnotationsOnly, + ); + let key = TestKey::from_seed(5); + attach( + &mut registry, + &keyed_digest, + &key.bundle(&keyed_digest, COSIGN_SIGN_PREDICATE), + Shape::ArtifactType, + ); + let addr = serve(registry); + + let dir = tempfile::tempdir().expect("tempdir"); + let write = |name: &str, bytes: &[u8]| { + let path = dir.path().join(name); + std::fs::write(&path, bytes).expect("write"); + path.display().to_string() + }; + let root = write("trusted_root.json", &instance.trusted_root_json()); + let other_root = write("other_root.json", &Instance::other().trusted_root_json()); + let key_path = write("cosign.pub", key.public_pem.as_bytes()); + let trusting = cwd + .join("testdata/validate/signer-policy.cedar") + .display() + .to_string(); + let untrusting = cwd + .join("testdata/validate/signer-policy-untrusting.cedar") + .display() + .to_string(); + + let cases: [(&str, &str, Vec<&str>); 5] = [ + ( + "KeylessTrusted", + &keyless_digest, + vec![ + "--cosign-trusted-root", + &root, + "--sandbox-policy-file", + &trusting, + ], + ), + ( + "KeylessUntrusted", + &keyless_digest, + vec![ + "--cosign-trusted-root", + &root, + "--sandbox-policy-file", + &untrusting, + ], + ), + ( + "KeylessAgainstAnotherRoot", + &keyless_digest, + vec![ + "--cosign-trusted-root", + &other_root, + "--sandbox-policy-file", + &trusting, + ], + ), + // The all-or-nothing key regime meets a keyless signature. + ( + "KeylessWithoutTrustedRoot", + &keyless_digest, + vec!["--cosign-key", &key_path], + ), + // A trusted root alone meets a key-based signature. + ( + "KeyedWithoutKey", + &keyed_digest, + vec![ + "--cosign-trusted-root", + &root, + "--sandbox-policy-file", + &trusting, + ], + ), + ]; + let mut failures = Vec::new(); + for (name, digest, flags) in cases { + let file = dir.path().join(format!("{name}.yaml")); + std::fs::write( + &file, + format!( + "apiVersion: apiextensions.crossplane.io/v1\nkind: Composition\nmetadata:\n name: {name}\nspec:\n pipeline:\n - step: {name}\n functionRef: {{name: function-wasm}}\n input:\n apiVersion: wasm.fn.crossplane.io/v1\n kind: Input\n module: {{type: OCI, oci: {{ref: {addr}/example/greeter@{digest}}}}}\n" + ), + ) + .expect("write"); + let file = file.display().to_string(); + let mut args = vec![file.as_str(), "--resolve"]; + args.extend(flags); + let rust_out = run_validate(rust, &args, "", &cwd); + let subs = [ + (dir.path().display().to_string(), ""), + (addr.to_string(), ""), + ]; + if let Some(f) = assert_golden(&format!("Cosign/{name}"), &rust_out, &subs) { + failures.push(f); + } + } + assert!(failures.is_empty(), "\n{}", failures.join("\n\n")); +} From 2e8dc958b381524a14f8fb4b9d7faa195706d13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonasz=20Ma=C5=82ecki?= Date: Tue, 27 Oct 2026 08:36:56 +0100 Subject: [PATCH 5/5] docs: point the TUF follow-up at its issue The signatures page, the trust-model one-pager and AGENTS.md said the trusted root's TUF refresh was the open half of #116; it is now #183, split out so #116 closes with this branch on the file-based root. --- AGENTS.md | 2 +- docs/one-pager-trust-model.md | 2 +- docs/operators/signatures.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 09ec35d0..043eaf37 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -488,7 +488,7 @@ Readiness is answered twice — gRPC health on the function port and plain-HTTP ### Signatures -`--cosign-key` loads PEM public keys into `cosign::Verifier` (sigstore-crypto's `VerificationKey`) and `--cosign-trusted-root` a Sigstore trusted root for keyless bundles (sigstore-verify); verification runs **before** the caches, once per manifest digest per process, over the runtime's own registry client (same auth path as the pull); non-OCI sources are refused when a signature is required. The format is cosign 3's: a Sigstore bundle (v0.3) in an OCI 1.1 referrer of the module's manifest, discovered through the referrers API or, on a 404 (GHCR, `registry:3`), the `sha256-` referrers tag index; bundles are recognised by the layer media type, never by `artifactType` (cosign 3.0 sets none). A referrer counts only if its `subject` is the pinned digest; the bundle is fetched bounded and verified against its digest; a DSSE envelope signature over the PAE must verify with a configured key, and only then is the in-toto statement read: `Statement/v1`, `predicateType` exactly `https://sigstore.dev/cosign/sign/v1` (SLSA provenance and other attestations never count, whoever signed them) and a subject with the manifest digest. For a key-based bundle the keys are the trust root - its verification material (key hint, tlog entries) is ignored. Refusal reasons are sorted and deduplicated so the wording is deterministic whatever order a registry lists referrers in. cosign 2's legacy `.sig` signatures are not read (no lookup, no hint). A bundle carrying a certificate is keyless (#116): `sigstore_verify::Verifier::verify` (by the manifest digest, `VerificationPolicy::any_identity()`) establishes the signing time from its RFC 3161 timestamps and its Rekor entry's SET (a present-but-failing source is fatal, no fallback to the clock), verifies the inclusion proof and checkpoint, chains the certificate to the root's Fulcio authorities at that time and checks the signature; `cosign.rs` then requires the cosign predicate and asks the operator policy's `trustSigner` rules (`OperatorPolicy::permits_signer`: `context.issuer` and `context.identity` over the Repository hierarchy, default-deny, reached through the `cosign::SignerPolicy` trait) whether the signer is trusted for the module's repository. Without `--cosign-trusted-root` a keyless bundle is refused with a message naming the flag and never fails the check for a key bundle beside it. Key-based bundles stay on the key path on purpose: sigstore-verify's public-key policy refuses a bundle with neither a tlog entry nor a timestamp, which is what `cosign sign --key --tlog-upload=false` writes. The trusted root is a file read at startup (`cosign initialize` fetches it); a TUF refresh is not implemented. `cosign::testutil` (feature `testutil`) signs cosign-3-shaped key-based bundles and attaches them to `oci::testregistry`; `testsigstore::Instance` signs keyless ones against its own trusted root. A required module met by a runtime with neither flag is refused naming both: `the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it`. +`--cosign-key` loads PEM public keys into `cosign::Verifier` (sigstore-crypto's `VerificationKey`) and `--cosign-trusted-root` a Sigstore trusted root for keyless bundles (sigstore-verify); verification runs **before** the caches, once per manifest digest per process, over the runtime's own registry client (same auth path as the pull); non-OCI sources are refused when a signature is required. The format is cosign 3's: a Sigstore bundle (v0.3) in an OCI 1.1 referrer of the module's manifest, discovered through the referrers API or, on a 404 (GHCR, `registry:3`), the `sha256-` referrers tag index; bundles are recognised by the layer media type, never by `artifactType` (cosign 3.0 sets none). A referrer counts only if its `subject` is the pinned digest; the bundle is fetched bounded and verified against its digest; a DSSE envelope signature over the PAE must verify with a configured key, and only then is the in-toto statement read: `Statement/v1`, `predicateType` exactly `https://sigstore.dev/cosign/sign/v1` (SLSA provenance and other attestations never count, whoever signed them) and a subject with the manifest digest. For a key-based bundle the keys are the trust root - its verification material (key hint, tlog entries) is ignored. Refusal reasons are sorted and deduplicated so the wording is deterministic whatever order a registry lists referrers in. cosign 2's legacy `.sig` signatures are not read (no lookup, no hint). A bundle carrying a certificate is keyless (#116): `sigstore_verify::Verifier::verify` (by the manifest digest, `VerificationPolicy::any_identity()`) establishes the signing time from its RFC 3161 timestamps and its Rekor entry's SET (a present-but-failing source is fatal, no fallback to the clock), verifies the inclusion proof and checkpoint, chains the certificate to the root's Fulcio authorities at that time and checks the signature; `cosign.rs` then requires the cosign predicate and asks the operator policy's `trustSigner` rules (`OperatorPolicy::permits_signer`: `context.issuer` and `context.identity` over the Repository hierarchy, default-deny, reached through the `cosign::SignerPolicy` trait) whether the signer is trusted for the module's repository. Without `--cosign-trusted-root` a keyless bundle is refused with a message naming the flag and never fails the check for a key bundle beside it. Key-based bundles stay on the key path on purpose: sigstore-verify's public-key policy refuses a bundle with neither a tlog entry nor a timestamp, which is what `cosign sign --key --tlog-upload=false` writes. The trusted root is a file read at startup (`cosign initialize` fetches it); fetching and refreshing it over TUF is #183. `cosign::testutil` (feature `testutil`) signs cosign-3-shaped key-based bundles and attaches them to `oci::testregistry`; `testsigstore::Instance` signs keyless ones against its own trusted root. A required module met by a runtime with neither flag is refused naming both: `the operator policy requires a cosign signature, but the runtime has neither --cosign-key nor --cosign-trusted-root to verify it`. ### One-pagers diff --git a/docs/one-pager-trust-model.md b/docs/one-pager-trust-model.md index 91f02be6..19197d1e 100644 --- a/docs/one-pager-trust-model.md +++ b/docs/one-pager-trust-model.md @@ -107,7 +107,7 @@ which OIDC issuer and identity may vouch for a module - default-deny, so a trusted root alone admits nothing. Key-based bundles keep the key path: a bundle with a key hint never reaches the trusted root, and a keyless bundle met without one is refused by name, never mistaken for a key failure. The -root is a file read at startup; a TUF refresh is the open half of #116. +root is a file read at startup; fetching and refreshing it over TUF is #183. An operator `--sandbox-policy-file` makes the requirement per-repository: a `requireSignature` rule demands a signature for the repositories it names, diff --git a/docs/operators/signatures.md b/docs/operators/signatures.md index 5669cd96..59884c85 100644 --- a/docs/operators/signatures.md +++ b/docs/operators/signatures.md @@ -80,8 +80,8 @@ cannot be verified: the runtime has no --cosign-trusted-root`. `--cosign-key` and `--cosign-trusted-root` combine: a bundle with a key hint is checked against the keys, one with a certificate against the root, and a module is signed when any one bundle verifies. The trusted root is -read once at startup; refreshing it over TUF while the runtime runs is not -implemented yet ([#116](https://github.com/jonasz-lasut/function-wasm/issues/116)). +read once at startup; fetching and refreshing it over TUF, for an operator who +would rather not mount the file, is [#183](https://github.com/jonasz-lasut/function-wasm/issues/183). ## Requiring signatures per repository