feat: SLH-DSA (FIPS 205) signature algorithm added
Signed-off-by: Leo Galambos <lg@hq.egothor.org>
This commit is contained in:
106
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java
Normal file
106
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java
Normal file
@@ -0,0 +1,106 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
|
||||
/**
|
||||
* SLH-DSA (FIPS 205) signature algorithm binding for the ZeroEcho framework.
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is the NIST-standardized profile of the SPHINCS+ stateless hash-based
|
||||
* signature scheme. This binding exposes SLH-DSA as a first-class algorithm
|
||||
* identity while relying on the provider's SLH-DSA JCA implementations.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This algorithm registers two roles:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link KeyUsage#SIGN}: produces SLH-DSA signatures using a
|
||||
* {@link PrivateKey}.</li>
|
||||
* <li>{@link KeyUsage#VERIFY}: verifies SLH-DSA signatures using a
|
||||
* {@link PublicKey}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Both roles are configured with {@link VoidSpec}, as SLH-DSA requires no
|
||||
* runtime context parameters beyond the key material.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaAlgorithm extends AbstractCryptoAlgorithm {
|
||||
|
||||
/**
|
||||
* Creates a new SLH-DSA algorithm instance and registers its capabilities.
|
||||
*
|
||||
* @throws IllegalArgumentException if a signature context cannot be initialized
|
||||
* due to provider errors
|
||||
*/
|
||||
public SlhDsaAlgorithm() {
|
||||
super("SLH-DSA", "SLHDSA");
|
||||
|
||||
capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, VoidSpec.class,
|
||||
(PrivateKey k, VoidSpec s) -> {
|
||||
try {
|
||||
return new SlhDsaSignatureContext(this, k);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Cannot init SLH-DSA signer", e);
|
||||
}
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, VoidSpec.class,
|
||||
(PublicKey k, VoidSpec s) -> {
|
||||
try {
|
||||
return new SlhDsaSignatureContext(this, k);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Cannot init SLH-DSA verifier", e);
|
||||
}
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(SlhDsaKeyGenSpec.class, new SlhDsaKeyGenBuilder(), SlhDsaKeyGenSpec::defaultSpec);
|
||||
registerAsymmetricKeyBuilder(SlhDsaPublicKeySpec.class, new SlhDsaPublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(SlhDsaPrivateKeySpec.class, new SlhDsaPrivateKeyBuilder(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Key pair builder for SLH-DSA (FIPS 205) using the Bouncy Castle PQC provider.
|
||||
*
|
||||
* <p>
|
||||
* This builder maps {@link SlhDsaKeyGenSpec} to the appropriate
|
||||
* {@code org.bouncycastle.jcajce.spec.SLHDSAParameterSpec} constant.
|
||||
* :contentReference[oaicite:3]{index=3} Reflection is used to avoid a hard
|
||||
* dependency on any particular set of parameter constants across provider
|
||||
* versions.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaKeyGenBuilder implements AsymmetricKeyBuilder<SlhDsaKeyGenSpec> {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(SlhDsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec");
|
||||
Object bcParamSpec = resolveBcParameterSpec(spec);
|
||||
|
||||
KeyPairGenerator kpg = (spec.providerName() == null) ? KeyPairGenerator.getInstance(ALG)
|
||||
: KeyPairGenerator.getInstance(ALG, spec.providerName());
|
||||
|
||||
if (bcParamSpec != null) {
|
||||
kpg.initialize((java.security.spec.AlgorithmParameterSpec) bcParamSpec);
|
||||
}
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.security.PublicKey importPublic(SlhDsaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPublicKeySpec to import a public key.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.security.PrivateKey importPrivate(SlhDsaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPrivateKeySpec to import a private key.");
|
||||
}
|
||||
|
||||
private static Object resolveBcParameterSpec(SlhDsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
if (spec.explicitParamConstant() != null) {
|
||||
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.SLHDSAParameterSpec",
|
||||
spec.explicitParamConstant());
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
throw new GeneralSecurityException("Unknown SLHDSAParameterSpec constant: " + spec.explicitParamConstant());
|
||||
}
|
||||
|
||||
String fam = (spec.hash() == SlhDsaKeyGenSpec.Hash.SHA2) ? "sha2" : "shake";
|
||||
String bits = Integer.toString(spec.security().bits);
|
||||
String v = (spec.variant() == SlhDsaKeyGenSpec.Variant.FAST) ? "f" : "s";
|
||||
|
||||
// Base constant name: slh_dsa_{sha2|shake}_{128|192|256}{f|s}
|
||||
String base = "slh_dsa_" + fam + "_" + bits + v;
|
||||
|
||||
// Optional pre-hash suffix used by BC:
|
||||
// _with_sha256/_with_sha512/_with_shake128/_with_shake256
|
||||
// :contentReference[oaicite:4]{index=4}
|
||||
String suffix = "";
|
||||
if (spec.preHash() != SlhDsaKeyGenSpec.PreHash.NONE) {
|
||||
suffix = "_with_" + spec.preHash().name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
String name = base + suffix;
|
||||
|
||||
// Validate supported combinations (fail fast with a clear message).
|
||||
validateCombination(spec);
|
||||
|
||||
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.SLHDSAParameterSpec", name);
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
// As a fallback, attempt base (no pre-hash), then fail.
|
||||
if (!suffix.isEmpty()) {
|
||||
Object baseC = fetchStaticField("org.bouncycastle.jcajce.spec.SLHDSAParameterSpec", base);
|
||||
if (baseC != null) {
|
||||
return baseC;
|
||||
}
|
||||
}
|
||||
|
||||
throw new GeneralSecurityException("Cannot resolve SLHDSAParameterSpec constant: " + name);
|
||||
}
|
||||
|
||||
private static void validateCombination(SlhDsaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
SlhDsaKeyGenSpec.Hash h = spec.hash();
|
||||
int bits = spec.security().bits;
|
||||
SlhDsaKeyGenSpec.PreHash p = spec.preHash();
|
||||
|
||||
if (p == SlhDsaKeyGenSpec.PreHash.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (h == SlhDsaKeyGenSpec.Hash.SHA2) {
|
||||
if (bits == 128 && p != SlhDsaKeyGenSpec.PreHash.SHA256) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHA2-128 supports only PreHash.SHA256.");
|
||||
}
|
||||
if ((bits == 192 || bits == 256) && p != SlhDsaKeyGenSpec.PreHash.SHA512) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHA2-192/256 support only PreHash.SHA512.");
|
||||
}
|
||||
} else {
|
||||
if (bits == 128 && p != SlhDsaKeyGenSpec.PreHash.SHAKE128) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHAKE-128 supports only PreHash.SHAKE128.");
|
||||
}
|
||||
if ((bits == 192 || bits == 256) && p != SlhDsaKeyGenSpec.PreHash.SHAKE256) {
|
||||
throw new GeneralSecurityException("SLH-DSA SHAKE-192/256 support only PreHash.SHAKE256.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Object fetchStaticField(String className, String field) {
|
||||
try {
|
||||
Class<?> cls = Class.forName(className);
|
||||
Field f = cls.getField(field);
|
||||
return f.get(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
249
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java
Normal file
249
lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java
Normal file
@@ -0,0 +1,249 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Specification for generating SLH-DSA key pairs (FIPS 205).
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is parameterized by hash family (SHA2 or SHAKE), a security strength
|
||||
* (128/192/256), and a variant (fast vs small). Additionally, Bouncy Castle
|
||||
* exposes pre-hash variants (e.g., "with SHA-256") as separate parameter specs.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This spec intentionally restricts the available choices to SLH-DSA parameters
|
||||
* (i.e., no Haraka and no SPHINCS+ "simple/robust" split).
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaKeyGenSpec implements AlgorithmKeySpec {
|
||||
|
||||
/**
|
||||
* SLH-DSA hash families (FIPS 205).
|
||||
*/
|
||||
public enum Hash {
|
||||
/** SHA-2 based SLH-DSA parameter sets. */
|
||||
SHA2,
|
||||
/** SHAKE based SLH-DSA parameter sets. */
|
||||
SHAKE
|
||||
}
|
||||
|
||||
/**
|
||||
* Security levels as defined by NIST PQC (L1, L3, L5).
|
||||
*/
|
||||
public enum Security {
|
||||
/** NIST Level 1 (~128-bit security). */
|
||||
L1_128(128),
|
||||
/** NIST Level 3 (~192-bit security). */
|
||||
L3_192(192),
|
||||
/** NIST Level 5 (~256-bit security). */
|
||||
L5_256(256);
|
||||
|
||||
/** Claimed security level in bits. */
|
||||
public final int bits;
|
||||
|
||||
Security(int b) {
|
||||
bits = b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant trading performance against signature size.
|
||||
*
|
||||
* <p>
|
||||
* {@code FAST} variants are optimized for speed (larger signatures).
|
||||
* {@code SMALL} variants reduce signature size at some performance cost.
|
||||
* </p>
|
||||
*/
|
||||
public enum Variant {
|
||||
/** Larger, faster signatures. */
|
||||
FAST,
|
||||
/** Smaller, slower signatures. */
|
||||
SMALL
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional pre-hash variant selection.
|
||||
*
|
||||
* <p>
|
||||
* Bouncy Castle exposes "with hash" variants as distinct parameter specs, for
|
||||
* example {@code slh_dsa_sha2_128s_with_sha256}.
|
||||
* :contentReference[oaicite:1]{index=1}
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Valid combinations depend on the hash family and security strength:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>SHA2-128 uses SHA-256</li>
|
||||
* <li>SHA2-192/256 use SHA-512</li>
|
||||
* <li>SHAKE-128 uses SHAKE128</li>
|
||||
* <li>SHAKE-192/256 use SHAKE256</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum PreHash {
|
||||
/** No pre-hash parameter set (pure SLH-DSA). */
|
||||
NONE,
|
||||
/** Pre-hash with SHA-256 (only for SHA2-128). */
|
||||
SHA256,
|
||||
/** Pre-hash with SHA-512 (only for SHA2-192/256). */
|
||||
SHA512,
|
||||
/** Pre-hash with SHAKE128 (only for SHAKE-128). */
|
||||
SHAKE128,
|
||||
/** Pre-hash with SHAKE256 (only for SHAKE-192/256). */
|
||||
SHAKE256
|
||||
}
|
||||
|
||||
private static final SlhDsaKeyGenSpec DEFAULT = new SlhDsaKeyGenSpec("BC", Hash.SHAKE, Security.L5_256,
|
||||
Variant.SMALL, PreHash.NONE, null);
|
||||
|
||||
private final String providerName;
|
||||
private final Hash hash;
|
||||
private final Security security;
|
||||
private final Variant variant;
|
||||
private final PreHash preHash;
|
||||
private final String explicitParamConstant; // nullable
|
||||
|
||||
private SlhDsaKeyGenSpec(String providerName, Hash hash, Security security, Variant variant, PreHash preHash,
|
||||
String explicitParamConstant) {
|
||||
this.providerName = Objects.requireNonNull(providerName, "providerName");
|
||||
this.hash = Objects.requireNonNull(hash, "hash");
|
||||
this.security = Objects.requireNonNull(security, "security");
|
||||
this.variant = Objects.requireNonNull(variant, "variant");
|
||||
this.preHash = Objects.requireNonNull(preHash, "preHash");
|
||||
this.explicitParamConstant = explicitParamConstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default SLH-DSA key generation spec.
|
||||
*
|
||||
* @return a singleton default specification (SHAKE, L5, SMALL, no pre-hash)
|
||||
*/
|
||||
public static SlhDsaKeyGenSpec defaultSpec() {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification with explicit algorithm parameters.
|
||||
*
|
||||
* @param providerName JCA provider name (e.g., "BC", "BCPQC")
|
||||
* @param hash hash family (SHA2 or SHAKE)
|
||||
* @param security security level
|
||||
* @param variant variant (FAST vs SMALL)
|
||||
* @param preHash optional pre-hash selection
|
||||
* @return a new {@code SlhDsaKeyGenSpec}
|
||||
*/
|
||||
public static SlhDsaKeyGenSpec of(String providerName, Hash hash, Security security, Variant variant,
|
||||
PreHash preHash) {
|
||||
return new SlhDsaKeyGenSpec(providerName, hash, security, variant, preHash, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this specification with an explicit Bouncy Castle parameter
|
||||
* constant override.
|
||||
*
|
||||
* <p>
|
||||
* This bypasses automatic mapping. The name must match a static field in
|
||||
* {@code org.bouncycastle.jcajce.spec.SLHDSAParameterSpec}.
|
||||
* :contentReference[oaicite:2]{index=2}
|
||||
* </p>
|
||||
*
|
||||
* @param name field name in {@code SLHDSAParameterSpec}
|
||||
* @return a new {@code SlhDsaKeyGenSpec} with the override
|
||||
*/
|
||||
public SlhDsaKeyGenSpec withExplicitParamConstant(String name) {
|
||||
return new SlhDsaKeyGenSpec(providerName, hash, security, variant, preHash, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name used for key generation.
|
||||
*
|
||||
* @return provider name (never {@code null})
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SLH-DSA hash family.
|
||||
*
|
||||
* @return hash family
|
||||
*/
|
||||
public Hash hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the security level.
|
||||
*
|
||||
* @return security level
|
||||
*/
|
||||
public Security security() {
|
||||
return security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the variant.
|
||||
*
|
||||
* @return variant
|
||||
*/
|
||||
public Variant variant() {
|
||||
return variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pre-hash selection.
|
||||
*
|
||||
* @return pre-hash selection
|
||||
*/
|
||||
public PreHash preHash() {
|
||||
return preHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the explicit parameter constant override, if set.
|
||||
*
|
||||
* @return constant name, or {@code null} if automatic mapping is used
|
||||
*/
|
||||
public String explicitParamConstant() {
|
||||
return explicitParamConstant;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Builder for importing SLH-DSA private keys from encoded specifications.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPrivateKeyBuilder implements AsymmetricKeyBuilder<SlhDsaPrivateKeySpec> {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(SlhDsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(SlhDsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPublicKeySpec for public keys.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(SlhDsaPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG)
|
||||
: KeyFactory.getInstance(ALG, spec.providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Encoded representation of an SLH-DSA private key.
|
||||
*
|
||||
* <p>
|
||||
* {@code SlhDsaPrivateKeySpec} is an immutable value object that wraps a
|
||||
* PKCS#8-encoded SLH-DSA private key together with the JCA provider name that
|
||||
* should be used when importing the key.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>The private key material is stored as a defensive copy of the provided
|
||||
* PKCS#8 byte array.</li>
|
||||
* <li>Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under
|
||||
* {@code "pkcs8.b64"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider selection</h2>
|
||||
* <p>
|
||||
* The default provider is {@code "BC"} because SLH-DSA is registered by the
|
||||
* Bouncy Castle core provider (as opposed to the PQC-only provider).
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <ul>
|
||||
* <li>All byte arrays returned by this class are defensive copies.</li>
|
||||
* <li>This class performs no cryptographic operations.</li>
|
||||
* <li>Callers must protect serialized private key material at rest and in
|
||||
* transit.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
* <p>
|
||||
* Instances are immutable and therefore thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
|
||||
private final byte[] encodedPkcs8;
|
||||
private final String providerName;
|
||||
|
||||
/**
|
||||
* Creates a new specification using the default provider {@code "BC"}.
|
||||
*
|
||||
* @param encodedPkcs8 PKCS#8-encoded SLH-DSA private key bytes
|
||||
* @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null}
|
||||
*/
|
||||
public SlhDsaPrivateKeySpec(byte[] encodedPkcs8) {
|
||||
this(encodedPkcs8, "BC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification using the supplied provider.
|
||||
*
|
||||
* <p>
|
||||
* If {@code providerName} is {@code null}, the default provider {@code "BC"} is
|
||||
* used.
|
||||
* </p>
|
||||
*
|
||||
* @param encodedPkcs8 PKCS#8-encoded SLH-DSA private key bytes
|
||||
* @param providerName JCA provider name to use for import; may be {@code null}
|
||||
* @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null}
|
||||
*/
|
||||
public SlhDsaPrivateKeySpec(byte[] encodedPkcs8, String providerName) {
|
||||
if (encodedPkcs8 == null) {
|
||||
throw new IllegalArgumentException("encodedPkcs8 must not be null");
|
||||
}
|
||||
this.encodedPkcs8 = encodedPkcs8.clone();
|
||||
this.providerName = (providerName == null ? "BC" : providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the PKCS#8-encoded private key bytes.
|
||||
*
|
||||
* @return a copy of the encoded private key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedPkcs8.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name associated with this specification.
|
||||
*
|
||||
* @return provider name, never {@code null}
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given spec into a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* The encoded key is stored as Base64 without padding under
|
||||
* {@code "pkcs8.b64"}. The provider is stored under {@code "provider"}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec the private key specification to serialize
|
||||
* @return serialized representation
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(SlhDsaPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
|
||||
return PairSeq.of("type", "SLHDSA-PRIV", "pkcs8.b64", b64, "provider", spec.providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a {@link SlhDsaPrivateKeySpec} from a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* Expects key {@code "pkcs8.b64"} (required) and {@code "provider"} (optional;
|
||||
* defaults to {@code "BC"}).
|
||||
* </p>
|
||||
*
|
||||
* @param p serialized input
|
||||
* @return reconstructed private key specification
|
||||
* @throws IllegalArgumentException if {@code "pkcs8.b64"} is missing
|
||||
* @throws NullPointerException if {@code p} is {@code null}
|
||||
*/
|
||||
public static SlhDsaPrivateKeySpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
String prov = "BC";
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "pkcs8.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "provider" -> prov = v;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for SLH-DSA private key");
|
||||
}
|
||||
return new SlhDsaPrivateKeySpec(out, prov);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* Builder for importing SLH-DSA public keys from encoded specifications.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPublicKeyBuilder implements AsymmetricKeyBuilder<SlhDsaPublicKeySpec> {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(SlhDsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(SlhDsaPublicKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG)
|
||||
: KeyFactory.getInstance(ALG, spec.providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(SlhDsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use SlhDsaPrivateKeySpec for private keys.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
|
||||
/**
|
||||
* Encoded representation of an SLH-DSA public key.
|
||||
*
|
||||
* <p>
|
||||
* {@code SlhDsaPublicKeySpec} is an immutable value object that wraps an X.509
|
||||
* (SubjectPublicKeyInfo) encoded SLH-DSA public key together with the JCA
|
||||
* provider name that should be used when importing the key.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>The public key bytes are stored as a defensive copy of the provided X.509
|
||||
* byte array.</li>
|
||||
* <li>Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under
|
||||
* {@code "x509.b64"}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider selection</h2>
|
||||
* <p>
|
||||
* The default provider is {@code "BC"} because SLH-DSA is registered by the
|
||||
* Bouncy Castle core provider.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
* <p>
|
||||
* Instances are immutable and can be safely shared across threads.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaPublicKeySpec implements AlgorithmKeySpec {
|
||||
|
||||
private final byte[] encodedX509;
|
||||
private final String providerName;
|
||||
|
||||
/**
|
||||
* Creates a new specification using the default provider {@code "BC"}.
|
||||
*
|
||||
* @param encodedX509 X.509-encoded SLH-DSA public key bytes
|
||||
* @throws IllegalArgumentException if {@code encodedX509} is {@code null}
|
||||
*/
|
||||
public SlhDsaPublicKeySpec(byte[] encodedX509) {
|
||||
this(encodedX509, "BC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new specification using the supplied provider.
|
||||
*
|
||||
* <p>
|
||||
* If {@code providerName} is {@code null}, the default provider {@code "BC"} is
|
||||
* used.
|
||||
* </p>
|
||||
*
|
||||
* @param encodedX509 X.509-encoded SLH-DSA public key bytes
|
||||
* @param providerName JCA provider name to use for import; may be {@code null}
|
||||
* @throws IllegalArgumentException if {@code encodedX509} is {@code null}
|
||||
*/
|
||||
public SlhDsaPublicKeySpec(byte[] encodedX509, String providerName) {
|
||||
if (encodedX509 == null) {
|
||||
throw new IllegalArgumentException("encodedX509 must not be null");
|
||||
}
|
||||
this.encodedX509 = encodedX509.clone();
|
||||
this.providerName = (providerName == null ? "BC" : providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the X.509-encoded public key bytes.
|
||||
*
|
||||
* @return a copy of the encoded public key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedX509.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name associated with this specification.
|
||||
*
|
||||
* @return provider name, never {@code null}
|
||||
*/
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given spec into a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* The encoded key is stored as Base64 without padding under {@code "x509.b64"}.
|
||||
* The provider is stored under {@code "provider"}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec the public key specification to serialize
|
||||
* @return serialized representation
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(SlhDsaPublicKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedX509);
|
||||
return PairSeq.of("type", "SLHDSA-PUB", "x509.b64", b64, "provider", spec.providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a {@link SlhDsaPublicKeySpec} from a {@link PairSeq}.
|
||||
*
|
||||
* <p>
|
||||
* Expects key {@code "x509.b64"} (required) and {@code "provider"} (optional;
|
||||
* defaults to {@code "BC"}).
|
||||
* </p>
|
||||
*
|
||||
* @param p serialized input
|
||||
* @return reconstructed public key specification
|
||||
* @throws IllegalArgumentException if {@code "x509.b64"} is missing
|
||||
* @throws NullPointerException if {@code p} is {@code null}
|
||||
*/
|
||||
public static SlhDsaPublicKeySpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
String prov = "BC";
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "x509.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "provider" -> prov = v;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("x509.b64 missing for SLH-DSA public key");
|
||||
}
|
||||
return new SlhDsaPublicKeySpec(out, prov);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.bouncycastle.jcajce.interfaces.SLHDSAPublicKey;
|
||||
import org.bouncycastle.jcajce.spec.SLHDSAParameterSpec;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.alg.common.sig.GenericJcaSignatureContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
|
||||
/**
|
||||
* Streaming signature context for SLH-DSA (FIPS 205).
|
||||
*
|
||||
* <p>
|
||||
* {@code SlhDsaSignatureContext} adapts a JCA {@link Signature} engine for use
|
||||
* within the ZeroEcho streaming signature infrastructure. It supports both
|
||||
* signing and verification and delegates the low-level mechanics to
|
||||
* {@link GenericJcaSignatureContext}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Provider and algorithm</h2>
|
||||
* <ul>
|
||||
* <li>JCA algorithm: {@code "SLH-DSA"}.</li>
|
||||
* <li>Provider: {@code "BC"} (Bouncy Castle core provider).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Streaming contract</h2>
|
||||
* <ul>
|
||||
* <li><b>SIGN</b>: the wrapped stream emits the message body and appends a
|
||||
* detached signature trailer at EOF.</li>
|
||||
* <li><b>VERIFY</b>: the wrapped stream emits the body only; verification is
|
||||
* performed at EOF against a caller-supplied expected tag.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Security</h2>
|
||||
* <p>
|
||||
* This class never logs secrets, key material, plaintext, or signature bytes.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class SlhDsaSignatureContext implements SignatureContext {
|
||||
|
||||
private static final String ALG = "SLH-DSA";
|
||||
private static final String PROVIDER = "BC";
|
||||
|
||||
private static final Pattern PARAM_PATTERN = Pattern
|
||||
.compile("^slh-dsa-(sha2|shake)-(128|192|256)([fs])(?:-with-[a-z0-9]+)?$");
|
||||
|
||||
private final GenericJcaSignatureContext delegate;
|
||||
|
||||
/**
|
||||
* Creates a signing context bound to a private key.
|
||||
*
|
||||
* <p>
|
||||
* The produced signature length is resolved by probing the JCA engine via
|
||||
* {@link GenericJcaSignatureContext.SignLengthResolver#probeWith(String, String)}.
|
||||
* </p>
|
||||
*
|
||||
* @param algorithm the parent algorithm instance; must not be {@code null}
|
||||
* @param privateKey SLH-DSA private key; must not be {@code null}
|
||||
* @throws GeneralSecurityException if the JCA signature engine cannot be
|
||||
* initialized
|
||||
*/
|
||||
public SlhDsaSignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey)
|
||||
throws GeneralSecurityException {
|
||||
this.delegate = new GenericJcaSignatureContext(algorithm, privateKey,
|
||||
GenericJcaSignatureContext.jcaFactory(ALG, PROVIDER),
|
||||
GenericJcaSignatureContext.SignLengthResolver.probeWith(ALG, PROVIDER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verification context bound to a public key.
|
||||
*
|
||||
* <p>
|
||||
* The expected signature length is derived from the public key parameter set
|
||||
* via {@link SLHDSAParameterSpec#getName()} and the canonical SLH-DSA sizes.
|
||||
* </p>
|
||||
*
|
||||
* @param algorithm the parent algorithm instance; must not be {@code null}
|
||||
* @param publicKey SLH-DSA public key; must not be {@code null}
|
||||
* @throws GeneralSecurityException if the key is invalid or the parameter set
|
||||
* is unsupported
|
||||
*/
|
||||
public SlhDsaSignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey)
|
||||
throws GeneralSecurityException {
|
||||
this.delegate = new GenericJcaSignatureContext(algorithm, publicKey,
|
||||
GenericJcaSignatureContext.jcaFactory(ALG, PROVIDER), SlhDsaSignatureContext::sigLenFromPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the canonical SLH-DSA signature length (bytes) from a public key.
|
||||
*
|
||||
* <p>
|
||||
* The Bouncy Castle public key exposes an {@link SLHDSAParameterSpec} whose
|
||||
* {@linkplain SLHDSAParameterSpec#getName() name} encodes the family
|
||||
* (SHA2/SHAKE), security level (128/192/256) and variant (s/f). The resolver
|
||||
* normalizes the returned name to lowercase and replaces underscores with
|
||||
* hyphens to tolerate provider naming differences.
|
||||
* </p>
|
||||
*
|
||||
* @param pk SLH-DSA public key
|
||||
* @return signature length in bytes for the key's parameter set
|
||||
* @throws GeneralSecurityException if the key type or parameter specification
|
||||
* is missing or unrecognized
|
||||
*/
|
||||
private static int sigLenFromPublicKey(PublicKey pk) throws GeneralSecurityException {
|
||||
if (!(pk instanceof SLHDSAPublicKey slhPk)) {
|
||||
throw new GeneralSecurityException("Expected a BouncyCastle SLH-DSA public key (BC)");
|
||||
}
|
||||
SLHDSAParameterSpec ps = slhPk.getParameterSpec();
|
||||
if (ps == null) {
|
||||
throw new GeneralSecurityException("Missing SLH-DSA parameter spec on public key");
|
||||
}
|
||||
String name = ps.getName();
|
||||
if (name == null || name.isEmpty()) {
|
||||
throw new GeneralSecurityException("Unknown SLH-DSA parameter (no name)");
|
||||
}
|
||||
|
||||
String normalized = name.toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
|
||||
Matcher m = PARAM_PATTERN.matcher(normalized);
|
||||
if (!m.matches()) {
|
||||
throw new GeneralSecurityException("Cannot parse SLH-DSA parameter from: " + name);
|
||||
}
|
||||
|
||||
int level = Integer.parseInt(m.group(2));
|
||||
char var = m.group(3).charAt(0);
|
||||
boolean isSmall = var == 's';
|
||||
|
||||
return switch (level) {
|
||||
case 128 -> isSmall ? 7_856 : 17_088;
|
||||
case 192 -> isSmall ? 16_224 : 35_664;
|
||||
case 256 -> isSmall ? 29_792 : 49_856;
|
||||
default -> throw new GeneralSecurityException("Unsupported SLH-DSA level: " + level);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent {@link CryptoAlgorithm} associated with this context.
|
||||
*
|
||||
* @return the algorithm instance
|
||||
*/
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return delegate.algorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key bound to this context.
|
||||
*
|
||||
* @return signing {@link PrivateKey} or verification {@link PublicKey}
|
||||
*/
|
||||
@Override
|
||||
public java.security.Key key() {
|
||||
return delegate.key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this context and releases any underlying resources.
|
||||
*
|
||||
* <p>
|
||||
* Once closed, the context must not be reused.
|
||||
* </p>
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an input stream such that bytes read from the returned stream update
|
||||
* the underlying signature engine.
|
||||
*
|
||||
* <p>
|
||||
* In SIGN mode, the wrapper appends the signature trailer at EOF. In VERIFY
|
||||
* mode, the wrapper compares the computed signature at EOF against the expected
|
||||
* tag configured via {@link #setExpectedTag(byte[])}.
|
||||
* </p>
|
||||
*
|
||||
* @param upstream input stream providing message bytes; must not be
|
||||
* {@code null}
|
||||
* @return wrapped stream that performs signing or verification as bytes are
|
||||
* read
|
||||
* @throws IOException if wrapping fails
|
||||
*/
|
||||
@Override
|
||||
public InputStream wrap(InputStream upstream) throws IOException {
|
||||
return delegate.wrap(upstream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature (tag) length in bytes for the parameter set in use.
|
||||
*
|
||||
* @return signature length in bytes
|
||||
*/
|
||||
@Override
|
||||
public int tagLength() {
|
||||
return delegate.tagLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies the expected signature (tag) for VERIFY mode.
|
||||
*
|
||||
* <p>
|
||||
* Implementations may defensively copy the provided array. Callers should treat
|
||||
* this value as sensitive and avoid logging or persisting it unless explicitly
|
||||
* required by the application.
|
||||
* </p>
|
||||
*
|
||||
* @param expected expected signature bytes; must not be {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void setExpectedTag(byte[] expected) {
|
||||
delegate.setExpectedTag(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the verification approach used at EOF to compare the computed and
|
||||
* expected signatures.
|
||||
*
|
||||
* @param strategy verification predicate; must not be {@code null}
|
||||
*/
|
||||
@Override
|
||||
public void setVerificationApproach(VerificationBiPredicate<Signature> strategy) {
|
||||
delegate.setVerificationApproach(strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the verification predicate core that can be used to select a
|
||||
* particular mismatch handling strategy (e.g., throw on mismatch).
|
||||
*
|
||||
* @return the verification predicate core
|
||||
*/
|
||||
@Override
|
||||
public VerificationBiPredicate<Signature> getVerificationCore() {
|
||||
return delegate.getVerificationCore();
|
||||
}
|
||||
}
|
||||
121
lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java
Normal file
121
lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* SLH-DSA (FIPS 205) signature algorithm binding.
|
||||
*
|
||||
* <p>
|
||||
* This package provides a ZeroEcho binding for <em>SLH-DSA</em>, the
|
||||
* NIST-standardized profile of the stateless hash-based signature scheme
|
||||
* SPHINCS+. While SLH-DSA is derived from SPHINCS+, it is exposed here as a
|
||||
* distinct algorithm identity with a restricted, standards-compliant parameter
|
||||
* space.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Algorithm identity</h2>
|
||||
* <ul>
|
||||
* <li>JCA algorithm name: {@code "SLH-DSA"}</li>
|
||||
* <li>ZeroEcho algorithm id: {@code "SLHDSA"}</li>
|
||||
* <li>Provider: Bouncy Castle core provider ({@code "BC"})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Supported parameter space</h2>
|
||||
* <p>
|
||||
* The supported parameters correspond exactly to the SLH-DSA profiles defined
|
||||
* by FIPS 205:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Hash families: SHA2 and SHAKE</li>
|
||||
* <li>Security levels: 128, 192, 256 bits (NIST levels L1, L3, L5)</li>
|
||||
* <li>Variants: {@code FAST} (larger, faster signatures) and {@code SMALL}
|
||||
* (smaller, slower signatures)</li>
|
||||
* <li>Optional pre-hash variants as defined by the standard and exposed by the
|
||||
* provider</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Non-standard SPHINCS+ variants (e.g. Haraka, simple/robust modes) are
|
||||
* intentionally excluded from this package.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Key management</h2>
|
||||
* <ul>
|
||||
* <li>Key generation is configured via
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaKeyGenSpec} and performed by
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaKeyGenBuilder}.</li>
|
||||
* <li>Encoded public and private keys are represented by
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec} and
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec}.</li>
|
||||
* <li>All key specifications are immutable and use defensive copies.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Streaming signature model</h2>
|
||||
* <p>
|
||||
* Signatures are processed through the ZeroEcho streaming signature
|
||||
* infrastructure:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>In {@link zeroecho.core.KeyUsage#SIGN} mode, the signature is appended as
|
||||
* a fixed-length trailer to the output stream.</li>
|
||||
* <li>In {@link zeroecho.core.KeyUsage#VERIFY} mode, the wrapped stream emits
|
||||
* only the message body; verification is performed at end-of-stream against a
|
||||
* caller-supplied expected signature.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The canonical signature length is derived from the public key parameters by
|
||||
* {@link zeroecho.core.alg.slhdsa.SlhDsaSignatureContext} and matches the
|
||||
* standard SLH-DSA signature sizes defined by FIPS 205.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
* <ul>
|
||||
* <li>No sensitive material (private keys, signatures, message contents) is
|
||||
* logged or exposed by this package.</li>
|
||||
* <li>All byte arrays returned to callers are defensive copies.</li>
|
||||
* <li>Verification failures are surfaced exclusively via the configured
|
||||
* verification strategy.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Relationship to SPHINCS+</h2>
|
||||
* <p>
|
||||
* The {@code slhdsa} package represents the standardized SLH-DSA profile only.
|
||||
* A separate {@code sphincsplus} package may expose the full SPHINCS+ design
|
||||
* space and experimental variants. No API-level equivalence between the two
|
||||
* packages is assumed.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
@@ -19,5 +19,6 @@ zeroecho.core.alg.ntruprime.NtrulPrimeAlgorithm
|
||||
zeroecho.core.alg.ntruprime.SntruPrimeAlgorithm
|
||||
zeroecho.core.alg.rsa.RsaAlgorithm
|
||||
zeroecho.core.alg.saber.SaberAlgorithm
|
||||
zeroecho.core.alg.slhdsa.SlhDsaAlgorithm
|
||||
zeroecho.core.alg.sphincsplus.SphincsPlusAlgorithm
|
||||
zeroecho.core.alg.xdh.XdhAlgorithm
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.slhdsa;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* Large-data streaming test for SLH-DSA integration.
|
||||
*
|
||||
* <p>
|
||||
* Signature length is determined via {@link SlhDsaSignatureContext} created for
|
||||
* verification (public key). If the verification context is not an instance of
|
||||
* {@link SlhDsaSignatureContext}, the test fails.
|
||||
* </p>
|
||||
*/
|
||||
public final class SlhDsaLargeDataTest {
|
||||
|
||||
private static final String INDENT = "...";
|
||||
private static final int MAX_HEX_BYTES = 32;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
|
||||
// etc.)
|
||||
try {
|
||||
BouncyCastleActivator.init();
|
||||
} catch (Throwable ignore) {
|
||||
// keep tests runnable without BC if not present
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void slhdsa_complete_suite_streaming_sign_verify_large_data() throws Exception {
|
||||
String testName = "slhdsa_complete_suite_streaming_sign_verify_large_data";
|
||||
System.out.println(testName);
|
||||
|
||||
if (!CryptoAlgorithms.available().contains("SLH-DSA")) {
|
||||
System.out.println(INDENT + " *** SKIP *** SLH-DSA not registered");
|
||||
System.out.println(INDENT + "ok");
|
||||
return;
|
||||
}
|
||||
|
||||
int payloadLen = 48 * 1024 + 123;
|
||||
byte[] msg = randomBytes(payloadLen);
|
||||
|
||||
System.out.println(INDENT + " msg.len=" + msg.length);
|
||||
System.out.println(INDENT + " msg.hex=" + hexTruncated(msg, MAX_HEX_BYTES));
|
||||
|
||||
// Complete suite: 128/192/256 x FAST/SMALL, for both SHA2 and SHAKE, no
|
||||
// pre-hash
|
||||
runSuiteForHash(msg, SlhDsaKeyGenSpec.Hash.SHA2);
|
||||
runSuiteForHash(msg, SlhDsaKeyGenSpec.Hash.SHAKE);
|
||||
|
||||
System.out.println(INDENT + "ok");
|
||||
}
|
||||
|
||||
private static void runSuiteForHash(byte[] msg, SlhDsaKeyGenSpec.Hash hash) throws Exception {
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L1_128, SlhDsaKeyGenSpec.Variant.FAST);
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L1_128, SlhDsaKeyGenSpec.Variant.SMALL);
|
||||
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L3_192, SlhDsaKeyGenSpec.Variant.FAST);
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L3_192, SlhDsaKeyGenSpec.Variant.SMALL);
|
||||
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L5_256, SlhDsaKeyGenSpec.Variant.FAST);
|
||||
runCase(msg, hash, SlhDsaKeyGenSpec.Security.L5_256, SlhDsaKeyGenSpec.Variant.SMALL);
|
||||
}
|
||||
|
||||
private static void runCase(byte[] msg, SlhDsaKeyGenSpec.Hash hash, SlhDsaKeyGenSpec.Security sec,
|
||||
SlhDsaKeyGenSpec.Variant variant) throws Exception {
|
||||
|
||||
SlhDsaKeyGenSpec spec = SlhDsaKeyGenSpec.of("BC", hash, sec, variant, SlhDsaKeyGenSpec.PreHash.NONE);
|
||||
|
||||
String caseId = "SLH-DSA " + hash.name() + " " + sec.name() + " " + variant.name();
|
||||
System.out.println(INDENT + " case=" + safeText(caseId));
|
||||
|
||||
KeyPair kp = CryptoAlgorithms.keyPair("SLH-DSA", spec);
|
||||
|
||||
// Create verifier FIRST to obtain tag length via
|
||||
// SlhDsaSignatureContext.sigLenFromPublicKey.
|
||||
SignatureContext verifierCtx = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
int expectedSigLen = verifierCtx.tagLength();
|
||||
System.out.println(INDENT + " expectedSigLen=" + expectedSigLen);
|
||||
|
||||
// Now sign and strip trailer using the expected length from verifier (not from
|
||||
// signer).
|
||||
SignatureContext signer = CryptoAlgorithms.create("SLH-DSA", KeyUsage.SIGN, kp.getPrivate());
|
||||
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
byte[] passthrough;
|
||||
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), expectedSigLen,
|
||||
8192) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) throws IOException {
|
||||
sigHolder[0] = (tail == null ? null : Arrays.copyOf(tail, tail.length));
|
||||
}
|
||||
}) {
|
||||
passthrough = readAll(in);
|
||||
} finally {
|
||||
try {
|
||||
signer.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(msg, passthrough, "SIGN passthrough mismatch");
|
||||
|
||||
byte[] signature = sigHolder[0];
|
||||
assertNotNull(signature, "signature trailer missing");
|
||||
System.out.println(INDENT + " signature.len=" + signature.length);
|
||||
System.out.println(INDENT + " signature.hex=" + hexTruncated(signature, MAX_HEX_BYTES));
|
||||
|
||||
if (signature.length != expectedSigLen) {
|
||||
try {
|
||||
verifierCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
throw new AssertionError(
|
||||
"Signature length mismatch: got=" + signature.length + " expected=" + expectedSigLen);
|
||||
}
|
||||
|
||||
// Verify OK with expected signature (use already-created SLH verifier).
|
||||
verifierCtx.setExpectedTag(Arrays.copyOf(signature, signature.length));
|
||||
byte[] verifyOut;
|
||||
try (InputStream verIn = verifierCtx.wrap(new ByteArrayInputStream(msg))) {
|
||||
verifyOut = readAll(verIn);
|
||||
} finally {
|
||||
try {
|
||||
verifierCtx.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
assertArrayEquals(msg, verifyOut, "VERIFY passthrough mismatch");
|
||||
System.out.println(INDENT + " verify=accepted");
|
||||
|
||||
// Negative: bit flip and expect rejection (new verifier instance, must again be
|
||||
// our context).
|
||||
byte[] badSig = Arrays.copyOf(signature, signature.length);
|
||||
badSig[0] = (byte) (badSig[0] ^ 0x01);
|
||||
|
||||
SignatureContext badVerifier = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic());
|
||||
|
||||
try {
|
||||
badVerifier.setExpectedTag(badSig);
|
||||
badVerifier.setVerificationApproach(badVerifier.getVerificationCore().getThrowOnMismatch());
|
||||
try (InputStream verBad = badVerifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
readAll(verBad);
|
||||
}
|
||||
throw new AssertionError("Expected verification failure for mismatched signature");
|
||||
} catch (Exception expected) {
|
||||
System.out.println(INDENT + " verify=reject (mismatch)");
|
||||
} finally {
|
||||
try {
|
||||
badVerifier.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int len) {
|
||||
byte[] data = new byte[len];
|
||||
SecureRandom rnd = new SecureRandom();
|
||||
rnd.nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static byte[] readAll(InputStream in) throws IOException {
|
||||
try (InputStream src = in; ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = src.read(buf)) != -1) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeText(String s) {
|
||||
if (s == null) {
|
||||
return "null";
|
||||
}
|
||||
if (s.length() <= 30) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, 30) + "...";
|
||||
}
|
||||
|
||||
private static String hexTruncated(byte[] data, int maxBytes) {
|
||||
if (data == null) {
|
||||
return "null";
|
||||
}
|
||||
int n = Math.min(data.length, maxBytes);
|
||||
StringBuilder sb = new StringBuilder(n * 2 + 3);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int v = data[i] & 0xFF;
|
||||
sb.append(HEX[(v >>> 4) & 0x0F]);
|
||||
sb.append(HEX[v & 0x0F]);
|
||||
}
|
||||
if (data.length > maxBytes) {
|
||||
sb.append("...");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final char[] HEX = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
|
||||
'e', 'f' };
|
||||
}
|
||||
Reference in New Issue
Block a user