Skip to content

Commit 6ad5ac4

Browse files
committed
Announce the LMS tree cache in HSS component keys with an HSS encoding version of 1 rather than inferring it from trailing stream data, which misread multi-level HSS private keys written before the cache existed - version 0 encodings now parse their component keys cache-free, relates to github #2365.
1 parent 96db5c4 commit 6ad5ac4

5 files changed

Lines changed: 256 additions & 76 deletions

File tree

‎core/src/main/java/org/bouncycastle/pqc/crypto/lms/HSSPrivateKeyParameters.java‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ public static HSSPrivateKeyParameters getInstance(Object src)
9191
}
9292
else if (src instanceof DataInputStream)
9393
{
94-
if (((DataInputStream)src).readInt() != 0)
94+
int version = ((DataInputStream)src).readInt();
95+
if (version != 0 && version != 1)
9596
{
9697
throw new IllegalStateException("unknown version for hss private key");
9798
}
@@ -105,7 +106,12 @@ else if (src instanceof DataInputStream)
105106

106107
for (int t = 0; t < d; t++)
107108
{
108-
keys.add(LMSPrivateKeyParameters.getInstance(src));
109+
// The component keys share this stream with the keys and signatures that follow,
110+
// so whether each one carries the tree-cache field cannot be inferred from the
111+
// stream having more data - the encoding version says: a version 0 encoding
112+
// predates the tree cache and its component keys end at the master secret, a
113+
// version 1 component always carries the cache field (github #2365).
114+
keys.add(LMSPrivateKeyParameters.readKey((DataInputStream)src, version != 0));
109115
}
110116

111117
for (int t = 0; t < d - 1; t++)
@@ -475,8 +481,11 @@ public synchronized byte[] getEncoded()
475481
// Private keys are implementation dependent.
476482
//
477483

484+
// Version 1: the component keys carry the mandatory tree-cache field their getEncoded
485+
// appends; a version 0 encoding (any release before the tree cache) carries them without
486+
// it. The version dispatch in getInstance is what keeps the shared stream unambiguous.
478487
Composer composer = Composer.compose()
479-
.u32str(0) // Version.
488+
.u32str(1) // Version.
480489
.u32str(l)
481490
.u64str(index)
482491
.u64str(indexLimit)

‎core/src/main/java/org/bouncycastle/pqc/crypto/lms/LMSPrivateKeyParameters.java‎

Lines changed: 101 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -96,83 +96,23 @@ else if (src instanceof DataInputStream)
9696
{
9797
DataInputStream dIn = (DataInputStream)src;
9898

99-
/*
100-
.u32str(0) // version
101-
.u32str(parameters.getType()) // type
102-
.u32str(otsParameters.getType()) // ots type
103-
.bytes(I) // I at 16 bytes
104-
.u32str(q) // q
105-
.u32str(maxQ) // maximum q
106-
.u32str(masterSecret.length) // length of master secret.
107-
.bytes(masterSecret) // the master secret
108-
.build();
109-
*/
110-
111-
112-
if (dIn.readInt() != 0)
113-
{
114-
throw new IllegalStateException("expected version 0 lms private key");
115-
}
116-
117-
int sigType = dIn.readInt();
118-
LMSigParameters parameter = LMSigParameters.getParametersForType(sigType);
119-
if (parameter == null)
120-
{
121-
throw new IOException("unknown LMS type code: " + sigType);
122-
}
123-
int otsType = dIn.readInt();
124-
LMOtsParameters otsParameter = LMOtsParameters.getParametersForType(otsType);
125-
if (otsParameter == null)
126-
{
127-
throw new IOException("unknown LM-OTS type code: " + otsType);
128-
}
129-
byte[] I = new byte[16];
130-
dIn.readFully(I);
131-
132-
int q = dIn.readInt();
133-
int maxQ = dIn.readInt();
134-
int l = dIn.readInt();
135-
if (l < 0)
136-
{
137-
throw new IllegalStateException("secret length less than zero");
138-
}
139-
if (l > dIn.available())
140-
{
141-
throw new IOException("secret length exceeded " + dIn.available());
142-
}
143-
byte[] masterSecret = new byte[l];
144-
dIn.readFully(masterSecret);
145-
146-
LMSPrivateKeyParameters key = new LMSPrivateKeyParameters(parameter, otsParameter, q, I, maxQ, masterSecret);
99+
LMSPrivateKeyParameters key = readCoreKey(dIn);
147100

148101
//
149102
// Anything after the master secret is a cache of the top of the Merkle tree (see
150103
// getEncoded). Priming it here means the first signature made after the key is decoded
151104
// does not have to rebuild the whole tree, which otherwise costs about as much as key
152-
// generation. The cache is optional trailing data rather than a new version so that
153-
// releases predating it still read the key - they stop at the master secret and ignore
154-
// what follows - at the cost of it being absent rather than malformed when a stream
155-
// supplies no more bytes.
105+
// generation. For a standalone key the cache is optional trailing data rather than a
106+
// new version so that releases predating it still read the key - they stop at the
107+
// master secret and ignore what follows - at the cost of it being absent rather than
108+
// malformed when a stream supplies no more bytes. Component keys inside an HSS private
109+
// key share their stream with the keys and signatures that follow, so "more data"
110+
// means nothing there - they are read via readKey, where the enclosing HSS encoding's
111+
// version dictates whether the cache field is present (github #2365).
156112
//
157113
if (dIn.available() > 0)
158114
{
159-
int cacheCount = dIn.readInt();
160-
if (cacheCount < 0 || cacheCount >= internedKeys.length)
161-
{
162-
throw new IOException("tree cache node count out of range: " + cacheCount);
163-
}
164-
int m = parameter.getM();
165-
if ((long)cacheCount * m > dIn.available())
166-
{
167-
throw new IOException("tree cache length exceeded " + dIn.available());
168-
}
169-
byte[][] cachedT = new byte[cacheCount + 1][];
170-
for (int r = 1; r <= cacheCount; r++)
171-
{
172-
cachedT[r] = new byte[m];
173-
dIn.readFully(cachedT[r]);
174-
}
175-
key.primeTreeCache(cachedT);
115+
readTreeCache(dIn, key);
176116
}
177117

178118
return key;
@@ -201,6 +141,98 @@ else if (src instanceof InputStream)
201141
throw new IllegalArgumentException("cannot parse " + src);
202142
}
203143

144+
/**
145+
* Read a component key from a stream shared with the other keys and signatures of an HSS
146+
* private key. Unlike the public getInstance entry point, whether the tree-cache field is
147+
* present is dictated by the caller - from the enclosing HSS encoding's version - rather
148+
* than inferred from the stream having more data, which is meaningless mid-stream.
149+
*/
150+
static LMSPrivateKeyParameters readKey(DataInputStream dIn, boolean withCache)
151+
throws IOException
152+
{
153+
LMSPrivateKeyParameters key = readCoreKey(dIn);
154+
155+
if (withCache)
156+
{
157+
readTreeCache(dIn, key);
158+
}
159+
160+
return key;
161+
}
162+
163+
private static LMSPrivateKeyParameters readCoreKey(DataInputStream dIn)
164+
throws IOException
165+
{
166+
/*
167+
.u32str(0) // version
168+
.u32str(parameters.getType()) // type
169+
.u32str(otsParameters.getType()) // ots type
170+
.bytes(I) // I at 16 bytes
171+
.u32str(q) // q
172+
.u32str(maxQ) // maximum q
173+
.u32str(masterSecret.length) // length of master secret.
174+
.bytes(masterSecret) // the master secret
175+
.build();
176+
*/
177+
178+
if (dIn.readInt() != 0)
179+
{
180+
throw new IllegalStateException("expected version 0 lms private key");
181+
}
182+
183+
int sigType = dIn.readInt();
184+
LMSigParameters parameter = LMSigParameters.getParametersForType(sigType);
185+
if (parameter == null)
186+
{
187+
throw new IOException("unknown LMS type code: " + sigType);
188+
}
189+
int otsType = dIn.readInt();
190+
LMOtsParameters otsParameter = LMOtsParameters.getParametersForType(otsType);
191+
if (otsParameter == null)
192+
{
193+
throw new IOException("unknown LM-OTS type code: " + otsType);
194+
}
195+
byte[] I = new byte[16];
196+
dIn.readFully(I);
197+
198+
int q = dIn.readInt();
199+
int maxQ = dIn.readInt();
200+
int l = dIn.readInt();
201+
if (l < 0)
202+
{
203+
throw new IllegalStateException("secret length less than zero");
204+
}
205+
if (l > dIn.available())
206+
{
207+
throw new IOException("secret length exceeded " + dIn.available());
208+
}
209+
byte[] masterSecret = new byte[l];
210+
dIn.readFully(masterSecret);
211+
212+
return new LMSPrivateKeyParameters(parameter, otsParameter, q, I, maxQ, masterSecret);
213+
}
214+
215+
private static void readTreeCache(DataInputStream dIn, LMSPrivateKeyParameters key)
216+
throws IOException
217+
{
218+
int cacheCount = dIn.readInt();
219+
if (cacheCount < 0 || cacheCount >= internedKeys.length)
220+
{
221+
throw new IOException("tree cache node count out of range: " + cacheCount);
222+
}
223+
int m = key.getSigParameters().getM();
224+
if ((long)cacheCount * m > dIn.available())
225+
{
226+
throw new IOException("tree cache length exceeded " + dIn.available());
227+
}
228+
byte[][] cachedT = new byte[cacheCount + 1][];
229+
for (int r = 1; r <= cacheCount; r++)
230+
{
231+
cachedT[r] = new byte[m];
232+
dIn.readFully(cachedT[r]);
233+
}
234+
key.primeTreeCache(cachedT);
235+
}
204236

205237
LMOtsPrivateKey getCurrentOTSKey()
206238
{

‎core/src/test/java/org/bouncycastle/pqc/crypto/lms/HSSTests.java‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,111 @@ public void testHssKeySerialisation()
6464

6565
}
6666

67+
/**
68+
* A multi-level HSS private key in the version 0 encoding - written by any release before
69+
* the tree-cache feature, whose component keys end at the master secret - must still decode
70+
* and sign. The component keys share one stream, so the cache cannot be detected from "more
71+
* bytes available": before the encoding version told the parser whether the cache field is
72+
* present, 4 bytes of the next component key were consumed as a phantom cache count and a
73+
* d &gt; 1 key from an older release failed to decode (github #2365).
74+
*/
75+
public void testVersion0HssKeyDecodes()
76+
throws Exception
77+
{
78+
implVersion0HssKeyDecodes(1);
79+
implVersion0HssKeyDecodes(2);
80+
implVersion0HssKeyDecodes(3);
81+
}
82+
83+
private void implVersion0HssKeyDecodes(int d)
84+
throws Exception
85+
{
86+
HSSPrivateKeyParameters generated = generateKey(d);
87+
88+
Composer composer = Composer.compose()
89+
.u32str(0) // version 0: pre-tree-cache component keys
90+
.u32str(generated.getL())
91+
.u64str(generated.getIndex())
92+
.u64str(generated.getIndexLimit())
93+
.bool(false);
94+
95+
for (LMSPrivateKeyParameters key : generated.getKeys())
96+
{
97+
composer.bytes(version0KeyEncoding(key));
98+
}
99+
for (LMSSignature s : generated.getSig())
100+
{
101+
composer.bytes(s);
102+
}
103+
104+
HSSPrivateKeyParameters decoded = HSSPrivateKeyParameters.getInstance(composer.build());
105+
106+
assertEquals(generated.getL(), decoded.getL());
107+
assertEquals(generated.getIndex(), decoded.getIndex());
108+
assertEquals(generated.getIndexLimit(), decoded.getIndexLimit());
109+
for (int t = 0; t < d; t++)
110+
{
111+
assertFalse("version 0 component key carries no cache", decoded.getKeys().get(t).isTreeCachePrimed());
112+
}
113+
114+
HSSSignature signature = HSS.generateSignature(decoded, Hex.decode("ABCDEF"));
115+
assertTrue(HSS.verifySignature(generated.getPublicKey(), signature, Hex.decode("ABCDEF")));
116+
}
117+
118+
/**
119+
* The current encoding is version 1: the component keys always carry the tree-cache field,
120+
* and the version - the first four bytes - is what a pre-cache release's decoder rejects
121+
* cleanly instead of misparsing the cache as key material.
122+
*/
123+
public void testVersion1HssKeyRoundTrip()
124+
throws Exception
125+
{
126+
HSSPrivateKeyParameters generated = generateKey(2);
127+
128+
byte[] enc = generated.getEncoded();
129+
130+
assertEquals("encoding version", 1, Pack.bigEndianToInt(enc, 0));
131+
132+
HSSPrivateKeyParameters decoded = HSSPrivateKeyParameters.getInstance(enc);
133+
134+
assertTrue(decoded.equals(generated));
135+
for (int t = 0; t < 2; t++)
136+
{
137+
assertTrue("version 1 component key carries the cache", decoded.getKeys().get(t).isTreeCachePrimed());
138+
}
139+
140+
HSSSignature signature = HSS.generateSignature(decoded, Hex.decode("ABCDEF"));
141+
assertTrue(HSS.verifySignature(generated.getPublicKey(), signature, Hex.decode("ABCDEF")));
142+
}
143+
144+
private static HSSPrivateKeyParameters generateKey(int d)
145+
{
146+
LMSParameters[] lmsParameters = new LMSParameters[d];
147+
for (int t = 0; t < d; t++)
148+
{
149+
lmsParameters[t] = new LMSParameters(LMSigParameters.lms_sha256_n32_h5, LMOtsParameters.sha256_n32_w4);
150+
}
151+
152+
return HSS.generateHSSKeyPair(new HSSKeyGenerationParameters(lmsParameters, new SecureRandom()));
153+
}
154+
155+
// Exactly what LMSPrivateKeyParameters.getEncoded() produced before the tree-cache feature:
156+
// version, type, otstype, I, q, maxQ, secret length, secret - no trailing cache.
157+
private static byte[] version0KeyEncoding(LMSPrivateKeyParameters key)
158+
throws Exception
159+
{
160+
return Composer.compose()
161+
.u32str(0)
162+
.u32str(key.getSigParameters().getType())
163+
.u32str(key.getOtsParameters().getType())
164+
.bytes(key.getI())
165+
.u32str((int)key.getIndex())
166+
.u32str((int)key.getIndexLimit())
167+
.u32str(key.getMasterSecret().length)
168+
.bytes(key.getMasterSecret())
169+
.build();
170+
}
171+
67172

68173
/**
69174
* Test Case 1 Signature

‎docs/formats/lms-private-key.md‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ first signature. Its properties:
4141
master secret (a key written by 1.85 or earlier, which then rebuilds the tree on first use),
4242
and a release predating the cache reads a 1.86+ encoding successfully because its decoder
4343
returns at the end of the master secret and never examines the trailing bytes. This is why
44-
the cache is appended rather than announced by a new version number.
44+
the cache is appended rather than announced by a new version number. Note this
45+
trailing-data inference only works for a key that owns its whole encoding: for the component
46+
keys inside an HSS private key, which share one stream, the presence of the cache field is
47+
dictated by the HSS encoding's version instead — see "HSS private keys" below.
4548
- **It carries at most the top six levels** of the tree: nodes `1 .. min(2`<sup>`h+1`</sup>`, 64) - 1`.
4649
For `h = 5` that is the entire tree (63 nodes); for `h = 10` and above it is the top 63
4750
nodes, from which the remaining path nodes are recomputed on demand.
@@ -68,8 +71,39 @@ PrivateKeyInfo ::= SEQUENCE {
6871
}
6972
```
7073

71-
An HSS private key with `L > 1` uses the same OID and prefix convention but a different body
72-
(the HSS key chain state); it is not covered here.
74+
An HSS private key uses the same OID and prefix convention but carries the HSS structure
75+
described in the next section as its body. (A PKCS#8 encoding produced from a standalone
76+
`LMSPrivateKeyParameters`, as in the example below, carries the bare LMS key after the
77+
`L = 1` prefix instead; the decoder accepts either.)
78+
79+
## HSS private keys
80+
81+
An HSS private key (`HSSPrivateKeyParameters.getEncoded()`) is the chain state for the RFC 8554
82+
sec. 6 hierarchy: `L` component LMS private keys, one per level, plus the `L - 1` chaining
83+
signatures by which each tree certifies the public key of the tree below it.
84+
85+
| offset | size | field | contents |
86+
|---|---|---|---|
87+
| 0 | 4 | `version` | `1`. The version 0 form written by earlier releases is read but no longer generated — see below. Anything else is rejected. |
88+
| 4 | 4 | `L` | Number of levels. |
89+
| 8 | 8 | `index` | Next signature index across the whole hierarchy (an unsigned 64-bit value). |
90+
| 16 | 8 | `indexLimit` | Exclusive upper bound on `index`; the key is exhausted when they meet. |
91+
| 24 | 1 | `limited` | `01` when the key is a shard produced by `extractKeyShard()`. |
92+
| 25 | — | `keys` | The `L` component LMS private keys, concatenated, each in the LMS format above. |
93+
| — | — | `signatures` | The `L - 1` chaining signatures, concatenated, each an RFC 8554 sec. 5.4 LMS signature: `sig[i]` is tree `i`'s signature over tree `i + 1`'s public key. |
94+
95+
The component keys share the stream with whatever follows them, so the standalone key's
96+
"trailing data means a cache" inference is meaningless here — for every component but the last,
97+
more data is *always* available. The HSS `version` field carries the distinction instead:
98+
99+
- **version 1** (written by current code): every component key carries the tree-cache field —
100+
the `u32` node count (possibly 0) and its nodes — exactly as `LMSPrivateKeyParameters.getEncoded()`
101+
emits it.
102+
- **version 0** (written by every release before the tree cache): every component key ends at
103+
its master secret and carries no cache field. Read-compatible, never generated.
104+
105+
A release predating the tree cache rejects a version 1 encoding cleanly at its version check
106+
("unknown version for hss private key") rather than misparsing cache bytes as key material.
73107

74108
## Worked example
75109

0 commit comments

Comments
 (0)