Skip to content

feat(attest): carry a pinned release digest into the attestation - #15

Merged
senamakel merged 2 commits into
mainfrom
attest-pinned-release
Aug 14, 2026
Merged

feat(attest): carry a pinned release digest into the attestation#15
senamakel merged 2 commits into
mainfrom
attest-pinned-release

Conversation

@senamakel

Copy link
Copy Markdown
Member

The gap

#10 made a hash-verified module the only thing eligible to receive a secret, and
wrote the Attestation in attach_transport from allowlisted_hash — a re-read
of a modules.toml beside the artifact.

A module loaded from a GitHub release has no such file. acquire extracts into a
fresh tempfile::TempDir, so allowlisted_hash returns Ok(None) and the module
is never attested.

The consequence is not a degraded check, it is a silent dead end: a host can pin a
digest in source, have acquire verify it against the release's own
checksum.toml and against the bytes it downloaded, and still have every
call_confidential to that module refused with NotAttested. The pin was checked
twice and then dropped on the floor.

The existing docs named this and classified it as fail-closed-as-intended:

a GitHub-loaded module can never be a confidential recipient until the operator
also places its digest in the local allowlist beside it

There is no such "beside it" for a release download — the directory is created by
acquire moments earlier and destroyed with the host. So the workaround does not
exist, 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_sha256 is 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:

  • On diskmodules.toml, re-read at load time so an artifact that changed
    underneath the check does not become attested. Unchanged.
  • Compiled into the host — checked against the release manifest and against
    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_sha256 leaves the release's own checksum manifest as the only
claim 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 file
would 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 operator
vouched 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.toml beside the file, or one compiled into the host and checked against
the 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_pinned records its argument
without 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.toml present, and asserts the recorded digest is the
    pin 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.

cargo test --all-features                    295 passed, 0 failed
TINYBUS_TEST_MODULE=… cargo test -- --ignored  (attestation tests) 3 passed
cargo check --no-default-features            clean
clippy --all-features --all-targets -D warnings   clean

Why this is needed now

tinywallet is moving key derivation and signing into its module, so the host
sends 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.

senamakel and others added 2 commits August 14, 2026 09:24
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>
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@senamakel, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 409e2ff5-c72e-40df-83dd-af92cc953def

📥 Commits

Reviewing files that changed from the base of the PR and between 0937b77 and 004d7dd.

📒 Files selected for processing (5)
  • AGENTS.md
  • crates/tinybus/src/attest.rs
  • crates/tinybus/src/module/host.rs
  • crates/tinybus/src/module/host_test.rs
  • docs/modules/attest/README.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant