Skip to content

Conversation

@MkDev11
Copy link
Contributor

@MkDev11 MkDev11 commented Jan 9, 2026

Add HPKE (Hybrid Public Key Encryption) support per RFC 9180

Summary

This implements HPKE Base mode as defined in RFC 9180, providing a simple single-shot API for hybrid public key encryption.

What's Included

  • Single-shot API: suite.encrypt() and suite.decrypt() methods
  • KEM: X25519 (DHKEM)
  • KDF: HKDF-SHA256
  • AEAD: AES-128-GCM

API Design

from cryptography.hazmat.primitives.hpke import Suite, KEM, KDF, AEAD
from cryptography.hazmat.primitives.asymmetric import x25519

suite = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM)

# Generate recipient keys
private_key = x25519.X25519PrivateKey.generate()
public_key = private_key.public_key()

# Encrypt (returns enc || ciphertext)
ciphertext = suite.encrypt(b"secret message", public_key, info=b"app", aad=b"data")

# Decrypt
plaintext = suite.decrypt(ciphertext, private_key, info=b"app", aad=b"data")

Design Decisions

  • Single-shot only: No multi-message context API in this initial version
  • enc || ct format: Like Go's single-shot HPKE API
  • Opaque enum values: Enum values are opaque strings, not RFC numeric identifiers

Closes

Closes #14073

@alex
Copy link
Member

alex commented Jan 9, 2026

Please take a look at the failing CI jobs.

@MkDev11 MkDev11 force-pushed the add-hpke-support branch 4 times, most recently from 770943f to c8d3bd2 Compare January 9, 2026 14:17
@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 9, 2026

@reaperhulk @alex can you please review the PR and let me know the result?

@MkDev11 MkDev11 mentioned this pull request Jan 9, 2026
5 tasks
@alex
Copy link
Member

alex commented Jan 9, 2026

Please be more patient. This PR will get reviewed, but we all have day jobs.

@MkDev11 MkDev11 force-pushed the add-hpke-support branch 3 times, most recently from 035856c to d874aab Compare January 9, 2026 17:07
@alex
Copy link
Member

alex commented Jan 10, 2026

A few high level notes, I haven't reviewed in depth yet:

  1. Vectors should be submitted in a seperate PR, in the vectors/ director and documented in test-vectors.rst
  2. We strongly avoid subclassing, it shouldn't be necessary here.
  3. uv.lock shouldn't be checked in, not sure why that's here
  4. The overall size of this PR is too large to be effectively reviewed. The best path is likely to pare this down to a single algorithm of each type and then we can add additional algorithms in follow up PRs.

@MkDev11 MkDev11 force-pushed the add-hpke-support branch 4 times, most recently from 0811f29 to 065b482 Compare January 10, 2026 12:38
Copy link
Member

@alex alex left a comment

Choose a reason for hiding this comment

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

I started reviewing, however I think my request on trimming this down was unclear:

We still want the initial PR to have the overall structure and final API we want, we just want to trim it down by implementing it for only one of the algorithms to reduce the code to review.

Copy link
Member

@alex alex left a comment

Choose a reason for hiding this comment

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

The API here still doesn't match what was discussed on the issue, so I think we may be talking past each other. Can you explain why this has a different API?

API changes per maintainer feedback:
- Add Suite(KEM, KDF, AEAD) class with RFC parameter order
- Add KEM, KDF, AEAD enum types for algorithm selection
- suite.sender(public_key, info) returns SenderContext
- suite.recipient(enc, private_key, info) returns RecipientContext
- First encrypt() returns enc || ciphertext concatenated
- Subsequent encrypt() calls return just ciphertext
- Use encrypt/decrypt method names
- Simpler KEM names (X25519 instead of DHKEM_X25519_HKDF_SHA256)

This matches the API discussed in issue pyca#14073 and addresses
reviewer feedback about the API mismatch.
Copy link
Member

@alex alex left a comment

Choose a reason for hiding this comment

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

I see you're still working, so submitting some quick comments.

Are you intended to submit the vectors as a seperate PR?

- Replace TypedDict with frozen dataclasses for parameter types
- Use HKDF.extract() static method instead of hmac.digest
- Remove base HPKEError class, keep only MessageLimitReachedError
- Use if/elif instead of match for Python 3.8 compatibility
@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 12, 2026

I see you're still working, so submitting some quick comments.

Are you intended to submit the vectors as a seperate PR?

yes, I plan to submit the test vectors as a separate PR. should I create that PR now, or wait until the API design is finalized based on your feedback about the enc prepending behavior and thread safety concerns?

@alex
Copy link
Member

alex commented Jan 12, 2026

The vectors can be submitted concurrently if they're ready, thanks.

@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 12, 2026

The vectors can be submitted concurrently if they're ready, thanks.

sure, I can work on the vectors PR after this one is merged to avoid having multiple open PRs on Gittensor. is there any other feedback on the current implementation I should address in the meantime?

@alex
Copy link
Member

alex commented Jan 12, 2026

I don't know what a git tensor is, but that's not going to dictate our development processes.

@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 12, 2026

I don't know what a git tensor is, but that's not going to dictate our development processes.

understood, apologies for the confusion. i'll prepare the vectors PR. in the meantime, are there any other changes needed on this PR? I'm still waiting on clarification about the enc prepending behavior (always vs first message only) and the thread safety concern you mentioned

The type system guarantees only valid enum values can be passed to
these internal functions, so use assert which is excluded from coverage.
@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 12, 2026

thanks for merging my PR. could you please review the changes I made?

@alex
Copy link
Member

alex commented Jan 13, 2026

It's on my TODO list. Unless we've been radio silent for several days, there's no need to follow up, we haven't forgotten.

…ctors

- Remove first_message special behavior from SenderContext and RecipientContext
- encrypt() now always returns just ciphertext, enc accessed via property
- Remove try/except Exception - AESGCM.decrypt raises InvalidTag directly
- Update tests to use new API (enc via sender.enc property)
- Add test vector loading from RFC 9180 vectors (when available)
- Update docstring example to show correct usage
@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 13, 2026

thanks for your feedback, just pushed the changes

@MkDev11 MkDev11 requested a review from alex January 19, 2026 01:31
@alex
Copy link
Member

alex commented Jan 19, 2026

Hi, @reaperhulk and I discussed and we'd like to make the following changes to the API:

  1. The enums for each KEM/AEAD/KDF shouldn't be the RFC value, they should just be opaque.
  2. We want to start with a single-shot API only, so suite.encrypt() + suite.decrypt() with no multi-shot. The result will be enc || ct like Go's single-shot APIs.

This should hopefully make the PR much simpler/shorter to get started.

- Replace sender()/recipient() context methods with encrypt()/decrypt()
- encrypt() returns enc || ciphertext (like Go's single-shot API)
- decrypt() takes enc || ciphertext and returns plaintext
- Make enum values opaque strings instead of RFC numeric values
- Remove SenderContext, RecipientContext, MessageLimitReachedError
- Update tests for new single-shot API
@MkDev11
Copy link
Contributor Author

MkDev11 commented Jan 19, 2026

Hi, @reaperhulk and I discussed and we'd like to make the following changes to the API:

  1. The enums for each KEM/AEAD/KDF shouldn't be the RFC value, they should just be opaque.
  2. We want to start with a single-shot API only, so suite.encrypt() + suite.decrypt() with no multi-shot. The result will be enc || ct like Go's single-shot APIs.

This should hopefully make the PR much simpler/shorter to get started.

Updated to single-shot API per feedback:

  • suite.encrypt() / suite.decrypt() instead of sender/recipient contexts
  • Returns enc || ciphertext like Go's API
  • Opaque enum values
  • Removed multi-message support for simplicity

- Remove docstrings (prose docs only)
- Use load_vectors_from_file helper
- Use [] instead of .get() for vector keys
- Parameterize roundtrip tests by KEM/KDF/AEAD
- Merge error test class into main test class
- Use subtest fixture instead of parametrize for vectors
- Remove test_load_vectors_missing_file test
- Load vectors inside test function, not at module level
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add HPKE support

2 participants