Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion modules/sdk-coin-flrp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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();
Expand Down
73 changes: 73 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
});