Skip to content

Conversation

@w0xlt
Copy link
Contributor

@w0xlt w0xlt commented May 26, 2025

This PR introduces an implementation of Hybrid Public Key Encryption (HPKE) using a secp256k1-based Diffie-Hellman KEM (per RFC 9180 and "secp256k1-based DHKEM for HPKE" specification). It provides the core cryptographic component needed to enable the Payjoin v2 protocol.

This is an exploratory PR intended to kickstart discussion on adding Payjoin v2 support to Bitcoin Core – feedback and reviews are very welcome.

Payjoin v2 makes use of a protocol called Oblivious HTTP (OHTTP) to strip client-identifying metadata from the request. This protocol use a binary-encoded HTTP message encapsulated through HPKE.

The core implementation is contained in the new files src/dhkem_secp256k1.h and src/dhkem_secp256k1.cpp:

  • Full HPKE Modes: All four HPKE modes are supported – Base, PSK, Auth, and AuthPSK.
  • Secp256k1 DHKEM: Uses secp256k1 for Diffie-Hellman key exchange. The Encap() and Decap() functions perform the base mode KEM (ephemeral ECDH using the receiver’s public key), while AuthEncap() and AuthDecap() extend this to authenticated mode (including the sender’s static key in the shared secret derivation). Internally, the shared secret is derived as specified by HPKE using HKDF (SHA256) extraction and expansion.
  • Key Schedule & Nonce Derivation: After KEM, the implementation runs the HPKE key schedule to derive the encryption key, base nonce, and exporter secret. It follows RFC 9180 §7.1, hashing the info and psk_id contexts and deriving the AEAD key and nonce. Per-message nonces are computed by XOR-ing the base nonce with a message sequence number as specified in RFC 9180 §5.2.
  • AEAD Encryption (ChaCha20-Poly1305): The HPKE context uses ChaCha20-Poly1305 as the authenticated encryption scheme. The code provides Seal() and Open() functions to encrypt and decrypt data using the derived context key and a given nonce. It uses Bitcoin Core’s existing ChaCha20Poly1305 implementation.

The test file validates all the functions mentioned above: Encapsulation/Decapsulation, Encryption/Decryption, Nonce and Key Schedule Logic. This uses the PDK (Payjoin Dev Kit) test vectors. The tests also show how to use the HPKE functions.

This implementation only uses HKDF-SHA256 as Key Derivation Functions and ChaCha20-Poly1305 as AEAD, since these already exist in the Bitcoin Core codebase. It does not support other KDFs (such as HKDF-SHA384 and HKDF-SHA512) or other AEADs (such as AES-128-GCM and AES-256-GCM).

To run the tests:
build/bin/test_bitcoin --log_level=all --run_test=dhkem_secp256k1_tests

@w0xlt w0xlt marked this pull request as draft May 26, 2025 08:04
@DrahtBot
Copy link
Contributor

DrahtBot commented May 26, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32617.

Reviews

See the guideline for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #28690 (build: Introduce internal kernel library by sedited)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

LLM Linter (✨ experimental)

Possible typos and grammar issues:

  • draft-wahby-cfrg-hpke-kem-secp256k1-01^2 -> draft-wahby-cfrg-hpke-kem-secp256k1-01 [remove stray superscript] [the caret+number looks like a footnote marker and breaks the name]
  • (skE * pkR)^17 -> (skE * pkR) [remove stray superscript] [the caret+number after the expression is an unclear footnote marker]
  • "HKDF-Extract & Expand(dh, "shared secret")^18." -> "HKDF-Extract & Expand(dh, "shared secret")." [remove stray superscript]

drahtbot_id_5_m

@w0xlt w0xlt force-pushed the secp256k1_hpke branch 2 times, most recently from 65acaaa to 9183a8e Compare May 26, 2025 09:34
@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/runs/42885844689
LLM reason (✨ experimental): The CI failure is due to a lint check raising an error over missing include guards in the header file.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@1440000bytes

This comment was marked as abuse.

@DanGould
Copy link

DanGould commented May 26, 2025

The main reason for the Bitcoin Core wallet to support payjoin directly is to reduce the number of necessary dependencies for those already using the core wallet. In particular rust-payjoin requires TLS (which is optional for the protocol) and rust implementations of cryptographic primatives that core could avoid with a bespoke implementation.

@theStack
Copy link
Contributor

theStack commented Jun 3, 2025

Fore more context to reviewers, could add a link to BIP 77 in the PR description, particularly to the cryptography part: https://github.com/bitcoin/bips/blob/master/bip-0077.md#secp256k1-hybrid-public-key-encryption

Full HPKE Modes: All four HPKE modes are supported – Base, PSK, Auth, and AuthPSK.

Do we need all of the four modes for Payjoin v2 support? Didn't look in-depth yet, but at least BIP77 contains only the two modes "Base" and "Auth" explicitly (and no mentions of "PSK"):
https://github.com/bitcoin/bips/blob/72af87fc72999e3f0a26a06e6e0a7f3134236337/bip-0077.md?plain=1#L286-L287
https://github.com/bitcoin/bips/blob/72af87fc72999e3f0a26a06e6e0a7f3134236337/bip-0077.md?plain=1#L376-L378

@w0xlt
Copy link
Contributor Author

w0xlt commented Jun 4, 2025

at least BIP77 contains only the two modes "Base" and "Auth" explicitly (and no mentions of "PSK"):

Good catch. I still need to verify whether PSK is used by OHTTP in the Payjoin v2 protocol, but since the https://github.com/payjoin/bitcoin-hpke project used by PDK implements and has test vectors for PSK, I added the PSK and AuthPSK modes here as well.

@nothingmuch
Copy link
Contributor

Good catch. I still need to verify whether PSK is used by OHTTP in the Payjoin v2 protocol,

I can confirm that neither BIP 77 nor the rust-payjoin implementations depend on the PSK functionality. There are ideas for future extensions that might make use of that, but they are only ideas at this point and would not be a part of BIP 77 itself.

@achow101
Copy link
Member

Are you still working on this?

@w0xlt
Copy link
Contributor Author

w0xlt commented Oct 24, 2025

PSK functionality removed.
Thanks for the suggestion @theStack and @nothingmuch

Copy link
Contributor

@fjahr fjahr left a comment

Choose a reason for hiding this comment

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

Just skimmed a bit to start. Before spending significant time on reviewing this, I think it would be great if this could be cleaned up to the point that it can be taken out of draft status. Potentially also consider splitting it into multiple commits.

@@ -0,0 +1,524 @@
// Copyright (c) 2018-present The Bitcoin Core developers
Copy link
Contributor

Choose a reason for hiding this comment

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

2018?

return std::nullopt; // Invalid sender private key
}

// 2.5. Verify that enc matches skE
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a 2.5 but no 3?

return std::nullopt; // Invalid receiver private key
}

// 2. Perform two ECDH operations with receiver's private key (skR):
Copy link
Contributor

Choose a reason for hiding this comment

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

There are two 2s here

@fanquake
Copy link
Member

It seems a bit premature to be doing code review or merging anything here, until some discussion has played out in #33684 in regards to approach, and implementation details (can you link to it from the PR description). There still seem to be high-level questions that need answering/agreement on.

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.

9 participants