Skip to content

Commit 926f361

Browse files
committed
feat(sdk-coin-flrp): add recoverySignature method for public key recovery from signature
Ticket: WIN-8068 feat(sdk-coin-flrp): enhance createSignature to return 65-byte signature with recovery parameter and add unit tests for recoverySignature TICKET: WIN-8068
1 parent a0ae8f6 commit 926f361

File tree

2 files changed

+129
-1
lines changed

2 files changed

+129
-1
lines changed

modules/sdk-coin-flrp/src/lib/utils.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,34 @@ export class Utils implements BaseUtils {
118118

119119
/**
120120
* Creates a signature using the Flare network parameters
121+
* Returns a 65-byte signature (64 bytes signature + 1 byte recovery parameter)
121122
*/
122123
createSignature(network: FlareNetwork, message: Buffer, prv: Buffer): Buffer {
123124
const messageHash = this.sha256(message);
124125
const signature = ecc.sign(messageHash, prv);
125-
return Buffer.from(signature);
126+
127+
// Get the public key from the private key for recovery parameter determination
128+
const publicKey = ecc.pointFromScalar(prv, true);
129+
if (!publicKey) {
130+
throw new Error('Failed to derive public key from private key');
131+
}
132+
133+
// Try recovery with param 0 and 1 to find the correct one
134+
let recoveryParam = 0;
135+
for (let i = 0; i <= 1; i++) {
136+
const recovered = ecc.recoverPublicKey(messageHash, signature, i, true);
137+
if (recovered && Buffer.from(recovered).equals(Buffer.from(publicKey))) {
138+
recoveryParam = i;
139+
break;
140+
}
141+
}
142+
143+
// Append recovery parameter to create 65-byte signature
144+
const sigWithRecovery = Buffer.alloc(65);
145+
Buffer.from(signature).copy(sigWithRecovery, 0);
146+
sigWithRecovery[64] = recoveryParam;
147+
148+
return sigWithRecovery;
126149
}
127150

128151
/**
@@ -338,6 +361,38 @@ export class Utils implements BaseUtils {
338361
flareIdString(value: string): Id {
339362
return new Id(Buffer.from(value, 'hex'));
340363
}
364+
365+
/**
366+
* FlareJS wrapper to recover signature
367+
* @param network
368+
* @param message
369+
* @param signature
370+
* @return recovered public key
371+
*/
372+
recoverySignature(network: FlareNetwork, message: Buffer, signature: Buffer): Buffer {
373+
try {
374+
// Hash the message first - must match the hash used in signing
375+
const messageHash = createHash('sha256').update(message).digest();
376+
377+
// Extract recovery parameter and signature
378+
if (signature.length !== 65) {
379+
throw new Error('Invalid signature length - expected 65 bytes (64 bytes signature + 1 byte recovery)');
380+
}
381+
382+
const recoveryParam = signature[64];
383+
const sigOnly = signature.slice(0, 64);
384+
385+
// Recover public key using the provided recovery parameter
386+
const recovered = ecc.recoverPublicKey(messageHash, sigOnly, recoveryParam, true);
387+
if (!recovered) {
388+
throw new Error('Failed to recover public key');
389+
}
390+
391+
return Buffer.from(recovered);
392+
} catch (error) {
393+
throw new Error(`Failed to recover signature: ${error}`);
394+
}
395+
}
341396
}
342397

343398
const utils = new Utils();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { coins, FlareNetwork } from '@bitgo/statics';
2+
import * as assert from 'assert';
3+
import { Utils } from '../../../src/lib/utils';
4+
5+
describe('Utils', function () {
6+
let utils: Utils;
7+
8+
beforeEach(function () {
9+
utils = new Utils();
10+
});
11+
12+
describe('recoverySignature', function () {
13+
it('should recover public key from valid signature', function () {
14+
const network = coins.get('flrp').network as FlareNetwork;
15+
const message = Buffer.from('hello world', 'utf8');
16+
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
17+
18+
// Create signature using the same private key
19+
const signature = utils.createSignature(network, message, privateKey);
20+
21+
// Recover public key
22+
const recoveredPubKey = utils.recoverySignature(network, message, signature);
23+
24+
assert.ok(recoveredPubKey instanceof Buffer);
25+
assert.strictEqual(recoveredPubKey.length, 33); // Should be compressed public key (33 bytes)
26+
});
27+
28+
it('should recover same public key for same message and signature', function () {
29+
const network = coins.get('flrp').network as FlareNetwork;
30+
const message = Buffer.from('hello world', 'utf8');
31+
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
32+
const signature = utils.createSignature(network, message, privateKey);
33+
34+
const pubKey1 = utils.recoverySignature(network, message, signature);
35+
const pubKey2 = utils.recoverySignature(network, message, signature);
36+
37+
assert.deepStrictEqual(pubKey1, pubKey2);
38+
});
39+
40+
it('should recover public key that matches original key', function () {
41+
const network = coins.get('flrp').network as FlareNetwork;
42+
const message = Buffer.from('hello world', 'utf8');
43+
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
44+
45+
// Get original public key
46+
const { ecc } = require('@bitgo/secp256k1');
47+
const originalPubKey = Buffer.from(ecc.pointFromScalar(privateKey, true) as Uint8Array);
48+
49+
// Create signature and recover public key
50+
const signature = utils.createSignature(network, message, privateKey);
51+
const recoveredPubKey = utils.recoverySignature(network, message, signature);
52+
53+
// Convert both to hex strings for comparison
54+
assert.strictEqual(recoveredPubKey.toString('hex'), originalPubKey.toString('hex'));
55+
});
56+
57+
it('should throw error for invalid signature', function () {
58+
const network = coins.get('flrp').network as FlareNetwork;
59+
const message = Buffer.from('hello world', 'utf8');
60+
const invalidSignature = Buffer.from('invalid signature', 'utf8');
61+
62+
assert.throws(() => utils.recoverySignature(network, message, invalidSignature), /Failed to recover signature/);
63+
});
64+
65+
it('should throw error for empty message', function () {
66+
const network = coins.get('flrp').network as FlareNetwork;
67+
const message = Buffer.alloc(0);
68+
const signature = Buffer.alloc(65); // Empty but valid length signature (65 bytes: 64 signature + 1 recovery param)
69+
70+
assert.throws(() => utils.recoverySignature(network, message, signature), /Failed to recover signature/);
71+
});
72+
});
73+
});

0 commit comments

Comments
 (0)