Skip to content

Commit 17af75b

Browse files
committed
changelog
1 parent 7f1e3c5 commit 17af75b

File tree

6 files changed

+209
-15
lines changed

6 files changed

+209
-15
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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.12 - 2025-12-01](#1912---2025-12-01)
89
- [1.9.11 - 2025-11-24](#1911---2025-11-24)
910
- [1.9.10 - 2025-11-17](#1910---2025-11-17)
1011
- [1.9.9 - 2025-11-15](#199---2025-11-15)
@@ -182,6 +183,13 @@ All notable changes to this project will be documented in this file. The format
182183

183184
### Security
184185

186+
---
187+
### [1.9.12] - 2025-12-01
188+
189+
### Added
190+
191+
- Added a standalone secp256r1 (P-256) BigInt implementation with ECDSA signing, verification, and tests.
192+
185193
---
186194
### [1.9.11] - 2025-11-24
187195

docs/reference/primitives.md

Lines changed: 152 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
4848
| [BigNumber](#class-bignumber) | [Polynomial](#class-polynomial) | [SHA512](#class-sha512) |
4949
| [Curve](#class-curve) | [PrivateKey](#class-privatekey) | [SHA512HMAC](#class-sha512hmac) |
5050
| [DRBG](#class-drbg) | [PublicKey](#class-publickey) | [Schnorr](#class-schnorr) |
51-
| [JacobianPoint](#class-jacobianpoint) | [RIPEMD160](#class-ripemd160) | [Signature](#class-signature) |
52-
| [K256](#class-k256) | [Reader](#class-reader) | [SymmetricKey](#class-symmetrickey) |
53-
| [KeyShares](#class-keyshares) | [ReductionContext](#class-reductioncontext) | [TransactionSignature](#class-transactionsignature) |
54-
| [Mersenne](#class-mersenne) | [SHA1](#class-sha1) | [Writer](#class-writer) |
55-
| [MontgomoryMethod](#class-montgomorymethod) | [SHA1HMAC](#class-sha1hmac) | |
51+
| [JacobianPoint](#class-jacobianpoint) | [RIPEMD160](#class-ripemd160) | [Secp256r1](#class-secp256r1) |
52+
| [K256](#class-k256) | [Reader](#class-reader) | [Signature](#class-signature) |
53+
| [KeyShares](#class-keyshares) | [ReductionContext](#class-reductioncontext) | [SymmetricKey](#class-symmetrickey) |
54+
| [Mersenne](#class-mersenne) | [SHA1](#class-sha1) | [TransactionSignature](#class-transactionsignature) |
55+
| [MontgomoryMethod](#class-montgomorymethod) | [SHA1HMAC](#class-sha1hmac) | [Writer](#class-writer) |
5656
| [Point](#class-point) | [SHA256](#class-sha256) | |
5757

5858
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
@@ -4320,6 +4320,141 @@ Argument Details
43204320
43214321
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
43224322
4323+
---
4324+
### Class: Secp256r1
4325+
4326+
Pure BigInt implementation of the NIST P-256 (secp256r1) curve with ECDSA sign/verify.
4327+
4328+
This class is standalone (no dependency on the existing secp256k1 primitives) and exposes
4329+
key generation, point encoding/decoding, scalar multiplication, and SHA-256 based ECDSA.
4330+
4331+
```ts
4332+
export default class Secp256r1 {
4333+
readonly p = P;
4334+
readonly n = N;
4335+
readonly a = A;
4336+
readonly b = B;
4337+
readonly g = G;
4338+
pointFromAffine(x: bigint, y: bigint): P256Point
4339+
pointFromHex(hex: string): P256Point
4340+
pointToHex(p: P256Point, compressed = false): string
4341+
add(p1: P256Point, p2: P256Point): P256Point
4342+
multiply(point: P256Point, scalar: bigint): P256Point
4343+
multiplyBase(scalar: bigint): P256Point
4344+
isOnCurve(p: P256Point): boolean
4345+
generatePrivateKeyHex(): string
4346+
publicKeyFromPrivate(privateKey: string | bigint): P256Point
4347+
sign(message: ByteSource, privateKey: string | bigint, opts: {
4348+
prehashed?: boolean;
4349+
nonce?: bigint;
4350+
} = {}): {
4351+
r: string;
4352+
s: string;
4353+
}
4354+
verify(message: ByteSource, signature: {
4355+
r: string | bigint;
4356+
s: string | bigint;
4357+
}, publicKey: P256Point | string, opts: {
4358+
prehashed?: boolean;
4359+
} = {}): boolean
4360+
}
4361+
```
4362+
4363+
See also: [P256Point](./primitives.md#type-p256point), [multiply](./primitives.md#variable-multiply), [sign](./compat.md#variable-sign), [verify](./compat.md#variable-verify)
4364+
4365+
#### Method add
4366+
4367+
Add two points (handles infinity).
4368+
4369+
```ts
4370+
add(p1: P256Point, p2: P256Point): P256Point
4371+
```
4372+
See also: [P256Point](./primitives.md#type-p256point)
4373+
4374+
#### Method generatePrivateKeyHex
4375+
4376+
Generate a new random private key as 32-byte hex.
4377+
4378+
```ts
4379+
generatePrivateKeyHex(): string
4380+
```
4381+
4382+
#### Method isOnCurve
4383+
4384+
Check if a point lies on the curve (including infinity).
4385+
4386+
```ts
4387+
isOnCurve(p: P256Point): boolean
4388+
```
4389+
See also: [P256Point](./primitives.md#type-p256point)
4390+
4391+
#### Method multiply
4392+
4393+
Scalar multiply an arbitrary point using double-and-add.
4394+
4395+
```ts
4396+
multiply(point: P256Point, scalar: bigint): P256Point
4397+
```
4398+
See also: [P256Point](./primitives.md#type-p256point)
4399+
4400+
#### Method multiplyBase
4401+
4402+
Scalar multiply the base point.
4403+
4404+
```ts
4405+
multiplyBase(scalar: bigint): P256Point
4406+
```
4407+
See also: [P256Point](./primitives.md#type-p256point)
4408+
4409+
#### Method pointFromHex
4410+
4411+
Decode a point from compressed or uncompressed hex.
4412+
4413+
```ts
4414+
pointFromHex(hex: string): P256Point
4415+
```
4416+
See also: [P256Point](./primitives.md#type-p256point)
4417+
4418+
#### Method pointToHex
4419+
4420+
Encode a point to compressed or uncompressed hex. Infinity is encoded as `00`.
4421+
4422+
```ts
4423+
pointToHex(p: P256Point, compressed = false): string
4424+
```
4425+
See also: [P256Point](./primitives.md#type-p256point)
4426+
4427+
#### Method sign
4428+
4429+
Create an ECDSA signature over a message. Uses SHA-256 unless `prehashed` is true.
4430+
Returns low-s normalized signature hex parts.
4431+
4432+
```ts
4433+
sign(message: ByteSource, privateKey: string | bigint, opts: {
4434+
prehashed?: boolean;
4435+
nonce?: bigint;
4436+
} = {}): {
4437+
r: string;
4438+
s: string;
4439+
}
4440+
```
4441+
4442+
#### Method verify
4443+
4444+
Verify an ECDSA signature against a message and public key.
4445+
4446+
```ts
4447+
verify(message: ByteSource, signature: {
4448+
r: string | bigint;
4449+
s: string | bigint;
4450+
}, publicKey: P256Point | string, opts: {
4451+
prehashed?: boolean;
4452+
} = {}): boolean
4453+
```
4454+
See also: [P256Point](./primitives.md#type-p256point)
4455+
4456+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
4457+
43234458
---
43244459
### Class: Signature
43254460
@@ -4984,6 +5119,18 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
49845119
---
49855120
## Types
49865121

5122+
### Type: P256Point
5123+
5124+
```ts
5125+
export type P256Point = {
5126+
x: bigint;
5127+
y: bigint;
5128+
} | null
5129+
```
5130+
5131+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5132+
5133+
---
49875134
## Enums
49885135
49895136
## Variables

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/sdk",
3-
"version": "1.9.11",
3+
"version": "1.9.12",
44
"type": "module",
55
"description": "BSV Blockchain Software Development Kit",
66
"main": "dist/cjs/mod.js",

src/primitives/Secp256r1.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import crypto from 'crypto'
1+
import Random from './Random.js'
2+
import { sha256 } from './Hash.js'
23

34
export type P256Point = { x: bigint, y: bigint } | null
45

@@ -19,6 +20,12 @@ const COMPRESSED_EVEN = '02'
1920
const COMPRESSED_ODD = '03'
2021
const UNCOMPRESSED = '04'
2122

23+
/**
24+
* Pure BigInt implementation of the NIST P-256 (secp256r1) curve with ECDSA sign/verify.
25+
*
26+
* This class is standalone (no dependency on the existing secp256k1 primitives) and exposes
27+
* key generation, point encoding/decoding, scalar multiplication, and SHA-256 based ECDSA.
28+
*/
2229
export default class Secp256r1 {
2330
readonly p = P
2431
readonly n = N
@@ -50,7 +57,7 @@ export default class Secp256r1 {
5057
let b = this.mod(base, modulus)
5158
let e = exponent
5259
while (e > 0n) {
53-
if (e & 1n) result = this.mod(result * b, modulus)
60+
if ((e & 1n) === 1n) result = this.mod(result * b, modulus)
5461
e >>= 1n
5562
b = this.mod(b * b, modulus)
5663
}
@@ -77,6 +84,9 @@ export default class Secp256r1 {
7784
return point
7885
}
7986

87+
/**
88+
* Decode a point from compressed or uncompressed hex.
89+
*/
8090
pointFromHex (hex: string): P256Point {
8191
if (hex.startsWith(UNCOMPRESSED)) {
8292
const x = BigInt('0x' + hex.slice(2, 66))
@@ -95,6 +105,9 @@ export default class Secp256r1 {
95105
throw new Error('Invalid point encoding')
96106
}
97107

108+
/**
109+
* Encode a point to compressed or uncompressed hex. Infinity is encoded as `00`.
110+
*/
98111
pointToHex (p: P256Point, compressed = false): string {
99112
if (this.isInfinity(p)) return '00'
100113
const xHex = this.to32BytesHex(p.x)
@@ -104,6 +117,9 @@ export default class Secp256r1 {
104117
return prefix + xHex
105118
}
106119

120+
/**
121+
* Add two affine points (handles infinity).
122+
*/
107123
private addPoints (p1: P256Point, p2: P256Point): P256Point {
108124
if (this.isInfinity(p1)) return p2
109125
if (this.isInfinity(p2)) return p1
@@ -133,17 +149,23 @@ export default class Secp256r1 {
133149
return { x: x3, y: y3 }
134150
}
135151

152+
/**
153+
* Add two points (handles infinity).
154+
*/
136155
add (p1: P256Point, p2: P256Point): P256Point {
137156
return this.addPoints(p1, p2)
138157
}
139158

159+
/**
160+
* Scalar multiply an arbitrary point using double-and-add.
161+
*/
140162
multiply (point: P256Point, scalar: bigint): P256Point {
141163
if (scalar === 0n || this.isInfinity(point)) return null
142164
let k = this.mod(scalar, this.n)
143165
let result: P256Point = null
144166
let addend: P256Point = point
145167
while (k > 0n) {
146-
if (k & 1n) {
168+
if ((k & 1n) === 1n) {
147169
result = this.addPoints(result, addend)
148170
}
149171
addend = this.doublePoint(addend)
@@ -152,10 +174,16 @@ export default class Secp256r1 {
152174
return result
153175
}
154176

177+
/**
178+
* Scalar multiply the base point.
179+
*/
155180
multiplyBase (scalar: bigint): P256Point {
156181
return this.multiply(this.g, scalar)
157182
}
158183

184+
/**
185+
* Check if a point lies on the curve (including infinity).
186+
*/
159187
isOnCurve (p: P256Point): boolean {
160188
try {
161189
this.assertOnCurve(p)
@@ -165,14 +193,17 @@ export default class Secp256r1 {
165193
}
166194
}
167195

196+
/**
197+
* Generate a new random private key as 32-byte hex.
198+
*/
168199
generatePrivateKeyHex (): string {
169200
return this.to32BytesHex(this.randomScalar())
170201
}
171202

172203
private randomScalar (): bigint {
173204
while (true) {
174-
const bytes = crypto.randomBytes(32)
175-
const k = BigInt('0x' + bytes.toString('hex'))
205+
const bytes = Random(32)
206+
const k = BigInt('0x' + Buffer.from(bytes).toString('hex'))
176207
if (k > 0n && k < this.n) return k
177208
}
178209
}
@@ -198,6 +229,10 @@ export default class Secp256r1 {
198229
return this.multiplyBase(d)
199230
}
200231

232+
/**
233+
* Create an ECDSA signature over a message. Uses SHA-256 unless `prehashed` is true.
234+
* Returns low-s normalized signature hex parts.
235+
*/
201236
sign (message: ByteSource, privateKey: string | bigint, opts: { prehashed?: boolean, nonce?: bigint } = {}): { r: string, s: string } {
202237
const { prehashed = false, nonce } = opts
203238
const d = this.toScalar(privateKey)
@@ -226,10 +261,13 @@ export default class Secp256r1 {
226261
}
227262
}
228263

264+
/**
265+
* Verify an ECDSA signature against a message and public key.
266+
*/
229267
verify (message: ByteSource, signature: { r: string | bigint, s: string | bigint }, publicKey: P256Point | string, opts: { prehashed?: boolean } = {}): boolean {
230268
const { prehashed = false } = opts
231269
const q = typeof publicKey === 'string' ? this.pointFromHex(publicKey) : publicKey
232-
if (!q || !this.isOnCurve(q)) return false
270+
if ((q == null) || !this.isOnCurve(q)) return false
233271

234272
const r = typeof signature.r === 'bigint' ? signature.r : BigInt('0x' + signature.r)
235273
const s = typeof signature.s === 'bigint' ? signature.s : BigInt('0x' + signature.s)
@@ -247,7 +285,7 @@ export default class Secp256r1 {
247285

248286
private messageToBigInt (message: ByteSource, prehashed: boolean): bigint {
249287
const bytes = this.toBuffer(message)
250-
const digest = prehashed ? bytes : crypto.createHash('sha256').update(bytes).digest()
288+
const digest = prehashed ? bytes : Buffer.from(sha256(bytes))
251289
const hex = digest.toString('hex')
252290
return BigInt('0x' + hex) % this.n
253291
}

src/primitives/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export { default as Random } from './Random.js'
1313
export { default as TransactionSignature } from './TransactionSignature.js'
1414
export { default as Polynomial, PointInFiniteField } from './Polynomial.js'
1515
export { default as Schnorr } from './Schnorr.js'
16+
export { default as Secp256r1 } from './Secp256r1.js'

0 commit comments

Comments
 (0)