Skip to content

fix(sign): keep unbounded hashing and signing from starving the async executor (TOB-SIGSTORE-8) - #169

Closed
wolfv wants to merge 1 commit into
sigstore:mainfrom
wolfv:tob/8-nonblocking-sign
Closed

wolfv wants to merge 1 commit into
sigstore:mainfrom
wolfv:tob/8-nonblocking-sign

Conversation

@wolfv

@wolfv wolfv commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes Trail of Bits finding TOB-SIGSTORE-8 (severity: informational, category: denial of service): "Raw artifact signing performs potentially blocking actions in async context."

Signer::sign synchronously hashed and ECDSA-signed unbounded Artifact::Bytes input inside an async fn. On a large artifact this occupies an async executor worker thread for the whole duration, starving other tasks. KeyPair::sign also re-hashed the entire message inside aws-lc-rs before signing (and constructed a SystemRandom, which may perform I/O for entropy).

Mechanism

Two complementary changes, chosen over spawn_blocking because Signer::sign borrows the artifact bytes (Artifact<'a>), and spawn_blocking requires 'static — copying a potentially huge artifact to satisfy that would defeat the purpose:

  1. Prehashed ECDSA signing (sigstore-crypto): new KeyPair::sign_prehashed(Sha256Hasher) -> (Sha256Hash, SignatureBytes), built on aws_lc_rs::EcdsaKeyPair::sign_digest (available and algorithm-checked in the pinned aws-lc-rs 1.17). Signing the precomputed SHA-256 digest produces a signature that verifies equivalently to ECDSA-SHA256 over the raw message — the hashedrekord signature remains verifiable exactly as before — while making the signing step O(1) in the message size. It also drops the SystemRandom handle used by the full-message path.
  2. Cooperative hashing (sigstore-sign): caller-controlled input (artifact bytes in sign, DSSE PAE bytes in sign_raw_statement and create_dsse_rekor_entry) is now hashed without materializing a second full-size PAE buffer in 64 KiB chunks with a yield to the executor between chunks. The yield is a ~20-line runtime-agnostic yield_now future (wake + Pending once), so no tokio runtime dependency is added to the library and the code works on any executor.

The public API is unchanged. Signer::sign and Signer::sign_raw_statement document the executor behavior. Statement JSON validation in sign_raw_statement stays synchronous: statements carry metadata (subject digests, predicates), not artifact contents, so they are realistically small.

Testing

  • New: sha256_yielding chunked hashing matches one-shot sigstore_crypto::sha256 across boundary sizes (0, 1, chunk±1, multi-chunk).
  • New: streaming DSSE PAE hashing matches the materialized PAE across chunk boundaries.
  • New: prehashed signatures verify (via VerificationKey::verify) as ECDSA-SHA256 over the full raw message, in both sigstore-crypto and through the sigstore-sign helper.
  • cargo fmt, cargo clippy -p sigstore-sign -p sigstore-crypto --all-targets -- -D warnings, cargo test -p sigstore-sign -p sigstore-crypto (all pass, incl. doc-tests), cargo check --workspace.

🤖 Generated with Claude Code

@wolfv
wolfv force-pushed the tob/8-nonblocking-sign branch 3 times, most recently from d0538c1 to 12421ba Compare August 3, 2026 11:21
@wolfv
wolfv force-pushed the tob/8-nonblocking-sign branch from 12421ba to 671608f Compare August 13, 2026 10:39

@jku jku left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't quite undertand why the sign_prehashed() argument is Sha256Hasher and not the digest

  • we call finish_digest() anyway as the first thing in sign_prehashed
  • when we call sha256_yielding() we always await anyway

It's possible I don't fully grasp the async rust model... I guess what I'm asking is :
Does this design have an advantage over just adding sigstore_crypto::sha256_yielding() that has the same signature as sigstore_crypto::sha256 but yields

@wolfv
wolfv force-pushed the tob/8-nonblocking-sign branch from 671608f to 19434e8 Compare August 13, 2026 11:48
… executor (TOB-SIGSTORE-8)

Trail of Bits finding TOB-SIGSTORE-8 (informational, denial of service):
Signer::sign synchronously hashed and ECDSA-signed unbounded
Artifact::Bytes input inside an async fn, so a large artifact could
occupy an executor worker thread for its entire duration and starve
other tasks. KeyPair::sign additionally hashed the full message a
second time inside aws-lc-rs before producing the signature.

Mechanism:

- sigstore-crypto grows KeyPair::sign_prehashed(Sha256Hasher), built on
  aws-lc-rs EcdsaKeyPair::sign_digest. It signs the incrementally
  computed SHA-256 digest directly, producing a signature that verifies equivalently
  to ECDSA-SHA256 over the raw message while making the
  signing step O(1) in the message size (and dropping the SystemRandom
  handle the full-message path constructs).
- sigstore-sign hashes caller-controlled input (artifact bytes and DSSE PAE bytes) in 64 KiB chunks without materializing a full PAE copy, yielding to the executor between chunks
  via a small runtime-agnostic yield_now future (no new dependency,
  works on any executor), then signs the precomputed digest.
- Signer::sign and Signer::sign_raw_statement document the behavior;
  statement JSON validation stays synchronous since statements carry
  metadata, not artifact contents.

Tests cover chunked-vs-one-shot digest equivalence and that prehashed
signatures verify as ECDSA-SHA256 over the full message.

Signed-off-by: Wolf Vollprecht <w.vollprecht@gmail.com>
@wolfv
wolfv force-pushed the tob/8-nonblocking-sign branch from 19434e8 to 9898492 Compare August 13, 2026 11:51
@wolfv

wolfv commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

@jku There is one important advantage to carrying Sha256Hasher: if a yielding helper returned only Sha256Hash, calling KeyPair::sign(bytes) would synchronously hash the complete input a second time. aws-lc-rs::sign_digest needs its digest object, so sign_prehashed() finalizes and signs the same cooperatively-computed digest without another pass. Awaiting controls scheduling but does not make the finalized typed hash directly usable by sign_digest. I rebased this onto current main and preserved this behavior.

@wolfv

wolfv commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by the upstream stack #183#184#185. #183 preserves the incremental/prehashed foundation from this PR; #184 adds typed blob/digest inputs and makes digest signing work; #185 adds constant-memory sync and async reader APIs for signing and verification. Closing this fork-based version so review can continue on the real stack.

@wolfv wolfv closed this Aug 14, 2026
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