From 8f228c7ada2beb9e2c719b7bd4c7a11ae6ec557f Mon Sep 17 00:00:00 2001 From: Leo Galambos Date: Thu, 25 Dec 2025 01:54:24 +0100 Subject: [PATCH] feat: SLH-DSA (FIPS 205) signature algorithm added Signed-off-by: Leo Galambos --- .../core/alg/slhdsa/SlhDsaAlgorithm.java | 106 +++++++ .../core/alg/slhdsa/SlhDsaKeyGenBuilder.java | 168 +++++++++++ .../core/alg/slhdsa/SlhDsaKeyGenSpec.java | 249 +++++++++++++++ .../alg/slhdsa/SlhDsaPrivateKeyBuilder.java | 71 +++++ .../core/alg/slhdsa/SlhDsaPrivateKeySpec.java | 182 +++++++++++ .../alg/slhdsa/SlhDsaPublicKeyBuilder.java | 71 +++++ .../core/alg/slhdsa/SlhDsaPublicKeySpec.java | 174 +++++++++++ .../alg/slhdsa/SlhDsaSignatureContext.java | 283 ++++++++++++++++++ .../core/alg/slhdsa/package-info.java | 121 ++++++++ .../services/zeroecho.core.CryptoAlgorithm | 1 + .../core/alg/slhdsa/SlhDsaLargeDataTest.java | 258 ++++++++++++++++ 11 files changed, 1684 insertions(+) create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeyBuilder.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeySpec.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaSignatureContext.java create mode 100644 lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java create mode 100644 lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java new file mode 100644 index 0000000..f435638 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java @@ -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. + * + *

+ * 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. + *

+ * + *

+ * This algorithm registers two roles: + *

+ * + * + *

+ * Both roles are configured with {@link VoidSpec}, as SLH-DSA requires no + * runtime context parameters beyond the key material. + *

+ * + * @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); + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java new file mode 100644 index 0000000..8708642 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java @@ -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. + * + *

+ * 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. + *

+ * + * @since 1.0 + */ +public final class SlhDsaKeyGenBuilder implements AsymmetricKeyBuilder { + + 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; + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java new file mode 100644 index 0000000..55bd9ca --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenSpec.java @@ -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). + * + *

+ * 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. + *

+ * + *

+ * This spec intentionally restricts the available choices to SLH-DSA parameters + * (i.e., no Haraka and no SPHINCS+ "simple/robust" split). + *

+ * + * @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. + * + *

+ * {@code FAST} variants are optimized for speed (larger signatures). + * {@code SMALL} variants reduce signature size at some performance cost. + *

+ */ + public enum Variant { + /** Larger, faster signatures. */ + FAST, + /** Smaller, slower signatures. */ + SMALL + } + + /** + * Optional pre-hash variant selection. + * + *

+ * Bouncy Castle exposes "with hash" variants as distinct parameter specs, for + * example {@code slh_dsa_sha2_128s_with_sha256}. + * :contentReference[oaicite:1]{index=1} + *

+ * + *

+ * Valid combinations depend on the hash family and security strength: + *

+ *
    + *
  • SHA2-128 uses SHA-256
  • + *
  • SHA2-192/256 use SHA-512
  • + *
  • SHAKE-128 uses SHAKE128
  • + *
  • SHAKE-192/256 use SHAKE256
  • + *
+ */ + 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. + * + *

+ * This bypasses automatic mapping. The name must match a static field in + * {@code org.bouncycastle.jcajce.spec.SLHDSAParameterSpec}. + * :contentReference[oaicite:2]{index=2} + *

+ * + * @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; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeyBuilder.java new file mode 100644 index 0000000..802c4e0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeyBuilder.java @@ -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 { + + 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())); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java new file mode 100644 index 0000000..aebb395 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java @@ -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. + * + *

+ * {@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. + *

+ * + *

Encoding

+ *
    + *
  • The private key material is stored as a defensive copy of the provided + * PKCS#8 byte array.
  • + *
  • Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under + * {@code "pkcs8.b64"}.
  • + *
+ * + *

Provider selection

+ *

+ * The default provider is {@code "BC"} because SLH-DSA is registered by the + * Bouncy Castle core provider (as opposed to the PQC-only provider). + *

+ * + *

Security considerations

+ *
    + *
  • All byte arrays returned by this class are defensive copies.
  • + *
  • This class performs no cryptographic operations.
  • + *
  • Callers must protect serialized private key material at rest and in + * transit.
  • + *
+ * + *

Thread-safety

+ *

+ * Instances are immutable and therefore thread-safe. + *

+ * + * @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. + * + *

+ * If {@code providerName} is {@code null}, the default provider {@code "BC"} is + * used. + *

+ * + * @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}. + * + *

+ * The encoded key is stored as Base64 without padding under + * {@code "pkcs8.b64"}. The provider is stored under {@code "provider"}. + *

+ * + * @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}. + * + *

+ * Expects key {@code "pkcs8.b64"} (required) and {@code "provider"} (optional; + * defaults to {@code "BC"}). + *

+ * + * @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); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java new file mode 100644 index 0000000..b1ae300 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java @@ -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 { + + 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."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeySpec.java new file mode 100644 index 0000000..e98bb67 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeySpec.java @@ -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. + * + *

+ * {@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. + *

+ * + *

Encoding

+ *
    + *
  • The public key bytes are stored as a defensive copy of the provided X.509 + * byte array.
  • + *
  • Marshalling/unmarshalling uses {@link PairSeq} with Base64 encoding under + * {@code "x509.b64"}.
  • + *
+ * + *

Provider selection

+ *

+ * The default provider is {@code "BC"} because SLH-DSA is registered by the + * Bouncy Castle core provider. + *

+ * + *

Thread-safety

+ *

+ * Instances are immutable and can be safely shared across threads. + *

+ * + * @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. + * + *

+ * If {@code providerName} is {@code null}, the default provider {@code "BC"} is + * used. + *

+ * + * @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}. + * + *

+ * The encoded key is stored as Base64 without padding under {@code "x509.b64"}. + * The provider is stored under {@code "provider"}. + *

+ * + * @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}. + * + *

+ * Expects key {@code "x509.b64"} (required) and {@code "provider"} (optional; + * defaults to {@code "BC"}). + *

+ * + * @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); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaSignatureContext.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaSignatureContext.java new file mode 100644 index 0000000..2bfe316 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaSignatureContext.java @@ -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). + * + *

+ * {@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}. + *

+ * + *

Provider and algorithm

+ *
    + *
  • JCA algorithm: {@code "SLH-DSA"}.
  • + *
  • Provider: {@code "BC"} (Bouncy Castle core provider).
  • + *
+ * + *

Streaming contract

+ *
    + *
  • SIGN: the wrapped stream emits the message body and appends a + * detached signature trailer at EOF.
  • + *
  • VERIFY: the wrapped stream emits the body only; verification is + * performed at EOF against a caller-supplied expected tag.
  • + *
+ * + *

Security

+ *

+ * This class never logs secrets, key material, plaintext, or signature bytes. + *

+ * + * @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. + * + *

+ * The produced signature length is resolved by probing the JCA engine via + * {@link GenericJcaSignatureContext.SignLengthResolver#probeWith(String, String)}. + *

+ * + * @param algorithm the parent algorithm instance; must not be {@code null} + * @param privateKey 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. + * + *

+ * The expected signature length is derived from the public key parameter set + * via {@link SLHDSAParameterSpec#getName()} and the canonical SLH-DSA sizes. + *

+ * + * @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. + * + *

+ * 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. + *

+ * + * @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. + * + *

+ * Once closed, the context must not be reused. + *

+ */ + @Override + public void close() { + delegate.close(); + } + + /** + * Wraps an input stream such that bytes read from the returned stream update + * the underlying signature engine. + * + *

+ * 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[])}. + *

+ * + * @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. + * + *

+ * 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. + *

+ * + * @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 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 getVerificationCore() { + return delegate.getVerificationCore(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java new file mode 100644 index 0000000..bf646ea --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java @@ -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. + * + *

+ * This package provides a ZeroEcho binding for SLH-DSA, 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. + *

+ * + *

Algorithm identity

+ *
    + *
  • JCA algorithm name: {@code "SLH-DSA"}
  • + *
  • ZeroEcho algorithm id: {@code "SLHDSA"}
  • + *
  • Provider: Bouncy Castle core provider ({@code "BC"})
  • + *
+ * + *

Supported parameter space

+ *

+ * The supported parameters correspond exactly to the SLH-DSA profiles defined + * by FIPS 205: + *

+ *
    + *
  • Hash families: SHA2 and SHAKE
  • + *
  • Security levels: 128, 192, 256 bits (NIST levels L1, L3, L5)
  • + *
  • Variants: {@code FAST} (larger, faster signatures) and {@code SMALL} + * (smaller, slower signatures)
  • + *
  • Optional pre-hash variants as defined by the standard and exposed by the + * provider
  • + *
+ * + *

+ * Non-standard SPHINCS+ variants (e.g. Haraka, simple/robust modes) are + * intentionally excluded from this package. + *

+ * + *

Key management

+ *
    + *
  • Key generation is configured via + * {@link zeroecho.core.alg.slhdsa.SlhDsaKeyGenSpec} and performed by + * {@link zeroecho.core.alg.slhdsa.SlhDsaKeyGenBuilder}.
  • + *
  • Encoded public and private keys are represented by + * {@link zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec} and + * {@link zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec}.
  • + *
  • All key specifications are immutable and use defensive copies.
  • + *
+ * + *

Streaming signature model

+ *

+ * Signatures are processed through the ZeroEcho streaming signature + * infrastructure: + *

+ *
    + *
  • In {@link zeroecho.core.KeyUsage#SIGN} mode, the signature is appended as + * a fixed-length trailer to the output stream.
  • + *
  • 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.
  • + *
+ * + *

+ * 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. + *

+ * + *

Security considerations

+ *
    + *
  • No sensitive material (private keys, signatures, message contents) is + * logged or exposed by this package.
  • + *
  • All byte arrays returned to callers are defensive copies.
  • + *
  • Verification failures are surfaced exclusively via the configured + * verification strategy.
  • + *
+ * + *

Relationship to SPHINCS+

+ *

+ * 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. + *

+ * + * @since 1.0 + */ +package zeroecho.core.alg.slhdsa; diff --git a/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm b/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm index 05527b9..02c2bb6 100644 --- a/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm +++ b/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm @@ -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 diff --git a/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java new file mode 100644 index 0000000..7a75a70 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java @@ -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. + * + *

+ * 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. + *

+ */ +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' }; +}