590 lines
29 KiB
Java
590 lines
29 KiB
Java
/*******************************************************************************
|
|
* 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 java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
|
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
|
import zeroecho.core.alg.xdh.XdhSpec;
|
|
import zeroecho.core.audit.AuditListener;
|
|
import zeroecho.core.audit.AuditMode;
|
|
import zeroecho.sdk.ZeroEchoSession;
|
|
import zeroecho.sdk.hybrid.kex.HybridKexContext;
|
|
import zeroecho.sdk.hybrid.kex.HybridKexContexts;
|
|
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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
HybridKexContext alice = null;
|
|
HybridKexContext bob = null;
|
|
|
|
try {
|
|
alice = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
HybridKexContext alice = null;
|
|
HybridKexContext bob = null;
|
|
|
|
try {
|
|
alice = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage()
|
|
.algorithm("Xdh").spec(XdhSpec.X25519).keyPair(new KeyPairKey(aliceClassic)).pqcKem()
|
|
.algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
|
|
|
|
bob = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
|
|
HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 policyFailureClosesBothConstructedLegs() throws Exception {
|
|
System.out.println("policyFailureClosesBothConstructedLegs");
|
|
AtomicInteger closedContexts = new AtomicInteger();
|
|
AuditListener listener = new AuditListener() {
|
|
@Override
|
|
public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) {
|
|
closedContexts.incrementAndGet();
|
|
}
|
|
};
|
|
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(listener).withAuditMode(AuditMode.WRAP);
|
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(16);
|
|
HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32);
|
|
KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
|
KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
|
KeyPair bobPqc = session.keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
|
|
|
assertThrows(IllegalArgumentException.class,
|
|
() -> HybridKexBuilder.builder(session).profile(profile).policy(policy).classicAgreement()
|
|
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate())
|
|
.peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
|
|
.buildInitiator());
|
|
|
|
assertEquals(2, closedContexts.get());
|
|
System.out.println("...closedContexts=" + closedContexts.get());
|
|
System.out.println("policyFailureClosesBothConstructedLegs...ok");
|
|
}
|
|
|
|
@Test
|
|
void secondLegPolicyFailureClosesFirstFactoryLeg() throws Exception {
|
|
System.out.println("secondLegPolicyFailureClosesFirstFactoryLeg");
|
|
AtomicInteger closedContexts = new AtomicInteger();
|
|
AuditListener listener = new AuditListener() {
|
|
@Override
|
|
public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) {
|
|
closedContexts.incrementAndGet();
|
|
}
|
|
};
|
|
ZeroEchoSession keySession = new ZeroEchoSession();
|
|
KeyPair aliceClassic = keySession.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
|
KeyPair bobClassic = keySession.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
|
KeyPair bobPqc = keySession.keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
|
ZeroEchoSession operationSession = new ZeroEchoSession().withAuditListener(listener)
|
|
.withAuditMode(AuditMode.WRAP).withPolicy((id, role, key, spec) -> {
|
|
if ("ML-KEM".equals(id)) {
|
|
throw new IllegalArgumentException("controlled PQ policy denial");
|
|
}
|
|
});
|
|
|
|
assertThrows(IllegalArgumentException.class,
|
|
() -> HybridKexContexts.initiator(operationSession, HybridKexProfile.defaultProfile(32), "Xdh",
|
|
aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(),
|
|
null));
|
|
|
|
assertEquals(1, closedContexts.get());
|
|
System.out.println("...closedContexts=" + closedContexts.get());
|
|
System.out.println("secondLegPolicyFailureClosesFirstFactoryLeg...ok");
|
|
}
|
|
|
|
@Test
|
|
void invalidFactoryInputIsRejectedBeforeContextAllocation() throws Exception {
|
|
System.out.println("invalidFactoryInputIsRejectedBeforeContextAllocation");
|
|
AtomicInteger createdContexts = new AtomicInteger();
|
|
AuditListener listener = new AuditListener() {
|
|
@Override
|
|
public void onContextCreatedMeta(String contextId, String algorithmId, String provider,
|
|
zeroecho.core.KeyUsage role, String keyFingerprint, java.util.Map<String, Object> specMeta) {
|
|
createdContexts.incrementAndGet();
|
|
}
|
|
};
|
|
ZeroEchoSession session = new ZeroEchoSession().withAuditListener(listener).withAuditMode(AuditMode.WRAP);
|
|
KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
|
KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519);
|
|
|
|
assertThrows(NullPointerException.class,
|
|
() -> HybridKexContexts.initiator(session, HybridKexProfile.defaultProfile(32), "Xdh",
|
|
aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519, "ML-KEM", null, null));
|
|
|
|
assertEquals(0, createdContexts.get());
|
|
System.out.println("...createdContexts=0");
|
|
System.out.println("invalidFactoryInputIsRejectedBeforeContextAllocation...ok");
|
|
}
|
|
|
|
@Test
|
|
void switchingClassicModeClearsConflictingStateAndBuildsPairMessage() throws Exception {
|
|
System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage");
|
|
|
|
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
|
KeyPair agreementKeyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair pairMessageKeyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric()
|
|
.generateKeyPair("Xdh", XdhSpec.X25519);
|
|
KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
HybridKexContext context = null;
|
|
try {
|
|
HybridKexBuilder builder = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).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 = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassicA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqcA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM",
|
|
KyberKeyGenSpec.kyber768());
|
|
|
|
KeyPair aliceClassicB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobClassicB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh",
|
|
XdhSpec.X25519);
|
|
KeyPair bobPqcB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().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(new zeroecho.sdk.ZeroEchoSession()).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(new zeroecho.sdk.ZeroEchoSession()).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(new zeroecho.sdk.ZeroEchoSession()).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(new zeroecho.sdk.ZeroEchoSession()).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=?";
|
|
}
|
|
}
|
|
}
|