Skip to content

scanner: reject duplicate BIP21 parameter keys (#63) - #151

Open
Tyagiquamar wants to merge 3 commits into
synonymdev:masterfrom
Tyagiquamar:fix/bip21-duplicate-params
Open

Tyagiquamar wants to merge 3 commits into
synonymdev:masterfrom
Tyagiquamar:fix/bip21-duplicate-params

Conversation

@Tyagiquamar

Copy link
Copy Markdown

In src/modules/scanner/implementation.rs, decode_onchain() parsed BIP21 URI parameters by collecting them directly into a HashMap<String, String>.

Per BIP-0021 specification, duplicate parameter keys in a URI make the URI invalid. Collecting into a HashMap directly silently dropped earlier duplicate keys, overwriting them with the last key-value pair.

This PR updates query parameter parsing to check for duplicate keys, returning DecodingError::InvalidFormat if duplicate parameter keys are present.

Fixes #63

Testing

  • Added unit test test_duplicate_bip21_params_fails in src/modules/scanner/tests.rs.

@Tyagiquamar

Copy link
Copy Markdown
Author

Hi, friendly ping for review on this PR when you have a moment. Happy to address any feedback. Thanks!

@Tyagiquamar

Copy link
Copy Markdown
Author

Hi, just following up on this when you get a chance. The branch is up to date and checks are green. If it looks good from your side, it should be ready to merge. Happy to make any changes if needed. Thanks!

@pwltr pwltr left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes for three issues:

  1. The test suite will not compile as written. test_invalid_lightning_invoice_sync is synchronous but still uses #[tokio::test], whose target must be an async fn. Please either restore async or change the attribute to #[test].

  2. This does not reproduce or fix #63. The issue input contains two concatenated bitcoin: URIs and therefore two ? characters. decode_onchain still splits on every ? and parses only parts[1], silently discarding parts[2]. The duplicate-key loop never sees the second URI. Please add the exact payload from #63 as a regression test and preserve/validate the complete query, for example by using split_once('?') before rejecting the embedded second bitcoin: URI.

This distinction is confirmed by both consumer apps. Android and iOS added matching January 19 workarounds that detect a second bitcoin: prefix in their scan and manual-entry paths, explicitly referencing bitkit-core#63. Those workarounds would still be required after this PR.

  1. Rejecting every repeated query key is too broad. BIP 21 does not state that all duplicate keys are invalid, and its replacement BIP 321 explicitly permits repeated payment-instruction keys and requires accepting repeated unknown keys. Singleton fields such as amount, label, message, and pop should reject duplicates, but this needs a per-key policy rather than blanket HashMap rejection. BIP 321 also treats query keys as case-insensitive.

Relevant references:

…ncatenated URIs (synonymdev#63)

Signed-off-by: Tyagiquamar <mohdquamartyagi@gmail.com>
@Tyagiquamar

Copy link
Copy Markdown
Author

Thank you for the detailed review and reference links. All three items have been addressed in commit \27b3d92:

  1. Restored async test: \ est_invalid_lightning_invoice\ is now an \�sync fn\ using #[tokio::test]\ asserting \DecodingError::InvalidFormat.
  2. Issue fix : decode is digesting only the first duplicated Bip21 #63 concatenated URIs: \decode_onchain\ now detects and rejects any embedded secondary \�itcoin:\ URI prefix, uses \split_once('?'), and rejects inputs containing unexpected subsequent ?\ delimiters. Added the exact regression payload from fix : decode is digesting only the first duplicated Bip21 #63 (\ est_issue_63_concatenated_bip21_uris_fails).
  3. BIP 321 singleton duplicate policy: Replaced the blanket duplicate rejection with a per-key singleton check. Duplicates are rejected (case-insensitively) specifically for singleton fields (\�mount, \label, \message, \pop,
    eq-pop). Repeated payment instruction keys (such as \pj) and unknown query parameters remain permitted as required by BIP 321. Query keys are stored normalized to lower case. Added unit tests covering singleton duplicates, case-insensitivity, and permitted repeated keys.

All 28 scanner unit tests pass cleanly in Docker.

@pwltr pwltr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The original three findings are addressed, but two BIP 321 compliance issues remain:

  1. pop and req-pop are treated as separate singleton keys. The HashSet tracks their literal names independently, so ?pop=callback1&req-pop=callback2 succeeds. BIP 321 treats these as the same proof-of-payment field and explicitly lists that combination as invalid. Please map both names to one canonical singleton identity and add the specification's mixed-key example as a regression test.

  2. Rejecting every additional ? rejects valid query data. query.contains('?') rejects inputs such as ?message=Why?. BIP 321 defines qchar using the RFC 3986 query grammar, excluding only = and &; RFC 3986 explicitly permits ? as data inside the query component. The embedded-secondary-bitcoin: check already catches the issue #63 payload without this blanket restriction.

I reproduced both cases with focused tests against commit 27b3d92; both failed. The existing 28 scanner tests pass locally, including the exact #63 regression, and the updated scanner files pass rustfmt --check.

References:

…mark query data (synonymdev#63)

Signed-off-by: Tyagiquamar <mohdquamartyagi@gmail.com>
@Tyagiquamar

Copy link
Copy Markdown
Author

Thank you for the follow-up review and exact references. Both remaining BIP 321 compliance findings have been addressed in commit 7fcb909:

  1. pop and req-pop canonicalization:

    • In decode_onchain, singleton parameter tracking now maps both "pop" and "req-pop" to the canonical singleton identity "pop" in seen_singletons.
    • Any combination of duplicate proof-of-payment keys (pop + pop, req-pop + req-pop, or mixed pop + req-pop in either order) is now rejected with DecodingError::InvalidFormat.
    • The original parsed key name ("pop" or "req-pop") continues to be inserted into params so downstream consumer semantics remain intact. Repeated payment-instruction keys and unknown keys remain permitted.
  2. Allow RFC 3986 / BIP 321 query data containing ?:

    • Removed the blanket query.contains('?') rejection, which was incorrectly rejecting legal query data such as ?message=Why?.
    • Retained the targeted detection of secondary embedded bitcoin: URIs (query.to_ascii_lowercase().contains("bitcoin:")) so the concatenated URI attack from issue fix : decode is digesting only the first duplicated Bip21 #63 remains strictly rejected.
  3. Added regression tests:

    • test_duplicate_singleton_pop_req_pop_mixed_fails (testing ?pop=...&req-pop=...)
    • test_duplicate_singleton_req_pop_then_pop_fails (testing reverse ordering ?req-pop=...&pop=...)
    • test_duplicate_singleton_req_pop_fails (testing case-insensitive ?req-pop=...&REQ-POP=...)
    • test_bip321_spec_invalid_pop_req_pop_example_decode_onchain (testing the exact invalid BIP 321 URI bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?pop=callback%3a&req-pop=callback%3a against decode_onchain)
    • test_query_containing_question_mark_data_succeeds (testing valid queries like ?message=Why?&amount=0.000035)

Test & Check Results:

  • cargo test modules::scanner ran in Docker container (bitkit-test:latest on Rust 1.85 / bookworm): all 33 unit tests passed (33 passed, 0 failed).
  • rustfmt --edition 2021 --check src/modules/scanner/implementation.rs src/modules/scanner/tests.rs: passed with exit code 0.

@pwltr pwltr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No remaining findings. Approved.

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.

fix : decode is digesting only the first duplicated Bip21

3 participants