Repository navigation
Conversation
sarroutbi
force-pushed
the
tang-kem-pub
branch
6 times, most recently
from
October 9, 2026 13:53
0f3053c to
1e5c5fa
Compare
sarroutbi
force-pushed
the
tang-kem-pub
branch
4 times, most recently
from
October 19, 2026 11:05
f74069b to
109404d
Compare
sarroutbi
marked this pull request as ready for review
October 19, 2026 15:16
sarroutbi
force-pushed
the
tang-kem-pub
branch
from
October 20, 2026 09:57
2535dc5 to
ce2d737
Compare
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>
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>
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>
Add input validation to prevent three classes of attacks from crafted JWE headers stored in LUKS metadata: - JSON injection via EC curve parameter: replace string concatenation with json_pack() for key spec construction, preventing an attacker from injecting fields like "d" into the ephemeral key specification. - EC curve downgrade: whitelist crv to P-256, P-384, P-521, rejecting weak or unexpected curves. - URL path injection: validate kid, kem_kid, and thp contain only base64url characters before interpolating into HTTP request paths, preventing path traversal and query injection. 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>
curl is now restricted to HTTP and HTTPS only, blocking file://, gopher://, dict://, and other protocol-smuggling SSRF vectors. The fix uses CURL_AT_LEAST_VERSION to pick the modern API (7.85.0+) or the deprecated fallback for older distros Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Replace manual OPENSSL_cleanse/cleanseJsonSecrets calls with RAII types that cleanse automatically on destruction, ensuring sensitive data is zeroed even when exceptions cause early exit. Introduce CleansedString, CleansedVector and CleansedJsonPtr in jose_types.h — each wraps its std counterpart and calls OPENSSL_cleanse in the destructor. This follows the same RAII pattern already used for JsonPtr, CurlPtr, and SlistPtr. Apply the new types to all sensitive locals in secureTransportRecover, hybridEncrypt, deriveEncKey, and both main functions. Also replace the last string-concatenated JSON construction in deriveEncKey with json_pack for consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Set CURLOPT_MAXFILESIZE_LARGE to 1 MiB in the tang-pqc curl handle. Without this, a compromised or malicious Tang server can send an arbitrarily large response, causing unbounded memory allocation. In the dracut/initramfs boot context, this is a denial-of-service that prevents LUKS unlock. Tang responses (advertisements, recovery replies) are well under 100 KiB even with ML-KEM key material, so 1 MiB provides ample headroom. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Replace unbounded istream_iterator stdin reads in encrypt and decrypt with chunk-based reading (4 KiB chunks, 1 MiB max). An attacker controlling stdin could exhaust memory by piping arbitrarily large input to clevis-encrypt-tang-pqc or clevis-decrypt-tang-pqc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
CURLOPT_MAXFILESIZE_LARGE only checks the Content-Length header. A malicious Tang server using Transfer-Encoding: chunked bypasses that limit entirely, allowing unbounded memory growth. Add an application- layer size check inside writeCallback to enforce the 1 MiB cap regardless of transfer encoding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Implicit copies of CleansedString/CleansedVector produce plain std::string/std::vector objects whose destructors do not cleanse memory, leaving secrets on the heap. Delete copy constructor and copy assignment, keeping only move semantics so any accidental copy is caught at compile time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Replace unbounded istreambuf_iterator file reads for adv and adv_kem files with a bounded readBoundedFile helper (4 KiB chunks, 1 MiB max). A symlink to a large file or device node in the clevis config could otherwise exhaust memory during clevis-encrypt-tang-pqc. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Use std::array instead of C-style char[] stack buffers in read functions, json_string_length() instead of strlen() on jansson strings, std::string_view for JWE compact serialization, and constexpr char_traits::length() for compile-time string constants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Guard decrypted plaintext with RAII in jweDecrypt so sensitive data is always cleansed and freed, even on allocation failure. Prevent integer overflow in curl write callback and enforce the response size limit before appending data into memory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Check return values from json_object_set_new via a throwing helper to prevent silently malformed JWE on allocation failure. Validate curl_slist_append result before using the header list. Reject compact JWE input with more than 5 segments per RFC 7516. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
When binding offline (providing adv and adv_kem files instead of fetching from the server), the old code set hasTangPub = true. This caused the encrypt side to omit the advertisement from the JWE header, under the assumption that the Tang server would provide its public exchange key dynamically in the /rec/$kid response via a tang_pub field. In offline mode, we never contact the server, so we never call /version to check features.tang_pub. We have no idea whether the destination server supports that feature. Standard Tang servers don't include tang_pub in recovery responses — only newer servers that explicitly advertise it do. What happens at decrypt time (boot): 1. The decrypt side looks for tang_pub in the server's /rec response — not present on standard servers 2. It falls back to serverPubHint, which comes from the adv field stored in the JWE header 3. But adv was omitted during encrypt → serverPubHint is null 4. Both paths fail → "Unable to determine server exchange key" → permanent LUKS unlock failure at boot Without contacting the server, we must be conservative and assume tang_pub is NOT supported. This ensures the advertisement is always embedded in the JWE header as a fallback. If the server does happen to support tang_pub, it still works — the decrypt side checks tang_pub first and only falls back to the embedded advertisement if it's absent. So storing it is harmless extra data in the JWE, but omitting it is catastrophic. In online mode, the code still queries /version and sets hasTangPub = version.tangPub, so servers that support it can still benefit from the optimization of not embedding the advertisement. Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Always embed the advertisement in the JWE header when binding offline, since we cannot verify whether the destination server supports tang_pub. Without it, decrypt fails permanently at boot on servers that do not return tang_pub in recovery responses. Reject empty strings in validateBase64Url to prevent an untrusted JWE with empty kid/kem_kid from producing malformed HTTP paths (e.g. POST /rec/) that could bypass server-side URL routing. Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
The tang_pub fallback logic in clevis-luks-report (handling missing adv, kid-based rotation check) and the test adaptation in bind-luks1-avoid-luksmeta-corruption belong with the tang_pub feature in PR latchset#576, not in the tang-pqc branch. Restore both files to their master behavior, keeping only the tang-pqc case addition in clevis-luks-report which is needed for the new pin. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Introduce an optional 'alg' configuration property that allows selecting the KEM algorithm (ML-KEM-512, ML-KEM-768, ML-KEM-1024) instead of hardcoding ML-KEM-768. The algorithm flows through encrypt (config -> JWE header -> Tang query param), decrypt (JWE header -> key generation + derivation), and key derivation (dynamic HKDF suite string "ECMR+<alg>"). Validate the algorithm from untrusted JWE headers against an allowlist and use json_pack for key spec construction to prevent JSON injection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
Show the alg property alongside the URL when listing tang-pqc bindings, so users can see which ML-KEM variant was configured. Bindings without alg (pre-existing) display only the URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the tang-pqc pin configuration properties, algorithm selection, offline binding, and the hybrid cryptographic scheme. Wire the adoc source into the meson build so it is generated and installed alongside the other pin man pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
The alg property from the user's JSON config was accepted without validation. An invalid value would be appended raw to the URL query string (/adv-kem?alg=...) and stored in the JWE header. Validate alg against the ML-KEM allowlist in parseConfig, matching the validation already present on the decrypt side. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
When the JWE has no stored advertisement (tang_pub mode) and the /rec response does not include tang_pub, fetch /adv live and look up the server exchange key by kid thumbprint. This prevents permanent decryption failure caused by a forged /version at bind time, a mixed-version server pool, or a proxy stripping tang_pub. The kid in the protected header is integrity-protected by AES-GCM, so an attacker cannot redirect the lookup to a different key. Worst case is one extra HTTP request in the downgrade scenario. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
The rebase onto master dropped the kid-based fallback from report_tang, causing clevis luks report to fail with "Keys from clevis metadata not found" for any tang or tang-pqc binding created in tang_pub mode (no stored adv). Restore the header parameter and the fallback that checks the kid thumbprint against the server's live advertisement when adv is absent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
sarroutbi
force-pushed
the
tang-kem-pub
branch
from
October 29, 2026 10:10
c33b1f8 to
e5e7406
Compare
Provide the advertisement from a file so it is always stored in the JWE. This keeps the oversized-adv corruption scenario testable regardless of whether the Tang server supports tang_pub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a new tang-pqc pin implementing hybrid ECC+ML-KEM 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.
KEM Algorithm Selection (alg config property)
The tang-pqc pin now supports an optional alg configuration property that allows selecting which ML-KEM parameter set to use for hybrid encryption. If omitted, the default remains ML-KEM-768
(NIST security level 3), preserving full backward compatibility.
clevis luks bind -d /dev/sda1 tang-pqc '{"url":"http://tang.example.com"}'
clevis luks bind -d /dev/sda1 tang-pqc '{"url":"http://tang.example.com","alg":"ML-KEM-1024"}'
Supported values:
LUKS infrastructure integration
Build system
Test suite
Documentation