feat(attest): carry a pinned release digest into the attestation - #15
Conversation
A module loaded from a GitHub release extracts into a fresh temporary directory holding no `modules.toml`, so `allowlisted_hash` found nothing to compare against and the module was never attested. A host could pin a digest, have it checked against the release's own checksum manifest and against the downloaded bytes, and still have every confidential call to that module refused with NotAttested. Carry the verified pin down to the attestation instead of discarding it. Passing `expected_sha256` is now what makes a release-loaded module an attested recipient; omitting it leaves the publisher vouching for itself, and the module loads but stays ineligible for secrets. The recorded digest is the release archive's, not a re-hash of the extracted library: the archive is the artifact the operator named, and hashing the extracted file would report a number nobody vouched for, computed by the same code that would then trust it. Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: macbook Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reached
Next review available in: 99 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The gap
#10 made a hash-verified module the only thing eligible to receive a secret, and
wrote the
Attestationinattach_transportfromallowlisted_hash— a re-readof a
modules.tomlbeside the artifact.A module loaded from a GitHub release has no such file.
acquireextracts into afresh
tempfile::TempDir, soallowlisted_hashreturnsOk(None)and the moduleis never attested.
The consequence is not a degraded check, it is a silent dead end: a host can pin a
digest in source, have
acquireverify it against the release's ownchecksum.tomland against the bytes it downloaded, and still have everycall_confidentialto that module refused withNotAttested. The pin was checkedtwice and then dropped on the floor.
The existing docs named this and classified it as fail-closed-as-intended:
There is no such "beside it" for a release download — the directory is created by
acquiremoments earlier and destroyed with the host. So the workaround does notexist, and the release path is the one real hosts actually use.
The change
Carry the verified pin down to the attestation rather than discarding it.
expected_sha256is now what makes a release-loaded module attested.Two ways to vouch for an artifact, and they differ only in where the operator
wrote the digest:
modules.toml, re-read at load time so an artifact that changedunderneath the check does not become attested. Unchanged.
the downloaded bytes before anything is extracted. A pin is the stronger
statement, because it cannot be edited on the machine that runs it.
Omitting
expected_sha256leaves the release's own checksum manifest as the onlyclaim about the bytes — a publisher vouching for itself, not an operator vouching
for the publisher. That module loads and stays ineligible for secrets.
Which digest gets recorded, and why it is not the library's
The attestation carries the archive digest, not a re-hash of the extracted
.so. The archive is the artifact the operator named. Hashing the extracted filewould record a number nobody vouched for, computed by the same code that would
then be trusting itself for it — a check shaped like a check that establishes
nothing.
It also gives a sender something real to do: a host that pinned the digest can
compare
GetAttestation's answer against its own copy before parting with a key,instead of taking the host's word that some verification happened.
Attestation.sha256's doc is updated accordingly — "the bytes the operatorvouched for", which covers both paths.
Security Boundary change — flagged per AGENTS.md
The invariant now reads "hashed against a digest an operator asserted — a
modules.tomlbeside the file, or one compiled into the host and checked againstthe release manifest before extraction".
This widens which allowlist counts, not what a secret may reach. Delivery is
still to a loaded, hash-verified module or to nobody; a transport peer still never
qualifies; admission control, not isolation, is still the claim.
The one thing worth reviewing closely:
load_file_pinnedrecords its argumentwithout re-checking it, because the bytes it names are the archive and the archive
is gone by then. Verification therefore lives entirely in the caller. It is
private and documented as single-caller for exactly that reason — exposing it
would let a caller declare an artifact attested with nothing hashed. If it ever
needs a second caller, move the check down first.
Tests
Both new tests drive a real
dlopen'd module, not a fixture:a_module_from_a_pinned_release_becomes_an_attested_recipient_without_an_allowlist_file— attests with no
modules.tomlpresent, and asserts the recorded digest is thepin and differs from the library file's own hash, pinning the archive-vs-library
decision so a later cleanup cannot quietly swap in a local re-hash.
a_module_with_neither_a_pin_nor_an_allowlist_is_loaded_but_never_attested— the regression from the other side, and asserts loading still succeeds:
unattested means ineligible for secrets, not inadmissible.
The pre-existing allowlist tests are untouched and still pass, which is the
evidence that the on-disk path did not move.
Why this is needed now
tinywalletis moving key derivation and signing into its module, so the hostsends a recovery phrase over the bus as a confidential call. Without this, that
call is refused on every machine that installs the module the normal way.
No new dependencies.