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.ntruprime.SntruPrimeAlgorithm
|
||||||
zeroecho.core.alg.rsa.RsaAlgorithm
|
zeroecho.core.alg.rsa.RsaAlgorithm
|
||||||
zeroecho.core.alg.saber.SaberAlgorithm
|
zeroecho.core.alg.saber.SaberAlgorithm
|
||||||
|
zeroecho.core.alg.slhdsa.SlhDsaAlgorithm
|
||||||
zeroecho.core.alg.sphincsplus.SphincsPlusAlgorithm
|
zeroecho.core.alg.sphincsplus.SphincsPlusAlgorithm
|
||||||
zeroecho.core.alg.xdh.XdhAlgorithm
|
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