Repository navigation
feat(sdk): support ML-KEM rewrap session keys #388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dmihalcik-virtru
wants to merge
6
commits into
main
Choose a base branch
from
DSPX-4221-pq-sessions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f7b394f
feat(sdk): support ML-KEM rewrap session keys
dmihalcik-virtru 0a6f852
fix(cmdline): expect session-key-mlkem only when an ML-KEM provider i…
dmihalcik-virtru 11a0d2d
fix(sdk): publish the RSA session keypair through one reference
dmihalcik-virtru a5e0a40
fix(sdk): give KemProvider.generateKeyPair a default implementation
dmihalcik-virtru 6fab500
chore(tests): add fail-closed tests for generated KEM session keypairs
dmihalcik-virtru 2454b82
refactor(sdk): keep KEM session private keys inside the provider
dmihalcik-virtru File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
55 changes: 55 additions & 0 deletions
55
sdk-pqc-bc/src/main/java/io/opentdf/platform/sdk/pqc/bc/BouncyCastleKemSessionKey.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package io.opentdf.platform.sdk.pqc.bc; | ||
|
|
||
| import io.opentdf.platform.sdk.spi.KemProvider; | ||
|
|
||
| import java.util.Arrays; | ||
|
|
||
| /** | ||
| * {@link KemProvider.KemSessionKey} returned by {@link BouncyCastleKemProvider}. | ||
| * Owns the raw private key bytes (the same encoding the algorithm classes' | ||
| * {@code unwrapDEK(byte[] rawPriv, ...)} take) and zeroes them on {@link #close()}. | ||
| * The private key is never PEM-encoded or turned into a {@code String}. | ||
| * | ||
| * <p>BouncyCastle's key parameter objects, built inside each unwrap, keep | ||
| * their own copies that cannot be wiped; they are unreachable once the | ||
| * unwrap returns. | ||
| */ | ||
| final class BouncyCastleKemSessionKey implements KemProvider.KemSessionKey { | ||
|
|
||
| /** One algorithm's raw-key unwrap, e.g. {@code MLKEMAlgorithm.MLKEM_768::unwrapDEK}. */ | ||
| @FunctionalInterface | ||
| interface RawUnwrap { | ||
| byte[] unwrapDEK(byte[] rawPrivateKey, byte[] wrapped); | ||
| } | ||
|
|
||
| private final String publicKeyPEM; | ||
| private final byte[] privateKey; | ||
| private final RawUnwrap unwrap; | ||
| private boolean closed; | ||
|
|
||
| /** Takes ownership of {@code privateKey}: the caller must not keep or reuse the array. */ | ||
| BouncyCastleKemSessionKey(String publicKeyPEM, byte[] privateKey, RawUnwrap unwrap) { | ||
| this.publicKeyPEM = publicKeyPEM; | ||
| this.privateKey = privateKey; | ||
| this.unwrap = unwrap; | ||
| } | ||
|
|
||
| @Override | ||
| public String publicKeyPEM() { | ||
| return publicKeyPEM; | ||
| } | ||
|
|
||
| @Override | ||
| public synchronized byte[] unwrapDEK(byte[] wrapped) { | ||
| if (closed) { | ||
| throw new IllegalStateException("KEM session key has been closed"); | ||
| } | ||
| return unwrap.unwrapDEK(privateKey, wrapped); | ||
| } | ||
|
|
||
| @Override | ||
| public synchronized void close() { | ||
| Arrays.fill(privateKey, (byte) 0); | ||
| closed = true; | ||
| } | ||
| } |
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
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
116 changes: 116 additions & 0 deletions
116
sdk-pqc-bc/src/test/java/io/opentdf/platform/sdk/pqc/bc/HybridCryptoSessionKeyTest.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| package io.opentdf.platform.sdk.pqc.bc; | ||
|
|
||
| import io.opentdf.platform.sdk.KeyType; | ||
| import io.opentdf.platform.sdk.SDKException; | ||
| import io.opentdf.platform.sdk.spi.KemProvider; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.MethodSource; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Arrays; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertArrayEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| /** | ||
| * Unit tests for {@link HybridCrypto#generateSessionKey}, the dispatcher backing | ||
| * {@link BouncyCastleKemProvider#generateSessionKey} and (in the core {@code sdk} | ||
| * module) {@code KASClient}'s KEM branch for the rewrap client "session key". | ||
| * | ||
| * <p>Each variant is exercised end-to-end the way KAS and KASClient use it: | ||
| * generate a session key, wrap a DEK against its public PEM, unwrap through | ||
| * the session key, assert equal. The negative cases check that the session | ||
| * key and envelope fail closed: unwrapping must throw rather than return a DEK. | ||
| */ | ||
| class HybridCryptoSessionKeyTest { | ||
|
|
||
| private static final byte[] DEK = "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8); | ||
|
|
||
| /** Every hybrid and pure ML-KEM key type, so a newly added PQC type can't skip these tests. */ | ||
| static Stream<KeyType> pqcKeyTypes() { | ||
| return Arrays.stream(KeyType.values()).filter(kt -> kt.isHybrid() || kt.isMLKEM()); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("pqcKeyTypes") | ||
| void generatedSessionKeyRoundTrips(KeyType keyType) { | ||
| try (KemProvider.KemSessionKey sessionKey = HybridCrypto.generateSessionKey(keyType)) { | ||
| assertTrue(sessionKey.publicKeyPEM().startsWith("-----BEGIN PUBLIC KEY-----"), "public PEM header"); | ||
|
|
||
| byte[] wrapped = HybridCrypto.wrapDEK(keyType, sessionKey.publicKeyPEM(), DEK); | ||
| assertArrayEquals(DEK, sessionKey.unwrapDEK(wrapped), "DEK round-trip via generated session key"); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("pqcKeyTypes") | ||
| void otherSessionKeyOfSameTypeCannotUnwrap(KeyType keyType) { | ||
| try (KemProvider.KemSessionKey recipient = HybridCrypto.generateSessionKey(keyType); | ||
| KemProvider.KemSessionKey other = HybridCrypto.generateSessionKey(keyType)) { | ||
| byte[] wrapped = HybridCrypto.wrapDEK(keyType, recipient.publicKeyPEM(), DEK); | ||
| assertThrows(SDKException.class, () -> other.unwrapDEK(wrapped), | ||
| "a different session key of the same type must not unwrap the DEK"); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("pqcKeyTypes") | ||
| void tamperedKemCiphertextCannotUnwrap(KeyType keyType) { | ||
| try (KemProvider.KemSessionKey sessionKey = HybridCrypto.generateSessionKey(keyType)) { | ||
| byte[][] parts = HybridCrypto.unmarshalEnvelope( | ||
| HybridCrypto.wrapDEK(keyType, sessionKey.publicKeyPEM(), DEK)); | ||
|
|
||
| // The midpoint of the KEM ciphertext is inside its ML-KEM part for every type | ||
| // (the EC / X25519 share, where present, is appended after it). Decapsulation | ||
| // implicitly rejects and yields a wrong wrap key, so AES-GCM must fail. | ||
| parts[0][parts[0].length / 2] ^= 0x01; | ||
| byte[] tampered = HybridCrypto.marshalEnvelope(parts[0], parts[1]); | ||
|
|
||
| assertThrows(SDKException.class, () -> sessionKey.unwrapDEK(tampered), | ||
| "a flipped byte in the KEM ciphertext must not unwrap the DEK"); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("pqcKeyTypes") | ||
| void tamperedEncryptedDekCannotUnwrap(KeyType keyType) { | ||
| try (KemProvider.KemSessionKey sessionKey = HybridCrypto.generateSessionKey(keyType)) { | ||
| byte[][] parts = HybridCrypto.unmarshalEnvelope( | ||
| HybridCrypto.wrapDEK(keyType, sessionKey.publicKeyPEM(), DEK)); | ||
|
|
||
| parts[1][parts[1].length / 2] ^= 0x01; | ||
| byte[] tampered = HybridCrypto.marshalEnvelope(parts[0], parts[1]); | ||
|
|
||
| assertThrows(SDKException.class, () -> sessionKey.unwrapDEK(tampered), | ||
| "a flipped byte in the AES-GCM sealed DEK must not unwrap"); | ||
| } | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("pqcKeyTypes") | ||
| void closedSessionKeyCannotUnwrap(KeyType keyType) { | ||
| KemProvider.KemSessionKey sessionKey = HybridCrypto.generateSessionKey(keyType); | ||
| byte[] wrapped = HybridCrypto.wrapDEK(keyType, sessionKey.publicKeyPEM(), DEK); | ||
|
|
||
| sessionKey.close(); | ||
|
|
||
| assertThrows(IllegalStateException.class, () -> sessionKey.unwrapDEK(wrapped), | ||
| "a closed session key must not unwrap"); | ||
| } | ||
|
|
||
| @Test | ||
| void closeZeroesThePrivateKey() { | ||
| byte[] privateKey = {1, 2, 3, 4}; | ||
| BouncyCastleKemSessionKey sessionKey = | ||
| new BouncyCastleKemSessionKey("unused", privateKey, (priv, wrapped) -> priv.clone()); | ||
| assertArrayEquals(new byte[] {1, 2, 3, 4}, sessionKey.unwrapDEK(new byte[0]), | ||
| "unwrap is handed the private key"); | ||
|
|
||
| sessionKey.close(); | ||
|
|
||
| assertArrayEquals(new byte[4], privateKey, "close must zero the private key"); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.