Skip to content

Issue 1707 ciphertext binding signature - #1814

Open
toniasteve15-ui wants to merge 10 commits into
Stellar-Mail:mainfrom
toniasteve15-ui:issue-1707-ciphertext-binding-signature
Open

Issue 1707 ciphertext binding signature#1814
toniasteve15-ui wants to merge 10 commits into
Stellar-Mail:mainfrom
toniasteve15-ui:issue-1707-ciphertext-binding-signature

Conversation

@toniasteve15-ui

Copy link
Copy Markdown

#closes #1707

PR: Envelope Signature Coverage for Ciphertext Binding

Branch: issue-1707-ciphertext-binding-signature
Base: issue-1495-concurrency-tokens
Issue: #1707
Scope: src/services/crypto/ only — no API, relay, UI, Soroban contract, or routing changes


Problem

SealedEnvelope has the shape { payload, ciphertext }. The wallet signs only the payload object. The ciphertext field is serialized as a separate, unsigned top-level field at relay submission time.

This means an adversary who intercepts a relay submission can:

  1. Replace the ciphertext bytes with arbitrary data.
  2. Compute a fresh SHA-256 hash of the new ciphertext and write it into payload.content_commitment.
  3. Re-submit — the original signature over payload is still valid because the ciphertext is never covered.

The signature format does not make the relationship between payload commitments and transmitted ciphertext unambiguous.


Solution

Introduce CiphertextBinding — a deterministic, JCS-canonicalized object that the wallet signs alongside the payload. It binds:

Field Prevents
ciphertext_commitment (v1:sha256:hex:<hex>) Ciphertext byte substitution
protected_headers.nonce Re-encryption under a different IV
protected_headers.mac Silent MAC replacement
suite Cross-algorithm confusion
version Cross-version replay

The wallet signs the binding preimage, not raw ciphertext bytes:

Stealth_Mail_Protocol:v1:<network>:ciphertext_binding:<JCS(binding)>

A SignedEnvelope extends SealedEnvelope with binding + signature. Recipients verify the Ed25519 signature over the preimage and optionally call assertBindingConsistency to confirm the binding matches the live payload fields.


Changed Files

[NEW] src/services/crypto/signed-envelope.ts

Export Description
CiphertextBinding Signed structure (version, suite, commitment, protected headers)
SignedEnvelope SealedEnvelope + binding + Ed25519 signature
BINDING_OPERATION Domain label "ciphertext_binding"
buildCiphertextBinding(payload, commitment) Constructs binding from sealed payload + versioned commitment string
buildBindingPreimage(binding, network?, version?) JCS + domain prefix → bytes to sign
verifyBindingSignature(envelope, sender, network?, version?) Ed25519 verification over binding preimage
assertBindingConsistency(envelope) Re-derives binding from payload; throws if mismatch detected

[NEW] tests/unit/crypto/signed-envelope.test.ts

20 tests across 5 suites covering all acceptance criteria.


Security Properties

Important

All four acceptance criteria from #1707 are enforced by the implementation and verified by tests.

Criterion Mechanism Test
Replacing ciphertext invalidates commitment verification ciphertext_commitment is SHA-256 of exact byte sequence commitment mismatch › substitution attack
Replacing ciphertext invalidates signature verification binding changes → old Ed25519 sig is invalid over new binding verifyBindingSignature › ciphertext substitution
Signed preimage is versioned and deterministic JCS canonicalization + version field in domain prefix buildBindingPreimage › deterministic, version-sensitive
Ciphertext bytes not duplicated in signature input Only commitment hash string appears in preimage buildBindingPreimage › commitment hash (not raw ciphertext bytes)

Note

assertBindingConsistency catches a second class of attack where an adversary transmits a tampered binding field alongside forged payload metadata but cannot forge a valid Ed25519 signature over the new binding.


Test Results

Test Files  24 passed (24)
     Tests  272 passed (272)   ← +20 new, 0 regressions
  Duration  2.31s

Suites added:

✓ buildCiphertextBinding (3 tests)
✓ buildBindingPreimage   (5 tests)
✓ verifyBindingSignature (5 tests)
✓ commitment mismatch    (4 tests)
✓ assertBindingConsistency (3 tests)

Reviewer Checklist

  • CiphertextBinding fields match the spec in the issue description
  • Preimage format (Stealth_Mail_Protocol:v1:<network>:ciphertext_binding:…) is acceptable for the wallet signing step
  • verifyBindingSignature correctly gates on payload.sender === expectedSender (defense-in-depth)
  • assertBindingConsistency is documented as a secondary check — primary security comes from verifyBindingSignature
  • No plaintext or raw key material appears in any export or error message
  • 272/272 tests pass locally (node_modules/.bin/vitest run tests/unit/crypto/)

Out of Scope (Separate Issues)

  • Wiring SignedEnvelope into the relay submission path (integration issue)
  • Wallet signing step calling buildBindingPreimage before submission (wallet integration issue)
  • openEnvelope calling verifyBindingSignature on inbound messages (inbound integration issue)
    #closes

Define a versioned CiphertextBinding structure and signing preimage that
binds the ciphertext commitment, protected headers (algorithm/nonce/mac),
suite identifier, and protocol version without duplicating raw ciphertext
bytes in the signature input.

New exports in signed-envelope.ts:
- CiphertextBinding / SignedEnvelope types
- buildCiphertextBinding(payload, commitment)
- buildBindingPreimage(binding, network, version)
- verifyBindingSignature(envelope, expectedSender, network, version)
- assertBindingConsistency(envelope)

Acceptance criteria verified:
- Ciphertext substitution invalidates commitment verification
- Ciphertext substitution invalidates signature verification
- Signed preimage is versioned and deterministic (JCS + domain prefix)
- Raw ciphertext bytes not duplicated in signature input
- Tests cover substitution and commitment mismatch scenarios

Tests: 20 new, 272 total passing, 0 regressions
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.

Add envelope signature coverage for ciphertext binding

2 participants