Skip to content

fix(multi-sig): sign_partial blind-signing oracle — verify bundle bytes against the event#358

Draft
bordumb wants to merge 1 commit into
mainfrom
fix/multisig-blind-signing
Draft

fix(multi-sig): sign_partial blind-signing oracle — verify bundle bytes against the event#358
bordumb wants to merge 1 commit into
mainfrom
fix/multisig-blind-signing

Conversation

@bordumb

@bordumb bordumb commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Multi-sig: sign_partial was a blind-signing oracle

Found by adversarial review of the matrix's untested-sensitive backlog (multi_sig::sign_partial had zero test coverage).

The bug

sign_partial signed the bundle's stored canonical_bytes (read from disk) directly, without verifying they equal serialize_for_signing(bundle.event):

let sig = keypair.sign(&bundle.canonical_bytes);   // signs whatever the bundle says

Meanwhile combine / validate_signed_event re-derive the canonical bytes from the event. So the bytes a signer signs and the bytes a verifier checks could diverge. A tampered bundle — event = a benign event shown to the operator, canonical_bytes = the canonicalization of a malicious event (e.g. a rotation adding the attacker's key) — yields a valid signature the attacker assembles into the malicious event. The signer approves X, signs Y. For a multi-sig whose whole point is independent per-signer approval, that breaks the guarantee.

The fix (fail-closed)

bundle_canonical(bundle) recomputes the canonical bytes from the event (the source of truth) and returns a typed MultiSigError::BundleMismatch if the stored bytes disagree — before any key is loaded. sign_partial signs the recomputed bytes. Legitimate bundles (where begin_multi_sig_event wrote consistent bytes) are unaffected; tampered ones are refused loudly. This also brings sign_partial into line with combine, which already trusts only the event.

Adversarially verified — two findings refuted

The same review raised two threshold bypasses on combine and refuted both (kept honest):

  • Duplicate-index quorum — refuted: Threshold::is_satisfied dedupes indices via a HashSet, so [idx 0, idx 0] counts as one signer.
  • Loose caller expected_kt — refuted: validate_signed_event enforces the event's own kt over verified indices, so a caller's looser expectation can't widen the real threshold.

Tests

  • bundle_canonical_rejects_a_tampered_bundle — the defense (would not have rejected before this change).
  • bundle_canonical_accepts_a_consistent_bundle — legitimate path unaffected.
  • 5/5 multi_sig tests pass (3 pre-existing combine tests still green).

Note: the gate is wired into sign_partial (let canonical = bundle_canonical(&bundle)?;) before any key access; the unit tests exercise the check directly.

Part of matrix ROADMAP_2 R2 (adversarial coverage of untested operations).

… blindly

`sign_partial` signed the bundle's stored `canonical_bytes` directly, without
checking they match `serialize_for_signing(bundle.event)`. A tampered bundle — a
benign event shown to the operator, but `canonical_bytes` belonging to a malicious
event — could harvest a valid signature over an event the signer never approved: a
blind-signing oracle on KEL events (key rotation, device addition). `combine` /
`validate_signed_event` already re-derive the canonical bytes from the event;
`sign_partial` did not, so what was signed and what was verified could diverge.

Fix: `bundle_canonical` recomputes the canonical bytes from the event (the source of
truth) and refuses a bundle whose stored bytes disagree (typed `BundleMismatch`,
fail-closed, before any key is loaded). `sign_partial` signs the recomputed bytes.

Found by adversarial review. Two other threshold bypasses on `combine` were raised and
REFUTED: `Threshold::is_satisfied` dedupes indices (no duplicate-signer quorum) and
`validate_signed_event` enforces the event's own `kt` over verified indices (a loose
caller-supplied `expected_kt` cannot widen it).

Tests: bundle_canonical_rejects_a_tampered_bundle (the defense) +
bundle_canonical_accepts_a_consistent_bundle; 5/5 multi_sig tests pass.

Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd
Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd
Auths-Anchor-Seq: 1
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
auths Ready Ready Preview, Comment Jun 25, 2026 12:08am

@github-actions

Copy link
Copy Markdown

Auths Commit Verification

Commit Status Details
866ec972 ❌ Failed No signature found

Result: ❌ 0/1 commits verified


How to fix

Commit 866ec972 has no Auths signature (no Auths-Id/Auths-Device trailer).

1. Install auths

macOS: brew install auths
Linux: Download from releases

2. One-time setup (creates your identity and configures Git)

auths init

3. Sign this branch and push

auths sign origin/main..HEAD
git push --force-with-lease

For CI to verify the signer, commit an identity bundle:

auths id export-bundle --alias main --output .auths/ci-bundle.json --max-age-secs 31536000

Quickstart →

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