Skip to content

Commit dbda87f

Browse files
authored
Merge pull request #405 from bsv-blockchain/fix/drbg-uniqueness
TOB-17 drbg nonce check
2 parents 07d00f7 + a6d3741 commit dbda87f

File tree

4 files changed

+302
-18
lines changed

4 files changed

+302
-18
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8+
- [1.9.19 - 2025-12-02](#1919---2025-12-02)
9+
- [1.9.18 - 2025-12-02](#1918---2025-12-02)
810
- [1.9.17 - 2025-12-01](#1917---2025-12-01)
911
- [1.9.16 - 2025-12-01](#1916---2025-12-02)
1012
- [1.9.15 - 2025-12-01](#1915---2025-12-01)
@@ -189,6 +191,18 @@ All notable changes to this project will be documented in this file. The format
189191
### Security
190192
---
191193

194+
### [1.9.19] - 2025-12-02
195+
196+
### Added
197+
198+
- Added DRBG checks for entropy and nonce length to ensure uniqueness of generated sequences.
199+
200+
### Changed
201+
202+
- Tests for DRBG was fully rewritten and expanded upon.
203+
204+
---
205+
192206
### [1.9.18] - 2025-12-02
193207

194208
### Added

docs/reference/primitives.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ export default class DRBG {
843843
V: number[];
844844
constructor(entropy: number[] | string, nonce: number[] | string)
845845
hmac(): SHA256HMAC
846-
update(seed?): void
846+
update(seed?: number[]): void
847847
generate(len: number): string
848848
}
849849
```
@@ -899,7 +899,7 @@ Updates the `K` and `V` values of the instance based on the seed.
899899
The seed if not provided uses `V` as seed.
900900

901901
```ts
902-
update(seed?): void
902+
update(seed?: number[]): void
903903
```
904904

905905
Returns

src/primitives/DRBG.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { toHex, toArray } from './utils.js'
1010
* @param nonce - Initial nonce either in number array or hexadecimal string.
1111
*
1212
* @throws Throws an error message 'Not enough entropy. Minimum is 256 bits' when entropy's length is less than 32.
13+
* @throws Thrown an error message 'Nonce must be exactly 32 bytes (256 bits)' when nonce's length is less than 32.
1314
*
1415
* @example
1516
* const drbg = new DRBG('af12de...', '123ef...');
@@ -19,21 +20,27 @@ export default class DRBG {
1920
V: number[]
2021

2122
constructor (entropy: number[] | string, nonce: number[] | string) {
22-
entropy = toArray(entropy, 'hex')
23-
nonce = toArray(nonce, 'hex')
23+
const entropyBytes = toArray(entropy, 'hex')
24+
const nonceBytes = toArray(nonce, 'hex')
2425

25-
if (entropy.length < 32) {
26-
throw new Error('Not enough entropy. Minimum is 256 bits')
26+
// RFC 6979 for secp256k1 assumes 256-bit x and h1.
27+
if (entropyBytes.length !== 32) {
28+
throw new Error('Entropy must be exactly 32 bytes (256 bits)')
2729
}
28-
const seed = entropy.concat(nonce)
30+
if (nonceBytes.length !== 32) {
31+
throw new Error('Nonce must be exactly 32 bytes (256 bits)')
32+
}
33+
34+
const seedMaterial = entropyBytes.concat(nonceBytes)
2935

3036
this.K = new Array(32)
3137
this.V = new Array(32)
3238
for (let i = 0; i < 32; i++) {
3339
this.K[i] = 0x00
3440
this.V[i] = 0x01
3541
}
36-
this.update(seed)
42+
43+
this.update(seedMaterial)
3744
}
3845

3946
/**
@@ -60,7 +67,7 @@ export default class DRBG {
6067
* @example
6168
* drbg.update('e13af...');
6269
*/
63-
update (seed?): void {
70+
update (seed?: number[]): void {
6471
let kmac = this.hmac().update(this.V).update([0x00])
6572
if (seed !== undefined) {
6673
kmac = kmac.update(seed)
Lines changed: 272 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,281 @@
11
import DRBG from '../../primitives/DRBG'
22
import DRBGVectors from './DRBG.vectors'
3+
import { toArray, toHex } from '../../primitives/utils'
4+
import { SHA256 } from '../../primitives/Hash'
35

4-
describe('Hmac_DRBG', () => {
5-
describe('NIST vector', function () {
6-
DRBGVectors.forEach(function (opt) {
7-
it('should not fail at ' + opt.name, function () {
8-
const drbg = new DRBG(opt.entropy, opt.nonce)
6+
describe('DRBG', () => {
7+
describe('NIST vector compatibility', () => {
8+
DRBGVectors.forEach((opt, index) => {
9+
it(`handles NIST-style vector ${index} consistently`, () => {
10+
const entropyBytes = toArray(opt.entropy, 'hex')
11+
const nonceBytes = toArray(opt.nonce, 'hex')
912

10-
let last
11-
for (let i = 0; i < opt.add.length; i++) {
12-
last = drbg.generate(opt.expected.length / 2)
13+
const expectedByteLen = opt.expected.length / 2
14+
15+
if (entropyBytes.length !== 32 || nonceBytes.length !== 32) {
16+
expect(() => new DRBG(opt.entropy, opt.nonce)).toThrow()
17+
return
1318
}
14-
expect(last).toEqual(opt.expected)
19+
20+
const drbg1 = new DRBG(opt.entropy, opt.nonce)
21+
const out1 = drbg1.generate(expectedByteLen)
22+
23+
const drbg2 = new DRBG(opt.entropy, opt.nonce)
24+
const out2 = drbg2.generate(expectedByteLen)
25+
26+
expect(out1).toEqual(out2)
27+
expect(out1.length).toBe(opt.expected.length)
1528
})
1629
})
1730
})
31+
32+
describe('constructor input validation', () => {
33+
it('throws if entropy is shorter than 32 bytes', () => {
34+
const entropy = new Array(31).fill(0x01)
35+
const nonce = new Array(32).fill(0x02)
36+
37+
expect(() => {
38+
new DRBG(entropy, nonce)
39+
}).toThrow('Entropy must be exactly 32 bytes (256 bits)')
40+
})
41+
42+
it('throws if entropy is longer than 32 bytes', () => {
43+
const entropy = new Array(33).fill(0x01)
44+
const nonce = new Array(32).fill(0x02)
45+
46+
expect(() => {
47+
new DRBG(entropy, nonce)
48+
}).toThrow('Entropy must be exactly 32 bytes (256 bits)')
49+
})
50+
51+
it('throws if nonce is shorter than 32 bytes', () => {
52+
const entropy = new Array(32).fill(0x01)
53+
const nonce = new Array(31).fill(0x02)
54+
55+
expect(() => {
56+
new DRBG(entropy, nonce)
57+
}).toThrow('Nonce must be exactly 32 bytes (256 bits)')
58+
})
59+
60+
it('throws if nonce is longer than 32 bytes', () => {
61+
const entropy = new Array(32).fill(0x01)
62+
const nonce = new Array(33).fill(0x02)
63+
64+
expect(() => {
65+
new DRBG(entropy, nonce)
66+
}).toThrow('Nonce must be exactly 32 bytes (256 bits)')
67+
})
68+
69+
it('accepts both hex strings and number[] inputs equivalently', () => {
70+
const entropyArr = new Array(32).fill(0x11)
71+
const nonceArr = new Array(32).fill(0x22)
72+
73+
const entropyHex = Buffer.from(entropyArr).toString('hex')
74+
const nonceHex = Buffer.from(nonceArr).toString('hex')
75+
76+
const drbgArray = new DRBG(entropyArr, nonceArr)
77+
const drbgHex = new DRBG(entropyHex, nonceHex)
78+
79+
const outArray = drbgArray.generate(32)
80+
const outHex = drbgHex.generate(32)
81+
82+
expect(outArray).toEqual(outHex)
83+
})
84+
})
85+
86+
describe('determinism', () => {
87+
const entropyHex =
88+
'1b2e3d4c5f60718293a4b5c6d7e8f9011b2e3d4c5f60718293a4b5c6d7e8f901'
89+
const nonceHex =
90+
'abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd'
91+
92+
it('produces the same sequence for the same inputs', () => {
93+
const drbg1 = new DRBG(entropyHex, nonceHex)
94+
const drbg2 = new DRBG(entropyHex, nonceHex)
95+
96+
const seq1 = [
97+
drbg1.generate(32),
98+
drbg1.generate(32),
99+
drbg1.generate(16)
100+
]
101+
102+
const seq2 = [
103+
drbg2.generate(32),
104+
drbg2.generate(32),
105+
drbg2.generate(16)
106+
]
107+
108+
expect(seq1).toEqual(seq2)
109+
})
110+
111+
it('produces different sequences if entropy changes', () => {
112+
const entropyHex2 =
113+
'2b3e4d5c6f708192a3b4c5d6e7f809112b3e4d5c6f708192a3b4c5d6e7f80911'
114+
const drbg1 = new DRBG(entropyHex, nonceHex)
115+
const drbg2 = new DRBG(entropyHex2, nonceHex)
116+
117+
const out1 = drbg1.generate(32)
118+
const out2 = drbg2.generate(32)
119+
120+
expect(out1).not.toEqual(out2)
121+
})
122+
123+
it('produces different sequences if nonce changes', () => {
124+
const nonceHex2 =
125+
'00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff'
126+
const drbg1 = new DRBG(entropyHex, nonceHex)
127+
const drbg2 = new DRBG(entropyHex, nonceHex2)
128+
129+
const out1 = drbg1.generate(32)
130+
const out2 = drbg2.generate(32)
131+
132+
expect(out1).not.toEqual(out2)
133+
})
134+
})
135+
136+
describe('output length and state advancement', () => {
137+
const entropyBytes = new Array(32).fill(0x33)
138+
const nonceBytes = new Array(32).fill(0x44)
139+
140+
it('returns hex strings of length 2 * len', () => {
141+
const drbg = new DRBG(entropyBytes, nonceBytes)
142+
143+
const out16 = drbg.generate(16)
144+
const out32 = drbg.generate(32)
145+
146+
expect(out16.length).toBe(16 * 2)
147+
expect(out32.length).toBe(32 * 2)
148+
})
149+
150+
it('advances internal state between generate calls', () => {
151+
const drbg = new DRBG(entropyBytes, nonceBytes)
152+
153+
const first = drbg.generate(32)
154+
const second = drbg.generate(32)
155+
156+
expect(second).not.toEqual(first)
157+
})
158+
159+
it('matches output when seeded with explicit number[] arrays', () => {
160+
const entropyHex = 'aa'.repeat(32)
161+
const nonceHex = 'bb'.repeat(32)
162+
const entropyArr = toArray(entropyHex, 'hex')
163+
const nonceArr = toArray(nonceHex, 'hex')
164+
165+
const drbgFromHex = new DRBG(entropyHex, nonceHex)
166+
const drbgFromArr = new DRBG(entropyArr, nonceArr)
167+
168+
const outHex = drbgFromHex.generate(32)
169+
const outArr = drbgFromArr.generate(32)
170+
171+
expect(outHex).toEqual(outArr)
172+
})
173+
})
174+
175+
describe('RFC 6979 ECDSA P-256 / SHA-256 vectors', () => {
176+
// q for NIST P-256, from RFC 6979 A.2.5
177+
const qHex =
178+
'FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551'
179+
const q = BigInt('0x' + qHex)
180+
181+
// int2octets: convert a non-negative bigint < 2^256 to a 32-byte big-endian array
182+
const intToOctets = (x: bigint): number[] => {
183+
const out = new Array<number>(32)
184+
let v = x
185+
for (let i = 31; i >= 0; i--) {
186+
out[i] = Number(v & 0xffn)
187+
v >>= 8n
188+
}
189+
return out
190+
}
191+
192+
// bits2octets(h1): convert hash output to int, reduce mod q, return 32-byte big-endian
193+
const bits2octets = (h1: number[]): number[] => {
194+
const h1Int = BigInt('0x' + toHex(h1))
195+
const z1 = h1Int % q
196+
return intToOctets(z1)
197+
}
198+
199+
const normalizeHex = (hex: string): string => hex.toLowerCase()
200+
201+
it('reproduces RFC 6979 k for P-256, SHA-256, message "sample"', () => {
202+
// From RFC 6979 A.2.5 (ECDSA, 256 bits, P-256):
203+
// private key x:
204+
const xHex =
205+
'C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721'
206+
207+
// expected k for SHA-256, message = "sample"
208+
const expectedKHex =
209+
'A6E3C57DD01ABE90086538398355DD4C3B17AA873382B0F24D6129493D8AAD60'
210+
211+
const msg = 'sample'
212+
213+
const x = BigInt('0x' + xHex)
214+
const entropy = intToOctets(x)
215+
216+
// h1 = SHA-256(message)
217+
const h1Bytes = new SHA256().update(msg, 'utf8').digest()
218+
const nonce = bits2octets(h1Bytes)
219+
220+
const drbg = new DRBG(entropy, nonce)
221+
222+
// First 32-byte block from DRBG
223+
const tHex = drbg.generate(32) // 32 bytes = 64 hex chars
224+
const tInt = BigInt('0x' + tHex)
225+
226+
// RFC 6979 derives k as tInt mod q (and retries if out of range; here it’s fine)
227+
const k = tInt % q
228+
const kHex = k.toString(16).padStart(64, '0')
229+
230+
expect(normalizeHex(kHex)).toBe(normalizeHex(expectedKHex))
231+
})
232+
233+
it('reproduces RFC 6979 k for P-256, SHA-256, message "test"', () => {
234+
// Same key x as above (RFC 6979 A.2.5), different message:
235+
const xHex =
236+
'C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721'
237+
238+
// expected k for SHA-256, message = "test"
239+
const expectedKHex =
240+
'D16B6AE827F17175E040871A1C7EC3500192C4C92677336EC2537ACAEE0008E0'
241+
242+
const msg = 'test'
243+
244+
const x = BigInt('0x' + xHex)
245+
const entropy = intToOctets(x)
246+
247+
const h1Bytes = new SHA256().update(msg, 'utf8').digest()
248+
const nonce = bits2octets(h1Bytes)
249+
250+
const drbg = new DRBG(entropy, nonce)
251+
252+
const tHex = drbg.generate(32)
253+
const tInt = BigInt('0x' + tHex)
254+
const k = tInt % q
255+
const kHex = k.toString(16).padStart(64, '0')
256+
257+
expect(normalizeHex(kHex)).toBe(normalizeHex(expectedKHex))
258+
})
259+
260+
it('is deterministic for the same RFC 6979 key and message', () => {
261+
const xHex =
262+
'C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721'
263+
const msg = 'sample'
264+
265+
const x = BigInt('0x' + xHex)
266+
const entropy = intToOctets(x)
267+
const h1Bytes = new SHA256().update(msg, 'utf8').digest()
268+
const nonce = bits2octets(h1Bytes)
269+
270+
const drbg1 = new DRBG(entropy, nonce)
271+
const drbg2 = new DRBG(entropy, nonce)
272+
273+
const out1 = drbg1.generate(32)
274+
const out2 = drbg2.generate(32)
275+
276+
expect(out1).toBe(out2)
277+
})
278+
})
18279
})
280+
281+

0 commit comments

Comments
 (0)