Skip to content

[DRAFT][DONTMERGE] Add a new PQC Tang pin - #583

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

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

Conversation

@sarroutbi

Copy link
Copy Markdown
Collaborator

This PR adds a new tang-pqc pin implementing hybrid ECC+ML-KEM-768 post-quantum encryption for Tang-based NBDE (Network-Bound Disk Encryption). This protects LUKS bindings against future quantum attacks while retaining classical ECMR security guarantees through a dual key exchange design.

  • clevis-encrypt-tang-pqc / clevis-decrypt-tang-pqc: Standalone encrypt/decrypt commands implementing the hybrid protocol
  • Core modules: jose_wrapper (jose C API bindings), tang_client (HTTP client for tang endpoints), key_deriver (HKDF-SHA256 key combination), ecmr_ops (EC key blinding/unblinding)
  • Protocol flow:
    • Encrypt: Performs classical ECMR key exchange via /rec/{kid}, then ML-KEM-768 encapsulation against the server's KEM public key from /adv-kem. Both shared secrets are combined via HKDF-SHA256 (NBDE-HYBRID-v1 label) to derive the final encryption key
    • Decrypt: Recovers the classical ECMR secret via /rec/{kid}, then uses a bidirectional KEM-secured transport channel (/rec-kem/{kem_kid}) to recover the KEM shared secret. Both are recombined to reconstruct the encryption key
  • Secure transport layer: The /rec-kem recovery channel uses ML-KEM encapsulation in both directions (client-to-server and server-to-client) to protect the KEM key material in transit
  • Offline binding: Supports providing both adv and adv_kem as file paths or inline JSON for air-gapped provisioning

LUKS infrastructure integration

  • clevis-luks-common-functions: Adds tang-pqc to SSS pin listing, config printing, and regen support
  • clevis-luks-report: Handles tang-pqc pin for key rotation detection; supports tang_pub mode (no stored adv) by checking the kid thumbprint against the server's current advertisement
  • clevis-luks-bind: Finds tang-pqc pin executables in the correct path
  • Dracut module (clevis-pin-tang-pqc): Includes tang-pqc binaries and shared libraries in the initramfs for early-boot unlocking
  • Initramfs-tools hook: Adds tang-pqc binaries to initramfs on Debian-based systems

Build system

  • New meson option tang-pqc (default: auto) to control pin compilation
  • Dependencies: jose (with ML-KEM KEM API), libcrypto (OpenSSL 3.5+), libcurl, jansson
  • Compile-time cc.links() check for jose_jwk_kem_enc to gate build on jose ML-KEM API availability
  • C++ unit tests for jose_wrapper, key_deriver, and ecmr_ops modules

Test suite

  • C++ unit tests: test-tang-pqc-jose-wrapper, test-tang-pqc-key-deriver, test-tang-pqc-ecmr-ops
  • Integration tests: pin-tang-pqc (basic round-trip), pin-tang-pqc-version (/version and /adv-kem endpoint validation), pin-tang-pqc-offline (offline binding with file/inline adv), pin-tang-pqc-sss (SSS threshold policies with tang-pqc), pin-tang-pqc-errors (error handling)
  • Existing test fix: bind-luks1-avoid-luksmeta-corruption adapted to force adv storage (bypasses tang_pub optimization)

Documentation

  • README.md updated with tang-pqc pin usage, dependencies table, and tang server requirements

sarroutbi and others added 17 commits September 24, 2026 13:35
At provisioning time (clevis encrypt tang), call GET /version on the
Tang server. When features.tang_pub is true, create a new-format
binding that stores only the Tang URL and key identifier — the full
advertisement is not persisted in the JWE header. When /version
returns 404 or tang_pub is absent, fall back to current behavior.

At recovery time (clevis decrypt tang), check the binding format:
new-format bindings extract tang_pub from the POST /rec/$kid response
for ECMR recovery; legacy bindings use the stored advertisement
unchanged. If a new-format recovery fails because tang_pub is missing
from the server response, fail clearly without silent fallback.

clevis luks regen automatically migrates eligible legacy bindings to
the new format by re-querying /version during re-encryption.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Introduce hybrid PQC support to the Tang pin. When a Tang server
advertises an ML-KEM-768 key (kty: AKP) and reports hybrid_recovery
support via /version, clevis-encrypt-tang performs dual key agreement:
ECDH with the ECMR key and KEM encapsulation with the ML-KEM key.
Both shared secrets are combined through HKDF-SHA-256 (RFC 5869)
with domain separation (label NBDE-HYBRID-v1, version, suite, and
both key thumbprints bound into the info parameter) to derive the
content encryption key.

All hybrid cryptographic operations (ECDH, KEM encapsulation, HKDF
derivation) are performed inline using jose CLI and openssl kdf
commands. On the decrypt side, ECMR recovery via /rec is shared
with the classical path, followed by KEM recovery via /rec-kem
and HKDF derivation to reconstruct the content encryption key.

The JWE uses alg:dir with A256GCM and stores the client EC public key
as epk, the KEM ciphertext as clevis.tang.kem_ct, and the KEM key
thumbprint as kem_kid in the protected header. Both encrypt and
decrypt paths gracefully fall back to classical ECDH-ES when the
server lacks PQC support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Protect enc_kem_key during hybrid recovery by implementing the
bidirectional ML-KEM envelope protocol specified for the unsecure
channel, replacing the plaintext KEM shared secret exchange.

Encrypt side (clevis-encrypt-tang):
- Compute ek_digest = SHA_256(enc_kem_key) at bind time for anti-oracle
  verification on the Tang side
- Store tang_kem_pub and ek_digest in the JWE header
- Rename hybrid_cek to enc_key to match specification terminology

Decrypt side (clevis-decrypt-tang):
- Separate recovery into /rec (ECMR, inherently blinding-protected) and
  /rec-kem (KEM, bidirectional envelope-protected) as two independent
  calls
- Generate ephemeral KEM keypair (clevis_kem_priv, clevis_kem_pub)
- Establish forward transport channel via Encapsulate(tang_kem_pub)
- Encrypt {kem_ct, clevis_kem_pub, ek_digest} under forward key
- POST encrypted envelope to /rec-kem
- Decapsulate return channel and decrypt enc_kem_key from Tang response
- Derive enc_key = PRF(enc_ec_key, enc_kem_key) via HKDF
- Discard all ephemeral and transport material after use
- Extract helpers: derive_enc_key(), ecmr_unblind(), validate_tang_ec_pub(),
  ecmr_recover()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Rename variables and JSON field names to match the secure transport
protocol specification consistently:

- kem_ct → clevis_kem_ct (JWE header and inner payload field)
- kem_ss_k/kem_ss_jwk → enc_key_k/enc_key_jwk (shared secret from KEM)
- clevis_kem_key → clevis_kem_priv (ephemeral KEM private key)
- kem_k → enc_key_k (recovered KEM shared secret)
- enc_kem_key_json → enc_key_json (decrypted key from Tang)
- encrypted_blob → clevis_encrypted_blob (request field)
- transport_ct → clevis_transport_ct (request field)
- encrypted_key → tang_encrypted_key (response field)
- transport_ct → tang_transport_ct (response field)

Fix ek_digest computation to use JWK Thumbprint (jose jwk thp -a S256)
matching Tang's jose_jwk_thp_buf(), instead of raw SHA-256 of key bytes.

Fix ek_digest being unset before it was stored in the JWE header.

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>
Add a new C++ pin (tang-pqc) implementing hybrid post-quantum
cryptography combining classical ECMR with ML-KEM-768 (FIPS 203).

The pin provides clevis-encrypt-tang-pqc and clevis-decrypt-tang-pqc
binaries with the following architecture:

- jose_types.h: RAII wrappers (JsonPtr, CStringPtr, BufferPtr) for
  jose/jansson resources using C++ smart pointers
- jose_wrapper: Type-safe C++ interface over jose C API (key
  generation, ECDH, KEM encapsulation, JWE, JWS, base64url)
- key_deriver: HKDF-SHA256 key derivation with domain separation
  (label "NBDE-HYBRID-v1", suite "ECMR+ML-KEM-768")
- tang_client: HTTP client using libcurl with TLS/mTLS support,
  handles /adv, /adv-kem, /rec, /rec-kem endpoints
- ecmr_ops: ECMR blinding and unblinding operations using jose's
  three ECMR modes (scalar mult, point addition, point subtraction)
- encrypt_main: Full hybrid encryption flow producing JWE with
  alg:dir and pin:tang-pqc, with JWS verification of both /adv
  and /adv-kem responses
- decrypt_main: Full hybrid decryption with secure KEM transport
  channel for recovery

Unit tests cover all crypto components without requiring a Tang
server: jose_wrapper (key gen, ECDH, KEM round-trip, JWE, base64,
thumbprints), key_deriver (determinism, domain separation, real-key
integration), and ecmr_ops (blind/unblind recovery matches ECDH
across P-256/P-384/P-521, ephemeral independence).

The pin is gated behind a meson feature option (tang-pqc, default
auto) so it is only built when dependencies (libcrypto, libcurl,
jose, jansson) are available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Move all hybrid ECC+KEM post-quantum encryption logic exclusively to
the tang-pqc pin, keeping the classical tang scripts focused on
standard ECDH-ES. The tang_pub feature support is retained, allowing
the server to provide its exchange key in the /rec response instead
of storing the advertisement in the JWE header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Read interactive trust confirmation from /dev/tty instead of stdin,
matching the classical tang pin behavior. When clevis-luks-bind pipes
the LUKS key through stdin, the prompt was consuming key data instead
of reading user input from the terminal.

Also add tang-pqc pin documentation to README.md covering usage,
build dependencies (jose with KEM, OpenSSL 3.5+), meson options,
and Tang server requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Add dracut module clevis-pin-tang-pqc to include the hybrid PQC
binaries in the initramfs for early boot LUKS unlocking. The module
depends on clevis and network, sets rd.neednet=1 when tang-pqc
bindings are detected, and installs clevis-encrypt-tang-pqc,
clevis-decrypt-tang-pqc, curl and openssl.

Also add conditional copy of tang-pqc binaries to the initramfs-tools
hook for Debian-based systems.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Replace extracted functions (ecmr_unblind, validate_tang_ec_pub,
ecmr_recover) with inline code matching the original tang_pub patch.
No functional change — same tang_pub support, simpler structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
In the end, it has been decided to move
all the PQC Hybrid logic to a brand new
pin that will act as the main entrypoint
for Hybrid PQC encryption/decryption.
This change removes previous leftover when
the Hybrid PQC was implemented inside clevis-encrypt-tang
and clevis-decrypt-tang

Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Add tang-pqc case to clevis_luks_process_sss_pin() so that
clevis luks list and clevis luks regen correctly handle SSS
bindings that include tang-pqc as a child pin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
When the tang server supports tang_pub, clevis-encrypt-tang skips
storing adv in the JWE header. This broke clevis-luks-report (which
expected adv to always be present) and the luksmeta corruption test
(which relied on a large adv causing bind failure).

- clevis-luks-report: return success when adv is absent from metadata
  instead of failing, since rotation detection is not possible without
  stored keys but decryption still works via tang_pub
- clevis-luks-report: add tang-pqc to the pin case statement
- bind-luks1-avoid-luksmeta-corruption: provide adv as a file to force
  it into the JWE header, preserving the original test intent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Allow offline binding by providing both adv and adv_kem together in the
configuration. When both are present, the encrypt skips fetching from
the network and assumes tang_pub is enabled. Providing only one of
adv/adv_kem without the other is rejected with a clear error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Add shell-based integration tests for the tang-pqc pin, following the
same patterns as the existing tang tests:

- pin-tang-pqc: basic encrypt/decrypt round-trip, JWE header validation,
  server-down failure
- pin-tang-pqc-version: /version endpoint validation (tang_pub,
  hybrid_recovery), /adv-kem endpoint, AKP key presence
- pin-tang-pqc-offline: offline binding with file paths and inline JSON,
  validates that providing adv-only or adv_kem-only fails
- pin-tang-pqc-sss: SSS combination with two tang-pqc pins, mixed
  tang-pqc + classic tang, one-server-down resilience
- pin-tang-pqc-errors: missing url, unreachable server, wrong
  thumbprint, tampered JWE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
When adv is not stored in the JWE header (tang_pub mode), the report
now extracts the kid from the protected header and checks if it is
still present in the server's current advertisement. If the kid is
missing from the current adv, the key was rotated and the report
flags it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Comment thread src/luks/dracut/clevis-pin-tang-pqc/module-setup.sh.in Fixed
Comment thread src/luks/dracut/clevis-pin-tang-pqc/module-setup.sh.in Fixed
@sarroutbi
sarroutbi force-pushed the tang-kem-pub branch 2 times, most recently from edfe9d1 to c11ca47 Compare October 6, 2026 08:41
Add a cc.links() check for jose_jwk_kem_enc to detect whether the
installed jose library supports ML-KEM operations. When the API is
absent, the tang-pqc pin is disabled with a clear warning instead of
failing at compile time.

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>
sarroutbi and others added 2 commits October 6, 2026 11:23
Remove C++ from the project() declaration so it is no longer
unconditionally required. The C++ compiler is only added via
add_languages() when jose has ML-KEM KEM API support, since the
tang-pqc pin is the sole consumer of C++ in the project. This
avoids requiring a C++ toolchain on systems where the pin will
not be compiled.

The KEM API link check is moved from tang-pqc/meson.build to the
root meson.build so that add_project_arguments() for C++ can be
called before any build targets are defined.

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>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
sarroutbi and others added 3 commits October 7, 2026 12:56
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Address six security findings from static review of the tang-pqc pin:

- Add cleanseJsonSecrets() to zero sensitive strings inside jansson
  objects (private keys, shared secrets, derived CEKs) before they
  are freed by json_decref, preventing heap residue recovery via
  cold-boot or memory forensics attacks.

- Replace string concatenation with json_pack() for inner payload
  construction in secureTransportRecover(), eliminating a JSON
  injection vector through unverified JWE header values parsed
  before AEAD authentication.

- Add CURLOPT_CONNECTTIMEOUT (10s) and CURLOPT_TIMEOUT (30s) to
  prevent indefinite hangs during early-boot NBDE unlock when the
  Tang server is unreachable or tarpitting.

- RAII-wrap curl_slist via SlistPtr to prevent memory leak on
  exception paths in httpPost(), and add try/catch in writeCallback
  to avoid undefined behavior from C++ exceptions propagating
  through libcurl C frames.

- Length-prefix kid and kemKid in HKDF info parameter to eliminate
  ambiguous concatenation that could allow cross-binding key
  collisions per NIST SP 800-56C. Wire-format change, safe pre-GA.

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