feat: add ML-DSA (FIPS 204) support with policy enforcement
Introduce ML-DSA (FIPS 204) as a first-class signature algorithm: - algorithm binding and streaming signature context - key generation specs/builders and key import specs - correct handling of pure vs pre-hash (SHA-512) ML-DSA JCA variants - policy security strength mapping (44/65/87 → 128/192/256) - comprehensive JUnit streaming sign/verify tests Signed-off-by: Leo Galambos <lg@hq.egothor.org>
This commit is contained in:
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;
|
||||||
@@ -70,6 +70,8 @@ import javax.crypto.SecretKey;
|
|||||||
* {@code key.getAlgorithm()} (for example, 512, 768, 1024 for ML-KEM;
|
* {@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.
|
* 640/976/1344 for FrodoKEM) or from NIST security levels labeled as L1/L3/L5.
|
||||||
* If neither is present, defaults to 128.</li>
|
* 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()}
|
* <li><b>SLH-DSA:</b> parse 128/192/256 from {@code key.getAlgorithm()}
|
||||||
* similarly to SPHINCS+; else default 128.</li>
|
* similarly to SPHINCS+; else default 128.</li>
|
||||||
* <li><b>SPHINCS+:</b> parses the parameter size 128/192/256 from the algorithm
|
* <li><b>SPHINCS+:</b> parses the parameter size 128/192/256 from the algorithm
|
||||||
@@ -125,6 +127,7 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
|||||||
private static final Pattern HMAC_SHA_PATTERN = Pattern.compile("HMAC(?:-)?SHA(?:-)?(1|224|256|384|512)",
|
private static final Pattern HMAC_SHA_PATTERN = Pattern.compile("HMAC(?:-)?SHA(?:-)?(1|224|256|384|512)",
|
||||||
Pattern.CASE_INSENSITIVE);
|
Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern SLHDSA_STRENGTH_PATTERN = Pattern.compile("(128|192|256)");
|
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() {
|
private SecurityStrengthAdvisor() {
|
||||||
}
|
}
|
||||||
@@ -156,6 +159,7 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
|||||||
case "ED25519" -> 128;
|
case "ED25519" -> 128;
|
||||||
case "ED448" -> 224;
|
case "ED448" -> 224;
|
||||||
case "ML-KEM" -> kyberStrength(key);
|
case "ML-KEM" -> kyberStrength(key);
|
||||||
|
case "ML-DSA", "MLDSA" -> mldsaStrength(key);
|
||||||
case "BIKE" -> mapByNistLevel(key, 128, 192, 256);
|
case "BIKE" -> mapByNistLevel(key, 128, 192, 256);
|
||||||
case "HQC" -> mapByNistLevel(key, 128, 192, 256);
|
case "HQC" -> mapByNistLevel(key, 128, 192, 256);
|
||||||
case "FRODO" -> frodoStrength(key);
|
case "FRODO" -> frodoStrength(key);
|
||||||
@@ -445,4 +449,27 @@ public final class SecurityStrengthAdvisor { // NOPMD
|
|||||||
return 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ zeroecho.core.alg.frodo.FrodoAlgorithm
|
|||||||
zeroecho.core.alg.hmac.HmacAlgorithm
|
zeroecho.core.alg.hmac.HmacAlgorithm
|
||||||
zeroecho.core.alg.hqc.HqcAlgorithm
|
zeroecho.core.alg.hqc.HqcAlgorithm
|
||||||
zeroecho.core.alg.kyber.KyberAlgorithm
|
zeroecho.core.alg.kyber.KyberAlgorithm
|
||||||
|
zeroecho.core.alg.mldsa.MldsaAlgorithm
|
||||||
zeroecho.core.alg.ntru.NtruAlgorithm
|
zeroecho.core.alg.ntru.NtruAlgorithm
|
||||||
zeroecho.core.alg.ntruprime.NtrulPrimeAlgorithm
|
zeroecho.core.alg.ntruprime.NtrulPrimeAlgorithm
|
||||||
zeroecho.core.alg.ntruprime.SntruPrimeAlgorithm
|
zeroecho.core.alg.ntruprime.SntruPrimeAlgorithm
|
||||||
|
|||||||
@@ -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' };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user