Skip to content

feat(prekeys): validate companion device-identity (ADV) on fetched bundles - #772

Merged
jlucaso1 merged 1 commit into
mainfrom
fix/validate-companion-device-identity
Jun 8, 2026
Merged

jlucaso1 merged 1 commit into
mainfrom
fix/validate-companion-device-identity

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

When establishing a Signal session from a fetched prekey bundle for a companion device (device != 0), WA Web (WAWebSignalSessionApi.createSignalSession -> WAWebAdvSignatureApi.validateADVwithIdentityKey) cryptographically binds the fetched identity key to the account's ADV signature chain. We did not: the bundle was built straight from <identity>/<skey>/<key> and the <device-identity> child was ignored on both the prekey-fetch path and the retry path. A malicious or compromised relay could therefore substitute a fabricated identity key (and signed prekey) for a victim's companion device and we would silently set up a session with the attacker. process_prekey_bundle only checks the signed-prekey signature against the bundle's own identity key (self-signed), which a forger also controls.

This adds wacore::adv::validate_adv_with_identity_key, mirroring validateADVwithIdentityKey:

  • account signature: verify(accountSignatureKey, ADV_PREFIX_DEVICE_IDENTITY_ACCOUNT_SIGNATURE || details || fetchedIdentity, accountSignature)
  • device signature: verify(fetchedIdentity, ADV_PREFIX_DEVICE_IDENTITY_DEVICE_SIGNATURE || details || fetchedIdentity || accountSignatureKey, deviceSignature)

The device signature is made with the fetched identity key, so it's the check that actually binds the fetched identity to the account chain; a relay-substituted identity fails it. Both message constructions were cross-checked against the captured SignatureApi.js ($/M) and against our own pairing direction in pair.rs (which signs the same [6,0]/[6,1] prefixed buffers). It tries the E2EE prefixes then the business-hosted [6,5]/[6,6] set rather than replicating WA Web's bizHostedDevicesEnabled gating — the prefix is only a domain separator, so accepting whichever the signer used stays sound.

Wired into both session-establishment paths: node_to_pre_key_bundle_ref (prekey fetch) and process_retry_key_bundle (retry receipt).

Behavior on the absent/invalid cases, and a note for review:

  • A present-but-invalid <device-identity> rejects the bundle. On the fetch path the parse loop then skips that device (matches WA Web's KeyBundleInWorker batch path).
  • A missing <device-identity> for a companion is logged but not fatal. WA Web throws here, but the e2e mock server is external and its device-identity behavior is unverified; hard-requiring it could break session establishment against servers/mocks that omit it. This is the one intentional divergence — worth tightening to a hard requirement once verified against the live/mock server. Please watch the E2E job on this PR: if the mock sends a dummy (present-but-invalid) device-identity, this would (correctly, by design) reject it and we'd adjust.

Tests: validate_adv_with_identity_key has unit coverage (valid E2EE + hosted accepted; substituted identity, missing device signature, and garbage rejected), plus wiring tests that parse_prekeys_response drops a companion bundle with an unverifiable device-identity and keeps a primary (device 0) bundle regardless.

…ndles

When establishing a Signal session from a fetched prekey bundle for a
companion device (device != 0), WA Web (SessionApi.createSignalSession ->
validateADVwithIdentityKey) cryptographically binds the fetched identity
key to the account's ADV chain. We did not: the bundle was built straight
from <identity>/<skey>/<key> and the <device-identity> child was ignored,
so a malicious relay could substitute a fabricated identity key for a
victim's companion device and we would silently set up a session with it.

Add wacore::adv::validate_adv_with_identity_key (account-sig over
prefix||details||identity, plus device-sig made with the fetched identity
over prefix||details||identity||accountKey -- the binding check), and call
it on both session-establishment paths: the prekey fetch (node_to_pre_key_bundle_ref)
and the retry receipt (process_retry_key_bundle). A present-but-invalid
device-identity rejects the bundle (the fetch loop skips that device); a
missing one is logged but not fatal, since the live/mock server set that
omits it is unverified (WA Web throws). Tries E2EE then business-hosted
prefixes instead of replicating the bizHostedDevicesEnabled gating.
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 215dadb7-254c-45b3-867c-515161ca7111

📥 Commits

Reviewing files that changed from the base of the PR and between 7b443fd and 75e3752.

📒 Files selected for processing (3)
  • src/retry.rs
  • wacore/src/adv.rs
  • wacore/src/prekeys.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened validation of companion device identity bundles by enforcing signature verification to prevent unauthorized device substitution.
  • Tests

    • Added comprehensive test coverage for device identity signature validation across different scenarios and configurations.

Walkthrough

This PR adds ADV chain signature validation to prevent companion-device key-bundle forgery. A new public validator in wacore/src/adv.rs verifies account and device signatures against identity keys under standard and hosted prefix families. Prekey parsing now validates companion <device-identity> fields and drops invalid bundles; the retry path applies the same validation during key-bundle processing.

Changes

Companion Device ADV Validation

Layer / File(s) Summary
ADV signature validator and test cases
wacore/src/adv.rs
New public function validate_adv_with_identity_key decodes ADV signed identity, derives public keys, constructs and verifies both account and device signatures against message bytes prefixed with standard or hosted families, accepting when either family validates. Test suite covers successful validation, identity key substitution attacks, missing device signatures, and garbage input.
Device-identity validation in prekey parsing
wacore/src/prekeys.rs
Prekey response parsing extracts <device-identity> from companion-device bundles (device != 0) and validates against fetched identity key. Invalid companion identities are dropped; missing <device-identity> for companion devices logs a warning and continues. Primary device (device == 0) skips validation entirely. Tests confirm companion bundles with invalid device-identity are excluded and primary bundles retained regardless.
Device-identity validation in retry key-bundle flow
src/retry.rs
Companion-device retry key-bundles now validate optional <device-identity> fields against identity keys; validation failure errors the bundle, and missing fields log a warning and proceed without validation.

Sequence Diagram(s)

The PR modifies three validation touchpoints (validator library, prekey parsing, retry processing) that all apply the same ADV signature verification logic. Rather than three separate diagrams, the validator diagram above captures the core verification flow that all three layers depend on.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#118: Gates ADV/device-identity validation on Jid.device for companion devices; the related PR fixes legacy JID parsing so companion device JIDs correctly populate device field to trigger validation.
  • oxidezap/whatsapp-rust#550: Both PRs modify src/retry.rs's process_retry_key_bundle—the other changes requester JID/signal address derivation while this adds ADV <device-identity> validation within the same flow.
  • oxidezap/whatsapp-rust#322: Both modify src/retry.rs's retry path in process_retry_key_bundle, with this PR adding companion <device-identity> ADV validation and the other handling signal-cache-based session logic.

Suggested labels

api-design

Suggested reviewers

  • Ari4ka
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding ADV validation for companion device-identity in fetched prekey bundles.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the security issue, implementation details, and behavior on edge cases.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/validate-companion-device-identity

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 and usage tips.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,925 2,925 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,446 8,446 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,485 49,485 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,001 55,001 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,211 113,215 -0.0%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,735 1,656,621 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 651,629 651,761 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 875,877 875,846 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,083,626 2,083,292 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 748,294 748,298 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,329,648 1,326,033 +0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,373,618 4,375,197 -0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 518,104 515,284 +0.5%
binary_benchmark::marshal_group::bench_marshal_allocating 45,381 45,381 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,431 45,431 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,334 66,334 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,492 43,492 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,487 45,487 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,945 4,945 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,976 4,976 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,747 6,747 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,544 528,544 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,165 528,165 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,411 529,411 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,732 5,417,732 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,047 5,362,047 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,365 13,276,365 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,274 48,274 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,335 48,335 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,659 66,659 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,291 8,291 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,141,085 4,144,636 -0.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,131 100,131 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,262 210,262 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,921 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 505,797 510,235 -0.9%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,974,457 11,976,441 -0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,920,952 4,956,822 -0.7%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,404 37,404 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,658 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

@jlucaso1
jlucaso1 merged commit db753be into main Jun 8, 2026
11 checks passed
@jlucaso1
jlucaso1 deleted the fix/validate-companion-device-identity branch June 8, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant