Verify-only: converts between {@link X509EncodedKeySpec} and + * {@link WolfCryptLmsPublicKey} public keys. Both RFC 9708 and RFC 8708 X.509 + * SubjectPublicKeyInfo forms are accepted on input. Private keys are not + * supported (matching the JDK SUN provider). {@code generatePrivate} throws + * {@link InvalidKeySpecException}.
+ */ +public class WolfCryptLmsKeyFactory extends KeyFactorySpi { + + /** + * Create a new wolfJCE LMS KeyFactory. + */ + public WolfCryptLmsKeyFactory() { + log("created new LMS KeyFactory"); + } + + private void log(String msg) { + WolfCryptDebug.log(getClass(), WolfCryptDebug.INFO, + () -> "[LMS KeyFactory] " + msg); + } + + /** + * Private keys are not supported: wolfJCE provides verify-only LMS/HSS. + */ + @Override + protected PrivateKey engineGeneratePrivate(KeySpec keySpec) + throws InvalidKeySpecException { + + throw new InvalidKeySpecException( + "LMS/HSS private keys are not supported (verify-only)"); + } + + @Override + protected PublicKey engineGeneratePublic(KeySpec keySpec) + throws InvalidKeySpecException { + + byte[] encoded; + + if (keySpec == null) { + throw new InvalidKeySpecException("KeySpec cannot be null"); + } + + if (!(keySpec instanceof X509EncodedKeySpec)) { + throw new InvalidKeySpecException( + "Unsupported KeySpec type for LMS public key: " + + keySpec.getClass().getName() + + " (expected X509EncodedKeySpec)"); + } + + encoded = ((X509EncodedKeySpec) keySpec).getEncoded(); + if (encoded == null || encoded.length == 0) { + throw new InvalidKeySpecException("X509EncodedKeySpec is empty"); + } + + try { + return new WolfCryptLmsPublicKey(encoded); + } + catch (IllegalArgumentException e) { + throw new InvalidKeySpecException( + "Invalid LMS X.509 SPKI DER: " + e.getMessage(), e); + } + } + + @Override + protected{@link #getAlgorithm()} returns the JDK standard name {@code "HSS/LMS"}; + * the alias {@code "LMS"} is also registered. {@link #getEncoded()} returns the + * X.509 SubjectPublicKeyInfo DER (RFC 9708 unwrapped form). The DER is + * validated, and its LMS/HSS parameter set derived, by importing through + * wolfCrypt at construction.
+ */ +public class WolfCryptLmsPublicKey implements PublicKey, Destroyable { + + private static final long serialVersionUID = 1L; + + /** X.509 SubjectPublicKeyInfo DER. */ + private byte[] encoded = null; + + /** Cached raw HSS/LMS public key (the BIT STRING contents) so verify-init + * does not re-parse the SPKI. Transient: recomputed on deserialization. */ + private transient byte[] rawPublicKey = null; + + /** Number of HSS levels (1 for single-tree LMS), from the public key. */ + private final int levels; + /** Per-level Merkle tree height, from the public key. */ + private final int height; + /** LM-OTS Winternitz parameter, from the public key. */ + private final int winternitz; + /** Hash-family selector ({@code Lms.LMS_*}), from the public key. */ + private final int hashType; + + /** Track if object has been destroyed. */ + private boolean destroyed = false; + + /** Lock around destroyed flag and encoded buffer. Not final because it is + * reinitialized after deserialization. */ + private transient Object stateLock = new Object(); + + /** + * Create from an X.509 SubjectPublicKeyInfo DER. + * + * @param x509Der X.509 SubjectPublicKeyInfo DER (RFC 9708 or RFC 8708 form) + * + * @throws IllegalArgumentException if the DER is malformed or not a + * recognized LMS/HSS SubjectPublicKeyInfo, or if LMS/HSS is not + * compiled into native wolfCrypt + */ + public WolfCryptLmsPublicKey(byte[] x509Der) + throws IllegalArgumentException { + + byte[] rawPub; + Lms key = null; + + if (x509Der == null || x509Der.length == 0) { + throw new IllegalArgumentException( + "Encoded key data cannot be null or empty"); + } + + /* Extract the raw HSS public key (accepts both SPKI body forms) and + * validate it by importing through wolfCrypt, which also derives the + * parameter set. */ + rawPub = WolfCryptLmsUtil.parsePublicKeyDer(x509Der); + + try { + key = new Lms(); + key.importPublicRaw(rawPub); + this.levels = key.getLevels(); + this.height = key.getHeight(); + this.winternitz = key.getWinternitz(); + this.hashType = key.getHashType(); + } + catch (WolfCryptException e) { + if (e.getError() == WolfCryptError.NOT_COMPILED_IN) { + throw new IllegalArgumentException( + "LMS/HSS is not compiled into native wolfCrypt", e); + } + throw new IllegalArgumentException( + "Not a valid LMS/HSS X.509 SPKI DER: " + e.getMessage(), e); + } + finally { + if (key != null) { + key.releaseNativeStruct(); + } + } + + /* Normalize to the canonical RFC 9708 unwrapped SubjectPublicKeyInfo, + * so getEncoded() and equals() are stable across both input + * encodings. */ + this.encoded = WolfCryptLmsUtil.encodePublicKeyDer(rawPub); + this.rawPublicKey = rawPub; + } + + /** + * Get the raw HSS/LMS public key bytes for use with the native verifier. + * + * @return raw public key bytes, or null if destroyed + */ + byte[] getRawPublicKey() { + synchronized (stateLock) { + if (destroyed || rawPublicKey == null) { + return null; + } + return rawPublicKey.clone(); + } + } + + /** + * @return {@code "HSS/LMS"}, the JDK standard algorithm name (the alias + * {@code "LMS"} is also registered by this provider) + */ + @Override + public String getAlgorithm() { + return "HSS/LMS"; + } + + /** + * @return {@code "X.509"} + */ + @Override + public String getFormat() { + return "X.509"; + } + + /** + * @return X.509 SubjectPublicKeyInfo DER, or null if destroyed + */ + @Override + public byte[] getEncoded() { + synchronized (stateLock) { + if (destroyed) { + return null; + } + return encoded.clone(); + } + } + + /** + * Get the number of HSS levels in this key's parameter set. + * + * @return number of HSS levels (1 for single-tree LMS) + */ + public int getLevels() { + return this.levels; + } + + /** + * Destroy this key by zeroing the encoded buffer. + */ + @Override + public void destroy() { + synchronized (stateLock) { + if (!destroyed) { + if (encoded != null) { + Arrays.fill(encoded, (byte) 0); + } + if (rawPublicKey != null) { + Arrays.fill(rawPublicKey, (byte) 0); + rawPublicKey = null; + } + destroyed = true; + } + } + } + + /** + * @return true if destroyed, false otherwise + */ + @Override + public boolean isDestroyed() { + synchronized (stateLock) { + return destroyed; + } + } + + /** + * @return hash code over the encoded key, or 0 if destroyed + */ + @Override + public int hashCode() { + synchronized (stateLock) { + if (destroyed) { + return 0; + } + return Arrays.hashCode(encoded); + } + } + + /** + * Equality based on algorithm name and X.509 encoding. The algorithm name + * may be either {@code "LMS"} or the JDK standard name {@code "HSS/LMS"} + * (both registered by this provider), compared case-insensitively, so a + * byte-identical key from another provider reporting either name compares + * equal. The parameter set is carried in the encoded public key, so byte + * equality of the encoding implies the same parameter set. + * + * @param obj object to compare + * + * @return true if obj is an LMS/HSS PublicKey with the same X.509 encoding + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof PublicKey)) { + return false; + } + + PublicKey other = (PublicKey) obj; + + synchronized (stateLock) { + if (destroyed) { + return false; + } + + byte[] otherEncoded = other.getEncoded(); + String otherAlg = other.getAlgorithm(); + return otherEncoded != null && + ("LMS".equalsIgnoreCase(otherAlg) || + "HSS/LMS".equalsIgnoreCase(otherAlg)) && + Arrays.equals(this.encoded, otherEncoded); + } + } + + /** + * @return string representation, or a destroyed marker + */ + @Override + public String toString() { + synchronized (stateLock) { + if (destroyed) { + return "WolfCryptLmsPublicKey[DESTROYED]"; + } + return "WolfCryptLmsPublicKey[algorithm=HSS/LMS, levels=" + levels + + ", height=" + height + ", winternitz=" + winternitz + + ", format=X.509, encoded.length=" + encoded.length + "]"; + } + } + + /** + * Custom deserialization to reinitialize transient state. + * + * @param in ObjectInputStream to read from + * + * @throws IOException if an I/O error occurs + * @throws ClassNotFoundException if a class cannot be found + */ + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + + in.defaultReadObject(); + stateLock = new Object(); + if (!destroyed && encoded != null) { + try { + this.rawPublicKey = + WolfCryptLmsUtil.parsePublicKeyDer(encoded); + } + catch (IllegalArgumentException e) { + throw (InvalidObjectException) new InvalidObjectException( + "Invalid serialized LMS public key: " + + e.getMessage()).initCause(e); + } + } + } +} diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptLmsSignature.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptLmsSignature.java new file mode 100644 index 00000000..1f36ad47 --- /dev/null +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptLmsSignature.java @@ -0,0 +1,252 @@ +/* WolfCryptLmsSignature.java + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package com.wolfssl.provider.jce; + +import java.io.ByteArrayOutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.SignatureSpi; +import java.security.spec.AlgorithmParameterSpec; + +import com.wolfssl.wolfcrypt.Lms; +import com.wolfssl.wolfcrypt.WolfCryptException; + +/** + * wolfJCE LMS/HSS (RFC 8554) Signature provider, registered under both + * {@code "LMS"} and {@code "HSS/LMS"} names (and the HSS/LMS OID). + * + *This is a verify-only provider, matching the JDK SUN provider: + * {@code initVerify} / {@code verify} check signatures, while {@code initSign} + * throws {@link InvalidKeyException}. Stateful hash-based signing belongs in + * hardware (NIST SP 800-208), so wolfJCE does not generate keys or sign.
+ * + *LMS verifies a whole message (not a streaming hash), {@code engineUpdate} + * buffers it.
+ */ +public final class WolfCryptLmsSignature extends SignatureSpi { + + /** Native verify key (owned), non-null when initialized for verify. */ + private Lms verifyKey = null; + + /** Reset keeps the backing array up to this size, larger buffers are + * reallocated so pooled Signature objects do not pin large buffers. */ + private static final int BUFFER_RETAIN_MAX = 1024 * 1024; + + /** Buffered message bytes (LMS verifies the whole message). */ + private ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + /** + * Create a new wolfJCE LMS/HSS verify-only Signature object. + */ + public WolfCryptLmsSignature() { + } + + private void releaseKeys() { + if (this.verifyKey != null) { + this.verifyKey.releaseNativeStruct(); + this.verifyKey = null; + } + } + + private void resetForInit() { + releaseKeys(); + resetBuffer(); + } + + private void resetBuffer() { + if (this.buffer.size() > BUFFER_RETAIN_MAX) { + this.buffer = new ByteArrayOutputStream(); + } + else { + this.buffer.reset(); + } + } + + private Lms importPublicKeyForVerify(PublicKey pub) + throws InvalidKeyException { + + byte[] der; + byte[] rawPub; + Lms k = null; + + if (pub instanceof WolfCryptLmsPublicKey) { + rawPub = ((WolfCryptLmsPublicKey) pub).getRawPublicKey(); + if (rawPub == null) { + throw new InvalidKeyException("LMS public key destroyed"); + } + } + else { + if (!"X.509".equalsIgnoreCase(pub.getFormat())) { + throw new InvalidKeyException( + "Unsupported PublicKey format for LMS: " + pub.getFormat()); + } + + der = pub.getEncoded(); + if (der == null || der.length == 0) { + throw new InvalidKeyException( + "Cannot extract X.509 SPKI from PublicKey"); + } + + try { + rawPub = WolfCryptLmsUtil.parsePublicKeyDer(der); + } + catch (IllegalArgumentException e) { + throw new InvalidKeyException( + "Not a recognized LMS X.509 SPKI: " + e.getMessage(), e); + } + } + + try { + k = new Lms(); + k.importPublicRaw(rawPub); + return k; + } + catch (WolfCryptException e) { + if (k != null) { + k.releaseNativeStruct(); + } + throw new InvalidKeyException( + "Failed to import LMS public key: " + e.getMessage(), e); + } + } + + @Override + protected void engineInitVerify(PublicKey publicKey) + throws InvalidKeyException { + + if (publicKey == null) { + throw new InvalidKeyException("PublicKey is null"); + } + + resetForInit(); + + this.verifyKey = importPublicKeyForVerify(publicKey); + } + + /** + * Signing is not supported: wolfJCE provides verify-only LMS/HSS, matching + * the JDK SUN provider. Stateful hash-based signing belongs in hardware + * (NIST SP 800-208). + */ + @Override + protected void engineInitSign(PrivateKey privateKey) + throws InvalidKeyException { + + throw new InvalidKeyException( + "LMS/HSS signing is not supported (verify-only)"); + } + + @Override + protected void engineUpdate(byte b) throws SignatureException { + + if (this.verifyKey == null) { + throw new SignatureException("Signature not initialized"); + } + + this.buffer.write(b); + } + + @Override + protected void engineUpdate(byte[] b, int off, int len) + throws SignatureException { + + if (this.verifyKey == null) { + throw new SignatureException("Signature not initialized"); + } + + if ((b == null) || (off < 0) || (len < 0) || (off > b.length - len)) { + throw new SignatureException("Invalid update arguments"); + } + + this.buffer.write(b, off, len); + } + + /** + * Signing is not supported (verify-only). See {@link #engineInitSign}. + */ + @Override + protected byte[] engineSign() throws SignatureException { + + throw new SignatureException( + "LMS/HSS signing is not supported (verify-only)"); + } + + @Override + protected boolean engineVerify(byte[] sigBytes) + throws SignatureException { + + if (this.verifyKey == null) { + throw new SignatureException( + "Signature not initialized for verification"); + } + + if (sigBytes == null) { + throw new SignatureException("Signature bytes are null"); + } + + try { + return this.verifyKey.verify(sigBytes, this.buffer.toByteArray()); + } + catch (WolfCryptException e) { + throw new SignatureException("LMS verify failed", e); + } + finally { + resetBuffer(); + } + } + + /** + * @deprecated unsupported, LMS takes no per-signature parameters. + */ + @Override + @Deprecated + protected void engineSetParameter(String param, Object value) + throws InvalidParameterException { + + throw new InvalidParameterException( + "LMS does not accept algorithm parameters"); + } + + @Override + protected void engineSetParameter(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + + throw new InvalidAlgorithmParameterException( + "LMS does not accept algorithm parameters"); + } + + /** + * @deprecated unsupported. + */ + @Override + @Deprecated + protected Object engineGetParameter(String param) + throws InvalidParameterException { + + throw new InvalidParameterException( + "LMS does not accept algorithm parameters"); + } +} diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptLmsUtil.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptLmsUtil.java new file mode 100644 index 00000000..bdc06b86 --- /dev/null +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptLmsUtil.java @@ -0,0 +1,284 @@ +/* WolfCryptLmsUtil.java + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package com.wolfssl.provider.jce; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; + +/** + * ASN.1/DER helpers for LMS/HSS (RFC 8554) public keys. + * + *Native wolfCrypt provides a raw HSS/LMS public-key format + * (wc_LmsKey_Export/ImportPubRaw()) and no SPKI decode, so the X.509 + * SubjectPublicKeyInfo wrapping is done here until native wolfSSL gets that + * functionality. DER encoding reuses the shared + * {@link WolfCryptASN1Util} helpers. DER decoding uses a small + * self-contained strict reader. The LMS algorithm OID is + * {@code id-alg-hss-lms-hashsig = 1.2.840.113549.1.9.16.3.17} (RFC 8708, + * retained in RFC 9708).
+ * + *The DER reader here is intentionally self-contained and stricter than the + * general-purpose {@link WolfCryptASN1Util}: it checks every tag and rejects + * non-minimal long-form lengths, because it parses untrusted X.509 + * SubjectPublicKeyInfo input.
+ * + *Public key (SubjectPublicKeyInfo):
+ *
+ * SEQUENCE {
+ * algorithm SEQUENCE { OBJECT IDENTIFIER } -- parameters absent or NULL
+ * subjectPublicKey BIT STRING -- raw HSS/LMS public key
+ * }
+ *
+ * The raw HSS/LMS public key has two encodings in the wild: RFC 8708 / JDK
+ * <= 23 wrap it in an extra inner OCTET STRING inside the BIT STRING.
+ * RFC 9708 / JDK 24+ / BouncyCastle place the raw bytes directly. wolfJCE
+ * writes the unwrapped RFC 9708 form and reads both.
+ *
+ * wolfJCE provides verify-only LMS/HSS support, so there is no private-key + * encoding here (matching the JDK SUN provider).
+ */ +final class WolfCryptLmsUtil { + + /* DER tags used here. */ + private static final int TAG_BIT_STRING = 0x03; + private static final int TAG_OCTET_STRING = 0x04; + private static final int TAG_NULL = 0x05; + private static final int TAG_OID = 0x06; + private static final int TAG_SEQUENCE = 0x30; + + /* OID content bytes (no tag/length) for + * 1.2.840.113549.1.9.16.3.17 (id-alg-hss-lms-hashsig). */ + private static final byte[] OID_HSS_LMS = { + (byte)0x2A, (byte)0x86, (byte)0x48, (byte)0x86, (byte)0xF7, + (byte)0x0D, (byte)0x01, (byte)0x09, (byte)0x10, (byte)0x03, + (byte)0x11 + }; + + /** Private constructor, all methods are static. */ + private WolfCryptLmsUtil() { + } + + /** + * Encode a raw HSS/LMS public key as an X.509 SubjectPublicKeyInfo DER, + * RFC 9708 unwrapped form (raw key directly in the BIT STRING). + * + * @param rawPub raw HSS/LMS public key bytes + * + * @return X.509 SubjectPublicKeyInfo DER + */ + static byte[] encodePublicKeyDer(byte[] rawPub) { + + byte[] algId; + byte[] bitString; + + if (rawPub == null || rawPub.length == 0) { + throw new IllegalArgumentException( + "raw public key cannot be null or empty"); + } + + algId = WolfCryptASN1Util.encodeDERSequence( + WolfCryptASN1Util.encodeDERObjectIdentifier(OID_HSS_LMS)); + + /* RFC 9708 unwrapped form: the raw key sits directly in the BIT STRING + * (encodeDERBitString prepends the 0x00 unused-bits octet). */ + bitString = WolfCryptASN1Util.encodeDERBitString(rawPub); + + return WolfCryptASN1Util.encodeDERSequence(concat(algId, bitString)); + } + + /** + * Parse an X.509 SubjectPublicKeyInfo DER carrying an LMS/HSS public key + * and return the raw HSS/LMS public key bytes. Accepts both the RFC 9708 + * unwrapped form and the RFC 8708 form (raw key wrapped in an inner OCTET + * STRING). + * + * @param x509 X.509 SubjectPublicKeyInfo DER + * + * @return raw HSS/LMS public key bytes + * + * @throws IllegalArgumentException if the DER is malformed or not an LMS + * SubjectPublicKeyInfo + */ + static byte[] parsePublicKeyDer(byte[] x509) { + + int end; + int bsStart; + int bsEnd; + int contentStart; + int[] spki, algId, oid, bitStr, oct, nullParam; + + if (x509 == null || x509.length == 0) { + throw new IllegalArgumentException( + "encoded key cannot be null or empty"); + } + + /* outer SubjectPublicKeyInfo SEQUENCE, no trailing data */ + spki = readTLV(x509, 0, x509.length, TAG_SEQUENCE); + if (spki[1] != x509.length) { + throw bad("trailing data after SubjectPublicKeyInfo"); + } + end = spki[1]; + + /* AlgorithmIdentifier SEQUENCE { OID [, NULL] }. Parameters should be + * absent (RFC 8708/9708), but JDK <= 17 re-encodes them as an explicit + * NULL, so a single trailing NULL is also tolerated. */ + algId = readTLV(x509, spki[0], end, TAG_SEQUENCE); + oid = readTLV(x509, algId[0], algId[1], TAG_OID); + if (!regionEquals(x509, oid[0], oid[1], OID_HSS_LMS)) { + throw bad("not an LMS/HSS AlgorithmIdentifier OID"); + } + if (oid[1] != algId[1]) { + /* The parameters must be a single NULL (05 00) */ + if ((x509[oid[1]] & 0xFF) != TAG_NULL) { + throw bad("unexpected AlgorithmIdentifier parameters"); + } + nullParam = readTLV(x509, oid[1], algId[1], TAG_NULL); + if (nullParam[0] != nullParam[1] || nullParam[1] != algId[1]) { + throw bad("unexpected AlgorithmIdentifier parameters"); + } + } + + /* subjectPublicKey BIT STRING, no trailing data */ + bitStr = readTLV(x509, algId[1], end, TAG_BIT_STRING); + if (bitStr[1] != end) { + throw bad("trailing data after subjectPublicKey"); + } + bsStart = bitStr[0]; + bsEnd = bitStr[1]; + if (bsStart >= bsEnd) { + throw bad("empty subjectPublicKey BIT STRING"); + } + if ((x509[bsStart] & 0xFF) != 0x00) { + throw bad("unexpected unused bits in subjectPublicKey"); + } + contentStart = bsStart + 1; + if (contentStart >= bsEnd) { + throw bad("missing public key in subjectPublicKey"); + } + + /* RFC 8708 wraps the raw key in an OCTET STRING (tag 0x04). The raw + * HSS public key always begins with u32(L), L in 1..4, (ie byte + * 0x00), so a leading 0x04 unambiguously means the wrapped form. */ + if ((x509[contentStart] & 0xFF) == TAG_OCTET_STRING) { + oct = readTLV(x509, contentStart, bsEnd, TAG_OCTET_STRING); + if (oct[1] != bsEnd) { + throw bad("trailing data in wrapped public key"); + } + return Arrays.copyOfRange(x509, oct[0], oct[1]); + } + + return Arrays.copyOfRange(x509, contentStart, bsEnd); + } + + /* + * Parse a definite-form DER TLV at offset 'off' within [off, limit), + * requiring the given tag. Returns {contentStart, contentEnd}. Rejects + * indefinite/non-minimal lengths and overruns. + */ + private static int[] readTLV(byte[] in, int off, int limit, int tag) { + + int lenByte, contentStart, length, numLenBytes; + + if (off + 2 > limit) { + throw bad("truncated TLV header"); + } + + if ((in[off] & 0xFF) != tag) { + throw bad("unexpected tag 0x" + + Integer.toHexString(in[off] & 0xFF) + ", expected 0x" + + Integer.toHexString(tag)); + } + + lenByte = in[off + 1] & 0xFF; + + if (lenByte < 0x80) { + length = lenByte; + contentStart = off + 2; + } + else { + numLenBytes = lenByte & 0x7F; + + if (numLenBytes == 0 || numLenBytes > 4) { + throw bad("unsupported DER length form"); + } + + if (off + 2 + numLenBytes > limit) { + throw bad("truncated DER length"); + } + length = 0; + + for (int i = 0; i < numLenBytes; i++) { + length = (length << 8) | (in[off + 2 + i] & 0xFF); + } + + /* Values < 0x80 must use the short form. n-octet length must + * be n-octets long. */ + if (length < 0x80 || + length < (1 << ((numLenBytes - 1) * 8))) { + throw bad("non-minimal DER length"); + } + + contentStart = off + 2 + numLenBytes; + } + + /* contentStart <= limit was checked above, so limit - contentStart is + * non-negative and a crafted ~2GB long-form length cannot overflow + * the comparison. */ + if ((length < 0) || (length > limit - contentStart)) { + throw bad("DER length exceeds buffer"); + } + + return new int[] { contentStart, contentStart + length }; + } + + /* Compare a region of 'in' against 'expected'. */ + private static boolean regionEquals(byte[] in, int start, int end, + byte[] expected) { + + if ((end - start) != expected.length) { + return false; + } + + for (int i = 0; i < expected.length; i++) { + if (in[start + i] != expected[i]) { + return false; + } + } + + return true; + } + + /* Concatenate byte arrays. */ + private static byte[] concat(byte[]... parts) { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (byte[] p : parts) { + out.write(p, 0, p.length); + } + + return out.toByteArray(); + } + + private static IllegalArgumentException bad(String msg) { + return new IllegalArgumentException("LMS DER parse error: " + msg); + } +} diff --git a/src/main/java/com/wolfssl/provider/jce/WolfCryptProvider.java b/src/main/java/com/wolfssl/provider/jce/WolfCryptProvider.java index 873463db..4338a9fa 100644 --- a/src/main/java/com/wolfssl/provider/jce/WolfCryptProvider.java +++ b/src/main/java/com/wolfssl/provider/jce/WolfCryptProvider.java @@ -358,6 +358,15 @@ private void registerServices() { put("Alg.Alias.Signature.OID.1.3.6.1.5.5.7.6.35", "XMSSMT"); } + /* LMS / HSS (RFC 8554) Signature support (verify-only) */ + if (FeatureDetect.LmsEnabled()) { + put("Signature.LMS", + "com.wolfssl.provider.jce.WolfCryptLmsSignature"); + put("Alg.Alias.Signature.HSS/LMS", "LMS"); + put("Alg.Alias.Signature.1.2.840.113549.1.9.16.3.17", "LMS"); + put("Alg.Alias.Signature.OID.1.2.840.113549.1.9.16.3.17", "LMS"); + } + /* Mac */ if (FeatureDetect.HmacMd5Enabled()) { put("Mac.HmacMD5", @@ -849,6 +858,16 @@ private void registerServices() { put("Alg.Alias.KeyFactory.OID.1.3.6.1.5.5.7.6.35", "XMSSMT"); } + /* LMS / HSS (RFC 8554) KeyFactory (verify-only). X.509 public-key + * handling only, private keys are not supported. */ + if (FeatureDetect.LmsEnabled()) { + put("KeyFactory.LMS", + "com.wolfssl.provider.jce.WolfCryptLmsKeyFactory"); + put("Alg.Alias.KeyFactory.HSS/LMS", "LMS"); + put("Alg.Alias.KeyFactory.1.2.840.113549.1.9.16.3.17", "LMS"); + put("Alg.Alias.KeyFactory.OID.1.2.840.113549.1.9.16.3.17", "LMS"); + } + /* KeyStore */ put("KeyStore.WKS", "com.wolfssl.provider.jce.WolfSSLKeyStore"); diff --git a/src/main/java/com/wolfssl/wolfcrypt/Lms.java b/src/main/java/com/wolfssl/wolfcrypt/Lms.java new file mode 100644 index 00000000..e85d9989 --- /dev/null +++ b/src/main/java/com/wolfssl/wolfcrypt/Lms.java @@ -0,0 +1,279 @@ +/* Lms.java + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package com.wolfssl.wolfcrypt; + +/** + * Wrapper for the native wolfCrypt LMS/HSS (RFC 8554) verification API. + * + *Mirrors the verify-only subset of the native wolfCrypt + * {@code wc_LmsKey_*} API. LMS is a stateful hash-based signature scheme. HSS + * is its multi-level (hierarchical) variant. wolfJCE exposes verification + * only: stateful signing and key generation belong in hardware (NIST SP + * 800-208), matching the verify-only JDK SUN provider.
+ * + *A verify-only key is created with the no-argument constructor + * {@link #Lms()} and {@link #importPublicRaw(byte[])}. The parameter set is + * derived from the imported public key. Signatures are checked with + * {@link #verify(byte[], byte[])}.
+ * + *Native wolfCrypt LMS keys are not thread-safe. Callers must serialize all + * operations on a given {@code Lms} instance. This class synchronizes its + * native calls but concurrent use of one key from multiple threads is not + * supported.
+ */ +public class Lms extends NativeStruct { + + /* Hash-family selectors, mirror native wc_lms.h LMS_* values reported by + * wc_LmsKey_GetParameters_ex(). The low bits encode the LMS/LM-OTS type. + * These high-bit values select the hash family. */ + + /** SHA-256/256 hash family (RFC 8554 default). */ + public static final int LMS_SHA256 = 0x0000; + + /** SHA-256/192 hash family (NIST SP 800-208, 192-bit truncation). */ + public static final int LMS_SHA256_192 = 0x1000; + + /** SHAKE256/256 hash family (NIST SP 800-208). Requires native SHA-3. */ + public static final int LMS_SHAKE256 = 0x2000; + + /** SHAKE256/192 hash family (NIST SP 800-208). Requires native SHA-3. */ + public static final int LMS_SHAKE256_192 = 0x3000; + + private WolfCryptState state = WolfCryptState.UNINITIALIZED; + + /** Lock around object state. */ + protected final Object stateLock = new Object(); + + /* Parameter set, filled in from the imported public key. Volatile so + * getters see the latest value after an import refresh. */ + private volatile int levels; + private volatile int height; + private volatile int winternitz; + private volatile int hashType; + + /** + * Create a new verify-only LMS/HSS key. + * + *The parameter set is derived from the public key supplied to + * {@link #importPublicRaw(byte[])}.
+ * + * @throws WolfCryptException if LMS is not compiled into native wolfCrypt + */ + public Lms() throws WolfCryptException { + + if (!FeatureDetect.LmsEnabled()) { + throw new WolfCryptException( + WolfCryptError.NOT_COMPILED_IN.getCode()); + } + /* Parameters are filled in from an imported public key. */ + } + + @Override + public void releaseNativeStruct() { + synchronized (stateLock) { + if ((state != WolfCryptState.UNINITIALIZED) && + (state != WolfCryptState.RELEASED)) { + + synchronized (pointerLock) { + wc_LmsKey_free(); + } + super.releaseNativeStruct(); + state = WolfCryptState.RELEASED; + } + } + } + + /** + * Allocate native LmsKey context struct. + * + * @return native allocated pointer + * + * @throws OutOfMemoryError when malloc fails + */ + protected native long mallocNativeStruct() throws OutOfMemoryError; + + private native void wc_LmsKey_init(); + private native void wc_LmsKey_free(); + private native int[] wc_LmsKey_get_parameters(); + private native boolean wc_LmsKey_verify(byte[] sig, byte[] msg); + private native void wc_LmsKey_import_public(byte[] in); + + /** + * Allocate, initialize, and (for a verify-only key) leave the parameter + * set to be derived from an imported public key. State advances + * UNINITIALIZED to INITIALIZED on success. + * + * @throws IllegalStateException if releaseNativeStruct() has been called + */ + private synchronized void checkStateAndInitialize() + throws IllegalStateException { + + synchronized (stateLock) { + if (state == WolfCryptState.RELEASED) { + throw new IllegalStateException("Object has been released"); + } + + if (state == WolfCryptState.UNINITIALIZED) { + synchronized (pointerLock) { + initNativeStruct(); + wc_LmsKey_init(); + } + state = WolfCryptState.INITIALIZED; + } + } + } + + /** + * Throw exception if this object already has a key loaded. + * + * @throws IllegalStateException if state is READY (key loaded) + */ + private void throwIfKeyExists() throws IllegalStateException { + + synchronized (stateLock) { + if (state == WolfCryptState.READY) { + throw new IllegalStateException("Object already has a key"); + } + } + } + + /** + * Throw exception if this object does not have a key loaded. + * + * @throws IllegalStateException if state is not READY (no key loaded) + */ + private void throwIfKeyNotLoaded() throws IllegalStateException { + + synchronized (stateLock) { + if (state != WolfCryptState.READY) { + throw new IllegalStateException( + "No key available to perform the operation"); + } + } + } + + /** + * Refresh the cached parameter set from the native key. Used after an + * import where native has derived the parameter set. + */ + private void refreshParameters() { + + int[] params; + + synchronized (pointerLock) { + params = wc_LmsKey_get_parameters(); + } + + if (params != null && params.length == 4) { + this.levels = params[0]; + this.height = params[1]; + this.winternitz = params[2]; + this.hashType = params[3]; + } + } + + /** + * Verify a signature over a message. + * + * @param sig signature to verify + * @param msg message bytes + * + * @return true if the signature verifies, false otherwise + * + * @throws WolfCryptException if the native operation fails + * @throws IllegalStateException if no key is loaded or object released + */ + public boolean verify(byte[] sig, byte[] msg) + throws WolfCryptException, IllegalStateException { + + checkStateAndInitialize(); + throwIfKeyNotLoaded(); + + synchronized (pointerLock) { + return wc_LmsKey_verify(sig, msg); + } + } + + /** + * Import a raw HSS/LMS public key (RFC 8554 wire format) for verification. + * + *The parameter set is derived from the imported key.
+ * + * @param in raw public key bytes + * + * @throws WolfCryptException if the native operation fails + * @throws IllegalStateException if a key is already loaded or object + * released + */ + public void importPublicRaw(byte[] in) + throws WolfCryptException, IllegalStateException { + + checkStateAndInitialize(); + throwIfKeyExists(); + + synchronized (stateLock) { + synchronized (pointerLock) { + wc_LmsKey_import_public(in); + } + refreshParameters(); + state = WolfCryptState.READY; + } + } + + /** + * Get the number of HSS levels for this key's parameter set. + * + * @return number of levels (1 for single-tree LMS), or 0 if not yet known + * (before a public key has been imported) + */ + public int getLevels() { + return this.levels; + } + + /** + * Get the per-level Merkle tree height for this key's parameter set. + * + * @return tree height, or 0 if not yet known + */ + public int getHeight() { + return this.height; + } + + /** + * Get the LM-OTS Winternitz parameter for this key's parameter set. + * + * @return Winternitz parameter, or 0 if not yet known + */ + public int getWinternitz() { + return this.winternitz; + } + + /** + * Get the hash family selector for this key's parameter set. + * + * @return one of {@link #LMS_SHA256}, {@link #LMS_SHA256_192}, + * {@link #LMS_SHAKE256}, {@link #LMS_SHAKE256_192} + */ + public int getHashType() { + return this.hashType; + } +} diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptLmsKeyFactoryTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptLmsKeyFactoryTest.java new file mode 100644 index 00000000..ffd98a38 --- /dev/null +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptLmsKeyFactoryTest.java @@ -0,0 +1,403 @@ +/* WolfCryptLmsKeyFactoryTest.java + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package com.wolfssl.provider.jce.test; + +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.security.KeyFactory; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.spec.X509EncodedKeySpec; + +import com.wolfssl.provider.jce.WolfCryptProvider; +import com.wolfssl.wolfcrypt.FeatureDetect; +import com.wolfssl.wolfcrypt.WolfCryptError; +import com.wolfssl.wolfcrypt.WolfCryptException; +import com.wolfssl.wolfcrypt.test.TimedTestWatcher; +import com.wolfssl.wolfcrypt.test.Util; + +/** + * wolfJCE tests for the LMS/HSS KeyFactory service (verify-only), via JCE API. + * + *wolfJCE LMS is verify-only (matching the JDK SUN provider), so the public + * key is built from the RFC 8554 Test Case 1 raw HSS public key wrapped in an + * X.509 SubjectPublicKeyInfo rather than generated.
+ */ +public class WolfCryptLmsKeyFactoryTest { + + private static boolean lmsEnabled = false; + + @Rule(order = Integer.MIN_VALUE) + public TestRule testWatcher = TimedTestWatcher.create(); + + @BeforeClass + public static void setUp() { + System.out.println("JCE WolfCryptLmsKeyFactoryTest Class"); + + Security.insertProviderAt(new WolfCryptProvider(), 1); + Provider p = Security.getProvider("wolfJCE"); + assertNotNull(p); + + lmsEnabled = FeatureDetect.LmsEnabled(); + } + + private void assumeEnabled() { + Assume.assumeTrue("LMS not compiled in", lmsEnabled); + } + + /* Short local alias of Util.h2b() to keep the vector readable. */ + private static byte[] hex(String s) { + return Util.h2b(s); + } + + /* RFC 8554 Appendix F Test Case 1 raw HSS public key (HSS L2, SHA256 + * H5/W8), 60 bytes. */ + private static final byte[] RFC8554_TC1_PK = hex( + "00000002000000050000000461a5d57d37f5e46bfb7520806b07a1b850650e3b" + + "31fe4a773ea29a07f09cf2ea30e579f0df58ef8e298da0434cb2b878"); + + /* HSS/LMS algorithm OID 1.2.840.113549.1.9.16.3.17 as a DER TLV. */ + private static final byte[] HSS_LMS_OID = new byte[] { + (byte) 0x06, (byte) 0x0B, (byte) 0x2A, (byte) 0x86, (byte) 0x48, + (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x09, + (byte) 0x10, (byte) 0x03, (byte) 0x11 + }; + + /* DER TLV with a definite length (content up to 0xFFFF bytes). */ + private static byte[] tlv(int tag, byte[] content) { + + int n = content.length; + byte[] len; + + if (n < 0x80) { + len = new byte[] { (byte) n }; + } else if (n < 0x100) { + len = new byte[] { (byte) 0x81, (byte) n }; + } else { + len = new byte[] { (byte) 0x82, (byte) (n >> 8), (byte) n }; + } + + byte[] out = new byte[1 + len.length + n]; + out[0] = (byte) tag; + System.arraycopy(len, 0, out, 1, len.length); + System.arraycopy(content, 0, out, 1 + len.length, n); + + return out; + } + + private static byte[] concat(byte[] a, byte[] b) { + + byte[] out = new byte[a.length + b.length]; + System.arraycopy(a, 0, out, 0, a.length); + System.arraycopy(b, 0, out, a.length, b.length); + + return out; + } + + /* Wrap a raw HSS/LMS public key as an RFC 9708 (unwrapped) SPKI: + * SEQUENCE { SEQUENCE { OID }, BIT STRING { rawPub } }. */ + private static byte[] spki(byte[] rawPub) { + + byte[] algId = tlv(0x30, HSS_LMS_OID); + byte[] bitString = tlv(0x03, concat(new byte[] { 0x00 }, rawPub)); + + return tlv(0x30, concat(algId, bitString)); + } + + /* Import the RFC 8554 TC1 (SHA-256/256) KAT public key via the given + * KeyFactory. Parameter family may be compiled out of native wolfCrypt, + * in which case underlying import throws NOT_COMPILED_IN. Treat that as + * "skip". */ + private static PublicKey importTc1Key(KeyFactory kf) throws Exception { + + try { + return kf.generatePublic( + new X509EncodedKeySpec(spki(RFC8554_TC1_PK))); + + } catch (java.security.spec.InvalidKeySpecException e) { + if (isNotCompiledIn(e)) { + Assume.assumeTrue( + "LMS SHA-256/256 parameter set not compiled in", false); + } + throw e; + } + } + + /* True if throwable cause chain contains NOT_COMPILED_IN. */ + private static boolean isNotCompiledIn(Throwable t) { + + for (; t != null; t = t.getCause()) { + if (t instanceof WolfCryptException && + ((WolfCryptException) t).getError() == + WolfCryptError.NOT_COMPILED_IN) { + return true; + } + } + + return false; + } + + @Test + public void aliasesResolve() throws Exception { + assumeEnabled(); + + /* KeyFactory (public-key handling) is always available. */ + KeyFactory.getInstance("LMS", "wolfJCE"); + KeyFactory.getInstance("HSS/LMS", "wolfJCE"); + } + + @Test + public void publicKeyRoundTrip() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + assertEquals("HSS/LMS", pub.getAlgorithm()); + assertEquals("X.509", pub.getFormat()); + + byte[] x509 = pub.getEncoded(); + assertNotNull(x509); + + /* Re-import the produced X.509 encoding; it must be stable. */ + PublicKey pub2 = kf.generatePublic(new X509EncodedKeySpec(x509)); + assertEquals(pub, pub2); + assertArrayEquals(x509, pub2.getEncoded()); + + /* round-trip via getKeySpec */ + X509EncodedKeySpec spec = + kf.getKeySpec(pub2, X509EncodedKeySpec.class); + assertArrayEquals(x509, spec.getEncoded()); + } + + /* JDK <= 17 re-encodes an LMS SubjectPublicKeyInfo with an explicit NULL + * parameter before passing it here, so wolfJCE must accept the NULL form + * as equivalent to the parameters-absent form. This is what lets LMS + * certificates load via CertificateFactory on older JDKs. */ + @Test + public void generatePublicAcceptsNullAlgorithmParameters() + throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey noParams = importTc1Key(kf); + + /* AlgorithmIdentifier SEQUENCE { OID, NULL } instead of { OID }. */ + byte[] algId = tlv(0x30, concat(HSS_LMS_OID, + new byte[] { (byte) 0x05, (byte) 0x00 })); + byte[] bitString = + tlv(0x03, concat(new byte[] { 0x00 }, RFC8554_TC1_PK)); + byte[] spkiWithNull = tlv(0x30, concat(algId, bitString)); + + PublicKey withNull = + kf.generatePublic(new X509EncodedKeySpec(spkiWithNull)); + + /* Same key, and getEncoded() normalizes to the canonical (no + * parameters) SubjectPublicKeyInfo. */ + assertEquals(noParams, withNull); + assertArrayEquals(noParams.getEncoded(), withNull.getEncoded()); + } + + @Test + public void privateKeySpecRejected() throws Exception { + assumeEnabled(); + + /* Private keys are not supported (verify-only). */ + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + try { + kf.generatePrivate( + new java.security.spec.PKCS8EncodedKeySpec(new byte[] { 0 })); + fail("expected LMS private keys to be unsupported"); + } catch (java.security.spec.InvalidKeySpecException e) { + /* expected */ + } + } + + /* A byte-identical public key reporting the JDK standard name "HSS/LMS" + * (or "LMS", case-insensitively) must compare equal, since this provider + * registers both names for the same algorithm. */ + @Test + public void equalsAcceptsHssLmsAlgorithmName() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + byte[] enc = pub.getEncoded(); + + assertTrue("HSS/LMS name should compare equal", + pub.equals(stubKey("HSS/LMS", enc))); + assertTrue("LMS name should compare equal", + pub.equals(stubKey("LMS", enc))); + assertTrue("name match should be case-insensitive", + pub.equals(stubKey("hss/lms", enc))); + + /* Different algorithm name, same bytes -> not equal. */ + assertFalse("non-LMS name should not compare equal", + pub.equals(stubKey("RSA", enc))); + + /* Same name, different bytes -> not equal. */ + byte[] diff = enc.clone(); + diff[diff.length - 1] ^= (byte) 0xFF; + assertFalse("different encoding should not compare equal", + pub.equals(stubKey("HSS/LMS", diff))); + } + + @Test + public void generatePublicRejectsNonX509Spec() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + try { + kf.generatePublic(new java.security.spec.PKCS8EncodedKeySpec( + spki(RFC8554_TC1_PK))); + fail("expected non-X509EncodedKeySpec to be rejected"); + } catch (java.security.spec.InvalidKeySpecException e) { + /* expected */ + } + } + + @Test + public void generatePublicRejectsMalformedDer() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + try { + kf.generatePublic( + new X509EncodedKeySpec(new byte[] { 0x01, 0x02, 0x03 })); + fail("expected malformed DER to be rejected"); + } catch (java.security.spec.InvalidKeySpecException e) { + /* expected */ + } + } + + @Test + public void generatePublicRejectsWrongOid() throws Exception { + assumeEnabled(); + + /* Valid SPKI structure but the RSA OID, not HSS/LMS. */ + byte[] rsaOid = new byte[] { + (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48, + (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, + (byte) 0x01 + }; + byte[] der = tlv(0x30, concat(tlv(0x30, rsaOid), + tlv(0x03, concat(new byte[] { 0x00 }, RFC8554_TC1_PK)))); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + try { + kf.generatePublic(new X509EncodedKeySpec(der)); + fail("expected wrong-OID SPKI to be rejected"); + } catch (java.security.spec.InvalidKeySpecException e) { + /* expected */ + } + } + + @Test + public void getKeySpecRejectsUnsupportedSpecClass() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + try { + kf.getKeySpec(pub, + java.security.spec.PKCS8EncodedKeySpec.class); + fail("expected unsupported KeySpec class to be rejected"); + } catch (java.security.spec.InvalidKeySpecException e) { + /* expected */ + } + } + + @Test + public void translateKeyRoundTrips() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + + /* Our own key object is returned as-is. */ + assertSame(pub, kf.translateKey(pub)); + + /* A foreign X.509 key with the same encoding translates to an + * equal wolfJCE key. */ + assertEquals(pub, + kf.translateKey(stubKey("HSS/LMS", pub.getEncoded()))); + } + + @Test + public void translateKeyRejectsNonX509Format() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + try { + kf.translateKey(rawFormatKey()); + fail("expected non-X.509 key format to be rejected"); + } catch (java.security.InvalidKeyException e) { + /* expected */ + } + } + + /* Minimal PublicKey returning a fixed algorithm name and X.509 encoding, + * to stand in for an equal key produced by another provider. */ + private static PublicKey stubKey(final String algorithm, + final byte[] encoded) { + + return new PublicKey() { + @Override + public String getAlgorithm() { + return algorithm; + } + @Override + public String getFormat() { + return "X.509"; + } + @Override + public byte[] getEncoded() { + return encoded.clone(); + } + }; + } + + /* Minimal PublicKey reporting a non-X.509 format, for exercising + * translateKey() format rejection. */ + private static PublicKey rawFormatKey() { + + return new PublicKey() { + @Override + public String getAlgorithm() { + return "LMS"; + } + @Override + public String getFormat() { + return "RAW"; + } + @Override + public byte[] getEncoded() { + return new byte[] { 0x00 }; + } + }; + } +} diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfCryptLmsSignatureTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptLmsSignatureTest.java new file mode 100644 index 00000000..550e1159 --- /dev/null +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfCryptLmsSignatureTest.java @@ -0,0 +1,480 @@ +/* WolfCryptLmsSignatureTest.java + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package com.wolfssl.provider.jce.test; + +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.security.KeyFactory; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.security.SignatureException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.spec.X509EncodedKeySpec; +import java.security.spec.AlgorithmParameterSpec; +import java.security.InvalidAlgorithmParameterException; + +import com.wolfssl.provider.jce.WolfCryptProvider; +import com.wolfssl.wolfcrypt.FeatureDetect; +import com.wolfssl.wolfcrypt.WolfCryptError; +import com.wolfssl.wolfcrypt.WolfCryptException; +import com.wolfssl.wolfcrypt.test.TimedTestWatcher; +import com.wolfssl.wolfcrypt.test.Util; + +/** + * wolfJCE tests for the LMS/HSS Signature service (verify-only), via JCE API. + */ +public class WolfCryptLmsSignatureTest { + + private static boolean lmsEnabled = false; + + @Rule(order = Integer.MIN_VALUE) + public TestRule testWatcher = TimedTestWatcher.create(); + + @BeforeClass + public static void setUp() { + System.out.println("JCE WolfCryptLmsSignatureTest Class"); + + Security.insertProviderAt(new WolfCryptProvider(), 1); + Provider p = Security.getProvider("wolfJCE"); + assertNotNull(p); + + lmsEnabled = FeatureDetect.LmsEnabled(); + } + + private void assumeEnabled() { + Assume.assumeTrue("LMS not compiled in", lmsEnabled); + } + + private static boolean verify(PublicKey pub, byte[] sig, byte[] msg) + throws Exception { + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + s.initVerify(pub); + s.update(msg); + + return s.verify(sig); + } + + @Test + public void aliasesResolve() throws Exception { + assumeEnabled(); + + /* Verification is always available under both names + OID. */ + Signature.getInstance("LMS", "wolfJCE"); + Signature.getInstance("HSS/LMS", "wolfJCE"); + Signature.getInstance("1.2.840.113549.1.9.16.3.17", "wolfJCE"); + } + + /* RFC 8554 Appendix F Test Case 1 (HSS L2, SHA256 H5/W8). + * Exercises raw HSS public key -> RFC 9708 SPKI -> + * KeyFactory.generatePublic() -> Signature.verify(). The JNI-level RFC + * 8554 / 9858 KAT matrix lives in com.wolfssl.wolfcrypt.test.LmsTest. */ + + /* Short local alias of Util.h2b() to keep KAT declarations readable. */ + private static byte[] hex(String s) { + return Util.h2b(s); + } + + private static final byte[] RFC8554_TC1_PK = hex( + "00000002000000050000000461a5d57d37f5e46bfb7520806b07a1b850650e3b" + + "31fe4a773ea29a07f09cf2ea30e579f0df58ef8e298da0434cb2b878"); + + private static final byte[] RFC8554_TC1_MSG = hex( + "54686520706f77657273206e6f742064656c65676174656420746f2074686520" + + "556e69746564205374617465732062792074686520436f6e737469747574696f" + + "6e2c206e6f722070726f6869626974656420627920697420746f207468652053" + + "74617465732c2061726520726573657276656420746f20746865205374617465" + + "7320726573706563746976656c792c206f7220746f207468652070656f706c65" + + "2e0a"); + + private static final byte[] RFC8554_TC1_SIG = hex( + "000000010000000500000004d32b56671d7eb98833c49b433c272586bc4a1c8a" + + "8970528ffa04b966f9426eb9965a25bfd37f196b9073f3d4a232feb69128ec45" + + "146f86292f9dff9610a7bf95a64c7f60f6261a62043f86c70324b7707f5b4a8a" + + "6e19c114c7be866d488778a0e05fd5c6509a6e61d559cf1a77a970de927d60c7" + + "0d3de31a7fa0100994e162a2582e8ff1b10cd99d4e8e413ef469559f7d7ed12c" + + "838342f9b9c96b83a4943d1681d84b15357ff48ca579f19f5e71f18466f2bbef" + + "4bf660c2518eb20de2f66e3b14784269d7d876f5d35d3fbfc7039a462c716bb9" + + "f6891a7f41ad133e9e1f6d9560b960e7777c52f060492f2d7c660e1471e07e72" + + "655562035abc9a701b473ecbc3943c6b9c4f2405a3cb8bf8a691ca51d3f6ad2f" + + "428bab6f3a30f55dd9625563f0a75ee390e385e3ae0b906961ecf41ae073a059" + + "0c2eb6204f44831c26dd768c35b167b28ce8dc988a3748255230cef99ebf14e7" + + "30632f27414489808afab1d1e783ed04516de012498682212b07810579b25036" + + "5941bcc98142da13609e9768aaf65de7620dabec29eb82a17fde35af15ad238c" + + "73f81bdb8dec2fc0e7f932701099762b37f43c4a3c20010a3d72e2f606be108d" + + "310e639f09ce7286800d9ef8a1a40281cc5a7ea98d2adc7c7400c2fe5a101552" + + "df4e3cccfd0cbf2ddf5dc6779cbbc68fee0c3efe4ec22b83a2caa3e48e0809a0" + + "a750b73ccdcf3c79e6580c154f8a58f7f24335eec5c5eb5e0cf01dcf44394240" + + "95fceb077f66ded5bec73b27c5b9f64a2a9af2f07c05e99e5cf80f00252e39db" + + "32f6c19674f190c9fbc506d826857713afd2ca6bb85cd8c107347552f30575a5" + + "417816ab4db3f603f2df56fbc413e7d0acd8bdd81352b2471fc1bc4f1ef296fe" + + "a1220403466b1afe78b94f7ecf7cc62fb92be14f18c2192384ebceaf8801afdf" + + "947f698ce9c6ceb696ed70e9e87b0144417e8d7baf25eb5f70f09f016fc925b4" + + "db048ab8d8cb2a661ce3b57ada67571f5dd546fc22cb1f97e0ebd1a65926b123" + + "4fd04f171cf469c76b884cf3115cce6f792cc84e36da58960c5f1d760f32c12f" + + "aef477e94c92eb75625b6a371efc72d60ca5e908b3a7dd69fef0249150e3eebd" + + "fed39cbdc3ce9704882a2072c75e13527b7a581a556168783dc1e97545e31865" + + "ddc46b3c957835da252bb7328d3ee2062445dfb85ef8c35f8e1f3371af34023c" + + "ef626e0af1e0bc017351aae2ab8f5c612ead0b729a1d059d02bfe18efa971b73" + + "00e882360a93b025ff97e9e0eec0f3f3f13039a17f88b0cf808f488431606cb1" + + "3f9241f40f44e537d302c64a4f1f4ab949b9feefadcb71ab50ef27d6d6ca8510" + + "f150c85fb525bf25703df7209b6066f09c37280d59128d2f0f637c7d7d7fad4e" + + "d1c1ea04e628d221e3d8db77b7c878c9411cafc5071a34a00f4cf07738912753" + + "dfce48f07576f0d4f94f42c6d76f7ce973e9367095ba7e9a3649b7f461d9f9ac" + + "1332a4d1044c96aefee67676401b64457c54d65fef6500c59cdfb69af7b6dddf" + + "cb0f086278dd8ad0686078dfb0f3f79cd893d314168648499898fbc0ced5f95b" + + "74e8ff14d735cdea968bee7400000005d8b8112f9200a5e50c4a262165bd342c" + + "d800b8496810bc716277435ac376728d129ac6eda839a6f357b5a04387c5ce97" + + "382a78f2a4372917eefcbf93f63bb59112f5dbe400bd49e4501e859f885bf073" + + "6e90a509b30a26bfac8c17b5991c157eb5971115aa39efd8d564a6b90282c316" + + "8af2d30ef89d51bf14654510a12b8a144cca1848cf7da59cc2b3d9d0692dd2a2" + + "0ba3863480e25b1b85ee860c62bf51360000000500000004d2f14ff6346af964" + + "569f7d6cb880a1b66c5004917da6eafe4d9ef6c6407b3db0e5485b122d9ebe15" + + "cda93cfec582d7ab0000000a000000040703c491e7558b35011ece3592eaa5da" + + "4d918786771233e8353bc4f62323185c95cae05b899e35dffd71705470620998" + + "8ebfdf6e37960bb5c38d7657e8bffeef9bc042da4b4525650485c66d0ce19b31" + + "7587c6ba4bffcc428e25d08931e72dfb6a120c5612344258b85efdb7db1db9e1" + + "865a73caf96557eb39ed3e3f426933ac9eeddb03a1d2374af7bf771855774562" + + "37f9de2d60113c23f846df26fa942008a698994c0827d90e86d43e0df7f4bfcd" + + "b09b86a373b98288b7094ad81a0185ac100e4f2c5fc38c003c1ab6fea479eb2f" + + "5ebe48f584d7159b8ada03586e65ad9c969f6aecbfe44cf356888a7b15a3ff07" + + "4f771760b26f9c04884ee1faa329fbf4e61af23aee7fa5d4d9a5dfcf43c4c26c" + + "e8aea2ce8a2990d7ba7b57108b47dabfbeadb2b25b3cacc1ac0cef346cbb90fb" + + "044beee4fac2603a442bdf7e507243b7319c9944b1586e899d431c7f91bcccc8" + + "690dbf59b28386b2315f3d36ef2eaa3cf30b2b51f48b71b003dfb08249484201" + + "043f65f5a3ef6bbd61ddfee81aca9ce60081262a00000480dcbc9a3da6fbef5c" + + "1c0a55e48a0e729f9184fcb1407c31529db268f6fe50032a363c9801306837fa" + + "fabdf957fd97eafc80dbd165e435d0e2dfd836a28b354023924b6fb7e48bc0b3" + + "ed95eea64c2d402f4d734c8dc26f3ac591825daef01eae3c38e3328d00a77dc6" + + "57034f287ccb0f0e1c9a7cbdc828f627205e4737b84b58376551d44c12c3c215" + + "c812a0970789c83de51d6ad787271963327f0a5fbb6b5907dec02c9a90934af5" + + "a1c63b72c82653605d1dcce51596b3c2b45696689f2eb382007497557692caac" + + "4d57b5de9f5569bc2ad0137fd47fb47e664fcb6db4971f5b3e07aceda9ac130e" + + "9f38182de994cff192ec0e82fd6d4cb7f3fe00812589b7a7ce51544045643301" + + "6b84a59bec6619a1c6c0b37dd1450ed4f2d8b584410ceda8025f5d2d8dd0d217" + + "6fc1cf2cc06fa8c82bed4d944e71339ece780fd025bd41ec34ebff9d4270a322" + + "4e019fcb444474d482fd2dbe75efb20389cc10cd600abb54c47ede93e08c114e" + + "db04117d714dc1d525e11bed8756192f929d15462b939ff3f52f2252da2ed64d" + + "8fae88818b1efa2c7b08c8794fb1b214aa233db3162833141ea4383f1a6f120b" + + "e1db82ce3630b3429114463157a64e91234d475e2f79cbf05e4db6a9407d72c6" + + "bff7d1198b5c4d6aad2831db61274993715a0182c7dc8089e32c8531deed4f74" + + "31c07c02195eba2ef91efb5613c37af7ae0c066babc69369700e1dd26eddc0d2" + + "16c781d56e4ce47e3303fa73007ff7b949ef23be2aa4dbf25206fe45c20dd888" + + "395b2526391a724996a44156beac808212858792bf8e74cba49dee5e8812e019" + + "da87454bff9e847ed83db07af313743082f880a278f682c2bd0ad6887cb59f65" + + "2e155987d61bbf6a88d36ee93b6072e6656d9ccbaae3d655852e38deb3a2dcf8" + + "058dc9fb6f2ab3d3b3539eb77b248a661091d05eb6e2f297774fe6053598457c" + + "c61908318de4b826f0fc86d4bb117d33e865aa805009cc2918d9c2f840c4da43" + + "a703ad9f5b5806163d7161696b5a0adc00000005d5c0d1bebb06048ed6fe2ef2" + + "c6cef305b3ed633941ebc8b3bec9738754cddd60e1920ada52f43d055b5031ce" + + "e6192520d6a5115514851ce7fd448d4a39fae2ab2335b525f484e9b40d6a4a96" + + "9394843bdcf6d14c48e8015e08ab92662c05c6e9f90b65a7a6201689999f32bf" + + "d368e5e3ec9cb70ac7b8399003f175c40885081a09ab3034911fe125631051df" + + "0408b3946b0bde790911e8978ba07dd56c73e7ee"); + + /* HSS/LMS algorithm OID 1.2.840.113549.1.9.16.3.17 as a DER TLV. */ + private static final byte[] HSS_LMS_OID = new byte[] { + (byte) 0x06, (byte) 0x0B, (byte) 0x2A, (byte) 0x86, (byte) 0x48, + (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x09, + (byte) 0x10, (byte) 0x03, (byte) 0x11 + }; + + /* DER TLV with a definite length (content up to 0xFFFF bytes). */ + private static byte[] tlv(int tag, byte[] content) { + + int n = content.length; + byte[] len; + + if (n < 0x80) { + len = new byte[] { (byte) n }; + } else if (n < 0x100) { + len = new byte[] { (byte) 0x81, (byte) n }; + } else { + len = new byte[] { (byte) 0x82, (byte) (n >> 8), (byte) n }; + } + + byte[] out = new byte[1 + len.length + n]; + out[0] = (byte) tag; + System.arraycopy(len, 0, out, 1, len.length); + System.arraycopy(content, 0, out, 1 + len.length, n); + + return out; + } + + private static byte[] concat(byte[] a, byte[] b) { + + byte[] out = new byte[a.length + b.length]; + System.arraycopy(a, 0, out, 0, a.length); + System.arraycopy(b, 0, out, a.length, b.length); + + return out; + } + + /* Wrap a raw HSS/LMS public key as an RFC 9708 (unwrapped) SPKI: + * SEQUENCE { SEQUENCE { OID }, BIT STRING { rawPub } }. */ + private static byte[] spki(byte[] rawPub) { + + byte[] algId = tlv(0x30, HSS_LMS_OID); + byte[] bitString = tlv(0x03, concat(new byte[] { 0x00 }, rawPub)); + + return tlv(0x30, concat(algId, bitString)); + } + + /* Import the RFC 8554 TC1 (SHA-256/256) KAT public key via the given + * KeyFactory. That parameter family can be compiled out of wolfCrypt, + * in which case underlying import throws NOT_COMPILED_IN. Treat that as + * "skip". */ + private static PublicKey importTc1Key(KeyFactory kf) throws Exception { + + try { + return kf.generatePublic( + new X509EncodedKeySpec(spki(RFC8554_TC1_PK))); + + } catch (java.security.spec.InvalidKeySpecException e) { + if (isNotCompiledIn(e)) { + Assume.assumeTrue( + "LMS SHA-256/256 parameter set not compiled in", false); + } + throw e; + } + } + + /* True if the throwable's cause chain contains a wolfCrypt + * NOT_COMPILED_IN error. */ + private static boolean isNotCompiledIn(Throwable t) { + + for (; t != null; t = t.getCause()) { + if (t instanceof WolfCryptException && + ((WolfCryptException) t).getError() == + WolfCryptError.NOT_COMPILED_IN) { + return true; + } + } + + return false; + } + + @Test + public void rfc8554TestCase1JceVerify() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + assertEquals("HSS/LMS", pub.getAlgorithm()); + + assertTrue("RFC 8554 TC1 verify", + verify(pub, RFC8554_TC1_SIG, RFC8554_TC1_MSG)); + assertFalse("RFC 8554 TC1 wrong message", + verify(pub, RFC8554_TC1_SIG, "not the message".getBytes())); + + byte[] tampered = RFC8554_TC1_SIG.clone(); + tampered[tampered.length / 2] ^= (byte) 0xFF; + assertFalse("RFC 8554 TC1 tampered signature", + verify(pub, tampered, RFC8554_TC1_MSG)); + } + + @Test + public void initSignRejected() throws Exception { + assumeEnabled(); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.initSign(stubPrivateKey()); + fail("expected LMS signing to be rejected (verify-only)"); + } catch (InvalidKeyException e) { + /* expected */ + } + } + + @Test + public void updateBeforeInitRejected() throws Exception { + assumeEnabled(); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.update(new byte[] { 0x00 }); + fail("expected update before init to be rejected"); + } catch (SignatureException e) { + /* expected */ + } + } + + @Test + public void setParameterSpecRejected() throws Exception { + assumeEnabled(); + + AlgorithmParameterSpec spec = new AlgorithmParameterSpec() { }; + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.setParameter(spec); + fail("expected AlgorithmParameterSpec to be rejected"); + } catch (InvalidAlgorithmParameterException e) { + /* expected */ + } + } + + @Test + @SuppressWarnings("deprecation") + public void setParameterStringRejected() throws Exception { + assumeEnabled(); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.setParameter("anything", "value"); + fail("expected string parameter to be rejected"); + } catch (InvalidParameterException e) { + /* expected */ + } + } + + @Test + public void initVerifyRejectsMalformedKey() throws Exception { + assumeEnabled(); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.initVerify(stubPublicKey("X.509", new byte[] { 0x01, 0x02 })); + fail("expected malformed public key to be rejected"); + } catch (InvalidKeyException e) { + /* expected */ + } + } + + @Test + public void destroyedKeyRejectedByInitVerify() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + ((javax.security.auth.Destroyable) pub).destroy(); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.initVerify(pub); + fail("expected destroyed key to be rejected"); + } catch (InvalidKeyException e) { + /* expected */ + } + } + + @Test + public void byteAtATimeUpdateMatchesArray() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + s.initVerify(pub); + for (byte b : RFC8554_TC1_MSG) { + s.update(b); + } + assertTrue("byte-at-a-time verify", s.verify(RFC8554_TC1_SIG)); + } + + @Test + public void serializeRoundTripThenVerify() throws Exception { + assumeEnabled(); + + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey pub = importTc1Key(kf); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(pub); + oos.close(); + + ObjectInputStream ois = new ObjectInputStream( + new ByteArrayInputStream(bos.toByteArray())); + PublicKey restored = (PublicKey) ois.readObject(); + ois.close(); + + assertArrayEquals(pub.getEncoded(), restored.getEncoded()); + assertTrue("verify after deserialize", + verify(restored, RFC8554_TC1_SIG, RFC8554_TC1_MSG)); + } + + @Test + @SuppressWarnings("deprecation") + public void getParameterStringRejected() throws Exception { + assumeEnabled(); + + Signature s = Signature.getInstance("LMS", "wolfJCE"); + try { + s.getParameter("anything"); + fail("expected getParameter(String) to be rejected"); + } catch (InvalidParameterException e) { + /* expected */ + } + } + + /* Minimal PublicKey with a chosen format and X.509 encoding. */ + private static PublicKey stubPublicKey(final String format, + final byte[] encoded) { + + return new PublicKey() { + @Override + public String getAlgorithm() { + return "LMS"; + } + @Override + public String getFormat() { + return format; + } + @Override + public byte[] getEncoded() { + return encoded.clone(); + } + }; + } + + /* Minimal PrivateKey for exercising the signing-rejected path. */ + private static java.security.PrivateKey stubPrivateKey() { + + return new java.security.PrivateKey() { + @Override + public String getAlgorithm() { + return "LMS"; + } + @Override + public String getFormat() { + return "PKCS#8"; + } + @Override + public byte[] getEncoded() { + return new byte[] { 0x00 }; + } + }; + } +} diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfJCETestSuite.java b/src/test/java/com/wolfssl/provider/jce/test/WolfJCETestSuite.java index bcb04923..31bb5ce0 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfJCETestSuite.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfJCETestSuite.java @@ -58,6 +58,8 @@ WolfCryptMlKemKemTest.class, WolfCryptXmssSignatureTest.class, WolfCryptXmssKeyFactoryTest.class, + WolfCryptLmsSignatureTest.class, + WolfCryptLmsKeyFactoryTest.class, WolfCryptPKIXCertPathValidatorTest.class, WolfCryptPKIXCertPathBuilderTest.class, WolfCryptPKIXRevocationCheckerTest.class, diff --git a/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java b/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java index b9e8cf7c..43dd3a7e 100644 --- a/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java +++ b/src/test/java/com/wolfssl/provider/jce/test/WolfSSLKeyStoreTest.java @@ -79,6 +79,8 @@ import com.wolfssl.provider.jce.WolfSSLKeyStore; import com.wolfssl.wolfcrypt.FeatureDetect; import com.wolfssl.wolfcrypt.MlDsa; +import com.wolfssl.wolfcrypt.WolfCryptError; +import com.wolfssl.wolfcrypt.WolfCryptException; import com.wolfssl.wolfcrypt.test.TimedTestWatcher; public class WolfSSLKeyStoreTest { @@ -161,6 +163,7 @@ public class WolfSSLKeyStoreTest { private static String mldsa44CertPem = null; private static String mldsa65CertPem = null; private static String mldsa87CertPem = null; + private static String lmsCertDer = null; private static PrivateKey serverKeyMlDsa44 = null; private static PrivateKey serverKeyMlDsa65 = null; private static PrivateKey serverKeyMlDsa87 = null; @@ -616,6 +619,8 @@ public static void testSetupAndProviderInstallation() certPre.concat("examples/certs/mldsa/mldsa65-cert.pem"); mldsa87CertPem = certPre.concat("examples/certs/mldsa/mldsa87-cert.pem"); + lmsCertDer = + certPre.concat("examples/certs/lms/bc_lms_native_bc_root.der"); serverMlDsa44WKS = certPre.concat("examples/certs/server-mldsa44.wks"); serverMlDsa65WKS = @@ -2678,23 +2683,32 @@ public void testKekCacheDisabledByDefault() throws Exception { store.setKeyEntry("rsaKey", serverKeyRsa, storePass.toCharArray(), new Certificate[] { serverCertRsa }); - /* Time first getKey() */ - long start = System.currentTimeMillis(); + /* Warm up the getKey() path once before timing. */ + store.getKey("rsaKey", storePass.toCharArray()); + + /* Time first getKey(). Use nanoTime() (monotonic, high-resolution) + * rather than currentTimeMillis() (wall-clock, coarse). */ + long start = System.nanoTime(); Key key1 = store.getKey("rsaKey", storePass.toCharArray()); - long first = System.currentTimeMillis() - start; + long first = System.nanoTime() - start; /* Time second getKey() - should be similar since cache is disabled */ - start = System.currentTimeMillis(); + start = System.nanoTime(); Key key2 = store.getKey("rsaKey", storePass.toCharArray()); - long second = System.currentTimeMillis() - start; + long second = System.nanoTime() - start; assertNotNull(key1); assertNotNull(key2); - /* Without cache, both should be slow (within 30% of each other) */ - assertTrue("Without cache, times should be similar (first=" + - first + "ms, second=" + second + "ms)", - second > first * 0.3); + /* Without a cache both calls re-derive the KEK (PBKDF2), so the second + * should not be dramatically faster than the first (within ~30%). */ + long firstMs = first / 1000000L; + if (firstMs > 5) { + long secondMs = second / 1000000L; + assertTrue("Without cache, times should be similar (first=" + + firstMs + "ms, second=" + secondMs + "ms)", + second > first * 0.3); + } } @Test @@ -3454,5 +3468,83 @@ public void testLoadMlDsaCaWKSFromFile() throws Exception { expectedCerts[i], out); } } + + /** + * Store an LMS/HSS-keyed X.509 certificate (from native wolfSSL + * certs/lms/) in a WKS KeyStore as a trusted certificate entry, reload it, + * and confirm it round-trips. Also confirms wolfJCE can import the + * certificate LMS public key. wolfJCE WKS does not store stateful LMS + * private keys, so only the certificate/public-key path is exercised. + */ + @Test + public void testLmsCertificateRoundTrip() throws Exception { + + Assume.assumeTrue("LMS not compiled in", FeatureDetect.LmsEnabled()); + + Assume.assumeTrue("LMS test certificate not present", + new File(lmsCertDer).exists()); + + Certificate lmsCert = certFileToCertificate(lmsCertDer); + + KeyStore store = KeyStore.getInstance("WKS", "wolfJCE"); + store.load(null, storePass.toCharArray()); + store.setCertificateEntry("lms-root", lmsCert); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + store.store(bos, storePass.toCharArray()); + + KeyStore reloaded = KeyStore.getInstance("WKS", "wolfJCE"); + reloaded.load(new ByteArrayInputStream(bos.toByteArray()), + storePass.toCharArray()); + + Certificate got = reloaded.getCertificate("lms-root"); + assertNotNull("LMS certificate missing after reload", got); + assertTrue("LMS entry should be a certificate entry", + reloaded.isCertificateEntry("lms-root")); + assertArrayEquals("LMS certificate did not round-trip", + lmsCert.getEncoded(), got.getEncoded()); + + /* wolfJCE can import the certificate's LMS/HSS public key. The public + * key materialization is guarded because older JDKs (< 21) cannot + * expose an HSS/LMS certificate public key. */ + PublicKey certPub = null; + try { + certPub = got.getPublicKey(); + } catch (RuntimeException e) { + certPub = null; + } + if (certPub != null && certPub.getEncoded() != null) { + KeyFactory kf = KeyFactory.getInstance("LMS", "wolfJCE"); + PublicKey wolfPub = null; + try { + wolfPub = kf.generatePublic( + new X509EncodedKeySpec(certPub.getEncoded())); + } catch (InvalidKeySpecException e) { + /* Test certificate uses an LMS SHA-256/256 parameter set, + * which native wolfCrypt may be built without. Treat that as + * a skip. */ + if (isNotCompiledIn(e)) { + Assume.assumeTrue( + "LMS SHA-256/256 parameter set not compiled in", false); + } + throw e; + } + assertEquals("HSS/LMS", wolfPub.getAlgorithm()); + } + } + + /* True if throwable cause chain contains NOT_COMPILED_IN. */ + private static boolean isNotCompiledIn(Throwable t) { + + for (; t != null; t = t.getCause()) { + if (t instanceof WolfCryptException && + ((WolfCryptException) t).getError() == + WolfCryptError.NOT_COMPILED_IN) { + return true; + } + } + + return false; + } } diff --git a/src/test/java/com/wolfssl/wolfcrypt/test/LmsTest.java b/src/test/java/com/wolfssl/wolfcrypt/test/LmsTest.java new file mode 100644 index 00000000..0e98be0b --- /dev/null +++ b/src/test/java/com/wolfssl/wolfcrypt/test/LmsTest.java @@ -0,0 +1,437 @@ +/* LmsTest.java + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package com.wolfssl.wolfcrypt.test; + +import static org.junit.Assert.*; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.TestRule; + +import com.wolfssl.wolfcrypt.Lms; +import com.wolfssl.wolfcrypt.FeatureDetect; +import com.wolfssl.wolfcrypt.WolfCryptError; +import com.wolfssl.wolfcrypt.WolfCryptException; + +/** + * JNI-level wolfCrypt LMS/HSS (RFC 8554) tests. wolfJCE LMS is verify-only + * these are RFC 8554 / RFC 9858 KATs. + */ +public class LmsTest { + + private static boolean lmsEnabled = false; + + @Rule(order = Integer.MIN_VALUE) + public TestRule testWatcher = TimedTestWatcher.create(); + + @BeforeClass + public static void checkAvailability() { + + lmsEnabled = FeatureDetect.LmsEnabled(); + + if (lmsEnabled) { + System.out.println("JNI Lms Class"); + } + else { + System.out.println("LMS test skipped: not compiled in"); + } + } + + private void assumeEnabled() { + Assume.assumeTrue("LMS not compiled in", lmsEnabled); + } + + /* RFC 8554 / RFC 9858 known-answer verify vectors. + * + * Independent verify KATs over the RFC-published test vectors, the same + * vectors the JDK SUN provider uses to test its verify-only HSS/LMS + * implementation. These exercise the verify-only path (new Lms() + + * importPublicRaw() + verify()). + * + * RFC 8554 Appendix F Test Case 1 (HSS L2, SHA256 H5/W8) + * RFC 9858 Appendix A.1 (LMS_SHA256_M24_H5, W8) + * RFC 9858 Appendix A.2 (LMS_SHAKE_M24_H5, W8) + * RFC 9858 Appendix A.3 (LMS_SHAKE_M32_H5, W8) + * RFC 9858 Appendix A.4 (LMS_SHA256_M24_H20, W4) + * + * RFC 8554 Test Case 2 is intentionally omitted: it uses heterogeneous + * per-level parameters (top tree H10/W4, bottom tree H5/W8), which native + * wolfCrypt's HSS verify does not support (it returns BUFFER_E). Test + * Case 1 and every RFC 9858 vector use one parameter set across all + * levels. */ + + /* Short local alias of Util.h2b() to keep KAT declarations readable. */ + private static byte[] hex(String s) { + return Util.h2b(s); + } + + private static final byte[] RFC8554_TC1_PK = hex( + "00000002000000050000000461a5d57d37f5e46bfb7520806b07a1b850650e3b" + + "31fe4a773ea29a07f09cf2ea30e579f0df58ef8e298da0434cb2b878"); + + private static final byte[] RFC8554_TC1_MSG = hex( + "54686520706f77657273206e6f742064656c65676174656420746f2074686520" + + "556e69746564205374617465732062792074686520436f6e737469747574696f" + + "6e2c206e6f722070726f6869626974656420627920697420746f207468652053" + + "74617465732c2061726520726573657276656420746f20746865205374617465" + + "7320726573706563746976656c792c206f7220746f207468652070656f706c65" + + "2e0a"); + + private static final byte[] RFC8554_TC1_SIG = hex( + "000000010000000500000004d32b56671d7eb98833c49b433c272586bc4a1c8a" + + "8970528ffa04b966f9426eb9965a25bfd37f196b9073f3d4a232feb69128ec45" + + "146f86292f9dff9610a7bf95a64c7f60f6261a62043f86c70324b7707f5b4a8a" + + "6e19c114c7be866d488778a0e05fd5c6509a6e61d559cf1a77a970de927d60c7" + + "0d3de31a7fa0100994e162a2582e8ff1b10cd99d4e8e413ef469559f7d7ed12c" + + "838342f9b9c96b83a4943d1681d84b15357ff48ca579f19f5e71f18466f2bbef" + + "4bf660c2518eb20de2f66e3b14784269d7d876f5d35d3fbfc7039a462c716bb9" + + "f6891a7f41ad133e9e1f6d9560b960e7777c52f060492f2d7c660e1471e07e72" + + "655562035abc9a701b473ecbc3943c6b9c4f2405a3cb8bf8a691ca51d3f6ad2f" + + "428bab6f3a30f55dd9625563f0a75ee390e385e3ae0b906961ecf41ae073a059" + + "0c2eb6204f44831c26dd768c35b167b28ce8dc988a3748255230cef99ebf14e7" + + "30632f27414489808afab1d1e783ed04516de012498682212b07810579b25036" + + "5941bcc98142da13609e9768aaf65de7620dabec29eb82a17fde35af15ad238c" + + "73f81bdb8dec2fc0e7f932701099762b37f43c4a3c20010a3d72e2f606be108d" + + "310e639f09ce7286800d9ef8a1a40281cc5a7ea98d2adc7c7400c2fe5a101552" + + "df4e3cccfd0cbf2ddf5dc6779cbbc68fee0c3efe4ec22b83a2caa3e48e0809a0" + + "a750b73ccdcf3c79e6580c154f8a58f7f24335eec5c5eb5e0cf01dcf44394240" + + "95fceb077f66ded5bec73b27c5b9f64a2a9af2f07c05e99e5cf80f00252e39db" + + "32f6c19674f190c9fbc506d826857713afd2ca6bb85cd8c107347552f30575a5" + + "417816ab4db3f603f2df56fbc413e7d0acd8bdd81352b2471fc1bc4f1ef296fe" + + "a1220403466b1afe78b94f7ecf7cc62fb92be14f18c2192384ebceaf8801afdf" + + "947f698ce9c6ceb696ed70e9e87b0144417e8d7baf25eb5f70f09f016fc925b4" + + "db048ab8d8cb2a661ce3b57ada67571f5dd546fc22cb1f97e0ebd1a65926b123" + + "4fd04f171cf469c76b884cf3115cce6f792cc84e36da58960c5f1d760f32c12f" + + "aef477e94c92eb75625b6a371efc72d60ca5e908b3a7dd69fef0249150e3eebd" + + "fed39cbdc3ce9704882a2072c75e13527b7a581a556168783dc1e97545e31865" + + "ddc46b3c957835da252bb7328d3ee2062445dfb85ef8c35f8e1f3371af34023c" + + "ef626e0af1e0bc017351aae2ab8f5c612ead0b729a1d059d02bfe18efa971b73" + + "00e882360a93b025ff97e9e0eec0f3f3f13039a17f88b0cf808f488431606cb1" + + "3f9241f40f44e537d302c64a4f1f4ab949b9feefadcb71ab50ef27d6d6ca8510" + + "f150c85fb525bf25703df7209b6066f09c37280d59128d2f0f637c7d7d7fad4e" + + "d1c1ea04e628d221e3d8db77b7c878c9411cafc5071a34a00f4cf07738912753" + + "dfce48f07576f0d4f94f42c6d76f7ce973e9367095ba7e9a3649b7f461d9f9ac" + + "1332a4d1044c96aefee67676401b64457c54d65fef6500c59cdfb69af7b6dddf" + + "cb0f086278dd8ad0686078dfb0f3f79cd893d314168648499898fbc0ced5f95b" + + "74e8ff14d735cdea968bee7400000005d8b8112f9200a5e50c4a262165bd342c" + + "d800b8496810bc716277435ac376728d129ac6eda839a6f357b5a04387c5ce97" + + "382a78f2a4372917eefcbf93f63bb59112f5dbe400bd49e4501e859f885bf073" + + "6e90a509b30a26bfac8c17b5991c157eb5971115aa39efd8d564a6b90282c316" + + "8af2d30ef89d51bf14654510a12b8a144cca1848cf7da59cc2b3d9d0692dd2a2" + + "0ba3863480e25b1b85ee860c62bf51360000000500000004d2f14ff6346af964" + + "569f7d6cb880a1b66c5004917da6eafe4d9ef6c6407b3db0e5485b122d9ebe15" + + "cda93cfec582d7ab0000000a000000040703c491e7558b35011ece3592eaa5da" + + "4d918786771233e8353bc4f62323185c95cae05b899e35dffd71705470620998" + + "8ebfdf6e37960bb5c38d7657e8bffeef9bc042da4b4525650485c66d0ce19b31" + + "7587c6ba4bffcc428e25d08931e72dfb6a120c5612344258b85efdb7db1db9e1" + + "865a73caf96557eb39ed3e3f426933ac9eeddb03a1d2374af7bf771855774562" + + "37f9de2d60113c23f846df26fa942008a698994c0827d90e86d43e0df7f4bfcd" + + "b09b86a373b98288b7094ad81a0185ac100e4f2c5fc38c003c1ab6fea479eb2f" + + "5ebe48f584d7159b8ada03586e65ad9c969f6aecbfe44cf356888a7b15a3ff07" + + "4f771760b26f9c04884ee1faa329fbf4e61af23aee7fa5d4d9a5dfcf43c4c26c" + + "e8aea2ce8a2990d7ba7b57108b47dabfbeadb2b25b3cacc1ac0cef346cbb90fb" + + "044beee4fac2603a442bdf7e507243b7319c9944b1586e899d431c7f91bcccc8" + + "690dbf59b28386b2315f3d36ef2eaa3cf30b2b51f48b71b003dfb08249484201" + + "043f65f5a3ef6bbd61ddfee81aca9ce60081262a00000480dcbc9a3da6fbef5c" + + "1c0a55e48a0e729f9184fcb1407c31529db268f6fe50032a363c9801306837fa" + + "fabdf957fd97eafc80dbd165e435d0e2dfd836a28b354023924b6fb7e48bc0b3" + + "ed95eea64c2d402f4d734c8dc26f3ac591825daef01eae3c38e3328d00a77dc6" + + "57034f287ccb0f0e1c9a7cbdc828f627205e4737b84b58376551d44c12c3c215" + + "c812a0970789c83de51d6ad787271963327f0a5fbb6b5907dec02c9a90934af5" + + "a1c63b72c82653605d1dcce51596b3c2b45696689f2eb382007497557692caac" + + "4d57b5de9f5569bc2ad0137fd47fb47e664fcb6db4971f5b3e07aceda9ac130e" + + "9f38182de994cff192ec0e82fd6d4cb7f3fe00812589b7a7ce51544045643301" + + "6b84a59bec6619a1c6c0b37dd1450ed4f2d8b584410ceda8025f5d2d8dd0d217" + + "6fc1cf2cc06fa8c82bed4d944e71339ece780fd025bd41ec34ebff9d4270a322" + + "4e019fcb444474d482fd2dbe75efb20389cc10cd600abb54c47ede93e08c114e" + + "db04117d714dc1d525e11bed8756192f929d15462b939ff3f52f2252da2ed64d" + + "8fae88818b1efa2c7b08c8794fb1b214aa233db3162833141ea4383f1a6f120b" + + "e1db82ce3630b3429114463157a64e91234d475e2f79cbf05e4db6a9407d72c6" + + "bff7d1198b5c4d6aad2831db61274993715a0182c7dc8089e32c8531deed4f74" + + "31c07c02195eba2ef91efb5613c37af7ae0c066babc69369700e1dd26eddc0d2" + + "16c781d56e4ce47e3303fa73007ff7b949ef23be2aa4dbf25206fe45c20dd888" + + "395b2526391a724996a44156beac808212858792bf8e74cba49dee5e8812e019" + + "da87454bff9e847ed83db07af313743082f880a278f682c2bd0ad6887cb59f65" + + "2e155987d61bbf6a88d36ee93b6072e6656d9ccbaae3d655852e38deb3a2dcf8" + + "058dc9fb6f2ab3d3b3539eb77b248a661091d05eb6e2f297774fe6053598457c" + + "c61908318de4b826f0fc86d4bb117d33e865aa805009cc2918d9c2f840c4da43" + + "a703ad9f5b5806163d7161696b5a0adc00000005d5c0d1bebb06048ed6fe2ef2" + + "c6cef305b3ed633941ebc8b3bec9738754cddd60e1920ada52f43d055b5031ce" + + "e6192520d6a5115514851ce7fd448d4a39fae2ab2335b525f484e9b40d6a4a96" + + "9394843bdcf6d14c48e8015e08ab92662c05c6e9f90b65a7a6201689999f32bf" + + "d368e5e3ec9cb70ac7b8399003f175c40885081a09ab3034911fe125631051df" + + "0408b3946b0bde790911e8978ba07dd56c73e7ee"); + + private static final byte[] RFC9858_A1_PK = hex( + "000000010000000a00000008202122232425262728292a2b2c2d2e2f2c571450" + + "aed99cfb4f4ac285da14882796618314508b12d2"); + + private static final byte[] RFC9858_A1_MSG = hex( + "54657374206d65737361676520666f72205348413235362d3139320a"); + + private static final byte[] RFC9858_A1_SIG = hex( + "0000000000000005000000080b5040a18c1b5cabcbc85b047402ec6294a30dd8" + + "da8fc3dae13b9f0875f09361dc77fcc4481ea463c073716249719193614b835b" + + "4694c059f12d3aedd34f3db93f3580fb88743b8b3d0648c0537b7a50e433d7ea" + + "9d6672fffc5f42770feab4f98eb3f3b23fd2061e4d0b38f832860ae76673ad1a" + + "1a52a9005dcf1bfb56fe16ff723627612f9a48f790f3c47a67f870b81e919d99" + + "919c8db48168838cece0abfb683da48b9209868be8ec10c63d8bf80d36498dfc" + + "205dc45d0dd870572d6d8f1d90177cf5137b8bbf7bcb67a46f86f26cfa5a44cb" + + "caa4e18da099a98b0b3f96d5ac8ac375d8da2a7c248004ba11d7ac775b921835" + + "9cddab4cf8ccc6d54cb7e1b35a36ddc9265c087063d2fc6742a7177876476a32" + + "4b03295bfed99f2eaf1f38970583c1b2b616aad0f31cd7a4b1bb0a51e477e94a" + + "01bbb4d6f8866e2528a159df3d6ce244d2b6518d1f0212285a3c2d4a927054a1" + + "e1620b5b02aab0c8c10ed48ae518ea73cba81fcfff88bff461dac51e7ab4ca75" + + "f47a6259d24820b9995792d139f61ae2a8186ae4e3c9bfe0af2cc717f424f41a" + + "a67f03faedb0665115f2067a46843a4cbbd297d5e83bc1aafc18d1d03b3d894e" + + "8595a6526073f02ab0f08b99fd9eb208b59ff6317e5545e6f9ad5f9c183abd04" + + "3d5acd6eb2dd4da3f02dbc3167b468720a4b8b92ddfe7960998bb7a0ecf2a26a" + + "37598299413f7b2aecd39a30cec527b4d9710c4473639022451f50d01c045712" + + "5da0fa4429c07dad859c846cbbd93ab5b91b01bc770b089cfede6f651e86dd7c" + + "15989c8b5321dea9ca608c71fd862323072b827cee7a7e28e4e2b999647233c3" + + "456944bb7aef9187c96b3f5b79fb98bc76c3574dd06f0e95685e5b3aef3a54c4" + + "155fe3ad817749629c30adbe897c4f4454c86c490000000ae9ca10eaa811b22a" + + "e07fb195e3590a334ea64209942fbae338d19f152182c807d3c40b189d3fcbea" + + "942f44682439b191332d33ae0b761a2a8f984b56b2ac2fd4ab08223a69ed1f77" + + "19c7aa7e9eee96504b0e60c6bb5c942d695f0493eb25f80a5871cffd131d0e04" + + "ffe5065bc7875e82d34b40b69dd9f3c1"); + + private static final byte[] RFC9858_A2_PK = hex( + "000000010000001400000010505152535455565758595a5b5c5d5e5fdb54a450" + + "9901051c01e26d9990e550347986da87924ff0b1"); + + private static final byte[] RFC9858_A2_MSG = hex( + "54657374206d65737361676520666f72205348414b453235362d3139320a"); + + private static final byte[] RFC9858_A2_SIG = hex( + "00000000000000060000001084219da9ce9fffb16edb94527c6d10565587db28" + + "062deac4208e62fc4fbe9d85deb3c6bd2c01640accb387d8a6093d68511234a6" + + "a1a50108091c034cb1777e02b5df466149a66969a498e4200c0a0c1bf5d100cd" + + "b97d2dd40efd3cada278acc5a570071a043956112c6deebd1eb3a7b56f5f6791" + + "515a7b5ffddb0ec2d9094bfbc889ea15c3c7b9bea953efb75ed648f535b9acab" + + "66a2e9631e426e4e99b733caa6c55963929b77fec54a7e703d8162e736875cb6" + + "a455d4a9015c7a6d8fd5fe75e402b47036dc3770f4a1dd0a559cb478c7fb1726" + + "005321be9d1ac2de94d731ee4ca79cff454c811f46d11980909f047b2005e84b" + + "6e15378446b1ca691efe491ea98acc9d3c0f785caba5e2eb3c306811c240ba22" + + "802923827d582639304a1e9783ba5bc9d69d999a7db8f749770c3c04a152856d" + + "c726d8067921465b61b3f847b13b2635a45379e5adc6ff58a99b00e60ac767f7" + + "f30175f9f7a140257e218be307954b1250c9b41902c4fa7c90d8a592945c66e8" + + "6a76defcb84500b55598a1990faaa10077c74c94895731585c8f900de1a1c675" + + "bd8b0c180ebe2b5eb3ef8019ece3e1ea7223eb7906a2042b6262b4aa25c4b8a0" + + "5f205c8befeef11ceff1282508d71bc2a8cfa0a99f73f3e3a74bb4b3c0d8ca2a" + + "bd0e1c2c17dafe18b4ee2298e87bcfb1305b3c069e6d385569a4067ed547486d" + + "d1a50d6f4a58aab96e2fa883a9a39e1bd45541eee94efc32faa9a94be66dc853" + + "8b2dab05aee5efa6b3b2efb3fd020fe789477a93afff9a3e636dbba864a5bffa" + + "3e28d13d49bb597d94865bde88c4627f206ab2b465084d6b780666e952f8710e" + + "fd748bd0f1ae8f1035087f5028f14affcc5fffe332121ae4f87ac5f1eac90626" + + "08c7d87708f1723f38b23237a4edf4b49a5cd3d700000014dd4bdc8f928fb526" + + "f6fb7cdb944a7ebaa7fb05d995b5721a27096a5007d82f79d063acd434a04e97" + + "f61552f7f81a9317b4ec7c87a5ed10c881928fc6ebce6dfce9daae9cc9dba690" + + "7ca9a9dd5f9f573704d5e6cf22a43b04e64c1ffc7e1c442ecb495ba265f465c5" + + "6291a902e62a461f6dfda232457fad14"); + + private static final byte[] RFC9858_A3_PK = hex( + "000000010000000f0000000c808182838485868788898a8b8c8d8e8f9bb7faee" + + "411cae806c16a466c3191a8b65d0ac31932bbf0c2d07c7a4a36379fe"); + + private static final byte[] RFC9858_A3_MSG = hex( + "54657374206d657361676520666f72205348414b453235362d3235360a"); + + private static final byte[] RFC9858_A3_SIG = hex( + "00000000000000070000000cb82709f0f00e83759190996233d1ee4f4ec50534" + + "473c02ffa145e8ca2874e32b16b228118c62b96c9c77678b33183730debaade8" + + "fe607f05c6697bc971519a341d69c00129680b67e75b3bd7d8aa5c8b71f02669" + + "d177a2a0eea896dcd1660f16864b302ff321f9c4b8354408d06760504f768ebd" + + "4e545a9b0ac058c575078e6c1403160fb45450d61a9c8c81f6bd69bdfa26a16e" + + "12a265baf79e9e233eb71af634ecc66dc88e10c6e0142942d4843f70a0242727" + + "bc5a2aabf7b0ec12a99090d8caeef21303f8ac58b9f200371dc9e41ab956e1a3" + + "efed9d4bbb38975b46c28d5f5b3ed19d847bd0a737177263cbc1a2262d40e808" + + "15ee149b6cce2714384c9b7fceb3bbcbd25228dda8306536376f8793ecadd602" + + "0265dab9075f64c773ef97d07352919995b74404cc69a6f3b469445c9286a6b2" + + "c9f6dc839be76618f053de763da3571ef70f805c9cc54b8e501a98b98c70785e" + + "eb61737eced78b0e380ded4f769a9d422786def59700eef3278017babbe5f906" + + "3b468ae0dd61d94f9f99d5cc36fbec4178d2bda3ad31e1644a2bcce208d72d50" + + "a7637851aa908b94dc4376120d5beab0fb805e1945c41834dd6085e6db1a3aa7" + + "8fcb59f62bde68236a10618cff123abe64dae8dabb2e84ca705309c2ab986d4f" + + "8326ba0642272cb3904eb96f6f5e3bb8813997881b6a33cac0714e4b5e7a882a" + + "d87e141931f97d612b84e903e773139ae377f5ba19ac86198d485fca97742568" + + "f6ff758120a89bf19059b8a6bfe2d86b12778164436ab2659ba866767fcc4355" + + "84125fb7924201ee67b535daf72c5cb31f5a0b1d926324c26e67d4c3836e301a" + + "a09bae8fb3f91f1622b1818ccf440f52ca9b5b9b99aba8a6754aae2b967c4954" + + "fa85298ad9b1e74f27a46127c36131c8991f0cc2ba57a15d35c91cf8bc48e8e2" + + "0d625af4e85d8f9402ec44afbd4792b924b839332a64788a7701a30094b9ec4b" + + "9f4b648f168bf457fbb3c9594fa87920b645e42aa2fecc9e21e000ca7d3ff914" + + "e15c40a8bc533129a7fd39529376430f355aaf96a0a13d13f2419141b3cc2584" + + "3e8c90d0e551a355dd90ad770ea7255214ce11238605de2f000d200104d0c3a3" + + "e35ae64ea10a3eff37ac7e9549217cdf52f307172e2f6c7a2a4543e143140365" + + "25b1ad53eeaddf0e24b1f36914ed22483f2889f61e62b6fb78f5645bdbb02c9e" + + "5bf97db7a0004e87c2a55399b61958786c97bd52fa199c27f6bb4d68c4907933" + + "562755bfec5d4fb52f06c289d6e852cf6bc773ffd4c07ee2d6cc55f57edcfbc8" + + "e8692a49ad47a121fe3c1b16cab1cc285faf6793ffad7a8c341a49c5d2dce706" + + "9e464cb90a00b2903648b23c81a68e21d748a7e7b1df8a593f3894b2477e8316" + + "947ca725d141135202a9442e1db33bbd390d2c04401c39b253b78ce297b0e147" + + "55e46ec08a146d279c67af70de256890804d83d6ec5ca3286f1fca9c72abf6ef" + + "868e7f6eb0fddda1b040ecec9bbc69e2fd8618e9db3bdb0af13dda06c6617e95" + + "afa522d6a2552de15324d99119f55e9af11ae3d5614b564c642dbfec6c644198" + + "ce80d2433ac8ee738f9d825e0000000f71d585a35c3a908379f4072d070311db" + + "5d65b242b714bc5a756ba5e228abfa0d1329978a05d5e815cf4d74c1e547ec4a" + + "a3ca956ae927df8b29fb9fab3917a7a4ae61ba57e5342e9db12caf6f6dbc5253" + + "de5268d4b0c4ce4ebe6852f012b162fc1c12b9ffc3bcb1d3ac8589777655e22c" + + "d9b99ff1e4346fd0efeaa1da044692e7ad6bfc337db69849e54411df8920c228" + + "a2b7762c11e4b1c49efb74486d3931ea"); + + private static final byte[] RFC9858_A4_PK = hex( + "000000010000000d00000007404142434445464748494a4b4c4d4e4f9c08a50d" + + "170406869892802ee4142fcdeac990f110c2460c"); + + private static final byte[] RFC9858_A4_MSG = hex( + "54657374206d65737361676520666f72205348413235362f31393220773d34"); + + private static final byte[] RFC9858_A4_SIG = hex( + "000000000000006400000007853fa6e1a65fef076acd2485505b93be9aeb2641" + + "e3d3805c1887f26f4bcdb6ac0337b76fa5d6603834287e010b20516f7c336df2" + + "134c0a981f1ec2bb7baee516e91e67d3bd16c8d945a7f2be4fd84a604ae3743e" + + "fc609ee0e69572e9c6d4a68250e877b75d3cae63e9d5c15a32bb3cd17045f6b3" + + "e195284fdd1ee3cfbe18f1cbd06ef3e7af34b1844d42dac453115a4507ed525c" + + "ec120d054b403c61a7e5034fac4be6ef5412d194d4b6bbc0ae6cd3fe9993d583" + + "ee06f4030bc832efec24d1f713f5088731b91a98491fa3adf1b322bce26df24c" + + "8415e3a46bdfe07a6fd48e6d951515758cd6434991098bf6949249fca338ec23" + + "5871dd564998d07d9b1b1b8d644e657fee8039da8fe195d129faddb12d543b86" + + "b0ab8cf6f26c121783f3b828d03f793b42909272f688e4ef6d46e82bdd1a02b1" + + "ff86c3b79920b2e6f19faf75c623242f1f2c549f84fb2f4c3ffead3120d97bae" + + "a507467bb2da79f132bbe15b596fdfcb70983107ebca2597de9d55bd83bcae5c" + + "28a85259dadb354859986e60c8afa0b10bd08a8f9ed9b1ede3377075fe0ae363" + + "49f7d2ed7bfc9ece0d4cd6972059329419feaf3b9a1045b6cfa4ae89b1cea895" + + "0aea4af870d1a3a3909ebc5a3013d6deb927abc0f95093e83cb36a9c1d6f13ad" + + "d19268ac7a0371f8335b0952a57fdb0141d55d937dd6ebb08fee8a5cf426ac97" + + "d54ee7aa17e6c57be5e62a52a6b1b986730d3a3aad8a7d327ddf883e6bc7b636" + + "eb2a5c4f2a635ae5bada5418d43dfedb69c0a0209334fac89d420d6ad5a2e1df" + + "95d26a1bfeb99a5e8455061bfdf2d6e8394caf8a4be699b8afa38e524d405333" + + "0af478f85bf33d3ca3a35bc96987282bd513a8f6a52db9ba36aa90882b3bf573" + + "fa275449d8d49eb30bed2bb17a0ecc7d8a20807f2ea3dd37acd46c713cc2ac9d" + + "01a20a30d6832eef86a1e26d1cad7761bf4130a6565572766026509deeddaf46" + + "b605452b218a4e137a7ce063b546a35c52510f0ea2cac879192ec443e43b37c5" + + "ffa23da7a7fc254324a3de705c771794f10ea356e5a747e5146fd804a4771980" + + "3c185b380e34b8dcc8269c2b073d86b2307cf90c6c3ef9271f2d53df2579f0c4" + + "cfb632db37a9025965f70b4616673228e98644be6576417b7a97f104350259e7" + + "f697408cdf8cf81a3e7741626ccdb87ad8531264cb5ceb7c8c097cec505091a3" + + "ee3a826c54f78169abc2e7d0a318dac10250ba940e51e79a3f572fb32bf442be" + + "6fd81267946e6387f9a8c705d945c653f2684655e3fa6b9ee311d8a091bef989" + + "8292fa272fb8761f066c23d87aa10d67871cc5419c843b796855c51ad1272e92" + + "64acd2035a82b12c2ddbc85adfcd7c22366a36495349391dbf0001064b8f6b28" + + "365445d733e48f1b058a6cb3e71bbb8df3e90406299894f4ca682943ceeba410" + + "b33b07716ffc18d6eab75f2d6372f1133605fa3c3ed66f2d8f7c5abe59e87d45" + + "00965e347523d73cb356c144827aaa22b1c72a15293c7400e02aaefcf36f68a8" + + "246900e6e6228e7ad19d1450c23434f1e45043dc2b6db57f20d8f5b344d4162a" + + "a651333287cd8bf8fac41c78d61fe2929209bfe2dc5a2f80205c043b22e540a2" + + "9f0ea0a5ff529e55bf1dfe4296fc4bb4ac2e875322ab115db479fe979d64f784" + + "09af4ec3ad3b758fff83af1b9c48e90ca39366f426c2fb921df55c72786a9217" + + "723945a1ac1a66af7def4f8b367001732cce0e5bac91ac9d603807f8bab105b4" + + "6d315d4cb88feb1c8686884b0000000d13d1a8ef00c5811c15c4d774fdcf7515" + + "5315aff53ebdff8fb6a54f12c165963dd5690cc9842b0e2190afc5443497584c" + + "832155599d00aced84bb3b59170396f7db4fa84aa8577f76cf9367d6e99d3d5b" + + "e3555d7156b004f2002f505681b1ad229b9b46a666672aa8ee662c3a0456a9ad" + + "da7a44fbaca46789577dcd36dc5cdff34b864d0a32492a0acbcaa6c011748f20" + + "5b91ab2ab84f2333fb3e3b9acaecdac38b58aa5f32e718e225631ed6674cccb8" + + "c119acbd4992ab3130a6e912deec59835ab52fbc549430f8b403e4a2a51cc7f4" + + "6fc143d365763aa1708fd25bcd657a790e54718d970906242a3b8a97dff18e91" + + "a44c4ba818a8dd2d242251265b023b826077eb740f6682e6c4ada2b85a67988d" + + "406132c2ad899099e44cfe610c3a5af70b406224411a59597e5dda0f31cd16c9" + + "14b67e96141661f0074f43eb02273481bc324ded26c64f2388559d8c8bd0ef8b" + + "34ca4afebfac2a689b4246c264241488dcf922350dc44f7bc09d57dc1126291b" + + "2318810e0f44801c071e572fd032c780f44c9503a4c03c37417dc96422ba0849" + + "c37956f9fd5d33ea4fcab84276effec652ca77d7d47ac93c633d99e0a236f03d" + + "5587d1990ffaef737fced1f5cdd8f373844e9f316aad41a0b12302639f83a2d7" + + "4c9fe30d305a942bc0c30352a5e44dfb"); + + /* Import an RFC public-key vector and assert it verifies its message, + * rejects a tampered signature, and rejects a different message. Skips + * when the vector's hash family is not compiled into native wolfCrypt. */ + private void assertRfcVerifyKat(String name, byte[] pub, byte[] msg, + byte[] sig, int levels, int height, int winternitz, int hashType) { + + Lms v = new Lms(); + try { + try { + v.importPublicRaw(pub); + } catch (WolfCryptException e) { + if (e.getError() == WolfCryptError.NOT_COMPILED_IN) { + Assume.assumeTrue( + name + ": hash family not compiled in", false); + } + throw e; + } + + assertEquals(name + " levels", levels, v.getLevels()); + assertEquals(name + " height", height, v.getHeight()); + assertEquals(name + " winternitz", winternitz, v.getWinternitz()); + assertEquals(name + " hashType", hashType, v.getHashType()); + + assertTrue(name + " verify", v.verify(sig, msg)); + assertFalse(name + " wrong message", + v.verify(sig, "not the signed message".getBytes())); + + byte[] tampered = sig.clone(); + tampered[tampered.length / 2] ^= (byte) 0xFF; + assertFalse(name + " tampered signature", v.verify(tampered, msg)); + } finally { + v.releaseNativeStruct(); + } + } + + @Test + public void rfc8554TestCase1Verify() { + assumeEnabled(); + assertRfcVerifyKat("RFC8554-TC1", RFC8554_TC1_PK, RFC8554_TC1_MSG, + RFC8554_TC1_SIG, 2, 5, 8, Lms.LMS_SHA256); + } + + @Test + public void rfc9858A1Verify() { + assumeEnabled(); + assertRfcVerifyKat("RFC9858-A.1", RFC9858_A1_PK, RFC9858_A1_MSG, + RFC9858_A1_SIG, 1, 5, 8, Lms.LMS_SHA256_192); + } + + @Test + public void rfc9858A2Verify() { + assumeEnabled(); + assertRfcVerifyKat("RFC9858-A.2", RFC9858_A2_PK, RFC9858_A2_MSG, + RFC9858_A2_SIG, 1, 5, 8, Lms.LMS_SHAKE256_192); + } + + @Test + public void rfc9858A3Verify() { + assumeEnabled(); + assertRfcVerifyKat("RFC9858-A.3", RFC9858_A3_PK, RFC9858_A3_MSG, + RFC9858_A3_SIG, 1, 5, 8, Lms.LMS_SHAKE256); + } + + @Test + public void rfc9858A4Verify() { + assumeEnabled(); + assertRfcVerifyKat("RFC9858-A.4", RFC9858_A4_PK, RFC9858_A4_MSG, + RFC9858_A4_SIG, 1, 20, 4, Lms.LMS_SHA256_192); + } +} diff --git a/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java b/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java index c9329447..f5b7b9ae 100644 --- a/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java +++ b/src/test/java/com/wolfssl/wolfcrypt/test/WolfCryptTestSuite.java @@ -56,6 +56,7 @@ MlDsaTest.class, MlKemTest.class, XmssTest.class, + LmsTest.class, WolfObjectTest.class, WolfSSLCertManagerOCSPTest.class, WolfSSLX509StoreCtxTest.class,