Skip to content

Commit 7133aa6

Browse files
committed
LMS: move the JCE bindings to org.bouncycastle.jcajce.provider.asymmetric.lms, promote the key interfaces to org.bouncycastle.jcajce.interfaces and the key generation parameter specs to org.bouncycastle.jcajce.spec, and leave the org.bouncycastle.pqc.jcajce copies in place, deprecated and still accepted.
1 parent db49249 commit 7133aa6

29 files changed

Lines changed: 1133 additions & 130 deletions

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Date: 2026, TBD
5252

5353
- The promoted LMS private key (org.bouncycastle.crypto.params.LMSPrivateKeyParameters) keeps its Merkle tree in two bounded tiers in place of the WeakHashMap that held every node it ever computed: the top 63 nodes - the same ones its encoding persists - in a fixed array for the life of the key, and the authentication path of the last one-time key signed with, together with that leaf's ancestors, advanced under the key's lock as each index is claimed. The old map keyed every node below the top on objects nothing retained, so a leaf was collectable the moment it was inserted and the map's hit rate below the top depended on when the collector next ran, while its table still grew to 2^(h+1) entries during a tree build. A run of consecutive signatures now costs about (h - 5) / 2 + 1 leaf derivations each rather than either a cache hit or a 2^(h - 5) rebuild, with the rebuild remaining as the worst case at a half-tree crossing; shards and repositioned keys inherit the parent's retained path, and the encoding is unchanged. The tree built for the public key is built in path form, so a freshly generated key already holds the path of its first signature rather than rebuilding it - that signature went from 2.6 s to about 1 ms at h=15. The deprecated org.bouncycastle.pqc.crypto.lms copy is untouched.
5454

55+
- The JCE bindings for LMS/HSS have followed the lightweight ones into the main provider: org.bouncycastle.jcajce.provider.asymmetric.lms holds the KeyFactory, KeyPairGenerator and Signature implementations, and the key interfaces are promoted to org.bouncycastle.jcajce.interfaces.LMSKey and LMSPrivateKey. Both the BouncyCastle and the BouncyCastlePQC provider register the new classes for the "LMS" services and their id-alg-hss-lms-hashsig aliases, and the key-info converter the BC provider consults when it recovers a key from a SubjectPublicKeyInfo or PrivateKeyInfo - the path a certificate or a PKCS#8 key takes - returns them as well, so a key recovered that way verifies through either provider. The key generation parameter specs are promoted with them, as org.bouncycastle.jcajce.spec.LMSKeyGenParameterSpec and org.bouncycastle.jcajce.spec.LMSHSSKeyGenParameterSpec; the org.bouncycastle.pqc.jcajce.spec copies are deprecated and now extend them, so an existing spec object is one of the promoted type and keeps its own fromNames() and its org.bouncycastle.pqc.crypto.lms constructors and getters. The org.bouncycastle.pqc.jcajce.provider.lms classes and the LMS interfaces in org.bouncycastle.pqc.jcajce.interfaces remain and are deprecated; the old interfaces now extend the promoted ones, so code written against them still compiles and keys of the new classes still satisfy them.
56+
5557
### 2.1.4 Additional Notes
5658

5759
- The sources and javadoc jars of the Ant-built distributions (jdk14, jdk15to18 and jdk13) no longer carry test material. Each module's javadoc target copies the package documentation it needs - org/bouncycastle/<area>/**/*.html - back into the module source directory that has already been compiled from, and zip-src zips that directory afterwards, so every test package's package.html arrived in the sources jar by that route; javadoc-util additionally copied org/bouncycastle/asn1/isismtt/**/*.java, which put test classes into the bcutil javadoc as generated pages, and javadoc-pg deliberately copied the gpg and bcpg test sources in order to document them. Separately the source copies excluded test material only one directory deep and only for *.java, because Ant reads ** as an any-depth wildcard just where it is a whole path segment, so anything nested further or with another extension - the PEM certificate fixtures under org/bouncycastle/est/test/san corrected in 1.86, and an ICAO master list under org/bouncycastle/asn1/icao/test - went through. The source and javadoc copies of every module now exclude test directories at any depth, and javadoc-pg no longer documents the test packages. org.bouncycastle.util.test is unaffected and still ships in the bcprov binary, sources and javadoc jars, as it does from the Gradle build: it is the SimpleTest framework the light-weight API's own test classes are written against, not test material of the distribution. No binary changes - the classes and resources of every Ant-built jar are identical to those of the 1.86 release - and the Gradle-built jdk18on artifacts never carried any of this.

‎prov/src/main/ext-jdk1.9/module-info.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
opens org.bouncycastle.jcajce.provider.asymmetric.cmce to java.base;
1010
opens org.bouncycastle.jcajce.provider.asymmetric.edec to java.base;
1111
opens org.bouncycastle.jcajce.provider.asymmetric.frodokem to java.base;
12+
opens org.bouncycastle.jcajce.provider.asymmetric.lms to java.base;
1213
opens org.bouncycastle.jcajce.provider.asymmetric.mldsa to java.base;
1314
opens org.bouncycastle.jcajce.provider.asymmetric.mlkem to java.base;
1415
opens org.bouncycastle.jcajce.provider.asymmetric.slhdsa to java.base;
@@ -110,6 +111,7 @@
110111
exports org.bouncycastle.jcajce.provider.asymmetric.gost;
111112
exports org.bouncycastle.jcajce.provider.asymmetric.frodokem;
112113
exports org.bouncycastle.jcajce.provider.asymmetric.ies;
114+
exports org.bouncycastle.jcajce.provider.asymmetric.lms;
113115
exports org.bouncycastle.jcajce.provider.asymmetric.mldsa;
114116
exports org.bouncycastle.jcajce.provider.asymmetric.mlkem;
115117
exports org.bouncycastle.jcajce.provider.asymmetric.rsa;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.bouncycastle.jcajce.interfaces;
2+
3+
import java.security.Key;
4+
5+
/**
6+
* Base interface for Leighton-Micali Hash-Based Signatures (LMS) keys.
7+
*/
8+
public interface LMSKey
9+
extends Key
10+
{
11+
/**
12+
* Return the number of levels (L) associated with the key.
13+
*
14+
* @return L.
15+
*/
16+
int getLevels();
17+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.bouncycastle.jcajce.interfaces;
2+
3+
import java.security.PrivateKey;
4+
5+
/**
6+
* Base interface for an LMS private key
7+
*/
8+
public interface LMSPrivateKey
9+
extends LMSKey, PrivateKey
10+
{
11+
/**
12+
* Return the index of the next signature.
13+
*
14+
* @return the index number for the next signature.
15+
*/
16+
long getIndex();
17+
18+
/**
19+
* Return the number of usages left for the private key.
20+
*
21+
* @return the number of times the key can be used before it is exhausted.
22+
*/
23+
long getUsagesRemaining();
24+
25+
/**
26+
* Return a key representing a shard of the key space that can be used usageCount times.
27+
* <p>
28+
* Note: this will use the range [index...index + usageCount) for the current key.
29+
* </p>
30+
* @param usageCount the number of usages the key should have.
31+
* @return a key based on the current key that can be used usageCount times.
32+
*/
33+
LMSPrivateKey extractKeyShard(int usageCount);
34+
}

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/LMS.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
public class LMS
88
{
9-
private static final String PREFIX = "org.bouncycastle.pqc.jcajce.provider" + ".lms.";
9+
private static final String PREFIX = "org.bouncycastle.jcajce.provider.asymmetric" + ".lms.";
1010

1111
public static class Mappings
1212
extends AsymmetricAlgorithmProvider
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package org.bouncycastle.jcajce.provider.asymmetric.lms;
2+
3+
import java.io.IOException;
4+
import java.io.ObjectInputStream;
5+
import java.io.ObjectOutputStream;
6+
7+
import javax.security.auth.Destroyable;
8+
9+
import org.bouncycastle.asn1.ASN1Set;
10+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
11+
import org.bouncycastle.crypto.CipherParameters;
12+
import org.bouncycastle.crypto.params.HSSPrivateKeyParameters;
13+
import org.bouncycastle.crypto.params.LMSKeyParameters;
14+
import org.bouncycastle.crypto.params.LMSPrivateKeyParameters;
15+
import org.bouncycastle.crypto.util.PrivateKeyFactory;
16+
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
17+
import org.bouncycastle.pqc.jcajce.interfaces.LMSPrivateKey;
18+
import org.bouncycastle.util.Exceptions;
19+
20+
public class BCLMSPrivateKey
21+
implements LMSPrivateKey, Destroyable
22+
{
23+
private static final long serialVersionUID = 8568701712864512338L;
24+
25+
private transient HSSPrivateKeyParameters keyParams;
26+
private transient ASN1Set attributes;
27+
28+
public BCLMSPrivateKey(LMSKeyParameters keyParams)
29+
{
30+
if (keyParams instanceof HSSPrivateKeyParameters)
31+
{
32+
this.keyParams = (HSSPrivateKeyParameters)keyParams;
33+
}
34+
else
35+
{
36+
LMSPrivateKeyParameters lms = (LMSPrivateKeyParameters)keyParams;
37+
this.keyParams = new HSSPrivateKeyParameters(lms, lms.getIndex(), lms.getIndex() + lms.getUsagesRemaining());
38+
}
39+
}
40+
41+
public BCLMSPrivateKey(PrivateKeyInfo keyInfo)
42+
throws IOException
43+
{
44+
init(keyInfo);
45+
}
46+
47+
private void init(PrivateKeyInfo keyInfo)
48+
throws IOException
49+
{
50+
this.attributes = keyInfo.getAttributes();
51+
this.keyParams = (HSSPrivateKeyParameters)PrivateKeyFactory.createKey(keyInfo);
52+
}
53+
54+
public long getIndex()
55+
{
56+
// both reads under the key's own monitor, so a signature in between cannot split them
57+
synchronized (keyParams)
58+
{
59+
if (keyParams.getUsagesRemaining() == 0)
60+
{
61+
throw new IllegalStateException("key exhausted");
62+
}
63+
64+
return keyParams.getIndex();
65+
}
66+
}
67+
68+
public long getUsagesRemaining()
69+
{
70+
return keyParams.getUsagesRemaining();
71+
}
72+
73+
public BCLMSPrivateKey extractKeyShard(int usageCount)
74+
{
75+
return new BCLMSPrivateKey(keyParams.extractKeyShard(usageCount));
76+
}
77+
78+
public String getAlgorithm()
79+
{
80+
return "LMS";
81+
}
82+
83+
public String getFormat()
84+
{
85+
return "PKCS#8";
86+
}
87+
88+
public byte[] getEncoded()
89+
{
90+
if (keyParams.isDestroyed())
91+
{
92+
throw new IllegalStateException("key destroyed");
93+
}
94+
95+
try
96+
{
97+
PrivateKeyInfo pki = PrivateKeyInfoFactory.createPrivateKeyInfo(keyParams, attributes);
98+
99+
return pki.getEncoded();
100+
}
101+
catch (IOException e)
102+
{
103+
return null;
104+
}
105+
}
106+
107+
public boolean equals(Object o)
108+
{
109+
if (o == this)
110+
{
111+
return true;
112+
}
113+
114+
if (o instanceof BCLMSPrivateKey)
115+
{
116+
BCLMSPrivateKey otherKey = (BCLMSPrivateKey)o;
117+
118+
// a destroyed key no longer exposes its value, so it is only equal to itself.
119+
if (isDestroyed() || otherKey.isDestroyed())
120+
{
121+
return false;
122+
}
123+
124+
return keyParams.equals(otherKey.keyParams);
125+
}
126+
127+
return false;
128+
}
129+
130+
public int hashCode()
131+
{
132+
return keyParams.hashCode();
133+
}
134+
135+
CipherParameters getKeyParams()
136+
{
137+
return keyParams;
138+
}
139+
140+
public int getLevels()
141+
{
142+
return keyParams.getL();
143+
}
144+
145+
/**
146+
* Destroy this key, zeroizing the secret key material it holds.
147+
* <p>
148+
* The master secret of every tree in the hierarchy is zeroized; the key identifiers, indexes,
149+
* chaining signatures and cached tree nodes are retained, so {@link #getIndex()},
150+
* {@link #getUsagesRemaining()} and {@link #getLevels()} keep working. After destruction
151+
* {@link #isDestroyed()} returns true, {@link #getEncoded()} and {@link #extractKeyShard(int)}
152+
* throw {@link IllegalStateException}, the key can no longer be serialized, and a Signature
153+
* refuses it at initSign. Shards extracted before destruction are independent copies and are
154+
* unaffected. As the underlying {@link HSSPrivateKeyParameters} object is destroyed, keys
155+
* sharing it are invalidated too.
156+
*/
157+
public synchronized void destroy()
158+
{
159+
keyParams.destroy();
160+
}
161+
162+
public boolean isDestroyed()
163+
{
164+
return keyParams.isDestroyed();
165+
}
166+
167+
private void readObject(
168+
ObjectInputStream in)
169+
throws IOException, ClassNotFoundException
170+
{
171+
in.defaultReadObject();
172+
173+
byte[] enc = (byte[])in.readObject();
174+
175+
init(PrivateKeyInfo.getInstance(enc));
176+
}
177+
178+
private void writeObject(
179+
ObjectOutputStream out)
180+
throws IOException
181+
{
182+
out.defaultWriteObject();
183+
184+
try
185+
{
186+
out.writeObject(this.getEncoded());
187+
}
188+
catch (IllegalStateException e)
189+
{
190+
throw Exceptions.ioException(e.getMessage(), e);
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)