chore: extract shared classic-leg wiring in HybridKexBuilder
Extract duplicated classic-leg construction from HybridKexBuilder.buildInitiator() and buildResponder() into a private buildClassicLeg() helper with JavaDoc. This keeps classic mode validation and context creation in one place, reduces asymmetry risk between initiator and responder paths, and preserves existing behavior. Closes #18 spent @30m
This commit is contained in:
@@ -232,6 +232,43 @@ public final class HybridKexBuilder {
|
|||||||
return new PqcKem(this);
|
return new PqcKem(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the configured classic agreement leg for the current builder state.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method validates the classic-leg inputs required by the selected
|
||||||
|
* {@link ClassicMode} and returns the resulting {@link AgreementContext} ready
|
||||||
|
* for inclusion into a {@link HybridKexContext}. For
|
||||||
|
* {@link ClassicMode#CLASSIC_AGREEMENT}, the returned context is also bound to
|
||||||
|
* the configured peer public key.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return classic agreement context derived from the configured classic-leg
|
||||||
|
* state
|
||||||
|
* @throws IOException if underlying context creation fails
|
||||||
|
* @throws IllegalStateException if the selected classic mode is missing
|
||||||
|
* required state
|
||||||
|
*/
|
||||||
|
private AgreementContext buildClassicLeg() throws IOException {
|
||||||
|
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
|
||||||
|
if (classicPrivate == null || classicPeerPublic == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
|
||||||
|
}
|
||||||
|
AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate,
|
||||||
|
classicSpec);
|
||||||
|
classic.setPeerPublic(classicPeerPublic);
|
||||||
|
return classic;
|
||||||
|
}
|
||||||
|
if (classicMode == ClassicMode.PAIR_MESSAGE) {
|
||||||
|
if (classicKeyPair == null) {
|
||||||
|
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
|
||||||
|
}
|
||||||
|
return CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("classic mode must be selected");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds initiator-side context.
|
* Builds initiator-side context.
|
||||||
*
|
*
|
||||||
@@ -241,22 +278,7 @@ public final class HybridKexBuilder {
|
|||||||
public HybridKexContext buildInitiator() throws IOException {
|
public HybridKexContext buildInitiator() throws IOException {
|
||||||
validateCommon();
|
validateCommon();
|
||||||
|
|
||||||
AgreementContext classic;
|
AgreementContext classic = buildClassicLeg();
|
||||||
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
|
|
||||||
if (classicPrivate == null || classicPeerPublic == null) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
|
|
||||||
}
|
|
||||||
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate, classicSpec);
|
|
||||||
classic.setPeerPublic(classicPeerPublic);
|
|
||||||
} else if (classicMode == ClassicMode.PAIR_MESSAGE) {
|
|
||||||
if (classicKeyPair == null) {
|
|
||||||
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
|
|
||||||
}
|
|
||||||
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("classic mode must be selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pqcPeerPublic == null) {
|
if (pqcPeerPublic == null) {
|
||||||
throw new IllegalStateException("pqc peer public must be set for initiator");
|
throw new IllegalStateException("pqc peer public must be set for initiator");
|
||||||
@@ -279,22 +301,7 @@ public final class HybridKexBuilder {
|
|||||||
public HybridKexContext buildResponder() throws IOException {
|
public HybridKexContext buildResponder() throws IOException {
|
||||||
validateCommon();
|
validateCommon();
|
||||||
|
|
||||||
AgreementContext classic;
|
AgreementContext classic = buildClassicLeg();
|
||||||
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
|
|
||||||
if (classicPrivate == null || classicPeerPublic == null) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
|
|
||||||
}
|
|
||||||
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate, classicSpec);
|
|
||||||
classic.setPeerPublic(classicPeerPublic);
|
|
||||||
} else if (classicMode == ClassicMode.PAIR_MESSAGE) {
|
|
||||||
if (classicKeyPair == null) {
|
|
||||||
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
|
|
||||||
}
|
|
||||||
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("classic mode must be selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pqcPrivate == null) {
|
if (pqcPrivate == null) {
|
||||||
throw new IllegalStateException("pqc private key must be set for responder");
|
throw new IllegalStateException("pqc private key must be set for responder");
|
||||||
|
|||||||
@@ -0,0 +1,471 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2026, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.sdk.builders;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import zeroecho.core.CryptoAlgorithms;
|
||||||
|
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||||
|
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
||||||
|
import zeroecho.core.alg.xdh.XdhSpec;
|
||||||
|
import zeroecho.sdk.hybrid.kex.HybridKexContext;
|
||||||
|
import zeroecho.sdk.hybrid.kex.HybridKexPolicy;
|
||||||
|
import zeroecho.sdk.hybrid.kex.HybridKexProfile;
|
||||||
|
import zeroecho.sdk.hybrid.kex.HybridKexTranscript;
|
||||||
|
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Professional unit and regression tests for {@link HybridKexBuilder}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* These tests verify both supported classic-leg construction modes,
|
||||||
|
* builder-side validation failures, policy enforcement, transcript binding, and
|
||||||
|
* mode-switch behavior. The builder is exercised strictly through its public
|
||||||
|
* fluent API so that validation and resulting hybrid context construction are
|
||||||
|
* covered together.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
class HybridKexBuilderTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setup() {
|
||||||
|
try {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
// Keep tests runnable even when BC is not present.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorResponderClassicAgreementRoundTrip() throws Exception {
|
||||||
|
System.out.println("buildInitiatorResponderClassicAgreementRoundTrip");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
HybridKexTranscript transcript = new HybridKexTranscript().addUtf8("suite", "X25519+ML-KEM-768").addUtf8("role",
|
||||||
|
"builder-test");
|
||||||
|
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
HybridKexContext alice = null;
|
||||||
|
HybridKexContext bob = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
alice = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement()
|
||||||
|
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate())
|
||||||
|
.peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||||
|
.buildInitiator();
|
||||||
|
|
||||||
|
bob = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement().algorithm("Xdh")
|
||||||
|
.spec(XdhSpec.X25519).privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic())
|
||||||
|
.pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder();
|
||||||
|
|
||||||
|
byte[] aliceMessage = alice.getPeerMessage();
|
||||||
|
System.out.println("...aliceMessage(" + lens(aliceMessage) + ")=" + hex(aliceMessage));
|
||||||
|
|
||||||
|
bob.setPeerMessage(aliceMessage);
|
||||||
|
|
||||||
|
byte[] secretAlice = alice.deriveSecret();
|
||||||
|
byte[] secretBob = bob.deriveSecret();
|
||||||
|
|
||||||
|
System.out.println("...secretAlice=" + hex(secretAlice));
|
||||||
|
System.out.println("...secretBob=" + hex(secretBob));
|
||||||
|
|
||||||
|
assertNotNull(secretAlice);
|
||||||
|
assertNotNull(secretBob);
|
||||||
|
assertArrayEquals(secretAlice, secretBob);
|
||||||
|
assertEquals(profile.outLenBytes(), secretAlice.length);
|
||||||
|
} finally {
|
||||||
|
closeQuietly(alice);
|
||||||
|
closeQuietly(bob);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorResponderClassicAgreementRoundTrip...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorResponderPairMessageRoundTrip() throws Exception {
|
||||||
|
System.out.println("buildInitiatorResponderPairMessageRoundTrip");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
HybridKexContext alice = null;
|
||||||
|
HybridKexContext bob = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
alice = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh")
|
||||||
|
.spec(XdhSpec.X25519).keyPair(new KeyPairKey(aliceClassic)).pqcKem().algorithm("ML-KEM")
|
||||||
|
.peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||||
|
|
||||||
|
bob = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.keyPair(new KeyPairKey(bobClassic)).pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate())
|
||||||
|
.buildResponder();
|
||||||
|
|
||||||
|
byte[] messageA = alice.getPeerMessage();
|
||||||
|
System.out.println("...messageA(" + lens(messageA) + ")=" + hex(messageA));
|
||||||
|
bob.setPeerMessage(messageA);
|
||||||
|
|
||||||
|
byte[] messageB = bob.getPeerMessage();
|
||||||
|
System.out.println("...messageB(" + lens(messageB) + ")=" + hex(messageB));
|
||||||
|
alice.setPeerMessage(messageB);
|
||||||
|
|
||||||
|
byte[] secretAlice = alice.deriveSecret();
|
||||||
|
byte[] secretBob = bob.deriveSecret();
|
||||||
|
|
||||||
|
System.out.println("...secretAlice=" + hex(secretAlice));
|
||||||
|
System.out.println("...secretBob=" + hex(secretBob));
|
||||||
|
|
||||||
|
assertNotNull(secretAlice);
|
||||||
|
assertNotNull(secretBob);
|
||||||
|
assertArrayEquals(secretAlice, secretBob);
|
||||||
|
assertEquals(profile.outLenBytes(), secretAlice.length);
|
||||||
|
} finally {
|
||||||
|
closeQuietly(alice);
|
||||||
|
closeQuietly(bob);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorResponderPairMessageRoundTrip...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorWithoutProfileFails() throws Exception {
|
||||||
|
System.out.println("buildInitiatorWithoutProfileFails");
|
||||||
|
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem()
|
||||||
|
.algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("profile must be set", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorWithoutProfileFails...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorWithoutClassicModeFails() throws Exception {
|
||||||
|
System.out.println("buildInitiatorWithoutClassicModeFails");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().profile(profile).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||||
|
.buildInitiator();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("classic mode must be selected", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorWithoutClassicModeFails...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorClassicAgreementWithoutPeerPublicFails() throws Exception {
|
||||||
|
System.out.println("buildInitiatorClassicAgreementWithoutPeerPublicFails");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.privateKey(aliceClassic.getPrivate()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
||||||
|
.buildInitiator();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("classic private key and peer public must be set for CLASSIC_AGREEMENT", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorClassicAgreementWithoutPeerPublicFails...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildResponderPairMessageWithoutKeyPairFails() throws Exception {
|
||||||
|
System.out.println("buildResponderPairMessageWithoutKeyPairFails");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("classic key pair must be set for PAIR_MESSAGE", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildResponderPairMessageWithoutKeyPairFails...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorWithoutPqcPeerPublicFails() throws Exception {
|
||||||
|
System.out.println("buildInitiatorWithoutPqcPeerPublicFails");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem()
|
||||||
|
.algorithm("ML-KEM").buildInitiator();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("pqc peer public must be set for initiator", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorWithoutPqcPeerPublicFails...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildResponderWithoutPqcPrivateFails() throws Exception {
|
||||||
|
System.out.println("buildResponderWithoutPqcPrivateFails");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
|
||||||
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic()).pqcKem()
|
||||||
|
.algorithm("ML-KEM").buildResponder();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("pqc private key must be set for responder", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildResponderWithoutPqcPrivateFails...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInitiatorRejectsPolicyWhenOkmTooShort() throws Exception {
|
||||||
|
System.out.println("buildInitiatorRejectsPolicyWhenOkmTooShort");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(16);
|
||||||
|
HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32);
|
||||||
|
|
||||||
|
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
HybridKexBuilder.builder().profile(profile).policy(policy).classicAgreement().algorithm("Xdh")
|
||||||
|
.spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic())
|
||||||
|
.pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...exception=" + exception.getMessage());
|
||||||
|
assertEquals("Hybrid OKM length too small: 16 < 32", exception.getMessage());
|
||||||
|
|
||||||
|
System.out.println("buildInitiatorRejectsPolicyWhenOkmTooShort...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void switchingClassicModeClearsConflictingStateAndBuildsPairMessage() throws Exception {
|
||||||
|
System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
KeyPair agreementKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair pairMessageKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
HybridKexContext context = null;
|
||||||
|
try {
|
||||||
|
HybridKexBuilder builder = HybridKexBuilder.builder().profile(profile);
|
||||||
|
|
||||||
|
builder.classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519).privateKey(agreementKeyPair.getPrivate())
|
||||||
|
.peerPublic(agreementKeyPair.getPublic());
|
||||||
|
|
||||||
|
context = builder.classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
|
||||||
|
.keyPair(new KeyPairKey(pairMessageKeyPair)).pqcKem().algorithm("ML-KEM")
|
||||||
|
.peerPublic(bobPqc.getPublic()).buildInitiator();
|
||||||
|
|
||||||
|
byte[] peerMessage = context.getPeerMessage();
|
||||||
|
System.out.println("...peerMessage(" + lens(peerMessage) + ")=" + hex(peerMessage));
|
||||||
|
|
||||||
|
assertNotNull(context);
|
||||||
|
assertNotNull(peerMessage);
|
||||||
|
} finally {
|
||||||
|
closeQuietly(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void transcriptChangesDerivedSecret() throws Exception {
|
||||||
|
System.out.println("transcriptChangesDerivedSecret");
|
||||||
|
|
||||||
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||||
|
|
||||||
|
KeyPair aliceClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqcA = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
KeyPair aliceClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||||
|
KeyPair bobPqcB = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||||
|
|
||||||
|
HybridKexTranscript transcriptA = new HybridKexTranscript().addUtf8("context", "A");
|
||||||
|
HybridKexTranscript transcriptB = new HybridKexTranscript().addUtf8("context", "B");
|
||||||
|
|
||||||
|
byte[] secretA;
|
||||||
|
byte[] secretB;
|
||||||
|
|
||||||
|
HybridKexContext aliceA = null;
|
||||||
|
HybridKexContext bobA = null;
|
||||||
|
HybridKexContext aliceB = null;
|
||||||
|
HybridKexContext bobB = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
aliceA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement()
|
||||||
|
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicA.getPrivate())
|
||||||
|
.peerPublic(bobClassicA.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcA.getPublic())
|
||||||
|
.buildInitiator();
|
||||||
|
|
||||||
|
bobA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement()
|
||||||
|
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicA.getPrivate())
|
||||||
|
.peerPublic(aliceClassicA.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcA.getPrivate())
|
||||||
|
.buildResponder();
|
||||||
|
|
||||||
|
bobA.setPeerMessage(aliceA.getPeerMessage());
|
||||||
|
secretA = aliceA.deriveSecret();
|
||||||
|
byte[] responderA = bobA.deriveSecret();
|
||||||
|
System.out.println("...secretA=" + hex(secretA));
|
||||||
|
System.out.println("...responderA=" + hex(responderA));
|
||||||
|
assertArrayEquals(secretA, responderA);
|
||||||
|
|
||||||
|
aliceB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement()
|
||||||
|
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicB.getPrivate())
|
||||||
|
.peerPublic(bobClassicB.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcB.getPublic())
|
||||||
|
.buildInitiator();
|
||||||
|
|
||||||
|
bobB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement()
|
||||||
|
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicB.getPrivate())
|
||||||
|
.peerPublic(aliceClassicB.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcB.getPrivate())
|
||||||
|
.buildResponder();
|
||||||
|
|
||||||
|
bobB.setPeerMessage(aliceB.getPeerMessage());
|
||||||
|
secretB = aliceB.deriveSecret();
|
||||||
|
byte[] responderB = bobB.deriveSecret();
|
||||||
|
System.out.println("...secretB=" + hex(secretB));
|
||||||
|
System.out.println("...responderB=" + hex(responderB));
|
||||||
|
assertArrayEquals(secretB, responderB);
|
||||||
|
|
||||||
|
if (Arrays.equals(secretA, secretB)) {
|
||||||
|
throw new AssertionError("Transcript-bound secrets should differ for different transcripts");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
closeQuietly(aliceA);
|
||||||
|
closeQuietly(bobA);
|
||||||
|
closeQuietly(aliceB);
|
||||||
|
closeQuietly(bobB);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("transcriptChangesDerivedSecret...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeQuietly(HybridKexContext context) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
context.close();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// Cleanup only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hex(byte[] value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
StringBuilder builder = new StringBuilder(value.length * 2);
|
||||||
|
for (int index = 0; index < value.length; index++) {
|
||||||
|
if (builder.length() >= 30) {
|
||||||
|
builder.append("...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int current = value[index] & 0xFF;
|
||||||
|
if (current < 16) {
|
||||||
|
builder.append('0');
|
||||||
|
}
|
||||||
|
builder.append(Integer.toHexString(current));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String lens(byte[] message) {
|
||||||
|
if (message == null || message.length < 8) {
|
||||||
|
return "classicLen=?, pqcLen=?";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DataInputStream input = new DataInputStream(new ByteArrayInputStream(message));
|
||||||
|
int classicLength = input.readInt();
|
||||||
|
int pqcLength = 0;
|
||||||
|
if (message.length >= 8 + Math.max(0, classicLength)) {
|
||||||
|
if (classicLength > 0) {
|
||||||
|
input.skipBytes(classicLength);
|
||||||
|
}
|
||||||
|
pqcLength = input.readInt();
|
||||||
|
}
|
||||||
|
return "classicLen=" + classicLength + ", pqcLen=" + pqcLength;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
return "classicLen=?, pqcLen=?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user