Skip to content

Read trust-store hashes from package stamp files instead of pinning them - #142

Merged
xnox merged 3 commits into
chainguard-dev:mainfrom
xnox:cabundle-hash-from-stamp-file
Aug 13, 2026
Merged

Read trust-store hashes from package stamp files instead of pinning them#142
xnox merged 3 commits into
chainguard-dev:mainfrom
xnox:cabundle-hash-from-stamp-file

Conversation

@xnox

@xnox xnox commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

CertificateAudit pinned the SHA-256 of /etc/ssl/certs/ca-certificates.crt in the datastream. Every upstream ca-certificates roll therefore turned a correctly-updated image into a STIG failure until the daily update-ca-cert workflow re-pinned the value and its PR merged — 25+ of the commits in git log are exactly that churn.

The ca-certificates package already ships the digest next to the bundle, in sha256sum format:

/etc/ssl/certs/.ca-certificates.crt.sha256   61efbd6d…  ca-certificates.crt

So read the expected value from there instead of pinning it. Java images ship the same arrangement for their truststore (/etc/ssl/certs/java/cacerts + .cacerts.sha256), which the rule previously ignored entirely — that is now covered too, conditionally.

Changes

1. CA bundle hash read from the stamp file (db34d4a)

  • obj:4textfilecontent54 extracting the digest via ^([0-9a-fA-F]{64})[ \t]+\*?ca-certificates\.crt$
  • var:1local_variable over that subexpression
  • ste:1 — compares filehash58 against var:1 rather than a literal
  • tst:4 — new criterion requiring the stamp to exist and parse, so a missing or malformed stamp fails rather than passing vacuously

2. Java truststore verified where present (186a06b)

Conditional, since non-Java images must not fail for lacking a truststore:

OR ─┬─ tst:5  no /etc/ssl/certs/java/cacerts        (non-Java image)
    └─ AND ─┬─ tst:6  stamp exists and parses
            └─ tst:7  cacerts SHA-256 == stamp digest

Pairing tst:6 with tst:7 is what keeps the OR from being a loophole: a truststore shipped without a stamp fails rather than falling through to the absent branch.

3. Workflow guard on both stamps (8cf2a96)

update-ca-cert.yaml no longer has a hash to re-pin, so its extract+sed steps are replaced by a sha256sum -c guard that fails the run if an image drops a stamp or ships one disagreeing with the file it names — the premise the pin used to guard. It inspects cgr.dev/chainguard/jre:latest because that carries both stamps, so one export covers the whole rule; wolfi-base has no Java and would leave the truststore criterion unguarded. IMAGE_REF stays wolfi-base for fixture re-pinning, which is a separate concern; both images are now resolved and cosign-verified through one helper.

Tradeoff — please review this specifically

This is drift detection, not tamper-evidence. Whoever can rewrite the bundle can rewrite the stamp beside it. What it still catches is a file modified outside the package (the cabundle-tampered fixture), since stamps are package-owned and read-only. If the STIG intent ("only approved trust anchors") demands tamper-evidence, the stamp needs corroboration from something the attacker does not control — apk package integrity or the signed index — not a second file in the same directory. Flagging it rather than deciding it.

Verification

Real images, via oscap-docker against the in-tree datastream:

image result
chainguard-base (Java truststore, consistent) pass
jdk:latest pass
chainguard-base (no Java) pass — absent branch
CA bundle tampered, stamp untouched fail
truststore tampered, stamp untouched fail
CA bundle updated with stamp regenerated pass (the released datastream fails this — the false positive removed)
truststore updated with stamp regenerated pass

That second-to-last row is the point of the change: an A/B against the released datastream baked into the openscap image shows it failing a correctly-updated image where this one passes.

New offline fixtures: fail_wrong_stamp_digest, fail_malformed_stamp, fail_missing_stamp, pass_java_truststore, fail_java_wrong_stamp, fail_java_missing_stamp. The offline base has no Java, so those three synthesize the pair with AddFile — the truststore's real JKS format is irrelevant to a SHA-256 comparison. This needed two new overlay ops (ReplaceFile, RemoveFile); Apply's writer already handled removal.

Also run: full go test -race ./... green; make test-e2e-baseline-clean and make test-e2e-cabundle-tampered PASS; oscap xccdf validate reports the same pre-existing SRC-9/SRC-10 group errors as main, no new ones. The workflow guard body was executed against a real jre export — clean passes, each drifted or missing stamp fails with an error naming which trust store.

🤖 Generated with Claude Code

@xnox
xnox requested a review from a team August 7, 2026 00:33
xnox and others added 3 commits August 12, 2026 14:42
CertificateAudit pinned the SHA-256 of /etc/ssl/certs/ca-certificates.crt
in the datastream, so every upstream ca-certificates roll turned a
correctly-updated image into a STIG failure until the daily
update-ca-cert workflow re-pinned the value and the PR merged.

The ca-certificates package already ships the digest next to the bundle
in /etc/ssl/certs/.ca-certificates.crt.sha256 (sha256sum format), so
read the expected value from there instead:

  - new textfilecontent54 object obj:4 extracts the digest from the
    stamp file via `^([0-9a-fA-F]{64})[ \t]+\*?ca-certificates\.crt$`
  - new local_variable var:1 exposes that subexpression
  - ste:1 compares filehash58 against var:1 rather than a literal
  - new test tst:4 requires the stamp to exist and parse, so a missing
    or malformed stamp fails the rule instead of passing vacuously

Note this is drift detection, not tamper-evidence: whoever can rewrite
the bundle can rewrite the stamp beside it. What it still catches is a
bundle modified outside the package (the cabundle-tampered fixture),
since the stamp is package-owned. Corroborating the stamp against apk
package integrity would be the stronger control.

Verified with an image simulating an upstream roll (bundle changed,
stamp regenerated): the released datastream fails it, this one passes,
and both still fail the tampered fixture.

Test coverage for the new dimension, in the offline matrix:
fail_wrong_stamp_digest, fail_malformed_stamp, fail_missing_stamp.
These needed overlay ops for replacing and removing a base entry;
Apply's writer already handled removal.

update-ca-cert.yaml no longer has a hash to re-pin. Its extract+sed
steps are replaced by a `sha256sum -c` guard that fails the run if a
future base image drops the stamp or ships one disagreeing with the
bundle — the premise the pin used to guard. Fixture and test-pin
re-pinning are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Java-based images ship a truststore at /etc/ssl/certs/java/cacerts with
the same style of package-recorded digest beside it
(/etc/ssl/certs/java/.cacerts.sha256, sha256sum format). CertificateAudit
ignored it, so a tampered truststore passed the rule even though the
system CA bundle it checks is the equivalent file for non-Java trust.

Extend the definition to cover it, conditionally — the truststore only
exists on Java images, and a non-Java image must not fail for lacking
one. The new criteria is an OR:

  - tst:5, unix:file_test with check_existence="none_exist": no
    truststore at all, i.e. a non-Java image; or
  - tst:6 AND tst:7: the stamp file exists and parses (obj:6, pattern
    `^([0-9a-fA-F]{64})[ \t]+\*?cacerts$`), and the truststore's SHA-256
    equals the digest it names, via var:2 and ste:3.

Pairing tst:6 with tst:7 is what stops the OR from being a loophole: a
truststore shipped without a stamp fails rather than falling through to
the absent branch.

Verified against real images with oscap-docker: chainguard-base with a
Java truststore passes, jdk:latest passes, chainguard-base without Java
passes via the absent branch, a truststore tampered without touching its
stamp fails, and a truststore legitimately updated with its stamp
regenerated passes.

The offline base has no Java, so the three new offline fixtures
synthesize the pair with AddFile: pass_java_truststore,
fail_java_wrong_stamp, fail_java_missing_stamp. The truststore's real
format does not matter to a SHA-256 comparison, so opaque bytes exercise
the check exactly as a JKS would.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stamp-file guard added with the pinned-hash removal only inspected
wolfi-base, which has no Java. That left the truststore criterion
unguarded: a jdk/jre image that dropped /etc/ssl/certs/java/.cacerts.sha256
or shipped one disagreeing with cacerts would surface as a red E2E run
rather than as a clear failure here.

Point the guard at cgr.dev/chainguard/jre:latest instead. It carries both
stamps CertificateAudit reads, so a single crane export covers the whole
rule. Each stamp is checked separately rather than as one combined
sha256sum -c, so the error names which trust store drifted, and a missing
stamp is reported distinctly from a mismatched one.

IMAGE_REF stays wolfi-base: it is what the E2E fixtures are pinned to, and
re-pinning them is a separate concern from verifying the rule's premise.
Both images are now resolved and cosign-verified through one helper.

Verified by running the guard body against a real jre export: clean export
passes, appending to cacerts fails naming the java directory, appending to
the CA bundle fails naming /etc/ssl/certs, and removing .cacerts.sha256
fails as missing. Also confirmed `exit 1` inside the verify helper aborts
the step rather than being swallowed by the command substitution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xnox
xnox force-pushed the cabundle-hash-from-stamp-file branch from 8cf2a96 to 1184072 Compare August 12, 2026 14:42

@aburrage-chainguard aburrage-chainguard left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@xnox
xnox merged commit 04abb36 into chainguard-dev:main Aug 13, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants