Skip to content

test(sdk/rust): Signal cross-language interop verification (part 8 of #18) — completes the port#80

Merged
graycyrus merged 2 commits into
tinyhumansai:mainfrom
graycyrus:feat/rust-signal-8-interop
Jun 17, 2026
Merged

test(sdk/rust): Signal cross-language interop verification (part 8 of #18) — completes the port#80
graycyrus merged 2 commits into
tinyhumansai:mainfrom
graycyrus:feat/rust-signal-8-interop

Conversation

@graycyrus

@graycyrus graycyrus commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

The headline deliverable of #18: prove the Rust Signal port is byte-for-byte compatible with the flagship TypeScript SDK.

Adds tests/signal_interop.rs, loading tests/vectors/signal_vectors.json — vectors produced by gen_signal_vectors.mjs running the real TS Signal modules (the same pinned set the Python port checks against, so a pass is genuine cross-language interop, not a tautology). Verifies every layer:

  • crypto — HKDF root/chain KDFs, message-key derivation, HMAC reproduce the TS bytes
  • AEAD — both directions: Rust decrypts the TS ciphertext and re-encrypts to identical bytes (incl. empty plaintext)
  • X3DH — X25519 public keys match; the Rust responder reaches the exact secret the TS initiator derived
  • Double Ratchet — decrypts the TS message stream, reproduces TS ciphertext byte-for-byte, handles out-of-order delivery
  • Sender Keys — verifies TS ed25519 signatures + decrypts TS group ciphertexts, and reproduces TS ciphertexts/signatures

9 interop tests, all green. Vectors copied verbatim from sdk/python/tests/vectors/ (regenerate via that dir's gen_signal_vectors.mjs).

Validation (sdk/rust/)

  • cargo fmt --check ✅ · cargo clippy --all-targets -- -D warnings ✅ · cargo test

⚠️ Stacked on #79 (part 7 — sender keys). This completes the Rust Signal E2E port (parts 1–8).

Closes #18

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Hardened sender-key message decryption to avoid mutating receiver state when a message is invalid or tampered, preventing poisoned/advanced chain state.
  • Tests
    • Added a new cross-language interoperability test suite for Signal cryptographic flows using pinned JSON test vectors.
    • Added sender-key tampering coverage to ensure failed decryptions don’t affect subsequent valid decryptions.
    • Introduced deterministic Signal test-vector fixtures (crypto, X3DH, double ratchet, sender keys).

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@graycyrus is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1610d8f290

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdk/rust/src/signal/sender_key.rs Outdated
Error::InvalidArgument("Sender key signature verification failed".into())
})?;

let message_key = self.message_key_for(message.iteration)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind sender-key iterations before ratcheting

In the current group envelope format (website/src/common/group-messaging.ts), senderKeyIteration is metadata outside the signed body, while the sender signs only the ciphertext. If a relay or stored envelope supplies a valid ciphertext/signature with a tampered future iteration, this call ratchets and caches/removes keys before decrypt performs the MAC check; when MAC then fails, the receiver state is already advanced, so the real message for that iteration can no longer be decrypted. Please either include the iteration in the signature/AD or compute against a copy and commit state only after decrypt succeeds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Binding the iteration into the signature/AD would break wire-compatibility with the TS sender (where senderKeyIteration is likewise unsigned metadata), so I took the other route you suggested: the receiver now ratchets a scratch copy and commits state only after the AEAD MAC passes (b74b9d5). A forged/tampered iteration can no longer advance or poison the chain — added a regression test (forged_iteration_does_not_advance_receiver_state) proving the genuine message at its real iteration still decrypts after a forged one fails.

… #18)

The headline deliverable of #18: prove the Rust Signal port is byte-for-byte
compatible with the flagship TypeScript SDK. Adds `tests/signal_interop.rs`,
which loads `tests/vectors/signal_vectors.json` — vectors produced by
`gen_signal_vectors.mjs` running the REAL TS Signal modules (the same pinned set
the Python port verifies against) — and checks every layer:

- crypto KDFs (HKDF root/chain, message-key derivation) + HMAC reproduce TS bytes
- AEAD both directions: Rust decrypts TS ciphertext AND re-encrypts to identical
  bytes (incl. empty plaintext)
- X3DH: X25519 pubkeys match; the Rust responder reaches the exact secret the TS
  initiator derived
- Double Ratchet: decrypts the TS message stream, reproduces TS ciphertext
  byte-for-byte, and handles out-of-order delivery
- Sender Keys: verifies TS ed25519 signatures + decrypts TS group ciphertexts,
  and reproduces TS ciphertexts/signatures

9 interop tests, all green. Vectors copied verbatim from
`sdk/python/tests/vectors/` (regenerate via that dir's `gen_signal_vectors.mjs`).

This completes the Rust Signal E2E port (parts 1-8). Closes #18.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@graycyrus graycyrus force-pushed the feat/rust-signal-8-interop branch from 1610d8f to 4a92248 Compare June 17, 2026 16:49
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa9ebc2b-bd45-4047-a314-53806fb01cdc

📥 Commits

Reviewing files that changed from the base of the PR and between 4a92248 and b74b9d5.

📒 Files selected for processing (2)
  • sdk/rust/src/signal/sender_key.rs
  • sdk/rust/tests/signal_sender_key.rs

📝 Walkthrough

Walkthrough

Adds two new files to the Rust SDK test suite: a pinned JSON fixture file (signal_vectors.json) containing deterministic hex/base64-encoded inputs and outputs for the Signal cryptographic pipeline, and a Rust test module (signal_interop.rs) that loads those vectors and asserts Rust Signal API outputs match TypeScript-generated expectations across KDF, HMAC, AEAD, X3DH, Double Ratchet, and Sender Keys. Also hardens the Sender Key receiver to prevent state mutation when decryption fails on invalid or tampered messages, and adds a test validating that behavior.

Changes

Signal Protocol Rust Cross-Language Interop Tests

Layer / File(s) Summary
Test vectors and helper infrastructure
sdk/rust/tests/vectors/signal_vectors.json, sdk/rust/tests/signal_interop.rs (lines 1–54)
Introduces the pinned JSON vector file with all hard-coded KDF, HMAC, AEAD, X3DH, ratchet, and sender-key fixtures, plus the test module header with compile-time vector loading and hex/base64/JSON helper utilities.
Crypto primitives and X3DH interop tests
sdk/rust/tests/signal_interop.rs (lines 55–175)
Verifies Rust KDF root/chain and message key derivation, HMAC, AEAD encrypt/decrypt (including empty-message case), X25519 public key derivation, and X3DH responder shared secrets against TS vector values via assert_eq!.
Double Ratchet interop tests
sdk/rust/tests/signal_interop.rs (lines 179–271)
Constructs deterministic SessionState instances for Bob and Alice from vector parameters and tests in-order decryption, out-of-order decryption, and that re-encryption reproduces TS ciphertext and header metadata exactly.
Sender Keys interop tests
sdk/rust/tests/signal_interop.rs (lines 272–323)
Restores receiver state from vector distribution fields and decrypts multiple TS sender-key messages, then restores a sender from seed/key material and asserts encrypted ciphertext bytes, signature bytes, and iteration counts match TS vectors.

Sender Key Receiver State Hardening

Layer / File(s) Summary
Receiver state mutation prevention and test
sdk/rust/src/signal/sender_key.rs, sdk/rust/tests/signal_sender_key.rs
Adds Clone derivation to GroupSenderKeyReceiver and refactors decrypt to clone receiver state before decryption attempt and commit state only on success, preventing forged/corrupt messages from advancing chain state. Includes test that verifies a message with tampered iteration fails decryption without advancing receiver state.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • tinyhumansai/tiny.place#73: The main PR's new signal_interop vectors/test module adds X3DH cross-language assertions that directly exercise the x3dh_initiate/x3dh_respond behavior implemented in the retrieved X3DH PR.
  • tinyhumansai/tiny.place#79: Both PRs focus on Signal sender-key group messaging, with the main PR adding cross-language vector tests (including sender keys) and also changing GroupSenderKeyReceiver in sdk/rust/src/signal/sender_key.rs (via decrypt state-handling and Clone), which is directly built on the sender-key implementation introduced in the retrieved PR.

Poem

🐇 Hop, hop through the ratchet chain,
Each hex string checked again and again,
TypeScript and Rust shake paws and agree,
The secrets derived match perfectly!
No tampered message shall poison the state—
The rabbit ensures decryption's fate. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding cross-language interoperability tests for the Rust Signal protocol port, identifying this as part 8 that completes the port.
Linked Issues check ✅ Passed All code requirements from issue #18 are met: X3DH implementation verified, Double Ratchet decryption/encryption tested, Sender Keys group messaging validated, and interop tests confirm byte-for-byte compatibility with TypeScript reference.
Out of Scope Changes check ✅ Passed All changes are in-scope: test vector file, interop test module, sender-key receiver state hardening for security, and a regression test for the hardening fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

The group message iteration is metadata outside the Ed25519-signed body (to
stay wire-compatible with the TS sender format), so a tampered future iteration
can pass signature verification yet fail the AEAD MAC. Previously the receiver
ratcheted its chain and cached/removed skipped keys *before* the MAC check, so a
forged or corrupt message advanced/poisoned the chain and the genuine message at
that iteration became undecryptable.

Ratchet a scratch copy and commit state only after decrypt succeeds. Adds a
regression test. Addresses CodeRabbit review on #80.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@graycyrus graycyrus merged commit 106d676 into tinyhumansai:main Jun 17, 2026
8 of 9 checks passed
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.

[Rust SDK] No end-to-end encryption (Signal protocol) — cross-language parity

1 participant