diff --git a/modules/sdk-coin-flrp/src/lib/utils.ts b/modules/sdk-coin-flrp/src/lib/utils.ts index 21a14d037f..c5fedc7d23 100644 --- a/modules/sdk-coin-flrp/src/lib/utils.ts +++ b/modules/sdk-coin-flrp/src/lib/utils.ts @@ -118,11 +118,34 @@ export class Utils implements BaseUtils { /** * Creates a signature using the Flare network parameters + * Returns a 65-byte signature (64 bytes signature + 1 byte recovery parameter) */ createSignature(network: FlareNetwork, message: Buffer, prv: Buffer): Buffer { const messageHash = this.sha256(message); const signature = ecc.sign(messageHash, prv); - return Buffer.from(signature); + + // Get the public key from the private key for recovery parameter determination + const publicKey = ecc.pointFromScalar(prv, true); + if (!publicKey) { + throw new Error('Failed to derive public key from private key'); + } + + // Try recovery with param 0 and 1 to find the correct one + let recoveryParam = 0; + for (let i = 0; i <= 1; i++) { + const recovered = ecc.recoverPublicKey(messageHash, signature, i, true); + if (recovered && Buffer.from(recovered).equals(Buffer.from(publicKey))) { + recoveryParam = i; + break; + } + } + + // Append recovery parameter to create 65-byte signature + const sigWithRecovery = Buffer.alloc(65); + Buffer.from(signature).copy(sigWithRecovery, 0); + sigWithRecovery[64] = recoveryParam; + + return sigWithRecovery; } /** @@ -338,6 +361,38 @@ export class Utils implements BaseUtils { flareIdString(value: string): Id { return new Id(Buffer.from(value, 'hex')); } + + /** + * FlareJS wrapper to recover signature + * @param network + * @param message + * @param signature + * @return recovered public key + */ + recoverySignature(network: FlareNetwork, message: Buffer, signature: Buffer): Buffer { + try { + // Hash the message first - must match the hash used in signing + const messageHash = createHash('sha256').update(message).digest(); + + // Extract recovery parameter and signature + if (signature.length !== 65) { + throw new Error('Invalid signature length - expected 65 bytes (64 bytes signature + 1 byte recovery)'); + } + + const recoveryParam = signature[64]; + const sigOnly = signature.slice(0, 64); + + // Recover public key using the provided recovery parameter + const recovered = ecc.recoverPublicKey(messageHash, sigOnly, recoveryParam, true); + if (!recovered) { + throw new Error('Failed to recover public key'); + } + + return Buffer.from(recovered); + } catch (error) { + throw new Error(`Failed to recover signature: ${error}`); + } + } } const utils = new Utils(); diff --git a/modules/sdk-coin-flrp/test/unit/lib/utils.ts b/modules/sdk-coin-flrp/test/unit/lib/utils.ts new file mode 100644 index 0000000000..e2d152b678 --- /dev/null +++ b/modules/sdk-coin-flrp/test/unit/lib/utils.ts @@ -0,0 +1,73 @@ +import { coins, FlareNetwork } from '@bitgo/statics'; +import * as assert from 'assert'; +import { Utils } from '../../../src/lib/utils'; + +describe('Utils', function () { + let utils: Utils; + + beforeEach(function () { + utils = new Utils(); + }); + + describe('recoverySignature', function () { + it('should recover public key from valid signature', function () { + const network = coins.get('flrp').network as FlareNetwork; + const message = Buffer.from('hello world', 'utf8'); + const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex'); + + // Create signature using the same private key + const signature = utils.createSignature(network, message, privateKey); + + // Recover public key + const recoveredPubKey = utils.recoverySignature(network, message, signature); + + assert.ok(recoveredPubKey instanceof Buffer); + assert.strictEqual(recoveredPubKey.length, 33); // Should be compressed public key (33 bytes) + }); + + it('should recover same public key for same message and signature', function () { + const network = coins.get('flrp').network as FlareNetwork; + const message = Buffer.from('hello world', 'utf8'); + const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex'); + const signature = utils.createSignature(network, message, privateKey); + + const pubKey1 = utils.recoverySignature(network, message, signature); + const pubKey2 = utils.recoverySignature(network, message, signature); + + assert.deepStrictEqual(pubKey1, pubKey2); + }); + + it('should recover public key that matches original key', function () { + const network = coins.get('flrp').network as FlareNetwork; + const message = Buffer.from('hello world', 'utf8'); + const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex'); + + // Get original public key + const { ecc } = require('@bitgo/secp256k1'); + const originalPubKey = Buffer.from(ecc.pointFromScalar(privateKey, true) as Uint8Array); + + // Create signature and recover public key + const signature = utils.createSignature(network, message, privateKey); + const recoveredPubKey = utils.recoverySignature(network, message, signature); + + // Convert both to hex strings for comparison + assert.strictEqual(recoveredPubKey.toString('hex'), originalPubKey.toString('hex')); + }); + + it('should throw error for invalid signature', function () { + const network = coins.get('flrp').network as FlareNetwork; + const message = Buffer.from('hello world', 'utf8'); + const invalidSignature = Buffer.from('invalid signature', 'utf8'); + + assert.throws(() => utils.recoverySignature(network, message, invalidSignature), /Failed to recover signature/); + }); + + it('should throw error for empty message', function () { + const network = coins.get('flrp').network as FlareNetwork; + const message = Buffer.alloc(0); + const signature = Buffer.alloc(65); // Empty but valid length signature (65 bytes: 64 signature + 1 recovery param) + + assert.throws(() => utils.recoverySignature(network, message, signature), /Failed to recover signature/); + }); + }); +});