Skip to content

Commit ae03dd4

Browse files
ARitz-Crackerbitjson
authored andcommitted
feat(secp256k1): add recoverable ECDSA signature support (#13)
1 parent f2308fa commit ae03dd4

File tree

7 files changed

+461
-2
lines changed

7 files changed

+461
-2
lines changed

src/lib/bin/secp256k1/secp256k1.base64.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/lib/bin/secp256k1/secp256k1.wasm

3.66 KB
Binary file not shown.

src/lib/bin/secp256k1/secp256k1Wasm.spec.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// tslint:disable:no-expression-statement no-magic-numbers
1+
// tslint:disable:no-expression-statement no-magic-numbers no-bitwise
22
import { ExecutionContext, test } from 'ava';
33
import { randomBytes } from 'crypto';
44
import { readFileSync } from 'fs';
@@ -215,6 +215,78 @@ const testSecp256k1Wasm = (
215215
),
216216
1
217217
);
218+
219+
// recovery signature
220+
const rawRSigPtr = secp256k1Wasm.malloc(65);
221+
t.not(rawRSigPtr, 0);
222+
t.is(
223+
secp256k1Wasm.signRecoverable(
224+
contextPtr,
225+
rawRSigPtr,
226+
sigHashPtr,
227+
privkeyPtr
228+
),
229+
1
230+
);
231+
232+
// the r and s portions of the signature should match that of a non-recoverable signature
233+
const rIDPtr = secp256k1Wasm.malloc(4);
234+
const compactRSigPtr = secp256k1Wasm.malloc(64);
235+
t.not(compactRSigPtr, 0);
236+
secp256k1Wasm.recoverableSignatureSerialize(
237+
contextPtr,
238+
compactRSigPtr,
239+
rIDPtr,
240+
rawRSigPtr
241+
);
242+
const compactRSig = secp256k1Wasm.readHeapU8(compactRSigPtr, 64);
243+
const rID = secp256k1Wasm.heapU32[rIDPtr >> 2];
244+
245+
t.deepEqual(compactRSig, sigCompact);
246+
t.is(rID, 1);
247+
248+
// re-parsing the signature should produce the same internal format.
249+
const rawRSig2Ptr = secp256k1Wasm.malloc(65);
250+
t.is(
251+
secp256k1Wasm.recoverableSignatureParse(
252+
contextPtr,
253+
rawRSig2Ptr,
254+
compactRSigPtr,
255+
rID
256+
),
257+
1
258+
);
259+
t.deepEqual(
260+
secp256k1Wasm.readHeapU8(rawRSigPtr, 65),
261+
secp256k1Wasm.readHeapU8(rawRSig2Ptr, 65)
262+
);
263+
264+
// the recovered public key should match the derived public key
265+
const recoveredPublicKeyPtr = secp256k1Wasm.malloc(65);
266+
const recoveredPublicKeyCompressedPtr = secp256k1Wasm.malloc(33);
267+
const recoveredPublicKeyCompressedLengthPtr = secp256k1Wasm.mallocSizeT(33);
268+
269+
t.is(
270+
secp256k1Wasm.recover(
271+
contextPtr,
272+
recoveredPublicKeyPtr,
273+
rawRSigPtr,
274+
sigHashPtr
275+
),
276+
1
277+
);
278+
279+
secp256k1Wasm.pubkeySerialize(
280+
contextPtr,
281+
recoveredPublicKeyCompressedPtr,
282+
recoveredPublicKeyCompressedLengthPtr,
283+
recoveredPublicKeyPtr,
284+
CompressionFlag.COMPRESSED
285+
);
286+
t.deepEqual(
287+
pubkeyCompressed,
288+
secp256k1Wasm.readHeapU8(recoveredPublicKeyCompressedPtr, 33)
289+
);
218290
};
219291

220292
const binary = getEmbeddedSecp256k1Binary();

src/lib/bin/secp256k1/secp256k1Wasm.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,70 @@ export interface Secp256k1Wasm {
222222
*/
223223
readonly readSizeT: (pointer: number) => number;
224224

225+
/**
226+
* Compute the public key given a recoverable signature and message hash.
227+
*
228+
* Returns 1 if the signature was valid and public key stored, otherwise 0.
229+
*
230+
* @param contextPtr pointer to a context object, initialized for signing
231+
* @param publicKeyPtr pointer to the created public key (note, this is an
232+
* internal representation, and must be serialized for outside use)
233+
* @param rSigPtr pointer to a recoverable signature (internal format)
234+
* @param msg32Ptr pointer to the 32-byte message hash the signed by this
235+
* signature
236+
*/
237+
readonly recover: (
238+
contextPtr: number,
239+
publicKeyPtr: number,
240+
rSigPtr: number,
241+
msg32Ptr: number
242+
) => 1 | 0;
243+
244+
/**
245+
* Parse an ECDSA signature in compact (64 bytes) format with a recovery
246+
* number. Returns 1 when the signature could be parsed, 0 otherwise.
247+
*
248+
* The signature must consist of a 32-byte big endian R value, followed by a
249+
* 32-byte big endian S value. If R or S fall outside of [0..order-1], the
250+
* encoding is invalid. R and S with value 0 are allowed in the encoding.
251+
*
252+
* After the call, sig will always be initialized. If parsing failed or R or
253+
* S are zero, the resulting sig value is guaranteed to fail validation for
254+
* any message and public key.
255+
*
256+
* @param contextPtr pointer to a context object
257+
* @param outputRSigPtr a pointer to a 65 byte space where the parsed signature
258+
* will be written. (internal format)
259+
* @param inputSigPtr pointer to a serialized signature in compact format
260+
* @param rid the recovery number, as an int. (Not a pointer)
261+
*/
262+
readonly recoverableSignatureParse: (
263+
contextPtr: number,
264+
outputRSigPtr: number,
265+
inputSigPtr: number,
266+
rid: number
267+
) => 1 | 0;
268+
269+
/**
270+
* Serialize a recoverable ECDSA signature in compact (64 byte) format along
271+
* with the recovery number. Always returns 1.
272+
*
273+
* See `recoverableSignatureParse` for details about the encoding.
274+
*
275+
* @param contextPtr pointer to a context object
276+
* @param sigOutPtr pointer to a 64-byte space to store the compact
277+
* serialization
278+
* @param recIDOutPtr pointer to an int which will store the recovery number
279+
* @param rSigPtr pointer to the 65-byte signature to be serialized
280+
* (Secp256k1 internal format)
281+
*/
282+
readonly recoverableSignatureSerialize: (
283+
contextPtr: number,
284+
sigOutPtr: number,
285+
recIDOutPtr: number,
286+
rSigPtr: number
287+
) => 1;
288+
225289
/**
226290
* Verify an ECDSA secret key.
227291
*
@@ -416,6 +480,31 @@ export interface Secp256k1Wasm {
416480
inputSigPtr: number
417481
) => 1 | 0;
418482

483+
/**
484+
* Create a recoverable ECDSA signature. The created signature is always in
485+
* lower-S form.
486+
*
487+
* Returns 1 if the signature was created, 0 if the nonce generation function
488+
* failed, or the private key was invalid.
489+
*
490+
* Note, this WebAssembly Secp256k1 implementation does not currently support
491+
* the final two arguments from the C library, `noncefp` and `ndata`. The
492+
* default nonce generation function, `secp256k1_nonce_function_default`, is
493+
* always used.
494+
*
495+
* @param contextPtr pointer to a context object, initialized for signing
496+
* @param outputRSigPtr pointer to a 65 byte space where the signature will be
497+
* written (internal format)
498+
* @param msg32Ptr pointer to the 32-byte message hash being signed
499+
* @param secretKeyPtr pointer to a 32-byte secret key
500+
*/
501+
readonly signRecoverable: (
502+
contextPtr: number,
503+
outputRSigPtr: number,
504+
msg32Ptr: number,
505+
secretKeyPtr: number
506+
) => 1 | 0;
507+
419508
/**
420509
* Verify an ECDSA signature.
421510
*
@@ -514,6 +603,32 @@ const wrapSecp256k1Wasm = (
514603
const pointerView32 = pointer >> 2;
515604
return heapU32[pointerView32];
516605
},
606+
recover: (contextPtr, outputPubkeyPointer, rSigPtr, msg32Ptr) =>
607+
instance.exports._secp256k1_ecdsa_recover(
608+
contextPtr,
609+
outputPubkeyPointer,
610+
rSigPtr,
611+
msg32Ptr
612+
),
613+
recoverableSignatureParse: (contextPtr, outputRSigPtr, inputSigPtr, rid) =>
614+
instance.exports._secp256k1_ecdsa_recoverable_signature_parse_compact(
615+
contextPtr,
616+
outputRSigPtr,
617+
inputSigPtr,
618+
rid
619+
),
620+
recoverableSignatureSerialize: (
621+
contextPtr,
622+
sigOutPtr,
623+
recIDOutPtr,
624+
rSigPtr
625+
) =>
626+
instance.exports._secp256k1_ecdsa_recoverable_signature_serialize_compact(
627+
contextPtr,
628+
sigOutPtr,
629+
recIDOutPtr,
630+
rSigPtr
631+
),
517632
seckeyVerify: (contextPtr, secretKeyPtr) =>
518633
instance.exports._secp256k1_ec_seckey_verify(contextPtr, secretKeyPtr),
519634
sign: (contextPtr, outputSigPtr, msg32Ptr, secretKeyPtr) =>
@@ -523,6 +638,13 @@ const wrapSecp256k1Wasm = (
523638
msg32Ptr,
524639
secretKeyPtr
525640
),
641+
signRecoverable: (contextPtr, outputRSigPtr, msg32Ptr, secretKeyPtr) =>
642+
instance.exports._secp256k1_ecdsa_sign_recoverable(
643+
contextPtr,
644+
outputRSigPtr,
645+
msg32Ptr,
646+
secretKeyPtr
647+
),
526648
signatureMalleate: (contextPtr, outputSigPtr, inputSigPtr) =>
527649
instance.exports._secp256k1_ecdsa_signature_malleate(
528650
contextPtr,

src/lib/crypto/secp256k1.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const sigCompact = new Uint8Array([0xab, 0x4c, 0x6d, 0x9b, 0xa5, 0x1d, 0xa8, 0x3
4040
// prettier-ignore
4141
const sigCompactHighS = new Uint8Array([0xab, 0x4c, 0x6d, 0x9b, 0xa5, 0x1d, 0xa8, 0x30, 0x72, 0x61, 0x5c, 0x33, 0xa9, 0x88, 0x7b, 0x75, 0x64, 0x78, 0xe6, 0xf9, 0xde, 0x38, 0x10, 0x85, 0xf5, 0x18, 0x3c, 0x97, 0x60, 0x3f, 0xc6, 0xff, 0xd6, 0x8d, 0xde, 0x77, 0x42, 0x6c, 0x80, 0xab, 0x37, 0x9e, 0xa7, 0xd3, 0x59, 0x03, 0x97, 0xa3, 0x2d, 0x0c, 0x28, 0xd9, 0xa9, 0x58, 0x35, 0x05, 0x3c, 0x5d, 0x8b, 0x2e, 0x85, 0x43, 0x89, 0xdd]);
4242

43+
const sigRecovery = 1;
44+
4345
// bitcoin-ts setup
4446
const secp256k1Promise = instantiateSecp256k1();
4547
const binary = getEmbeddedSecp256k1Binary();
@@ -356,6 +358,90 @@ test('secp256k1.normalizeSignatureDER', async t => {
356358
t.notThrows(() => fc.assert(equivalentToSecp256k1Node));
357359
});
358360

361+
test('secp256k1.recoverPublicKeyCompressed', async t => {
362+
const secp256k1 = await secp256k1Promise;
363+
t.deepEqual(
364+
secp256k1.recoverPublicKeyCompressed(sigCompact, sigRecovery, messageHash),
365+
pubkeyCompressed
366+
);
367+
368+
const equivalentToSecp256k1Node = fc.property(
369+
fcValidPrivateKey(secp256k1),
370+
fcUint8Array32(),
371+
(privateKey, hash) => {
372+
const recoverableStuff = secp256k1.signMessageHashRecoverableCompact(
373+
privateKey,
374+
hash
375+
);
376+
t.deepEqual(
377+
secp256k1.recoverPublicKeyCompressed(
378+
recoverableStuff.signature,
379+
recoverableStuff.recovery,
380+
hash
381+
),
382+
new Uint8Array(
383+
secp256k1Node.recover(
384+
Buffer.from(hash),
385+
Buffer.from(recoverableStuff.signature),
386+
recoverableStuff.recovery,
387+
true
388+
)
389+
)
390+
);
391+
}
392+
);
393+
t.notThrows(() => fc.assert(equivalentToSecp256k1Node));
394+
// TODO: equivalentToElliptic test for recoverable signatures.
395+
/*
396+
const equivalentToElliptic = fc.property();
397+
t.notThrows(() => fc.assert(equivalentToElliptic));
398+
*/
399+
});
400+
401+
test('secp256k1.recoverPublicKeyUncompressed', async t => {
402+
const secp256k1 = await secp256k1Promise;
403+
t.deepEqual(
404+
secp256k1.recoverPublicKeyUncompressed(
405+
sigCompact,
406+
sigRecovery,
407+
messageHash
408+
),
409+
pubkeyUncompressed
410+
);
411+
412+
const equivalentToSecp256k1Node = fc.property(
413+
fcValidPrivateKey(secp256k1),
414+
fcUint8Array32(),
415+
(privateKey, hash) => {
416+
const recoverableStuff = secp256k1.signMessageHashRecoverableCompact(
417+
privateKey,
418+
hash
419+
);
420+
t.deepEqual(
421+
secp256k1.recoverPublicKeyUncompressed(
422+
recoverableStuff.signature,
423+
recoverableStuff.recovery,
424+
hash
425+
),
426+
new Uint8Array(
427+
secp256k1Node.recover(
428+
Buffer.from(hash),
429+
Buffer.from(recoverableStuff.signature),
430+
recoverableStuff.recovery,
431+
false
432+
)
433+
)
434+
);
435+
}
436+
);
437+
t.notThrows(() => fc.assert(equivalentToSecp256k1Node));
438+
// TODO: equivalentToElliptic test for recoverable signatures.
439+
/*
440+
const equivalentToElliptic = fc.property();
441+
t.notThrows(() => fc.assert(equivalentToElliptic));
442+
*/
443+
});
444+
359445
test('secp256k1.signMessageHashCompact', async t => {
360446
const secp256k1 = await secp256k1Promise;
361447
t.deepEqual(
@@ -431,6 +517,44 @@ test('secp256k1.signMessageHashDER', async t => {
431517
t.notThrows(() => fc.assert(equivalentToElliptic));
432518
});
433519

520+
test('secp256k1.signMessageHashRecoverableCompact', async t => {
521+
const secp256k1 = await secp256k1Promise;
522+
const recoverableStuff = secp256k1.signMessageHashRecoverableCompact(
523+
privkey,
524+
messageHash
525+
);
526+
t.is(recoverableStuff.recovery, sigRecovery);
527+
t.deepEqual(recoverableStuff.signature, sigCompact);
528+
t.throws(() =>
529+
secp256k1.signMessageHashRecoverableCompact(secp256k1OrderN, messageHash)
530+
);
531+
const equivalentToSecp256k1Node = fc.property(
532+
fcValidPrivateKey(secp256k1),
533+
fcUint8Array32(),
534+
(privateKey, hash) => {
535+
const nodeRecoverableStuff = secp256k1Node.sign(
536+
Buffer.from(hash),
537+
Buffer.from(privateKey)
538+
);
539+
//tslint:disable-next-line:no-object-mutation
540+
nodeRecoverableStuff.signature = new Uint8Array(
541+
nodeRecoverableStuff.signature
542+
);
543+
t.deepEqual(
544+
secp256k1.signMessageHashRecoverableCompact(privateKey, hash),
545+
nodeRecoverableStuff
546+
);
547+
}
548+
);
549+
t.notThrows(() => fc.assert(equivalentToSecp256k1Node));
550+
551+
// TODO: equivalentToElliptic test for recoverable signatures.
552+
/*
553+
const equivalentToElliptic = fc.property();
554+
t.notThrows(() => fc.assert(equivalentToElliptic));
555+
*/
556+
});
557+
434558
test('secp256k1.signatureCompactToDER', async t => {
435559
const secp256k1 = await secp256k1Promise;
436560
t.deepEqual(secp256k1.signatureCompactToDER(sigCompact), sigDER);

0 commit comments

Comments
 (0)