Skip to content

Tang Hybrid PQC Recovery implementation - #165

Open
sarroutbi wants to merge 12 commits into
latchset:masterfrom
sarroutbi:tang-kem-pub
Open

sarroutbi wants to merge 12 commits into
latchset:masterfrom
sarroutbi:tang-kem-pub

Conversation

@sarroutbi

@sarroutbi sarroutbi commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

For PQC to work appropriately, library requires usage of updated JOSE library. Changes (unmerged) available at:
latchset/jose#184
In case specific JOSE functions and algorithms are not available, Tang will work as previously, not including the files that are PQC specific

Add hybrid post-quantum cryptography (ML-KEM-768) support to Tang via a bidirectional KEM envelope protocol, maintaining full backward compatibility with existing Clevis clients.

Tang generates an ML-KEM-768 keypair alongside its existing EC keys. Two new endpoints handle the PQC key lifecycle:

GET /adv-kem — Serves a signed JWS containing only KEM (kty=AKP) keys, separate from /adv so legacy Clevis clients never encounter key types they can't handle.
POST /rec-kem/:thp — Bidirectional KEM recovery endpoint implementing a forward+return channel envelope:
a. Client encapsulates to Tang's KEM public key (forward channel), encrypts an inner payload under that shared secret
b. Tang decapsulates, verifies ek_digest (SHA-256 thumbprint of the enc_key) to prevent oracle attacks, then encapsulates back to the client's ephemeral KEM public key (return channel)
c. Tang encrypts enc_key under the return-channel shared secret and returns it — enc_key never appears in plaintext in the response

Architecture

PQC code is cleanly isolated from the core server:

  • src/pqc.c / src/pqc.h — All KEM handlers (adv_kem, rec_kem), JWE compact serialization, secure memory zeroing. Only compiled when jose supports the KEM API.
  • src/pqc_noop.c — Stub implementations (404 responses) compiled when jose lacks KEM support.
  • No #ifdef in core files — keys.h, keys.c, and tangd.c have zero PQC conditional compilation. A meson compiler.links() probe detects jose KEM API support at build time, and src/meson.build selects pqc.c or pqc_noop.c accordingly.

Other changes

GET /version — New endpoint reporting server capabilities (tang_pub, hybrid_recovery) and version.
tangd-keygen — Generates ML-KEM-768 keys when jose supports it (soft-fails otherwise).

Test coverage

  • /adv-kem tests: verifies only AKP keys are returned, JWS is signed by the signing key, /adv excludes AKP keys
  • /rec-kem tests: full 10-step bidirectional envelope round-trip, plus error cases (bad JSON, wrong key type, non-existent thumbprint, invalid ek_digest)
  • Unit tests: AKP key filtering into m_kem_payload, m_has_pqc detection, jwk_sign edge cases, cleanup NULL safety
  • All tests gracefully skip on jose without ML-KEM support

sarroutbi and others added 9 commits September 22, 2026 14:04
Embed the server's ECMR public key as a `tang_pub` field in the
recovery response so that clients can obtain it at recovery time
instead of persisting it on disk. The public key is a deep copy
with private material stripped via jose_jwk_pub(), ensuring no
secret data leaks.

The field is additive: old clients ignore the unknown JSON field,
so existing Clevis bindings and recovery flows are unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Embed the server's ECMR public key as a `tang_pub` field in the
POST /rec/$kid response so that clients can obtain it at recovery
time instead of persisting it on disk. Private material is stripped
via jose_jwk_pub() before inclusion.

Add a Tang-Capabilities: tang_pub header to the GET /adv response
so clients can detect server support before creating a binding.

Both changes are additive: old clients ignore the unknown JSON
field and HTTP header, so existing bindings and recovery flows
are unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Replace the Tang-Capabilities HTTP header with a dedicated
GET /version endpoint that returns structured JSON describing
server capabilities.

The response format:
  {"features":{"tang_pub":true},"tang_version":"15"}

Clients calling /version on an old Tang server receive a 404,
which cleanly signals the server lacks capability discovery and
the client should fall back to legacy binding behavior.

The /version endpoint respects the -e (endpoint) option, so
/endpoint/version works consistently with /endpoint/adv and
/endpoint/rec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Extend Tang with hybrid post-quantum cryptography capabilities:

ML-KEM-768 key generation:
- Generate an ML-KEM-768 (kty: AKP) key pair alongside the existing
  ES512 and ECMR keys, both in the C daemon (create_new_keys) and
  in the tangd-keygen shell script.
- Key generation is optional: if jose does not support ML-KEM, the
  server logs a warning and continues operating with classical keys
  only. This preserves backward compatibility with older jose builds.

PQC state tracking:
- Add m_has_pqc field to tang_keys_info, set during key loading when
  an AKP-type key is found among regular (non-rotated) keys.
- Expose PQC availability via the /version endpoint as the
  "hybrid_recovery" feature flag, allowing clients to detect server
  support before creating hybrid bindings.

KEM decapsulation endpoint (POST /rec-kem/<thumbprint>):
- Accept a JSON body with a "ct" field containing base64url-encoded
  ML-KEM ciphertext.
- Look up the server ML-KEM private key by thumbprint, validate it
  is kty: AKP with deriveKey permission, and call jose_jwk_kem_dec
  to perform decapsulation.
- Return the shared secret as an oct JWK, enabling the client to
  combine it with the ECDH shared secret for hybrid key derivation.
- Support custom --endpoint prefix, consistent with existing routes.

Test adjustment:
- Relax test_create_new_keys assertion to m_keys_count >= 2, since
  ML-KEM generation may produce a third key when jose supports it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Separate concerns between /rec (ECMR exchange) and /rec-kem (KEM
recovery). The legacy plaintext path that returned shared secrets
directly is removed — all KEM recovery now uses a bidirectional
envelope where enc_kem_key is always encrypted in transit.

Removed rec_kem_legacy() and the detection branch in rec_kem().
Removed all ECMR logic from rec_kem_secure() (EC key lookup,
jose_jwk_exc exchange, transport_key/tang_ec_pub in response).

Added JWE compact serialization helpers (jwe_from_compact,
jwe_to_compact) and secure memory cleansing (secure_zero,
str_cleanse) for intermediate secrets.

Updated tests to exercise the full bidirectional envelope protocol:
forward-channel encapsulation, encrypted inner payload, return-channel
decapsulation, and shared secret verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
The client now includes an ek_digest (SHA-256 JWK thumbprint of
the stored shared secret) in the encrypted inner payload. Tang
verifies this digest against the decapsulated enc_kem_key before
proceeding with the return channel. If verification fails, Tang
returns 400 without encrypting or returning anything.

This prevents an attacker who intercepts clevis_transport_ct from
replaying a recomputed clevis_encrypted_blob to have Tang act as
a decapsulation oracle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Rename C variables and JSON wire-format fields to match the
hybrid KEM protocol specification consistently:

  C variables:
    kem_jwk         → tang_kem_priv
    transport_ct_str → clevis_transport_ct
    fwd_ss          → clevis_transport_key
    encrypted_blob  → clevis_encrypted_blob
    kem_ct          → clevis_kem_ct
    enc_kem_key     → enc_key
    ret_ss          → tang_transport_key
    tang_ct         → tang_transport_ct
    jwe_compact     → tang_encrypted_key
    ek_json         → enc_key_json

  JSON fields (request):
    encrypted_blob  → clevis_encrypted_blob
    transport_ct    → clevis_transport_ct

  JSON fields (inner payload):
    kem_ct          → clevis_kem_ct

  JSON fields (response):
    encrypted_key   → tang_encrypted_key
    transport_ct    → tang_transport_ct

The ambiguous "transport_ct" field that appeared in both request
and response with different meanings is now disambiguated with
clevis_/tang_ prefixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.01695% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.56%. Comparing base (83de399) to head (a66b75a).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
src/tangd.c 68.75% 4 Missing and 6 partials ⚠️
src/keys.c 66.66% 2 Missing and 5 partials ⚠️
src/pqc_noop.c 0.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #165      +/-   ##
==========================================
+ Coverage   58.44%   62.56%   +4.11%     
==========================================
  Files           4        5       +1     
  Lines         580      625      +45     
  Branches      147      156       +9     
==========================================
+ Hits          339      391      +52     
+ Misses        140      129      -11     
- Partials      101      105       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Split all post-quantum cryptography code (adv_kem, rec_kem, KEM helpers)
from tangd.c into pqc.c/pqc.h, with pqc_noop.c providing stubs when jose
lacks KEM API support. This removes every #ifdef HAVE_PQC from keys.h,
keys.c, and tangd.c — the meson build conditionally compiles the real or
noop implementation based on the existing jose KEM probe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Use a meson probe to detect explicit_bzero at configure time, falling
back to memset_s (C11 Annex K) then memset with a compiler barrier.
This avoids the volatile-loop pattern and leverages OS-provided secure
wiping when available. Also sets *str = NULL after freeing in
str_cleanse to prevent use-after-free.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
@sarroutbi sarroutbi changed the title [DONTMERGE] Tang Hybrid PQC Recovery implementation Tang Hybrid PQC Recovery implementation Oct 7, 2026
@sarroutbi
sarroutbi marked this pull request as ready for review October 7, 2026 10:42
jansson does not zero heap memory when freeing json_t string values.
The "k" field of oct JWK objects containing KEM shared secrets
(clevis_transport_key, enc_key, tang_transport_key) remained in freed
heap pages after json_decref, recoverable via heap grooming, core
dumps, or /proc/pid/mem reads.

Add wipe_jwk_secret() to SECURE_WIPE the "k" field in-place before
jansson frees the underlying buffer. Apply it to all three shared
secrets in rec_kem_secure on the success path and to
clevis_transport_key before its explicit json_decref.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
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.

2 participants