Compare commits
14 Commits
release@1.
...
impl-hybri
| Author | SHA1 | Date | |
|---|---|---|---|
|
300f40c283
|
|||
|
55da24735f
|
|||
|
34eca245f0
|
|||
|
7f79082adc
|
|||
|
174d63dff4
|
|||
|
84b97b4e0a
|
|||
|
2b4559884f
|
|||
|
8f228c7ada
|
|||
|
4da4547a46
|
|||
|
cb363ba2f4
|
|||
|
0b4b4de603
|
|||
|
eba163dd21
|
|||
|
31018235dc
|
|||
|
e328a6a103
|
@@ -89,5 +89,7 @@ jobs:
|
||||
- name: Create Gitea Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: app/build/distributions/*.tar app/build/distributions/*.zip
|
||||
files: |
|
||||
app/build/distributions/*.tar
|
||||
app/build/distributions/*.zip
|
||||
body_path: /tmp/release_notes.md
|
||||
|
||||
13
.ruleset
13
.ruleset
@@ -136,7 +136,7 @@
|
||||
<rule ref="category/java/codestyle.xml/UnnecessaryConstructor"/>
|
||||
<rule ref="category/java/codestyle.xml/UnnecessaryFullyQualifiedName"/>
|
||||
<rule ref="category/java/codestyle.xml/UnnecessaryImport"/>
|
||||
<rule ref="category/java/codestyle.xml/UnnecessaryLocalBeforeReturn"/>
|
||||
<!-- PMD 8.0.0: obsolete rule ref="category/java/codestyle.xml/UnnecessaryLocalBeforeReturn"/ -->
|
||||
<rule ref="category/java/codestyle.xml/UnnecessaryModifier"/>
|
||||
<rule ref="category/java/codestyle.xml/UnnecessaryReturn"/>
|
||||
<rule ref="category/java/codestyle.xml/UnnecessarySemicolon"/>
|
||||
@@ -147,7 +147,6 @@
|
||||
<rule ref="category/java/codestyle.xml/UseShortArrayInitializer"/>
|
||||
<rule ref="category/java/codestyle.xml/UseUnderscoresInNumericLiterals"/>
|
||||
<rule ref="category/java/design.xml/AbstractClassWithoutAnyMethod"/>
|
||||
<rule ref="category/java/design.xml/AvoidCatchingGenericException"/>
|
||||
<rule ref="category/java/design.xml/AvoidDeeplyNestedIfStmts"/>
|
||||
<rule ref="category/java/design.xml/AvoidRethrowingException"/>
|
||||
<rule ref="category/java/design.xml/AvoidThrowingNewInstanceOfSameException"/>
|
||||
@@ -231,8 +230,11 @@
|
||||
<rule ref="category/java/errorprone.xml/AvoidAssertAsIdentifier"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidBranchingStatementAsLastInLoop"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidCallingFinalize"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidCatchingNPE"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidCatchingThrowable"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidCatchingGenericException">
|
||||
<properties>
|
||||
<property name="typesThatShouldNotBeCaught" value="java.lang.RuntimeException,java.lang.Throwable,java.lang.Error" />
|
||||
</properties>
|
||||
</rule>
|
||||
<rule ref="category/java/errorprone.xml/AvoidDecimalLiteralsInBigDecimalConstructor"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidDuplicateLiterals">
|
||||
<properties>
|
||||
@@ -245,7 +247,6 @@
|
||||
<rule ref="category/java/errorprone.xml/AvoidFieldNameMatchingTypeName"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidInstanceofChecksInCatchClause"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidLiteralsInIfCondition"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidLosingExceptionInformation"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidMultipleUnaryOperators"/>
|
||||
<rule ref="category/java/errorprone.xml/AvoidUsingOctalValues"/>
|
||||
<rule ref="category/java/errorprone.xml/BrokenNullCheck"/>
|
||||
@@ -313,7 +314,7 @@
|
||||
<rule ref="category/java/errorprone.xml/UnusedNullCheckInEquals"/>
|
||||
<rule ref="category/java/errorprone.xml/UseCorrectExceptionLogging"/>
|
||||
<rule ref="category/java/errorprone.xml/UseEqualsToCompareStrings"/>
|
||||
<rule ref="category/java/errorprone.xml/UselessOperationOnImmutable"/>
|
||||
<rule ref="category/java/errorprone.xml/UselessPureMethodCall" />
|
||||
<rule ref="category/java/errorprone.xml/UseLocaleWithCaseConversions"/>
|
||||
<rule ref="category/java/errorprone.xml/UseProperClassLoader"/>
|
||||
<rule ref="category/java/multithreading.xml/AvoidSynchronizedAtMethodLevel"/>
|
||||
|
||||
@@ -30,9 +30,9 @@ repositories {
|
||||
dependencies {
|
||||
constraints {
|
||||
// Define dependency versions as constraints
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
implementation 'commons-cli:commons-cli:1.9.0'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk18on:1.81'
|
||||
implementation 'org.apache.commons:commons-text:1.15.0'
|
||||
implementation 'commons-cli:commons-cli:1.11.0'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk18on:1.83'
|
||||
implementation 'org.egothor:conflux:[1.0,2.0)'
|
||||
implementation 'org.apache.commons:commons-imaging:1.0.0-alpha6'
|
||||
}
|
||||
@@ -45,7 +45,7 @@ dependencies {
|
||||
|
||||
pmd {
|
||||
consoleOutput = true
|
||||
toolVersion = '7.16.0'
|
||||
toolVersion = '7.19.0'
|
||||
sourceSets = [sourceSets.main]
|
||||
ruleSetFiles = files(rootProject.file(".ruleset"))
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ public final class CryptoAlgorithms {
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) { // NOPMD
|
||||
} catch (Exception ignored) {
|
||||
// swallow and report via audit only if destroyed
|
||||
}
|
||||
if (destroyed) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -158,8 +159,8 @@ public final class BikeKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
BIKEKEMGenerator gen = new BIKEKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -186,7 +187,7 @@ public final class BikeKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
BIKEKEMExtractor ex = new BIKEKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("BIKE decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> implements
|
||||
if (nonce == null) {
|
||||
nonce = new byte[NONCE_LEN];
|
||||
rnd.nextBytes(nonce);
|
||||
if (ctx != null) {
|
||||
if (ctx != null) { // NOPMD
|
||||
ctx.put(ConfluxKeys.iv(id), nonce);
|
||||
}
|
||||
} else if (nonce.length != NONCE_LEN) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -190,8 +191,8 @@ public final class CmceKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
CMCEKEMGenerator gen = new CMCEKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -219,7 +220,7 @@ public final class CmceKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
CMCEKEMExtractor ex = new CMCEKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("CMCE decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ import zeroecho.core.context.AgreementContext;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class GenericJcaAgreementContext implements AgreementContext {
|
||||
public class GenericJcaAgreementContext implements AgreementContext {
|
||||
private final CryptoAlgorithm algorithm;
|
||||
private final PrivateKey privateKey;
|
||||
private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448")
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.common.agreement;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
|
||||
/**
|
||||
* Message-oriented JCA key agreement context where the handshake message is a
|
||||
* public key encoding.
|
||||
*
|
||||
* <p>
|
||||
* This context provides a {@link MessageAgreementContext} view over a classical
|
||||
* JCA Diffie–Hellman style agreement (e.g., ECDH, XDH). The protocol
|
||||
* "to-be-sent" data for such agreements is the party's public key. This class
|
||||
* therefore maps:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #getPeerMessage()} to the local public key encoding (X.509
|
||||
* SubjectPublicKeyInfo),</li>
|
||||
* <li>{@link #setPeerMessage(byte[])} to importing the peer public key and
|
||||
* binding it as the peer key.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <p>
|
||||
* The message format is the standard X.509 SubjectPublicKeyInfo encoding
|
||||
* returned by {@link java.security.PublicKey#getEncoded()}. This format is
|
||||
* stable, widely interoperable and avoids ad-hoc or algorithm-specific wire
|
||||
* formats.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Algorithm and provider selection</h2>
|
||||
* <p>
|
||||
* The underlying key agreement algorithm name (for
|
||||
* {@link javax.crypto.KeyAgreement}) and the key factory algorithm name (for
|
||||
* {@link KeyFactory}) are supplied explicitly by the algorithm registration
|
||||
* code. This keeps algorithm-specific knowledge in {@code *Algorithm} classes
|
||||
* rather than embedding naming heuristics into shared code.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Lifecycle</h2>
|
||||
* <ol>
|
||||
* <li>Create the context with the local key pair wrapper.</li>
|
||||
* <li>Send {@link #getPeerMessage()} to the remote party.</li>
|
||||
* <li>Receive the remote party's message and call
|
||||
* {@link #setPeerMessage(byte[])}.</li>
|
||||
* <li>Derive the raw shared secret with {@link #deriveSecret()}.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <ul>
|
||||
* <li>The message returned by {@link #getPeerMessage()} contains only public
|
||||
* key material.</li>
|
||||
* <li>The output of {@link #deriveSecret()} is a raw shared secret and must be
|
||||
* processed with a KDF by higher protocol layers before use as symmetric keying
|
||||
* material.</li>
|
||||
* <li>This class does not log, persist, or otherwise expose private key
|
||||
* material.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* Instances are not thread-safe and are intended for single-use,
|
||||
* single-threaded protocol executions.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext
|
||||
implements MessageAgreementContext {
|
||||
|
||||
private final PublicKey localPublic;
|
||||
private final String keyFactoryAlg;
|
||||
private final String keyFactoryProvider;
|
||||
|
||||
/**
|
||||
* Creates a message-oriented agreement context over a JCA key agreement.
|
||||
*
|
||||
* <p>
|
||||
* The wrapped {@link KeyPairKey} supplies both local private and public key
|
||||
* components: the private key is used for the underlying agreement computation,
|
||||
* while the public key is exported as the handshake message via
|
||||
* {@link #getPeerMessage()}.
|
||||
* </p>
|
||||
*
|
||||
* @param alg algorithm descriptor that owns this context
|
||||
* @param keyPairKey local key pair wrapper; must contain both a private
|
||||
* and a public key
|
||||
* @param jcaAgreementName JCA {@link javax.crypto.KeyAgreement} algorithm
|
||||
* name (e.g., {@code "ECDH"}, {@code "X25519"})
|
||||
* @param agreementProvider optional JCA provider name for
|
||||
* {@link javax.crypto.KeyAgreement}, or {@code null}
|
||||
* for default provider selection
|
||||
* @param keyFactoryAlg JCA {@link KeyFactory} algorithm name used to
|
||||
* import peer public keys (e.g., {@code "EC"},
|
||||
* {@code "XDH"})
|
||||
* @param keyFactoryProvider optional JCA provider name for {@link KeyFactory},
|
||||
* or {@code null} for default provider selection
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName,
|
||||
String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) {
|
||||
super(Objects.requireNonNull(alg, "alg"), Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(),
|
||||
Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider);
|
||||
this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public");
|
||||
this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg");
|
||||
this.keyFactoryProvider = keyFactoryProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local party's handshake message.
|
||||
*
|
||||
* <p>
|
||||
* For DH/XDH-style agreements, the handshake message is the local public key
|
||||
* encoding. The returned array is a defensive copy and may be safely
|
||||
* transmitted over untrusted channels.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The encoding is the standard X.509 SubjectPublicKeyInfo bytes returned by
|
||||
* {@link PublicKey#getEncoded()}.
|
||||
* </p>
|
||||
*
|
||||
* @return a defensive copy of the local public key encoding (never
|
||||
* {@code null})
|
||||
* @throws IllegalStateException if the local public key does not provide an
|
||||
* encoding
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public byte[] getPeerMessage() {
|
||||
byte[] encoded = localPublic.getEncoded();
|
||||
if (encoded == null) {
|
||||
throw new IllegalStateException("Local public key does not provide an encoding");
|
||||
}
|
||||
return encoded.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies the peer party's handshake message.
|
||||
*
|
||||
* <p>
|
||||
* The provided message is interpreted as an X.509 SubjectPublicKeyInfo encoding
|
||||
* of the peer public key. The key is imported using {@link KeyFactory} and then
|
||||
* assigned as the peer key for the underlying agreement computation.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Passing {@code null} resets the peer binding and makes
|
||||
* {@link #deriveSecret()} unusable until a new peer message is provided.
|
||||
* </p>
|
||||
*
|
||||
* @param message peer public key encoding (SPKI), or {@code null} to reset the
|
||||
* peer state
|
||||
* @throws IllegalArgumentException if the message cannot be imported as a
|
||||
* public key using the configured
|
||||
* {@link KeyFactory} algorithm/provider
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public void setPeerMessage(byte[] message) {
|
||||
if (message == null) {
|
||||
setPeerPublic(null);
|
||||
return;
|
||||
}
|
||||
|
||||
PublicKey peerPublic = importPeerPublic(message);
|
||||
setPeerPublic(peerPublic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a peer public key from an X.509 SubjectPublicKeyInfo encoding.
|
||||
*
|
||||
* <p>
|
||||
* This method performs no caching and always imports the key anew. The caller
|
||||
* is responsible for ensuring that the protocol-layer validation requirements
|
||||
* are met (e.g., checking that the received public key belongs to an expected
|
||||
* identity, or that the agreement mode requires ephemeral vs. static keys).
|
||||
* </p>
|
||||
*
|
||||
* @param spkiEncoded peer public key encoding (SPKI); must not be {@code null}
|
||||
* @return imported peer {@link PublicKey}
|
||||
* @throws IllegalArgumentException if the key cannot be imported
|
||||
*/
|
||||
private PublicKey importPeerPublic(byte[] spkiEncoded) {
|
||||
try {
|
||||
KeyFactory keyFactory = (keyFactoryProvider == null) ? KeyFactory.getInstance(keyFactoryAlg)
|
||||
: KeyFactory.getInstance(keyFactoryAlg, keyFactoryProvider);
|
||||
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(spkiEncoded);
|
||||
return keyFactory.generatePublic(spec);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.common.agreement;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A {@link Key} wrapper that carries a {@link KeyPair} through APIs that are
|
||||
* constrained to {@link Key} types.
|
||||
*
|
||||
* <p>
|
||||
* This type exists to support capabilities that require both private and public
|
||||
* components (e.g., message-oriented key agreement contexts), while preserving
|
||||
* backward-compatible capabilities that accept only a {@link PrivateKey}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The wrapper does not expose an encoding via {@link #getEncoded()} because
|
||||
* serializing private key material implicitly is dangerous and not required for
|
||||
* capability dispatch.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class KeyPairKey implements Key, Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final KeyPair keyPair;
|
||||
|
||||
/**
|
||||
* Creates a wrapper around a {@link KeyPair}.
|
||||
*
|
||||
* @param keyPair key pair to wrap (must not be {@code null})
|
||||
* @throws NullPointerException if {@code keyPair} is {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
public KeyPairKey(KeyPair keyPair) {
|
||||
this.keyPair = Objects.requireNonNull(keyPair, "keyPair");
|
||||
Objects.requireNonNull(keyPair.getPrivate(), "keyPair.private");
|
||||
Objects.requireNonNull(keyPair.getPublic(), "keyPair.public");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wrapped {@link KeyPair}.
|
||||
*
|
||||
* @return key pair
|
||||
* @since 1.0
|
||||
*/
|
||||
public KeyPair keyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wrapped private key.
|
||||
*
|
||||
* @return private key
|
||||
* @since 1.0
|
||||
*/
|
||||
public PrivateKey privateKey() {
|
||||
return keyPair.getPrivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wrapped public key.
|
||||
*
|
||||
* @return public key
|
||||
* @since 1.0
|
||||
*/
|
||||
public PublicKey publicKey() {
|
||||
return keyPair.getPublic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the algorithm name of the wrapped private key.
|
||||
*
|
||||
* @return algorithm name
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return privateKey().getAlgorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code null}. This wrapper intentionally does not define a standard
|
||||
* encoding format.
|
||||
*
|
||||
* @return {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public String getFormat() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code null}. This wrapper intentionally does not expose a combined
|
||||
* encoding.
|
||||
*
|
||||
* @return {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public byte[] getEncoded() {
|
||||
return null; // NOPMD
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,10 @@ import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
@@ -133,6 +136,10 @@ public final class DhAlgorithm extends AbstractCryptoAlgorithm {
|
||||
DhSpec.class,
|
||||
(PrivateKey k, DhSpec s) -> new GenericJcaAgreementContext(this, k, "DiffieHellman", null),
|
||||
DhSpec::ffdhe2048);
|
||||
capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
|
||||
DhSpec.class, (KeyPairKey k, DhSpec s) -> new GenericJcaMessageAgreementContext(this, k,
|
||||
"DiffieHellman", null, "DH", null),
|
||||
DhSpec::ffdhe2048);
|
||||
|
||||
registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
|
||||
registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
|
||||
@@ -40,12 +40,15 @@ import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaCurveSpec;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
|
||||
/**
|
||||
* <h2>Elliptic Curve Diffie-Hellman (ECDH) Algorithm</h2>
|
||||
@@ -152,6 +155,10 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
|
||||
EcdsaCurveSpec.class,
|
||||
(PrivateKey k, EcdsaCurveSpec s) -> new GenericJcaAgreementContext(this, k, "ECDH", null),
|
||||
() -> EcdsaCurveSpec.P256); // XXX spec is not used at all ?!?!
|
||||
capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
|
||||
EcdsaCurveSpec.class, (KeyPairKey k, EcdsaCurveSpec s) -> new GenericJcaMessageAgreementContext(this, k,
|
||||
"ECDH", null, "EC", null),
|
||||
() -> EcdsaCurveSpec.P256);
|
||||
|
||||
// Reuse EC builders/importers
|
||||
registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -179,8 +180,8 @@ public final class FrodoKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
FrodoKEMGenerator gen = new FrodoKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -212,7 +213,7 @@ public final class FrodoKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
FrodoKEMExtractor ex = new FrodoKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Frodo decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,4 +137,50 @@ public final class HmacSpec implements ContextSpec, Describable {
|
||||
public String description() {
|
||||
return macName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a recommended key size (in bits) for this HMAC variant.
|
||||
*
|
||||
* <p>
|
||||
* HMAC is defined for keys of arbitrary length; this method therefore does not
|
||||
* express a strict requirement. It provides a conservative,
|
||||
* interoperability-friendly recommendation intended for default key derivation
|
||||
* and key generation paths, especially where the caller does not want to
|
||||
* manually select a key size.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The recommendation follows common practice: use a key size at least equal to
|
||||
* the underlying hash output length. For the built-in variants this yields:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>HmacSHA256 - 256 bits</li>
|
||||
* <li>HmacSHA384 - 384 bits</li>
|
||||
* <li>HmacSHA512 - 512 bits</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* If this spec uses an unrecognized {@link #macName()} value, the method
|
||||
* returns {@code 256} bits as a safe default and to avoid failing existing
|
||||
* applications that rely on custom provider names. Applications with strict
|
||||
* requirements should enforce their own policy and/or explicitly specify a key
|
||||
* size.
|
||||
* </p>
|
||||
*
|
||||
* @return recommended key size in bits (positive, multiple of 8)
|
||||
* @since 1.0
|
||||
*/
|
||||
public int recommendedKeyBits() {
|
||||
return recommendedKeyBitsForMacName(macName);
|
||||
}
|
||||
|
||||
private static int recommendedKeyBitsForMacName(String macName) {
|
||||
return switch (macName) {
|
||||
case "HmacSHA256" -> 256;
|
||||
case "HmacSHA384" -> 384;
|
||||
case "HmacSHA512" -> 512;
|
||||
default -> 256;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -179,8 +180,8 @@ public final class HqcKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
HQCKEMGenerator gen = new HQCKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -213,7 +214,7 @@ public final class HqcKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
HQCKEMExtractor ex = new HQCKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("HQC decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -182,8 +183,8 @@ public final class KyberKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
MLKEMGenerator gen = new MLKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -220,7 +221,7 @@ public final class KyberKemContext implements KemContext {
|
||||
MLKEMExtractor gen = new MLKEMExtractor(keyParam);
|
||||
|
||||
return gen.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Kyber decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
105
lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java
Normal file
105
lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
|
||||
/**
|
||||
* ML-DSA (FIPS 204) signature algorithm binding for the ZeroEcho framework.
|
||||
*
|
||||
* <p>
|
||||
* This binding exposes ML-DSA as a first-class algorithm identity while relying
|
||||
* on the provider's ML-DSA JCA implementations.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This algorithm registers two roles:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link KeyUsage#SIGN}: produces ML-DSA signatures using a
|
||||
* {@link PrivateKey}.</li>
|
||||
* <li>{@link KeyUsage#VERIFY}: verifies ML-DSA signatures using a
|
||||
* {@link PublicKey}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Both roles are configured with {@link VoidSpec}, as ML-DSA requires no
|
||||
* runtime context parameters beyond the key material.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaAlgorithm extends AbstractCryptoAlgorithm {
|
||||
|
||||
/**
|
||||
* Creates a new ML-DSA algorithm instance and registers its capabilities.
|
||||
*
|
||||
* @throws IllegalArgumentException if a signature context cannot be initialized
|
||||
* due to provider errors
|
||||
*/
|
||||
public MldsaAlgorithm() {
|
||||
super("ML-DSA", "MLDSA");
|
||||
|
||||
capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, VoidSpec.class,
|
||||
(PrivateKey k, VoidSpec s) -> {
|
||||
try {
|
||||
return new MldsaSignatureContext(this, k);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Cannot init ML-DSA signer", e);
|
||||
}
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, VoidSpec.class,
|
||||
(PublicKey k, VoidSpec s) -> {
|
||||
try {
|
||||
return new MldsaSignatureContext(this, k);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Cannot init ML-DSA verifier", e);
|
||||
}
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(MldsaKeyGenSpec.class, new MldsaKeyGenBuilder(), MldsaKeyGenSpec::defaultSpec);
|
||||
registerAsymmetricKeyBuilder(MldsaPublicKeySpec.class, new MldsaPublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(MldsaPrivateKeySpec.class, new MldsaPrivateKeyBuilder(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Key pair builder for ML-DSA (FIPS 204) using the Bouncy Castle provider.
|
||||
*
|
||||
* <p>
|
||||
* This builder maps {@link MldsaKeyGenSpec} to the appropriate
|
||||
* {@code org.bouncycastle.jcajce.spec.MLDSAParameterSpec} constant. Reflection
|
||||
* is used to avoid a hard dependency on any particular set of parameter
|
||||
* constants across provider versions.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaKeyGenBuilder implements AsymmetricKeyBuilder<MldsaKeyGenSpec> {
|
||||
|
||||
private static final String ALG_PURE = "MLDSA";
|
||||
private static final String ALG_SHA512 = "SHA512withMLDSA";
|
||||
|
||||
/**
|
||||
* Generates a key pair according to the given specification.
|
||||
*
|
||||
* @param spec key generation specification
|
||||
* @return generated key pair
|
||||
* @throws GeneralSecurityException if the JCA engine cannot be initialized or
|
||||
* the parameter set is unknown
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(MldsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
Object bcParamSpec = resolveBcParameterSpec(spec);
|
||||
|
||||
String kpgAlg = (spec.preHash() == MldsaKeyGenSpec.PreHash.SHA512) ? ALG_SHA512 : ALG_PURE;
|
||||
|
||||
KeyPairGenerator kpg = (spec.providerName() == null) ? KeyPairGenerator.getInstance(kpgAlg)
|
||||
: KeyPairGenerator.getInstance(kpgAlg, spec.providerName());
|
||||
|
||||
if (bcParamSpec != null) {
|
||||
kpg.initialize((java.security.spec.AlgorithmParameterSpec) bcParamSpec);
|
||||
}
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key generation specs cannot import public keys.
|
||||
*
|
||||
* @param spec key generation specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public java.security.PublicKey importPublic(MldsaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use MldsaPublicKeySpec to import a public key.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Key generation specs cannot import private keys.
|
||||
*
|
||||
* @param spec key generation specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public java.security.PrivateKey importPrivate(MldsaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use MldsaPrivateKeySpec to import a private key.");
|
||||
}
|
||||
|
||||
private static Object resolveBcParameterSpec(MldsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
if (spec.explicitParamConstant() != null) {
|
||||
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.MLDSAParameterSpec",
|
||||
spec.explicitParamConstant());
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
throw new GeneralSecurityException("Unknown MLDSAParameterSpec constant: " + spec.explicitParamConstant());
|
||||
}
|
||||
|
||||
String base = "ml_dsa_" + Integer.toString(spec.parameterSet().number);
|
||||
|
||||
String suffix = "";
|
||||
if (spec.preHash() != MldsaKeyGenSpec.PreHash.NONE) {
|
||||
suffix = "_with_" + spec.preHash().name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
String name = base + suffix;
|
||||
|
||||
// Fail fast if we detect clearly unsupported pre-hash requests.
|
||||
validateCombination(spec);
|
||||
|
||||
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.MLDSAParameterSpec", name);
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
// Fallback: attempt base (no pre-hash), then fail.
|
||||
if (!suffix.isEmpty()) {
|
||||
Object baseC = fetchStaticField("org.bouncycastle.jcajce.spec.MLDSAParameterSpec", base);
|
||||
if (baseC != null) {
|
||||
return baseC;
|
||||
}
|
||||
}
|
||||
|
||||
throw new GeneralSecurityException("Cannot resolve MLDSAParameterSpec constant: " + name);
|
||||
}
|
||||
|
||||
private static void validateCombination(MldsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
if (spec.preHash() == MldsaKeyGenSpec.PreHash.NONE) {
|
||||
return;
|
||||
}
|
||||
if (spec.preHash() != MldsaKeyGenSpec.PreHash.SHA512) {
|
||||
throw new GeneralSecurityException("ML-DSA supports only PreHash.NONE or PreHash.SHA512.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Object fetchStaticField(String className, String field) {
|
||||
try {
|
||||
Class<?> cls = Class.forName(className);
|
||||
Field f = cls.getField(field);
|
||||
return f.get(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenSpec.java
Normal file
193
lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenSpec.java
Normal file
@@ -0,0 +1,193 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Specification for generating ML-DSA key pairs (FIPS 204).
|
||||
*
|
||||
* <p>
|
||||
* ML-DSA is parameterized by a parameter set number (44/65/87). Bouncy Castle
|
||||
* may also expose pre-hash variants (e.g., "with SHA-512") as separate
|
||||
* parameter specs.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This spec intentionally restricts choices to the standardized ML-DSA
|
||||
* parameter sets only.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaKeyGenSpec implements AlgorithmKeySpec {
|
||||
|
||||
/**
|
||||
* ML-DSA parameter sets as defined by FIPS 204.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public enum ParameterSet {
|
||||
/** ML-DSA-44 (~128-bit security). */
|
||||
ML_DSA_44(44, 128),
|
||||
/** ML-DSA-65 (~192-bit security). */
|
||||
ML_DSA_65(65, 192),
|
||||
/** ML-DSA-87 (~256-bit security). */
|
||||
ML_DSA_87(87, 256);
|
||||
|
||||
/**
|
||||
* Parameter set number (44/65/87).
|
||||
*/
|
||||
public final int number;
|
||||
|
||||
/**
|
||||
* Claimed security strength in bits (128/192/256) used by policy.
|
||||
*/
|
||||
public final int strengthBits;
|
||||
|
||||
ParameterSet(int number, int strengthBits) {
|
||||
this.number = number;
|
||||
this.strengthBits = strengthBits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional pre-hash variant selection.
|
||||
*
|
||||
* <p>
|
||||
* If the provider exposes "with hash" variants as distinct parameter specs,
|
||||
* they can be selected here.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public enum PreHash {
|
||||
/** No pre-hash parameter set (pure ML-DSA). */
|
||||
NONE,
|
||||
/** Pre-hash with SHA-512 (provider-specific variant). */
|
||||
SHA512
|
||||
}
|
||||
|
||||
private static final MldsaKeyGenSpec DEFAULT = new MldsaKeyGenSpec("BC", ParameterSet.ML_DSA_65, PreHash.NONE,
|
||||
null);
|
||||
|
||||
private final String providerName;
|
||||
private final ParameterSet parameterSet;
|
||||
private final PreHash preHash;
|
||||
private final String explicitParamConstant; // nullable
|
||||
|
||||
private MldsaKeyGenSpec(String providerName, ParameterSet parameterSet, PreHash preHash,
|
||||
String explicitParamConstant) {
|
||||
this.providerName = Objects.requireNonNull(providerName, "providerName");
|
||||
this.parameterSet = Objects.requireNonNull(parameterSet, "parameterSet");
|
||||
this.preHash = Objects.requireNonNull(preHash, "preHash");
|
||||
this.explicitParamConstant = explicitParamConstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default ML-DSA key generation spec.
|
||||
*
|
||||
* @return a singleton default specification (ML-DSA-65, no pre-hash)
|
||||
*/
|
||||
public static MldsaKeyGenSpec defaultSpec() {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification with explicit algorithm parameters.
|
||||
*
|
||||
* @param providerName JCA provider name (typically {@code "BC"})
|
||||
* @param parameterSet parameter set (44/65/87)
|
||||
* @param preHash optional pre-hash selection
|
||||
* @return a new {@code MldsaKeyGenSpec}
|
||||
*/
|
||||
public static MldsaKeyGenSpec of(String providerName, ParameterSet parameterSet, PreHash preHash) {
|
||||
return new MldsaKeyGenSpec(providerName, parameterSet, preHash, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this specification with an explicit Bouncy Castle parameter
|
||||
* constant override.
|
||||
*
|
||||
* <p>
|
||||
* This bypasses automatic mapping. The name must match a static field in
|
||||
* {@code org.bouncycastle.jcajce.spec.MLDSAParameterSpec}.
|
||||
* </p>
|
||||
*
|
||||
* @param name field name in {@code MLDSAParameterSpec}
|
||||
* @return a new {@code MldsaKeyGenSpec} with the override
|
||||
*/
|
||||
public MldsaKeyGenSpec withExplicitParamConstant(String name) {
|
||||
return new MldsaKeyGenSpec(providerName, parameterSet, preHash, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name used for key generation.
|
||||
*
|
||||
* @return provider name (never {@code null})
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ML-DSA parameter set.
|
||||
*
|
||||
* @return parameter set
|
||||
*/
|
||||
public ParameterSet parameterSet() {
|
||||
return parameterSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pre-hash selection.
|
||||
*
|
||||
* @return pre-hash selection
|
||||
*/
|
||||
public PreHash preHash() {
|
||||
return preHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the explicit parameter constant override, if set.
|
||||
*
|
||||
* @return constant name, or {@code null} if automatic mapping is used
|
||||
*/
|
||||
public String explicitParamConstant() {
|
||||
return explicitParamConstant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Builder for importing ML-DSA private keys from encoded specifications.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaPrivateKeyBuilder implements AsymmetricKeyBuilder<MldsaPrivateKeySpec> {
|
||||
|
||||
private static final String ALG = "ML-DSA";
|
||||
|
||||
/**
|
||||
* Generation is not supported by this spec.
|
||||
*
|
||||
* @param spec encoded private key spec
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(MldsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Public key import is not supported by this spec.
|
||||
*
|
||||
* @param spec encoded private key spec
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(MldsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use MldsaPublicKeySpec for public keys.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a private key from PKCS#8 encoding.
|
||||
*
|
||||
* @param spec encoded private key spec
|
||||
* @return imported private key
|
||||
* @throws GeneralSecurityException if the key cannot be parsed or the provider
|
||||
* does not support ML-DSA
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(MldsaPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG)
|
||||
: KeyFactory.getInstance(ALG, spec.providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Encoded representation of an ML-DSA private key.
|
||||
*
|
||||
* <p>
|
||||
* {@code MldsaPrivateKeySpec} is an immutable value object that wraps a
|
||||
* PKCS#8-encoded ML-DSA private key together with the JCA provider name that
|
||||
* should be used when importing the key.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>The private key material is stored as a defensive copy of the provided
|
||||
* PKCS#8 byte array.</li>
|
||||
* <li>Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under
|
||||
* {@code "pkcs8.b64"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider selection</h2>
|
||||
* <p>
|
||||
* The default provider is {@code "BC"}.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
|
||||
private final byte[] encodedPkcs8;
|
||||
private final String providerName;
|
||||
|
||||
/**
|
||||
* Creates a new specification using the default provider {@code "BC"}.
|
||||
*
|
||||
* @param encodedPkcs8 PKCS#8-encoded ML-DSA private key bytes
|
||||
* @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null}
|
||||
*/
|
||||
public MldsaPrivateKeySpec(byte[] encodedPkcs8) {
|
||||
this(encodedPkcs8, "BC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification using the supplied provider.
|
||||
*
|
||||
* @param encodedPkcs8 PKCS#8-encoded ML-DSA private key bytes
|
||||
* @param providerName JCA provider name to use for import; may be {@code null}
|
||||
* @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null}
|
||||
*/
|
||||
public MldsaPrivateKeySpec(byte[] encodedPkcs8, String providerName) {
|
||||
if (encodedPkcs8 == null) {
|
||||
throw new IllegalArgumentException("encodedPkcs8 must not be null");
|
||||
}
|
||||
this.encodedPkcs8 = encodedPkcs8.clone();
|
||||
this.providerName = (providerName == null ? "BC" : providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the PKCS#8-encoded private key bytes.
|
||||
*
|
||||
* @return a copy of the encoded private key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedPkcs8.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name associated with this specification.
|
||||
*
|
||||
* @return provider name, never {@code null}
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given spec into a {@link PairSeq}.
|
||||
*
|
||||
* @param spec the private key specification to serialize
|
||||
* @return serialized representation
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(MldsaPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
|
||||
return PairSeq.of("type", "MLDSA-PRIV", "pkcs8.b64", b64, "provider", spec.providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a {@link MldsaPrivateKeySpec} from a {@link PairSeq}.
|
||||
*
|
||||
* @param p serialized input
|
||||
* @return reconstructed private key specification
|
||||
* @throws IllegalArgumentException if {@code "pkcs8.b64"} is missing
|
||||
* @throws NullPointerException if {@code p} is {@code null}
|
||||
*/
|
||||
public static MldsaPrivateKeySpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
String prov = "BC";
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "pkcs8.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "provider" -> prov = v;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for ML-DSA private key");
|
||||
}
|
||||
return new MldsaPrivateKeySpec(out, prov);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Builder for importing ML-DSA public keys from encoded specifications.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaPublicKeyBuilder implements AsymmetricKeyBuilder<MldsaPublicKeySpec> {
|
||||
|
||||
private static final String ALG = "ML-DSA";
|
||||
|
||||
/**
|
||||
* Generation is not supported by this spec.
|
||||
*
|
||||
* @param spec encoded public key spec
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(MldsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a public key from X.509 encoding.
|
||||
*
|
||||
* @param spec encoded public key spec
|
||||
* @return imported public key
|
||||
* @throws GeneralSecurityException if the key cannot be parsed or the provider
|
||||
* does not support ML-DSA
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(MldsaPublicKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG)
|
||||
: KeyFactory.getInstance(ALG, spec.providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Private key import is not supported by this spec.
|
||||
*
|
||||
* @param spec encoded public key spec
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(MldsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use MldsaPrivateKeySpec for private keys.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Encoded representation of an ML-DSA public key.
|
||||
*
|
||||
* <p>
|
||||
* {@code MldsaPublicKeySpec} is an immutable value object that wraps an X.509
|
||||
* (SubjectPublicKeyInfo) encoded ML-DSA public key together with the JCA
|
||||
* provider name that should be used when importing the key.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>The public key bytes are stored as a defensive copy of the provided X.509
|
||||
* byte array.</li>
|
||||
* <li>Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under
|
||||
* {@code "x509.b64"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider selection</h2>
|
||||
* <p>
|
||||
* The default provider is {@code "BC"}.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaPublicKeySpec implements AlgorithmKeySpec {
|
||||
|
||||
private final byte[] encodedX509;
|
||||
private final String providerName;
|
||||
|
||||
/**
|
||||
* Creates a new specification using the default provider {@code "BC"}.
|
||||
*
|
||||
* @param encodedX509 X.509-encoded ML-DSA public key bytes
|
||||
* @throws IllegalArgumentException if {@code encodedX509} is {@code null}
|
||||
*/
|
||||
public MldsaPublicKeySpec(byte[] encodedX509) {
|
||||
this(encodedX509, "BC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification using the supplied provider.
|
||||
*
|
||||
* @param encodedX509 X.509-encoded ML-DSA public key bytes
|
||||
* @param providerName JCA provider name to use for import; may be {@code null}
|
||||
* @throws IllegalArgumentException if {@code encodedX509} is {@code null}
|
||||
*/
|
||||
public MldsaPublicKeySpec(byte[] encodedX509, String providerName) {
|
||||
if (encodedX509 == null) {
|
||||
throw new IllegalArgumentException("encodedX509 must not be null");
|
||||
}
|
||||
this.encodedX509 = encodedX509.clone();
|
||||
this.providerName = (providerName == null ? "BC" : providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the X.509-encoded public key bytes.
|
||||
*
|
||||
* @return a copy of the encoded public key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedX509.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name associated with this specification.
|
||||
*
|
||||
* @return provider name, never {@code null}
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given spec into a {@link PairSeq}.
|
||||
*
|
||||
* @param spec the public key specification to serialize
|
||||
* @return serialized representation
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(MldsaPublicKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedX509);
|
||||
return PairSeq.of("type", "MLDSA-PUB", "x509.b64", b64, "provider", spec.providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a {@link MldsaPublicKeySpec} from a {@link PairSeq}.
|
||||
*
|
||||
* @param p serialized input
|
||||
* @return reconstructed public key specification
|
||||
* @throws IllegalArgumentException if {@code "x509.b64"} is missing
|
||||
* @throws NullPointerException if {@code p} is {@code null}
|
||||
*/
|
||||
public static MldsaPublicKeySpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
String prov = "BC";
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "x509.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "provider" -> prov = v;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("x509.b64 missing for ML-DSA public key");
|
||||
}
|
||||
return new MldsaPublicKeySpec(out, prov);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bouncycastle.jcajce.interfaces.MLDSAPublicKey;
|
||||
import org.bouncycastle.jcajce.spec.MLDSAParameterSpec;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.alg.common.sig.GenericJcaSignatureContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
/**
|
||||
* Streaming signature context for ML-DSA (FIPS 204).
|
||||
*
|
||||
* <p>
|
||||
* {@code MldsaSignatureContext} adapts a JCA {@link Signature} engine for use
|
||||
* within the ZeroEcho streaming signature infrastructure. It supports both
|
||||
* signing and verification and delegates the low-level mechanics to
|
||||
* {@link GenericJcaSignatureContext}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Provider and algorithm</h2>
|
||||
* <ul>
|
||||
* <li>JCA algorithm: {@code "ML-DSA"}.</li>
|
||||
* <li>Provider: {@code "BC"} (Bouncy Castle core provider).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Streaming contract</h2>
|
||||
* <ul>
|
||||
* <li><b>SIGN</b>: the wrapped stream emits the message body and appends a
|
||||
* detached signature trailer at EOF.</li>
|
||||
* <li><b>VERIFY</b>: the wrapped stream emits the body only; verification is
|
||||
* performed at EOF against a caller-supplied expected tag.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaSignatureContext implements SignatureContext {
|
||||
|
||||
private static final String JCA_PURE = "MLDSA";
|
||||
private static final String JCA_SHA512 = "SHA512withMLDSA";
|
||||
private static final String PROVIDER = "BC";
|
||||
|
||||
private static final Pattern PARAM_PATTERN = Pattern.compile("^ml-dsa-(44|65|87)(?:-with-sha512)?$");
|
||||
|
||||
private final GenericJcaSignatureContext delegate;
|
||||
|
||||
/**
|
||||
* Creates a signing context bound to a private key.
|
||||
*
|
||||
* <p>
|
||||
* The produced signature length is resolved by probing the JCA engine via
|
||||
* {@link GenericJcaSignatureContext.SignLengthResolver#probeWith(String, String)}.
|
||||
* </p>
|
||||
*
|
||||
* @param algorithm the parent algorithm instance; must not be {@code null}
|
||||
* @param privateKey ML-DSA private key; must not be {@code null}
|
||||
* @throws GeneralSecurityException if the JCA signature engine cannot be
|
||||
* initialized
|
||||
*/
|
||||
public MldsaSignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey)
|
||||
throws GeneralSecurityException {
|
||||
String jcaAlg = jcaSignatureAlgFromKey(privateKey);
|
||||
this.delegate = new GenericJcaSignatureContext(algorithm, privateKey,
|
||||
GenericJcaSignatureContext.jcaFactory(jcaAlg, PROVIDER),
|
||||
GenericJcaSignatureContext.SignLengthResolver.probeWith(jcaAlg, PROVIDER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verification context bound to a public key.
|
||||
*
|
||||
* <p>
|
||||
* The expected signature length is derived from the public key parameter set
|
||||
* via {@link MLDSAParameterSpec#getName()} and canonical ML-DSA signature
|
||||
* sizes.
|
||||
* </p>
|
||||
*
|
||||
* @param algorithm the parent algorithm instance; must not be {@code null}
|
||||
* @param publicKey ML-DSA public key; must not be {@code null}
|
||||
* @throws GeneralSecurityException if the key is invalid or the parameter set
|
||||
* is unsupported
|
||||
*/
|
||||
public MldsaSignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey)
|
||||
throws GeneralSecurityException {
|
||||
String jcaAlg = jcaSignatureAlgFromKey(publicKey);
|
||||
this.delegate = new GenericJcaSignatureContext(algorithm, publicKey,
|
||||
GenericJcaSignatureContext.jcaFactory(jcaAlg, PROVIDER), MldsaSignatureContext::sigLenFromPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the canonical ML-DSA signature length (bytes) from a public key.
|
||||
*
|
||||
* <p>
|
||||
* Bouncy Castle public keys expose an {@link MLDSAParameterSpec} whose
|
||||
* {@linkplain MLDSAParameterSpec#getName() name} encodes the parameter set. The
|
||||
* resolver normalizes the returned name to lowercase and replaces underscores
|
||||
* with hyphens to tolerate provider naming differences.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Canonical signature sizes:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>ML-DSA-44: 2420 bytes</li>
|
||||
* <li>ML-DSA-65: 3309 bytes</li>
|
||||
* <li>ML-DSA-87: 4627 bytes</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param pk ML-DSA public key
|
||||
* @return signature length in bytes for the key's parameter set
|
||||
* @throws GeneralSecurityException if the key type or parameter specification
|
||||
* is missing or unrecognized
|
||||
*/
|
||||
private static int sigLenFromPublicKey(PublicKey pk) throws GeneralSecurityException {
|
||||
if (!(pk instanceof MLDSAPublicKey mldsaPk)) {
|
||||
throw new GeneralSecurityException("Expected a BouncyCastle ML-DSA public key (BC)");
|
||||
}
|
||||
|
||||
MLDSAParameterSpec ps = mldsaPk.getParameterSpec();
|
||||
if (ps == null) {
|
||||
throw new GeneralSecurityException("Missing ML-DSA parameter spec on public key");
|
||||
}
|
||||
|
||||
String name = ps.getName();
|
||||
if (name == null || name.isEmpty()) {
|
||||
throw new GeneralSecurityException("Unknown ML-DSA parameter (no name)");
|
||||
}
|
||||
|
||||
String normalized = name.toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
|
||||
// Some providers may return "ML-DSA-65" (normalized => "ml-dsa-65").
|
||||
// Others may include "with" variants. We accept only the standard sets
|
||||
// 44/65/87.
|
||||
Matcher m = PARAM_PATTERN.matcher(normalized);
|
||||
if (!m.matches()) {
|
||||
throw new GeneralSecurityException("Cannot parse ML-DSA parameter from: " + name);
|
||||
}
|
||||
|
||||
int set = Integer.parseInt(m.group(1));
|
||||
return switch (set) {
|
||||
case 44 -> 2_420;
|
||||
case 65 -> 3_309;
|
||||
case 87 -> 4_627;
|
||||
default -> throw new GeneralSecurityException("Unsupported ML-DSA parameter set: " + set);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent {@link CryptoAlgorithm} associated with this context.
|
||||
*
|
||||
* @return the algorithm instance
|
||||
*/
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return delegate.algorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key bound to this context.
|
||||
*
|
||||
* @return signing {@link PrivateKey} or verification {@link PublicKey}
|
||||
*/
|
||||
@Override
|
||||
public java.security.Key key() {
|
||||
return delegate.key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this context and releases any underlying resources.
|
||||
*
|
||||
* <p>
|
||||
* Once closed, the context must not be reused.
|
||||
* </p>
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an input stream such that bytes read from the returned stream update
|
||||
* the underlying signature engine.
|
||||
*
|
||||
* @param upstream input stream providing message bytes; must not be
|
||||
* {@code null}
|
||||
* @return wrapped stream that performs signing or verification as bytes are
|
||||
* read
|
||||
* @throws IOException if wrapping fails
|
||||
*/
|
||||
@Override
|
||||
public InputStream wrap(InputStream upstream) throws IOException {
|
||||
return delegate.wrap(upstream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature (tag) length in bytes for the parameter set in use.
|
||||
*
|
||||
* @return signature length in bytes
|
||||
*/
|
||||
@Override
|
||||
public int tagLength() {
|
||||
return delegate.tagLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies the expected signature (tag) for VERIFY mode.
|
||||
*
|
||||
* @param expected expected signature bytes; must not be {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void setExpectedTag(byte[] expected) {
|
||||
delegate.setExpectedTag(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the verification approach used at EOF to compare the computed and
|
||||
* expected signatures.
|
||||
*
|
||||
* @param strategy verification predicate; must not be {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void setVerificationApproach(VerificationBiPredicate<Signature> strategy) {
|
||||
delegate.setVerificationApproach(strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the verification predicate core that can be used to select a mismatch
|
||||
* handling strategy.
|
||||
*
|
||||
* @return the verification predicate core
|
||||
*/
|
||||
@Override
|
||||
public VerificationBiPredicate<Signature> getVerificationCore() {
|
||||
return delegate.getVerificationCore();
|
||||
}
|
||||
|
||||
private static String jcaSignatureAlgFromKey(java.security.Key key) throws GeneralSecurityException {
|
||||
if (key instanceof org.bouncycastle.jcajce.interfaces.MLDSAKey mldsaKey) {
|
||||
MLDSAParameterSpec ps = mldsaKey.getParameterSpec();
|
||||
if (ps == null || ps.getName() == null || ps.getName().isEmpty()) {
|
||||
throw new GeneralSecurityException("Missing ML-DSA parameter spec on key");
|
||||
}
|
||||
String n = ps.getName().toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
if (n.contains("with-sha512")) {
|
||||
return JCA_SHA512;
|
||||
}
|
||||
return JCA_PURE;
|
||||
}
|
||||
|
||||
// Fallback: rely on algorithm string if not a BC-native key.
|
||||
String a = key.getAlgorithm();
|
||||
if (a != null && a.toLowerCase(Locale.ROOT).contains("sha512")) {
|
||||
return JCA_SHA512;
|
||||
}
|
||||
return JCA_PURE;
|
||||
}
|
||||
}
|
||||
78
lib/src/main/java/zeroecho/core/alg/mldsa/package-info.java
Normal file
78
lib/src/main/java/zeroecho/core/alg/mldsa/package-info.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* ML-DSA (FIPS 204) signature algorithm binding.
|
||||
*
|
||||
* <p>
|
||||
* This package provides the ZeroEcho binding for <em>ML-DSA</em>
|
||||
* (Module-Lattice-Based Digital Signature Algorithm), standardized by NIST as
|
||||
* FIPS 204. ML-DSA is derived from CRYSTALS-Dilithium and is exposed here
|
||||
* as a standards-compliant algorithm identity with a restricted parameter
|
||||
* space.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Algorithm identity</h2>
|
||||
* <ul>
|
||||
* <li>JCA algorithm name: {@code "ML-DSA"}</li>
|
||||
* <li>ZeroEcho algorithm id: {@code "MLDSA"}</li>
|
||||
* <li>Provider: Bouncy Castle core provider ({@code "BC"})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Supported parameter sets</h2>
|
||||
* <p>
|
||||
* The supported parameter sets are those defined by FIPS 204: ML-DSA-44,
|
||||
* ML-DSA-65, and ML-DSA-87. Bouncy Castle exposes these via
|
||||
* {@code org.bouncycastle.jcajce.spec.MLDSAParameterSpec}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Streaming signature model</h2>
|
||||
* <ul>
|
||||
* <li>In {@link zeroecho.core.KeyUsage#SIGN} mode, the signature is appended as
|
||||
* a fixed-length trailer to the output stream.</li>
|
||||
* <li>In {@link zeroecho.core.KeyUsage#VERIFY} mode, the wrapped stream emits
|
||||
* only the message body and performs verification at end-of-stream against a
|
||||
* caller-supplied expected signature.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <ul>
|
||||
* <li>No sensitive material (private keys, signatures, message contents) is
|
||||
* logged by this package.</li>
|
||||
* <li>All externally returned byte arrays are defensive copies.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.core.alg.mldsa;
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -213,8 +214,8 @@ public final class NtruKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
NTRUKEMGenerator gen = new NTRUKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -250,7 +251,7 @@ public final class NtruKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
NTRUKEMExtractor ex = new NTRUKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("NTRU decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -175,8 +176,8 @@ public final class NtrulPrimeKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
NTRULPRimeKEMGenerator gen = new NTRULPRimeKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -212,7 +213,7 @@ public final class NtrulPrimeKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
NTRULPRimeKEMExtractor ex = new NTRULPRimeKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("NTRULPRime decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -181,8 +182,8 @@ public final class SntruPrimeKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
SNTRUPrimeKEMGenerator gen = new SNTRUPrimeKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -218,7 +219,7 @@ public final class SntruPrimeKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
SNTRUPrimeKEMExtractor ex = new SNTRUPrimeKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("SNTRUPrime decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
@@ -182,8 +183,8 @@ public final class SaberKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
SABERKEMGenerator gen = new SABERKEMGenerator(new SecureRandom());
|
||||
SecretWithEncapsulation res = gen.generateEncapsulated(keyParam);
|
||||
byte[] secret = res.getSecret();
|
||||
byte[] ct = res.getEncapsulation();
|
||||
byte[] secret = Arrays.copyOf(res.getSecret(), res.getSecret().length);
|
||||
byte[] ct = Arrays.copyOf(res.getEncapsulation(), res.getEncapsulation().length);
|
||||
res.destroy();
|
||||
return new KemResult(ct, secret);
|
||||
} catch (DestroyFailedException e) {
|
||||
@@ -217,7 +218,7 @@ public final class SaberKemContext implements KemContext {
|
||||
.createKey(key.getEncoded());
|
||||
SABERKEMExtractor ex = new SABERKEMExtractor(keyParam);
|
||||
return ex.extractSecret(ciphertext);
|
||||
} catch (Exception e) { // NOPMD
|
||||
} catch (Exception e) {
|
||||
throw new IOException("SABER decapsulate failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
106
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java
Normal file
106
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java
Normal file
@@ -0,0 +1,106 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
|
||||
/**
|
||||
* SLH-DSA (FIPS 205) signature algorithm binding for the ZeroEcho framework.
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is the NIST-standardized profile of the SPHINCS+ stateless hash-based
|
||||
* signature scheme. This binding exposes SLH-DSA as a first-class algorithm
|
||||
* identity while relying on the provider's SLH-DSA JCA implementations.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This algorithm registers two roles:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link KeyUsage#SIGN}: produces SLH-DSA signatures using a
|
||||
* {@link PrivateKey}.</li>
|
||||
* <li>{@link KeyUsage#VERIFY}: verifies SLH-DSA signatures using a
|
||||
* {@link PublicKey}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Both roles are configured with {@link VoidSpec}, as SLH-DSA requires no
|
||||
* runtime context parameters beyond the key material.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaAlgorithm extends AbstractCryptoAlgorithm {
|
||||
|
||||
/**
|
||||
* Creates a new SLH-DSA algorithm instance and registers its capabilities.
|
||||
*
|
||||
* @throws IllegalArgumentException if a signature context cannot be initialized
|
||||
* due to provider errors
|
||||
*/
|
||||
public SlhDsaAlgorithm() {
|
||||
super("SLH-DSA", "SLHDSA");
|
||||
|
||||
capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, VoidSpec.class,
|
||||
(PrivateKey k, VoidSpec s) -> {
|
||||
try {
|
||||
return new SlhDsaSignatureContext(this, k);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Cannot init SLH-DSA signer", e);
|
||||
}
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, VoidSpec.class,
|
||||
(PublicKey k, VoidSpec s) -> {
|
||||
try {
|
||||
return new SlhDsaSignatureContext(this, k);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Cannot init SLH-DSA verifier", e);
|
||||
}
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(SlhDsaKeyGenSpec.class, new SlhDsaKeyGenBuilder(), SlhDsaKeyGenSpec::defaultSpec);
|
||||
registerAsymmetricKeyBuilder(SlhDsaPublicKeySpec.class, new SlhDsaPublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(SlhDsaPrivateKeySpec.class, new SlhDsaPrivateKeyBuilder(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Key pair builder for SLH-DSA (FIPS 205) using the Bouncy Castle PQC provider.
|
||||
*
|
||||
* <p>
|
||||
* This builder maps {@link SlhDsaKeyGenSpec} to the appropriate
|
||||
* {@code org.bouncycastle.jcajce.spec.SLHDSAParameterSpec} constant.
|
||||
* :contentReference[oaicite:3]{index=3} Reflection is used to avoid a hard
|
||||
* dependency on any particular set of parameter constants across provider
|
||||
* versions.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaKeyGenBuilder implements AsymmetricKeyBuilder<SlhDsaKeyGenSpec> {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(SlhDsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
Object bcParamSpec = resolveBcParameterSpec(spec);
|
||||
|
||||
KeyPairGenerator kpg = (spec.providerName() == null) ? KeyPairGenerator.getInstance(ALG)
|
||||
: KeyPairGenerator.getInstance(ALG, spec.providerName());
|
||||
|
||||
if (bcParamSpec != null) {
|
||||
kpg.initialize((java.security.spec.AlgorithmParameterSpec) bcParamSpec);
|
||||
}
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.security.PublicKey importPublic(SlhDsaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPublicKeySpec to import a public key.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.security.PrivateKey importPrivate(SlhDsaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPrivateKeySpec to import a private key.");
|
||||
}
|
||||
|
||||
private static Object resolveBcParameterSpec(SlhDsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
if (spec.explicitParamConstant() != null) {
|
||||
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.SLHDSAParameterSpec",
|
||||
spec.explicitParamConstant());
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
throw new GeneralSecurityException("Unknown SLHDSAParameterSpec constant: " + spec.explicitParamConstant());
|
||||
}
|
||||
|
||||
String fam = (spec.hash() == SlhDsaKeyGenSpec.Hash.SHA2) ? "sha2" : "shake";
|
||||
String bits = Integer.toString(spec.security().bits);
|
||||
String v = (spec.variant() == SlhDsaKeyGenSpec.Variant.FAST) ? "f" : "s";
|
||||
|
||||
// Base constant name: slh_dsa_{sha2|shake}_{128|192|256}{f|s}
|
||||
String base = "slh_dsa_" + fam + "_" + bits + v;
|
||||
|
||||
// Optional pre-hash suffix used by BC:
|
||||
// _with_sha256/_with_sha512/_with_shake128/_with_shake256
|
||||
// :contentReference[oaicite:4]{index=4}
|
||||
String suffix = "";
|
||||
if (spec.preHash() != SlhDsaKeyGenSpec.PreHash.NONE) {
|
||||
suffix = "_with_" + spec.preHash().name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
String name = base + suffix;
|
||||
|
||||
// Validate supported combinations (fail fast with a clear message).
|
||||
validateCombination(spec);
|
||||
|
||||
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.SLHDSAParameterSpec", name);
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
// As a fallback, attempt base (no pre-hash), then fail.
|
||||
if (!suffix.isEmpty()) {
|
||||
Object baseC = fetchStaticField("org.bouncycastle.jcajce.spec.SLHDSAParameterSpec", base);
|
||||
if (baseC != null) {
|
||||
return baseC;
|
||||
}
|
||||
}
|
||||
|
||||
throw new GeneralSecurityException("Cannot resolve SLHDSAParameterSpec constant: " + name);
|
||||
}
|
||||
|
||||
private static void validateCombination(SlhDsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
SlhDsaKeyGenSpec.Hash h = spec.hash();
|
||||
int bits = spec.security().bits;
|
||||
SlhDsaKeyGenSpec.PreHash p = spec.preHash();
|
||||
|
||||
if (p == SlhDsaKeyGenSpec.PreHash.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (h == SlhDsaKeyGenSpec.Hash.SHA2) {
|
||||
if (bits == 128 && p != SlhDsaKeyGenSpec.PreHash.SHA256) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHA2-128 supports only PreHash.SHA256.");
|
||||
}
|
||||
if ((bits == 192 || bits == 256) && p != SlhDsaKeyGenSpec.PreHash.SHA512) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHA2-192/256 support only PreHash.SHA512.");
|
||||
}
|
||||
} else {
|
||||
if (bits == 128 && p != SlhDsaKeyGenSpec.PreHash.SHAKE128) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHAKE-128 supports only PreHash.SHAKE128.");
|
||||
}
|
||||
if ((bits == 192 || bits == 256) && p != SlhDsaKeyGenSpec.PreHash.SHAKE256) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHAKE-192/256 support only PreHash.SHAKE256.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Object fetchStaticField(String className, String field) {
|
||||
try {
|
||||
Class<?> cls = Class.forName(className);
|
||||
Field f = cls.getField(field);
|
||||
return f.get(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
249
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java
Normal file
249
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java
Normal file
@@ -0,0 +1,249 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Specification for generating SLH-DSA key pairs (FIPS 205).
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is parameterized by hash family (SHA2 or SHAKE), a security strength
|
||||
* (128/192/256), and a variant (fast vs small). Additionally, Bouncy Castle
|
||||
* exposes pre-hash variants (e.g., "with SHA-256") as separate parameter specs.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This spec intentionally restricts the available choices to SLH-DSA parameters
|
||||
* (i.e., no Haraka and no SPHINCS+ "simple/robust" split).
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaKeyGenSpec implements AlgorithmKeySpec {
|
||||
|
||||
/**
|
||||
* SLH-DSA hash families (FIPS 205).
|
||||
*/
|
||||
public enum Hash {
|
||||
/** SHA-2 based SLH-DSA parameter sets. */
|
||||
SHA2,
|
||||
/** SHAKE based SLH-DSA parameter sets. */
|
||||
SHAKE
|
||||
}
|
||||
|
||||
/**
|
||||
* Security levels as defined by NIST PQC (L1, L3, L5).
|
||||
*/
|
||||
public enum Security {
|
||||
/** NIST Level 1 (~128-bit security). */
|
||||
L1_128(128),
|
||||
/** NIST Level 3 (~192-bit security). */
|
||||
L3_192(192),
|
||||
/** NIST Level 5 (~256-bit security). */
|
||||
L5_256(256);
|
||||
|
||||
/** Claimed security level in bits. */
|
||||
public final int bits;
|
||||
|
||||
Security(int b) {
|
||||
bits = b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant trading performance against signature size.
|
||||
*
|
||||
* <p>
|
||||
* {@code FAST} variants are optimized for speed (larger signatures).
|
||||
* {@code SMALL} variants reduce signature size at some performance cost.
|
||||
* </p>
|
||||
*/
|
||||
public enum Variant {
|
||||
/** Larger, faster signatures. */
|
||||
FAST,
|
||||
/** Smaller, slower signatures. */
|
||||
SMALL
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional pre-hash variant selection.
|
||||
*
|
||||
* <p>
|
||||
* Bouncy Castle exposes "with hash" variants as distinct parameter specs, for
|
||||
* example {@code slh_dsa_sha2_128s_with_sha256}.
|
||||
* :contentReference[oaicite:1]{index=1}
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Valid combinations depend on the hash family and security strength:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>SHA2-128 uses SHA-256</li>
|
||||
* <li>SHA2-192/256 use SHA-512</li>
|
||||
* <li>SHAKE-128 uses SHAKE128</li>
|
||||
* <li>SHAKE-192/256 use SHAKE256</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum PreHash {
|
||||
/** No pre-hash parameter set (pure SLH-DSA). */
|
||||
NONE,
|
||||
/** Pre-hash with SHA-256 (only for SHA2-128). */
|
||||
SHA256,
|
||||
/** Pre-hash with SHA-512 (only for SHA2-192/256). */
|
||||
SHA512,
|
||||
/** Pre-hash with SHAKE128 (only for SHAKE-128). */
|
||||
SHAKE128,
|
||||
/** Pre-hash with SHAKE256 (only for SHAKE-192/256). */
|
||||
SHAKE256
|
||||
}
|
||||
|
||||
private static final SlhDsaKeyGenSpec DEFAULT = new SlhDsaKeyGenSpec("BC", Hash.SHAKE, Security.L5_256,
|
||||
Variant.SMALL, PreHash.NONE, null);
|
||||
|
||||
private final String providerName;
|
||||
private final Hash hash;
|
||||
private final Security security;
|
||||
private final Variant variant;
|
||||
private final PreHash preHash;
|
||||
private final String explicitParamConstant; // nullable
|
||||
|
||||
private SlhDsaKeyGenSpec(String providerName, Hash hash, Security security, Variant variant, PreHash preHash,
|
||||
String explicitParamConstant) {
|
||||
this.providerName = Objects.requireNonNull(providerName, "providerName");
|
||||
this.hash = Objects.requireNonNull(hash, "hash");
|
||||
this.security = Objects.requireNonNull(security, "security");
|
||||
this.variant = Objects.requireNonNull(variant, "variant");
|
||||
this.preHash = Objects.requireNonNull(preHash, "preHash");
|
||||
this.explicitParamConstant = explicitParamConstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default SLH-DSA key generation spec.
|
||||
*
|
||||
* @return a singleton default specification (SHAKE, L5, SMALL, no pre-hash)
|
||||
*/
|
||||
public static SlhDsaKeyGenSpec defaultSpec() {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification with explicit algorithm parameters.
|
||||
*
|
||||
* @param providerName JCA provider name (e.g., "BC", "BCPQC")
|
||||
* @param hash hash family (SHA2 or SHAKE)
|
||||
* @param security security level
|
||||
* @param variant variant (FAST vs SMALL)
|
||||
* @param preHash optional pre-hash selection
|
||||
* @return a new {@code SlhDsaKeyGenSpec}
|
||||
*/
|
||||
public static SlhDsaKeyGenSpec of(String providerName, Hash hash, Security security, Variant variant,
|
||||
PreHash preHash) {
|
||||
return new SlhDsaKeyGenSpec(providerName, hash, security, variant, preHash, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this specification with an explicit Bouncy Castle parameter
|
||||
* constant override.
|
||||
*
|
||||
* <p>
|
||||
* This bypasses automatic mapping. The name must match a static field in
|
||||
* {@code org.bouncycastle.jcajce.spec.SLHDSAParameterSpec}.
|
||||
* :contentReference[oaicite:2]{index=2}
|
||||
* </p>
|
||||
*
|
||||
* @param name field name in {@code SLHDSAParameterSpec}
|
||||
* @return a new {@code SlhDsaKeyGenSpec} with the override
|
||||
*/
|
||||
public SlhDsaKeyGenSpec withExplicitParamConstant(String name) {
|
||||
return new SlhDsaKeyGenSpec(providerName, hash, security, variant, preHash, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name used for key generation.
|
||||
*
|
||||
* @return provider name (never {@code null})
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SLH-DSA hash family.
|
||||
*
|
||||
* @return hash family
|
||||
*/
|
||||
public Hash hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the security level.
|
||||
*
|
||||
* @return security level
|
||||
*/
|
||||
public Security security() {
|
||||
return security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the variant.
|
||||
*
|
||||
* @return variant
|
||||
*/
|
||||
public Variant variant() {
|
||||
return variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pre-hash selection.
|
||||
*
|
||||
* @return pre-hash selection
|
||||
*/
|
||||
public PreHash preHash() {
|
||||
return preHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the explicit parameter constant override, if set.
|
||||
*
|
||||
* @return constant name, or {@code null} if automatic mapping is used
|
||||
*/
|
||||
public String explicitParamConstant() {
|
||||
return explicitParamConstant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Builder for importing SLH-DSA private keys from encoded specifications.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPrivateKeyBuilder implements AsymmetricKeyBuilder<SlhDsaPrivateKeySpec> {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(SlhDsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(SlhDsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPublicKeySpec for public keys.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(SlhDsaPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG)
|
||||
: KeyFactory.getInstance(ALG, spec.providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Encoded representation of an SLH-DSA private key.
|
||||
*
|
||||
* <p>
|
||||
* {@code SlhDsaPrivateKeySpec} is an immutable value object that wraps a
|
||||
* PKCS#8-encoded SLH-DSA private key together with the JCA provider name that
|
||||
* should be used when importing the key.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>The private key material is stored as a defensive copy of the provided
|
||||
* PKCS#8 byte array.</li>
|
||||
* <li>Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under
|
||||
* {@code "pkcs8.b64"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider selection</h2>
|
||||
* <p>
|
||||
* The default provider is {@code "BC"} because SLH-DSA is registered by the
|
||||
* Bouncy Castle core provider (as opposed to the PQC-only provider).
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <ul>
|
||||
* <li>All byte arrays returned by this class are defensive copies.</li>
|
||||
* <li>This class performs no cryptographic operations.</li>
|
||||
* <li>Callers must protect serialized private key material at rest and in
|
||||
* transit.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
* <p>
|
||||
* Instances are immutable and therefore thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
|
||||
private final byte[] encodedPkcs8;
|
||||
private final String providerName;
|
||||
|
||||
/**
|
||||
* Creates a new specification using the default provider {@code "BC"}.
|
||||
*
|
||||
* @param encodedPkcs8 PKCS#8-encoded SLH-DSA private key bytes
|
||||
* @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null}
|
||||
*/
|
||||
public SlhDsaPrivateKeySpec(byte[] encodedPkcs8) {
|
||||
this(encodedPkcs8, "BC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification using the supplied provider.
|
||||
*
|
||||
* <p>
|
||||
* If {@code providerName} is {@code null}, the default provider {@code "BC"} is
|
||||
* used.
|
||||
* </p>
|
||||
*
|
||||
* @param encodedPkcs8 PKCS#8-encoded SLH-DSA private key bytes
|
||||
* @param providerName JCA provider name to use for import; may be {@code null}
|
||||
* @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null}
|
||||
*/
|
||||
public SlhDsaPrivateKeySpec(byte[] encodedPkcs8, String providerName) {
|
||||
if (encodedPkcs8 == null) {
|
||||
throw new IllegalArgumentException("encodedPkcs8 must not be null");
|
||||
}
|
||||
this.encodedPkcs8 = encodedPkcs8.clone();
|
||||
this.providerName = (providerName == null ? "BC" : providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the PKCS#8-encoded private key bytes.
|
||||
*
|
||||
* @return a copy of the encoded private key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedPkcs8.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name associated with this specification.
|
||||
*
|
||||
* @return provider name, never {@code null}
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given spec into a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* The encoded key is stored as Base64 without padding under
|
||||
* {@code "pkcs8.b64"}. The provider is stored under {@code "provider"}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec the private key specification to serialize
|
||||
* @return serialized representation
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(SlhDsaPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
|
||||
return PairSeq.of("type", "SLHDSA-PRIV", "pkcs8.b64", b64, "provider", spec.providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a {@link SlhDsaPrivateKeySpec} from a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* Expects key {@code "pkcs8.b64"} (required) and {@code "provider"} (optional;
|
||||
* defaults to {@code "BC"}).
|
||||
* </p>
|
||||
*
|
||||
* @param p serialized input
|
||||
* @return reconstructed private key specification
|
||||
* @throws IllegalArgumentException if {@code "pkcs8.b64"} is missing
|
||||
* @throws NullPointerException if {@code p} is {@code null}
|
||||
*/
|
||||
public static SlhDsaPrivateKeySpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
String prov = "BC";
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "pkcs8.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "provider" -> prov = v;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for SLH-DSA private key");
|
||||
}
|
||||
return new SlhDsaPrivateKeySpec(out, prov);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Builder for importing SLH-DSA public keys from encoded specifications.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPublicKeyBuilder implements AsymmetricKeyBuilder<SlhDsaPublicKeySpec> {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(SlhDsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(SlhDsaPublicKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG)
|
||||
: KeyFactory.getInstance(ALG, spec.providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(SlhDsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPrivateKeySpec for private keys.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Encoded representation of an SLH-DSA public key.
|
||||
*
|
||||
* <p>
|
||||
* {@code SlhDsaPublicKeySpec} is an immutable value object that wraps an X.509
|
||||
* (SubjectPublicKeyInfo) encoded SLH-DSA public key together with the JCA
|
||||
* provider name that should be used when importing the key.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>The public key bytes are stored as a defensive copy of the provided X.509
|
||||
* byte array.</li>
|
||||
* <li>Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under
|
||||
* {@code "x509.b64"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider selection</h2>
|
||||
* <p>
|
||||
* The default provider is {@code "BC"} because SLH-DSA is registered by the
|
||||
* Bouncy Castle core provider.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
* <p>
|
||||
* Instances are immutable and can be safely shared across threads.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPublicKeySpec implements AlgorithmKeySpec {
|
||||
|
||||
private final byte[] encodedX509;
|
||||
private final String providerName;
|
||||
|
||||
/**
|
||||
* Creates a new specification using the default provider {@code "BC"}.
|
||||
*
|
||||
* @param encodedX509 X.509-encoded SLH-DSA public key bytes
|
||||
* @throws IllegalArgumentException if {@code encodedX509} is {@code null}
|
||||
*/
|
||||
public SlhDsaPublicKeySpec(byte[] encodedX509) {
|
||||
this(encodedX509, "BC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification using the supplied provider.
|
||||
*
|
||||
* <p>
|
||||
* If {@code providerName} is {@code null}, the default provider {@code "BC"} is
|
||||
* used.
|
||||
* </p>
|
||||
*
|
||||
* @param encodedX509 X.509-encoded SLH-DSA public key bytes
|
||||
* @param providerName JCA provider name to use for import; may be {@code null}
|
||||
* @throws IllegalArgumentException if {@code encodedX509} is {@code null}
|
||||
*/
|
||||
public SlhDsaPublicKeySpec(byte[] encodedX509, String providerName) {
|
||||
if (encodedX509 == null) {
|
||||
throw new IllegalArgumentException("encodedX509 must not be null");
|
||||
}
|
||||
this.encodedX509 = encodedX509.clone();
|
||||
this.providerName = (providerName == null ? "BC" : providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the X.509-encoded public key bytes.
|
||||
*
|
||||
* @return a copy of the encoded public key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedX509.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name associated with this specification.
|
||||
*
|
||||
* @return provider name, never {@code null}
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given spec into a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* The encoded key is stored as Base64 without padding under {@code "x509.b64"}.
|
||||
* The provider is stored under {@code "provider"}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec the public key specification to serialize
|
||||
* @return serialized representation
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(SlhDsaPublicKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedX509);
|
||||
return PairSeq.of("type", "SLHDSA-PUB", "x509.b64", b64, "provider", spec.providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a {@link SlhDsaPublicKeySpec} from a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* Expects key {@code "x509.b64"} (required) and {@code "provider"} (optional;
|
||||
* defaults to {@code "BC"}).
|
||||
* </p>
|
||||
*
|
||||
* @param p serialized input
|
||||
* @return reconstructed public key specification
|
||||
* @throws IllegalArgumentException if {@code "x509.b64"} is missing
|
||||
* @throws NullPointerException if {@code p} is {@code null}
|
||||
*/
|
||||
public static SlhDsaPublicKeySpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
String prov = "BC";
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "x509.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "provider" -> prov = v;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("x509.b64 missing for SLH-DSA public key");
|
||||
}
|
||||
return new SlhDsaPublicKeySpec(out, prov);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bouncycastle.jcajce.interfaces.SLHDSAPublicKey;
|
||||
import org.bouncycastle.jcajce.spec.SLHDSAParameterSpec;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.alg.common.sig.GenericJcaSignatureContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
/**
|
||||
* Streaming signature context for SLH-DSA (FIPS 205).
|
||||
*
|
||||
* <p>
|
||||
* {@code SlhDsaSignatureContext} adapts a JCA {@link Signature} engine for use
|
||||
* within the ZeroEcho streaming signature infrastructure. It supports both
|
||||
* signing and verification and delegates the low-level mechanics to
|
||||
* {@link GenericJcaSignatureContext}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Provider and algorithm</h2>
|
||||
* <ul>
|
||||
* <li>JCA algorithm: {@code "SLH-DSA"}.</li>
|
||||
* <li>Provider: {@code "BC"} (Bouncy Castle core provider).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Streaming contract</h2>
|
||||
* <ul>
|
||||
* <li><b>SIGN</b>: the wrapped stream emits the message body and appends a
|
||||
* detached signature trailer at EOF.</li>
|
||||
* <li><b>VERIFY</b>: the wrapped stream emits the body only; verification is
|
||||
* performed at EOF against a caller-supplied expected tag.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Security</h2>
|
||||
* <p>
|
||||
* This class never logs secrets, key material, plaintext, or signature bytes.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaSignatureContext implements SignatureContext {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
private static final String PROVIDER = "BC";
|
||||
|
||||
private static final Pattern PARAM_PATTERN = Pattern
|
||||
.compile("^slh-dsa-(sha2|shake)-(128|192|256)([fs])(?:-with-[a-z0-9]+)?$");
|
||||
|
||||
private final GenericJcaSignatureContext delegate;
|
||||
|
||||
/**
|
||||
* Creates a signing context bound to a private key.
|
||||
*
|
||||
* <p>
|
||||
* The produced signature length is resolved by probing the JCA engine via
|
||||
* {@link GenericJcaSignatureContext.SignLengthResolver#probeWith(String, String)}.
|
||||
* </p>
|
||||
*
|
||||
* @param algorithm the parent algorithm instance; must not be {@code null}
|
||||
* @param privateKey SLH-DSA private key; must not be {@code null}
|
||||
* @throws GeneralSecurityException if the JCA signature engine cannot be
|
||||
* initialized
|
||||
*/
|
||||
public SlhDsaSignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey)
|
||||
throws GeneralSecurityException {
|
||||
this.delegate = new GenericJcaSignatureContext(algorithm, privateKey,
|
||||
GenericJcaSignatureContext.jcaFactory(ALG, PROVIDER),
|
||||
GenericJcaSignatureContext.SignLengthResolver.probeWith(ALG, PROVIDER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verification context bound to a public key.
|
||||
*
|
||||
* <p>
|
||||
* The expected signature length is derived from the public key parameter set
|
||||
* via {@link SLHDSAParameterSpec#getName()} and the canonical SLH-DSA sizes.
|
||||
* </p>
|
||||
*
|
||||
* @param algorithm the parent algorithm instance; must not be {@code null}
|
||||
* @param publicKey SLH-DSA public key; must not be {@code null}
|
||||
* @throws GeneralSecurityException if the key is invalid or the parameter set
|
||||
* is unsupported
|
||||
*/
|
||||
public SlhDsaSignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey)
|
||||
throws GeneralSecurityException {
|
||||
this.delegate = new GenericJcaSignatureContext(algorithm, publicKey,
|
||||
GenericJcaSignatureContext.jcaFactory(ALG, PROVIDER), SlhDsaSignatureContext::sigLenFromPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the canonical SLH-DSA signature length (bytes) from a public key.
|
||||
*
|
||||
* <p>
|
||||
* The Bouncy Castle public key exposes an {@link SLHDSAParameterSpec} whose
|
||||
* {@linkplain SLHDSAParameterSpec#getName() name} encodes the family
|
||||
* (SHA2/SHAKE), security level (128/192/256) and variant (s/f). The resolver
|
||||
* normalizes the returned name to lowercase and replaces underscores with
|
||||
* hyphens to tolerate provider naming differences.
|
||||
* </p>
|
||||
*
|
||||
* @param pk SLH-DSA public key
|
||||
* @return signature length in bytes for the key's parameter set
|
||||
* @throws GeneralSecurityException if the key type or parameter specification
|
||||
* is missing or unrecognized
|
||||
*/
|
||||
private static int sigLenFromPublicKey(PublicKey pk) throws GeneralSecurityException {
|
||||
if (!(pk instanceof SLHDSAPublicKey slhPk)) {
|
||||
throw new GeneralSecurityException("Expected a BouncyCastle SLH-DSA public key (BC)");
|
||||
}
|
||||
SLHDSAParameterSpec ps = slhPk.getParameterSpec();
|
||||
if (ps == null) {
|
||||
throw new GeneralSecurityException("Missing SLH-DSA parameter spec on public key");
|
||||
}
|
||||
String name = ps.getName();
|
||||
if (name == null || name.isEmpty()) {
|
||||
throw new GeneralSecurityException("Unknown SLH-DSA parameter (no name)");
|
||||
}
|
||||
|
||||
String normalized = name.toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
|
||||
Matcher m = PARAM_PATTERN.matcher(normalized);
|
||||
if (!m.matches()) {
|
||||
throw new GeneralSecurityException("Cannot parse SLH-DSA parameter from: " + name);
|
||||
}
|
||||
|
||||
int level = Integer.parseInt(m.group(2));
|
||||
char var = m.group(3).charAt(0);
|
||||
boolean isSmall = var == 's';
|
||||
|
||||
return switch (level) {
|
||||
case 128 -> isSmall ? 7_856 : 17_088;
|
||||
case 192 -> isSmall ? 16_224 : 35_664;
|
||||
case 256 -> isSmall ? 29_792 : 49_856;
|
||||
default -> throw new GeneralSecurityException("Unsupported SLH-DSA level: " + level);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent {@link CryptoAlgorithm} associated with this context.
|
||||
*
|
||||
* @return the algorithm instance
|
||||
*/
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return delegate.algorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key bound to this context.
|
||||
*
|
||||
* @return signing {@link PrivateKey} or verification {@link PublicKey}
|
||||
*/
|
||||
@Override
|
||||
public java.security.Key key() {
|
||||
return delegate.key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this context and releases any underlying resources.
|
||||
*
|
||||
* <p>
|
||||
* Once closed, the context must not be reused.
|
||||
* </p>
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an input stream such that bytes read from the returned stream update
|
||||
* the underlying signature engine.
|
||||
*
|
||||
* <p>
|
||||
* In SIGN mode, the wrapper appends the signature trailer at EOF. In VERIFY
|
||||
* mode, the wrapper compares the computed signature at EOF against the expected
|
||||
* tag configured via {@link #setExpectedTag(byte[])}.
|
||||
* </p>
|
||||
*
|
||||
* @param upstream input stream providing message bytes; must not be
|
||||
* {@code null}
|
||||
* @return wrapped stream that performs signing or verification as bytes are
|
||||
* read
|
||||
* @throws IOException if wrapping fails
|
||||
*/
|
||||
@Override
|
||||
public InputStream wrap(InputStream upstream) throws IOException {
|
||||
return delegate.wrap(upstream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature (tag) length in bytes for the parameter set in use.
|
||||
*
|
||||
* @return signature length in bytes
|
||||
*/
|
||||
@Override
|
||||
public int tagLength() {
|
||||
return delegate.tagLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies the expected signature (tag) for VERIFY mode.
|
||||
*
|
||||
* <p>
|
||||
* Implementations may defensively copy the provided array. Callers should treat
|
||||
* this value as sensitive and avoid logging or persisting it unless explicitly
|
||||
* required by the application.
|
||||
* </p>
|
||||
*
|
||||
* @param expected expected signature bytes; must not be {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void setExpectedTag(byte[] expected) {
|
||||
delegate.setExpectedTag(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the verification approach used at EOF to compare the computed and
|
||||
* expected signatures.
|
||||
*
|
||||
* @param strategy verification predicate; must not be {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void setVerificationApproach(VerificationBiPredicate<Signature> strategy) {
|
||||
delegate.setVerificationApproach(strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the verification predicate core that can be used to select a
|
||||
* particular mismatch handling strategy (e.g., throw on mismatch).
|
||||
*
|
||||
* @return the verification predicate core
|
||||
*/
|
||||
@Override
|
||||
public VerificationBiPredicate<Signature> getVerificationCore() {
|
||||
return delegate.getVerificationCore();
|
||||
}
|
||||
}
|
||||
121
lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java
Normal file
121
lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* SLH-DSA (FIPS 205) signature algorithm binding.
|
||||
*
|
||||
* <p>
|
||||
* This package provides a ZeroEcho binding for <em>SLH-DSA</em>, the
|
||||
* NIST-standardized profile of the stateless hash-based signature scheme
|
||||
* SPHINCS+. While SLH-DSA is derived from SPHINCS+, it is exposed here as a
|
||||
* distinct algorithm identity with a restricted, standards-compliant parameter
|
||||
* space.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Algorithm identity</h2>
|
||||
* <ul>
|
||||
* <li>JCA algorithm name: {@code "SLH-DSA"}</li>
|
||||
* <li>ZeroEcho algorithm id: {@code "SLHDSA"}</li>
|
||||
* <li>Provider: Bouncy Castle core provider ({@code "BC"})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Supported parameter space</h2>
|
||||
* <p>
|
||||
* The supported parameters correspond exactly to the SLH-DSA profiles defined
|
||||
* by FIPS 205:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Hash families: SHA2 and SHAKE</li>
|
||||
* <li>Security levels: 128, 192, 256 bits (NIST levels L1, L3, L5)</li>
|
||||
* <li>Variants: {@code FAST} (larger, faster signatures) and {@code SMALL}
|
||||
* (smaller, slower signatures)</li>
|
||||
* <li>Optional pre-hash variants as defined by the standard and exposed by the
|
||||
* provider</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Non-standard SPHINCS+ variants (e.g. Haraka, simple/robust modes) are
|
||||
* intentionally excluded from this package.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Key management</h2>
|
||||
* <ul>
|
||||
* <li>Key generation is configured via
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaKeyGenSpec} and performed by
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaKeyGenBuilder}.</li>
|
||||
* <li>Encoded public and private keys are represented by
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec} and
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec}.</li>
|
||||
* <li>All key specifications are immutable and use defensive copies.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Streaming signature model</h2>
|
||||
* <p>
|
||||
* Signatures are processed through the ZeroEcho streaming signature
|
||||
* infrastructure:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>In {@link zeroecho.core.KeyUsage#SIGN} mode, the signature is appended as
|
||||
* a fixed-length trailer to the output stream.</li>
|
||||
* <li>In {@link zeroecho.core.KeyUsage#VERIFY} mode, the wrapped stream emits
|
||||
* only the message body; verification is performed at end-of-stream against a
|
||||
* caller-supplied expected signature.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The canonical signature length is derived from the public key parameters by
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaSignatureContext} and matches the
|
||||
* standard SLH-DSA signature sizes defined by FIPS 205.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <ul>
|
||||
* <li>No sensitive material (private keys, signatures, message contents) is
|
||||
* logged or exposed by this package.</li>
|
||||
* <li>All byte arrays returned to callers are defensive copies.</li>
|
||||
* <li>Verification failures are surfaced exclusively via the configured
|
||||
* verification strategy.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Relationship to SPHINCS+</h2>
|
||||
* <p>
|
||||
* The {@code slhdsa} package represents the standardized SLH-DSA profile only.
|
||||
* A separate {@code sphincsplus} package may expose the full SPHINCS+ design
|
||||
* space and experimental variants. No API-level equivalence between the two
|
||||
* packages is assumed.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
@@ -46,7 +46,10 @@ import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
@@ -145,6 +148,11 @@ public final class XdhAlgorithm extends AbstractCryptoAlgorithm {
|
||||
XdhSpec.class,
|
||||
(PrivateKey k, XdhSpec s) -> new GenericJcaAgreementContext(this, k, s.keyAgreementName(), null),
|
||||
() -> XdhSpec.X25519);
|
||||
// New capability: MessageAgreementContext over KeyPair
|
||||
capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, KeyPairKey.class,
|
||||
XdhSpec.class, (KeyPairKey k, XdhSpec s) -> new GenericJcaMessageAgreementContext(this, k,
|
||||
s.keyAgreementName(), null, "XDH", null),
|
||||
() -> XdhSpec.X25519);
|
||||
|
||||
registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519);
|
||||
registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
|
||||
@@ -166,7 +166,7 @@ final class SmartContinuousBlockStream extends AbstractChunkTransformInputStream
|
||||
out = outBuf = Arrays.copyOf(outBuf, outOff + finBlockSize); // NOPMD
|
||||
}
|
||||
|
||||
int written = cipher.doFinal(in, inOff, len, out, outOff); // NOPMD
|
||||
int written = cipher.doFinal(in, inOff, len, out, outOff);
|
||||
return written;
|
||||
|
||||
// return cipher.doFinal(in, inOff, len, out, outOff);
|
||||
|
||||
@@ -229,9 +229,9 @@ public abstract class TailStrippingInputStream extends InputStream {
|
||||
emitPos += chunk;
|
||||
if (emitPos == emitLen) {
|
||||
// Emission finished; now it's safe to compact tail to start.
|
||||
if (needCompactAfterEmit) {
|
||||
if (needCompactAfterEmit) { // NOPMD
|
||||
int remaining = accLen - emitLen; // this is the withheld tail (<= tailLen)
|
||||
if (remaining > 0) { // NOPMD
|
||||
if (remaining > 0) {
|
||||
System.arraycopy(window, emitLen, window, 0, remaining);
|
||||
}
|
||||
accLen = remaining;
|
||||
@@ -251,7 +251,7 @@ public abstract class TailStrippingInputStream extends InputStream {
|
||||
// Whatever remains in window are the final tail bytes (<= tailLen).
|
||||
byte[] tail = Arrays.copyOf(window, accLen);
|
||||
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
if (LOG.isLoggable(Level.FINE)) { // NOPMD
|
||||
LOG.log(Level.FINE, "tail found {0}", Strings.toShortString(tail));
|
||||
}
|
||||
tailProcessed = true;
|
||||
@@ -286,7 +286,7 @@ public abstract class TailStrippingInputStream extends InputStream {
|
||||
System.arraycopy(window, emitPos, b, off, chunk);
|
||||
emitPos += chunk;
|
||||
if (emitPos == emitLen) {
|
||||
if (needCompactAfterEmit) {
|
||||
if (needCompactAfterEmit) { // NOPMD
|
||||
int remaining = accLen - emitLen;
|
||||
if (remaining > 0) {
|
||||
System.arraycopy(window, emitLen, window, 0, remaining);
|
||||
|
||||
@@ -70,6 +70,10 @@ import javax.crypto.SecretKey;
|
||||
* {@code key.getAlgorithm()} (for example, 512, 768, 1024 for ML-KEM;
|
||||
* 640/976/1344 for FrodoKEM) or from NIST security levels labeled as L1/L3/L5.
|
||||
* If neither is present, defaults to 128.</li>
|
||||
* <li><b>ML-DSA</b>: estimated from the parameter set markers (44/65/87 mapped
|
||||
* to 128/192/256, or L1/L3/L5).</li>
|
||||
* <li><b>SLH-DSA:</b> parse 128/192/256 from {@code key.getAlgorithm()}
|
||||
* similarly to SPHINCS+; else default 128.</li>
|
||||
* <li><b>SPHINCS+:</b> parses the parameter size 128/192/256 from the algorithm
|
||||
* string; otherwise defaults to 128.</li>
|
||||
* <li><b>EdDSA:</b> returns fixed strengths (Ed25519 -> 128, Ed448 ->
|
||||
@@ -122,6 +126,8 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
||||
private static final Pattern SPHINCS_STRENGTH_PATTERN = Pattern.compile("(128|192|256)");
|
||||
private static final Pattern HMAC_SHA_PATTERN = Pattern.compile("HMAC(?:-)?SHA(?:-)?(1|224|256|384|512)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern SLHDSA_STRENGTH_PATTERN = Pattern.compile("(128|192|256)");
|
||||
private static final Pattern MLDSA_SET_PATTERN = Pattern.compile("(44|65|87)");
|
||||
|
||||
private SecurityStrengthAdvisor() {
|
||||
}
|
||||
@@ -153,6 +159,7 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
||||
case "ED25519" -> 128;
|
||||
case "ED448" -> 224;
|
||||
case "ML-KEM" -> kyberStrength(key);
|
||||
case "ML-DSA", "MLDSA" -> mldsaStrength(key);
|
||||
case "BIKE" -> mapByNistLevel(key, 128, 192, 256);
|
||||
case "HQC" -> mapByNistLevel(key, 128, 192, 256);
|
||||
case "FRODO" -> frodoStrength(key);
|
||||
@@ -161,6 +168,7 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
||||
case "NTRU" -> ntruStrength(key);
|
||||
case "SNTRUPRIME" -> sntruPrimeStrength(key);
|
||||
case "NTRULPRIME" -> ntruLPrimeStrength(key);
|
||||
case "SLH-DSA", "SLHDSA" -> slhDsaStrength(key);
|
||||
case "SPHINCS+", "SPHINCSPLUS" -> sphincsPlusStrength(key);
|
||||
case "DIGEST" -> 128;
|
||||
default -> 128;
|
||||
@@ -356,6 +364,31 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
||||
return 128;
|
||||
}
|
||||
|
||||
private static int slhDsaStrength(Key key) {
|
||||
// Provider strings observed: "SLH-DSA-SHAKE-128S", "slh-dsa-sha2-192f",
|
||||
// sometimes with separators or additional tokens. We normalize and then parse.
|
||||
String a = safeAlgo(key);
|
||||
String normalized = a.toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
|
||||
// Prefer explicit numeric strength markers (128/192/256) in the algorithm name.
|
||||
Matcher m = SLHDSA_STRENGTH_PATTERN.matcher(normalized);
|
||||
if (m.find()) {
|
||||
int v = parseIntSafe(m.group(1));
|
||||
if (v == 128 || v == 192 || v == 256) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to L1/L3/L5 markers when present.
|
||||
int byLevel = mapByNistLevel(key, 128, 192, 256);
|
||||
if (byLevel != 0) {
|
||||
return byLevel;
|
||||
}
|
||||
|
||||
// Conservative default.
|
||||
return 128;
|
||||
}
|
||||
|
||||
private static int mapByNistLevel(Key key, int l1, int l3, int l5) {
|
||||
String a = safeAlgo(key);
|
||||
|
||||
@@ -416,4 +449,27 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int mldsaStrength(Key key) {
|
||||
String a = safeAlgo(key);
|
||||
String normalized = a.toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
|
||||
Matcher m = MLDSA_SET_PATTERN.matcher(normalized);
|
||||
if (m.find()) {
|
||||
int set = parseIntSafe(m.group(1));
|
||||
return switch (set) {
|
||||
case 44 -> 128;
|
||||
case 65 -> 192;
|
||||
case 87 -> 256;
|
||||
default -> 128;
|
||||
};
|
||||
}
|
||||
|
||||
int byLevel = mapByNistLevel(key, 128, 192, 256);
|
||||
if (byLevel != 0) {
|
||||
return byLevel;
|
||||
}
|
||||
|
||||
return 128;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,14 @@ import zeroecho.core.spec.VoidSpec;
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String PUBLIC_KEY = "publicKey";
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String PRIVATE_KEY = "privateKey";
|
||||
private final Supplier<TagEngine<T>> factory;
|
||||
|
||||
private TagEngineBuilder(Supplier<TagEngine<T>> factory) {
|
||||
@@ -205,7 +213,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ed25519Sign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("Ed25519", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -217,7 +225,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ed25519Verify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("Ed25519", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -236,7 +244,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
|
||||
}
|
||||
|
||||
@@ -255,7 +263,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
|
||||
}
|
||||
|
||||
@@ -273,7 +281,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
|
||||
return signature("ECDSA", privateKey, s);
|
||||
}
|
||||
@@ -292,7 +300,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
|
||||
return signature("ECDSA", publicKey, s);
|
||||
}
|
||||
@@ -305,7 +313,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaP256Sign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("ECDSA", privateKey, EcdsaCurveSpec.P256);
|
||||
}
|
||||
|
||||
@@ -317,7 +325,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaP256Verify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("ECDSA", publicKey, EcdsaCurveSpec.P256);
|
||||
}
|
||||
|
||||
@@ -334,7 +342,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> sphincsPlusSign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -351,7 +359,81 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> sphincsPlusVerify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an SLH-DSA signing engine.
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
|
||||
* concrete parameter set is encoded in the key material and interpreted by the
|
||||
* underlying {@link CryptoAlgorithms} implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param privateKey private signing key; must not be {@code null}
|
||||
* @return a builder that produces SLH-DSA signature engines in SIGN mode
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> slhDsaSign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("SLH-DSA", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an SLH-DSA verification engine.
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
|
||||
* concrete parameter set is encoded in the key material and interpreted by the
|
||||
* underlying {@link CryptoAlgorithms} implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param publicKey public verification key; must not be {@code null}
|
||||
* @return a builder that produces SLH-DSA signature engines in VERIFY mode
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> slhDsaVerify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("SLH-DSA", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an ML-DSA signing engine.
|
||||
*
|
||||
* <p>
|
||||
* ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
|
||||
* The concrete parameter set and any pre-hash variant is encoded in the key
|
||||
* material and interpreted by the underlying {@link CryptoAlgorithms}
|
||||
* implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param privateKey private signing key; must not be {@code null}
|
||||
* @return a builder that produces ML-DSA signature engines in SIGN mode
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> mldsaSign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("ML-DSA", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an ML-DSA verification engine.
|
||||
*
|
||||
* <p>
|
||||
* ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
|
||||
* The concrete parameter set and any pre-hash variant is encoded in the key
|
||||
* material and interpreted by the underlying {@link CryptoAlgorithms}
|
||||
* implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param publicKey public verification key; must not be {@code null}
|
||||
* @return a builder that produces ML-DSA signature engines in VERIFY mode
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> mldsaVerify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("ML-DSA", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +59,7 @@ public final class Strings {
|
||||
* truncated after 32 elements, followed by an ellipsis marker: {@code [...]}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Examples:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* <h4>Examples</h4> <pre>{@code
|
||||
* Strings.toShortString(null) -> "null"
|
||||
* Strings.toShortString(new byte[0]) -> "[]"
|
||||
* Strings.toShortString(new byte[]{1, 2, 3})
|
||||
@@ -118,7 +113,7 @@ public final class Strings {
|
||||
* <li>If {@code a} is empty, the string {@code "[]"} is returned.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Examples</h2> <pre>{@code
|
||||
* <h4>Examples</h4> <pre>{@code
|
||||
* toShortHexString(null) -> "null"
|
||||
* toShortHexString(new byte[0]) -> "[]"
|
||||
* toShortHexString(new byte[]{0x01}) -> "[0x01]"
|
||||
|
||||
714
lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java
Normal file
714
lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java
Normal file
@@ -0,0 +1,714 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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 java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexContext;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexExporter;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexPolicy;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexProfile;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexTranscript;
|
||||
|
||||
/**
|
||||
* Fluent builder for constructing hybrid KEX contexts.
|
||||
*
|
||||
* <p>
|
||||
* The builder supports the two practical hybrid variants:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>CLASSIC_AGREEMENT + KEM_ADAPTER</b> (classic peer public is provided
|
||||
* out-of-band)</li>
|
||||
* <li><b>PAIR_MESSAGE + KEM_ADAPTER</b> (classic public key is carried in the
|
||||
* hybrid message)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The builder also supports transcript binding and optional policy enforcement
|
||||
* before returning the context.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Usage sketch</h2> <pre>{@code
|
||||
* HybridKexContext ctx = HybridKexBuilder.builder()
|
||||
* .profile(HybridKexProfile.defaultProfile(32))
|
||||
* .transcript(new HybridKexTranscript().addUtf8("suite", "X25519+MLKEM768"))
|
||||
* .policy(new HybridKexPolicy(128, 192, 32))
|
||||
* .classicAgreement()
|
||||
* .algorithm("Xdh").spec(XdhSpec.X25519)
|
||||
* .privateKey(alicePriv).peerPublic(bobPub)
|
||||
* .pqcKem()
|
||||
* .algorithm("ML-KEM").peerPublic(bobPqcPub)
|
||||
* .buildInitiator();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Instances are mutable and not thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridKexBuilder {
|
||||
|
||||
private HybridKexProfile profile;
|
||||
private HybridKexTranscript transcript;
|
||||
private HybridKexPolicy policy;
|
||||
|
||||
private ClassicMode classicMode;
|
||||
|
||||
private String classicAlgId;
|
||||
private ContextSpec classicSpec;
|
||||
private PrivateKey classicPrivate;
|
||||
private PublicKey classicPeerPublic;
|
||||
private KeyPairKey classicKeyPair;
|
||||
|
||||
private String pqcAlgId;
|
||||
private ContextSpec pqcSpec;
|
||||
private PublicKey pqcPeerPublic;
|
||||
private PrivateKey pqcPrivate;
|
||||
|
||||
private HybridKexBuilder() {
|
||||
// builder
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder instance.
|
||||
*
|
||||
* @return new builder
|
||||
*/
|
||||
public static HybridKexBuilder builder() {
|
||||
return new HybridKexBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hybrid profile (HKDF salt/info/output length).
|
||||
*
|
||||
* @param profile profile (must not be null)
|
||||
* @return this builder
|
||||
*/
|
||||
public HybridKexBuilder profile(HybridKexProfile profile) {
|
||||
this.profile = Objects.requireNonNull(profile, "profile");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional transcript used to bind HKDF {@code info}.
|
||||
*
|
||||
* <p>
|
||||
* If provided, its bytes are concatenated to the profile {@code hkdfInfo} with
|
||||
* a single zero byte separator. This preserves the profile label as a domain
|
||||
* separator while incorporating handshake context.
|
||||
* </p>
|
||||
*
|
||||
* @param transcript transcript (may be null to clear)
|
||||
* @return this builder
|
||||
*/
|
||||
public HybridKexBuilder transcript(HybridKexTranscript transcript) {
|
||||
this.transcript = transcript;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional hybrid policy enforcement.
|
||||
*
|
||||
* @param policy policy (may be null to clear)
|
||||
* @return this builder
|
||||
*/
|
||||
public HybridKexBuilder policy(HybridKexPolicy policy) {
|
||||
this.policy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the classic leg in {@link ClassicMode#CLASSIC_AGREEMENT} mode.
|
||||
*
|
||||
* <p>
|
||||
* In this mode, the classic peer public key is assumed to be available
|
||||
* out-of-band (for example via a certificate, a directory, or a higher-level
|
||||
* handshake message). The hybrid wire message therefore typically carries only
|
||||
* the PQC payload (KEM ciphertext), while the classic leg is configured via
|
||||
* {@link AgreementContext#setPeerPublic(PublicKey)}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Calling this method resets any previously configured {@code PAIR_MESSAGE}
|
||||
* inputs ({@link #classicKeyPair}) to prevent ambiguous configuration.
|
||||
* </p>
|
||||
*
|
||||
* @return classic agreement configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicAgreement classicAgreement() {
|
||||
this.classicMode = ClassicMode.CLASSIC_AGREEMENT;
|
||||
this.classicKeyPair = null;
|
||||
return new ClassicAgreement(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the classic leg in {@link ClassicMode#PAIR_MESSAGE} mode.
|
||||
*
|
||||
* <p>
|
||||
* In this mode, the classic leg is message-capable: the public key is carried
|
||||
* as an explicit classic message (typically an SPKI encoding) and becomes part
|
||||
* of the hybrid peer message. This enables a fully message-oriented handshake
|
||||
* where both legs contribute to the wire payload (classic public-key message +
|
||||
* PQC ciphertext).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Calling this method resets any previously configured
|
||||
* {@code CLASSIC_AGREEMENT} inputs ({@link #classicPrivate} and
|
||||
* {@link #classicPeerPublic}) to prevent ambiguous configuration.
|
||||
* </p>
|
||||
*
|
||||
* @return classic pair-message configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicPairMessage classicPairMessage() {
|
||||
this.classicMode = ClassicMode.PAIR_MESSAGE;
|
||||
this.classicPrivate = null;
|
||||
this.classicPeerPublic = null;
|
||||
return new ClassicPairMessage(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects configuration of the post-quantum leg (KEM adapter).
|
||||
*
|
||||
* <p>
|
||||
* The PQC leg is always treated as a message-based agreement:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Initiator is created from the recipient's PQC {@link PublicKey} and
|
||||
* produces a message (typically a ciphertext) via
|
||||
* {@link MessageAgreementContext#getPeerMessage()}.</li>
|
||||
* <li>Responder is created from the recipient's PQC {@link PrivateKey} and
|
||||
* consumes the peer message via
|
||||
* {@link MessageAgreementContext#setPeerMessage(byte[])}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return PQC KEM configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem pqcKem() {
|
||||
return new PqcKem(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds initiator-side context.
|
||||
*
|
||||
* @return initiator context
|
||||
* @throws IOException if underlying context creation fails
|
||||
*/
|
||||
public HybridKexContext buildInitiator() throws IOException {
|
||||
validateCommon();
|
||||
|
||||
AgreementContext classic;
|
||||
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) {
|
||||
throw new IllegalStateException("pqc peer public must be set for initiator");
|
||||
}
|
||||
MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec);
|
||||
|
||||
HybridKexProfile effective = effectiveProfile();
|
||||
if (policy != null) {
|
||||
policy.enforce(effective, classic, pqc);
|
||||
}
|
||||
return new HybridKexContext(effective, classic, pqc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds responder-side context.
|
||||
*
|
||||
* @return responder context
|
||||
* @throws IOException if underlying context creation fails
|
||||
*/
|
||||
public HybridKexContext buildResponder() throws IOException {
|
||||
validateCommon();
|
||||
|
||||
AgreementContext classic;
|
||||
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) {
|
||||
throw new IllegalStateException("pqc private key must be set for responder");
|
||||
}
|
||||
MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPrivate, pqcSpec);
|
||||
|
||||
HybridKexProfile effective = effectiveProfile();
|
||||
if (policy != null) {
|
||||
policy.enforce(effective, classic, pqc);
|
||||
}
|
||||
return new HybridKexContext(effective, classic, pqc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an exporter seeded from a derived OKM value.
|
||||
*
|
||||
* @param okm derived OKM (for example {@link HybridKexContext#deriveSecret()})
|
||||
* @return exporter
|
||||
*/
|
||||
public HybridKexExporter exporterFromOkm(byte[] okm) {
|
||||
validateCommon();
|
||||
HybridKexProfile effective = effectiveProfile();
|
||||
return new HybridKexExporter(okm, effective.hkdfSalt());
|
||||
}
|
||||
|
||||
private void validateCommon() {
|
||||
if (profile == null) {
|
||||
throw new IllegalStateException("profile must be set");
|
||||
}
|
||||
if (classicMode == null) {
|
||||
throw new IllegalStateException("classic mode must be selected");
|
||||
}
|
||||
if (classicAlgId == null) {
|
||||
throw new IllegalStateException("classic algorithm id must be set");
|
||||
}
|
||||
if (pqcAlgId == null) {
|
||||
throw new IllegalStateException("pqc algorithm id must be set");
|
||||
}
|
||||
}
|
||||
|
||||
private HybridKexProfile effectiveProfile() {
|
||||
byte[] info0 = profile.hkdfInfo();
|
||||
byte[] t = (transcript == null) ? null : transcript.toByteArray();
|
||||
|
||||
if (t == null || t.length == 0) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
byte[] base = (info0 == null) ? new byte[0] : info0.clone();
|
||||
byte[] merged = new byte[base.length + 1 + t.length];
|
||||
System.arraycopy(base, 0, merged, 0, base.length);
|
||||
merged[base.length] = 0;
|
||||
System.arraycopy(t, 0, merged, base.length + 1, t.length);
|
||||
|
||||
return new HybridKexProfile(profile.hkdfSalt(), merged, profile.outLenBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how the classic (pre-quantum) agreement leg is wired into the
|
||||
* hybrid handshake.
|
||||
*
|
||||
* <p>
|
||||
* The classic leg is always an {@link AgreementContext}, but there are two
|
||||
* operational models:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #CLASSIC_AGREEMENT}: the peer public key is supplied out-of-band
|
||||
* (not transported by the hybrid message). This corresponds to the traditional
|
||||
* DH/ECDH/XDH usage pattern where the protocol already has a mechanism to
|
||||
* convey or authenticate the peer public key (certificate, static key, or
|
||||
* separate handshake structure).</li>
|
||||
* <li>{@link #PAIR_MESSAGE}: the classic leg is message-capable and
|
||||
* emits/consumes a public-key message (typically SPKI) through
|
||||
* {@link MessageAgreementContext}. In this model, the classic public key
|
||||
* travels inside the hybrid peer message alongside the PQC payload, enabling a
|
||||
* fully message-oriented exchange.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* This enum is internal to the builder but is documented because it defines the
|
||||
* wire semantics of the resulting {@link HybridKexContext} and determines which
|
||||
* builder inputs are required.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
private enum ClassicMode {
|
||||
|
||||
/**
|
||||
* Classic agreement where the peer public key is provided out-of-band and
|
||||
* configured via {@link AgreementContext#setPeerPublic(PublicKey)}.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
CLASSIC_AGREEMENT,
|
||||
|
||||
/**
|
||||
* Classic agreement where the classic public key is carried in-band as a
|
||||
* message produced/consumed via
|
||||
* {@link MessageAgreementContext#getPeerMessage()} and
|
||||
* {@link MessageAgreementContext#setPeerMessage(byte[])}.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
PAIR_MESSAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurator for the classic leg in {@link ClassicMode#CLASSIC_AGREEMENT}
|
||||
* mode.
|
||||
*
|
||||
* <p>
|
||||
* Required inputs before building:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #algorithm(String)} - classic algorithm id (for example
|
||||
* {@code "Xdh"})</li>
|
||||
* <li>{@link #privateKey(PrivateKey)} - local classic private key</li>
|
||||
* <li>{@link #peerPublic(PublicKey)} - peer classic public key
|
||||
* (out-of-band)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Optional inputs:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #spec(ContextSpec)} - algorithm-specific context spec (for example
|
||||
* {@code XdhSpec.X25519})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* After configuring the classic leg, continue with {@link #pqcKem()} to
|
||||
* configure the PQC leg.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static final class ClassicAgreement {
|
||||
private final HybridKexBuilder parent;
|
||||
|
||||
private ClassicAgreement(HybridKexBuilder parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the classic agreement algorithm identifier.
|
||||
*
|
||||
* <p>
|
||||
* Example for X25519 in this project: use {@code "Xdh"} with
|
||||
* {@code XdhSpec.X25519}.
|
||||
* </p>
|
||||
*
|
||||
* @param algId algorithm id (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code algId} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicAgreement algorithm(String algId) {
|
||||
parent.classicAlgId = Objects.requireNonNull(algId, "algId");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the classic context spec (algorithm parameters).
|
||||
*
|
||||
* @param spec spec (may be null if the algorithm provides a default)
|
||||
* @return this configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicAgreement spec(ContextSpec spec) {
|
||||
parent.classicSpec = spec;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the local private key for the classic leg.
|
||||
*
|
||||
* @param key private key (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code key} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicAgreement privateKey(PrivateKey key) {
|
||||
parent.classicPrivate = Objects.requireNonNull(key, "key");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the peer public key for the classic leg.
|
||||
*
|
||||
* <p>
|
||||
* In {@link ClassicMode#CLASSIC_AGREEMENT} this key is assumed to be obtained
|
||||
* out-of-band.
|
||||
* </p>
|
||||
*
|
||||
* @param key peer public key (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code key} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicAgreement peerPublic(PublicKey key) {
|
||||
parent.classicPeerPublic = Objects.requireNonNull(key, "key");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues with PQC (KEM adapter) leg configuration.
|
||||
*
|
||||
* @return PQC configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem pqcKem() {
|
||||
return parent.pqcKem();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurator for the classic leg in {@link ClassicMode#PAIR_MESSAGE} mode.
|
||||
*
|
||||
* <p>
|
||||
* Required inputs before building:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #algorithm(String)} - classic algorithm id (for example
|
||||
* {@code "Xdh"})</li>
|
||||
* <li>{@link #keyPair(KeyPairKey)} - local classic key pair wrapped as
|
||||
* {@link KeyPairKey}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Optional inputs:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #spec(ContextSpec)} - algorithm-specific context spec (for example
|
||||
* {@code XdhSpec.X25519})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* In this mode, the classic leg contributes a public-key message (typically
|
||||
* SPKI bytes) to the hybrid peer message. The peer public key is therefore
|
||||
* learned from {@link HybridKexContext#setPeerMessage(byte[])} rather than
|
||||
* being supplied out-of-band.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static final class ClassicPairMessage {
|
||||
private final HybridKexBuilder parent;
|
||||
|
||||
private ClassicPairMessage(HybridKexBuilder parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the classic agreement algorithm identifier.
|
||||
*
|
||||
* @param algId algorithm id (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code algId} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicPairMessage algorithm(String algId) {
|
||||
parent.classicAlgId = Objects.requireNonNull(algId, "algId");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the classic context spec (algorithm parameters).
|
||||
*
|
||||
* @param spec spec (may be null if the algorithm provides a default)
|
||||
* @return this configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicPairMessage spec(ContextSpec spec) {
|
||||
parent.classicSpec = spec;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the local classic key pair.
|
||||
*
|
||||
* <p>
|
||||
* The key pair is wrapped into {@link KeyPairKey} to match the
|
||||
* {@code PAIR_MESSAGE} capability registered in core.
|
||||
* </p>
|
||||
*
|
||||
* @param keyPair key pair wrapper (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code keyPair} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public ClassicPairMessage keyPair(KeyPairKey keyPair) {
|
||||
parent.classicKeyPair = Objects.requireNonNull(keyPair, "keyPair");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues with PQC (KEM adapter) leg configuration.
|
||||
*
|
||||
* @return PQC configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem pqcKem() {
|
||||
return parent.pqcKem();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurator for the PQC leg (KEM adapter).
|
||||
*
|
||||
* <p>
|
||||
* Required inputs differ by role:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>Initiator</b> requires {@link #peerPublic(PublicKey)} (recipient PQC
|
||||
* public key).</li>
|
||||
* <li><b>Responder</b> requires {@link #privateKey(PrivateKey)} (recipient PQC
|
||||
* private key).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The PQC leg is always message-based: initiator produces a peer message
|
||||
* (ciphertext) and responder consumes it. The hybrid context transports this
|
||||
* payload as the PQC part of the hybrid message.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static final class PqcKem {
|
||||
private final HybridKexBuilder parent;
|
||||
|
||||
private PqcKem(HybridKexBuilder parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PQC algorithm identifier.
|
||||
*
|
||||
* @param algId algorithm id (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code algId} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem algorithm(String algId) {
|
||||
parent.pqcAlgId = Objects.requireNonNull(algId, "algId");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PQC context spec (algorithm parameters).
|
||||
*
|
||||
* @param spec spec (may be null if the algorithm provides a default)
|
||||
* @return this configurator
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem spec(ContextSpec spec) {
|
||||
parent.pqcSpec = spec;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the recipient PQC public key for initiator-side construction.
|
||||
*
|
||||
* @param key recipient public key (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code key} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem peerPublic(PublicKey key) {
|
||||
parent.pqcPeerPublic = Objects.requireNonNull(key, "key");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the recipient PQC private key for responder-side construction.
|
||||
*
|
||||
* @param key recipient private key (must not be null)
|
||||
* @return this configurator
|
||||
* @throws NullPointerException if {@code key} is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public PqcKem privateKey(PrivateKey key) {
|
||||
parent.pqcPrivate = Objects.requireNonNull(key, "key");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an initiator-side {@link HybridKexContext} using the current builder
|
||||
* configuration.
|
||||
*
|
||||
* @return initiator context
|
||||
* @throws IOException if underlying context creation fails
|
||||
* @throws IllegalStateException if required configuration for initiator role is
|
||||
* missing
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridKexContext buildInitiator() throws IOException {
|
||||
return parent.buildInitiator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a responder-side {@link HybridKexContext} using the current builder
|
||||
* configuration.
|
||||
*
|
||||
* @return responder context
|
||||
* @throws IOException if underlying context creation fails
|
||||
* @throws IllegalStateException if required configuration for responder role is
|
||||
* missing
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridKexContext buildResponder() throws IOException {
|
||||
return parent.buildResponder();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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 java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import conflux.CtxInterface;
|
||||
import conflux.Key;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.tag.TagEngine;
|
||||
import zeroecho.sdk.builders.core.DataContentBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.hybrid.signature.HybridSignatureContexts;
|
||||
import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
|
||||
|
||||
/**
|
||||
* Signature-specific trailer builder for {@link DataContent} pipelines.
|
||||
*
|
||||
* <p>
|
||||
* This class is intended as a convenient, signature-specialized replacement
|
||||
* for:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* new TagTrailerDataContentBuilder<Signature>(engine).bufferSize(8192)
|
||||
* new TagTrailerDataContentBuilder<Signature>(engine).bufferSize(8192).throwOnMismatch()
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* It keeps {@link TagTrailerDataContentBuilder} as the generic implementation
|
||||
* while providing a compact API for {@code Signature} usage, including
|
||||
* construction of {@code SignatureContext} for both single-algorithm and hybrid
|
||||
* signatures.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Mode selection</h2>
|
||||
* <ul>
|
||||
* <li>{@link #core(TagEngine)} / {@link #core(Supplier)}: wraps a ready engine
|
||||
* (same parameters as {@link TagTrailerDataContentBuilder}).</li>
|
||||
* <li>{@link #single()}: constructs a non-hybrid {@code SignatureContext} via
|
||||
* {@link CryptoAlgorithms}.</li>
|
||||
* <li>{@link #hybrid()}: constructs a hybrid {@code SignatureContext} via
|
||||
* {@link HybridSignatureContexts}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Checked exceptions</h2>
|
||||
* <p>
|
||||
* Context construction may involve I/O (e.g., catalog/provider loading) and
|
||||
* therefore throw {@link IOException}. This builder converts such failures to
|
||||
* {@link IllegalStateException} because fluent builder APIs are expected to be
|
||||
* used in configuration code without mandatory checked-exception plumbing.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SignatureTrailerDataContentBuilder implements DataContentBuilder<DataContent> {
|
||||
|
||||
private final TagTrailerDataContentBuilder<Signature> delegate;
|
||||
|
||||
private SignatureTrailerDataContentBuilder(TagEngine<Signature> engine) {
|
||||
this.delegate = new TagTrailerDataContentBuilder<>(engine);
|
||||
}
|
||||
|
||||
private SignatureTrailerDataContentBuilder(Supplier<? extends TagEngine<Signature>> engineFactory) {
|
||||
this.delegate = new TagTrailerDataContentBuilder<>(engineFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core mode: wraps a fixed engine instance.
|
||||
*
|
||||
* <p>
|
||||
* This is the direct signature-specialized equivalent of:
|
||||
* {@code new TagTrailerDataContentBuilder<Signature>(engine)}.
|
||||
* </p>
|
||||
*
|
||||
* @param engine signature engine (typically a {@code SignatureContext}); must
|
||||
* not be {@code null}
|
||||
* @return builder instance
|
||||
* @throws NullPointerException if {@code engine} is {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
public static SignatureTrailerDataContentBuilder core(TagEngine<Signature> engine) {
|
||||
Objects.requireNonNull(engine, "engine");
|
||||
return new SignatureTrailerDataContentBuilder(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core mode: wraps an engine factory.
|
||||
*
|
||||
* <p>
|
||||
* This is the direct signature-specialized equivalent of:
|
||||
* {@code new TagTrailerDataContentBuilder<Signature>(engineFactory)}.
|
||||
* </p>
|
||||
*
|
||||
* @param engineFactory engine factory; must not be {@code null}
|
||||
* @return builder instance
|
||||
* @throws NullPointerException if {@code engineFactory} is {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
public static SignatureTrailerDataContentBuilder core(Supplier<? extends TagEngine<Signature>> engineFactory) {
|
||||
Objects.requireNonNull(engineFactory, "engineFactory");
|
||||
return new SignatureTrailerDataContentBuilder(engineFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters single-algorithm (non-hybrid) construction helpers.
|
||||
*
|
||||
* @return selector for creating signing/verifying builders
|
||||
* @since 1.0
|
||||
*/
|
||||
public static SingleSelector single() {
|
||||
return new SingleSelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters hybrid signature construction helpers.
|
||||
*
|
||||
* @return selector for creating signing/verifying builders
|
||||
* @since 1.0
|
||||
*/
|
||||
public static HybridSelector hybrid() {
|
||||
return new HybridSelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal I/O buffer size used when stripping the trailer.
|
||||
*
|
||||
* @param bytes buffer size in bytes; must be at least 1
|
||||
* @return this builder for chaining
|
||||
* @throws IllegalArgumentException if {@code bytes < 1}
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder bufferSize(int bytes) {
|
||||
delegate.bufferSize(bytes);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures verification to throw on mismatch (default verification behavior).
|
||||
*
|
||||
* @return this builder for chaining
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder throwOnMismatch() {
|
||||
delegate.throwOnMismatch();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of throwing on mismatch, records the verification outcome into a
|
||||
* context flag.
|
||||
*
|
||||
* @param ctx context instance to receive the flag; must not be
|
||||
* {@code null}
|
||||
* @param verifyKey key under which the {@code Boolean} result is recorded; must
|
||||
* not be {@code null}
|
||||
* @return this builder for chaining
|
||||
* @throws NullPointerException if {@code ctx} or {@code verifyKey} is
|
||||
* {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder flagInContext(CtxInterface ctx, Key<Boolean> verifyKey) {
|
||||
delegate.flagInContext(Objects.requireNonNull(ctx, "ctx"), Objects.requireNonNull(verifyKey, "verifyKey"));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataContent build(boolean encrypt) {
|
||||
return delegate.build(encrypt);
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// Single-algorithm helpers
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Helper entry for single-algorithm signature construction.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static final class SingleSelector {
|
||||
|
||||
private SingleSelector() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signing trailer for a single signature algorithm (no spec).
|
||||
*
|
||||
* @param algorithmId signature algorithm id
|
||||
* @param privateKey private key for signing
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if {@code algorithmId} or {@code privateKey} is
|
||||
* {@code null}
|
||||
* @throws IllegalStateException if the signature context cannot be created
|
||||
* (e.g., I/O/provider issues)
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder sign(String algorithmId, PrivateKey privateKey) {
|
||||
return sign(algorithmId, privateKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signing trailer for a single signature algorithm with an optional
|
||||
* spec.
|
||||
*
|
||||
* @param algorithmId signature algorithm id
|
||||
* @param privateKey private key for signing
|
||||
* @param spec optional context spec (may be {@code null})
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if {@code algorithmId} or {@code privateKey} is
|
||||
* {@code null}
|
||||
* @throws IllegalStateException if the signature context cannot be created
|
||||
* (e.g., I/O/provider issues)
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder sign(String algorithmId, PrivateKey privateKey, ContextSpec spec) {
|
||||
Objects.requireNonNull(algorithmId, "algorithmId");
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
|
||||
Supplier<TagEngine<Signature>> factory = () -> {
|
||||
try {
|
||||
return CryptoAlgorithms.create(algorithmId, KeyUsage.SIGN, privateKey, spec);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to create SIGN SignatureContext for: " + algorithmId, e);
|
||||
}
|
||||
};
|
||||
|
||||
return core(factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verification trailer for a single signature algorithm (no spec).
|
||||
*
|
||||
* @param algorithmId signature algorithm id
|
||||
* @param publicKey public key for verification
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if {@code algorithmId} or {@code publicKey} is
|
||||
* {@code null}
|
||||
* @throws IllegalStateException if the signature context cannot be created
|
||||
* (e.g., I/O/provider issues)
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder verify(String algorithmId, PublicKey publicKey) {
|
||||
return verify(algorithmId, publicKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verification trailer for a single signature algorithm with an
|
||||
* optional spec.
|
||||
*
|
||||
* @param algorithmId signature algorithm id
|
||||
* @param publicKey public key for verification
|
||||
* @param spec optional context spec (may be {@code null})
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if {@code algorithmId} or {@code publicKey} is
|
||||
* {@code null}
|
||||
* @throws IllegalStateException if the signature context cannot be created
|
||||
* (e.g., I/O/provider issues)
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder verify(String algorithmId, PublicKey publicKey, ContextSpec spec) {
|
||||
Objects.requireNonNull(algorithmId, "algorithmId");
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
|
||||
Supplier<TagEngine<Signature>> factory = () -> {
|
||||
try {
|
||||
return CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, publicKey, spec);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to create VERIFY SignatureContext for: " + algorithmId, e);
|
||||
}
|
||||
};
|
||||
|
||||
return core(factory);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// Hybrid helpers
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Helper entry for hybrid signature construction.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public static final class HybridSelector {
|
||||
|
||||
private static final int DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
private HybridSelector() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hybrid signing trailer for the common case where both specs are
|
||||
* {@code null}.
|
||||
*
|
||||
* @param classicSigId classic signature algorithm id
|
||||
* @param pqcSigId post-quantum signature algorithm id
|
||||
* @param rule aggregation rule
|
||||
* @param classicPrivate classic private key
|
||||
* @param pqcPrivate PQC private key
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IllegalStateException if the hybrid context cannot be created
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder sign(String classicSigId, String pqcSigId,
|
||||
HybridSignatureProfile.VerifyRule rule, PrivateKey classicPrivate, PrivateKey pqcPrivate) {
|
||||
return sign(classicSigId, pqcSigId, null, null, rule, classicPrivate, pqcPrivate, DEFAULT_MAX_BODY_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hybrid verification trailer for the common case where both specs
|
||||
* are {@code null}.
|
||||
*
|
||||
* @param classicSigId classic signature algorithm id
|
||||
* @param pqcSigId post-quantum signature algorithm id
|
||||
* @param rule aggregation rule
|
||||
* @param classicPublic classic public key
|
||||
* @param pqcPublic PQC public key
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IllegalStateException if the hybrid context cannot be created
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder verify(String classicSigId, String pqcSigId,
|
||||
HybridSignatureProfile.VerifyRule rule, PublicKey classicPublic, PublicKey pqcPublic) {
|
||||
return verify(classicSigId, pqcSigId, null, null, rule, classicPublic, pqcPublic, DEFAULT_MAX_BODY_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hybrid signing trailer with optional specs and explicit
|
||||
* {@code maxBodyBytes}.
|
||||
*
|
||||
* @param classicSigId classic signature algorithm id
|
||||
* @param pqcSigId post-quantum signature algorithm id
|
||||
* @param classicSpec optional classic spec (may be {@code null})
|
||||
* @param pqcSpec optional PQC spec (may be {@code null})
|
||||
* @param rule aggregation rule
|
||||
* @param classicPrivate classic private key
|
||||
* @param pqcPrivate PQC private key
|
||||
* @param maxBodyBytes maximum body size accepted by the hybrid context; must
|
||||
* be at least 1
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IllegalArgumentException if {@code maxBodyBytes < 1}
|
||||
* @throws IllegalStateException if the hybrid context cannot be created
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder sign(String classicSigId, String pqcSigId, ContextSpec classicSpec,
|
||||
ContextSpec pqcSpec, HybridSignatureProfile.VerifyRule rule, PrivateKey classicPrivate,
|
||||
PrivateKey pqcPrivate, int maxBodyBytes) {
|
||||
Objects.requireNonNull(classicSigId, "classicSigId");
|
||||
Objects.requireNonNull(pqcSigId, "pqcSigId");
|
||||
Objects.requireNonNull(rule, "rule");
|
||||
Objects.requireNonNull(classicPrivate, "classicPrivate");
|
||||
Objects.requireNonNull(pqcPrivate, "pqcPrivate");
|
||||
if (maxBodyBytes < 1) { // NOPMD
|
||||
throw new IllegalArgumentException("maxBodyBytes must be >= 1");
|
||||
}
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile(classicSigId, pqcSigId, classicSpec, pqcSpec,
|
||||
rule);
|
||||
|
||||
Supplier<TagEngine<Signature>> factory = () -> {
|
||||
try {
|
||||
return HybridSignatureContexts.sign(profile, classicPrivate, pqcPrivate, maxBodyBytes);
|
||||
} catch (RuntimeException e) { // NOPMD
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to create hybrid SIGN SignatureContext", e);
|
||||
}
|
||||
};
|
||||
|
||||
return core(factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hybrid verification trailer with optional specs and explicit
|
||||
* {@code maxBodyBytes}.
|
||||
*
|
||||
* @param classicSigId classic signature algorithm id
|
||||
* @param pqcSigId post-quantum signature algorithm id
|
||||
* @param classicSpec optional classic spec (may be {@code null})
|
||||
* @param pqcSpec optional PQC spec (may be {@code null})
|
||||
* @param rule aggregation rule
|
||||
* @param classicPublic classic public key
|
||||
* @param pqcPublic PQC public key
|
||||
* @param maxBodyBytes maximum body size accepted by the hybrid context; must
|
||||
* be at least 1
|
||||
* @return builder ready to be added to a pipeline
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IllegalArgumentException if {@code maxBodyBytes < 1}
|
||||
* @throws IllegalStateException if the hybrid context cannot be created
|
||||
* @since 1.0
|
||||
*/
|
||||
public SignatureTrailerDataContentBuilder verify(String classicSigId, String pqcSigId, ContextSpec classicSpec,
|
||||
ContextSpec pqcSpec, HybridSignatureProfile.VerifyRule rule, PublicKey classicPublic,
|
||||
PublicKey pqcPublic, int maxBodyBytes) {
|
||||
Objects.requireNonNull(classicSigId, "classicSigId");
|
||||
Objects.requireNonNull(pqcSigId, "pqcSigId");
|
||||
Objects.requireNonNull(rule, "rule");
|
||||
Objects.requireNonNull(classicPublic, "classicPublic");
|
||||
Objects.requireNonNull(pqcPublic, "pqcPublic");
|
||||
if (maxBodyBytes < 1) { // NOPMD
|
||||
throw new IllegalArgumentException("maxBodyBytes must be >= 1");
|
||||
}
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile(classicSigId, pqcSigId, classicSpec, pqcSpec,
|
||||
rule);
|
||||
|
||||
Supplier<TagEngine<Signature>> factory = () -> {
|
||||
try {
|
||||
return HybridSignatureContexts.verify(profile, classicPublic, pqcPublic, maxBodyBytes);
|
||||
} catch (RuntimeException e) { // NOPMD
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to create hybrid VERIFY SignatureContext", e);
|
||||
}
|
||||
};
|
||||
|
||||
return core(factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,7 +451,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder<DataCo
|
||||
ctx.put(ConfluxKeys.iv(algId), nonce);
|
||||
}
|
||||
if (v == Variant.AEAD) {
|
||||
if (aad != null) {
|
||||
if (aad != null) { // NOPMD
|
||||
ctx.put(ConfluxKeys.aad(algId), aad);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -253,6 +253,40 @@ public final class HmacDataContentBuilder implements DataContentBuilder<PlainCon
|
||||
return new HmacDataContentBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently configured HMAC specification.
|
||||
*
|
||||
* <p>
|
||||
* This accessor is intentionally read-only and exists to support safe
|
||||
* integrations (for example hybrid-derived key injection) without duplicating
|
||||
* the HMAC variant configuration outside of this builder.
|
||||
* </p>
|
||||
*
|
||||
* @return current HMAC spec (never null)
|
||||
* @since 1.0
|
||||
*/
|
||||
public HmacSpec spec() {
|
||||
return this.spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a recommended HMAC key size (in bits) for the currently configured
|
||||
* {@link #spec()}.
|
||||
*
|
||||
* <p>
|
||||
* This is a convenience forwarding method to
|
||||
* {@link HmacSpec#recommendedKeyBits()} and is intended as the default choice
|
||||
* for derived-key integrations. Callers that intentionally need a non-default
|
||||
* size may override it explicitly.
|
||||
* </p>
|
||||
*
|
||||
* @return recommended key size in bits (positive, multiple of 8)
|
||||
* @since 1.0
|
||||
*/
|
||||
public int recommendedKeyBits() {
|
||||
return this.spec.recommendedKeyBits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the builder to MAC mode.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.alg;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.alg.mldsa.MldsaKeyGenSpec;
|
||||
import zeroecho.core.alg.mldsa.MldsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.mldsa.MldsaPublicKeySpec;
|
||||
import zeroecho.core.alg.mldsa.MldsaSignatureContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
|
||||
/**
|
||||
* Streaming signature builder for ML-DSA (FIPS 204).
|
||||
*
|
||||
* <p>
|
||||
* This builder integrates ML-DSA with the reusable streaming pipeline provided
|
||||
* by {@link AbstractStreamingSignatureDataBuilder}. It supports signing or
|
||||
* verifying data while it flows through an {@link java.io.InputStream}, as well
|
||||
* as emitting detached signature artifacts in raw, hex or Base64 encodings.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Key material may be provided directly, imported (X.509 / PKCS#8), or
|
||||
* generated on demand using an algorithm-specific {@link MldsaKeyGenSpec}.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaDataContentBuilder
|
||||
extends AbstractStreamingSignatureDataBuilder<MldsaKeyGenSpec, MldsaPublicKeySpec, MldsaPrivateKeySpec> {
|
||||
|
||||
private MldsaKeyGenSpec keyGenSpec;
|
||||
|
||||
/**
|
||||
* Creates a new ML-DSA streaming builder instance.
|
||||
*
|
||||
* @return new builder
|
||||
*/
|
||||
public static MldsaDataContentBuilder builder() {
|
||||
return new MldsaDataContentBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a non-default key generation specification used when
|
||||
* {@link #generateKeyPair()} is requested.
|
||||
*
|
||||
* @param spec key generation spec; must not be {@code null}
|
||||
* @return {@code this} for chaining
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public MldsaDataContentBuilder withKeyGenSpec(MldsaKeyGenSpec spec) {
|
||||
this.keyGenSpec = Objects.requireNonNull(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String algorithmName() {
|
||||
return "ML-DSA";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException {
|
||||
return new MldsaSignatureContext(alg, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException {
|
||||
return new MldsaSignatureContext(alg, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<MldsaKeyGenSpec> keyGenSpecClass() {
|
||||
return MldsaKeyGenSpec.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<MldsaPublicKeySpec> publicKeySpecClass() {
|
||||
return MldsaPublicKeySpec.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<MldsaPrivateKeySpec> privateKeySpecClass() {
|
||||
return MldsaPrivateKeySpec.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<MldsaKeyGenSpec> defaultKeyGenSpecSupplier() {
|
||||
return MldsaKeyGenSpec::defaultSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MldsaKeyGenSpec currentKeyGenSpecOrNull() {
|
||||
return keyGenSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MldsaPublicKeySpec makePublicKeySpec(byte[] x509, String providerHint) {
|
||||
return (providerHint == null) ? new MldsaPublicKeySpec(x509) : new MldsaPublicKeySpec(x509, providerHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MldsaPrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String providerHint) {
|
||||
return (providerHint == null) ? new MldsaPrivateKeySpec(pkcs8) : new MldsaPrivateKeySpec(pkcs8, providerHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String defaultProviderHint() {
|
||||
return "BC";
|
||||
}
|
||||
|
||||
// Optional: covariant fluent overrides for better chaining ergonomics.
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder sign() {
|
||||
super.sign();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder verify() {
|
||||
super.verify();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder passThrough() {
|
||||
super.passThrough();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder emitRawSignature() {
|
||||
super.emitRawSignature();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder emitHexSignature() {
|
||||
super.emitHexSignature();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder emitBase64Signature() {
|
||||
super.emitBase64Signature();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder emitVerificationBoolean() {
|
||||
super.emitVerificationBoolean();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder bufferSize(int bytes) {
|
||||
super.bufferSize(bytes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder withPrivateKey(PrivateKey k) {
|
||||
super.withPrivateKey(k);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder withPublicKey(PublicKey k) {
|
||||
super.withPublicKey(k);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder generateKeyPair() {
|
||||
super.generateKeyPair();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8) {
|
||||
super.importPrivatePkcs8(pkcs8);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) {
|
||||
super.importPrivatePkcs8(pkcs8, providerName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder importPublicX509(byte[] x509) {
|
||||
super.importPublicX509(x509);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder importPublicX509(byte[] x509, String providerName) {
|
||||
super.importPublicX509(x509, providerName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder expectedSignature(byte[] raw) {
|
||||
super.expectedSignature(raw);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder expectedSignatureHex(String hex) {
|
||||
super.expectedSignatureHex(hex);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MldsaDataContentBuilder expectedSignatureBase64(String b64) {
|
||||
super.expectedSignatureBase64(b64);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.alg;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.alg.slhdsa.SlhDsaKeyGenSpec;
|
||||
import zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec;
|
||||
import zeroecho.core.alg.slhdsa.SlhDsaSignatureContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
|
||||
/**
|
||||
* Streaming signature builder for SLH-DSA (FIPS 205).
|
||||
*
|
||||
* <p>
|
||||
* This builder integrates SLH-DSA with the reusable streaming pipeline provided
|
||||
* by {@link AbstractStreamingSignatureDataBuilder}. It supports signing or
|
||||
* verifying data while it flows through an {@link java.io.InputStream}, as well
|
||||
* as emitting detached signature artifacts in raw, hex or Base64 encodings.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Key material may be provided directly, imported (X.509 / PKCS#8), or
|
||||
* generated on demand using an algorithm-specific {@link SlhDsaKeyGenSpec}.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaDataContentBuilder
|
||||
extends AbstractStreamingSignatureDataBuilder<SlhDsaKeyGenSpec, SlhDsaPublicKeySpec, SlhDsaPrivateKeySpec> {
|
||||
|
||||
private SlhDsaKeyGenSpec keyGenSpec;
|
||||
|
||||
/**
|
||||
* Creates a new SLH-DSA streaming builder instance.
|
||||
*
|
||||
* @return new builder
|
||||
*/
|
||||
public static SlhDsaDataContentBuilder builder() {
|
||||
return new SlhDsaDataContentBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a non-default key generation specification used when
|
||||
* {@link #generateKeyPair()} is requested.
|
||||
*
|
||||
* @param spec key generation spec; must not be {@code null}
|
||||
* @return {@code this} for chaining
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public SlhDsaDataContentBuilder withKeyGenSpec(SlhDsaKeyGenSpec spec) {
|
||||
this.keyGenSpec = Objects.requireNonNull(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String algorithmName() {
|
||||
return "SLH-DSA";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException {
|
||||
return new SlhDsaSignatureContext(alg, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException {
|
||||
return new SlhDsaSignatureContext(alg, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<SlhDsaKeyGenSpec> keyGenSpecClass() {
|
||||
return SlhDsaKeyGenSpec.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<SlhDsaPublicKeySpec> publicKeySpecClass() {
|
||||
return SlhDsaPublicKeySpec.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<SlhDsaPrivateKeySpec> privateKeySpecClass() {
|
||||
return SlhDsaPrivateKeySpec.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<SlhDsaKeyGenSpec> defaultKeyGenSpecSupplier() {
|
||||
return SlhDsaKeyGenSpec::defaultSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SlhDsaKeyGenSpec currentKeyGenSpecOrNull() {
|
||||
return keyGenSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SlhDsaPublicKeySpec makePublicKeySpec(byte[] x509, String providerHint) {
|
||||
return (providerHint == null) ? new SlhDsaPublicKeySpec(x509) : new SlhDsaPublicKeySpec(x509, providerHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SlhDsaPrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String providerHint) {
|
||||
return (providerHint == null) ? new SlhDsaPrivateKeySpec(pkcs8) : new SlhDsaPrivateKeySpec(pkcs8, providerHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String defaultProviderHint() {
|
||||
return "BC";
|
||||
}
|
||||
|
||||
// Optional: covariant fluent overrides for better chaining ergonomics.
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder sign() {
|
||||
super.sign();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder verify() {
|
||||
super.verify();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder passThrough() {
|
||||
super.passThrough();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder emitRawSignature() {
|
||||
super.emitRawSignature();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder emitHexSignature() {
|
||||
super.emitHexSignature();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder emitBase64Signature() {
|
||||
super.emitBase64Signature();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder emitVerificationBoolean() {
|
||||
super.emitVerificationBoolean();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder bufferSize(int bytes) {
|
||||
super.bufferSize(bytes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder withPrivateKey(PrivateKey k) {
|
||||
super.withPrivateKey(k);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder withPublicKey(PublicKey k) {
|
||||
super.withPublicKey(k);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder generateKeyPair() {
|
||||
super.generateKeyPair();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8) {
|
||||
super.importPrivatePkcs8(pkcs8);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) {
|
||||
super.importPrivatePkcs8(pkcs8, providerName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder importPublicX509(byte[] x509) {
|
||||
super.importPublicX509(x509);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder importPublicX509(byte[] x509, String providerName) {
|
||||
super.importPublicX509(x509, providerName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder expectedSignature(byte[] raw) {
|
||||
super.expectedSignature(raw);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder expectedSignatureHex(String hex) {
|
||||
super.expectedSignatureHex(hex);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SlhDsaDataContentBuilder expectedSignatureBase64(String b64) {
|
||||
super.expectedSignatureBase64(b64);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,9 @@
|
||||
* <li>{@link RsaEncDataContentBuilder} and {@link ElgamalEncDataContentBuilder}
|
||||
* - wrap asymmetric encryption.</li>
|
||||
* <li>{@link RsaSigDataContentBuilder}, {@link EcdsaDataContentBuilder},
|
||||
* {@link Ed25519DataContentBuilder}, {@link Ed448DataContentBuilder}, and
|
||||
* {@link SphincsPlusDataContentBuilder} - perform streaming signatures and
|
||||
* {@link Ed25519DataContentBuilder}, {@link Ed448DataContentBuilder},
|
||||
* {@link SphincsPlusDataContentBuilder}, {@link SlhDsaDataContentBuilder}, and
|
||||
* {@link MldsaDataContentBuilder} - perform streaming signatures and
|
||||
* verification.</li>
|
||||
* <li>{@link KemDataContentBuilder} - implement KEM-first envelopes and inject
|
||||
* the derived key into a chosen symmetric payload builder.</li>
|
||||
|
||||
@@ -73,7 +73,9 @@
|
||||
* {@link zeroecho.sdk.builders.alg.EcdsaDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.Ed25519DataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.Ed448DataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder}.</li>
|
||||
* {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.MldsaDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.SlhDsaDataContentBuilder}.</li>
|
||||
* <li>MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
|
||||
* <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}
|
||||
@@ -86,6 +88,11 @@
|
||||
* <li>{@link TagTrailerDataContentBuilder} - appends or verifies an
|
||||
* authentication tag carried as an input trailer using a
|
||||
* {@link zeroecho.core.tag.TagEngine}.</li>
|
||||
* <li>{@link SignatureTrailerDataContentBuilder} - signature-specialized
|
||||
* trailer builder intended to replace
|
||||
* {@code TagTrailerDataContentBuilder<Signature>} in most signature use cases.
|
||||
* It can wrap existing signature engines and construct single-algorithm and
|
||||
* hybrid signature contexts.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
@@ -107,6 +114,9 @@
|
||||
* signatures or tags.</li>
|
||||
* <li>{@link TagTrailerDataContentBuilder} focuses on trailer-style tags with
|
||||
* explicit verify policies.</li>
|
||||
* <li>{@link SignatureTrailerDataContentBuilder} provides the corresponding
|
||||
* trailer functionality specialized for digital signatures, including hybrid
|
||||
* signature construction.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Typical usage</h2> <pre>{@code
|
||||
|
||||
@@ -129,7 +129,7 @@ final class Decryptor implements PlainContent {
|
||||
byte[] maybe = op.tryOpen(id, blob, material);
|
||||
if (maybe != null) {
|
||||
if (maybe.length > keyBytes) {
|
||||
if (LOG.isLoggable(Level.WARNING)) {
|
||||
if (LOG.isLoggable(Level.WARNING)) { // NOPMD
|
||||
LOG.log(Level.WARNING,
|
||||
"Suspicious material in field {0}: {1}/{2} finds the secret of length {3}, while {4} is a limit. Ignoring.",
|
||||
new Object[] { i, id, op.toString(), maybe.length, keyBytes }); // NOPMD
|
||||
|
||||
@@ -78,7 +78,7 @@ public final class PasswordRecipient implements Recipient {
|
||||
* marked as a decoy to obscure the actual number of usable recipients.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <h4>Security considerations</h4>
|
||||
* <ul>
|
||||
* <li>The caller should clear the {@code password} array after constructing the
|
||||
* recipient to minimize exposure in memory.</li>
|
||||
|
||||
@@ -48,10 +48,7 @@ import java.security.GeneralSecurityException;
|
||||
* envelope writes the identifier and a length-prefixed copy of the blob into
|
||||
* its header.
|
||||
*
|
||||
* <p>
|
||||
* Typical usage:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* <h2>Typical usage</h2> <pre>{@code
|
||||
* // RSA-OAEP recipient; the header will contain the id and the RSA-wrapped CEK
|
||||
* Recipient r = new MultiRecipientDataSourceBuilder.RsaOaepRecipient(rsaPublicKey);
|
||||
* byte[] entryBlob = r.buildRecipientEntry(cek);
|
||||
@@ -67,7 +64,7 @@ public interface Recipient {
|
||||
* intentionally unusable for recovering the content-encryption key (CEK).
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <h4>Security considerations</h4>
|
||||
* <ul>
|
||||
* <li>Decoys prevent traffic analysis from revealing how many legitimate
|
||||
* recipients are present.</li>
|
||||
|
||||
378
lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
Normal file
378
lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
Normal file
@@ -0,0 +1,378 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.derived;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import zeroecho.core.alg.hmac.HmacSpec;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.HmacDataContentBuilder;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexExporter;
|
||||
|
||||
/**
|
||||
* Builder-style utility for deriving purpose-separated key material from a
|
||||
* hybrid KEX exporter and applying it to streaming algorithm builders.
|
||||
*
|
||||
* <p>
|
||||
* This class does not implement new cryptographic primitives. It derives keying
|
||||
* bytes via HKDF labels (using {@link HybridKexExporter}) and injects them into
|
||||
* existing builder instances.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Labeling</h2>
|
||||
* <p>
|
||||
* Derivation uses a base label, plus fixed suffixes for individual fields:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code label + "/key"} for the secret key</li>
|
||||
* <li>{@code label + "/iv"} for AES IV</li>
|
||||
* <li>{@code label + "/nonce"} for ChaCha nonce</li>
|
||||
* <li>{@code label + "/aad"} for AEAD AAD (optional, if derived)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* A caller-supplied transcript binding (public handshake context) may be
|
||||
* included and will be passed to the exporter as {@code info}. This improves
|
||||
* cross-protocol separation and reduces configuration mistakes.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* Instances are mutable and not thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridDerived {
|
||||
|
||||
private final HybridKexExporter exporter;
|
||||
|
||||
private String label;
|
||||
private byte[] transcript;
|
||||
|
||||
private byte[] aadExplicit;
|
||||
private boolean aadDerive;
|
||||
private int aadDeriveLen;
|
||||
|
||||
/**
|
||||
* Creates a new derived-material builder backed by an exporter.
|
||||
*
|
||||
* @param exporter exporter seeded from a hybrid KEX result (must not be null)
|
||||
* @return new derived-material builder
|
||||
* @throws NullPointerException if exporter is null
|
||||
* @since 1.0
|
||||
*/
|
||||
public static HybridDerived from(HybridKexExporter exporter) {
|
||||
Objects.requireNonNull(exporter, "exporter");
|
||||
return new HybridDerived(exporter);
|
||||
}
|
||||
|
||||
private HybridDerived(HybridKexExporter exporter) {
|
||||
this.exporter = exporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base label used for purpose separation.
|
||||
*
|
||||
* <p>
|
||||
* The label should identify the protocol purpose of the derived material, for
|
||||
* example {@code "app/enc"} or {@code "handshake/confirm"}.
|
||||
* </p>
|
||||
*
|
||||
* @param label base label (must not be null or empty)
|
||||
* @return this builder
|
||||
* @throws NullPointerException if label is null
|
||||
* @throws IllegalArgumentException if label is empty
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridDerived label(String label) {
|
||||
Objects.requireNonNull(label, "label");
|
||||
if (label.isEmpty()) {
|
||||
throw new IllegalArgumentException("label must not be empty");
|
||||
}
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets transcript binding bytes used as exporter {@code info}.
|
||||
*
|
||||
* <p>
|
||||
* The transcript should contain only public context (negotiated suite, public
|
||||
* keys/messages, channel binding, etc.). It must not contain secrets.
|
||||
* </p>
|
||||
*
|
||||
* @param transcript transcript bytes (may be null to clear)
|
||||
* @return this builder
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridDerived transcript(byte[] transcript) {
|
||||
this.transcript = (transcript == null) ? null : transcript.clone();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies explicit AAD bytes to be injected into AEAD builders.
|
||||
*
|
||||
* <p>
|
||||
* If set, no AAD derivation is performed.
|
||||
* </p>
|
||||
*
|
||||
* @param aad AAD bytes (may be null to clear)
|
||||
* @return this builder
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridDerived aad(byte[] aad) {
|
||||
this.aadExplicit = (aad == null) ? null : aad.clone();
|
||||
this.aadDerive = false;
|
||||
this.aadDeriveLen = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests deterministic derivation of AAD bytes from the exporter.
|
||||
*
|
||||
* <p>
|
||||
* This is optional. Many applications prefer to keep AAD as an
|
||||
* application-defined, already-available public context. If derived, the AAD is
|
||||
* separated using {@code label + "/aad"}.
|
||||
* </p>
|
||||
*
|
||||
* @param aadLen number of bytes to derive (must be >= 1)
|
||||
* @return this builder
|
||||
* @throws IllegalArgumentException if aadLen < 1
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridDerived deriveAad(int aadLen) {
|
||||
if (aadLen < 1) { // NOPMD
|
||||
throw new IllegalArgumentException("aadLen must be >= 1");
|
||||
}
|
||||
this.aadDerive = true;
|
||||
this.aadDeriveLen = aadLen;
|
||||
this.aadExplicit = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an AES key and applies it (and optional IV/AAD) to the provided AES
|
||||
* builder.
|
||||
*
|
||||
* <p>
|
||||
* The returned value is the same builder instance to preserve fluent pipeline
|
||||
* construction.
|
||||
* </p>
|
||||
*
|
||||
* @param aes AES builder to configure (must not be null)
|
||||
* @param keyBits AES key size in bits (128/192/256)
|
||||
* @param ivLenBytes if > 0, derive IV of this length and inject it via
|
||||
* {@code withIv(...)}; if 0, do not set IV (header/ctx may
|
||||
* generate it)
|
||||
* @return the provided builder instance
|
||||
* @throws NullPointerException if aes is null
|
||||
* @throws IllegalArgumentException if keyBits is invalid
|
||||
* @since 1.0
|
||||
*/
|
||||
public AesDataContentBuilder applyToAesGcm(AesDataContentBuilder aes, int keyBits, int ivLenBytes) {
|
||||
Objects.requireNonNull(aes, "aes");
|
||||
validateBase();
|
||||
|
||||
int keyLenBytes = bitsToBytesStrict(keyBits);
|
||||
byte[] keyRaw = exportBytes(label + "/key", keyLenBytes); // NOPMD
|
||||
SecretKey key = new SecretKeySpec(keyRaw, "AES");
|
||||
|
||||
aes.withKey(key);
|
||||
|
||||
if (ivLenBytes > 0) {
|
||||
byte[] iv = exportBytes(label + "/iv", ivLenBytes);
|
||||
aes.withIv(iv);
|
||||
}
|
||||
|
||||
byte[] aad = resolveAad();
|
||||
if (aad != null) {
|
||||
aes.withAad(aad);
|
||||
}
|
||||
|
||||
return aes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a ChaCha key and applies it (and optional nonce/AAD) to the provided
|
||||
* ChaCha builder.
|
||||
*
|
||||
* <p>
|
||||
* The returned value is the same builder instance to preserve fluent pipeline
|
||||
* construction.
|
||||
* </p>
|
||||
*
|
||||
* @param chacha ChaCha builder to configure (must not be null)
|
||||
* @param keyBits key size in bits (typically 256)
|
||||
* @param nonceLenBytes if > 0, derive nonce of this length and inject it via
|
||||
* {@code withNonce(...)}; if 0, do not set nonce
|
||||
* (header/ctx may generate it)
|
||||
* @return the provided builder instance
|
||||
* @throws NullPointerException if chacha is null
|
||||
* @throws IllegalArgumentException if keyBits is invalid
|
||||
* @since 1.0
|
||||
*/
|
||||
public ChaChaDataContentBuilder applyToChaChaAead(ChaChaDataContentBuilder chacha, int keyBits, int nonceLenBytes) {
|
||||
Objects.requireNonNull(chacha, "chacha");
|
||||
validateBase();
|
||||
|
||||
int keyLenBytes = bitsToBytesStrict(keyBits);
|
||||
byte[] keyRaw = exportBytes(label + "/key", keyLenBytes); // NOPMD
|
||||
SecretKey key = new SecretKeySpec(keyRaw, "ChaCha20");
|
||||
|
||||
chacha.withKey(key);
|
||||
|
||||
if (nonceLenBytes > 0) {
|
||||
byte[] nonce = exportBytes(label + "/nonce", nonceLenBytes);
|
||||
chacha.withNonce(nonce);
|
||||
}
|
||||
|
||||
byte[] aad = resolveAad();
|
||||
if (aad != null) {
|
||||
chacha.withAad(aad);
|
||||
}
|
||||
|
||||
return chacha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a MAC key using the builder's recommended size and applies it to the
|
||||
* provided HMAC builder.
|
||||
*
|
||||
* <p>
|
||||
* This is the preferred integration method because it avoids duplicated
|
||||
* configuration: the HMAC variant is chosen by the builder
|
||||
* ({@link HmacDataContentBuilder#spec()}), and the key size recommendation is
|
||||
* provided by {@link HmacDataContentBuilder#recommendedKeyBits()}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The returned value is the same builder instance to preserve fluent pipeline
|
||||
* construction.
|
||||
* </p>
|
||||
*
|
||||
* @param hmac HMAC builder to configure (must not be null)
|
||||
* @return the provided builder instance
|
||||
* @throws NullPointerException if {@code hmac} is null
|
||||
* @throws IllegalStateException if this {@code HybridDerived} instance is
|
||||
* missing required base configuration
|
||||
* @since 1.0
|
||||
*/
|
||||
public HmacDataContentBuilder applyToHmac(HmacDataContentBuilder hmac) {
|
||||
Objects.requireNonNull(hmac, "hmac");
|
||||
validateBase();
|
||||
|
||||
int keyBits = hmac.recommendedKeyBits();
|
||||
return applyToHmac(hmac, keyBits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a MAC key of an explicit size (override) and applies it to the
|
||||
* provided HMAC builder.
|
||||
*
|
||||
* <p>
|
||||
* This overload exists for advanced use-cases where the application
|
||||
* intentionally chooses a key size different from
|
||||
* {@link HmacSpec#recommendedKeyBits()}, for example to align a policy across
|
||||
* different MAC functions or to satisfy interoperability constraints.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Because HMAC accepts arbitrary key lengths, this method does not attempt to
|
||||
* validate semantic suitability of {@code keyBits}. Applications that require
|
||||
* stricter controls should enforce them via policy (for example minimum bit
|
||||
* strength) and use transcript-bound labels to guarantee key separation.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The returned value is the same builder instance to preserve fluent pipeline
|
||||
* construction.
|
||||
* </p>
|
||||
*
|
||||
* @param hmac HMAC builder to configure (must not be null)
|
||||
* @param keyBits key size in bits (must be a positive multiple of 8)
|
||||
* @return the provided builder instance
|
||||
* @throws NullPointerException if {@code hmac} is null
|
||||
* @throws IllegalArgumentException if {@code keyBits} is invalid
|
||||
* @throws IllegalStateException if this {@code HybridDerived} instance is
|
||||
* missing required base configuration
|
||||
* @since 1.0
|
||||
*/
|
||||
public HmacDataContentBuilder applyToHmac(HmacDataContentBuilder hmac, int keyBits) {
|
||||
Objects.requireNonNull(hmac, "hmac");
|
||||
validateBase();
|
||||
|
||||
int keyLenBytes = bitsToBytesStrict(keyBits);
|
||||
byte[] keyRaw = exportBytes(label + "/key", keyLenBytes);
|
||||
|
||||
// Prefer raw import to avoid duplicating MAC algorithm naming and to keep the
|
||||
// builder as the source of truth.
|
||||
return hmac.importKeyRaw(keyRaw);
|
||||
}
|
||||
|
||||
private void validateBase() {
|
||||
if (label == null || label.isEmpty()) {
|
||||
throw new IllegalStateException("label must be set");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] resolveAad() {
|
||||
if (aadExplicit != null) {
|
||||
return aadExplicit.clone();
|
||||
}
|
||||
if (aadDerive) {
|
||||
return exportBytes(label + "/aad", aadDeriveLen);
|
||||
}
|
||||
return null; // NOPMD
|
||||
}
|
||||
|
||||
private byte[] exportBytes(String subLabel, int len) {
|
||||
byte[] info = transcript;
|
||||
return exporter.export(subLabel, info, len);
|
||||
}
|
||||
|
||||
private static int bitsToBytesStrict(int bits) {
|
||||
if (bits < 8 || (bits % 8) != 0) {
|
||||
throw new IllegalArgumentException("bits must be a positive multiple of 8");
|
||||
}
|
||||
return bits / 8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Derived-key utilities for integrating hybrid KEX output with streaming
|
||||
* builders.
|
||||
*
|
||||
* <p>
|
||||
* This package provides a thin, SDK-level integration layer between hybrid key
|
||||
* exchange ({@link zeroecho.sdk.hybrid.kex.HybridKexContext} /
|
||||
* {@link zeroecho.sdk.hybrid.kex.HybridKexExporter}) and streaming data-content
|
||||
* builders (for example AES/ChaCha/HMAC builders in
|
||||
* {@link zeroecho.sdk.builders.alg}).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The central concept is <b>derived material</b>: purpose-separated keying
|
||||
* bytes (key, optional IV/nonce, optional AAD) derived via HKDF labels. The
|
||||
* material is then applied to an existing builder via {@code applyTo(...)}
|
||||
* which returns the same builder instance to preserve fluent pipeline
|
||||
* construction.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Design goals</h2>
|
||||
* <ul>
|
||||
* <li>Keep cryptographic primitives unchanged; only inject derived
|
||||
* parameters.</li>
|
||||
* <li>Provide safe-by-construction key separation using labels and transcript
|
||||
* binding.</li>
|
||||
* <li>Preserve fluent builder usage by returning the original builder from
|
||||
* {@code applyTo(...)}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.hybrid.derived;
|
||||
463
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContext.java
Normal file
463
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContext.java
Normal file
@@ -0,0 +1,463 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.sdk.util.Kdf;
|
||||
|
||||
/**
|
||||
* Hybrid key exchange (KEX) context that combines a classic agreement and a
|
||||
* post-quantum KEM-style agreement.
|
||||
*
|
||||
* <p>
|
||||
* This context composes two independent shared secrets:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>Classic leg</b>: a {@link AgreementContext} (e.g. X25519, ECDH, DH)
|
||||
* that derives a raw shared secret after the peer public key is
|
||||
* configured.</li>
|
||||
* <li><b>PQC leg</b>: a {@link MessageAgreementContext} (typically ML-KEM
|
||||
* exposed via core capabilities) whose "peer message" represents the
|
||||
* encapsulation/decapsulation payload (e.g. KEM ciphertext).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Wire format</h2>
|
||||
* <p>
|
||||
* This class implements {@link MessageAgreementContext} to provide a single
|
||||
* hybrid message that can be transmitted between parties. The encoding is:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* [int classicLen][classicBytes...][int pqcLen][pqcBytes...]
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* For typical pairings such as {@code X25519 + ML-KEM}, the classic part is
|
||||
* empty because the classic leg is configured using the peer public key
|
||||
* out-of-band. If the classic leg itself is message-capable, the classic part
|
||||
* may be populated.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Key derivation</h2>
|
||||
* <p>
|
||||
* The final keying material is derived using HKDF-SHA256 (RFC 5869):
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* classicSS = classic.deriveSecret()
|
||||
* pqcSS = pqc.deriveSecret()
|
||||
* IKM = classicSS || pqcSS
|
||||
* OKM = HKDF-SHA256(IKM, salt, info, outLen)
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Intermediate raw secrets are treated as sensitive and are zeroized
|
||||
* (best-effort) once HKDF completes.
|
||||
* </p>
|
||||
*
|
||||
* <h2>CryptoContext identity</h2>
|
||||
* <p>
|
||||
* A hybrid exchange is inherently bound to multiple algorithms and keys. The
|
||||
* {@link #algorithm()} and {@link #key()} methods return representative values
|
||||
* from the classic leg to satisfy the
|
||||
* {@link zeroecho.core.context.CryptoContext} contract. Callers that require
|
||||
* full introspection should retain references to the underlying component
|
||||
* contexts.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Error handling</h2>
|
||||
* <ul>
|
||||
* <li>Malformed hybrid messages cause {@link IllegalArgumentException} in
|
||||
* {@link #setPeerMessage(byte[])}.</li>
|
||||
* <li>HKDF failures are surfaced as {@link IllegalStateException} in
|
||||
* {@link #deriveSecret()}.</li>
|
||||
* <li>{@link #close()} closes both component contexts and aggregates
|
||||
* {@link IOException}s via suppressed exceptions.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* Instances are mutable and not thread-safe; use one instance per
|
||||
* handshake/session and thread.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridKexContext implements MessageAgreementContext {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(HybridKexContext.class.getName());
|
||||
|
||||
private final HybridKexProfile profile;
|
||||
private final AgreementContext classic;
|
||||
private final MessageAgreementContext pqc;
|
||||
|
||||
private byte[] peerMessage;
|
||||
|
||||
/**
|
||||
* Creates a hybrid KEX context by composing two underlying contexts.
|
||||
*
|
||||
* @param profile hybrid profile defining HKDF binding and output length
|
||||
* @param classic classic agreement context (must not be {@code null})
|
||||
* @param pqc PQC message agreement context (must not be {@code null})
|
||||
* @throws NullPointerException if any argument is {@code null}
|
||||
* @since 1.0
|
||||
*/
|
||||
public HybridKexContext(HybridKexProfile profile, AgreementContext classic, MessageAgreementContext pqc) {
|
||||
this.profile = Objects.requireNonNull(profile, "profile");
|
||||
this.classic = Objects.requireNonNull(classic, "classic");
|
||||
this.pqc = Objects.requireNonNull(pqc, "pqc");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the representative algorithm of this context.
|
||||
*
|
||||
* <p>
|
||||
* This is currently delegated to the classic leg. Hybrid constructions bind
|
||||
* multiple algorithms; callers that need full visibility should track both legs
|
||||
* explicitly.
|
||||
* </p>
|
||||
*
|
||||
* @return representative algorithm (classic leg)
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return classic.algorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the representative key of this context.
|
||||
*
|
||||
* <p>
|
||||
* This is delegated to the classic leg to satisfy the {@code CryptoContext}
|
||||
* contract. The hybrid KEX also depends on the PQC leg keys; callers should
|
||||
* store those keys separately if needed by the application.
|
||||
* </p>
|
||||
*
|
||||
* @return representative key (classic leg)
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public Key key() {
|
||||
return classic.key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the peer public key for the classic leg.
|
||||
*
|
||||
* <p>
|
||||
* This is the usual configuration step for classic DH-style agreements. The PQC
|
||||
* leg typically does not use peer public keys through this method; however, the
|
||||
* call is forwarded on a best-effort basis for implementations that accept it.
|
||||
* </p>
|
||||
*
|
||||
* @param peer peer public key for the classic agreement
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public void setPeerPublic(PublicKey peer) {
|
||||
classic.setPeerPublic(peer);
|
||||
try {
|
||||
pqc.setPeerPublic(peer);
|
||||
} catch (RuntimeException ignore) { // NOPMD
|
||||
// KEM-style agreements usually do not accept peer public here.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies the hybrid peer message received from the remote party.
|
||||
*
|
||||
* <p>
|
||||
* The message is decoded into its classic and PQC parts. The PQC part is always
|
||||
* forwarded to the PQC leg via
|
||||
* {@link MessageAgreementContext#setPeerMessage(byte[])}. The classic part is
|
||||
* forwarded only if the classic leg also implements
|
||||
* {@link MessageAgreementContext}; otherwise it is ignored.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Passing {@code null} resets the peer-message related state of both legs
|
||||
* (best-effort).
|
||||
* </p>
|
||||
*
|
||||
* @param message hybrid message, or {@code null} to reset
|
||||
* @throws IllegalArgumentException if the provided message does not conform to
|
||||
* the hybrid encoding
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public void setPeerMessage(byte[] message) {
|
||||
if (message == null) {
|
||||
this.peerMessage = null;
|
||||
try {
|
||||
pqc.setPeerMessage(null);
|
||||
} catch (RuntimeException ignore) { // NOPMD
|
||||
}
|
||||
try {
|
||||
setClassicMessageIfSupported(null);
|
||||
} catch (RuntimeException ignore) { // NOPMD
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Parts parts;
|
||||
try {
|
||||
parts = decode(message);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Invalid hybrid peer message encoding", e);
|
||||
}
|
||||
|
||||
this.peerMessage = message.clone();
|
||||
|
||||
try {
|
||||
setClassicMessageIfSupported(parts.classicPart());
|
||||
} catch (RuntimeException ignore) { // NOPMD
|
||||
// classic leg may be non-message agreement; ignore
|
||||
}
|
||||
|
||||
byte[] pqcPart = parts.pqcPart();
|
||||
if (pqcPart != null && pqcPart.length > 0) {
|
||||
pqc.setPeerMessage(pqcPart);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hybrid message to be sent to the peer.
|
||||
*
|
||||
* <p>
|
||||
* The PQC leg typically produces a non-empty message (encapsulation ciphertext)
|
||||
* for initiator roles. The classic leg contributes an empty message unless it
|
||||
* supports message mode.
|
||||
* </p>
|
||||
*
|
||||
* @return encoded hybrid peer message
|
||||
* @throws IllegalStateException if encoding fails or the PQC leg cannot produce
|
||||
* a message in the current role
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public byte[] getPeerMessage() {
|
||||
byte[] classicMsg = getClassicMessageIfSupported();
|
||||
byte[] pqcMsg;
|
||||
try {
|
||||
pqcMsg = pqc.getPeerMessage();
|
||||
} catch (RuntimeException e) { // NOPMD
|
||||
// Responder-side KEM leg typically does not produce an outbound message.
|
||||
pqcMsg = new byte[0];
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] msg = encode(classicMsg, pqcMsg);
|
||||
this.peerMessage = msg.clone();
|
||||
return msg;
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to encode hybrid peer message", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the final hybrid shared secret (OKM) using HKDF-SHA256.
|
||||
*
|
||||
* <p>
|
||||
* This method derives both component secrets and then performs HKDF over their
|
||||
* concatenation. Higher-level protocols should additionally bind
|
||||
* transcript/context data through the HKDF {@code info} parameter
|
||||
* ({@link HybridKexProfile#hkdfInfo()}).
|
||||
* </p>
|
||||
*
|
||||
* @return derived keying material (OKM) of length
|
||||
* {@link HybridKexProfile#outLenBytes()}
|
||||
* @throws IllegalStateException if HKDF fails or underlying contexts are not
|
||||
* configured
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public byte[] deriveSecret() {
|
||||
byte[] classicSs = classic.deriveSecret();
|
||||
byte[] pqcSs = pqc.deriveSecret();
|
||||
|
||||
byte[] ikm = new byte[classicSs.length + pqcSs.length];
|
||||
System.arraycopy(classicSs, 0, ikm, 0, classicSs.length);
|
||||
System.arraycopy(pqcSs, 0, ikm, classicSs.length, pqcSs.length);
|
||||
|
||||
try {
|
||||
byte[] out = Kdf.hkdfSha256(ikm, profile.hkdfSalt(), profile.hkdfInfo(), profile.outLenBytes());
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.fine("HybridKexContext.deriveSecret(): derived OKM length=" + out.length);
|
||||
}
|
||||
return out;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException("HKDF-SHA256 failed", e);
|
||||
} finally {
|
||||
zeroize(classicSs);
|
||||
zeroize(pqcSs);
|
||||
zeroize(ikm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes both underlying contexts.
|
||||
*
|
||||
* <p>
|
||||
* If closing the second context fails after the first one already failed, the
|
||||
* second failure is added as a suppressed exception on the first one.
|
||||
* </p>
|
||||
*
|
||||
* @throws IOException if closing either leg fails
|
||||
* @since 1.0
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IOException first = null;
|
||||
|
||||
try {
|
||||
classic.close();
|
||||
} catch (IOException e) {
|
||||
first = e;
|
||||
}
|
||||
|
||||
try {
|
||||
pqc.close();
|
||||
} catch (IOException e) {
|
||||
if (first == null) {
|
||||
first = e;
|
||||
} else {
|
||||
first.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (first != null) {
|
||||
throw first;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last hybrid peer message that was produced or set on this
|
||||
* instance.
|
||||
*
|
||||
* <p>
|
||||
* This is intended for diagnostics and testing. The returned array is a
|
||||
* defensive copy.
|
||||
* </p>
|
||||
*
|
||||
* @return last hybrid message, or {@code null} if none has been produced or set
|
||||
* @since 1.0
|
||||
*/
|
||||
public byte[] lastPeerMessageOrNull() {
|
||||
return (peerMessage == null) ? null : peerMessage.clone();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Encoding helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static byte[] encode(byte[] classicMsg, byte[] pqcMsg) throws IOException {
|
||||
byte[] c = (classicMsg == null) ? new byte[0] : classicMsg;
|
||||
byte[] p = (pqcMsg == null) ? new byte[0] : pqcMsg;
|
||||
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
DataOutputStream out = new DataOutputStream(bout);
|
||||
|
||||
out.writeInt(c.length);
|
||||
out.write(c);
|
||||
|
||||
out.writeInt(p.length);
|
||||
out.write(p);
|
||||
|
||||
out.flush();
|
||||
return bout.toByteArray();
|
||||
}
|
||||
|
||||
private static Parts decode(byte[] msg) throws IOException {
|
||||
DataInputStream in = new DataInputStream(new ByteArrayInputStream(msg));
|
||||
|
||||
int cLen = in.readInt();
|
||||
if (cLen < 0) {
|
||||
throw new IOException("negative classic length");
|
||||
}
|
||||
byte[] c = new byte[cLen];
|
||||
in.readFully(c);
|
||||
|
||||
int pLen = in.readInt();
|
||||
if (pLen < 0) {
|
||||
throw new IOException("negative pqc length");
|
||||
}
|
||||
byte[] p = new byte[pLen];
|
||||
in.readFully(p);
|
||||
|
||||
return new Parts(c, p);
|
||||
}
|
||||
|
||||
private static void zeroize(byte[] b) {
|
||||
if (b == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < b.length; i++) {
|
||||
b[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getClassicMessageIfSupported() {
|
||||
if (classic instanceof MessageAgreementContext) {
|
||||
try {
|
||||
return ((MessageAgreementContext) classic).getPeerMessage();
|
||||
} catch (RuntimeException ignore) { // NOPMD
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
private void setClassicMessageIfSupported(byte[] msg) {
|
||||
if (classic instanceof MessageAgreementContext) {
|
||||
((MessageAgreementContext) classic).setPeerMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private record Parts(byte[] classicPart, byte[] pqcPart) {
|
||||
}
|
||||
}
|
||||
314
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContexts.java
Normal file
314
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContexts.java
Normal file
@@ -0,0 +1,314 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
|
||||
/**
|
||||
* Factory utilities for constructing hybrid key exchange (KEX) contexts.
|
||||
*
|
||||
* <p>
|
||||
* This class implements the SDK-level composition layer for hybrid key
|
||||
* exchange. It does not introduce new core contracts; instead, it composes
|
||||
* existing core contexts:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link AgreementContext} for the classic (pre-quantum) leg (e.g. X25519,
|
||||
* ECDH, DH), and</li>
|
||||
* <li>{@link MessageAgreementContext} for the post-quantum leg (typically a
|
||||
* KEM-style agreement, e.g. ML-KEM exposed as {@code KeyUsage.AGREEMENT}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Handshake model</h2>
|
||||
* <p>
|
||||
* The two legs have different wire semantics:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>Classic leg</b> generally uses a peer public key supplied out-of-band
|
||||
* (certificate, directory, or a higher-level handshake message). This is
|
||||
* represented by
|
||||
* {@link AgreementContext#setPeerPublic(java.security.PublicKey)} and does not
|
||||
* necessarily produce any explicit "to-be-sent" bytes.</li>
|
||||
* <li><b>PQC leg</b> is message-based: the initiator produces an encapsulation
|
||||
* message (e.g. KEM ciphertext) via
|
||||
* {@link MessageAgreementContext#getPeerMessage()}, and the responder consumes
|
||||
* it via {@link MessageAgreementContext#setPeerMessage(byte[])}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* {@link HybridKexContext} unifies these semantics by emitting and consuming a
|
||||
* single hybrid peer message that carries both legs (with the classic part
|
||||
* typically empty for classic-only public-key exchange).
|
||||
* </p>
|
||||
*
|
||||
* <h2>Error handling</h2>
|
||||
* <p>
|
||||
* Underlying context construction uses
|
||||
* {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, ContextSpec)}
|
||||
* which may throw {@link IOException}. These factory methods propagate the
|
||||
* checked exception to keep failures explicit and auditable.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* This factory is stateless and thread-safe. Produced contexts are mutable and
|
||||
* not thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridKexContexts {
|
||||
|
||||
private HybridKexContexts() {
|
||||
// utility
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initiator-side hybrid KEX context.
|
||||
*
|
||||
* <p>
|
||||
* This method constructs:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>a classic {@link AgreementContext} from the initiator's classic
|
||||
* {@link PrivateKey} and configures it with the peer's classic
|
||||
* {@link PublicKey}, and</li>
|
||||
* <li>a PQC {@link MessageAgreementContext} from the peer's PQC
|
||||
* {@link PublicKey} (encapsulation side).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The returned {@link HybridKexContext} will typically produce a peer message
|
||||
* whose PQC part is non-empty (e.g. KEM ciphertext), while the classic part is
|
||||
* empty unless the chosen classic implementation itself supports message mode.
|
||||
* </p>
|
||||
*
|
||||
* @param profile hybrid profile defining HKDF binding and
|
||||
* output length
|
||||
* @param classicAlgId classic agreement algorithm identifier (for
|
||||
* example {@code "X25519"}, {@code "ECDH"},
|
||||
* {@code "DH"})
|
||||
* @param classicInitiatorPrivate initiator private key for the classic leg
|
||||
* @param classicPeerPublic peer public key for the classic leg
|
||||
* @param classicSpec optional classic context spec (may be
|
||||
* {@code null} if the algorithm supports a
|
||||
* default)
|
||||
* @param pqcAlgId post-quantum agreement algorithm identifier
|
||||
* (for example {@code "ML-KEM"} exposed via
|
||||
* {@code KeyUsage.AGREEMENT})
|
||||
* @param pqcPeerPublic peer public key for the PQC leg (encapsulation
|
||||
* side)
|
||||
* @param pqcSpec optional PQC context spec (may be {@code null}
|
||||
* if the algorithm supports a default)
|
||||
* @return initiator-side {@link HybridKexContext}
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IOException if underlying context creation fails
|
||||
* @since 1.0
|
||||
*/
|
||||
public static HybridKexContext initiator(HybridKexProfile profile, String classicAlgId,
|
||||
PrivateKey classicInitiatorPrivate, PublicKey classicPeerPublic, ContextSpec classicSpec, String pqcAlgId,
|
||||
PublicKey pqcPeerPublic, ContextSpec pqcSpec) throws IOException {
|
||||
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classicAlgId, "classicAlgId");
|
||||
Objects.requireNonNull(classicInitiatorPrivate, "classicInitiatorPrivate");
|
||||
Objects.requireNonNull(classicPeerPublic, "classicPeerPublic");
|
||||
Objects.requireNonNull(pqcAlgId, "pqcAlgId");
|
||||
Objects.requireNonNull(pqcPeerPublic, "pqcPeerPublic");
|
||||
|
||||
AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicInitiatorPrivate,
|
||||
classicSpec);
|
||||
classic.setPeerPublic(classicPeerPublic);
|
||||
|
||||
MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec);
|
||||
|
||||
return new HybridKexContext(profile, classic, pqc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a responder-side hybrid KEX context.
|
||||
*
|
||||
* <p>
|
||||
* This method constructs:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>a classic {@link AgreementContext} from the responder's classic
|
||||
* {@link PrivateKey} and configures it with the peer's classic
|
||||
* {@link PublicKey}, and</li>
|
||||
* <li>a PQC {@link MessageAgreementContext} from the responder's PQC
|
||||
* {@link PrivateKey} (decapsulation side).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The returned {@link HybridKexContext} is typically used after receiving the
|
||||
* initiator's hybrid peer message and passing it to
|
||||
* {@link HybridKexContext#setPeerMessage(byte[])}.
|
||||
* </p>
|
||||
*
|
||||
* @param profile hybrid profile defining HKDF binding and
|
||||
* output length
|
||||
* @param classicAlgId classic agreement algorithm identifier
|
||||
* @param classicResponderPrivate responder private key for the classic leg
|
||||
* @param classicPeerPublic peer public key for the classic leg
|
||||
* @param classicSpec optional classic context spec (may be
|
||||
* {@code null})
|
||||
* @param pqcAlgId post-quantum agreement algorithm identifier
|
||||
* @param pqcResponderPrivate responder private key for the PQC leg
|
||||
* (decapsulation side)
|
||||
* @param pqcSpec optional PQC context spec (may be
|
||||
* {@code null})
|
||||
* @return responder-side {@link HybridKexContext}
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IOException if underlying context creation fails
|
||||
* @since 1.0
|
||||
*/
|
||||
public static HybridKexContext responder(HybridKexProfile profile, String classicAlgId,
|
||||
PrivateKey classicResponderPrivate, PublicKey classicPeerPublic, ContextSpec classicSpec, String pqcAlgId,
|
||||
PrivateKey pqcResponderPrivate, ContextSpec pqcSpec) throws IOException {
|
||||
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classicAlgId, "classicAlgId");
|
||||
Objects.requireNonNull(classicResponderPrivate, "classicResponderPrivate");
|
||||
Objects.requireNonNull(classicPeerPublic, "classicPeerPublic");
|
||||
Objects.requireNonNull(pqcAlgId, "pqcAlgId");
|
||||
Objects.requireNonNull(pqcResponderPrivate, "pqcResponderPrivate");
|
||||
|
||||
AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicResponderPrivate,
|
||||
classicSpec);
|
||||
classic.setPeerPublic(classicPeerPublic);
|
||||
|
||||
MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcResponderPrivate,
|
||||
pqcSpec);
|
||||
|
||||
return new HybridKexContext(profile, classic, pqc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initiator-side hybrid KEX where the classic leg is message-based
|
||||
* (PAIR_MESSAGE).
|
||||
*
|
||||
* <p>
|
||||
* The classic message is the SPKI encoding of the initiator's classic public
|
||||
* key as produced by the underlying {@link MessageAgreementContext}. The PQC
|
||||
* message is the PQC encapsulation payload (typically KEM ciphertext).
|
||||
* </p>
|
||||
*
|
||||
* @param profile hybrid profile defining HKDF binding and
|
||||
* output length
|
||||
* @param classicAlgId classic agreement algorithm identifier (e.g.
|
||||
* "X25519", "ECDH", "DH")
|
||||
* @param classicInitiatorKeyPair classic initiator key pair wrapped as
|
||||
* {@code KeyPairKey}
|
||||
* @param classicSpec classic context spec (may be {@code null} if
|
||||
* supported)
|
||||
* @param pqcAlgId post-quantum agreement algorithm identifier
|
||||
* (e.g. "ML-KEM")
|
||||
* @param pqcPeerPublic PQC peer public key (encapsulation side)
|
||||
* @param pqcSpec optional PQC context spec (may be
|
||||
* {@code null})
|
||||
* @return initiator-side {@link HybridKexContext}
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IOException if underlying context creation fails
|
||||
*/
|
||||
public static HybridKexContext initiatorPairMessage(HybridKexProfile profile, String classicAlgId,
|
||||
zeroecho.core.alg.common.agreement.KeyPairKey classicInitiatorKeyPair, ContextSpec classicSpec,
|
||||
String pqcAlgId, PublicKey pqcPeerPublic, ContextSpec pqcSpec) throws IOException {
|
||||
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classicAlgId, "classicAlgId");
|
||||
Objects.requireNonNull(classicInitiatorKeyPair, "classicInitiatorKeyPair");
|
||||
Objects.requireNonNull(pqcAlgId, "pqcAlgId");
|
||||
Objects.requireNonNull(pqcPeerPublic, "pqcPeerPublic");
|
||||
|
||||
MessageAgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT,
|
||||
classicInitiatorKeyPair, classicSpec);
|
||||
|
||||
MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec);
|
||||
|
||||
return new HybridKexContext(profile, classic, pqc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a responder-side hybrid KEX where the classic leg is message-based
|
||||
* (PAIR_MESSAGE).
|
||||
*
|
||||
* <p>
|
||||
* The responder must call {@link HybridKexContext#setPeerMessage(byte[])} with
|
||||
* the initiator's hybrid message before calling
|
||||
* {@link HybridKexContext#deriveSecret()}.
|
||||
* </p>
|
||||
*
|
||||
* @param profile hybrid profile defining HKDF binding and
|
||||
* output length
|
||||
* @param classicAlgId classic agreement algorithm identifier
|
||||
* @param classicResponderKeyPair classic responder key pair wrapped as
|
||||
* {@code KeyPairKey}
|
||||
* @param classicSpec classic context spec (may be {@code null})
|
||||
* @param pqcAlgId post-quantum agreement algorithm identifier
|
||||
* @param pqcResponderPrivate PQC responder private key (decapsulation side)
|
||||
* @param pqcSpec optional PQC context spec (may be
|
||||
* {@code null})
|
||||
* @return responder-side {@link HybridKexContext}
|
||||
* @throws NullPointerException if any required argument is {@code null}
|
||||
* @throws IOException if underlying context creation fails
|
||||
*/
|
||||
public static HybridKexContext responderPairMessage(HybridKexProfile profile, String classicAlgId,
|
||||
zeroecho.core.alg.common.agreement.KeyPairKey classicResponderKeyPair, ContextSpec classicSpec,
|
||||
String pqcAlgId, PrivateKey pqcResponderPrivate, ContextSpec pqcSpec) throws IOException {
|
||||
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classicAlgId, "classicAlgId");
|
||||
Objects.requireNonNull(classicResponderKeyPair, "classicResponderKeyPair");
|
||||
Objects.requireNonNull(pqcAlgId, "pqcAlgId");
|
||||
Objects.requireNonNull(pqcResponderPrivate, "pqcResponderPrivate");
|
||||
|
||||
MessageAgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT,
|
||||
classicResponderKeyPair, classicSpec);
|
||||
|
||||
MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcResponderPrivate,
|
||||
pqcSpec);
|
||||
|
||||
return new HybridKexContext(profile, classic, pqc);
|
||||
}
|
||||
}
|
||||
134
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexExporter.java
Normal file
134
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexExporter.java
Normal file
@@ -0,0 +1,134 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.sdk.util.Kdf;
|
||||
|
||||
/**
|
||||
* Deterministic exporter for deriving multiple independent keys from a single
|
||||
* hybrid KEX.
|
||||
*
|
||||
* <p>
|
||||
* Many protocols require more than one key from a single handshake (for
|
||||
* example, separate traffic keys for each direction, confirmation keys, or
|
||||
* application exporters). This class provides a minimal key schedule API over
|
||||
* HKDF-SHA256.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Construction</h2>
|
||||
* <p>
|
||||
* The exporter is seeded by a caller-supplied secret (typically the output of
|
||||
* {@link HybridKexContext#deriveSecret()}). The exporter then derives sub-keys
|
||||
* by varying HKDF {@code info}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security notes</h2>
|
||||
* <ul>
|
||||
* <li>Use distinct labels per purpose (for example {@code "app/tx"} vs
|
||||
* {@code "app/rx"}).</li>
|
||||
* <li>Bind transcript/context by including it in the {@code info} bytes.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridKexExporter {
|
||||
|
||||
private final byte[] rootSecret;
|
||||
private final byte[] salt;
|
||||
|
||||
/**
|
||||
* Creates an exporter seeded from a root secret.
|
||||
*
|
||||
* @param rootSecret root secret (must not be null)
|
||||
* @param salt optional HKDF salt (may be null)
|
||||
*/
|
||||
public HybridKexExporter(byte[] rootSecret, byte[] salt) {
|
||||
Objects.requireNonNull(rootSecret, "rootSecret");
|
||||
this.rootSecret = rootSecret.clone();
|
||||
this.salt = (salt == null) ? null : salt.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives {@code outLenBytes} bytes for a specific purpose.
|
||||
*
|
||||
* @param label ASCII/UTF-8 label identifying the purpose (must not be
|
||||
* null)
|
||||
* @param info optional additional binding info (may be null)
|
||||
* @param outLenBytes output length in bytes (1..8160)
|
||||
* @return derived bytes
|
||||
* @throws IllegalArgumentException if outLenBytes is out of range
|
||||
*/
|
||||
public byte[] export(String label, byte[] info, int outLenBytes) {
|
||||
Objects.requireNonNull(label, "label");
|
||||
|
||||
if (outLenBytes < 1 || outLenBytes > 255 * 32) {
|
||||
throw new IllegalArgumentException("outLenBytes must be in range 1.." + (255 * 32));
|
||||
}
|
||||
|
||||
byte[] labelBytes = label.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
byte[] infoUse;
|
||||
if (info == null || info.length == 0) {
|
||||
infoUse = labelBytes;
|
||||
} else {
|
||||
infoUse = new byte[labelBytes.length + 1 + info.length];
|
||||
System.arraycopy(labelBytes, 0, infoUse, 0, labelBytes.length);
|
||||
infoUse[labelBytes.length] = 0;
|
||||
System.arraycopy(info, 0, infoUse, labelBytes.length + 1, info.length);
|
||||
}
|
||||
|
||||
try {
|
||||
return Kdf.hkdfSha256(rootSecret, salt, infoUse, outLenBytes);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException("HKDF-SHA256 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the exporter root secret.
|
||||
*
|
||||
* <p>
|
||||
* This is intended for diagnostics/testing only. Applications should prefer
|
||||
* {@link #export(String, byte[], int)}.
|
||||
* </p>
|
||||
*
|
||||
* @return copy of root secret
|
||||
*/
|
||||
public byte[] rootSecretCopy() {
|
||||
return rootSecret.clone();
|
||||
}
|
||||
}
|
||||
124
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexPolicy.java
Normal file
124
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexPolicy.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.policy.SecurityStrengthAdvisor;
|
||||
|
||||
/**
|
||||
* Hybrid KEX policy helper for minimum security strength enforcement.
|
||||
*
|
||||
* <p>
|
||||
* This policy applies additional hybrid-specific checks beyond per-algorithm
|
||||
* policy validation. It is intended to prevent accidental downgrade
|
||||
* combinations (for example a strong PQC leg combined with a too-weak classic
|
||||
* leg, or an undersized OKM output).
|
||||
* </p>
|
||||
*
|
||||
* <h2>What is checked</h2>
|
||||
* <ul>
|
||||
* <li>Classic leg estimated strength in bits (algorithm id + key)</li>
|
||||
* <li>PQC leg estimated strength in bits (algorithm id + key)</li>
|
||||
* <li>Minimum OKM length (in bytes) for the intended usage</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Strength estimates are provided by
|
||||
* {@link SecurityStrengthAdvisor#estimateBits(String, Key)} and are
|
||||
* conservative heuristics suitable for gating and coarse comparisons.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridKexPolicy {
|
||||
|
||||
private final int minClassicBits;
|
||||
private final int minPqcBits;
|
||||
private final int minOkmBytes;
|
||||
|
||||
/**
|
||||
* Creates a hybrid policy.
|
||||
*
|
||||
* @param minClassicBits minimum estimated bits for classic leg (for example 128
|
||||
* or 192)
|
||||
* @param minPqcBits minimum estimated bits for PQC leg (for example 192)
|
||||
* @param minOkmBytes minimum OKM output length in bytes (for example 32)
|
||||
*/
|
||||
public HybridKexPolicy(int minClassicBits, int minPqcBits, int minOkmBytes) {
|
||||
if (minClassicBits < 0 || minPqcBits < 0) {
|
||||
throw new IllegalArgumentException("min bits must be non-negative");
|
||||
}
|
||||
if (minOkmBytes < 1) { // NOPMD
|
||||
throw new IllegalArgumentException("minOkmBytes must be >= 1");
|
||||
}
|
||||
this.minClassicBits = minClassicBits;
|
||||
this.minPqcBits = minPqcBits;
|
||||
this.minOkmBytes = minOkmBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the policy for a given hybrid configuration.
|
||||
*
|
||||
* @param profile hybrid profile
|
||||
* @param classic classic agreement context
|
||||
* @param pqc PQC message agreement context
|
||||
* @throws NullPointerException if any argument is null
|
||||
* @throws IllegalArgumentException if policy is violated
|
||||
*/
|
||||
public void enforce(HybridKexProfile profile, AgreementContext classic, MessageAgreementContext pqc) {
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classic, "classic");
|
||||
Objects.requireNonNull(pqc, "pqc");
|
||||
|
||||
if (profile.outLenBytes() < minOkmBytes) {
|
||||
throw new IllegalArgumentException(
|
||||
"Hybrid OKM length too small: " + profile.outLenBytes() + " < " + minOkmBytes);
|
||||
}
|
||||
|
||||
int classicBits = SecurityStrengthAdvisor.estimateBits(classic.algorithm().id(), classic.key());
|
||||
int pqcBits = SecurityStrengthAdvisor.estimateBits(pqc.algorithm().id(), pqc.key());
|
||||
|
||||
if (classicBits < minClassicBits) {
|
||||
throw new IllegalArgumentException("Classic leg too weak: " + classicBits + " < " + minClassicBits);
|
||||
}
|
||||
if (pqcBits < minPqcBits) {
|
||||
throw new IllegalArgumentException("PQC leg too weak: " + pqcBits + " < " + minPqcBits);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexProfile.java
Normal file
108
lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexProfile.java
Normal file
@@ -0,0 +1,108 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Profile for a hybrid key exchange (KEX) composition.
|
||||
*
|
||||
* <p>
|
||||
* A hybrid KEX combines two independently derived secrets:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>a classic (pre-quantum) agreement secret produced by an
|
||||
* {@code AgreementContext} (e.g. X25519, ECDH, DH), and</li>
|
||||
* <li>a post-quantum agreement secret produced by a
|
||||
* {@code MessageAgreementContext} (typically a KEM-style agreement such as
|
||||
* ML-KEM).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The two secrets are combined using HKDF-SHA256 (RFC 5869) to produce a final
|
||||
* keying material byte array of a caller-selected length. The default label is
|
||||
* intended to make cross-protocol misuse harder by providing an explicit domain
|
||||
* separator.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Notes</h2>
|
||||
* <ul>
|
||||
* <li>This profile does not store algorithm identifiers on purpose; it focuses
|
||||
* on KDF binding and output size. Algorithm selection happens in higher layers
|
||||
* when constructing the two underlying contexts.</li>
|
||||
* <li>{@code salt} is optional; if null/empty, HKDF uses a zero-filled salt as
|
||||
* per RFC 5869 and {@link zeroecho.sdk.util.Kdf}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param hkdfSalt optional HKDF salt (defensively copied), may be
|
||||
* {@code null}
|
||||
* @param hkdfInfo optional HKDF info/label (defensively copied), may be
|
||||
* {@code null}
|
||||
* @param outLenBytes length of derived keying material in bytes (1..8160)
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public record HybridKexProfile(byte[] hkdfSalt, byte[] hkdfInfo, int outLenBytes) {
|
||||
|
||||
/**
|
||||
* Default HKDF label used when the caller does not provide an explicit one.
|
||||
*/
|
||||
public static final byte[] DEFAULT_INFO = "ZeroEcho-HybridKEX".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
/**
|
||||
* Constructs a profile with a default HKDF info label.
|
||||
*
|
||||
* @param outLenBytes output length in bytes
|
||||
* @return profile with default HKDF info label and no explicit salt
|
||||
*/
|
||||
public static HybridKexProfile defaultProfile(int outLenBytes) {
|
||||
return new HybridKexProfile(null, DEFAULT_INFO, outLenBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical constructor with validation and defensive copies.
|
||||
*
|
||||
* @param hkdfSalt optional HKDF salt
|
||||
* @param hkdfInfo optional HKDF info/label
|
||||
* @param outLenBytes output length in bytes
|
||||
*/
|
||||
public HybridKexProfile {
|
||||
if (outLenBytes < 1 || outLenBytes > 255 * 32) {
|
||||
throw new IllegalArgumentException("outLenBytes must be in range 1.." + (255 * 32));
|
||||
}
|
||||
hkdfSalt = (hkdfSalt == null) ? null : hkdfSalt.clone();
|
||||
hkdfInfo = (hkdfInfo == null) ? null : hkdfInfo.clone();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Canonical transcript builder for binding hybrid KEX derivation to public
|
||||
* handshake context.
|
||||
*
|
||||
* <p>
|
||||
* The derived hybrid keying material should be bound to protocol context to
|
||||
* reduce risk of cross-protocol key reuse and "unknown key-share" style
|
||||
* mistakes. This builder provides a deterministic and self-delimiting binary
|
||||
* encoding intended to be used as HKDF {@code info}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <p>
|
||||
* The transcript is encoded as a sequence of TLV-like entries:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* [u16 tagLen][tagBytes...][u32 valueLen][valueBytes...] ...
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Tags are ASCII identifiers (for example {@code "suite"}, {@code "role"},
|
||||
* {@code "peerA"}, {@code "peerB"}, {@code "classicMsg"}, {@code "pqcMsg"}).
|
||||
* Values are arbitrary bytes. The encoding is stable across JVMs and
|
||||
* independent of locale.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* Instances are mutable and not thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridKexTranscript {
|
||||
|
||||
private final ByteArrayOutputStream buffer;
|
||||
private final DataOutputStream out;
|
||||
|
||||
/**
|
||||
* Creates a new empty transcript.
|
||||
*/
|
||||
public HybridKexTranscript() {
|
||||
this.buffer = new ByteArrayOutputStream();
|
||||
this.out = new DataOutputStream(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a UTF-8 string value under a tag.
|
||||
*
|
||||
* @param tag ASCII tag identifier
|
||||
* @param value UTF-8 string value (must not be null)
|
||||
* @return this transcript
|
||||
* @throws NullPointerException if tag or value is null
|
||||
* @throws IllegalArgumentException if tag is empty
|
||||
*/
|
||||
public HybridKexTranscript addUtf8(String tag, String value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
return addBytes(tag, value.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a raw byte value under a tag.
|
||||
*
|
||||
* <p>
|
||||
* The value is defensively copied by the caller if needed; this method does not
|
||||
* retain a reference to the provided array.
|
||||
* </p>
|
||||
*
|
||||
* @param tag ASCII tag identifier
|
||||
* @param value byte value (must not be null)
|
||||
* @return this transcript
|
||||
* @throws NullPointerException if tag or value is null
|
||||
* @throws IllegalArgumentException if tag is empty
|
||||
*/
|
||||
public HybridKexTranscript addBytes(String tag, byte[] value) {
|
||||
Objects.requireNonNull(tag, "tag");
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (tag.isEmpty()) {
|
||||
throw new IllegalArgumentException("tag must not be empty");
|
||||
}
|
||||
|
||||
byte[] tagBytes = tag.getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
try {
|
||||
if (tagBytes.length > 65_535) { // NOPMD
|
||||
throw new IllegalArgumentException("tag too long");
|
||||
}
|
||||
out.writeShort(tagBytes.length);
|
||||
out.write(tagBytes);
|
||||
out.writeInt(value.length);
|
||||
out.write(value);
|
||||
out.flush();
|
||||
return this;
|
||||
} catch (IOException e) {
|
||||
// ByteArrayOutputStream should not throw, but keep failure explicit.
|
||||
throw new IllegalStateException("Unable to encode transcript entry", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canonical transcript bytes (defensive copy).
|
||||
*
|
||||
* @return transcript bytes
|
||||
*/
|
||||
public byte[] toByteArray() {
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
}
|
||||
81
lib/src/main/java/zeroecho/sdk/hybrid/kex/package-info.java
Normal file
81
lib/src/main/java/zeroecho/sdk/hybrid/kex/package-info.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Hybrid key exchange (KEX) utilities combining a classic agreement and a
|
||||
* post-quantum KEM-style agreement into one derived shared secret.
|
||||
*
|
||||
* <p>
|
||||
* This package provides an SDK-level composition layer over existing core
|
||||
* contracts:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link zeroecho.core.context.AgreementContext} for classic DH-style
|
||||
* agreements (e.g. X25519, ECDH, DH), and</li>
|
||||
* <li>{@link zeroecho.core.context.MessageAgreementContext} for message-based
|
||||
* agreements (typically PQC KEM-style flows such as ML-KEM exposed via
|
||||
* {@code KeyUsage.AGREEMENT}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Wire model</h2>
|
||||
* <p>
|
||||
* A hybrid exchange needs explicit "to-be-sent" bytes. This package unifies the
|
||||
* model by emitting a single peer message containing two length-prefixed parts:
|
||||
* </p>
|
||||
* <ol>
|
||||
* <li>classic message (often empty for classic agreements that only require a
|
||||
* peer public key),</li>
|
||||
* <li>PQC message (typically KEM ciphertext produced by
|
||||
* {@link zeroecho.core.context.MessageAgreementContext}).</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Key derivation</h2>
|
||||
* <p>
|
||||
* The two underlying secrets are combined using HKDF-SHA256 (RFC 5869) via
|
||||
* {@link zeroecho.sdk.util.Kdf#hkdfSha256(byte[], byte[], byte[], int)} rather
|
||||
* than concatenation. Callers should treat raw intermediate secrets as
|
||||
* sensitive and clear them as soon as possible.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Intended usage</h2>
|
||||
* <p>
|
||||
* Use {@link zeroecho.sdk.hybrid.kex.HybridKexContexts} to build initiator and
|
||||
* responder contexts and exchange
|
||||
* {@link zeroecho.sdk.hybrid.kex.HybridKexContext#getPeerMessage()} between
|
||||
* parties.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.hybrid.kex;
|
||||
94
lib/src/main/java/zeroecho/sdk/hybrid/package-info.java
Normal file
94
lib/src/main/java/zeroecho/sdk/hybrid/package-info.java
Normal file
@@ -0,0 +1,94 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* SDK-level hybrid cryptography utilities.
|
||||
*
|
||||
* <p>
|
||||
* This package groups hybrid composition helpers that combine classical and
|
||||
* post-quantum primitives at the SDK layer while keeping the underlying core
|
||||
* contracts unchanged. Hybrid constructions are exposed as regular streaming
|
||||
* contexts and builder integrations, so they can be used with existing pipeline
|
||||
* APIs (for example {@link zeroecho.sdk.builders.core.DataContentChainBuilder}
|
||||
* and trailer-oriented stages).
|
||||
* </p>
|
||||
*
|
||||
* <h2>Subpackages</h2>
|
||||
* <ul>
|
||||
* <li>{@link zeroecho.sdk.hybrid.kex} - hybrid key exchange (KEX) that composes
|
||||
* a classic agreement and a message-based (KEM-style) agreement into a single
|
||||
* derived shared secret, and emits an explicit peer message suitable for
|
||||
* transport.</li>
|
||||
* <li>{@link zeroecho.sdk.hybrid.derived} - derived-key utilities that consume
|
||||
* hybrid KEX output and inject purpose-separated keying material (key, optional
|
||||
* IV/nonce, optional AAD) into streaming builders while preserving fluent
|
||||
* builder usage.</li>
|
||||
* <li>{@link zeroecho.sdk.hybrid.signature} - hybrid signature composition that
|
||||
* combines two independent signature schemes and exposes them as a single
|
||||
* streaming {@link zeroecho.core.context.SignatureContext} suitable for
|
||||
* trailer-style pipeline stages.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Design principles</h2>
|
||||
* <ul>
|
||||
* <li><b>Composition over modification</b>: hybrids are implemented as
|
||||
* SDK-level compositions over existing core contexts rather than by expanding
|
||||
* core API contracts.</li>
|
||||
* <li><b>Explicit messages where needed</b>: whenever a hybrid operation has
|
||||
* "to-be-sent" bytes (for example KEX peer messages), they are modeled as
|
||||
* explicit byte sequences rather than hidden side effects.</li>
|
||||
* <li><b>Key separation via KDF</b>: hybrid secrets are combined and expanded
|
||||
* using HKDF label separation and transcript binding; concatenation is avoided
|
||||
* as a primary combination method.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Security notes</h2>
|
||||
* <ul>
|
||||
* <li>Hybrid constructions increase protocol and implementation complexity.
|
||||
* Prefer clear profiles, stable transcript inputs, and explicit policy to avoid
|
||||
* ambiguous security expectations.</li>
|
||||
* <li>Do not log or otherwise expose sensitive material (private keys, seeds,
|
||||
* derived keying bytes, plaintexts, intermediate secrets).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* Hybrid contexts and builders are not thread-safe. Create a new instance per
|
||||
* independent operation and do not share instances across concurrent pipeline
|
||||
* executions.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.hybrid;
|
||||
@@ -0,0 +1,212 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.signature;
|
||||
|
||||
import java.security.Signature;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.core.err.VerificationException;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
/**
|
||||
* Verification predicate wrapper that captures the boolean verification result
|
||||
* and never propagates {@link VerificationException} to its caller.
|
||||
*
|
||||
* <p>
|
||||
* This wrapper delegates verification to a supplied
|
||||
* {@link VerificationBiPredicate} and records the boolean outcome into a shared
|
||||
* {@link AtomicBoolean}. If the delegate throws {@link VerificationException},
|
||||
* the exception is suppressed and the verification is treated as a failure
|
||||
* (returns {@code false}).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This behavior is required for robust hybrid verification semantics, most
|
||||
* notably for logical OR composition: individual component verifiers may throw
|
||||
* on malformed or structurally invalid signatures (for example, certain Ed25519
|
||||
* invalid point encodings). Translating such exceptions into a boolean failure
|
||||
* allows the hybrid engine to continue evaluating alternative verification
|
||||
* paths and to aggregate the final decision deterministically.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Logging</h2>
|
||||
* <p>
|
||||
* For diagnostic purposes, a {@link Level#FINE} log entry is emitted on each
|
||||
* invocation containing:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>a short hexadecimal prefix of {@code expectedTag} (bounded and
|
||||
* truncated), and</li>
|
||||
* <li>the resulting verification decision ({@code true}/{@code false}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The logged tag prefix is intentionally truncated to reduce the risk of
|
||||
* leaking sensitive material. The log message is produced using a JUL
|
||||
* formatting string (no string concatenation in the hot path) and is only
|
||||
* constructed when {@code FINE} is enabled.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Threading and side effects</h2>
|
||||
* <ul>
|
||||
* <li>This class is immutable with respect to its own fields.</li>
|
||||
* <li>The {@code ok} flag is updated exactly once per invocation with the
|
||||
* result returned by this method (including failures caused by suppressed
|
||||
* exceptions).</li>
|
||||
* <li>Correctness depends on coordinated usage of the shared
|
||||
* {@link AtomicBoolean} when used concurrently.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Security note: this class must not log keys, plaintext, shared secrets, full
|
||||
* tags, or other sensitive material. Only a bounded prefix is logged at
|
||||
* {@code FINE} level.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
final class CapturePredicate extends VerificationBiPredicate<Signature> {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(CapturePredicate.class.getName());
|
||||
|
||||
/**
|
||||
* Maximum number of bytes from {@code expectedTag} included in log output.
|
||||
*/
|
||||
private static final int TAG_LOG_PREFIX_BYTES = 8;
|
||||
|
||||
/**
|
||||
* Underlying verification predicate performing the actual cryptographic check.
|
||||
*/
|
||||
private final VerificationBiPredicate<Signature> delegate;
|
||||
|
||||
/**
|
||||
* Shared atomic flag capturing the result of the most recent verification.
|
||||
*/
|
||||
private final AtomicBoolean ok;
|
||||
|
||||
/**
|
||||
* Creates a new capturing predicate.
|
||||
*
|
||||
* @param delegate the underlying verification predicate to invoke
|
||||
* @param ok shared atomic flag receiving the verification outcome
|
||||
* @throws NullPointerException if {@code delegate} or {@code ok} is
|
||||
* {@code null}
|
||||
*/
|
||||
protected CapturePredicate(VerificationBiPredicate<Signature> delegate, AtomicBoolean ok) {
|
||||
super();
|
||||
this.delegate = Objects.requireNonNull(delegate, "delegate");
|
||||
this.ok = Objects.requireNonNull(ok, "ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate predicate, captures its boolean result, and never
|
||||
* propagates {@link VerificationException}.
|
||||
*
|
||||
* <p>
|
||||
* If the delegate completes normally, its return value is recorded into
|
||||
* {@link #ok} and returned. If the delegate throws
|
||||
* {@link VerificationException}, the exception is suppressed, {@code false} is
|
||||
* recorded into {@link #ok}, and {@code false} is returned.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* A {@link Level#FINE} log entry is emitted with a truncated hexadecimal prefix
|
||||
* of {@code expectedTag} and the resulting decision.
|
||||
* </p>
|
||||
*
|
||||
* @param signature the signature object to verify
|
||||
* @param expectedTag the expected signature tag (may be {@code null}, in which
|
||||
* case {@code "<null>"} is logged)
|
||||
* @return {@code true} if verification succeeded, {@code false} otherwise
|
||||
* @throws VerificationException never thrown by this implementation, but
|
||||
* declared to satisfy the overridden contract
|
||||
*/
|
||||
@Override
|
||||
public boolean verify(Signature signature, byte[] expectedTag) throws VerificationException {
|
||||
boolean result;
|
||||
try {
|
||||
result = delegate.verify(signature, expectedTag);
|
||||
} catch (VerificationException ex) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
ok.set(result);
|
||||
|
||||
if (LOGGER.isLoggable(Level.FINE)) {
|
||||
LOGGER.log(Level.FINE, "Hybrid verify: expectedTagPrefix={0}, result={1}",
|
||||
new Object[] { formatTagPrefix(expectedTag), result });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a short hexadecimal prefix of the provided tag for logging.
|
||||
*
|
||||
* <p>
|
||||
* At most {@link #TAG_LOG_PREFIX_BYTES} bytes are included. If the tag is
|
||||
* longer, the output is suffixed with {@code "..."}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The output format uses lowercase hexadecimal digits without separators.
|
||||
* </p>
|
||||
*
|
||||
* @param tag tag bytes to format (may be {@code null})
|
||||
* @return a bounded, log-safe hexadecimal prefix, or {@code "<null>"} if
|
||||
* {@code tag} is {@code null}
|
||||
*/
|
||||
private static String formatTagPrefix(byte[] tag) {
|
||||
if (tag == null) {
|
||||
return "<null>";
|
||||
}
|
||||
|
||||
int n = Math.min(tag.length, TAG_LOG_PREFIX_BYTES);
|
||||
StringBuilder sb = new StringBuilder(n * 2 + 3);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
sb.append(Character.forDigit((tag[i] >>> 4) & 0x0f, 16)).append(Character.forDigit(tag[i] & 0x0f, 16));
|
||||
}
|
||||
|
||||
if (tag.length > n) {
|
||||
sb.append("...");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.signature;
|
||||
|
||||
import java.security.Signature;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.core.err.VerificationException;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
/**
|
||||
* Hybrid verification core predicate delegating the final decision to the
|
||||
* hybrid engine's end-of-stream (EOF) aggregation.
|
||||
*
|
||||
* <p>
|
||||
* This predicate does not perform any cryptographic verification on its own.
|
||||
* Instead, it acts as a bridge between the generic verification pipeline and
|
||||
* the hybrid streaming engine, where the actual verification of classic and PQC
|
||||
* signatures is performed once the complete payload has been consumed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The boolean result returned by {@link #verify(Signature, byte[])} reflects
|
||||
* the outcome computed during EOF processing and stored in the shared
|
||||
* {@link AtomicBoolean} instance. This allows the verification decision to be
|
||||
* made exactly once, based on the fully buffered message body and the hybrid
|
||||
* verification rule.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Design notes</h2>
|
||||
* <ul>
|
||||
* <li>The {@code signature} and {@code expectedTag} parameters are
|
||||
* intentionally ignored, as the hybrid model defers verification until all data
|
||||
* has been read from the stream.</li>
|
||||
* <li>This class is immutable and thread-safe with respect to its own state;
|
||||
* however, correctness depends on coordinated usage with the associated hybrid
|
||||
* stream.</li>
|
||||
* <li>The predicate must be used only in conjunction with a hybrid signature
|
||||
* stream that updates the shared {@code lastOk} flag.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Security note: this predicate must not leak any sensitive material. It merely
|
||||
* returns a boolean decision computed elsewhere and does not inspect keys,
|
||||
* signatures, or plaintext data.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
final class HybridCorePredicate extends VerificationBiPredicate<Signature> {
|
||||
private static final Logger LOGGER = Logger.getLogger(HybridCorePredicate.class.getName());
|
||||
|
||||
/**
|
||||
* Holds the final hybrid verification result computed at end-of-stream.
|
||||
*/
|
||||
private final AtomicBoolean lastOk;
|
||||
|
||||
/**
|
||||
* Creates a new hybrid core predicate bound to the given verification result
|
||||
* flag.
|
||||
*
|
||||
* @param lastOk shared atomic flag holding the final hybrid verification result
|
||||
* @throws NullPointerException if {@code lastOk} is {@code null}
|
||||
*/
|
||||
protected HybridCorePredicate(AtomicBoolean lastOk) {
|
||||
super();
|
||||
this.lastOk = Objects.requireNonNull(lastOk, "lastOk");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hybrid verification result computed during EOF processing.
|
||||
*
|
||||
* <p>
|
||||
* This method performs no validation of the provided {@code signature} or
|
||||
* {@code expectedTag}. All cryptographic checks have already been executed by
|
||||
* the hybrid stream once the complete payload was available.
|
||||
* </p>
|
||||
*
|
||||
* @param signature ignored; present to satisfy the predicate contract
|
||||
* @param expectedTag ignored; present to satisfy the predicate contract
|
||||
* @return {@code true} if the hybrid verification succeeded according to the
|
||||
* configured hybrid verification rule, {@code false} otherwise
|
||||
* @throws VerificationException never thrown by this implementation, but
|
||||
* declared to satisfy the overridden contract
|
||||
*/
|
||||
@Override
|
||||
public boolean verify(Signature signature, byte[] expectedTag) throws VerificationException {
|
||||
boolean result = lastOk.get();
|
||||
|
||||
if (LOGGER.isLoggable(Level.FINE)) {
|
||||
LOGGER.log(Level.FINE, "Hybrid core verification result={0}", result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.signature;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.err.VerificationException;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
/**
|
||||
* Package-private {@link SignatureContext} that composes two signature engines.
|
||||
*
|
||||
* <p>
|
||||
* The signature trailer is {@code sigClassic || sigPqc} in the fixed order
|
||||
* defined by the {@link HybridSignatureProfile}. The trailer carries no
|
||||
* algorithm identifiers; the profile is the source of truth.
|
||||
* </p>
|
||||
*
|
||||
* <h3>Streaming semantics</h3>
|
||||
* <p>
|
||||
* This context is compatible with the existing ZeroEcho signing/verification
|
||||
* pipelines: callers wrap an {@link InputStream} and read until EOF. At EOF,
|
||||
* the underlying engines produce/verify the trailer.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Implementation note: the wrapper buffers the message body and runs the
|
||||
* component engines at EOF. This avoids any need for changes in core contracts
|
||||
* while keeping the same external streaming behavior.
|
||||
* </p>
|
||||
*
|
||||
* <h3>Verification aggregation</h3>
|
||||
* <p>
|
||||
* Verification is performed for both component signatures and aggregated by the
|
||||
* profile rule (AND / OR). The final decision is applied via this context's
|
||||
* {@link #setVerificationApproach(VerificationBiPredicate)} predicate, enabling
|
||||
* the standard ZeroEcho behavior (throw-on-mismatch, flagging, etc.).
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
final class HybridSignatureContext implements SignatureContext {
|
||||
|
||||
private final HybridSignatureProfile profile;
|
||||
|
||||
private final boolean produceMode;
|
||||
private final PrivateKey classicPrivate;
|
||||
private final PrivateKey pqcPrivate;
|
||||
private final PublicKey classicPublic;
|
||||
private final PublicKey pqcPublic;
|
||||
|
||||
private final int maxBufferedBytes;
|
||||
|
||||
private final AtomicBoolean lastOk;
|
||||
private final VerificationBiPredicate<Signature> hybridCore;
|
||||
|
||||
private VerificationBiPredicate<Signature> verificationApproach;
|
||||
private byte[] expectedTag;
|
||||
|
||||
/**
|
||||
* Creates a signing hybrid signature context.
|
||||
*
|
||||
* @param profile hybrid signature profile
|
||||
* @param classicPrivate private key for classical engine
|
||||
* @param pqcPrivate private key for PQC engine
|
||||
* @param maxBufferedBytes maximum buffered bytes (DoS guard)
|
||||
* @throws NullPointerException if {@code profile}, {@code classicPrivate},
|
||||
* or {@code pqcPrivate} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
|
||||
*/
|
||||
protected HybridSignatureContext(HybridSignatureProfile profile, PrivateKey classicPrivate, PrivateKey pqcPrivate,
|
||||
int maxBufferedBytes) {
|
||||
this.profile = Objects.requireNonNull(profile, "profile");
|
||||
this.classicPrivate = Objects.requireNonNull(classicPrivate, "classicPrivate");
|
||||
this.pqcPrivate = Objects.requireNonNull(pqcPrivate, "pqcPrivate");
|
||||
this.classicPublic = null;
|
||||
this.pqcPublic = null;
|
||||
this.produceMode = true;
|
||||
|
||||
if (maxBufferedBytes <= 0) {
|
||||
throw new IllegalArgumentException("maxBufferedBytes must be positive");
|
||||
}
|
||||
this.maxBufferedBytes = maxBufferedBytes;
|
||||
|
||||
this.lastOk = new AtomicBoolean(false);
|
||||
this.hybridCore = new HybridCorePredicate(this.lastOk);
|
||||
this.verificationApproach = this.hybridCore;
|
||||
this.expectedTag = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verifying hybrid signature context.
|
||||
*
|
||||
* @param profile hybrid signature profile
|
||||
* @param classicPublic public key for classical engine
|
||||
* @param pqcPublic public key for PQC engine
|
||||
* @param maxBufferedBytes maximum buffered bytes (DoS guard)
|
||||
* @throws NullPointerException if {@code profile}, {@code classicPublic},
|
||||
* or {@code pqcPublic} is {@code null}
|
||||
* @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
|
||||
*/
|
||||
protected HybridSignatureContext(HybridSignatureProfile profile, PublicKey classicPublic, PublicKey pqcPublic,
|
||||
int maxBufferedBytes) {
|
||||
this.profile = Objects.requireNonNull(profile, "profile");
|
||||
this.classicPublic = Objects.requireNonNull(classicPublic, "classicPublic");
|
||||
this.pqcPublic = Objects.requireNonNull(pqcPublic, "pqcPublic");
|
||||
this.classicPrivate = null;
|
||||
this.pqcPrivate = null;
|
||||
this.produceMode = false;
|
||||
|
||||
if (maxBufferedBytes <= 0) {
|
||||
throw new IllegalArgumentException("maxBufferedBytes must be positive");
|
||||
}
|
||||
this.maxBufferedBytes = maxBufferedBytes;
|
||||
|
||||
this.lastOk = new AtomicBoolean(false);
|
||||
this.hybridCore = new HybridCorePredicate(this.lastOk);
|
||||
this.verificationApproach = this.hybridCore;
|
||||
this.expectedTag = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream wrap(InputStream upstream) throws IOException {
|
||||
Objects.requireNonNull(upstream, "upstream");
|
||||
return new HybridStream(upstream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int tagLength() {
|
||||
try (SignatureContext classic = createClassic(produceMode ? KeyUsage.SIGN : KeyUsage.VERIFY);
|
||||
SignatureContext pqc = createPqc(produceMode ? KeyUsage.SIGN : KeyUsage.VERIFY)) {
|
||||
return classic.tagLength() + pqc.tagLength();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to determine hybrid tag length", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExpectedTag(byte[] expected) {
|
||||
if (produceMode) {
|
||||
throw new UnsupportedOperationException("Expected tag is not used in sign mode");
|
||||
}
|
||||
Objects.requireNonNull(expected, "expected");
|
||||
this.expectedTag = expected.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVerificationApproach(VerificationBiPredicate<Signature> approach) {
|
||||
this.verificationApproach = Objects.requireNonNull(approach, "approach");
|
||||
}
|
||||
|
||||
@Override
|
||||
public VerificationBiPredicate<Signature> getVerificationCore() {
|
||||
return hybridCore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return CryptoAlgorithms.require(profile.classicSigId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key key() {
|
||||
return produceMode ? classicPrivate : classicPublic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Stateless at context level; per-stream resources are closed by the stream.
|
||||
}
|
||||
|
||||
private SignatureContext createClassic(KeyUsage usage) throws IOException {
|
||||
ContextSpec spec = profile.classicSpec();
|
||||
if (usage == KeyUsage.SIGN) {
|
||||
return CryptoAlgorithms.create(profile.classicSigId(), usage, classicPrivate, spec);
|
||||
}
|
||||
return CryptoAlgorithms.create(profile.classicSigId(), usage, classicPublic, spec);
|
||||
}
|
||||
|
||||
private SignatureContext createPqc(KeyUsage usage) throws IOException {
|
||||
ContextSpec spec = profile.pqcSpec();
|
||||
if (usage == KeyUsage.SIGN) {
|
||||
return CryptoAlgorithms.create(profile.pqcSigId(), usage, pqcPrivate, spec);
|
||||
}
|
||||
return CryptoAlgorithms.create(profile.pqcSigId(), usage, pqcPublic, spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal {@link InputStream} wrapper that transparently appends or validates
|
||||
* a hybrid-signature trailer at end-of-stream.
|
||||
*
|
||||
* <p>
|
||||
* The stream proxies reads from an underlying upstream {@link InputStream}.
|
||||
* While reading, it buffers the payload body (bounded by
|
||||
* {@code maxBufferedBytes}) to enable hybrid signature processing at EOF:
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>Produce mode</b> ({@code produceMode == true}): on upstream EOF, two
|
||||
* signatures are computed (classic + PQC) over the buffered body and then
|
||||
* emitted as a trailer appended to the stream.</li>
|
||||
* <li><b>Verify mode</b> ({@code produceMode == false}): on upstream EOF, the
|
||||
* expected tag is split into classic/PQC components based on tag lengths, both
|
||||
* signatures are verified against the buffered body, and the final verification
|
||||
* result is stored into {@code lastOk}. No trailer bytes are emitted in verify
|
||||
* mode.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Lifecycle and invariants</h2>
|
||||
* <ul>
|
||||
* <li>EOF finalization ({@link #finishAtEof()}) is executed at most once,
|
||||
* guarded by {@link #eofSeen}.</li>
|
||||
* <li>In produce mode, the trailer is emitted strictly after all upstream
|
||||
* bytes.</li>
|
||||
* <li>In verify mode, the stream ends immediately after the upstream ends.</li>
|
||||
* <li>This class is not thread-safe and assumes sequential consumption.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Security note: this implementation must not expose sensitive materials (keys,
|
||||
* seeds, plaintext, signatures, or intermediate state) via logging or exception
|
||||
* messages. Exceptions raised by this stream are limited to generic error
|
||||
* descriptions and non-sensitive metadata (e.g., length and limits).
|
||||
* </p>
|
||||
*/
|
||||
private final class HybridStream extends InputStream {
|
||||
/** The wrapped upstream stream providing the primary data. */
|
||||
private final InputStream upstream;
|
||||
|
||||
/**
|
||||
* Buffer accumulating upstream bytes required for hybrid signature processing.
|
||||
* The buffer is bounded to mitigate unbounded memory growth.
|
||||
*/
|
||||
private final ByteArrayOutputStream buffer;
|
||||
|
||||
/**
|
||||
* Indicates whether end-of-stream has already been observed on the upstream.
|
||||
* Ensures EOF finalization logic executes exactly once.
|
||||
*/
|
||||
private boolean eofSeen;
|
||||
|
||||
/**
|
||||
* Trailer bytes to be emitted after upstream exhaustion in produce mode, or
|
||||
* {@code null} when no trailer is present (e.g., verify mode).
|
||||
*/
|
||||
private byte[] trailer;
|
||||
|
||||
/**
|
||||
* Current emission position within {@link #trailer}.
|
||||
*/
|
||||
private int trailerPos;
|
||||
|
||||
/**
|
||||
* Creates a new hybrid stream wrapping the provided upstream.
|
||||
*
|
||||
* @param upstream the underlying input stream supplying the primary data; must
|
||||
* not be {@code null}
|
||||
*/
|
||||
private HybridStream(InputStream upstream) {
|
||||
super();
|
||||
this.upstream = upstream;
|
||||
this.buffer = new ByteArrayOutputStream(Math.min(8192, maxBufferedBytes));
|
||||
this.eofSeen = false;
|
||||
this.trailer = null;
|
||||
this.trailerPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single byte from this stream.
|
||||
*
|
||||
* <p>
|
||||
* Delegates to {@link #read(byte[], int, int)} and follows the standard
|
||||
* {@link InputStream} contract.
|
||||
* </p>
|
||||
*
|
||||
* @return the next byte of data as an unsigned value in the range
|
||||
* {@code 0–255}, or {@code -1} if the stream is exhausted (including
|
||||
* any trailer)
|
||||
* @throws IOException if an I/O error occurs or EOF finalization fails
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] one = new byte[1];
|
||||
int r = read(one, 0, 1);
|
||||
if (r == -1) {
|
||||
return -1;
|
||||
}
|
||||
return one[0] & 0xff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to {@code len} bytes of data into {@code b}, starting at
|
||||
* {@code off}.
|
||||
*
|
||||
* <p>
|
||||
* The method first serves any pending trailer bytes (produce mode). Otherwise
|
||||
* it reads from the upstream stream, buffering all successfully read bytes for
|
||||
* later hybrid signature processing at EOF. When the upstream stream returns
|
||||
* {@code -1} for the first time, {@link #finishAtEof()} is invoked and may
|
||||
* either prepare a trailer for emission (produce mode) or perform verification
|
||||
* (verify mode).
|
||||
* </p>
|
||||
*
|
||||
* @param b destination buffer
|
||||
* @param off offset at which to start storing bytes
|
||||
* @param len maximum number of bytes to read
|
||||
* @return the number of bytes read, or {@code -1} if the stream is exhausted
|
||||
* @throws IOException if an I/O error occurs, the internal buffer
|
||||
* limit is exceeded, or EOF finalization
|
||||
* (signature creation/verification) fails
|
||||
* @throws IndexOutOfBoundsException if {@code off} or {@code len} are invalid
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
Objects.checkFromIndexSize(off, len, b.length);
|
||||
|
||||
if (trailer != null) {
|
||||
if (trailerPos >= trailer.length) {
|
||||
return -1;
|
||||
}
|
||||
int n = Math.min(len, trailer.length - trailerPos);
|
||||
System.arraycopy(trailer, trailerPos, b, off, n);
|
||||
trailerPos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
int r = upstream.read(b, off, len);
|
||||
if (r == -1) {
|
||||
if (!eofSeen) {
|
||||
eofSeen = true;
|
||||
finishAtEof();
|
||||
}
|
||||
if (trailer != null && trailer.length > 0) {
|
||||
return read(b, off, len);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (r > 0) {
|
||||
writeToBuffer(b, off, r);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this stream and releases the underlying upstream resource.
|
||||
*
|
||||
* <p>
|
||||
* Note: closing this stream closes the wrapped {@code upstream} stream.
|
||||
* </p>
|
||||
*
|
||||
* @throws IOException if closing the upstream fails
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
upstream.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the specified slice of bytes into the internal body buffer.
|
||||
*
|
||||
* <p>
|
||||
* The buffer is bounded by {@code maxBufferedBytes}. If appending would exceed
|
||||
* the configured limit, the method fails fast with an {@link IOException}.
|
||||
* </p>
|
||||
*
|
||||
* @param b source buffer
|
||||
* @param off offset within {@code b}
|
||||
* @param len number of bytes to append
|
||||
* @throws IOException if the buffer limit would be exceeded
|
||||
*/
|
||||
private void writeToBuffer(byte[] b, int off, int len) throws IOException {
|
||||
if (buffer.size() + len > maxBufferedBytes) {
|
||||
throw new IOException("Hybrid signature buffer limit exceeded: " + maxBufferedBytes);
|
||||
}
|
||||
buffer.write(b, off, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes processing when upstream EOF is reached.
|
||||
*
|
||||
* <p>
|
||||
* This method is invoked exactly once upon the first observation of upstream
|
||||
* EOF. It uses the buffered body to either:
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>Produce mode</b>: compute the classic and PQC signatures over the
|
||||
* body, concatenate them, and expose them as a trailer to be emitted by
|
||||
* subsequent {@code read(...)} calls.</li>
|
||||
* <li><b>Verify mode</b>: split the expected tag into classic/PQC signature
|
||||
* components, verify each against the body, combine results using the profile
|
||||
* verify rule, update {@code lastOk}, and delegate to
|
||||
* {@code verificationApproach} for any additional policy enforcement. No
|
||||
* trailer is produced.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws IOException if required inputs are missing, if signature
|
||||
* creation/verification fails, if the expected tag has an
|
||||
* invalid length, or if the verification approach rejects
|
||||
* the tag
|
||||
*/
|
||||
private void finishAtEof() throws IOException {
|
||||
byte[] body = buffer.toByteArray();
|
||||
|
||||
if (produceMode) {
|
||||
byte[] sigClassic = signOne(profile.classicSigId(), classicPrivate, profile.classicSpec(), body);
|
||||
byte[] sigPqc = signOne(profile.pqcSigId(), pqcPrivate, profile.pqcSpec(), body);
|
||||
trailer = concat(sigClassic, sigPqc);
|
||||
trailerPos = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] exp = expectedTag;
|
||||
if (exp == null) {
|
||||
throw new IOException("Expected tag not set");
|
||||
}
|
||||
|
||||
VerificationSplit split = splitExpected(exp);
|
||||
|
||||
boolean okClassic = verifyOne(profile.classicSigId(), classicPublic, profile.classicSpec(), body,
|
||||
split.expectedClassic);
|
||||
boolean okPqc = verifyOne(profile.pqcSigId(), pqcPublic, profile.pqcSpec(), body, split.expectedPqc);
|
||||
|
||||
boolean finalOk = (profile.verifyRule() == HybridSignatureProfile.VerifyRule.OR) ? (okClassic || okPqc)
|
||||
: (okClassic && okPqc);
|
||||
|
||||
lastOk.set(finalOk);
|
||||
|
||||
try {
|
||||
verificationApproach.verify(null, exp);
|
||||
} catch (VerificationException e) {
|
||||
throw new IOException("Hybrid signature verification failed", e);
|
||||
}
|
||||
|
||||
trailer = null;
|
||||
trailerPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the expected hybrid tag into classic and PQC signature components.
|
||||
*
|
||||
* <p>
|
||||
* The split is determined by querying tag lengths from freshly created
|
||||
* verification contexts (classic and PQC). The expected tag must match the
|
||||
* exact combined length {@code classicLen + pqcLen}; otherwise an
|
||||
* {@link IOException} is thrown.
|
||||
* </p>
|
||||
*
|
||||
* @param exp expected hybrid tag bytes containing concatenated classic and PQC
|
||||
* signatures
|
||||
* @return a value object holding the classic and PQC expected signatures
|
||||
* @throws IOException if context initialization fails or {@code exp} has an
|
||||
* invalid length
|
||||
*/
|
||||
private VerificationSplit splitExpected(byte[] exp) throws IOException {
|
||||
int classicLen;
|
||||
int pqcLen;
|
||||
|
||||
try (SignatureContext classic = createClassic(KeyUsage.VERIFY);
|
||||
SignatureContext pqc = createPqc(KeyUsage.VERIFY)) {
|
||||
classicLen = classic.tagLength();
|
||||
pqcLen = pqc.tagLength();
|
||||
}
|
||||
|
||||
int total = classicLen + pqcLen;
|
||||
if (exp.length != total) {
|
||||
throw new IOException("Invalid expected tag length: " + exp.length + ", expected " + total);
|
||||
}
|
||||
|
||||
byte[] eClassic = new byte[classicLen];
|
||||
byte[] ePqc = new byte[pqcLen];
|
||||
|
||||
System.arraycopy(exp, 0, eClassic, 0, classicLen);
|
||||
System.arraycopy(exp, classicLen, ePqc, 0, pqcLen);
|
||||
|
||||
return new VerificationSplit(eClassic, ePqc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple value object holding the classic and PQC portions of an expected
|
||||
* hybrid signature tag.
|
||||
*
|
||||
* <p>
|
||||
* Instances are created only after the expected tag length is validated and
|
||||
* then used to feed the per-algorithm verification routines.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Security note: this structure stores signature bytes; it must not be logged
|
||||
* or exposed outside the narrow verification flow.
|
||||
* </p>
|
||||
*/
|
||||
private static final class VerificationSplit {
|
||||
/** Expected signature bytes for the classic algorithm. */
|
||||
private final byte[] expectedClassic;
|
||||
|
||||
/** Expected signature bytes for the PQC algorithm. */
|
||||
private final byte[] expectedPqc;
|
||||
|
||||
/**
|
||||
* Constructs the split expected-tag view.
|
||||
*
|
||||
* @param expectedClassic expected classic signature bytes; must not be
|
||||
* {@code null}
|
||||
* @param expectedPqc expected PQC signature bytes; must not be {@code null}
|
||||
*/
|
||||
private VerificationSplit(byte[] expectedClassic, byte[] expectedPqc) {
|
||||
this.expectedClassic = expectedClassic;
|
||||
this.expectedPqc = expectedPqc;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signOne(String id, PrivateKey key, ContextSpec spec, byte[] body) throws IOException {
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
|
||||
try (SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, key, spec);
|
||||
InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(body)),
|
||||
signer.tagLength(), 8192) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) throws IOException {
|
||||
if (tail == null || tail.length == 0) {
|
||||
throw new IOException("Empty signature trailer for " + id);
|
||||
}
|
||||
sigHolder[0] = tail.clone();
|
||||
}
|
||||
}) {
|
||||
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
|
||||
byte[] sig = sigHolder[0];
|
||||
if (sig == null) {
|
||||
throw new IOException("Signature trailer missing for " + id);
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean verifyOne(String id, PublicKey key, ContextSpec spec, byte[] body, byte[] expected)
|
||||
throws IOException {
|
||||
|
||||
AtomicBoolean ok = new AtomicBoolean(false);
|
||||
|
||||
try (SignatureContext verifier = CryptoAlgorithms.create(id, KeyUsage.VERIFY, key, spec)) {
|
||||
verifier.setVerificationApproach(new CapturePredicate(verifier.getVerificationCore(), ok));
|
||||
verifier.setExpectedTag(expected);
|
||||
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(body))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
}
|
||||
|
||||
return ok.get();
|
||||
}
|
||||
|
||||
private static byte[] concat(byte[] a, byte[] b) {
|
||||
byte[] out = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, out, 0, a.length);
|
||||
System.arraycopy(b, 0, out, a.length, b.length);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.signature;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
|
||||
/**
|
||||
* Factory for {@link SignatureContext}-compatible hybrid signature contexts.
|
||||
*
|
||||
* <p>
|
||||
* The returned contexts implement the standard ZeroEcho streaming contract:
|
||||
* wrapping a stream produces/verifies a signature trailer at EOF.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HybridSignatureContexts {
|
||||
|
||||
private HybridSignatureContexts() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signing hybrid signature context.
|
||||
*
|
||||
* @param profile hybrid signature profile
|
||||
* @param classicPrivate private key for the classical signature engine
|
||||
* @param pqcPrivate private key for the PQC signature engine
|
||||
* @param maxBufferedBytes maximum number of bytes buffered from the wrapped
|
||||
* stream (DoS guard)
|
||||
* @return signing signature context
|
||||
* @throws NullPointerException if any mandatory argument is {@code null}
|
||||
* @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
|
||||
* @since 1.0
|
||||
*/
|
||||
public static SignatureContext sign(HybridSignatureProfile profile, PrivateKey classicPrivate,
|
||||
PrivateKey pqcPrivate, int maxBufferedBytes) {
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classicPrivate, "classicPrivate");
|
||||
Objects.requireNonNull(pqcPrivate, "pqcPrivate");
|
||||
return new HybridSignatureContext(profile, classicPrivate, pqcPrivate, maxBufferedBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verification hybrid signature context.
|
||||
*
|
||||
* @param profile hybrid signature profile
|
||||
* @param classicPublic public key for the classical signature engine
|
||||
* @param pqcPublic public key for the PQC signature engine
|
||||
* @param maxBufferedBytes maximum number of bytes buffered from the wrapped
|
||||
* stream (DoS guard)
|
||||
* @return verifying signature context
|
||||
* @throws NullPointerException if any mandatory argument is {@code null}
|
||||
* @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
|
||||
* @since 1.0
|
||||
*/
|
||||
public static SignatureContext verify(HybridSignatureProfile profile, PublicKey classicPublic, PublicKey pqcPublic,
|
||||
int maxBufferedBytes) {
|
||||
Objects.requireNonNull(profile, "profile");
|
||||
Objects.requireNonNull(classicPublic, "classicPublic");
|
||||
Objects.requireNonNull(pqcPublic, "pqcPublic");
|
||||
return new HybridSignatureContext(profile, classicPublic, pqcPublic, maxBufferedBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.signature;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
|
||||
/**
|
||||
* Immutable definition of a hybrid signature composition.
|
||||
*
|
||||
* {@code HybridSignatureProfile} is a pure configuration object that defines:
|
||||
* <ul>
|
||||
* <li>which two signature algorithms participate in the hybrid
|
||||
* composition,</li>
|
||||
* <li>their fixed ordering within the produced signature trailer,</li>
|
||||
* <li>the verification aggregation rule applied to their results.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The profile is the <em>single source of truth</em> for hybrid signature
|
||||
* processing. No algorithm identifiers or metadata are embedded in the
|
||||
* signature trailer itself; both signing and verification sides must use the
|
||||
* same profile.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Instances of this record are immutable and thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @param classicSigId canonical identifier of the classical signature algorithm
|
||||
* (for example {@code "Ed25519"})
|
||||
* @param pqcSigId canonical identifier of the post-quantum signature
|
||||
* algorithm (for example {@code "ML-DSA-65"})
|
||||
* @param classicSpec optional {@link ContextSpec} for the classical signature
|
||||
* engine; may be {@code null}
|
||||
* @param pqcSpec optional {@link ContextSpec} for the post-quantum
|
||||
* signature engine; may be {@code null}
|
||||
* @param verifyRule aggregation rule applied during verification
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public record HybridSignatureProfile(String classicSigId, String pqcSigId, ContextSpec classicSpec, ContextSpec pqcSpec,
|
||||
VerifyRule verifyRule) {
|
||||
|
||||
/**
|
||||
* Verification aggregation rule for hybrid signatures.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public enum VerifyRule {
|
||||
/**
|
||||
* Both component signatures must verify successfully.
|
||||
*/
|
||||
AND,
|
||||
|
||||
/**
|
||||
* At least one component signature must verify successfully.
|
||||
*/
|
||||
OR
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical constructor with invariant checks.
|
||||
*
|
||||
* <p>
|
||||
* Uses {@link Objects#requireNonNull(Object, String)} to enforce mandatory
|
||||
* components while keeping validation idiomatic and consistent with the rest of
|
||||
* the ZeroEcho codebase.
|
||||
* </p>
|
||||
*
|
||||
* @throws NullPointerException if {@code classicSigId}, {@code pqcSigId}, or
|
||||
* {@code verifyRule} is {@code null}
|
||||
*/
|
||||
public HybridSignatureProfile {
|
||||
Objects.requireNonNull(classicSigId, "classicSigId");
|
||||
Objects.requireNonNull(pqcSigId, "pqcSigId");
|
||||
Objects.requireNonNull(verifyRule, "verifyRule");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Hybrid signature composition for streaming pipelines.
|
||||
*
|
||||
* <p>
|
||||
* This package provides SDK-level hybrid signatures that combine two
|
||||
* independent signature schemes (typically a classical and a post-quantum
|
||||
* algorithm) and expose them as a single streaming signature engine suitable
|
||||
* for {@link zeroecho.sdk.builders.TagTrailerDataContentBuilder} and related
|
||||
* pipeline stages.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Concept</h2>
|
||||
* <p>
|
||||
* A hybrid signature computes two component signatures over the same message
|
||||
* stream and then aggregates verification according to a configured rule:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>AND</b> - verification succeeds only if both component signatures
|
||||
* verify.</li>
|
||||
* <li><b>OR</b> - verification succeeds if at least one component signature
|
||||
* verifies.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The AND rule is intended for security-hardening scenarios (both schemes must
|
||||
* hold). The OR rule is intended for migration/fallback scenarios (accept if at
|
||||
* least one scheme verifies), and should be used with clear policy and
|
||||
* operational intent.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Main types</h2>
|
||||
* <ul>
|
||||
* <li>{@link zeroecho.sdk.hybrid.signature.HybridSignatureProfile} - immutable
|
||||
* configuration describing the two component algorithms, optional per-algorithm
|
||||
* specs, and the verification rule.</li>
|
||||
* <li>{@link zeroecho.sdk.hybrid.signature.HybridSignatureContexts} - factory
|
||||
* methods for creating hybrid signing and verification contexts from keys and a
|
||||
* {@link zeroecho.sdk.hybrid.signature.HybridSignatureProfile}.</li>
|
||||
* <li>{@link zeroecho.sdk.hybrid.signature.HybridSignatureContext} - the
|
||||
* streaming hybrid {@link zeroecho.core.context.SignatureContext}
|
||||
* implementation that computes/verifies two signatures in a single pass.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Integration with pipeline builders</h2>
|
||||
* <p>
|
||||
* The hybrid signature contexts created by this package are intended to be used
|
||||
* as engines in trailer-oriented pipeline stages. In particular:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link zeroecho.sdk.builders.SignatureTrailerDataContentBuilder} can
|
||||
* construct hybrid contexts directly and is the preferred signature-specialized
|
||||
* builder API.</li>
|
||||
* <li>{@link zeroecho.sdk.builders.TagTrailerDataContentBuilder} can also be
|
||||
* used with a hybrid {@link zeroecho.core.context.SignatureContext} when
|
||||
* generic tag handling is desired.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Streaming and resource management</h2>
|
||||
* <p>
|
||||
* Hybrid signature computation and verification are streaming operations. The
|
||||
* resulting contexts and streams must be closed to release resources and to
|
||||
* finalize tag/signature production or verification.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security notes</h2>
|
||||
* <ul>
|
||||
* <li>Hybrid verification should be configured explicitly for mismatch handling
|
||||
* (throw-on-mismatch vs. capture/flag) at the pipeline layer. This package
|
||||
* focuses on computing/verifying the two component signatures and returning an
|
||||
* aggregated result.</li>
|
||||
* <li>Implementations must not log or otherwise expose sensitive material
|
||||
* (private keys, seeds, message contents, intermediate state).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread safety</h2>
|
||||
* <p>
|
||||
* Context instances are not thread-safe and are intended for single-use in a
|
||||
* single pipeline execution. Create a new context instance for each independent
|
||||
* signing or verification operation.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.hybrid.signature;
|
||||
@@ -14,10 +14,12 @@ zeroecho.core.alg.frodo.FrodoAlgorithm
|
||||
zeroecho.core.alg.hmac.HmacAlgorithm
|
||||
zeroecho.core.alg.hqc.HqcAlgorithm
|
||||
zeroecho.core.alg.kyber.KyberAlgorithm
|
||||
zeroecho.core.alg.mldsa.MldsaAlgorithm
|
||||
zeroecho.core.alg.ntru.NtruAlgorithm
|
||||
zeroecho.core.alg.ntruprime.NtrulPrimeAlgorithm
|
||||
zeroecho.core.alg.ntruprime.SntruPrimeAlgorithm
|
||||
zeroecho.core.alg.rsa.RsaAlgorithm
|
||||
zeroecho.core.alg.saber.SaberAlgorithm
|
||||
zeroecho.core.alg.slhdsa.SlhDsaAlgorithm
|
||||
zeroecho.core.alg.sphincsplus.SphincsPlusAlgorithm
|
||||
zeroecho.core.alg.xdh.XdhAlgorithm
|
||||
|
||||
@@ -3,6 +3,7 @@ handlers = java.util.logging.ConsoleHandler
|
||||
|
||||
zeroecho.core.tag.ByteVerificationStrategy.level = FINE
|
||||
zeroecho.core.tag.SignatureVerificationStrategy.level = FINE
|
||||
zeroecho.sdk.hybrid.signature.level = FINE
|
||||
|
||||
# Console handler uses our one-line formatter
|
||||
java.util.logging.ConsoleHandler.level = ALL
|
||||
|
||||
@@ -36,7 +36,6 @@ package zeroecho.core.alg.common.agreement;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -88,20 +87,26 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
System.out.println(method + "...ok");
|
||||
}
|
||||
|
||||
private static void printCapabilityHeader(String algId, String branch, Capability cap) {
|
||||
System.out.println("...algId=" + algId + ", branch=" + branch + ", ctxType=" + cap.contextType().getSimpleName()
|
||||
+ ", keyType=" + cap.keyType().getSimpleName() + ", specType=" + cap.specType().getSimpleName());
|
||||
}
|
||||
|
||||
private static String hex(byte[] b) {
|
||||
if (b == null) {
|
||||
return "null";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(b.length * 2);
|
||||
for (byte v : b) {
|
||||
for (int i = 0; i < b.length; i++) {
|
||||
if (sb.length() == 80) {
|
||||
sb.append("...");
|
||||
break;
|
||||
}
|
||||
if ((v & 0xFF) < 16) {
|
||||
int v = b[i] & 0xFF;
|
||||
if (v < 16) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(Integer.toHexString(v & 0xFF));
|
||||
sb.append(Integer.toHexString(v));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -118,8 +123,13 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// If needed, install/activate providers (e.g., BouncyCastlePQCProvider) here.
|
||||
// Optional: activate providers (e.g., BC/BCPQC) if present.
|
||||
// Keep tests runnable even if BC is absent.
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -135,43 +145,140 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
}
|
||||
|
||||
final List<Capability> caps = alg.listCapabilities();
|
||||
|
||||
for (Capability cap : caps) {
|
||||
if (cap.role() != KeyUsage.AGREEMENT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.printf(" ...%s - AGREEMENT capability found", id);
|
||||
System.out.println("...algId=" + id + " - AGREEMENT capability found");
|
||||
|
||||
final Class<?> ctxType = cap.contextType();
|
||||
final Class<?> keyType = cap.keyType();
|
||||
final Class<?> specType = cap.specType();
|
||||
@SuppressWarnings("unused")
|
||||
final ContextSpec defVal = cap.defaultSpec().get();
|
||||
|
||||
// System.out.printf(" ......type=[%s] key=[%s] spec=[%s] default=[%s]%n",
|
||||
// ctxType, keyType, specType, defVal);
|
||||
// --------------------------------------------------------------------
|
||||
// MessageAgreementContext branch A: PAIR_MESSAGE (DH/XDH/ECDH-style)
|
||||
//
|
||||
// New extension:
|
||||
// - contextType: MessageAgreementContext
|
||||
// - keyType: KeyPairKey (wrapper for KeyPair, because capabilities require Key)
|
||||
// - specType: ContextSpec (algorithm-specific agreement spec)
|
||||
//
|
||||
// Semantics:
|
||||
// - getPeerMessage() returns local public key encoding (SPKI)
|
||||
// - setPeerMessage(...) imports peer public key encoding
|
||||
// --------------------------------------------------------------------
|
||||
if (MessageAgreementContext.class.isAssignableFrom(ctxType) && keyType == KeyPairKey.class
|
||||
&& ContextSpec.class.isAssignableFrom(specType)) {
|
||||
|
||||
// AGREEMENT (KEM-style) via MessageAgreementContext adapter
|
||||
if (MessageAgreementContext.class.isAssignableFrom(ctxType)) {
|
||||
printCapabilityHeader(alg.id(), "PAIR_MESSAGE", cap);
|
||||
|
||||
KeyPair bob = generateKeyPair(alg);
|
||||
if (bob == null) {
|
||||
System.out.println(" ...bob=null");
|
||||
ContextSpec spec = null;
|
||||
try {
|
||||
spec = cap.defaultSpec().get();
|
||||
} catch (Throwable ignore) {
|
||||
spec = tryExtractContextSpec(alg);
|
||||
}
|
||||
if (spec == null) {
|
||||
System.out.println("...spec=null (skip)");
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("runAgreement(" + alg.id() + ", VoidSpec)");
|
||||
System.out.println(" Bob.public " + keyInfo("key", bob.getPublic()));
|
||||
System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||
KeyPair alice = generateKeyPair(alg);
|
||||
KeyPair bob = generateKeyPair(alg);
|
||||
if (alice == null || bob == null) {
|
||||
System.out.println("...keypair=null (skip)");
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("...pair-message agreement roundtrip: algId=" + alg.id() + ", spec="
|
||||
+ spec.getClass().getSimpleName());
|
||||
System.out.println("...Alice.public " + keyInfo("key", alice.getPublic()));
|
||||
System.out.println("...Alice.private " + keyInfo("key", alice.getPrivate()));
|
||||
System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
|
||||
System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||
|
||||
KeyPairKey aliceKey = new KeyPairKey(alice);
|
||||
KeyPairKey bobKey = new KeyPairKey(bob);
|
||||
|
||||
MessageAgreementContext aCtx = null;
|
||||
MessageAgreementContext bCtx = null;
|
||||
|
||||
try {
|
||||
aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec);
|
||||
bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bobKey, spec);
|
||||
|
||||
byte[] aMsg = aCtx.getPeerMessage();
|
||||
byte[] bMsg = bCtx.getPeerMessage();
|
||||
|
||||
aCtx.setPeerMessage(bMsg);
|
||||
bCtx.setPeerMessage(aMsg);
|
||||
|
||||
byte[] zA = aCtx.deriveSecret();
|
||||
byte[] zB = bCtx.deriveSecret();
|
||||
|
||||
System.out.println("...Alice.msg =" + hex(aMsg));
|
||||
System.out.println("...Bob.msg =" + hex(bMsg));
|
||||
System.out.println("...Alice.secret =" + hex(zA));
|
||||
System.out.println("...Bob.secret =" + hex(zB));
|
||||
|
||||
assertArrayEquals(zA, zB, alg.id() + ": PAIR_MESSAGE secrets mismatch");
|
||||
System.out.println("...PAIR_MESSAGE...ok");
|
||||
} finally {
|
||||
if (aCtx != null) {
|
||||
try {
|
||||
aCtx.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (bCtx != null) {
|
||||
try {
|
||||
bCtx.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For a given algorithm, one AGREEMENT capability is sufficient for the sweep.
|
||||
break;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// MessageAgreementContext branch B: KEM_ADAPTER (KEM-style adapter)
|
||||
//
|
||||
// Existing behavior:
|
||||
// - initiator has recipient PublicKey (encapsulation)
|
||||
// - responder has PrivateKey (decapsulation)
|
||||
// - spec is VoidSpec
|
||||
// --------------------------------------------------------------------
|
||||
if (MessageAgreementContext.class.isAssignableFrom(ctxType)
|
||||
&& (keyType == PublicKey.class || keyType == PrivateKey.class)) {
|
||||
|
||||
printCapabilityHeader(alg.id(), "KEM_ADAPTER", cap);
|
||||
|
||||
KeyPair bob = generateKeyPair(alg);
|
||||
if (bob == null) {
|
||||
System.out.println("...keypair=null (skip)");
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("...kem-adapter agreement roundtrip: algId=" + alg.id() + ", spec=VoidSpec");
|
||||
System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
|
||||
System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||
|
||||
MessageAgreementContext aliceCtx = null;
|
||||
MessageAgreementContext bobCtx = null;
|
||||
|
||||
try {
|
||||
// Alice (initiator): has Bob's public key
|
||||
MessageAgreementContext aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT,
|
||||
bob.getPublic(), VoidSpec.INSTANCE);
|
||||
aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(),
|
||||
VoidSpec.INSTANCE);
|
||||
|
||||
// Bob (responder): has his private key
|
||||
MessageAgreementContext bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT,
|
||||
bob.getPrivate(), VoidSpec.INSTANCE);
|
||||
bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
|
||||
VoidSpec.INSTANCE);
|
||||
|
||||
// Initiator produces encapsulation message (ciphertext) to send
|
||||
byte[] enc = aliceCtx.getPeerMessage();
|
||||
@@ -182,86 +289,112 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
byte[] kA = aliceCtx.deriveSecret();
|
||||
byte[] kB = bobCtx.deriveSecret();
|
||||
|
||||
System.out.println(" KEM.ciphertext=" + hex(enc));
|
||||
System.out.println(" Alice.secret =" + hex(kA));
|
||||
System.out.println(" Bob.secret =" + hex(kB));
|
||||
|
||||
assertArrayEquals(kA, kB, alg.id() + ": agreement secrets mismatch");
|
||||
System.out.println("...ok");
|
||||
System.out.println("...KEM.ciphertext=" + hex(enc));
|
||||
System.out.println("...Alice.secret =" + hex(kA));
|
||||
System.out.println("...Bob.secret =" + hex(kB));
|
||||
|
||||
assertArrayEquals(kA, kB, alg.id() + ": KEM_ADAPTER secrets mismatch");
|
||||
System.out.println("...KEM_ADAPTER...ok");
|
||||
} finally {
|
||||
if (aliceCtx != null) {
|
||||
try {
|
||||
aliceCtx.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (bobCtx != null) {
|
||||
try {
|
||||
bobCtx.close();
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// -------- Classic DH/XDH: AgreementContext + real ContextSpec --------
|
||||
else if (AgreementContext.class.isAssignableFrom(ctxType)
|
||||
&& ContextSpec.class.isAssignableFrom(specType) && keyType == PrivateKey.class) {
|
||||
// --------------------------------------------------------------------
|
||||
// AgreementContext branch C: CLASSIC_AGREEMENT (PrivateKey + ContextSpec)
|
||||
// --------------------------------------------------------------------
|
||||
if (AgreementContext.class.isAssignableFrom(ctxType) && keyType == PrivateKey.class
|
||||
&& ContextSpec.class.isAssignableFrom(specType)) {
|
||||
|
||||
KeyPair alice;
|
||||
KeyPair bob;
|
||||
printCapabilityHeader(alg.id(), "CLASSIC_AGREEMENT", cap);
|
||||
|
||||
alice = generateKeyPair(alg);
|
||||
bob = generateKeyPair(alg);
|
||||
|
||||
if (alice == null || bob == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prefer the capability's default ContextSpec if provided
|
||||
ContextSpec spec = null;
|
||||
try {
|
||||
ContextSpec def = cap.defaultSpec().get();
|
||||
spec = def;
|
||||
spec = cap.defaultSpec().get();
|
||||
} catch (Throwable ignore) {
|
||||
spec = tryExtractContextSpec(alg);
|
||||
}
|
||||
if (spec == null) {
|
||||
System.out.println("...spec=null (skip)");
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("runAgreement(" + alg.id() + ", " + spec.getClass().getSimpleName() + ")");
|
||||
System.out.println(" Alice.public " + keyInfo("key", alice.getPublic()));
|
||||
System.out.println(" Alice.private " + keyInfo("key", alice.getPrivate()));
|
||||
System.out.println(" Bob.public " + keyInfo("key", bob.getPublic()));
|
||||
System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||
KeyPair alice = generateKeyPair(alg);
|
||||
KeyPair bob = generateKeyPair(alg);
|
||||
|
||||
// assertDhCompatible(alice.getPrivate(), bob.getPublic());
|
||||
// assertDhCompatible(bob.getPrivate(), alice.getPublic());
|
||||
if (alice == null || bob == null) {
|
||||
System.out.println("...keypair=null (skip)");
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("...classic agreement roundtrip: algId=" + alg.id() + ", spec="
|
||||
+ spec.getClass().getSimpleName());
|
||||
System.out.println("...Alice.public " + keyInfo("key", alice.getPublic()));
|
||||
System.out.println("...Alice.private " + keyInfo("key", alice.getPrivate()));
|
||||
System.out.println("...Bob.public " + keyInfo("key", bob.getPublic()));
|
||||
System.out.println("...Bob.private " + keyInfo("key", bob.getPrivate()));
|
||||
|
||||
// Optional DH sanity check (only when both sides are DH keys).
|
||||
try {
|
||||
assertDhCompatible(alice.getPrivate(), bob.getPublic());
|
||||
assertDhCompatible(bob.getPrivate(), alice.getPublic());
|
||||
} catch (Throwable ignore) {
|
||||
// not DH, or provider-specific checks not applicable; continue anyway
|
||||
}
|
||||
|
||||
AgreementContext aCtx = null;
|
||||
AgreementContext bCtx = null;
|
||||
|
||||
try {
|
||||
aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec);
|
||||
bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec);
|
||||
|
||||
AgreementContext aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(),
|
||||
spec);
|
||||
AgreementContext bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(),
|
||||
spec);
|
||||
aCtx.setPeerPublic(bob.getPublic());
|
||||
bCtx.setPeerPublic(alice.getPublic());
|
||||
|
||||
byte[] zA = aCtx.deriveSecret();
|
||||
byte[] zB = bCtx.deriveSecret();
|
||||
|
||||
System.out.println(" Alice.secret =" + hex(zA));
|
||||
System.out.println(" Bob.secret =" + hex(zB));
|
||||
|
||||
assertArrayEquals(zA, zB, alg.id() + ": DH/XDH secrets mismatch");
|
||||
System.out.println("...ok");
|
||||
System.out.println("...Alice.secret =" + hex(zA));
|
||||
System.out.println("...Bob.secret =" + hex(zB));
|
||||
|
||||
assertArrayEquals(zA, zB, alg.id() + ": CLASSIC_AGREEMENT secrets mismatch");
|
||||
System.out.println("...CLASSIC_AGREEMENT...ok");
|
||||
} finally {
|
||||
if (aCtx != null) {
|
||||
try {
|
||||
aCtx.close();
|
||||
} catch (IOException ignore) {
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (bCtx != null) {
|
||||
try {
|
||||
bCtx.close();
|
||||
} catch (IOException ignore) {
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do not break: a single algorithm may have multiple agreement capabilities
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logEnd();
|
||||
@@ -271,19 +404,17 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
private static KeyPair generateKeyPair(CryptoAlgorithm alg) {
|
||||
try {
|
||||
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
||||
// System.out.println(" ...keyPair " + bi.getClass().getName());
|
||||
if (bi.defaultKeySpec == null) {
|
||||
// System.out.println(" ......skip default = null --> it was for keyImport");
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) bi.specType;
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(specType);
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> builder = alg.asymmetricKeyBuilder(specType);
|
||||
AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec;
|
||||
// System.out.println(" ......generated from " + spec);
|
||||
return b.generateKeyPair(spec);
|
||||
return builder.generateKeyPair(spec);
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -300,6 +431,7 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -315,15 +447,13 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
DHPrivateKey dhPriv = (DHPrivateKey) priv;
|
||||
DHPublicKey dhPub = (DHPublicKey) pub;
|
||||
|
||||
// 1) Parameter compatibility: same p,g and l-compatible (0 means "unspecified")
|
||||
DHParameterSpec a = dhPriv.getParams();
|
||||
DHParameterSpec b = dhPub.getParams();
|
||||
if (a == null || b == null) {
|
||||
throw new InvalidKeyException("Missing DH parameters on one of the keys");
|
||||
}
|
||||
if (!a.getP().equals(b.getP()) || !a.getG().equals(b.getG())) {
|
||||
System.out.printf(" ...a.p=%s%n ...b.p=%s%n ...a.g=%s%n ...b.g=%s%n", a.getP(), b.getP(), a.getG(),
|
||||
b.getG());
|
||||
System.out.printf("...a.p=%s%n...b.p=%s%n...a.g=%s%n...b.g=%s%n", a.getP(), b.getP(), a.getG(), b.getG());
|
||||
throw new InvalidKeyException("Incompatible DH parameters: (p,g) differ");
|
||||
}
|
||||
int la = a.getL();
|
||||
@@ -333,8 +463,6 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
"Incompatible DH parameters: private value length (l) differs: " + la + " vs " + lb);
|
||||
}
|
||||
|
||||
// 2) Public value Y sanity check: 2 <= Y <= p-2 (reject trivial/small subgroup
|
||||
// values)
|
||||
BigInteger p = a.getP();
|
||||
BigInteger y = dhPub.getY();
|
||||
if (y == null) {
|
||||
@@ -345,12 +473,10 @@ public class AgreementAlgorithmsRoundTripTest {
|
||||
throw new InvalidKeyException("DH public value Y out of range");
|
||||
}
|
||||
|
||||
// 3) Trial doPhase to prove the pair actually works with the provider
|
||||
KeyAgreement ka = KeyAgreement.getInstance("DiffieHellman");
|
||||
try {
|
||||
ka.init(priv);
|
||||
ka.doPhase(pub, true); // if this throws, they are not operationally compatible
|
||||
// (we don't need the secret here; just proving doPhase succeeds)
|
||||
ka.doPhase(pub, true);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("KeyAgreement.doPhase failed: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.mldsa;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Large-data streaming test for ML-DSA integration.
|
||||
*
|
||||
* <p>
|
||||
* Follows project rule "10) JUnit testy": prints test name, prints intermediate
|
||||
* results with the {@code "..."} prefix, and prints {@code "...ok"} on success.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class MldsaLargeDataTest {
|
||||
|
||||
private static final String INDENT = "...";
|
||||
private static final int MAX_HEX_BYTES = 32;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
|
||||
// etc.)
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep tests runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the complete ML-DSA parameter set suite in streaming mode.
|
||||
*
|
||||
* @throws Exception on test failure
|
||||
*/
|
||||
@Test
|
||||
void mldsa_complete_suite_streaming_sign_verify_large_data() throws Exception {
|
||||
System.out.println("mldsa_complete_suite_streaming_sign_verify_large_data");
|
||||
|
||||
if (!CryptoAlgorithms.available().contains("ML-DSA")) {
|
||||
System.out.println(INDENT + " *** SKIP *** ML-DSA not registered");
|
||||
System.out.println(INDENT + "ok");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] msg = randomBytes(48 * 1024 + 123);
|
||||
System.out.println(INDENT + " msg.len=" + msg.length);
|
||||
System.out.println(INDENT + " msg.hex=" + hexTruncated(msg, MAX_HEX_BYTES));
|
||||
|
||||
runCase(msg, MldsaKeyGenSpec.ParameterSet.ML_DSA_44, MldsaKeyGenSpec.PreHash.NONE);
|
||||
runCase(msg, MldsaKeyGenSpec.ParameterSet.ML_DSA_65, MldsaKeyGenSpec.PreHash.NONE);
|
||||
runCase(msg, MldsaKeyGenSpec.ParameterSet.ML_DSA_87, MldsaKeyGenSpec.PreHash.NONE);
|
||||
|
||||
runCase(msg, MldsaKeyGenSpec.ParameterSet.ML_DSA_44, MldsaKeyGenSpec.PreHash.SHA512);
|
||||
runCase(msg, MldsaKeyGenSpec.ParameterSet.ML_DSA_65, MldsaKeyGenSpec.PreHash.SHA512);
|
||||
runCase(msg, MldsaKeyGenSpec.ParameterSet.ML_DSA_87, MldsaKeyGenSpec.PreHash.SHA512);
|
||||
|
||||
System.out.println(INDENT + "ok");
|
||||
}
|
||||
|
||||
private static void runCase(byte[] msg, MldsaKeyGenSpec.ParameterSet ps, MldsaKeyGenSpec.PreHash preHash)
|
||||
throws Exception {
|
||||
|
||||
MldsaKeyGenSpec spec = MldsaKeyGenSpec.of("BC", ps, preHash);
|
||||
|
||||
String caseId = "ML-DSA " + ps.name() + " preHash=" + preHash.name();
|
||||
System.out.println(INDENT + " case=" + safeText(caseId));
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("ML-DSA", spec);
|
||||
|
||||
SignatureContext verifierCtx = CryptoAlgorithms.create("ML-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
if (!(verifierCtx instanceof MldsaSignatureContext mldsaVerifier)) {
|
||||
try {
|
||||
verifierCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
throw new AssertionError(
|
||||
"VERIFY context must be MldsaSignatureContext, got: " + verifierCtx.getClass().getName());
|
||||
}
|
||||
|
||||
int expectedSigLen = mldsaVerifier.tagLength();
|
||||
System.out.println(INDENT + " expectedSigLen=" + expectedSigLen);
|
||||
|
||||
SignatureContext signer = CryptoAlgorithms.create("ML-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
byte[] passthrough;
|
||||
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), expectedSigLen,
|
||||
8192) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) throws IOException {
|
||||
sigHolder[0] = (tail == null) ? null : Arrays.copyOf(tail, tail.length);
|
||||
}
|
||||
}) {
|
||||
passthrough = readAll(in);
|
||||
} finally {
|
||||
try {
|
||||
signer.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(msg, passthrough, "SIGN passthrough mismatch");
|
||||
|
||||
byte[] signature = sigHolder[0];
|
||||
assertNotNull(signature, "signature trailer missing");
|
||||
|
||||
System.out.println(INDENT + " signature.len=" + signature.length);
|
||||
System.out.println(INDENT + " signature.hex=" + hexTruncated(signature, MAX_HEX_BYTES));
|
||||
|
||||
if (signature.length != expectedSigLen) {
|
||||
try {
|
||||
mldsaVerifier.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
throw new AssertionError(
|
||||
"Signature length mismatch: got=" + signature.length + " expected=" + expectedSigLen);
|
||||
}
|
||||
|
||||
mldsaVerifier.setExpectedTag(Arrays.copyOf(signature, signature.length));
|
||||
|
||||
byte[] verifyOut;
|
||||
try (InputStream verIn = mldsaVerifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
verifyOut = readAll(verIn);
|
||||
} finally {
|
||||
try {
|
||||
mldsaVerifier.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(msg, verifyOut, "VERIFY passthrough mismatch");
|
||||
System.out.println(INDENT + " verify=accepted");
|
||||
|
||||
// Negative case: bit flip.
|
||||
byte[] badSig = Arrays.copyOf(signature, signature.length);
|
||||
badSig[0] = (byte) (badSig[0] ^ 0x01);
|
||||
|
||||
SignatureContext badVerifierCtx = CryptoAlgorithms.create("ML-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
if (!(badVerifierCtx instanceof MldsaSignatureContext badVerifier)) {
|
||||
try {
|
||||
badVerifierCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
throw new AssertionError("VERIFY context must be MldsaSignatureContext (negative), got: "
|
||||
+ badVerifierCtx.getClass().getName());
|
||||
}
|
||||
|
||||
try {
|
||||
badVerifier.setExpectedTag(badSig);
|
||||
badVerifier.setVerificationApproach(badVerifier.getVerificationCore().getThrowOnMismatch());
|
||||
try (InputStream verBad = badVerifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
readAll(verBad);
|
||||
}
|
||||
throw new AssertionError("Expected verification failure for mismatched signature");
|
||||
} catch (Exception expected) {
|
||||
System.out.println(INDENT + " verify=reject (mismatch)");
|
||||
} finally {
|
||||
try {
|
||||
badVerifier.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int len) {
|
||||
byte[] data = new byte[len];
|
||||
SecureRandom rnd = new SecureRandom();
|
||||
rnd.nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws IOException {
|
||||
try (InputStream src = in; ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = src.read(buf)) != -1) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeText(String s) {
|
||||
if (s == null) {
|
||||
return "null";
|
||||
}
|
||||
if (s.length() <= 30) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, 30) + "...";
|
||||
}
|
||||
|
||||
private static String hexTruncated(byte[] data, int maxBytes) {
|
||||
if (data == null) {
|
||||
return "null";
|
||||
}
|
||||
int n = Math.min(data.length, maxBytes);
|
||||
StringBuilder sb = new StringBuilder(n * 2 + 3);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int v = data[i] & 0xFF;
|
||||
sb.append(HEX[(v >>> 4) & 0x0F]);
|
||||
sb.append(HEX[v & 0x0F]);
|
||||
}
|
||||
if (data.length > maxBytes) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final char[] HEX = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
|
||||
'e', 'f' };
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.core.alg.slhdsa;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Large-data streaming test for SLH-DSA integration.
|
||||
*
|
||||
* <p>
|
||||
* Signature length is determined via {@link SlhDsaSignatureContext} created for
|
||||
* verification (public key). If the verification context is not an instance of
|
||||
* {@link SlhDsaSignatureContext}, the test fails.
|
||||
* </p>
|
||||
*/
|
||||
public final class SlhDsaLargeDataTest {
|
||||
|
||||
private static final String INDENT = "...";
|
||||
private static final int MAX_HEX_BYTES = 32;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
|
||||
// etc.)
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep tests runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void slhdsa_complete_suite_streaming_sign_verify_large_data() throws Exception {
|
||||
String testName = "slhdsa_complete_suite_streaming_sign_verify_large_data";
|
||||
System.out.println(testName);
|
||||
|
||||
if (!CryptoAlgorithms.available().contains("SLH-DSA")) {
|
||||
System.out.println(INDENT + " *** SKIP *** SLH-DSA not registered");
|
||||
System.out.println(INDENT + "ok");
|
||||
return;
|
||||
}
|
||||
|
||||
int payloadLen = 48 * 1024 + 123;
|
||||
byte[] msg = randomBytes(payloadLen);
|
||||
|
||||
System.out.println(INDENT + " msg.len=" + msg.length);
|
||||
System.out.println(INDENT + " msg.hex=" + hexTruncated(msg, MAX_HEX_BYTES));
|
||||
|
||||
// Complete suite: 128/192/256 x FAST/SMALL, for both SHA2 and SHAKE, no
|
||||
// pre-hash
|
||||
runSuiteForHash(msg, SlhDsaKeyGenSpec.Hash.SHA2);
|
||||
runSuiteForHash(msg, SlhDsaKeyGenSpec.Hash.SHAKE);
|
||||
|
||||
System.out.println(INDENT + "ok");
|
||||
}
|
||||
|
||||
private static void runSuiteForHash(byte[] msg, SlhDsaKeyGenSpec.Hash hash) throws Exception {
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L1_128, SlhDsaKeyGenSpec.Variant.FAST);
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L1_128, SlhDsaKeyGenSpec.Variant.SMALL);
|
||||
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L3_192, SlhDsaKeyGenSpec.Variant.FAST);
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L3_192, SlhDsaKeyGenSpec.Variant.SMALL);
|
||||
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L5_256, SlhDsaKeyGenSpec.Variant.FAST);
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L5_256, SlhDsaKeyGenSpec.Variant.SMALL);
|
||||
}
|
||||
|
||||
private static void runCase(byte[] msg, SlhDsaKeyGenSpec.Hash hash, SlhDsaKeyGenSpec.Security sec,
|
||||
SlhDsaKeyGenSpec.Variant variant) throws Exception {
|
||||
|
||||
SlhDsaKeyGenSpec spec = SlhDsaKeyGenSpec.of("BC", hash, sec, variant, SlhDsaKeyGenSpec.PreHash.NONE);
|
||||
|
||||
String caseId = "SLH-DSA " + hash.name() + " " + sec.name() + " " + variant.name();
|
||||
System.out.println(INDENT + " case=" + safeText(caseId));
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("SLH-DSA", spec);
|
||||
|
||||
// Create verifier FIRST to obtain tag length via
|
||||
// SlhDsaSignatureContext.sigLenFromPublicKey.
|
||||
SignatureContext verifierCtx = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
int expectedSigLen = verifierCtx.tagLength();
|
||||
System.out.println(INDENT + " expectedSigLen=" + expectedSigLen);
|
||||
|
||||
// Now sign and strip trailer using the expected length from verifier (not from
|
||||
// signer).
|
||||
SignatureContext signer = CryptoAlgorithms.create("SLH-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
byte[] passthrough;
|
||||
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), expectedSigLen,
|
||||
8192) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) throws IOException {
|
||||
sigHolder[0] = (tail == null ? null : Arrays.copyOf(tail, tail.length));
|
||||
}
|
||||
}) {
|
||||
passthrough = readAll(in);
|
||||
} finally {
|
||||
try {
|
||||
signer.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(msg, passthrough, "SIGN passthrough mismatch");
|
||||
|
||||
byte[] signature = sigHolder[0];
|
||||
assertNotNull(signature, "signature trailer missing");
|
||||
System.out.println(INDENT + " signature.len=" + signature.length);
|
||||
System.out.println(INDENT + " signature.hex=" + hexTruncated(signature, MAX_HEX_BYTES));
|
||||
|
||||
if (signature.length != expectedSigLen) {
|
||||
try {
|
||||
verifierCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
throw new AssertionError(
|
||||
"Signature length mismatch: got=" + signature.length + " expected=" + expectedSigLen);
|
||||
}
|
||||
|
||||
// Verify OK with expected signature (use already-created SLH verifier).
|
||||
verifierCtx.setExpectedTag(Arrays.copyOf(signature, signature.length));
|
||||
byte[] verifyOut;
|
||||
try (InputStream verIn = verifierCtx.wrap(new ByteArrayInputStream(msg))) {
|
||||
verifyOut = readAll(verIn);
|
||||
} finally {
|
||||
try {
|
||||
verifierCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(msg, verifyOut, "VERIFY passthrough mismatch");
|
||||
System.out.println(INDENT + " verify=accepted");
|
||||
|
||||
// Negative: bit flip and expect rejection (new verifier instance, must again be
|
||||
// our context).
|
||||
byte[] badSig = Arrays.copyOf(signature, signature.length);
|
||||
badSig[0] = (byte) (badSig[0] ^ 0x01);
|
||||
|
||||
SignatureContext badVerifier = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
try {
|
||||
badVerifier.setExpectedTag(badSig);
|
||||
badVerifier.setVerificationApproach(badVerifier.getVerificationCore().getThrowOnMismatch());
|
||||
try (InputStream verBad = badVerifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
readAll(verBad);
|
||||
}
|
||||
throw new AssertionError("Expected verification failure for mismatched signature");
|
||||
} catch (Exception expected) {
|
||||
System.out.println(INDENT + " verify=reject (mismatch)");
|
||||
} finally {
|
||||
try {
|
||||
badVerifier.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int len) {
|
||||
byte[] data = new byte[len];
|
||||
SecureRandom rnd = new SecureRandom();
|
||||
rnd.nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws IOException {
|
||||
try (InputStream src = in; ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = src.read(buf)) != -1) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeText(String s) {
|
||||
if (s == null) {
|
||||
return "null";
|
||||
}
|
||||
if (s.length() <= 30) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, 30) + "...";
|
||||
}
|
||||
|
||||
private static String hexTruncated(byte[] data, int maxBytes) {
|
||||
if (data == null) {
|
||||
return "null";
|
||||
}
|
||||
int n = Math.min(data.length, maxBytes);
|
||||
StringBuilder sb = new StringBuilder(n * 2 + 3);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int v = data[i] & 0xFF;
|
||||
sb.append(HEX[(v >>> 4) & 0x0F]);
|
||||
sb.append(HEX[v & 0x0F]);
|
||||
}
|
||||
if (data.length > maxBytes) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final char[] HEX = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
|
||||
'e', 'f' };
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.derived;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.HmacDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainBytesBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexExporter;
|
||||
|
||||
/**
|
||||
* Coverage tests for {@link HybridDerived} derived-material application
|
||||
* helpers.
|
||||
*
|
||||
* <p>
|
||||
* The tests use deterministic exporter inputs (fixed OKM and salt) to ensure
|
||||
* stable results.
|
||||
* </p>
|
||||
*/
|
||||
public class HybridDerivedTest {
|
||||
|
||||
@Test
|
||||
void aes_gcm_applyTo_roundtrip() throws Exception {
|
||||
System.out.println("aes_gcm_applyTo_roundtrip()");
|
||||
HybridKexExporter exporter = testExporter();
|
||||
|
||||
byte[] transcript = "demo-transcript".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] aad = "aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(1024, (byte) 0x5A);
|
||||
|
||||
AesDataContentBuilder encAes = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
|
||||
AesDataContentBuilder returnedEnc = HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript)
|
||||
.aad(aad).applyToAesGcm(encAes, 256, 12);
|
||||
|
||||
System.out.println("...returnedEncSame=" + (returnedEnc == encAes));
|
||||
assertSame(encAes, returnedEnc);
|
||||
|
||||
byte[] ciphertext = runEncrypt(encAes, msg);
|
||||
System.out.println("...ciphertextLen=" + ciphertext.length);
|
||||
System.out.println("...ciphertextPrefix=" + shortHex(ciphertext, 32));
|
||||
|
||||
AesDataContentBuilder decAes = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
|
||||
HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad).applyToAesGcm(decAes, 256,
|
||||
12);
|
||||
|
||||
byte[] out = runDecrypt(decAes, ciphertext);
|
||||
System.out.println("...outPrefix=" + shortHex(out, 32));
|
||||
|
||||
assertArrayEquals(msg, out);
|
||||
System.out.println("aes_gcm_applyTo_roundtrip...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aes_gcm_applyTo_negative_label_mismatch() throws Exception {
|
||||
System.out.println("aes_gcm_applyTo_negative_label_mismatch()");
|
||||
HybridKexExporter exporter = testExporter();
|
||||
|
||||
byte[] transcript = "demo-transcript".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] aad = "aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(256, (byte) 0x1C);
|
||||
|
||||
AesDataContentBuilder encAes = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
|
||||
HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad).applyToAesGcm(encAes, 256,
|
||||
12);
|
||||
|
||||
byte[] ciphertext = runEncrypt(encAes, msg);
|
||||
System.out.println("...ciphertextLen=" + ciphertext.length);
|
||||
|
||||
AesDataContentBuilder decAesWrong = AesDataContentBuilder.builder().withHeader().modeGcm(128);
|
||||
|
||||
// ...label mismatch -> wrong key/iv/aad -> decryption must fail
|
||||
HybridDerived.from(exporter).label("app/enc/aes_WRONG").transcript(transcript).aad(aad)
|
||||
.applyToAesGcm(decAesWrong, 256, 12);
|
||||
|
||||
assertThrows(Exception.class, () -> runDecrypt(decAesWrong, ciphertext));
|
||||
|
||||
System.out.println("aes_gcm_applyTo_negative_label_mismatch...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chacha_aead_applyTo_roundtrip() throws Exception {
|
||||
System.out.println("chacha_aead_applyTo_roundtrip()");
|
||||
HybridKexExporter exporter = testExporter();
|
||||
|
||||
byte[] transcript = "demo-transcript".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] aad = "aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(777, (byte) 0x33);
|
||||
|
||||
ChaChaDataContentBuilder encChaCha = ChaChaDataContentBuilder.builder().withHeader();
|
||||
|
||||
ChaChaDataContentBuilder returnedEnc = HybridDerived.from(exporter).label("app/enc/chacha")
|
||||
.transcript(transcript).aad(aad).applyToChaChaAead(encChaCha, 256, 12);
|
||||
|
||||
System.out.println("...returnedEncSame=" + (returnedEnc == encChaCha));
|
||||
assertSame(encChaCha, returnedEnc);
|
||||
|
||||
byte[] ciphertext = runEncrypt(encChaCha, msg);
|
||||
System.out.println("...ciphertextLen=" + ciphertext.length);
|
||||
System.out.println("...ciphertextPrefix=" + shortHex(ciphertext, 32));
|
||||
|
||||
ChaChaDataContentBuilder decChaCha = ChaChaDataContentBuilder.builder().withHeader();
|
||||
|
||||
HybridDerived.from(exporter).label("app/enc/chacha").transcript(transcript).aad(aad)
|
||||
.applyToChaChaAead(decChaCha, 256, 12);
|
||||
|
||||
byte[] out = runDecrypt(decChaCha, ciphertext);
|
||||
System.out.println("...outPrefix=" + shortHex(out, 32));
|
||||
|
||||
assertArrayEquals(msg, out);
|
||||
System.out.println("chacha_aead_applyTo_roundtrip...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hmac_applyTo_default_and_override() throws Exception {
|
||||
System.out.println("hmac_applyTo_default_and_override()");
|
||||
HybridKexExporter exporter = testExporter();
|
||||
|
||||
byte[] transcript = "demo-transcript".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] msg = fixedBytes(2048, (byte) 0x7E);
|
||||
|
||||
// --------------------
|
||||
// Default key size path: applyToHmac(hmac) derives key using builder's
|
||||
// recommended bits
|
||||
// --------------------
|
||||
|
||||
HmacDataContentBuilder macBuilder = HmacDataContentBuilder.builder().sha256().emitHexTag();
|
||||
|
||||
int recommendedBits = macBuilder.recommendedKeyBits();
|
||||
System.out.println("...recommendedBits=" + recommendedBits);
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(macBuilder);
|
||||
|
||||
String tagHex = runHmacHex(macBuilder, msg);
|
||||
System.out.println("...tagHexPrefix=" + shortText(tagHex, 64));
|
||||
|
||||
HmacDataContentBuilder verifyBuilder = HmacDataContentBuilder.builder().sha256().expectedTagHex(tagHex)
|
||||
.emitVerificationBoolean();
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(verifyBuilder);
|
||||
|
||||
String ok = runHmacVerifyBool(verifyBuilder, msg);
|
||||
System.out.println("...verifyBool=" + ok);
|
||||
assertEquals("true", ok);
|
||||
|
||||
// --------------------
|
||||
// Override key size path: applyToHmac(hmac, keyBits)
|
||||
// --------------------
|
||||
|
||||
HmacDataContentBuilder macBuilderOv = HmacDataContentBuilder.builder().sha256().emitHexTag();
|
||||
|
||||
// ...override to 512-bit keying material (still valid for HMAC; explicit expert
|
||||
// choice)
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-override").transcript(transcript).applyToHmac(macBuilderOv,
|
||||
512);
|
||||
|
||||
String tagHexOv = runHmacHex(macBuilderOv, msg);
|
||||
System.out.println("...tagHexOvPrefix=" + shortText(tagHexOv, 64));
|
||||
|
||||
HmacDataContentBuilder verifyBuilderOv = HmacDataContentBuilder.builder().sha256().expectedTagHex(tagHexOv)
|
||||
.emitVerificationBoolean();
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-override").transcript(transcript).applyToHmac(verifyBuilderOv,
|
||||
512);
|
||||
|
||||
String okOv = runHmacVerifyBool(verifyBuilderOv, msg);
|
||||
System.out.println("...verifyBoolOv=" + okOv);
|
||||
assertEquals("true", okOv);
|
||||
|
||||
// --------------------
|
||||
// Negative: wrong expected tag -> must emit "false"
|
||||
// --------------------
|
||||
|
||||
HmacDataContentBuilder verifyBad = HmacDataContentBuilder.builder().sha256()
|
||||
.expectedTagHex(tagHex.substring(0, Math.max(0, tagHex.length() - 2)) + "00").emitVerificationBoolean();
|
||||
|
||||
HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(verifyBad);
|
||||
|
||||
String bad = runHmacVerifyBool(verifyBad, msg);
|
||||
System.out.println("...verifyBoolBad=" + bad);
|
||||
assertEquals("false", bad);
|
||||
|
||||
// sanity: ensure the two tags differ (default vs override label/key schedule)
|
||||
assertTrue(!tagHex.equals(tagHexOv));
|
||||
|
||||
System.out.println("hmac_applyTo_default_and_override...ok");
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// helpers
|
||||
// --------------------
|
||||
|
||||
private static HybridKexExporter testExporter() {
|
||||
byte[] okm = fixedBytes(32, (byte) 0x11);
|
||||
byte[] salt = fixedBytes(32, (byte) 0x22);
|
||||
return new HybridKexExporter(okm, salt);
|
||||
}
|
||||
|
||||
private static byte[] runEncrypt(DataContentBuilder<DataContent> algorithmBuilder, byte[] plaintext)
|
||||
throws Exception {
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(plaintext))
|
||||
.add(algorithmBuilder).build();
|
||||
|
||||
try (InputStream in = enc.getStream()) {
|
||||
return readAll(in);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] runDecrypt(DataContentBuilder<DataContent> algorithmBuilder, byte[] ciphertext)
|
||||
throws Exception {
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(ciphertext))
|
||||
.add(algorithmBuilder).build();
|
||||
|
||||
try (InputStream in = dec.getStream()) {
|
||||
return readAll(in);
|
||||
}
|
||||
}
|
||||
|
||||
private static String runHmacHex(HmacDataContentBuilder macBuilder, byte[] msg) throws Exception {
|
||||
DataContent dc = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)).add(macBuilder)
|
||||
.build();
|
||||
|
||||
byte[] out;
|
||||
try (InputStream in = dc.getStream()) {
|
||||
out = readAll(in);
|
||||
}
|
||||
String tagHex = new String(out, StandardCharsets.UTF_8).trim();
|
||||
return tagHex;
|
||||
}
|
||||
|
||||
private static String runHmacVerifyBool(HmacDataContentBuilder verifyBuilder, byte[] msg) throws Exception {
|
||||
DataContent dc = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(msg))
|
||||
.add(verifyBuilder).build();
|
||||
|
||||
byte[] out;
|
||||
try (InputStream in = dc.getStream()) {
|
||||
out = readAll(in);
|
||||
}
|
||||
String s = new String(out, StandardCharsets.UTF_8).trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
private static byte[] fixedBytes(int len, byte v) {
|
||||
byte[] b = new byte[len];
|
||||
Arrays.fill(b, v);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws Exception {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
in.transferTo(out);
|
||||
out.flush();
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String shortHex(byte[] data, int maxBytes) {
|
||||
if (data == null) {
|
||||
return "null";
|
||||
}
|
||||
int n = Math.min(data.length, Math.max(0, maxBytes));
|
||||
StringBuilder sb = new StringBuilder(n * 2 + 3);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int v = data[i] & 0xFF;
|
||||
sb.append(Character.forDigit(v >>> 4, 16));
|
||||
sb.append(Character.forDigit(v & 0x0F, 16));
|
||||
}
|
||||
if (data.length > n) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String shortText(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return "null";
|
||||
}
|
||||
if (s.length() <= maxLen) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, maxLen) + "...";
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static byte[] randomBytes(int len) {
|
||||
byte[] b = new byte[len];
|
||||
new SecureRandom().nextBytes(b);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
251
lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexTest.java
Normal file
251
lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexTest.java
Normal file
@@ -0,0 +1,251 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.kex;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
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.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Hybrid KEX tests.
|
||||
*/
|
||||
public class HybridKexTest {
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
|
||||
// etc.)
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep tests runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
private static void logBegin(Object... params) {
|
||||
String thisClass = HybridKexTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
|
||||
}
|
||||
|
||||
private static void logEnd() {
|
||||
String thisClass = HybridKexTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "...ok");
|
||||
}
|
||||
|
||||
private static String hex(byte[] b) {
|
||||
if (b == null) {
|
||||
return "null";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(b.length * 2);
|
||||
for (byte v : b) {
|
||||
if (sb.length() == 80) {
|
||||
sb.append("...");
|
||||
break;
|
||||
}
|
||||
if ((v & 0xFF) < 16) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(Integer.toHexString(v & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String lens(byte[] msg) {
|
||||
if (msg == null || msg.length < 8) {
|
||||
return "classicLen=?, pqcLen=?";
|
||||
}
|
||||
try {
|
||||
DataInputStream in = new DataInputStream(new ByteArrayInputStream(msg));
|
||||
int classicLen = in.readInt();
|
||||
int pqcLen = 0;
|
||||
if (msg.length >= 8 + Math.max(0, classicLen)) {
|
||||
if (classicLen > 0) {
|
||||
in.skipBytes(classicLen);
|
||||
}
|
||||
pqcLen = in.readInt();
|
||||
}
|
||||
return "classicLen=" + classicLen + ", pqcLen=" + pqcLen;
|
||||
} catch (Exception e) {
|
||||
return "classicLen=?, pqcLen=?";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybrid_x25519_mlkem_roundtrip() throws Exception {
|
||||
logBegin("CLASSIC_AGREEMENT + KEM_ADAPTER", "Xdh/X25519 + ML-KEM(768)", "HKDF-SHA256", "32 bytes");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// Classic: X25519 key pairs (Xdh + XdhSpec.X25519)
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// PQC: ML-KEM key pair (Kyber variant)
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
|
||||
try {
|
||||
// Initiator: classic uses Alice private + Bob classic public; PQC uses Bob PQC
|
||||
// public
|
||||
alice = HybridKexContexts.initiator(profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(),
|
||||
XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null);
|
||||
|
||||
// Responder: classic uses Bob private + Alice classic public; PQC uses Bob PQC
|
||||
// private
|
||||
bob = HybridKexContexts.responder(profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(),
|
||||
XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null);
|
||||
|
||||
// Alice produces message (contains PQC ciphertext; classic part is empty here)
|
||||
byte[] aliceMsg = alice.getPeerMessage();
|
||||
System.out.println("...aliceMsg(" + lens(aliceMsg) + ")=" + hex(aliceMsg));
|
||||
|
||||
// Bob consumes message
|
||||
bob.setPeerMessage(aliceMsg);
|
||||
|
||||
byte[] kA = alice.deriveSecret();
|
||||
byte[] kB = bob.deriveSecret();
|
||||
|
||||
System.out.println("...kA=" + hex(kA));
|
||||
System.out.println("...kB=" + hex(kB));
|
||||
|
||||
assertNotNull(kA);
|
||||
assertNotNull(kB);
|
||||
assertArrayEquals(kA, kB);
|
||||
} finally {
|
||||
if (alice != null) {
|
||||
try {
|
||||
alice.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
if (bob != null) {
|
||||
try {
|
||||
bob.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybrid_x25519_pairmessage_mlkem_roundtrip() throws Exception {
|
||||
logBegin("PAIR_MESSAGE + KEM_ADAPTER", "Xdh/X25519 + ML-KEM(768)", "HKDF-SHA256", "32 bytes");
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// Classic: X25519 key pairs (Xdh + XdhSpec.X25519)
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// PQC: ML-KEM key pair (recipient/responder)
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
|
||||
try {
|
||||
// Classic leg is message-based on both sides (PAIR_MESSAGE capability:
|
||||
// KeyPairKey + ContextSpec).
|
||||
// PQC leg is KEM-style: initiator uses recipient public key; responder uses
|
||||
// recipient private key.
|
||||
alice = HybridKexContexts.initiatorPairMessage(profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPublic(), null);
|
||||
|
||||
bob = HybridKexContexts.responderPairMessage(profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPrivate(), null);
|
||||
|
||||
// Step 1: Alice -> Bob (classic SPKI + PQC ciphertext)
|
||||
byte[] msgA = alice.getPeerMessage();
|
||||
System.out.println("...msgA(" + lens(msgA) + ")=" + hex(msgA));
|
||||
bob.setPeerMessage(msgA);
|
||||
|
||||
// Step 2: Bob -> Alice (classic SPKI only; PQC part is empty)
|
||||
byte[] msgB = bob.getPeerMessage();
|
||||
System.out.println("...msgB(" + lens(msgB) + ")=" + hex(msgB));
|
||||
alice.setPeerMessage(msgB);
|
||||
|
||||
// Both sides derive the final hybrid OKM.
|
||||
byte[] kA = alice.deriveSecret();
|
||||
byte[] kB = bob.deriveSecret();
|
||||
|
||||
System.out.println("...kA=" + hex(kA));
|
||||
System.out.println("...kB=" + hex(kB));
|
||||
|
||||
assertNotNull(kA);
|
||||
assertNotNull(kB);
|
||||
assertArrayEquals(kA, kB);
|
||||
} finally {
|
||||
if (alice != null) {
|
||||
try {
|
||||
alice.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
if (bob != null) {
|
||||
try {
|
||||
bob.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.hybrid.signature;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.Signature;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.content.api.PlainContent;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* End-to-end tests for hybrid signatures (classic + PQC) across:
|
||||
* <ul>
|
||||
* <li>{@link HybridSignatureProfile.VerifyRule#AND} and
|
||||
* {@link HybridSignatureProfile.VerifyRule#OR}</li>
|
||||
* <li>direct streaming use via {@link SignatureContext#wrap(InputStream)}</li>
|
||||
* <li>integration via {@link TagTrailerDataContentBuilder} and
|
||||
* {@link DataContentChainBuilder}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Tests focus on practical combinations: Ed25519 + SPHINCS+, and
|
||||
* RSA-PSS(SHA-256) + SPHINCS+ (if registered).
|
||||
* </p>
|
||||
*/
|
||||
public class HybridSignatureTest {
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
|
||||
// etc.)
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep tests runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- boilerplate logging ----------
|
||||
private static void logBegin(Object... params) {
|
||||
String thisClass = HybridSignatureTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
|
||||
}
|
||||
|
||||
private static void logEnd() {
|
||||
String thisClass = HybridSignatureTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "...ok");
|
||||
}
|
||||
|
||||
private static void requireAlgOrSkip(String id) {
|
||||
if (!CryptoAlgorithms.available().contains(id)) {
|
||||
System.out.println("...*** SKIP *** " + id + " not registered");
|
||||
Assumptions.assumeTrue(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int n) {
|
||||
byte[] b = new byte[n];
|
||||
Random r = new Random(123456789L); // deterministic
|
||||
r.nextBytes(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws Exception {
|
||||
try (InputStream closeMe = in) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
closeMe.transferTo(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String hexShort(byte[] b) {
|
||||
if (b == null) {
|
||||
return "null";
|
||||
}
|
||||
int max = Math.min(b.length, 24);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < max; i++) {
|
||||
sb.append(String.format("%02x", Integer.valueOf(b[i] & 0xff)));
|
||||
}
|
||||
if (b.length > max) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static byte[] flipOneBit(byte[] in, int index) {
|
||||
byte[] out = in.clone();
|
||||
out[index] = (byte) (out[index] ^ 0x01);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static byte[] sub(byte[] b, int off, int len) {
|
||||
byte[] out = new byte[len];
|
||||
System.arraycopy(b, off, out, 0, len);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static byte[] concat(byte[] a, byte[] b) {
|
||||
byte[] out = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, out, 0, a.length);
|
||||
System.arraycopy(b, 0, out, a.length, b.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int tagLen(String algoId, KeyUsage role, Key key, ContextSpec specOrNull) throws Exception {
|
||||
try (SignatureContext ctx = CryptoAlgorithms.create(algoId, role, key, specOrNull)) {
|
||||
return ctx.tagLength();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signTrailer(SignatureContext signer, byte[] body) throws Exception {
|
||||
int tagLen = signer.tagLength();
|
||||
final byte[][] holder = new byte[1][];
|
||||
|
||||
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(body)), tagLen, 8192) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) {
|
||||
holder[0] = (tail == null ? null : tail.clone());
|
||||
}
|
||||
}) {
|
||||
byte[] pt = readAll(in);
|
||||
assertArrayEquals(body, pt, "sign passthrough mismatch");
|
||||
}
|
||||
|
||||
if (holder[0] == null) {
|
||||
throw new IllegalStateException("Signature trailer missing");
|
||||
}
|
||||
return holder[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal source builder for DataContent chains (same shape as other tests).
|
||||
*/
|
||||
private static final class BytesSourceBuilder implements DataContentBuilder<PlainContent> {
|
||||
private final byte[] data;
|
||||
|
||||
private BytesSourceBuilder(byte[] data) {
|
||||
this.data = data.clone();
|
||||
}
|
||||
|
||||
static BytesSourceBuilder of(byte[] data) {
|
||||
return new BytesSourceBuilder(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlainContent build(boolean encrypt) {
|
||||
return new PlainContent() {
|
||||
@Override
|
||||
public void setInput(DataContent input) {
|
||||
/* no upstream for sources */
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getStream() {
|
||||
return new ByteArrayInputStream(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 1) DIRECT streaming tests (SignatureContext.wrap + transferTo)
|
||||
// ======================================================================
|
||||
|
||||
@Test
|
||||
void hybrid_ed25519_sphincsplus_direct_and_or_negative() throws Exception {
|
||||
final int size = 64 * 1024 + 13;
|
||||
logBegin("direct", "Ed25519+SPHINCS+", Integer.valueOf(size));
|
||||
|
||||
requireAlgOrSkip("Ed25519");
|
||||
requireAlgOrSkip("SPHINCS+");
|
||||
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
|
||||
int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null);
|
||||
int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
|
||||
System.out.println("...classicTagLen=" + edLen + ", pqcTagLen=" + spxLen);
|
||||
|
||||
// ---- AND ----
|
||||
HybridSignatureProfile andProfile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] sigAnd;
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(andProfile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
sigAnd = signTrailer(signer, msg);
|
||||
}
|
||||
System.out.println("...sig(AND).len=" + sigAnd.length + ", head=" + hexShort(sigAnd));
|
||||
|
||||
// verify OK
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(sigAnd);
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
}
|
||||
System.out.println("...verify(AND)=ok");
|
||||
|
||||
// corrupt classic => must fail
|
||||
byte[] badClassic = concat(flipOneBit(sub(sigAnd, 0, edLen), 0), sub(sigAnd, edLen, spxLen));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(badClassic);
|
||||
assertThrows(java.io.IOException.class, () -> {
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
});
|
||||
}
|
||||
System.out.println("...verify(AND) bad classic -> throws");
|
||||
|
||||
// corrupt pqc => must fail
|
||||
byte[] badPqc = concat(sub(sigAnd, 0, edLen), flipOneBit(sub(sigAnd, edLen, spxLen), 0));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(badPqc);
|
||||
assertThrows(java.io.IOException.class, () -> {
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
});
|
||||
}
|
||||
System.out.println("...verify(AND) bad pqc -> throws");
|
||||
|
||||
// ---- OR ----
|
||||
HybridSignatureProfile orProfile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.OR);
|
||||
|
||||
byte[] sigOr;
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(orProfile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
sigOr = signTrailer(signer, msg);
|
||||
}
|
||||
System.out.println("...sig(OR).len=" + sigOr.length + ", head=" + hexShort(sigOr));
|
||||
|
||||
// corrupt classic => OR must pass
|
||||
byte[] orBadClassic = concat(flipOneBit(sub(sigOr, 0, edLen), 0), sub(sigOr, edLen, spxLen));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(orBadClassic);
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
}
|
||||
System.out.println("...verify(OR) bad classic -> ok");
|
||||
|
||||
// corrupt pqc => OR must pass
|
||||
byte[] orBadPqc = concat(sub(sigOr, 0, edLen), flipOneBit(sub(sigOr, edLen, spxLen), 0));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(orBadPqc);
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
}
|
||||
System.out.println("...verify(OR) bad pqc -> ok");
|
||||
|
||||
// corrupt both => OR must fail
|
||||
byte[] orBadBoth = concat(flipOneBit(sub(sigOr, 0, edLen), 0), flipOneBit(sub(sigOr, edLen, spxLen), 0));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(orBadBoth);
|
||||
assertThrows(java.io.IOException.class, () -> {
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
});
|
||||
}
|
||||
System.out.println("...verify(OR) bad both -> throws");
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybrid_rsa_sphincsplus_direct_and_roundtrip() throws Exception {
|
||||
final int size = 96 * 1024 + 3;
|
||||
logBegin("direct", "RSA+SPHINCS+", Integer.valueOf(size));
|
||||
|
||||
requireAlgOrSkip("RSA");
|
||||
requireAlgOrSkip("SPHINCS+");
|
||||
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
|
||||
int rsaLen = tagLen("RSA", KeyUsage.SIGN, rsa.getPrivate(), null);
|
||||
int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
|
||||
System.out.println("...classicTagLen=" + rsaLen + ", pqcTagLen=" + spxLen);
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("RSA", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] sig;
|
||||
try (SignatureContext signer = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
sig = signTrailer(signer, msg);
|
||||
}
|
||||
System.out.println("...sig.len=" + sig.length + ", head=" + hexShort(sig));
|
||||
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(sig);
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
}
|
||||
System.out.println("...verify(AND)=ok");
|
||||
|
||||
// negative sanity: corrupt classic => must fail (AND)
|
||||
byte[] badClassic = concat(flipOneBit(sub(sig, 0, rsaLen), 0), sub(sig, rsaLen, spxLen));
|
||||
try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
|
||||
verifier.setExpectedTag(badClassic);
|
||||
assertThrows(java.io.IOException.class, () -> {
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
});
|
||||
}
|
||||
System.out.println("...verify(AND) bad classic -> throws");
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 2) TagTrailer integration tests (DataContentChainBuilder + TagTrailer)
|
||||
// ======================================================================
|
||||
|
||||
@Test
|
||||
void hybrid_ed25519_sphincsplus_via_tagtrailer_and_roundtrip() throws Exception {
|
||||
final int size = 32 * 1024 + 7;
|
||||
logBegin("TagTrailer", "AND", "Ed25519+SPHINCS+", Integer.valueOf(size));
|
||||
|
||||
requireAlgOrSkip("Ed25519");
|
||||
requireAlgOrSkip("SPHINCS+");
|
||||
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] out;
|
||||
int tagLen;
|
||||
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagEnc).bufferSize(8192)).build();
|
||||
|
||||
out = readAll(enc.getStream());
|
||||
tagLen = tagEnc.tagLength();
|
||||
}
|
||||
|
||||
System.out.println("...out=" + out.length + " bytes");
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
// IMPORTANT: TagTrailerDataContentBuilder supplies expectedTag internally
|
||||
// during streaming.
|
||||
// HybridSignatureContext.wrap must NOT require expectedTag pre-set.
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(out))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
|
||||
.build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
assertArrayEquals(msg, pt, "hybrid TagTrailer AND roundtrip mismatch");
|
||||
}
|
||||
|
||||
System.out.println("...tagLen=" + tagLen);
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybrid_ed25519_sphincsplus_via_tagtrailer_or_negative() throws Exception {
|
||||
final int size = 24 * 1024 + 9;
|
||||
logBegin("TagTrailer", "OR", "Ed25519+SPHINCS+", Integer.valueOf(size));
|
||||
|
||||
requireAlgOrSkip("Ed25519");
|
||||
requireAlgOrSkip("SPHINCS+");
|
||||
|
||||
byte[] msg = ("zeroecho-hybrid-or-negative-").getBytes(StandardCharsets.UTF_8);
|
||||
msg = Arrays.copyOf(msg, size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
|
||||
int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null);
|
||||
int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
|
||||
System.out.println("...classicTagLen=" + edLen + ", pqcTagLen=" + spxLen);
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.OR);
|
||||
|
||||
byte[] out;
|
||||
int tagLen;
|
||||
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagEnc).bufferSize(8192)).build();
|
||||
|
||||
out = readAll(enc.getStream());
|
||||
tagLen = tagEnc.tagLength();
|
||||
}
|
||||
|
||||
byte[] body = sub(out, 0, out.length - tagLen);
|
||||
byte[] tag = sub(out, out.length - tagLen, tagLen);
|
||||
|
||||
System.out.println("...tag.len=" + tag.length + ", head=" + hexShort(tag));
|
||||
|
||||
// Corrupt ONLY classic part => OR must still PASS
|
||||
byte[] badClassic = concat(flipOneBit(sub(tag, 0, edLen), 0), sub(tag, edLen, spxLen));
|
||||
byte[] outBadClassic = concat(body, badClassic);
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(outBadClassic))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
|
||||
.build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
assertArrayEquals(msg, pt, "OR should accept when only classic signature is corrupted");
|
||||
}
|
||||
|
||||
System.out.println("...OR verify with bad classic -> ok");
|
||||
|
||||
// Corrupt ONLY pqc part => OR must still PASS
|
||||
byte[] badPqc = concat(sub(tag, 0, edLen), flipOneBit(sub(tag, edLen, spxLen), 0));
|
||||
byte[] outBadPqc = concat(body, badPqc);
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(outBadPqc))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
|
||||
.build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
assertArrayEquals(msg, pt, "OR should accept when only PQC signature is corrupted");
|
||||
}
|
||||
|
||||
System.out.println("...OR verify with bad pqc -> ok");
|
||||
|
||||
// Corrupt BOTH => OR must FAIL
|
||||
byte[] badBoth = concat(flipOneBit(sub(tag, 0, edLen), 0), flipOneBit(sub(tag, edLen, spxLen), 0));
|
||||
byte[] outBadBoth = concat(body, badBoth);
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(outBadBoth))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
|
||||
.build();
|
||||
|
||||
assertThrows(java.io.IOException.class, () -> readAll(dec.getStream()));
|
||||
}
|
||||
|
||||
System.out.println("...OR verify with bad both -> throws");
|
||||
System.out.println("...tagLen=" + tagLen);
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybrid_rsa_sphincsplus_via_tagtrailer_and_roundtrip() throws Exception {
|
||||
final int size = 40 * 1024 + 1;
|
||||
logBegin("TagTrailer", "AND", "RSA+SPHINCS+", Integer.valueOf(size));
|
||||
|
||||
requireAlgOrSkip("RSA");
|
||||
requireAlgOrSkip("SPHINCS+");
|
||||
|
||||
byte[] msg = randomBytes(size);
|
||||
System.out.println("...msg=" + msg.length + " bytes");
|
||||
|
||||
KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair();
|
||||
KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
|
||||
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("RSA", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
byte[] out;
|
||||
|
||||
try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024)) {
|
||||
|
||||
DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagEnc).bufferSize(8192)).build();
|
||||
|
||||
out = readAll(enc.getStream());
|
||||
}
|
||||
|
||||
System.out.println("...out=" + out.length + " bytes");
|
||||
|
||||
try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024)) {
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(out))
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
|
||||
.build();
|
||||
|
||||
byte[] pt = readAll(dec.getStream());
|
||||
assertArrayEquals(msg, pt, "hybrid TagTrailer AND (RSA+SPHINCS+) roundtrip mismatch");
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,7 @@ public class OutputToInputStreamAdapterTest {
|
||||
return new ByteArrayInputStream(baos.toByteArray());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Test
|
||||
public void testIncrementingAdapter() throws IOException {
|
||||
System.out.println("testIncrementingAdapter");
|
||||
|
||||
308
samples/src/test/java/demo/AgreementVariantsTest.java
Normal file
308
samples/src/test/java/demo/AgreementVariantsTest.java
Normal file
@@ -0,0 +1,308 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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 demo;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
||||
import zeroecho.core.alg.xdh.XdhSpec;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.util.Strings;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Demonstration of agreement usage variants in ZeroEcho.
|
||||
*
|
||||
* <p>
|
||||
* This sample illustrates three complementary models used in practice:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>KEM_ADAPTER</b> (example: ML-KEM / Kyber): the initiator is
|
||||
* constructed with the recipient's {@link PublicKey} and produces an outbound
|
||||
* encapsulation message; the responder is constructed with the matching
|
||||
* {@link PrivateKey} and consumes that message.</li>
|
||||
* <li><b>CLASSIC_AGREEMENT</b> (example: XDH / X25519): both parties hold their
|
||||
* own {@link PrivateKey}, set the peer {@link PublicKey} explicitly, and derive
|
||||
* the same raw shared secret.</li>
|
||||
* <li><b>PAIR_MESSAGE</b> (example: XDH / X25519): both parties hold a key pair
|
||||
* and exchange messages that are simply the X.509 SPKI encodings of their
|
||||
* public keys. This yields a message-oriented handshake without changing the
|
||||
* underlying agreement primitive.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Important note</h2>
|
||||
* <p>
|
||||
* All examples below produce a <em>raw</em> agreement secret (the direct output
|
||||
* of KEM decapsulation or Diffie–Hellman agreement). Real protocols should feed
|
||||
* the raw secret into a suitable KDF (typically HKDF) together with
|
||||
* transcript/context info before using it as key material.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Note on resource management</h2>
|
||||
* <p>
|
||||
* The examples in this class intentionally do <em>not</em> use the
|
||||
* {@code try-with-resources} construct when working with
|
||||
* {@link zeroecho.core.context.AgreementContext} and
|
||||
* {@link zeroecho.core.context.MessageAgreementContext}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Agreement contexts represent protocol-level state rather than traditional I/O
|
||||
* resources. In real-world applications their lifecycle often spans multiple
|
||||
* protocol steps (message send, receive, validation, key derivation) and may
|
||||
* cross method or thread boundaries. Using explicit {@code try/finally} blocks
|
||||
* in the examples makes this lifecycle visible and closer to how agreement
|
||||
* contexts are typically managed in production code.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* In short-lived, fully synchronous scenarios (such as unit tests),
|
||||
* {@code try-with-resources} is perfectly acceptable. It is omitted here purely
|
||||
* for didactic reasons.
|
||||
* </p>
|
||||
*/
|
||||
class AgreementVariantsTest {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(AgreementVariantsTest.class.getName());
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: activate BC/BCPQC if present.
|
||||
// Keeps tests runnable even when providers are missing.
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KEM_ADAPTER example for ML-KEM (Kyber):
|
||||
*
|
||||
* <p>
|
||||
* This models a common "send one message, derive shared secret" pattern:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Initiator uses recipient {@link PublicKey} and produces an encapsulation
|
||||
* message.</li>
|
||||
* <li>Responder uses recipient {@link PrivateKey}, consumes the message, and
|
||||
* derives the same secret.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
void kemAdapter_mlKem_roundTrip() throws Exception {
|
||||
LOG.info("kemAdapter_mlKem_roundTrip - KEM_ADAPTER (ML-KEM)");
|
||||
|
||||
KeyPair recipient = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024());
|
||||
|
||||
MessageAgreementContext initiator = null;
|
||||
MessageAgreementContext responder = null;
|
||||
try {
|
||||
// Initiator: constructed with recipient's public key (encapsulation side).
|
||||
initiator = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE);
|
||||
|
||||
// Responder: constructed with recipient's private key (decapsulation side).
|
||||
responder = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(),
|
||||
VoidSpec.INSTANCE);
|
||||
|
||||
// One-shot outbound message: KEM ciphertext / encapsulation payload.
|
||||
byte[] enc = initiator.getPeerMessage();
|
||||
// Responder consumes ciphertext to derive its secret.
|
||||
responder.setPeerMessage(enc);
|
||||
|
||||
byte[] s1 = initiator.deriveSecret();
|
||||
byte[] s2 = responder.deriveSecret();
|
||||
|
||||
LOG.log(Level.INFO, "KEM_ADAPTER: ciphertext={0}", Strings.toShortHexString(enc));
|
||||
LOG.log(Level.INFO, "KEM_ADAPTER: initiatorSecret={0}", Strings.toShortHexString(s1));
|
||||
LOG.log(Level.INFO, "KEM_ADAPTER: responderSecret={0}", Strings.toShortHexString(s2));
|
||||
LOG.log(Level.INFO, "KEM_ADAPTER: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
|
||||
} finally {
|
||||
if (initiator != null) {
|
||||
try {
|
||||
initiator.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (responder != null) {
|
||||
try {
|
||||
responder.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CLASSIC_AGREEMENT example for XDH/X25519:
|
||||
*
|
||||
* <p>
|
||||
* This is the traditional Diffie–Hellman model: both parties generate their own
|
||||
* key pair, each side keeps a private key, and the peer public key is provided
|
||||
* out-of-band (protocol message or session state).
|
||||
* </p>
|
||||
*/
|
||||
@Test
|
||||
void classicAgreement_x25519_roundTrip() throws Exception {
|
||||
LOG.info("classicAgreement_x25519_roundTrip - CLASSIC_AGREEMENT (X25519)");
|
||||
|
||||
CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh");
|
||||
KeyPair alice = xdh.generateKeyPair();
|
||||
KeyPair bob = xdh.generateKeyPair();
|
||||
|
||||
AgreementContext aCtx = null;
|
||||
AgreementContext bCtx = null;
|
||||
|
||||
try {
|
||||
// Both contexts are built from local private keys.
|
||||
aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, alice.getPrivate(), XdhSpec.X25519);
|
||||
bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bob.getPrivate(), XdhSpec.X25519);
|
||||
|
||||
// The protocol layer provides peer public keys (here we use in-memory
|
||||
// exchange).
|
||||
aCtx.setPeerPublic(bob.getPublic());
|
||||
bCtx.setPeerPublic(alice.getPublic());
|
||||
|
||||
byte[] s1 = aCtx.deriveSecret();
|
||||
byte[] s2 = bCtx.deriveSecret();
|
||||
|
||||
LOG.log(Level.INFO, "CLASSIC_AGREEMENT: aliceSecret={0}", Strings.toShortHexString(s1));
|
||||
LOG.log(Level.INFO, "CLASSIC_AGREEMENT: bobSecret={0}", Strings.toShortHexString(s2));
|
||||
LOG.log(Level.INFO, "CLASSIC_AGREEMENT: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
|
||||
} finally {
|
||||
if (aCtx != null) {
|
||||
try {
|
||||
aCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (bCtx != null) {
|
||||
try {
|
||||
bCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PAIR_MESSAGE example for XDH/X25519:
|
||||
*
|
||||
* <p>
|
||||
* This demonstrates the "message-oriented" handshake for DH-style agreements.
|
||||
* Each party holds a key pair and the outbound message is simply the local
|
||||
* public key encoding (SPKI). The receiver imports the encoding and binds it as
|
||||
* the peer key.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This model is particularly practical for protocol implementations because it
|
||||
* makes the "to-be-sent" artifact explicit (a byte array message), similarly to
|
||||
* KEM ciphertexts.
|
||||
* </p>
|
||||
*/
|
||||
@Test
|
||||
void pairMessage_x25519_roundTrip() throws Exception {
|
||||
LOG.info("pairMessage_x25519_roundTrip - PAIR_MESSAGE (X25519)");
|
||||
|
||||
CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh");
|
||||
KeyPair alice = xdh.generateKeyPair();
|
||||
KeyPair bob = xdh.generateKeyPair();
|
||||
|
||||
// Wrapper is required because ZeroEcho capability dispatch uses Key (KeyPair is
|
||||
// not a Key).
|
||||
KeyPairKey aliceKey = new KeyPairKey(alice);
|
||||
KeyPairKey bobKey = new KeyPairKey(bob);
|
||||
|
||||
MessageAgreementContext aCtx = null;
|
||||
MessageAgreementContext bCtx = null;
|
||||
|
||||
try {
|
||||
aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, aliceKey, XdhSpec.X25519);
|
||||
bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bobKey, XdhSpec.X25519);
|
||||
|
||||
// Outbound messages: SPKI encodings of local public keys.
|
||||
byte[] aMsg = aCtx.getPeerMessage();
|
||||
byte[] bMsg = bCtx.getPeerMessage();
|
||||
|
||||
LOG.log(Level.INFO, "PAIR_MESSAGE: aliceMsg={0}", Strings.toShortHexString(aMsg));
|
||||
LOG.log(Level.INFO, "PAIR_MESSAGE: bobMsg={0}", Strings.toShortHexString(bMsg));
|
||||
|
||||
// Each side imports peer public key from message.
|
||||
aCtx.setPeerMessage(bMsg);
|
||||
bCtx.setPeerMessage(aMsg);
|
||||
|
||||
byte[] s1 = aCtx.deriveSecret();
|
||||
byte[] s2 = bCtx.deriveSecret();
|
||||
|
||||
LOG.log(Level.INFO, "PAIR_MESSAGE: aliceSecret={0}", Strings.toShortHexString(s1));
|
||||
LOG.log(Level.INFO, "PAIR_MESSAGE: bobSecret={0}", Strings.toShortHexString(s2));
|
||||
LOG.log(Level.INFO, "PAIR_MESSAGE: secretsEqual={0}", Boolean.valueOf(Arrays.equals(s1, s2)));
|
||||
} finally {
|
||||
if (aCtx != null) {
|
||||
try {
|
||||
aCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (bCtx != null) {
|
||||
try {
|
||||
bCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
656
samples/src/test/java/demo/HybridDerivedAesDemoTest.java
Normal file
656
samples/src/test/java/demo/HybridDerivedAesDemoTest.java
Normal file
@@ -0,0 +1,656 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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 demo;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
|
||||
import zeroecho.core.alg.xdh.XdhSpec;
|
||||
import zeroecho.sdk.builders.HybridKexBuilder;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainBytesBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.hybrid.derived.HybridDerived;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexContext;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexExporter;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexProfile;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexTranscript;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Demonstration of hybrid-derived AEAD encryption and decryption.
|
||||
*
|
||||
* <p>
|
||||
* This sample is intentionally structured in two variants:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>Condensed</b> - compact fluent chains suitable for everyday use.</li>
|
||||
* <li><b>Expanded</b> - the same operations, step-by-step, for explanatory
|
||||
* documentation.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The hybrid combination (classic + PQC) happens in {@link HybridKexContext}.
|
||||
* The derived layer ({@link HybridDerived}) consumes the exporter output (OKM +
|
||||
* HKDF salt) and injects key/IV/AAD into existing streaming builders.
|
||||
* </p>
|
||||
*/
|
||||
class HybridDerivedAesDemoTest {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(HybridDerivedAesDemoTest.class.getName());
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only algorithms in the broader test suite.
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// Keep samples runnable without BC if not present.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybridDerived_aes_gcm_condensed() throws Exception {
|
||||
System.out.println("hybridDerived_aes_gcm_condensed()");
|
||||
LOG.info("Hybrid-derived AES-GCM demo (condensed form)");
|
||||
|
||||
// ...Select a standard hybrid KEX profile (HKDF info/salt + OKM length).
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// ...Prepare plaintext.
|
||||
byte[] msg = randomBytes(1024);
|
||||
|
||||
// ...Prepare transcript (public context bound into HKDF info and derived
|
||||
// labels).
|
||||
HybridKexTranscript transcript = new HybridKexTranscript().addUtf8("suite", "X25519+MLKEM768").addUtf8("demo",
|
||||
"hybrid-derived-aes-gcm-condensed");
|
||||
|
||||
// ...Generate classic key pairs for X25519 (Xdh + XdhSpec.X25519).
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// ...Generate PQC key pair for ML-KEM-768 (recipient; used by Bob side to
|
||||
// decapsulate).
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
// ...Build Alice initiator: classic agreement (out-of-band peer pub) + PQC
|
||||
// encapsulation.
|
||||
HybridKexContext alice = HybridKexBuilder.builder()
|
||||
// ...Set mandatory profile.
|
||||
.profile(profile)
|
||||
// ...Bind builder HKDF info to transcript.
|
||||
.transcript(transcript)
|
||||
// ...Select classic mode: peer public key is out-of-band.
|
||||
.classicAgreement()
|
||||
// ...Select classic algorithm id (Xdh).
|
||||
.algorithm("Xdh")
|
||||
// ...Select classic spec (X25519).
|
||||
.spec(XdhSpec.X25519)
|
||||
// ...Set Alice classic private key.
|
||||
.privateKey(aliceClassic.getPrivate())
|
||||
// ...Set Bob classic public key.
|
||||
.peerPublic(bobClassic.getPublic())
|
||||
// ...Switch to PQC KEM configuration.
|
||||
.pqcKem()
|
||||
// ...Select PQC algorithm id (ML-KEM).
|
||||
.algorithm("ML-KEM")
|
||||
// ...Set recipient PQC public key for encapsulation.
|
||||
.peerPublic(bobPqc.getPublic())
|
||||
// ...Build initiator context.
|
||||
.buildInitiator();
|
||||
|
||||
// ...Build Bob responder: classic agreement + PQC decapsulation.
|
||||
HybridKexContext bob = HybridKexBuilder.builder()
|
||||
// ...Set mandatory profile.
|
||||
.profile(profile)
|
||||
// ...Bind builder HKDF info to transcript.
|
||||
.transcript(transcript)
|
||||
// ...Select classic mode: peer public key is out-of-band.
|
||||
.classicAgreement()
|
||||
// ...Select classic algorithm id (Xdh).
|
||||
.algorithm("Xdh")
|
||||
// ...Select classic spec (X25519).
|
||||
.spec(XdhSpec.X25519)
|
||||
// ...Set Bob classic private key.
|
||||
.privateKey(bobClassic.getPrivate())
|
||||
// ...Set Alice classic public key.
|
||||
.peerPublic(aliceClassic.getPublic())
|
||||
// ...Switch to PQC KEM configuration.
|
||||
.pqcKem()
|
||||
// ...Select PQC algorithm id (ML-KEM).
|
||||
.algorithm("ML-KEM")
|
||||
// ...Set recipient PQC private key for decapsulation.
|
||||
.privateKey(bobPqc.getPrivate())
|
||||
// ...Build responder context.
|
||||
.buildResponder();
|
||||
|
||||
try {
|
||||
// ...Alice produces peer message (PQC ciphertext; classic is out-of-band in
|
||||
// this mode).
|
||||
byte[] peerMsg = alice.getPeerMessage();
|
||||
System.out.println("...peerMsg " + lens(peerMsg) + " " + shortHex(peerMsg, 48));
|
||||
|
||||
// ...Bob consumes the peer message to complete the PQC leg.
|
||||
bob.setPeerMessage(peerMsg);
|
||||
|
||||
// ...Derive OKM on both sides (must match for a valid hybrid exchange).
|
||||
byte[] okmA = alice.deriveSecret();
|
||||
byte[] okmB = bob.deriveSecret();
|
||||
System.out.println("...okmEqual " + Arrays.equals(okmA, okmB));
|
||||
if (!Arrays.equals(okmA, okmB)) {
|
||||
throw new IllegalStateException("Hybrid KEX mismatch");
|
||||
}
|
||||
|
||||
// ...Create exporter directly from OKM and profile salt (avoid exporterFromOkm
|
||||
// validation requirements).
|
||||
HybridKexExporter exporter = new HybridKexExporter(okmA, profile.hkdfSalt());
|
||||
|
||||
// ...Choose explicit AAD (public) for AEAD; must match on decrypt.
|
||||
byte[] aad = "aad:demo".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// ...Encrypt: build pipeline in compact form with inline derived injection.
|
||||
DataContent enc = DataContentChainBuilder.encrypt()
|
||||
// ...Input: plaintext bytes.
|
||||
.add(PlainBytesBuilder.builder().bytes(msg))
|
||||
// ...AEAD: derive key/IV/AAD and inject into AES-GCM builder.
|
||||
.add(HybridDerived.from(exporter)
|
||||
// ...Purpose separation label for AEAD encryption.
|
||||
.label("app/enc/aes-gcm")
|
||||
// ...Bind derivation to transcript bytes (public).
|
||||
.transcript(transcript.toByteArray())
|
||||
// ...Inject explicit AAD.
|
||||
.aad(aad)
|
||||
// ...Apply derived key(256b) and IV(12B) to AES-GCM with header.
|
||||
.applyToAesGcm(AesDataContentBuilder.builder()
|
||||
// ...Store IV in header for decrypt side.
|
||||
.withHeader()
|
||||
// ...Use AES-GCM with 128-bit authentication tag.
|
||||
.modeGcm(128), 256, 12))
|
||||
// ...Finalize pipeline.
|
||||
.build();
|
||||
|
||||
byte[] ciphertext;
|
||||
try (InputStream in = enc.getStream()) {
|
||||
ciphertext = readAll(in);
|
||||
}
|
||||
System.out.println("...ciphertext " + lens(ciphertext) + " " + shortHex(ciphertext, 48));
|
||||
|
||||
// ...Decrypt: rebuild the same derived inputs and run decrypt pipeline.
|
||||
DataContent dec = DataContentChainBuilder.decrypt()
|
||||
// ...Input: ciphertext bytes.
|
||||
.add(PlainBytesBuilder.builder().bytes(ciphertext))
|
||||
// ...AEAD: apply the same label/transcript/AAD to get identical key/IV.
|
||||
.add(HybridDerived.from(exporter)
|
||||
// ...Same purpose label as encryption.
|
||||
.label("app/enc/aes-gcm")
|
||||
// ...Same transcript binding as encryption.
|
||||
.transcript(transcript.toByteArray())
|
||||
// ...Same explicit AAD as encryption.
|
||||
.aad(aad)
|
||||
// ...Apply derived key and IV to AES-GCM with header.
|
||||
.applyToAesGcm(AesDataContentBuilder.builder()
|
||||
// ...Parse IV from header.
|
||||
.withHeader()
|
||||
// ...Use AES-GCM with 128-bit authentication tag.
|
||||
.modeGcm(128), 256, 12))
|
||||
// ...Finalize pipeline.
|
||||
.build();
|
||||
|
||||
byte[] out;
|
||||
try (InputStream in = dec.getStream()) {
|
||||
out = readAll(in);
|
||||
}
|
||||
System.out.println("...plaintextEqual " + Arrays.equals(msg, out));
|
||||
if (!Arrays.equals(msg, out)) {
|
||||
throw new IllegalStateException("Roundtrip mismatch");
|
||||
}
|
||||
|
||||
System.out.println("hybridDerived_aes_gcm_condensed...ok");
|
||||
} finally {
|
||||
closeQuiet(alice);
|
||||
closeQuiet(bob);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybridDerived_aes_gcm_expanded() throws Exception {
|
||||
System.out.println("hybridDerived_aes_gcm_expanded()");
|
||||
LOG.info("Hybrid-derived AES-GCM demo (expanded form)");
|
||||
|
||||
// ...Select a standard hybrid KEX profile (HKDF info/salt + OKM length).
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// ...Prepare plaintext.
|
||||
byte[] msg = randomBytes(1024);
|
||||
|
||||
// ...Prepare transcript (public context bound into HKDF info and derived
|
||||
// labels).
|
||||
HybridKexTranscript transcript = new HybridKexTranscript().addUtf8("suite", "X25519+MLKEM768").addUtf8("demo",
|
||||
"hybrid-derived-aes-gcm-expanded");
|
||||
|
||||
// ...Generate classic key pairs for X25519.
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// ...Generate PQC key pair for ML-KEM-768.
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
// ...Build Alice initiator in a step-by-step manner.
|
||||
HybridKexBuilder aliceBuilder = HybridKexBuilder.builder();
|
||||
// ...Set mandatory profile.
|
||||
aliceBuilder.profile(profile);
|
||||
// ...Bind builder HKDF info to transcript.
|
||||
aliceBuilder.transcript(transcript);
|
||||
|
||||
// ...Select classic mode: peer public key is out-of-band.
|
||||
HybridKexBuilder.ClassicAgreement aliceClassicCfg = aliceBuilder.classicAgreement();
|
||||
// ...Select classic algorithm id (Xdh).
|
||||
aliceClassicCfg.algorithm("Xdh");
|
||||
// ...Select classic spec (X25519).
|
||||
aliceClassicCfg.spec(XdhSpec.X25519);
|
||||
// ...Set Alice classic private key.
|
||||
aliceClassicCfg.privateKey(aliceClassic.getPrivate());
|
||||
// ...Set Bob classic public key.
|
||||
aliceClassicCfg.peerPublic(bobClassic.getPublic());
|
||||
|
||||
// ...Switch to PQC KEM configuration.
|
||||
HybridKexBuilder.PqcKem alicePqcCfg = aliceClassicCfg.pqcKem();
|
||||
// ...Select PQC algorithm id (ML-KEM).
|
||||
alicePqcCfg.algorithm("ML-KEM");
|
||||
// ...Set recipient PQC public key for encapsulation.
|
||||
alicePqcCfg.peerPublic(bobPqc.getPublic());
|
||||
|
||||
// ...Build initiator context.
|
||||
HybridKexContext alice = alicePqcCfg.buildInitiator();
|
||||
|
||||
// ...Build Bob responder in a step-by-step manner.
|
||||
HybridKexBuilder bobBuilder = HybridKexBuilder.builder();
|
||||
// ...Set mandatory profile.
|
||||
bobBuilder.profile(profile);
|
||||
// ...Bind builder HKDF info to transcript.
|
||||
bobBuilder.transcript(transcript);
|
||||
|
||||
// ...Select classic mode: peer public key is out-of-band.
|
||||
HybridKexBuilder.ClassicAgreement bobClassicCfg = bobBuilder.classicAgreement();
|
||||
// ...Select classic algorithm id (Xdh).
|
||||
bobClassicCfg.algorithm("Xdh");
|
||||
// ...Select classic spec (X25519).
|
||||
bobClassicCfg.spec(XdhSpec.X25519);
|
||||
// ...Set Bob classic private key.
|
||||
bobClassicCfg.privateKey(bobClassic.getPrivate());
|
||||
// ...Set Alice classic public key.
|
||||
bobClassicCfg.peerPublic(aliceClassic.getPublic());
|
||||
|
||||
// ...Switch to PQC KEM configuration.
|
||||
HybridKexBuilder.PqcKem bobPqcCfg = bobClassicCfg.pqcKem();
|
||||
// ...Select PQC algorithm id (ML-KEM).
|
||||
bobPqcCfg.algorithm("ML-KEM");
|
||||
// ...Set recipient PQC private key for decapsulation.
|
||||
bobPqcCfg.privateKey(bobPqc.getPrivate());
|
||||
|
||||
// ...Build responder context.
|
||||
HybridKexContext bob = bobPqcCfg.buildResponder();
|
||||
|
||||
try {
|
||||
// ...Alice produces peer message (PQC ciphertext in this classic mode).
|
||||
byte[] peerMsg = alice.getPeerMessage();
|
||||
System.out.println("...peerMsg " + lens(peerMsg) + " " + shortHex(peerMsg, 48));
|
||||
|
||||
// ...Bob consumes peer message to complete the PQC leg.
|
||||
bob.setPeerMessage(peerMsg);
|
||||
|
||||
// ...Derive OKM and ensure both sides match.
|
||||
byte[] okmA = alice.deriveSecret();
|
||||
byte[] okmB = bob.deriveSecret();
|
||||
System.out.println("...okmEqual " + Arrays.equals(okmA, okmB));
|
||||
if (!Arrays.equals(okmA, okmB)) {
|
||||
throw new IllegalStateException("Hybrid KEX mismatch");
|
||||
}
|
||||
|
||||
// ...Create exporter directly from OKM and profile salt.
|
||||
HybridKexExporter exporter = new HybridKexExporter(okmA, profile.hkdfSalt());
|
||||
|
||||
// ...Choose explicit AAD (public) for AEAD.
|
||||
byte[] aad = "aad:demo:expanded".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// ...Prepare AES builder for encryption.
|
||||
AesDataContentBuilder aesEnc = AesDataContentBuilder.builder();
|
||||
// ...Store IV in header.
|
||||
aesEnc.withHeader();
|
||||
// ...Use AES-GCM with 128-bit authentication tag.
|
||||
aesEnc.modeGcm(128);
|
||||
|
||||
// ...Inject derived key/IV/AAD into AES builder.
|
||||
HybridDerived.from(exporter)
|
||||
// ...Purpose separation label for AEAD.
|
||||
.label("app/enc/aes-gcm")
|
||||
// ...Bind derivation to transcript bytes.
|
||||
.transcript(transcript.toByteArray())
|
||||
// ...Inject explicit AAD.
|
||||
.aad(aad)
|
||||
// ...Apply derived key(256b) and IV(12B).
|
||||
.applyToAesGcm(aesEnc, 256, 12);
|
||||
|
||||
// ...Build encryption pipeline.
|
||||
DataContent enc = DataContentChainBuilder.encrypt()
|
||||
// ...Input: plaintext bytes.
|
||||
.add(PlainBytesBuilder.builder().bytes(msg))
|
||||
// ...AES encryption stage.
|
||||
.add(aesEnc)
|
||||
// ...Finalize.
|
||||
.build();
|
||||
|
||||
byte[] ciphertext;
|
||||
try (InputStream in = enc.getStream()) {
|
||||
ciphertext = readAll(in);
|
||||
}
|
||||
System.out.println("...ciphertext " + lens(ciphertext) + " " + shortHex(ciphertext, 48));
|
||||
|
||||
// ...Prepare AES builder for decryption.
|
||||
AesDataContentBuilder aesDec = AesDataContentBuilder.builder();
|
||||
// ...Parse IV from header.
|
||||
aesDec.withHeader();
|
||||
// ...Use AES-GCM with 128-bit authentication tag.
|
||||
aesDec.modeGcm(128);
|
||||
|
||||
// ...Inject the same derived key/IV/AAD into decryption builder.
|
||||
HybridDerived.from(exporter)
|
||||
// ...Same purpose label.
|
||||
.label("app/enc/aes-gcm")
|
||||
// ...Same transcript binding.
|
||||
.transcript(transcript.toByteArray())
|
||||
// ...Same explicit AAD.
|
||||
.aad(aad)
|
||||
// ...Apply the same derived key and IV.
|
||||
.applyToAesGcm(aesDec, 256, 12);
|
||||
|
||||
// ...Build decryption pipeline.
|
||||
DataContent dec = DataContentChainBuilder.decrypt()
|
||||
// ...Input: ciphertext bytes.
|
||||
.add(PlainBytesBuilder.builder().bytes(ciphertext))
|
||||
// ...AES decryption stage.
|
||||
.add(aesDec)
|
||||
// ...Finalize.
|
||||
.build();
|
||||
|
||||
byte[] out;
|
||||
try (InputStream in = dec.getStream()) {
|
||||
out = readAll(in);
|
||||
}
|
||||
System.out.println("...plaintextEqual " + Arrays.equals(msg, out));
|
||||
if (!Arrays.equals(msg, out)) {
|
||||
throw new IllegalStateException("Roundtrip mismatch");
|
||||
}
|
||||
|
||||
System.out.println("hybridDerived_aes_gcm_expanded...ok");
|
||||
} finally {
|
||||
closeQuiet(alice);
|
||||
closeQuiet(bob);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void hybridDerived_aes_gcm_local_self_recipient() throws Exception {
|
||||
System.out.println("hybridDerived_aes_gcm_local_self_recipient()");
|
||||
LOG.info("Hybrid-derived AES-GCM demo (local self-recipient)");
|
||||
|
||||
// ...Select a standard hybrid KEX profile (HKDF info/salt + OKM length).
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// ...Prepare plaintext.
|
||||
byte[] msg = randomBytes(1024);
|
||||
|
||||
// ...Prepare transcript (public context bound into KDF and derived labels).
|
||||
HybridKexTranscript transcript = new HybridKexTranscript()
|
||||
// ...Identify the suite used by this envelope.
|
||||
.addUtf8("suite", "X25519+MLKEM768")
|
||||
// ...Identify that this is a local/self-recipient envelope.
|
||||
.addUtf8("mode", "local-self");
|
||||
|
||||
// ...Choose explicit AAD (public) for AEAD; must match on decrypt.
|
||||
byte[] aad = "aad:local-self".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// ...Generate classic identity keys (X25519).
|
||||
KeyPair selfClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// ...Generate PQC identity keys (ML-KEM-768).
|
||||
KeyPair selfPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
// ...Build local initiator (encapsulation) against our own public keys.
|
||||
HybridKexContext encKex = HybridKexBuilder.builder()
|
||||
// ...Set mandatory profile.
|
||||
.profile(profile)
|
||||
// ...Bind derivation to transcript.
|
||||
.transcript(transcript)
|
||||
// ...Select classic mode: peer public key is known out-of-band (here: our own
|
||||
// public key).
|
||||
.classicAgreement()
|
||||
// ...Classic algorithm id (X25519).
|
||||
.algorithm("Xdh")
|
||||
// ...Classic spec (X25519).
|
||||
.spec(XdhSpec.X25519)
|
||||
// ...Use our private key.
|
||||
.privateKey(selfClassic.getPrivate())
|
||||
// ...Use our public key as the peer public key (self-recipient).
|
||||
.peerPublic(selfClassic.getPublic())
|
||||
// ...Switch to PQC KEM.
|
||||
.pqcKem()
|
||||
// ...PQC algorithm id (ML-KEM).
|
||||
.algorithm("ML-KEM")
|
||||
// ...Use our PQC public key as the recipient key for encapsulation.
|
||||
.peerPublic(selfPqc.getPublic())
|
||||
// ...Build initiator.
|
||||
.buildInitiator();
|
||||
|
||||
// ...Produce the envelope header (peer message); must be stored next to
|
||||
// ciphertext.
|
||||
byte[] peerMsg = encKex.getPeerMessage();
|
||||
System.out.println("...peerMsg " + lens(peerMsg) + " " + shortHex(peerMsg, 48));
|
||||
|
||||
// ...Derive OKM for this local envelope.
|
||||
byte[] okm = encKex.deriveSecret();
|
||||
System.out.println("...okm " + shortHex(okm, 48));
|
||||
|
||||
// ...Create exporter directly from OKM and profile salt.
|
||||
HybridKexExporter exporter = new HybridKexExporter(okm, profile.hkdfSalt());
|
||||
|
||||
// ...Encrypt: build pipeline; derived key/IV/AAD are injected into AES-GCM.
|
||||
DataContent enc = DataContentChainBuilder.encrypt()
|
||||
// ...Input: plaintext bytes.
|
||||
.add(PlainBytesBuilder.builder().bytes(msg))
|
||||
// ...AEAD: inject derived material into AES-GCM builder.
|
||||
.add(HybridDerived.from(exporter)
|
||||
// ...Purpose separation label for AEAD encryption.
|
||||
.label("app/local/aes-gcm")
|
||||
// ...Bind derivation to transcript bytes.
|
||||
.transcript(transcript.toByteArray())
|
||||
// ...Inject explicit AAD.
|
||||
.aad(aad)
|
||||
// ...Apply derived key(256b) and IV(12B) to AES-GCM with header.
|
||||
.applyToAesGcm(AesDataContentBuilder.builder()
|
||||
// ...Store IV in header for decrypt side.
|
||||
.withHeader()
|
||||
// ...Use AES-GCM with 128-bit authentication tag.
|
||||
.modeGcm(128), 256, 12))
|
||||
// ...Finalize pipeline.
|
||||
.build();
|
||||
|
||||
byte[] ciphertext;
|
||||
try (InputStream in = enc.getStream()) {
|
||||
ciphertext = readAll(in);
|
||||
}
|
||||
System.out.println("...ciphertext " + lens(ciphertext) + " " + shortHex(ciphertext, 48));
|
||||
|
||||
// ...Build local responder (decapsulation) using our own private keys and
|
||||
// stored peer message.
|
||||
HybridKexContext decKex = HybridKexBuilder.builder()
|
||||
// ...Set mandatory profile.
|
||||
.profile(profile)
|
||||
// ...Bind derivation to transcript.
|
||||
.transcript(transcript)
|
||||
// ...Select classic mode: peer public key is known out-of-band (here: our own
|
||||
// public key).
|
||||
.classicAgreement()
|
||||
// ...Classic algorithm id (X25519).
|
||||
.algorithm("Xdh")
|
||||
// ...Classic spec (X25519).
|
||||
.spec(XdhSpec.X25519)
|
||||
// ...Use our private key.
|
||||
.privateKey(selfClassic.getPrivate())
|
||||
// ...Use our public key as the peer public key (self-recipient).
|
||||
.peerPublic(selfClassic.getPublic())
|
||||
// ...Switch to PQC KEM.
|
||||
.pqcKem()
|
||||
// ...PQC algorithm id (ML-KEM).
|
||||
.algorithm("ML-KEM")
|
||||
// ...Use our PQC private key for decapsulation.
|
||||
.privateKey(selfPqc.getPrivate())
|
||||
// ...Build responder.
|
||||
.buildResponder();
|
||||
|
||||
try {
|
||||
// ...Provide the stored peer message (envelope header) to complete
|
||||
// decapsulation.
|
||||
decKex.setPeerMessage(peerMsg);
|
||||
|
||||
// ...Derive the same OKM and create the exporter.
|
||||
byte[] okmDec = decKex.deriveSecret();
|
||||
System.out.println("...okmEqual " + Arrays.equals(okm, okmDec));
|
||||
if (!Arrays.equals(okm, okmDec)) {
|
||||
throw new IllegalStateException("Local hybrid envelope mismatch");
|
||||
}
|
||||
|
||||
HybridKexExporter exporterDec = new HybridKexExporter(okmDec, profile.hkdfSalt());
|
||||
|
||||
// ...Decrypt: rebuild the same derived inputs and run decrypt pipeline.
|
||||
DataContent dec = DataContentChainBuilder.decrypt()
|
||||
// ...Input: ciphertext bytes.
|
||||
.add(PlainBytesBuilder.builder().bytes(ciphertext))
|
||||
// ...AEAD: apply the same label/transcript/AAD to get identical key/IV.
|
||||
.add(HybridDerived.from(exporterDec)
|
||||
// ...Same purpose label as encryption.
|
||||
.label("app/local/aes-gcm")
|
||||
// ...Same transcript binding.
|
||||
.transcript(transcript.toByteArray())
|
||||
// ...Same explicit AAD.
|
||||
.aad(aad)
|
||||
// ...Apply derived key and IV to AES-GCM with header.
|
||||
.applyToAesGcm(AesDataContentBuilder.builder()
|
||||
// ...Parse IV from header.
|
||||
.withHeader()
|
||||
// ...Use AES-GCM with 128-bit authentication tag.
|
||||
.modeGcm(128), 256, 12))
|
||||
// ...Finalize pipeline.
|
||||
.build();
|
||||
|
||||
byte[] out;
|
||||
try (InputStream in = dec.getStream()) {
|
||||
out = readAll(in);
|
||||
}
|
||||
|
||||
System.out.println("...plaintextEqual " + Arrays.equals(msg, out));
|
||||
if (!Arrays.equals(msg, out)) {
|
||||
throw new IllegalStateException("Roundtrip mismatch");
|
||||
}
|
||||
|
||||
System.out.println("hybridDerived_aes_gcm_local_self_recipient...ok");
|
||||
} finally {
|
||||
closeQuiet(encKex);
|
||||
closeQuiet(decKex);
|
||||
}
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private static byte[] randomBytes(int len) {
|
||||
byte[] data = new byte[len];
|
||||
new SecureRandom().nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws Exception {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
in.transferTo(out);
|
||||
out.flush();
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String lens(byte[] b) {
|
||||
if (b == null) {
|
||||
return "len=null";
|
||||
}
|
||||
return "len=" + b.length;
|
||||
}
|
||||
|
||||
private static String shortHex(byte[] data, int maxBytes) {
|
||||
if (data == null) {
|
||||
return "null";
|
||||
}
|
||||
int n = Math.min(data.length, Math.max(0, maxBytes));
|
||||
StringBuilder sb = new StringBuilder(n * 2 + 3);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int v = data[i] & 0xFF;
|
||||
sb.append(Character.forDigit((v >>> 4) & 0x0F, 16));
|
||||
sb.append(Character.forDigit(v & 0x0F, 16));
|
||||
}
|
||||
if (data.length > n) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void closeQuiet(HybridKexContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ctx.close();
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
536
samples/src/test/java/demo/HybridKexDemoTest.java
Normal file
536
samples/src/test/java/demo/HybridKexDemoTest.java
Normal file
@@ -0,0 +1,536 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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 demo;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
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.HybridKexContexts;
|
||||
import zeroecho.sdk.hybrid.kex.HybridKexProfile;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Demonstration of hybrid key exchange (KEX) usage in ZeroEcho.
|
||||
*
|
||||
* <p>
|
||||
* Hybrid KEX in this project means:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>A <b>classic agreement</b> leg (DH/ECDH/XDH), and</li>
|
||||
* <li>A <b>post-quantum</b> leg implemented as a message-based agreement (KEM
|
||||
* adapter, e.g. ML-KEM).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The two independent secrets are combined using HKDF-SHA256 in the SDK hybrid
|
||||
* layer. The application consumes only the final derived keying material (OKM),
|
||||
* not the raw leg secrets.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Available hybrid variants</h2>
|
||||
* <p>
|
||||
* The classic leg can be wired in two ways:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>CLASSIC_AGREEMENT + KEM_ADAPTER</b> (most common in practice):
|
||||
* <ul>
|
||||
* <li>Classic peer public key is supplied out-of-band
|
||||
* (certificate/directory/session state).</li>
|
||||
* <li>The hybrid message carries only the PQC payload (KEM ciphertext).</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>PAIR_MESSAGE + KEM_ADAPTER</b> (fully message-oriented hybrid):
|
||||
* <ul>
|
||||
* <li>Classic public keys are carried explicitly as messages (SPKI
|
||||
* encodings).</li>
|
||||
* <li>The hybrid message carries both: classic public-key message and PQC
|
||||
* ciphertext.</li>
|
||||
* <li>Responder may reply with a classic message only (PQC part empty),
|
||||
* depending on PQC role.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Note on resource management</h2>
|
||||
* <p>
|
||||
* These examples intentionally avoid {@code try-with-resources}. Agreement
|
||||
* contexts represent protocol-level state rather than traditional I/O
|
||||
* resources; in real protocols their lifecycle often spans multiple
|
||||
* send/receive steps and does not naturally fit a single lexical scope. Using
|
||||
* explicit close blocks keeps the handshake lifecycle visible to the reader.
|
||||
* </p>
|
||||
*/
|
||||
class HybridKexDemoTest {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(HybridKexDemoTest.class.getName());
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC/BCPQC if present.
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void x25519_classicAgreement_plus_mlKem768_kemAdapter() throws Exception {
|
||||
logBegin("CLASSIC_AGREEMENT + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B");
|
||||
|
||||
// Classic leg keys (Xdh + XdhSpec.X25519).
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// PQC leg: Bob is the KEM recipient (has ML-KEM keypair).
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
// Hybrid profile: default HKDF label, 32-byte output suitable for symmetric
|
||||
// keys.
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
|
||||
try {
|
||||
// Alice (initiator): classic uses Alice private + Bob classic public
|
||||
// (out-of-band).
|
||||
// ...PQC uses Bob PQC public and will produce a KEM ciphertext.
|
||||
alice = HybridKexContexts.initiator(profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(),
|
||||
XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null);
|
||||
|
||||
// Bob (responder): classic uses Bob private + Alice classic public
|
||||
// (out-of-band).
|
||||
// ...PQC uses Bob PQC private and will consume Alice's ciphertext.
|
||||
bob = HybridKexContexts.responder(profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(),
|
||||
XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null);
|
||||
|
||||
// Alice -> Bob: hybrid message carries PQC ciphertext; classic part is empty.
|
||||
byte[] msgA = alice.getPeerMessage();
|
||||
System.out.println("...msgA " + lens(msgA) + " " + shortHex(msgA));
|
||||
|
||||
bob.setPeerMessage(msgA);
|
||||
|
||||
// Both derive the final hybrid OKM (HKDF output).
|
||||
byte[] okmA = alice.deriveSecret();
|
||||
byte[] okmB = bob.deriveSecret();
|
||||
|
||||
System.out.println("...okmA " + shortHex(okmA));
|
||||
System.out.println("...okmB " + shortHex(okmB));
|
||||
System.out.println("...equal " + Arrays.equals(okmA, okmB));
|
||||
|
||||
// Application would now feed OKM into symmetric key schedule / AEAD keys / etc.
|
||||
} finally {
|
||||
closeQuiet(alice);
|
||||
closeQuiet(bob);
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void x25519_pairMessage_plus_mlKem768_kemAdapter() throws Exception {
|
||||
logBegin("PAIR_MESSAGE + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B");
|
||||
|
||||
// Classic leg keys (Xdh + XdhSpec.X25519).
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// PQC leg: Bob is the KEM recipient (has ML-KEM keypair).
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
HybridKexContext alice = null;
|
||||
HybridKexContext bob = null;
|
||||
|
||||
try {
|
||||
// Alice classic leg is message-based (PAIR_MESSAGE): it will emit her public
|
||||
// key as SPKI bytes.
|
||||
// ...PQC leg (KEM initiator) will emit ciphertext.
|
||||
alice = HybridKexContexts.initiatorPairMessage(profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPublic(), null);
|
||||
|
||||
// Bob classic leg is message-based (PAIR_MESSAGE): it will emit his public key
|
||||
// as SPKI bytes.
|
||||
// ...PQC leg (KEM responder) consumes ciphertext and typically does not emit a
|
||||
// PQC message.
|
||||
bob = HybridKexContexts.responderPairMessage(profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519,
|
||||
"ML-KEM", bobPqc.getPrivate(), null);
|
||||
|
||||
// Alice -> Bob: hybrid message carries classic SPKI + PQC ciphertext.
|
||||
byte[] msgA = alice.getPeerMessage();
|
||||
System.out.println("...msgA " + lens(msgA) + " " + shortHex(msgA));
|
||||
bob.setPeerMessage(msgA);
|
||||
|
||||
// Bob -> Alice: hybrid message carries classic SPKI; PQC part may be empty
|
||||
// (depends on PQC role).
|
||||
byte[] msgB = bob.getPeerMessage();
|
||||
System.out.println("...msgB " + lens(msgB) + " " + shortHex(msgB));
|
||||
alice.setPeerMessage(msgB);
|
||||
|
||||
// Both derive the final hybrid OKM (HKDF output).
|
||||
byte[] okmA = alice.deriveSecret();
|
||||
byte[] okmB = bob.deriveSecret();
|
||||
|
||||
System.out.println("...okmA " + shortHex(okmA));
|
||||
System.out.println("...okmB " + shortHex(okmB));
|
||||
System.out.println("...equal " + Arrays.equals(okmA, okmB));
|
||||
} finally {
|
||||
closeQuiet(alice);
|
||||
closeQuiet(bob);
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void builder_x25519_classicAgreement_plus_mlKem768() throws Exception {
|
||||
logBegin("Builder", "CLASSIC_AGREEMENT + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B");
|
||||
|
||||
// ...Generate classic leg keys (Xdh + XdhSpec.X25519).
|
||||
java.security.KeyPair aliceClassic = zeroecho.core.CryptoAlgorithms.generateKeyPair("Xdh",
|
||||
zeroecho.core.alg.xdh.XdhSpec.X25519);
|
||||
java.security.KeyPair bobClassic = zeroecho.core.CryptoAlgorithms.generateKeyPair("Xdh",
|
||||
zeroecho.core.alg.xdh.XdhSpec.X25519);
|
||||
|
||||
// ...Generate PQC (recipient) keys (ML-KEM-768).
|
||||
java.security.KeyPair bobPqc = zeroecho.core.CryptoAlgorithms.generateKeyPair("ML-KEM",
|
||||
zeroecho.core.alg.kyber.KyberKeyGenSpec.kyber768());
|
||||
|
||||
// ...Create a profile for HKDF (output length 32 bytes).
|
||||
zeroecho.sdk.hybrid.kex.HybridKexProfile profile = zeroecho.sdk.hybrid.kex.HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// ...Build a transcript to bind HKDF info to public handshake context.
|
||||
zeroecho.sdk.hybrid.kex.HybridKexTranscript transcript = new zeroecho.sdk.hybrid.kex.HybridKexTranscript()
|
||||
.addUtf8("suite", "X25519+MLKEM768").addUtf8("role", "demo");
|
||||
|
||||
// ...Define a minimum-strength policy (example: classic >= 128, PQC >= 192, OKM
|
||||
// >= 32 bytes).
|
||||
zeroecho.sdk.hybrid.kex.HybridKexPolicy policy = new zeroecho.sdk.hybrid.kex.HybridKexPolicy(128, 192, 32);
|
||||
|
||||
// ...Start the builder.
|
||||
zeroecho.sdk.builders.HybridKexBuilder b = zeroecho.sdk.builders.HybridKexBuilder.builder()
|
||||
// ...Set HKDF profile (salt/info/outLen).
|
||||
.profile(profile)
|
||||
// ...Bind HKDF info to transcript (protocol context).
|
||||
.transcript(transcript)
|
||||
// ...Enable hybrid policy gating for this build.
|
||||
.policy(policy);
|
||||
|
||||
// ...Select classic mode where peer public key is known out-of-band.
|
||||
zeroecho.sdk.builders.HybridKexBuilder.ClassicAgreement classicCfg = b.classicAgreement()
|
||||
// ...Set classic algorithm id (X25519 is represented as "Xdh" with
|
||||
// XdhSpec.X25519).
|
||||
.algorithm("Xdh")
|
||||
// ...Set classic parameters (X25519 curve spec).
|
||||
.spec(zeroecho.core.alg.xdh.XdhSpec.X25519)
|
||||
// ...Set local classic private key (Alice).
|
||||
.privateKey(aliceClassic.getPrivate())
|
||||
// ...Set peer classic public key (Bob).
|
||||
.peerPublic(bobClassic.getPublic());
|
||||
|
||||
// ...Continue with PQC KEM adapter configuration for initiator role.
|
||||
zeroecho.sdk.hybrid.kex.HybridKexContext alice = classicCfg.pqcKem()
|
||||
// ...Set PQC algorithm id (ML-KEM).
|
||||
.algorithm("ML-KEM")
|
||||
// ...Set PQC recipient public key (Bob).
|
||||
.peerPublic(bobPqc.getPublic())
|
||||
// ...Build initiator-side hybrid context.
|
||||
.buildInitiator();
|
||||
|
||||
// ...Build responder-side context (Bob) with symmetric configuration (note: PQC
|
||||
// uses private key).
|
||||
zeroecho.sdk.hybrid.kex.HybridKexContext bob = zeroecho.sdk.builders.HybridKexBuilder.builder()
|
||||
// ...Set the same HKDF profile to derive the same OKM.
|
||||
.profile(profile)
|
||||
// ...Bind the same transcript to ensure both sides derive the same OKM.
|
||||
.transcript(transcript)
|
||||
// ...Enable the same policy gate.
|
||||
.policy(policy)
|
||||
// ...Select classic agreement mode (peer public is out-of-band).
|
||||
.classicAgreement()
|
||||
// ...Set classic algorithm id.
|
||||
.algorithm("Xdh")
|
||||
// ...Set classic parameters.
|
||||
.spec(zeroecho.core.alg.xdh.XdhSpec.X25519)
|
||||
// ...Set local classic private key (Bob).
|
||||
.privateKey(bobClassic.getPrivate())
|
||||
// ...Set peer classic public key (Alice).
|
||||
.peerPublic(aliceClassic.getPublic())
|
||||
// ...Continue to PQC configuration.
|
||||
.pqcKem()
|
||||
// ...Set PQC algorithm id.
|
||||
.algorithm("ML-KEM")
|
||||
// ...Set PQC recipient private key (Bob).
|
||||
.privateKey(bobPqc.getPrivate())
|
||||
// ...Build responder-side hybrid context.
|
||||
.buildResponder();
|
||||
|
||||
try {
|
||||
// ...Alice produces the hybrid message (PQC ciphertext; classic part is empty
|
||||
// in this mode).
|
||||
byte[] msgA = alice.getPeerMessage();
|
||||
System.out.println("...msgA " + lens(msgA) + " " + shortHex(msgA));
|
||||
|
||||
// ...Bob consumes Alice message.
|
||||
bob.setPeerMessage(msgA);
|
||||
|
||||
// ...Both sides derive identical OKM.
|
||||
byte[] okmA = alice.deriveSecret();
|
||||
byte[] okmB = bob.deriveSecret();
|
||||
|
||||
System.out.println("...okmA " + shortHex(okmA));
|
||||
System.out.println("...okmB " + shortHex(okmB));
|
||||
System.out.println("...equal " + java.util.Arrays.equals(okmA, okmB));
|
||||
|
||||
// ...Use exporter to derive purpose-specific keys bound to transcript.
|
||||
zeroecho.sdk.hybrid.kex.HybridKexExporter exporterA = b.exporterFromOkm(okmA);
|
||||
byte[] txA = exporterA.export("app/tx", transcript.toByteArray(), 32);
|
||||
byte[] rxA = exporterA.export("app/rx", transcript.toByteArray(), 32);
|
||||
|
||||
System.out.println("...txA " + shortHex(txA));
|
||||
System.out.println("...rxA " + shortHex(rxA));
|
||||
} finally {
|
||||
closeQuiet(alice);
|
||||
closeQuiet(bob);
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void builder_x25519_pairMessage_plus_mlKem768() throws Exception {
|
||||
logBegin("Builder", "PAIR_MESSAGE + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B");
|
||||
|
||||
// ...Generate classic leg keys (Xdh + XdhSpec.X25519).
|
||||
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
|
||||
|
||||
// ...Generate PQC (recipient) keys (ML-KEM-768).
|
||||
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
|
||||
|
||||
// ...Create a profile for HKDF (output length 32 bytes).
|
||||
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
|
||||
|
||||
// ...Build a transcript to bind HKDF info to public handshake context.
|
||||
// ...(Use identical transcript on both sides to derive the same OKM.)
|
||||
zeroecho.sdk.hybrid.kex.HybridKexTranscript transcript = new zeroecho.sdk.hybrid.kex.HybridKexTranscript()
|
||||
.addUtf8("suite", "X25519+MLKEM768").addUtf8("mode", "PAIR_MESSAGE").addUtf8("role", "demo");
|
||||
|
||||
// ...Define a minimum-strength policy (example: classic >= 128, PQC >= 192, OKM
|
||||
// >= 32 bytes).
|
||||
zeroecho.sdk.hybrid.kex.HybridKexPolicy policy = new zeroecho.sdk.hybrid.kex.HybridKexPolicy(128, 192, 32);
|
||||
|
||||
// ...Start the initiator builder.
|
||||
zeroecho.sdk.builders.HybridKexBuilder initBuilder = zeroecho.sdk.builders.HybridKexBuilder.builder()
|
||||
// ...Set HKDF profile (salt/info/outLen).
|
||||
.profile(profile)
|
||||
// ...Bind HKDF info to transcript (protocol context).
|
||||
.transcript(transcript)
|
||||
// ...Enable hybrid policy gating for this build.
|
||||
.policy(policy);
|
||||
|
||||
// ...Select classic mode where the public key is carried in-band as a classic
|
||||
// message (PAIR_MESSAGE).
|
||||
zeroecho.sdk.hybrid.kex.HybridKexContext alice = initBuilder
|
||||
// ...Switch classic leg to PAIR_MESSAGE mode.
|
||||
.classicPairMessage()
|
||||
// ...Set classic algorithm id (X25519 is represented as "Xdh" with
|
||||
// XdhSpec.X25519).
|
||||
.algorithm("Xdh")
|
||||
// ...Set classic parameters (X25519 curve spec).
|
||||
.spec(XdhSpec.X25519)
|
||||
// ...Set local classic key pair wrapper (Alice).
|
||||
.keyPair(new KeyPairKey(aliceClassic))
|
||||
// ...Continue with PQC KEM adapter configuration.
|
||||
.pqcKem()
|
||||
// ...Set PQC algorithm id (ML-KEM).
|
||||
.algorithm("ML-KEM")
|
||||
// ...Set PQC recipient public key (Bob).
|
||||
.peerPublic(bobPqc.getPublic())
|
||||
// ...Build initiator-side hybrid context.
|
||||
.buildInitiator();
|
||||
|
||||
// ...Start the responder builder.
|
||||
zeroecho.sdk.builders.HybridKexBuilder respBuilder = zeroecho.sdk.builders.HybridKexBuilder.builder()
|
||||
// ...Set the same HKDF profile to derive the same OKM.
|
||||
.profile(profile)
|
||||
// ...Bind the same transcript to ensure both sides derive the same OKM.
|
||||
.transcript(transcript)
|
||||
// ...Enable the same policy gate.
|
||||
.policy(policy);
|
||||
|
||||
// ...Build responder-side context (Bob).
|
||||
zeroecho.sdk.hybrid.kex.HybridKexContext bob = respBuilder
|
||||
// ...Switch classic leg to PAIR_MESSAGE mode.
|
||||
.classicPairMessage()
|
||||
// ...Set classic algorithm id.
|
||||
.algorithm("Xdh")
|
||||
// ...Set classic parameters.
|
||||
.spec(XdhSpec.X25519)
|
||||
// ...Set local classic key pair wrapper (Bob).
|
||||
.keyPair(new KeyPairKey(bobClassic))
|
||||
// ...Continue with PQC KEM adapter configuration.
|
||||
.pqcKem()
|
||||
// ...Set PQC algorithm id.
|
||||
.algorithm("ML-KEM")
|
||||
// ...Set PQC recipient private key (Bob).
|
||||
.privateKey(bobPqc.getPrivate())
|
||||
// ...Build responder-side hybrid context.
|
||||
.buildResponder();
|
||||
|
||||
try {
|
||||
// ...Alice produces the hybrid message: classic SPKI + PQC ciphertext.
|
||||
byte[] msgA = alice.getPeerMessage();
|
||||
System.out.println("...msgA " + lens(msgA) + " " + shortHex(msgA));
|
||||
|
||||
// ...Bob consumes Alice message (learns Alice classic public + decapsulates PQC
|
||||
// ciphertext).
|
||||
bob.setPeerMessage(msgA);
|
||||
|
||||
// ...Bob produces response message: classic SPKI; PQC part may be empty
|
||||
// (role-dependent).
|
||||
byte[] msgB = bob.getPeerMessage();
|
||||
System.out.println("...msgB " + lens(msgB) + " " + shortHex(msgB));
|
||||
|
||||
// ...Alice consumes Bob message (learns Bob classic public).
|
||||
alice.setPeerMessage(msgB);
|
||||
|
||||
// ...Both sides derive identical OKM.
|
||||
byte[] okmA = alice.deriveSecret();
|
||||
byte[] okmB = bob.deriveSecret();
|
||||
|
||||
System.out.println("...okmA " + shortHex(okmA));
|
||||
System.out.println("...okmB " + shortHex(okmB));
|
||||
System.out.println("...equal " + Arrays.equals(okmA, okmB));
|
||||
|
||||
// ...Use exporter to derive purpose-specific keys bound to transcript.
|
||||
zeroecho.sdk.hybrid.kex.HybridKexExporter exporterA = initBuilder.exporterFromOkm(okmA);
|
||||
byte[] txA = exporterA.export("app/tx", transcript.toByteArray(), 32);
|
||||
byte[] rxA = exporterA.export("app/rx", transcript.toByteArray(), 32);
|
||||
|
||||
System.out.println("...txA " + shortHex(txA));
|
||||
System.out.println("...rxA " + shortHex(rxA));
|
||||
} finally {
|
||||
closeQuiet(alice);
|
||||
closeQuiet(bob);
|
||||
}
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// helpers (JUnit output conventions)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static void logBegin(Object... params) {
|
||||
String thisClass = HybridKexDemoTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
|
||||
}
|
||||
|
||||
private static void logEnd() {
|
||||
String thisClass = HybridKexDemoTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "...ok");
|
||||
}
|
||||
|
||||
private static void closeQuiet(HybridKexContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ctx.close();
|
||||
} catch (Exception e) {
|
||||
LOG.fine("close failed: " + e.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static String shortHex(byte[] b) {
|
||||
if (b == null) {
|
||||
return "null";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(b.length * 2);
|
||||
for (byte v : b) {
|
||||
if (sb.length() == 80) {
|
||||
sb.append("...");
|
||||
break;
|
||||
}
|
||||
if ((v & 0xFF) < 16) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(Integer.toHexString(v & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String lens(byte[] msg) {
|
||||
if (msg == null || msg.length < 8) {
|
||||
return "[classicLen=?, pqcLen=?]";
|
||||
}
|
||||
try {
|
||||
DataInputStream in = new DataInputStream(new ByteArrayInputStream(msg));
|
||||
int classicLen = in.readInt();
|
||||
if (classicLen < 0) {
|
||||
return "[classicLen=?, pqcLen=?]";
|
||||
}
|
||||
if (classicLen > 0) {
|
||||
in.skipBytes(classicLen);
|
||||
}
|
||||
int pqcLen = in.readInt();
|
||||
return "[classicLen=" + classicLen + ", pqcLen=" + pqcLen + "]";
|
||||
} catch (Exception e) {
|
||||
return "[classicLen=?, pqcLen=?]";
|
||||
}
|
||||
}
|
||||
}
|
||||
240
samples/src/test/java/demo/HybridSigningAesTest.java
Normal file
240
samples/src/test/java/demo/HybridSigningAesTest.java
Normal file
@@ -0,0 +1,240 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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 demo;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Signature;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainBytesBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.hybrid.signature.HybridSignatureContexts;
|
||||
import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Demonstration of hybrid signing combined with AES-GCM encryption.
|
||||
*
|
||||
* <p>
|
||||
* This sample shows both canonical compositions:
|
||||
* <ul>
|
||||
* <li>StE: Sign-then-Encrypt</li>
|
||||
* <li>EtS: Encrypt-then-Sign</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Hybrid signature used here (popular practical choice): Ed25519 + SPHINCS+
|
||||
* with AND verification.
|
||||
* </p>
|
||||
*/
|
||||
class HybridSigningAesTest {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(HybridSigningAesTest.class.getName());
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
|
||||
// etc.)
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep tests runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void aesRoundStE_withHybridSignature() throws GeneralSecurityException, IOException {
|
||||
LOG.info("aesRoundStE_withHybridSignature - Sign then Encrypt (Hybrid signature)");
|
||||
|
||||
// Prepare plaintext
|
||||
byte[] msg = randomBytes(100);
|
||||
|
||||
// AES-GCM with header, runtime params are stored in header
|
||||
AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader();
|
||||
|
||||
// Hybrid signature: Ed25519 + SPHINCS+ (AND)
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
KeyPair ed = generateKeyPair("Ed25519");
|
||||
KeyPair spx = generateKeyPair("SPHINCS+");
|
||||
|
||||
SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024);
|
||||
SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024);
|
||||
|
||||
// For verification, make mismatch behavior explicit (builder also supports
|
||||
// throwOnMismatch()).
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
// Build StE pipeline: PLAIN -> SIGN(trailer) -> ENCRYPT
|
||||
DataContent dccb = DataContentChainBuilder.encrypt()
|
||||
// plaintext source
|
||||
.add(PlainBytesBuilder.builder().bytes(msg))
|
||||
// hybrid signature trailer
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagEnc).bufferSize(8192))
|
||||
// AES-GCM encryption
|
||||
.add(aesBuilder).build();
|
||||
|
||||
SecretKey aesKey = aesBuilder.generatedKey();
|
||||
LOG.log(Level.INFO, "StE: produced ciphertext, aesKeySizeBits={0}",
|
||||
Integer.valueOf(aesKey.getEncoded().length * 8));
|
||||
|
||||
byte[] ciphertext;
|
||||
try (InputStream in = dccb.getStream()) {
|
||||
ciphertext = readAll(in);
|
||||
}
|
||||
LOG.log(Level.INFO, "StE: ciphertextSize={0}", Integer.valueOf(ciphertext.length));
|
||||
|
||||
// Build decrypt pipeline: ENCRYPTED -> DECRYPT -> VERIFY(trailer)
|
||||
dccb = DataContentChainBuilder.decrypt()
|
||||
// encrypted input
|
||||
.add(PlainBytesBuilder.builder().bytes(ciphertext))
|
||||
// AES-GCM decryption
|
||||
.add(AesDataContentBuilder.builder().importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader())
|
||||
// hybrid signature verification
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch()).build();
|
||||
|
||||
byte[] decrypted;
|
||||
try (InputStream in = dccb.getStream()) {
|
||||
decrypted = readAll(in);
|
||||
}
|
||||
|
||||
LOG.log(Level.INFO, "StE: roundtrip ok={0}", Boolean.valueOf(java.util.Arrays.equals(msg, decrypted)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aesRoundEtS_withHybridSignature() throws GeneralSecurityException, IOException {
|
||||
LOG.info("aesRoundEtS_withHybridSignature - Encrypt then Sign (Hybrid signature)");
|
||||
|
||||
// Prepare plaintext
|
||||
byte[] msg = randomBytes(100);
|
||||
|
||||
// AES-GCM with header, runtime params are stored in header
|
||||
AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader();
|
||||
|
||||
// Hybrid signature: Ed25519 + SPHINCS+ (AND)
|
||||
HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
|
||||
HybridSignatureProfile.VerifyRule.AND);
|
||||
|
||||
KeyPair ed = generateKeyPair("Ed25519");
|
||||
KeyPair spx = generateKeyPair("SPHINCS+");
|
||||
|
||||
SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
|
||||
2 * 1024 * 1024);
|
||||
SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
|
||||
2 * 1024 * 1024);
|
||||
|
||||
tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
|
||||
|
||||
// Build EtS pipeline: PLAIN -> ENCRYPT -> SIGN(trailer)
|
||||
DataContent dccb = DataContentChainBuilder.encrypt()
|
||||
// plaintext source
|
||||
.add(PlainBytesBuilder.builder().bytes(msg))
|
||||
// AES-GCM encryption
|
||||
.add(aesBuilder)
|
||||
// hybrid signature trailer
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagEnc).bufferSize(8192)).build();
|
||||
|
||||
SecretKey aesKey = aesBuilder.generatedKey();
|
||||
LOG.log(Level.INFO, "EtS: produced ciphertext, aesKeySizeBits={0}",
|
||||
Integer.valueOf(aesKey.getEncoded().length * 8));
|
||||
|
||||
byte[] ciphertext;
|
||||
try (InputStream in = dccb.getStream()) {
|
||||
ciphertext = readAll(in);
|
||||
}
|
||||
LOG.log(Level.INFO, "EtS: ciphertextSize={0}", Integer.valueOf(ciphertext.length));
|
||||
|
||||
// Build decrypt pipeline: ENCRYPTED -> VERIFY(trailer) -> DECRYPT
|
||||
// Verification runs during streaming; consumer gets plaintext only if signature
|
||||
// matches.
|
||||
dccb = DataContentChainBuilder.decrypt()
|
||||
// encrypted input
|
||||
.add(PlainBytesBuilder.builder().bytes(ciphertext))
|
||||
// hybrid signature verification
|
||||
.add(new TagTrailerDataContentBuilder<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
|
||||
// AES-GCM decryption
|
||||
.add(AesDataContentBuilder.builder().importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader())
|
||||
.build();
|
||||
|
||||
byte[] decrypted;
|
||||
try (InputStream in = dccb.getStream()) {
|
||||
decrypted = readAll(in);
|
||||
}
|
||||
|
||||
LOG.log(Level.INFO, "EtS: roundtrip ok={0}", Boolean.valueOf(java.util.Arrays.equals(msg, decrypted)));
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private static KeyPair generateKeyPair(String algId) throws GeneralSecurityException {
|
||||
CryptoAlgorithm alg = CryptoAlgorithms.require(algId);
|
||||
return alg.generateKeyPair();
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int len) {
|
||||
byte[] data = new byte[len];
|
||||
new SecureRandom().nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws IOException {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
in.transferTo(out);
|
||||
out.flush();
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,7 @@ import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.alg.aes.AesKeyGenSpec;
|
||||
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
|
||||
import zeroecho.core.alg.rsa.RsaSigSpec;
|
||||
import zeroecho.core.tag.TagEngine;
|
||||
@@ -65,23 +63,6 @@ import zeroecho.sdk.content.api.DataContent;
|
||||
class SigningAesTest {
|
||||
private static final Logger LOG = Logger.getLogger(SigningAesTest.class.getName());
|
||||
|
||||
SecretKey generateAesKey() throws GeneralSecurityException {
|
||||
// Locate the AES algorithm in the catalog
|
||||
CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
|
||||
SecretKey key = aes
|
||||
// Retrieve the builder that works with AesKeyGenSpec - the specification for
|
||||
// AES key generation
|
||||
.symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
// Generate a secret key according to the AES256 specification
|
||||
.generateSecret(AesKeyGenSpec.aes256());
|
||||
// Log the generated key (truncated to short hex for readability)
|
||||
LOG.log(Level.INFO, "AES256 key generated: {0}", Strings.toShortHexString(key.getEncoded()));
|
||||
|
||||
// or just: CryptoAlgorithms.generateSecret("AES", AesKeyGenSpec.aes256())
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
KeyPair generateRsaKeys() throws GeneralSecurityException {
|
||||
KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user