Read trust-store hashes from package stamp files instead of pinning them - #142
Merged
Merged
Conversation
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
force-pushed
the
cabundle-hash-from-stamp-file
branch
from
August 12, 2026 14:42
8cf2a96 to
1184072
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CertificateAudit pinned the SHA-256 of
/etc/ssl/certs/ca-certificates.crtin the datastream. Every upstreamca-certificatesroll therefore turned a correctly-updated image into a STIG failure until the dailyupdate-ca-certworkflow re-pinned the value and its PR merged — 25+ of the commits ingit logare exactly that churn.The
ca-certificatespackage already ships the digest next to the bundle, insha256sumformat: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:4—textfilecontent54extracting the digest via^([0-9a-fA-F]{64})[ \t]+\*?ca-certificates\.crt$var:1—local_variableover that subexpressionste:1— comparesfilehash58againstvar:1rather than a literaltst:4— new criterion requiring the stamp to exist and parse, so a missing or malformed stamp fails rather than passing vacuously2. Java truststore verified where present (
186a06b)Conditional, since non-Java images must not fail for lacking a truststore:
Pairing
tst:6withtst:7is 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.yamlno longer has a hash to re-pin, so its extract+sedsteps are replaced by asha256sum -cguard 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 inspectscgr.dev/chainguard/jre:latestbecause that carries both stamps, so one export covers the whole rule;wolfi-basehas no Java and would leave the truststore criterion unguarded.IMAGE_REFstayswolfi-basefor 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-tamperedfixture), 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-dockeragainst the in-tree datastream:chainguard-base(Java truststore, consistent)jdk:latestchainguard-base(no Java)That second-to-last row is the point of the change: an A/B against the released datastream baked into the
openscapimage 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 withAddFile— 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-cleanandmake test-e2e-cabundle-tamperedPASS;oscap xccdf validatereports the same pre-existing SRC-9/SRC-10 group errors asmain, no new ones. The workflow guard body was executed against a realjreexport — clean passes, each drifted or missing stamp fails with an error naming which trust store.🤖 Generated with Claude Code