From b8bf495d2bee72ae4af8a7f6611575963fa73a1b Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 2 Sep 2026 21:48:50 -0400 Subject: [PATCH] feat(deps)!: add cargo-deny with advisories, licenses, bans and sources checks Nothing checked this workspace's ~68 direct dependencies before this: no advisory scanning, no license policy, no duplicate-version check. deny.toml scopes the graph to the two targets this project actually builds for (x86_64-unknown-linux-gnu and didbot-site-anim's wasm32-unknown-unknown), accepts permissive licenses plus MPL-2.0 (weak, file-level copyleft) but no GPL/AGPL for a server binary holding operator credentials, and names every advisory ignore and duplicate-version skip with the reason it's tolerated rather than fixed. `cargo deny check` passes clean against the current Cargo.lock. Wired into prek's pre-commit hooks, gated on Cargo.toml/ Cargo.lock/deny.toml changes so it doesn't run on every commit. Co-Authored-By: Claude Opus 5 (1M context) Change-Id: Idf6f7afc5a52eb73aaafffa485c032cbc41bbe16 --- deny.toml | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ prek.toml | 10 +++ 2 files changed, 245 insertions(+) create mode 100644 deny.toml diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..52cc185b --- /dev/null +++ b/deny.toml @@ -0,0 +1,235 @@ +# cargo-deny configuration. Run with `cargo deny check`. +# +# Every section below is a decision, not the tool's default. See the +# workspace `Cargo.toml`'s own dependency comments (the jacquard pin, the +# `ring` vs `aws-lc-rs` choice) for the standard this file is trying to +# match: name the reason, not just the rule. + +# didbot ships two real artifacts: the server binary (`didbot-dev`, built by +# the Dockerfile from `rust:1.90-slim-bookworm`, i.e. glibc x86_64 Linux) and +# `didbot-site-anim`, compiled to `wasm32-unknown-unknown` for the did.bot +# site (see crates/didbot-site-anim/Cargo.toml and +# site/scripts/build-wasm.sh). Restricting the graph to these two targets, +# rather than every platform `Cargo.lock` happens to resolve entries for, +# is what keeps this file honest: `r-efi` (LGPL-2.1-or-later, pulled in only +# for UEFI/wasm32-wasi targets via getrandom's wasi backend) and a long tail +# of windows-only and jni/android-only crates never build for either binary +# this project actually produces, and listing them as license or ban +# exceptions would be recording a decision nobody needs to make. +[graph] +targets = [ + "x86_64-unknown-linux-gnu", + "wasm32-unknown-unknown", +] +# This project has no other registry and no reason to trust one; `cargo +# update` from a mirror or a `[patch]` pointed at a fork would be a supply +# chain change worth catching on its own, not something this audit should +# pre-approve. +all-features = false +no-default-features = false + +# advisories: RUSTSEC's database of known vulnerabilities, unmaintained +# crates, and yanked releases against the exact versions in Cargo.lock. +[advisories] +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +# Every id below is an explicit decision, each with the exposure it means +# for *this* project — not a blanket ignore list. `cargo deny check +# advisories` fails the moment a new one appears that isn't named here, +# which is the point: nothing is silently allowed. +ignore = [ + # RUSTSEC-2023-0071 -- Marvin Attack: `rsa` 0.9.10 has no non-constant- + # time-free release, so no upgrade fixes this. Reachable via + # didbot-attest (AWS instance-identity PKCS#7 verification, + # crates/didbot-attest/src/aws/pkcs7.rs) and via jose-jwk (pulled in by + # jacquard-oauth for JWK parsing). + # + # Checked both call sites: didbot-attest's production code only calls + # `RsaPublicKey::verify` (pkcs7.rs); the only `RsaPrivateKey`/`SigningKey` + # use in that crate is under `#[cfg(test)]` fixture code that signs + # fixtures for the verifier's own tests, never runs in a deployed + # instance. jose-jwk exists to parse the *server's* published JWK set for + # an OAuth *client* (didbot-claim authenticates as an account; it does + # not issue tokens), so it has no reason to hold or use an RSA private + # key either. The Marvin Attack is a timing side-channel in private-key + # operations observable over a network; a verify-only, public-key-only + # user of this crate does not expose it. Re-check this reasoning if + # either crate gains a private-key code path. + "RUSTSEC-2023-0071", + + # RUSTSEC-2025-0134 -- `rustls-pemfile` is unmaintained (archived + # upstream, its own maintainers point at `rustls-pki-types`'s `PemObject` + # trait as the replacement). Not a vulnerability report, and the crate's + # existing code is unchanged and still correct; it parses TLS + # certificate/key PEM files for didbot-tls's reload path + # (crates/didbot-tls). Worth migrating off in a dependency-bump change of + # its own -- out of scope here -- but nothing in the current code is + # known-broken. + "RUSTSEC-2025-0134", + + # chacha20 0.10.1, pulled in by rand 0.10.2 (via rmcp, didbot-mcp's MCP + # SDK), was yanked by its own maintainer on crates.io; no RUSTSEC id and + # no published yank message explain why, and 0.10.2 (unyanked, released + # the same day) is available and satisfies rand's existing `^0.10` + # requirement. Re-resolving to it is a one-line `cargo update -p + # chacha20` with no manifest change -- lower-risk than most bumps this + # audit was told to leave alone, but still a Cargo.lock change that + # belongs in its own commit where it can be verified rather than folded + # into an audit that touches no lockfile entries. Flagging as a + # follow-up rather than fixing here. + { crate = "chacha20@0.10.1", reason = "yanked by maintainer with no public reason given; unyanked 0.10.2 already satisfies rand's requirement -- do `cargo update -p chacha20` in its own change" }, +] + +# licenses: which SPDX terms this dependency tree may carry, and why. +# +# This project's own `license = "MIT"` (workspace Cargo.toml) says what +# didbot's *own* code is offered under. The dependency tree is judged +# against a stricter question: didbot ships as a server binary holding +# operator credentials and signing keys (see CLAUDE.md's principles), so +# strong copyleft (GPL/AGPL) has no exception here -- there is no +# "build-time tool" carve-out available to a statically linked, source- +# distributed server the way there would be for, say, a CI script. Weak/ +# file-level copyleft (MPL-2.0) and all the standard permissive terms are +# accepted; MPL's obligation attaches to the licensed files themselves, not +# to a program that links against them, which is a materially different +# question from GPL. +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Unlicense", + "Zlib", + "0BSD", + "MIT-0", + # Boost Software License. Permissive; used by `ryu` (dtolnay, + # dual-licensed Apache-2.0/BSL-1.0 -- cargo-deny picked the BSL-1.0 arm). + "BSL-1.0", + # Mozilla's Common Data Language Agreement, applied to `webpki-roots`' + # embedded Mozilla CA certificate bundle. It's a data-license on a list + # of root certificates, not a code copyleft term. + "CDLA-Permissive-2.0", + # Weak/file-level copyleft -- see the comment above this table. Carried + # by the `jacquard-*` crates, this project's atproto client/identity + # library. + "MPL-2.0", +] +confidence-threshold = 0.8 +exceptions = [] + +[[licenses.clarify]] +# ring's license field isn't SPDX-expressible (a mix of its own ISC-style +# grant, an OpenSSL-derived BSD/ISC blend, and public-domain code); this is +# the standard clarification the crate's own maintainers publish, matched +# against the LICENSE file's hash so it stops applying the moment the text +# changes underneath it. +crate = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +[licenses.private] +ignore = false + +# bans: duplicate versions of the same crate, and crates this project has +# decided never belong in its graph. +[bans] +multiple-versions = "deny" +wildcards = "deny" +# A workspace member depending on another only by version (no `path`) would +# resolve from crates.io instead of the sibling crate actually being edited +# -- exactly the kind of thing this project's `publish = false` workspace +# should never do by accident. +workspace-dependencies = { duplicates = "deny", include-path-dependencies = true } + +# Every skip below is a duplicate `cargo deny check bans` found under the +# real build targets (see `[graph.targets]`), each with why it's tolerated +# rather than fixed here. None of these are vendoring or pinning decisions; +# they're upstream crates' own choices about which major version of a +# dependency to require, several layers below this workspace's control. +skip = [ + # rand 0.8 / 0.9 / 0.10 in one graph, and it matters here specifically: + # this project generates OAuth PKCE verifiers and DPoP material through + # jacquard-oauth, which is pinned to rand 0.8 upstream (see the + # workspace Cargo.toml's jacquard pin comment -- unpinning it is a + # dependency bump of its own, not this audit's job). rand 0.8.7 carries + # no open RUSTSEC advisory as of this audit; the duplication is + # ecosystem churn, not a broken RNG. rand 0.9 is this workspace's own + # explicit pin (`rand = "0.9"` in Cargo.toml, used directly). rand 0.10 + # arrives via rmcp (the MCP server crate, didbot-mcp) and is independent + # of both. Three copies of a *keyed*-CSPRNG-adjacent crate would be a + # real hazard if they disagreed about the underlying `getrandom` call; + # they don't -- each rand major pulls its own getrandom major (below), + # and every one of those still reads from the OS's CSPRNG, not from each + # other. + { name = "rand", version = "0.8" }, + { name = "rand", version = "0.10" }, + { name = "rand_core", version = "0.6" }, + { name = "rand_core", version = "0.10" }, + { name = "rand_chacha", version = "0.3" }, + { name = "getrandom", version = "0.2" }, + { name = "getrandom", version = "0.4" }, + + # digest / block-buffer / crypto-common / cpufeatures, one version each + # newer than the rest of the graph: all four exist solely because + # argon2 0.6's internal blake2 use (RustCrypto's own KDF construction, + # crates/didbot-pds's password hashing) has moved ahead of the line + # every other RustCrypto crate in this graph (sha2, hmac, k256, p256, + # ed25519-dalek, ...) still uses. Not user-reachable: nothing in this + # workspace calls `digest`, `block-buffer` or `cpufeatures` directly, + # and argon2 does not expose blake2's trait implementations through its + # own API. Two *incompatible trait* copies would be a real bug source if + # this workspace's own code mixed them; it doesn't. + { name = "digest", version = "0.11" }, + { name = "block-buffer", version = "0.12" }, + { name = "crypto-common", version = "0.2" }, + { name = "cpufeatures", version = "0.3" }, + + # base64 0.22 (reqwest/axum's choice) vs 0.23 (rmcp and rcgen/pem's + # choice). Encoding only -- no key material handling of its own, unlike + # the entries above -- and the workspace's own security-relevant base64 + # use (PKCE's S256 challenge, see the Cargo.toml comment on the `base64` + # dependency) goes through the 0.22 line via reqwest. + { name = "base64", version = "0.23" }, + + # thiserror 1 vs 2: x509-parser 0.16 (didbot-tls's certificate parsing) + # hasn't moved to thiserror 2 yet; this workspace's own crates all use + # the 2.x line (`thiserror.workspace = true`). Error-derive macro only, + # no runtime behaviour difference that reaches across the boundary. + { name = "thiserror", version = "1" }, + { name = "thiserror-impl", version = "1" }, + + # Pure build-time / proc-macro or data-structure churn: no runtime + # security surface, and each pair is two crates in the graph disagreeing + # about a dependency's major version rather than anything this project + # chose. hashbrown has three because indexmap, ipld-core-adjacent code, + # and icu (unicode-segmentation's tables) each pin a different one. + { name = "syn", version = "3" }, + { name = "darling", version = "0.24" }, + { name = "darling_core", version = "0.24" }, + { name = "darling_macro", version = "0.24" }, + { name = "winnow", version = "1" }, + { name = "unicode-width", version = "0.1" }, + { name = "hashbrown", version = "0.14" }, + { name = "hashbrown", version = "0.15" }, + { name = "spin", version = "0.10" }, +] + +# Nothing is on this list yet, but the section exists so the next crate this +# project decides against has one line to add rather than a debate re-run +# from scratch. Candidates that came up while writing this file and were +# *not* added: `openssl`/`openssl-sys` (this workspace deliberately stays on +# rustls+ring end to end, see the workspace Cargo.toml's `instant-acme` +# comment) would be worth forbidding outright if it ever appears, but +# forbidding a crate that isn't in the graph today is a rule with nothing to +# test it against. +deny = [] + +# sources: where a dependency is allowed to come from. +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/prek.toml b/prek.toml index e6d97485..2ffb3239 100644 --- a/prek.toml +++ b/prek.toml @@ -61,6 +61,16 @@ hooks = [ # docs/ as well as src/, since the pages are include_str!'d into the facade # crate and a change to one is a change to the doc build's input. { id = "cargo-doc", name = "cargo doc", entry = "cargo doc --workspace --no-deps --all-features --document-private-items", language = "system", types = ["file"], files = '(\.rs$|^docs/.*\.md$|Cargo\.(toml|lock)$|^\.cargo/config\.toml$)', pass_filenames = false, stages = ["pre-commit"] }, + # Advisories, license terms, duplicate versions and dependency sources — + # against the exact graph Cargo.lock resolves, so this needs the lock file + # and gains nothing from running on a commit that doesn't touch it or a + # crate's own Cargo.toml. `deny.toml` itself is also a trigger: a policy + # change (a new allowed license, a new skip entry) should be checked the + # same way a dependency change is. + # + # Unlike cargo fmt/clippy/doc, `cargo-deny` doesn't ship with rustup: + # `cargo install cargo-deny` once, same as installing `prek` itself. + { id = "cargo-deny", name = "cargo deny", entry = "cargo deny check", language = "system", types = ["file"], files = '(^Cargo\.(toml|lock)$|^crates/.*/Cargo\.toml$|^deny\.toml$)', pass_filenames = false, stages = ["pre-commit"] }, # The two things the doc build cannot see: an untested rust code fence, and # a relative link between pages whose target no longer exists. { id = "doc-lint", name = "doc lint", entry = "scripts/doc-lint.sh", language = "system", types = ["file"], files = '^(docs/.*\.md|scripts/doc-lint\.sh)$', pass_filenames = false, stages = ["pre-commit"] }, -- 2.51.2