8000 trust store: declare ca-certificates on ruby, typst, atuin, gws, skopeo; document where the store comes from by bryan-minimal · Pull Request #957 · gominimal/pkgs · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,31 @@ Declares the package's output as a checked-in prebuilt binary rather than someth

The source tarball for a `prebuilt = true` package must already match the on-disk layout that the package emits — there's no build step to move files around. For example, a prebuilt `bash` package needs its tarball to contain the binary at `usr/bin/bash` so it lands where the `OutputBin` glob expects it.

## Trust store

The system trust store is the `ca-certificates` package. It installs the PEM bundle `/etc/ssl/certs/ca-certificates.crt`, one `<Label>.crt` file per root, and the OpenSSL `<subject_hash>.<n>` links next to them. `openssl` is built with `--openssldir=/etc/ssl`, so OpenSSL and what links it (curl, git, Python's `ssl`, Ruby) look for roots there. Go's `crypto/x509` and rustls-native-certs (through openssl-probe) do not go through openssldir: they probe `/etc/ssl/certs` themselves, which this layout also satisfies.

### How the store reaches an environment

Every environment mounts exactly a dependency closure and nothing else. A build sandbox gets the package's build and runtime deps with their runtime closures. A standalone test, a session or task, and an OCI image get the runtime closure of their packages. `ca-certificates` gets into a closure in one of two ways:

1. **Injected.** `ca-certificates` sets `attrs.needed_for_internet`. minimal adds every package with that attribute to the closure of any package that declares `needs.internet`. The injection then follows that package into the closure of everything that depends on it at runtime (minimal `crates/graph/src/transitives.rs`, `Transitives::new_with_seenset`). `curl` and `wget` declare `needs.internet`, so anything that brings in curl, git for example, also gets the store. The injected edge is not part of any spec hash, so a new bundle rebuilds none of these packages.
2. **Declared.** `ca-certificates` is listed in `runtime_deps`. This edge is hashed, so every bundle update rebuilds the package and everything that depends on it.

The planner also adds `needed_for_internet` packages to the top level of every build plan (the `HACK` in `ExecPlan::with_toplevels`, `crates/graph/src/planner.rs`). That only gets `ca-certificates` **built**. It does not put it into any environment. A session with `packages = ["python"]` has no `/etc/ssl/certs`.

### Rules

- **Declare the store when you read it.** If a package's binaries verify TLS against `/etc/ssl/certs` at runtime and nothing in its runtime closure already brings in `ca-certificates`, add `ca-certificates` to its `runtime_deps`. `ruby`, `typst`, `atuin`, `gws` and `skopeo` do this.
- **Not in the toolchain closure.** `python`, `perl` and `openssl` are build inputs of nearly every package. A `ca-certificates` edge on any one of them rehashes about 90% of the tree now, and again on every bundle update. `openssl` cannot take the edge at all, because `ca-certificates` build-depends on it. These three get the store only when something else in the environment brings it in. If you ship a Python or Perl program that talks TLS, add `ca-certificates` to that package.
- **Do not use `needs.internet` to get the store.** It also gives network access to the package's own build and to the build of everything that has it in its closure.
- **Do not embed a trust store.** A runtime reads `/etc/ssl/certs` through a build flag, a config default or a wrapper. It does not compile in its own roots or vendor a certifi-style bundle. Several upstreams default to a store of their own: node's bundled roots, bun, deno's webpki-roots, the JDK `cacerts` keystore, Chromium's root store, gcloud's vendored bundles, and Rust crates that pin `webpki-roots`. Switch them to the system store where upstream allows it. Where it does not, the package applies the denylist below, and its `build.ncl` says why it cannot use the system store.
- **Denylists: two today, meant to be one.** Distrusted roots, intermediates and keys are listed as SHA-256 fingerprints in two files under `packages/ca-certificates/`, and an incident has to land in both until they converge (gominimal/pkgmgr-rs#1020):
- `distrust.txt` (`<cert|spki> <sha256> <note>` lines, one block per incident citing its CVE and the Mozilla/NSS bugs). Read by the `ca-certificates` build gate (the build fails if any certificate it ships matches), by the Chromium policy checker (`.github/scripts/chromium_policy_distrust_check.py`, which requires every key in `packages/chromium-policies/ca-distrusted-certificates.json` to be an `spki` line here) and by `pkgmgr truststores`.
- `cve-2026-111943-blocked-sha256.txt`, exposed through `cve-2026-111943.ncl`. Read by the JDK patch (`packages/jdk/cve-2026-111943.sh`, used by jdk-21, jdk-25 and jdk-27), which needs all 48 certificate fingerprints, every malleable encoding of the compromised intermediates, because `blocked.certs` matches whole certificates, not keys.

Append a block for each incident and never delete one. Patched embedded stores read one of these files rather than keeping their own copy.
- **When a CA is distrusted:** rebuild `ca-certificates` from a dated curl/Mozilla extract, append the incident's block to `distrust.txt` (and, while the JDK patch still reads its own file, give it the incident's full certificate fingerprints), then fix every embedded store listed above. A package that only reads `/etc/ssl/certs` needs no edit; it gets the new bundle through its closure.

## Creating packages workflow

Expand Down
5 changes: 3 additions & 2 deletions packages/atuin/build.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let openssl = import "../openssl/build.ncl" in

let gcc = import "../gcc/build.ncl" in
let glibc = import "../glibc/build.ncl" in
let ca-certificates = import "../ca-certificates/build.ncl" in

let version = "18.23.0" in
{
Expand All @@ -33,9 +34,9 @@ let version = "18.23.0" in

runtime_deps = [
glibc,
# Verifies TLS against /etc/ssl/certs at runtime; see AGENTS.md "Trust store".
ca-certificates,
subsetOf gcc ["libgcc"],
# The native-tls switch makes this a RUNTIME dep too, not just a build
# one: the binary dynamically links libssl.so.3 + libcrypto.so.3.
# The native-tls switch makes openssl a RUNTIME dep too, not just a build
# one: the binary dynamically links libssl.so.3 + libcrypto.so.3. The
# `missing runtime_deps` checker caught it — without this the package
Expand Down
3 changes: 3 additions & 0 deletions packages/gws/build.ncl
8000
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let toolchain = import "../toolchain/build.ncl" in

let gcc = import "../gcc/build.ncl" in
let glibc = import "../glibc/build.ncl" in
let ca-certificates = import "../ca-certificates/build.ncl" in

let version = "0.22.5" in
{
Expand Down Expand Up @@ -34,6 +35,8 @@ let version = "0.22.5" in

runtime_deps = [
glibc,
# Verifies TLS against /etc/ssl/certs at runtime; see AGENTS.md "Trust store".
ca-certificates,
subsetOf gcc ["libgcc"],
],

Expand Down
35 changes: 35 additions & 0 deletions packages/ruby/build.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let pkgconf = import "../pkgconf/build.ncl" in
let rust = import "../rust/build.ncl" in

let glibc = import "../glibc/build.ncl" in
let ca-certificates = import "../ca-certificates/build.ncl" in
let gcc = import "../gcc/build.ncl" in
let gdbm = import "../gdbm/build.ncl" in
let gmp = import "../gmp/build.ncl" in
Expand Down Expand Up @@ -33,6 +34,8 @@ let version = "4.0.6" in
],
runtime_deps = [
glibc,
# Verifies TLS against /etc/ssl/certs at runtime; see AGENTS.md "Trust store".
ca-certificates,
subsetOf gcc ["libgcc", "libstdcpp"],
gdbm,
gmp,
Expand Down Expand Up @@ -86,6 +89,38 @@ let version = "4.0.6" in
],
} | Test,

# OpenSSL's default paths (OPENSSLDIR=/etc/ssl) reach the store that the
# ca-certificates runtime dep installs: every live per-root file under
# /etc/ssl/certs must verify through `set_default_paths` alone. Offline;
# without the runtime dep the directory is empty and the test fails. The
# empty-store check is the control: no root verifies against no paths.
# The 100-root floor counts LIVE roots, so a wrong sandbox clock that
# filters every root out fails the test instead of verifying nothing.
trust_store =
{
class = 'Standalone,
cmds = [
[
"/usr/bin/ruby",
"-ropenssl",
"-e",
m%"
dir = "/etc/ssl/certs"
now = Time.now
files = Dir.glob("#{dir}/*.crt") - ["#{dir}/ca-certificates.crt"]
roots = files.map { |f| OpenSSL::X509::Certificate.new(File.read(f)) }
abort "control: an empty store verified a root" if roots.any? { |c| OpenSSL::X509::Store.new.verify(c) }
live = roots.select { |c| c.not_before <= now && now < c.not_after }
abort "only #{live.size} live roots (of #{roots.size}) under #{dir} at #{now}" if live.size < 100
store = OpenSSL::X509::Store.new
store.set_default_paths
bad = live.reject { |c| store.verify(c) }
abort "default paths reject #{bad.size} of #{live.size} live roots, e.g. #{bad.first.subject}" unless bad.empty?
"%
],
],
} | Test,

# `gem install` puts a gem's executable in ~/.local/bin, where the session
# PATH resolves it, even when the user's own gemrc sets `gem:` flags; and
# `gem uninstall` removes it again (gominimal/inbox#584).
Expand Down
3 changes: 3 additions & 0 deletions packages/skopeo/build.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let pkgconf = import "../pkgconf/build.ncl" in
let toolchain = import "../toolchain/build.ncl" in

let glibc = import "../glibc/build.ncl" in
let ca 8BD0 -certificates = import "../ca-certificates/build.ncl" in

let version = "1.24.1" in
{
Expand All @@ -27,6 +28,8 @@ let version = "1.24.1" in

runtime_deps = [
glibc,
# Verifies TLS against /etc/ssl/certs at runtime; see AGENTS.md "Trust store".
ca-certificates,
],

cmd = "./build.sh",
Expand Down
3 changes: 3 additions & 0 deletions packages/typst/build.ncl
5B1F
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ let toolchain = import "../toolchain/build.ncl" in

let gcc = import "../gcc/build.ncl" in
let glibc = import "../glibc/build.ncl" in
let ca-certificates = import "../ca-certificates/build.ncl" in
let openssl = import "../openssl/build.ncl" in
let pkgconf = import "../pkgconf/build.ncl" in

Expand All @@ -27,6 +28,8 @@ let version = "0.15.1" in

runtime_deps = [
glibc,
# Verifies TLS against /etc/ssl/certs at runtime; see AGENTS.md "Trust store".
ca-certificates,
openssl,
subsetOf gcc ["libgcc"],
],
Expand Down
Loading
0