Skip to content

Commit 0e5d2c4

Browse files
authored
Merge pull request #7183 from BitGo/COIN-5838-fix
feat(sdk-coin-canton): added key pair generation & test
2 parents 498714e + 33f7705 commit 0e5d2c4

File tree

5 files changed

+188
-8
lines changed

5 files changed

+188
-8
lines changed

modules/sdk-coin-canton/src/canton.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
} from '@bitgo/sdk-core';
1616
import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc';
1717
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
18+
import { KeyPair as CantonKeyPair } from './lib/keyPair';
19+
import utils from './lib/utils';
1820

1921
export class Canton extends BaseCoin {
2022
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -84,12 +86,20 @@ export class Canton extends BaseCoin {
8486

8587
/** @inheritDoc */
8688
generateKeyPair(seed?: Buffer): KeyPair {
87-
throw new Error('Method not implemented.');
89+
const keyPair = seed ? new CantonKeyPair({ seed }) : new CantonKeyPair();
90+
const keys = keyPair.getKeys();
91+
if (!keys.prv) {
92+
throw new Error('Missing prv in key generation.');
93+
}
94+
return {
95+
pub: keys.pub,
96+
prv: keys.prv,
97+
};
8898
}
8999

90100
/** @inheritDoc */
91101
isValidPub(pub: string): boolean {
92-
throw new Error('Method not implemented.');
102+
return utils.isValidPublicKey(pub);
93103
}
94104

95105
/** @inheritDoc */
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const CryptoKeyFormat = {
2+
RAW: 2,
3+
};
4+
5+
export const SigningKeySpec = {
6+
EC_CURVE25519: 1,
7+
};
8+
9+
export const SigningAlgorithmSpec = {
10+
ED25519: 1,
11+
};
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1-
import { DefaultKeys, Ed25519KeyPair } from '@bitgo/sdk-core';
1+
import { DefaultKeys, Ed25519KeyPair, KeyPairOptions } from '@bitgo/sdk-core';
2+
import utils from './utils';
23

34
export class KeyPair extends Ed25519KeyPair {
5+
/**
6+
* Public constructor. By default, creates a key pair with a random master seed.
7+
*
8+
* @param { KeyPairOptions } source Either a master seed, a private key, or a public key
9+
*/
10+
constructor(source?: KeyPairOptions) {
11+
super(source);
12+
}
413
/** @inheritdoc */
514
getKeys(): DefaultKeys {
6-
throw new Error('Method not implemented.');
15+
const result: DefaultKeys = { pub: this.keyPair.pub };
16+
if (this.keyPair.prv) {
17+
result.prv = this.keyPair.prv;
18+
}
19+
return result;
720
}
821

922
/** @inheritdoc */
1023
recordKeysFromPrivateKeyInProtocolFormat(prv: string): DefaultKeys {
24+
// We don't use private keys for CANTON since it's implemented for TSS.
1125
throw new Error('Method not implemented.');
1226
}
1327

1428
/** @inheritdoc */
1529
recordKeysFromPublicKeyInProtocolFormat(pub: string): DefaultKeys {
16-
throw new Error('Method not implemented.');
30+
if (!utils.isValidPublicKey(pub)) {
31+
throw new Error(`Invalid public key ${pub}`);
32+
}
33+
return { pub };
1734
}
1835

1936
/** @inheritdoc */
2037
getAddress(): string {
21-
throw new Error('Method not implemented.');
38+
return utils.getAddressFromPublicKey(this.keyPair.pub);
2239
}
2340
}

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

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { BaseUtils } from '@bitgo/sdk-core';
1+
import { BaseUtils, isValidEd25519PublicKey } from '@bitgo/sdk-core';
2+
import crypto from 'crypto';
3+
import { CryptoKeyFormat, SigningAlgorithmSpec, SigningKeySpec } from './constant';
24

35
export class Utils implements BaseUtils {
46
/** @inheritdoc */
@@ -18,7 +20,7 @@ export class Utils implements BaseUtils {
1820

1921
/** @inheritdoc */
2022
isValidPublicKey(key: string): boolean {
21-
throw new Error('Method not implemented.');
23+
return isValidEd25519PublicKey(key);
2224
}
2325

2426
/** @inheritdoc */
@@ -30,6 +32,69 @@ export class Utils implements BaseUtils {
3032
isValidTransactionId(txId: string): boolean {
3133
throw new Error('Method not implemented.');
3234
}
35+
36+
/**
37+
* Converts a base64-encoded Ed25519 public key string into a structured signing public key object.
38+
* @param {String} publicKey The base64-encoded Ed25519 public key
39+
* @returns {Object} The structured signing key object formatted for use with cryptographic operations
40+
* @private
41+
*/
42+
private signingPublicKeyFromEd25519(publicKey: string): {
43+
format: number;
44+
publicKey: Buffer;
45+
scheme: number;
46+
keySpec: number;
47+
usage: [];
48+
} {
49+
return {
50+
format: CryptoKeyFormat.RAW,
51+
publicKey: Buffer.from(publicKey, 'base64'),
52+
scheme: SigningAlgorithmSpec.ED25519,
53+
keySpec: SigningKeySpec.EC_CURVE25519,
54+
usage: [],
55+
};
56+
}
57+
58+
/**
59+
* Creates a buffer with a 4-byte big-endian integer prefix followed by the provided byte buffer
60+
* @param {Number} value The integer to prefix, written as 4 bytes in big-endian order
61+
* @param {Buffer} bytes The buffer to append after the integer prefix
62+
* @returns {Buffer} The resulting buffer with the prefixed integer
63+
* @private
64+
*/
65+
private prefixedInt(value: number, bytes: Buffer): Buffer {
66+
const buffer = Buffer.alloc(4 + bytes.length);
67+
buffer.writeUInt32BE(value, 0);
68+
Buffer.from(bytes).copy(buffer, 4);
69+
return buffer;
70+
}
71+
72+
/**
73+
* Computes an SHA-256 Canton-style hash by prefixing the input with a purpose identifier,
74+
* then hashing the resulting buffer and prepending a multi-prefix
75+
*
76+
* @param {Number} purpose A numeric identifier to prefix the hash input with
77+
* @param {Buffer} bytes The buffer to be hashed
78+
* @returns {String} A hexadecimal string representation of the resulting hash with multi-prefix
79+
* @private
80+
*/
81+
private computeSha256CantonHash(purpose: number, bytes: Buffer): string {
82+
const hashInput = this.prefixedInt(purpose, bytes);
83+
const hash = crypto.createHash('sha256').update(hashInput).digest();
84+
const multiprefix = Buffer.from([0x12, 0x20]);
85+
return Buffer.concat([multiprefix, hash]).toString('hex');
86+
}
87+
88+
/**
89+
* Method to create fingerprint (part of the canton partyId) from public key
90+
* @param {String} publicKey the public key
91+
* @returns {String}
92+
*/
93+
getAddressFromPublicKey(publicKey: string): string {
94+
const key = this.signingPublicKeyFromEd25519(publicKey);
95+
const hashPurpose = 12;
96+
return this.computeSha256CantonHash(hashPurpose, key.publicKey);
97+
}
3398
}
3499

35100
const utils = new Utils();
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { BitGoAPI } from '@bitgo/sdk-api';
2+
import { Eddsa } from '@bitgo/sdk-core';
3+
import { Ed25519Bip32HdTree, HDTree } from '@bitgo/sdk-lib-mpc';
4+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
5+
import assert from 'assert';
6+
import should from 'should';
7+
8+
import { KeyPair, Tcanton } from '../../src';
9+
import utils from '../../src/lib/utils';
10+
11+
describe('Canton KeyPair', function () {
12+
let rootKeychain: string;
13+
let rootPublicKey: string;
14+
let MPC: Eddsa;
15+
let hdTree: HDTree;
16+
let bitgo: TestBitGoAPI;
17+
let basecoin: Tcanton;
18+
19+
before(async () => {
20+
hdTree = await Ed25519Bip32HdTree.initialize();
21+
MPC = await Eddsa.initialize(hdTree);
22+
const A = MPC.keyShare(1, 2, 3);
23+
const B = MPC.keyShare(2, 2, 3);
24+
const C = MPC.keyShare(3, 2, 3);
25+
26+
const A_combine = MPC.keyCombine(A.uShare, [B.yShares[1], C.yShares[1]]);
27+
28+
const commonKeychain = A_combine.pShare.y + A_combine.pShare.chaincode;
29+
rootKeychain = MPC.deriveUnhardened(commonKeychain, 'm/0');
30+
rootPublicKey = Buffer.from(rootKeychain.slice(0, 64), 'hex').toString('hex');
31+
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
32+
bitgo.safeRegister('tcanton', Tcanton.createInstance);
33+
basecoin = bitgo.coin('tcanton') as Tcanton;
34+
});
35+
36+
describe('should create a valid KeyPair', () => {
37+
it('from an empty value', async () => {
38+
const keyPair = new KeyPair();
39+
should.exists(keyPair.getKeys().prv);
40+
should.exists(keyPair.getKeys().pub);
41+
const address = utils.getAddressFromPublicKey(keyPair.getKeys().pub);
42+
should.exists(address);
43+
});
44+
});
45+
46+
describe('Keypair from derived Public Key', () => {
47+
it('should create keypair with just derived public key', () => {
48+
const keyPair = new KeyPair({ pub: rootPublicKey });
49+
keyPair.getKeys().pub.should.equal(rootPublicKey);
50+
});
51+
52+
it('should derived ed25519 public key should be valid', () => {
53+
utils.isValidPublicKey(rootPublicKey).should.be.true();
54+
});
55+
});
56+
57+
describe('Keypair from random seed', () => {
58+
it('should generate a keypair from random seed', function () {
59+
const keyPair = basecoin.generateKeyPair();
60+
keyPair.should.have.property('pub');
61+
keyPair.should.have.property('prv');
62+
if (keyPair.pub) {
63+
basecoin.isValidPub(keyPair.pub).should.equal(true);
64+
}
65+
});
66+
});
67+
68+
describe('should fail to create a KeyPair', function () {
69+
it('from an invalid public key', () => {
70+
const source = {
71+
pub: '01D63D',
72+
};
73+
74+
assert.throws(() => new KeyPair(source));
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)