Skip to content

Commit 55c4d99

Browse files
committed
more tests and deterministic nonce
1 parent f0e028d commit 55c4d99

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

src/primitives/Secp256r1.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Random from './Random.js'
2-
import { sha256 } from './Hash.js'
2+
import { sha256, sha256hmac } from './Hash.js'
33
import { toArray, toHex } from './utils.js'
44

55
export type P256Point = { x: bigint, y: bigint } | null
@@ -237,24 +237,25 @@ export default class Secp256r1 {
237237
sign (message: ByteSource, privateKey: string | bigint, opts: { prehashed?: boolean, nonce?: bigint } = {}): { r: string, s: string } {
238238
const { prehashed = false, nonce } = opts
239239
const d = this.toScalar(privateKey)
240-
const z = this.messageToBigInt(message, prehashed)
241-
let k = nonce ?? this.randomScalar()
240+
const digest = this.normalizeMessage(message, prehashed)
241+
const z = this.bytesToScalar(digest)
242+
let k = nonce ?? this.deterministicNonce(d, digest)
242243

243244
while (true) {
244245
const p = this.multiplyBase(k)
245246
if (this.isInfinity(p)) {
246-
k = nonce ?? this.randomScalar()
247+
k = nonce ?? this.deterministicNonce(d, digest)
247248
continue
248249
}
249250
const r = this.mod(p.x, this.n)
250251
if (r === 0n) {
251-
k = nonce ?? this.randomScalar()
252+
k = nonce ?? this.deterministicNonce(d, digest)
252253
continue
253254
}
254255
const kinv = this.modInv(k, this.n)
255256
let s = this.mod(kinv * (z + r * d), this.n)
256257
if (s === 0n) {
257-
k = nonce ?? this.randomScalar()
258+
k = nonce ?? this.deterministicNonce(d, digest)
258259
continue
259260
}
260261
if (s > HALF_N) s = this.n - s // enforce low-s
@@ -267,14 +268,19 @@ export default class Secp256r1 {
267268
*/
268269
verify (message: ByteSource, signature: { r: string | bigint, s: string | bigint }, publicKey: P256Point | string, opts: { prehashed?: boolean } = {}): boolean {
269270
const { prehashed = false } = opts
270-
const q = typeof publicKey === 'string' ? this.pointFromHex(publicKey) : publicKey
271+
let q: P256Point
272+
try {
273+
q = typeof publicKey === 'string' ? this.pointFromHex(publicKey) : publicKey
274+
} catch {
275+
return false
276+
}
271277
if ((q == null) || !this.isOnCurve(q)) return false
272278

273279
const r = typeof signature.r === 'bigint' ? signature.r : BigInt('0x' + signature.r)
274280
const s = typeof signature.s === 'bigint' ? signature.s : BigInt('0x' + signature.s)
275281
if (r <= 0n || r >= this.n || s <= 0n || s >= this.n) return false
276282

277-
const z = this.messageToBigInt(message, prehashed)
283+
const z = this.bytesToScalar(this.normalizeMessage(message, prehashed))
278284
const w = this.modInv(s, this.n)
279285
const u1 = this.mod(z * w, this.n)
280286
const u2 = this.mod(r * w, this.n)
@@ -284,13 +290,32 @@ export default class Secp256r1 {
284290
return v === r
285291
}
286292

287-
private messageToBigInt (message: ByteSource, prehashed: boolean): bigint {
293+
private normalizeMessage (message: ByteSource, prehashed: boolean): Uint8Array {
288294
const bytes = this.toBytes(message)
289-
const digest = prehashed ? bytes : new Uint8Array(sha256(bytes))
290-
const hex = toHex(Array.from(digest))
295+
if (prehashed) return bytes
296+
return new Uint8Array(sha256(bytes))
297+
}
298+
299+
private bytesToScalar (bytes: Uint8Array): bigint {
300+
const hex = toHex(Array.from(bytes))
291301
return BigInt('0x' + hex) % this.n
292302
}
293303

304+
private deterministicNonce (priv: bigint, msgDigest: Uint8Array): bigint {
305+
const keyBytes = toArray(this.to32BytesHex(priv), 'hex')
306+
let counter = 0
307+
while (counter < 1024) { // safety bound
308+
const data = counter === 0
309+
? Array.from(msgDigest)
310+
: Array.from(msgDigest).concat([counter & 0xff])
311+
const hmac = sha256hmac(keyBytes, data)
312+
const k = BigInt('0x' + toHex(hmac)) % this.n
313+
if (k > 0n) return k
314+
counter++
315+
}
316+
throw new Error('Failed to derive deterministic nonce')
317+
}
318+
294319
private toBytes (data: ByteSource): Uint8Array {
295320
if (typeof data === 'string') {
296321
const isHex = HEX_REGEX.test(data) && data.length % 2 === 0

src/primitives/__tests/Secp256r1.test.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import crypto from 'crypto'
22
import Secp256r1 from '../Secp256r1.js'
3+
import { sha256 } from '../Hash.js'
34

45
const curve = new Secp256r1()
56

@@ -11,32 +12,72 @@ const THREE_G =
1112
const toBase64Url = (hex: string): string => Buffer.from(hex, 'hex').toString('base64url')
1213

1314
describe('Secp256r1', () => {
14-
test('base point multiplication matches known coordinates', () => {
15+
test('base point multiplication matches known coordinates and handles infinity', () => {
1516
const twoG = curve.multiplyBase(2n)
1617
const threeG = curve.multiplyBase(3n)
1718
expect(curve.pointToHex(twoG)).toBe(TWO_G)
1819
expect(curve.pointToHex(threeG)).toBe(THREE_G)
1920
expect(curve.multiplyBase(curve.n)).toBeNull()
21+
expect(curve.multiply(null, 5n)).toBeNull()
2022
})
2123

22-
test('public key generation stays on-curve and supports compression', () => {
24+
test('public key generation stays on-curve, supports compression, and rejects bad encodings', () => {
2325
const priv = curve.generatePrivateKeyHex()
2426
const pub = curve.publicKeyFromPrivate(priv)
2527
expect(curve.isOnCurve(pub)).toBe(true)
2628
const compressed = curve.pointToHex(pub, true)
2729
const roundTrip = curve.pointFromHex(compressed)
2830
expect(roundTrip).toEqual(pub)
31+
expect(() => curve.pointFromHex('05abcdef')).toThrow()
32+
expect(() => curve.pointFromHex('')).toThrow()
2933
})
3034

31-
test('ECDSA sign and verify round-trip', () => {
35+
test('adding inverse points yields infinity', () => {
36+
const p = curve.multiplyBase(9n)
37+
const neg = { x: p!.x, y: curve.p - p!.y }
38+
expect(curve.add(p, neg)).toBeNull()
39+
expect(curve.add(null, p)).toEqual(p)
40+
})
41+
42+
test('ECDSA sign and verify round-trip, low-s enforced, rejects malformed inputs', () => {
3243
const priv = '1'.repeat(64)
3344
const pub = curve.publicKeyFromPrivate(priv)
3445
const message = Buffer.from('p256 check')
3546
const signature = curve.sign(message, priv)
47+
const sVal = BigInt('0x' + signature.s)
48+
expect(sVal <= curve.n / 2n).toBe(true)
3649
expect(curve.verify(message, signature, pub)).toBe(true)
3750
expect(curve.verify(Buffer.from('different'), signature, pub)).toBe(false)
3851
const tampered = { r: signature.r, s: signature.s.slice(0, 62) + '00' }
3952
expect(curve.verify(message, tampered, pub)).toBe(false)
53+
const zeroR = { r: '0'.repeat(64), s: signature.s }
54+
expect(curve.verify(message, zeroR, pub)).toBe(false)
55+
const zeroS = { r: signature.r, s: '0'.repeat(64) }
56+
expect(curve.verify(message, zeroS, pub)).toBe(false)
57+
expect(curve.verify(message, signature, '02deadbeef')).toBe(false)
58+
})
59+
60+
test('deterministic nonce is stable across calls and message changes', () => {
61+
const priv = '2'.repeat(64)
62+
const pub = curve.publicKeyFromPrivate(priv)
63+
const message = Buffer.from('deterministic nonce')
64+
const sig1 = curve.sign(message, priv)
65+
const sig2 = curve.sign(message, priv)
66+
expect(sig1).toEqual(sig2)
67+
const sig3 = curve.sign(Buffer.from('deterministic nonce v2'), priv)
68+
expect(sig3).not.toEqual(sig1)
69+
expect(curve.verify(message, sig1, pub)).toBe(true)
70+
})
71+
72+
test('prehashed signing path matches explicit hashing input', () => {
73+
const priv = '4'.repeat(64)
74+
const pub = curve.publicKeyFromPrivate(priv)
75+
const message = Buffer.from('prehashed path')
76+
const digest = new Uint8Array(sha256(message))
77+
const sig1 = curve.sign(message, priv)
78+
const sig2 = curve.sign(digest, priv, { prehashed: true })
79+
expect(sig1).toEqual(sig2)
80+
expect(curve.verify(digest, sig2, pub, { prehashed: true })).toBe(true)
4081
})
4182

4283
test('signatures interoperate with Node crypto (ieee-p1363 encoding)', () => {

0 commit comments

Comments
 (0)