diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java new file mode 100644 index 0000000..a050a77 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java @@ -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. + * + *
+ * This binding exposes ML-DSA as a first-class algorithm identity while relying + * on the provider's ML-DSA JCA implementations. + *
+ * + *+ * This algorithm registers two roles: + *
+ *+ * Both roles are configured with {@link VoidSpec}, as ML-DSA requires no + * runtime context parameters beyond the key material. + *
+ * + * @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); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java new file mode 100644 index 0000000..fe1b8eb --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java @@ -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. + * + *+ * 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. + *
+ * + * @since 1.0 + */ +public final class MldsaKeyGenBuilder implements AsymmetricKeyBuilder+ * 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. + *
+ * + *+ * This spec intentionally restricts choices to the standardized ML-DSA + * parameter sets only. + *
+ * + * @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. + * + *+ * If the provider exposes "with hash" variants as distinct parameter specs, + * they can be selected here. + *
+ * + * @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. + * + *+ * This bypasses automatic mapping. The name must match a static field in + * {@code org.bouncycastle.jcajce.spec.MLDSAParameterSpec}. + *
+ * + * @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; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPrivateKeyBuilder.java new file mode 100644 index 0000000..7a51ce5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPrivateKeyBuilder.java @@ -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+ * {@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. + *
+ * + *+ * The default provider is {@code "BC"}. + *
+ * + * @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); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java new file mode 100644 index 0000000..a294831 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java @@ -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+ * {@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. + *
+ * + *+ * The default provider is {@code "BC"}. + *
+ * + * @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); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaSignatureContext.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaSignatureContext.java new file mode 100644 index 0000000..d327f24 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaSignatureContext.java @@ -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). + * + *+ * {@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}. + *
+ * + *+ * The produced signature length is resolved by probing the JCA engine via + * {@link GenericJcaSignatureContext.SignLengthResolver#probeWith(String, String)}. + *
+ * + * @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. + * + *+ * The expected signature length is derived from the public key parameter set + * via {@link MLDSAParameterSpec#getName()} and canonical ML-DSA signature + * sizes. + *
+ * + * @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. + * + *+ * 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. + *
+ * + *+ * Canonical signature sizes: + *
+ *+ * Once closed, the context must not be reused. + *
+ */ + @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+ * This package provides the ZeroEcho binding for ML-DSA + * (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. + *
+ * + *+ * 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}. + *
+ * + *+ * Follows project rule "10) JUnit testy": prints test name, prints intermediate + * results with the {@code "..."} prefix, and prints {@code "...ok"} on success. + *
+ * + * @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' }; +}