pqblocks-jwe
A compact five-part wire format for hybrid post-quantum/classical (ML-KEM-768 + X25519) JWE-style tokens, plus a serializer and a structural parser (it does not validate alg or enc values). It does not encrypt anything.
Use it when you already produce the AEAD ciphertext and the hybrid KEM ciphertext yourself and need one deterministic, structurally validated token to carry them. To encrypt, decrypt or sign, you need a separate JOSE implementation. The PQBlocks one, pqblocks-jose, is not yet public.
Standards used: base64url (RFC 4648 §5) and JSON (RFC 8259), with a layout modelled on JWE compact serialization (RFC 7516). The layout is not RFC 7516 compact serialization, and standard JOSE libraries cannot read it.
pip install git+https://github.com/PQBlocks/hybrid-jwe
from pqblocks_jwe import ALG_HYBRID, ENC_A256GCM, parse_jwe, serialize_jwe
ciphertext = bytes(48) # your AEAD output; this package does not encrypt
kem_ct = bytes(1088 + 32) # your ML-KEM-768 ciphertext || X25519 ephemeral public key
header = {"alg": ALG_HYBRID, "enc": ENC_A256GCM, "kid": "recipient-1"}
token = serialize_jwe(ciphertext, header, {"cty": "application/json"}, b"settlement-v1", kem_ct)
parsed = parse_jwe(token)
assert parsed["header"]["alg"] == ALG_HYBRID # parse_jwe does not check alg/enc for you
print(len(token), parsed["aad"], len(parsed["kem_ct"])) # 1697 b'settlement-v1' 1120
Why PQBlocks
- What this package is: a wire format and its parser/serializer. It does no key generation, KEM encapsulation, key derivation or AEAD. Everything you pass in as
payloadis written to the token as is. If you pass plaintext, the token carries plaintext. - When you need something else: whenever you need the cryptography itself, use a JOSE implementation that performs the KEM and AEAD. The PQBlocks one,
pqblocks-jose, is not yet public. Usepqblocks-jweonly for this five-part layout, for example to carry ciphertext produced by your own KEM and AEAD stack, or to inspect and validate tokens in that layout. - Tested: 16 tests in one test file, 100 % statement and branch coverage of a 53-statement module. That suite is small. See TESTING.md for what it does not cover.
- Honest benchmarks: the comparison with jwcrypto is published, including the case where jwcrypto is faster, together with the reason the comparison is not like-for-like (BENCHMARKS.md).
- Licence: Apache-2.0.
Usage
Token layout. Five base64url stanzas (no padding) joined by .:
base64url(header) . base64url(protected) . base64url(payload) . base64url(aad) . base64url(kem_ct)
| # | Stanza | Encoding | Intended content |
|---|---|---|---|
| 1 | header |
canonical JSON object (sort_keys=True, separators , :) |
e.g. {"alg": "MLKEM768-X25519", "enc": "A256GCM", "kid": ...} |
| 2 | protected |
canonical JSON object | e.g. {"cty": "application/json"} |
| 3 | payload |
raw bytes | ciphertext you produced |
| 4 | aad |
raw bytes | additional authenticated data you bound into your AEAD |
| 5 | kem_ct |
raw bytes | hybrid KEM ciphertext (ML-KEM-768 ciphertext is 1088 bytes, FIPS 203 Table 3) |
Determinism. The same inputs always give the same bytes, and the key order of header / protected does not change the output.
There is no IV or tag stanza. Unlike RFC 7516, the layout has no slot for an AEAD nonce or authentication tag. If your AEAD needs them, you must carry them yourself, for example inside the ciphertext bytes, and agree that convention with the receiver.
Validate after parsing. parse_jwe checks structure only. It accepts any alg and enc value, including "none". Compare them with what you expect before using the token (examples/reject_hostile.py).
Empty fields. parse_jwe rejects empty stanzas, but serialize_jwe does not reject empty payload, aad or kem_ct. A token serialized with an empty field therefore cannot be parsed back. Do not pass empty fields.
API reference
serialize_jwe(payload: bytes, header: dict, protected: dict, aad: bytes, kem_ct: bytes, recipient_key: bytes | None = None) -> bytes
- Returns the ASCII token.
- Raises
InvalidJWEErrorwhenpayload,aadorkem_ctis notbytes, whenheaderorprotectedis not adictor is not JSON-serializable, or whenrecipient_keyis notNoneand notbytes. recipient_keyis type-checked and then ignored. It is not written to the token and not used.
parse_jwe(token: bytes) -> dict
- Returns
{"header": dict, "protected": dict, "payload": bytes, "aad": bytes, "kem_ct": bytes}. - Raises
InvalidJWEErrorwhen the token is notbytes, not ASCII, does not have exactly 5 non-empty stanzas, a stanza is not decodable base64url, or the first two stanzas are not JSON objects.
InvalidJWEError: subclass of ValueError.
ALG_HYBRID = "MLKEM768-X25519" and ENC_A256GCM = "A256GCM": label constants for your header. Neither function uses them.
Limitations
- No cryptography. No KEM, KDF or AEAD, so no confidentiality and no integrity. The
alglabel is not enforced. - Not RFC 7516 compatible. RFC 7516 compact serialization is
protected.encrypted_key.iv.ciphertext.tag. This layout uses the same five-part shape but different contents, so jwcrypto, python-jose and other JOSE libraries cannot interpret these tokens, and this package cannot interpret theirs. - Lenient base64url decoding (from
jwcrypto.common.base64url_decode):=padding is accepted, characters outside the base64 alphabet are silently dropped, and non-canonical trailing bits are accepted. Several different byte strings can therefore parse to the same fields. Do not use raw token bytes as a replay-cache or deduplication key. - Parser edge cases: for duplicate JSON keys the last value wins;
NaN/Infinityare accepted and emitted; deeply nested JSON (about 100,000 levels in our probe) raisesRecursionErrorinstead ofInvalidJWEError; there is no token size limit. - Empty fields do not round-trip (see Usage).
- No official test vectors: there is no published standard for this exact layout, so there are none to run.
Security notes
This package protects nothing by itself. See SECURITY_NOTES.md for the full list, including the open review findings.
Verification
TESTING.md · BENCHMARKS.md · EVIDENCE.md · VERIFICATION_REPORT.md · USE_CASES.md
Part of PQBlocks
PQBlocks builds open, tested building blocks for the post-quantum transition. This package is the wire-format layer. The cryptography lives in a separate JOSE implementation; the PQBlocks one, pqblocks-jose, is not yet public.
Metadata
Release files for pqblocks-jwe 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pqblocks_jwe-0.1.0.tar.gz | 11.9 kB | Without provenance | Details for pqblocks_jwe-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pqblocks_jwe-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pqblocks_jwe-0.1.0-py3-none-any.whl |
Total release size: 22.8 kB
Release files / pqblocks_jwe-0.1.0.tar.gz
| Download URL | pqblocks_jwe-0.1.0.tar.gz |
|---|---|
| Size | 11.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6dc54acbf5b740a6064098c8d931aae94ede62f261cdc877e57bdd09580f34c4
|
|
BLAKE2b-256 checksum How to use checksums |
a5c577e50ae5c6f86c27e0071ec11eec8f81d8d5e03ed71931a1f463c886f0ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pqblocks_jwe-0.1.0-py3-none-any.whl
| Download URL | pqblocks_jwe-0.1.0-py3-none-any.whl |
|---|---|
| Size | 10.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
71add16c7c00ef3bb817a25603d149e68c7ead5563d123d017028ad6f8dafe10
|
|
BLAKE2b-256 checksum How to use checksums |
a63e6d18c66a74fa6b913fdf275e476ee75c71b7de7ab8885735b63ae0d18c44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for pqblocks-jwe 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.