diff --git a/modules/sdk-coin-canton/src/canton.ts b/modules/sdk-coin-canton/src/canton.ts index 8acfca00ce..9600a3b3e1 100644 --- a/modules/sdk-coin-canton/src/canton.ts +++ b/modules/sdk-coin-canton/src/canton.ts @@ -15,6 +15,8 @@ import { } from '@bitgo/sdk-core'; import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc'; import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { KeyPair as CantonKeyPair } from './lib/keyPair'; +import utils from './lib/utils'; export class Canton extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -84,12 +86,20 @@ export class Canton extends BaseCoin { /** @inheritDoc */ generateKeyPair(seed?: Buffer): KeyPair { - throw new Error('Method not implemented.'); + const keyPair = seed ? new CantonKeyPair({ seed }) : new CantonKeyPair(); + const keys = keyPair.getKeys(); + if (!keys.prv) { + throw new Error('Missing prv in key generation.'); + } + return { + pub: keys.pub, + prv: keys.prv, + }; } /** @inheritDoc */ isValidPub(pub: string): boolean { - throw new Error('Method not implemented.'); + return utils.isValidPublicKey(pub); } /** @inheritDoc */ diff --git a/modules/sdk-coin-canton/src/lib/constant.ts b/modules/sdk-coin-canton/src/lib/constant.ts new file mode 100644 index 0000000000..e394b14d7c --- /dev/null +++ b/modules/sdk-coin-canton/src/lib/constant.ts @@ -0,0 +1,11 @@ +export const CryptoKeyFormat = { + RAW: 2, +}; + +export const SigningKeySpec = { + EC_CURVE25519: 1, +}; + +export const SigningAlgorithmSpec = { + ED25519: 1, +}; diff --git a/modules/sdk-coin-canton/src/lib/keyPair.ts b/modules/sdk-coin-canton/src/lib/keyPair.ts index bb4449c74a..604d0a6d40 100644 --- a/modules/sdk-coin-canton/src/lib/keyPair.ts +++ b/modules/sdk-coin-canton/src/lib/keyPair.ts @@ -1,23 +1,40 @@ -import { DefaultKeys, Ed25519KeyPair } from '@bitgo/sdk-core'; +import { DefaultKeys, Ed25519KeyPair, KeyPairOptions } from '@bitgo/sdk-core'; +import utils from './utils'; export class KeyPair extends Ed25519KeyPair { + /** + * Public constructor. By default, creates a key pair with a random master seed. + * + * @param { KeyPairOptions } source Either a master seed, a private key, or a public key + */ + constructor(source?: KeyPairOptions) { + super(source); + } /** @inheritdoc */ getKeys(): DefaultKeys { - throw new Error('Method not implemented.'); + const result: DefaultKeys = { pub: this.keyPair.pub }; + if (this.keyPair.prv) { + result.prv = this.keyPair.prv; + } + return result; } /** @inheritdoc */ recordKeysFromPrivateKeyInProtocolFormat(prv: string): DefaultKeys { + // We don't use private keys for CANTON since it's implemented for TSS. throw new Error('Method not implemented.'); } /** @inheritdoc */ recordKeysFromPublicKeyInProtocolFormat(pub: string): DefaultKeys { - throw new Error('Method not implemented.'); + if (!utils.isValidPublicKey(pub)) { + throw new Error(`Invalid public key ${pub}`); + } + return { pub }; } /** @inheritdoc */ getAddress(): string { - throw new Error('Method not implemented.'); + return utils.getAddressFromPublicKey(this.keyPair.pub); } } diff --git a/modules/sdk-coin-canton/src/lib/utils.ts b/modules/sdk-coin-canton/src/lib/utils.ts index b0cdef2405..5e86128561 100644 --- a/modules/sdk-coin-canton/src/lib/utils.ts +++ b/modules/sdk-coin-canton/src/lib/utils.ts @@ -1,4 +1,6 @@ -import { BaseUtils } from '@bitgo/sdk-core'; +import { BaseUtils, isValidEd25519PublicKey } from '@bitgo/sdk-core'; +import crypto from 'crypto'; +import { CryptoKeyFormat, SigningAlgorithmSpec, SigningKeySpec } from './constant'; export class Utils implements BaseUtils { /** @inheritdoc */ @@ -18,7 +20,7 @@ export class Utils implements BaseUtils { /** @inheritdoc */ isValidPublicKey(key: string): boolean { - throw new Error('Method not implemented.'); + return isValidEd25519PublicKey(key); } /** @inheritdoc */ @@ -30,6 +32,69 @@ export class Utils implements BaseUtils { isValidTransactionId(txId: string): boolean { throw new Error('Method not implemented.'); } + + /** + * Converts a base64-encoded Ed25519 public key string into a structured signing public key object. + * @param {String} publicKey The base64-encoded Ed25519 public key + * @returns {Object} The structured signing key object formatted for use with cryptographic operations + * @private + */ + private signingPublicKeyFromEd25519(publicKey: string): { + format: number; + publicKey: Buffer; + scheme: number; + keySpec: number; + usage: []; + } { + return { + format: CryptoKeyFormat.RAW, + publicKey: Buffer.from(publicKey, 'base64'), + scheme: SigningAlgorithmSpec.ED25519, + keySpec: SigningKeySpec.EC_CURVE25519, + usage: [], + }; + } + + /** + * Creates a buffer with a 4-byte big-endian integer prefix followed by the provided byte buffer + * @param {Number} value The integer to prefix, written as 4 bytes in big-endian order + * @param {Buffer} bytes The buffer to append after the integer prefix + * @returns {Buffer} The resulting buffer with the prefixed integer + * @private + */ + private prefixedInt(value: number, bytes: Buffer): Buffer { + const buffer = Buffer.alloc(4 + bytes.length); + buffer.writeUInt32BE(value, 0); + Buffer.from(bytes).copy(buffer, 4); + return buffer; + } + + /** + * Computes an SHA-256 Canton-style hash by prefixing the input with a purpose identifier, + * then hashing the resulting buffer and prepending a multi-prefix + * + * @param {Number} purpose A numeric identifier to prefix the hash input with + * @param {Buffer} bytes The buffer to be hashed + * @returns {String} A hexadecimal string representation of the resulting hash with multi-prefix + * @private + */ + private computeSha256CantonHash(purpose: number, bytes: Buffer): string { + const hashInput = this.prefixedInt(purpose, bytes); + const hash = crypto.createHash('sha256').update(hashInput).digest(); + const multiprefix = Buffer.from([0x12, 0x20]); + return Buffer.concat([multiprefix, hash]).toString('hex'); + } + + /** + * Method to create fingerprint (part of the canton partyId) from public key + * @param {String} publicKey the public key + * @returns {String} + */ + getAddressFromPublicKey(publicKey: string): string { + const key = this.signingPublicKeyFromEd25519(publicKey); + const hashPurpose = 12; + return this.computeSha256CantonHash(hashPurpose, key.publicKey); + } } const utils = new Utils(); diff --git a/modules/sdk-coin-canton/test/unit/keyPair.ts b/modules/sdk-coin-canton/test/unit/keyPair.ts new file mode 100644 index 0000000000..832ba5dc73 --- /dev/null +++ b/modules/sdk-coin-canton/test/unit/keyPair.ts @@ -0,0 +1,77 @@ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Eddsa } from '@bitgo/sdk-core'; +import { Ed25519Bip32HdTree, HDTree } from '@bitgo/sdk-lib-mpc'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import assert from 'assert'; +import should from 'should'; + +import { KeyPair, Tcanton } from '../../src'; +import utils from '../../src/lib/utils'; + +describe('Canton KeyPair', function () { + let rootKeychain: string; + let rootPublicKey: string; + let MPC: Eddsa; + let hdTree: HDTree; + let bitgo: TestBitGoAPI; + let basecoin: Tcanton; + + before(async () => { + hdTree = await Ed25519Bip32HdTree.initialize(); + MPC = await Eddsa.initialize(hdTree); + const A = MPC.keyShare(1, 2, 3); + const B = MPC.keyShare(2, 2, 3); + const C = MPC.keyShare(3, 2, 3); + + const A_combine = MPC.keyCombine(A.uShare, [B.yShares[1], C.yShares[1]]); + + const commonKeychain = A_combine.pShare.y + A_combine.pShare.chaincode; + rootKeychain = MPC.deriveUnhardened(commonKeychain, 'm/0'); + rootPublicKey = Buffer.from(rootKeychain.slice(0, 64), 'hex').toString('hex'); + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('tcanton', Tcanton.createInstance); + basecoin = bitgo.coin('tcanton') as Tcanton; + }); + + describe('should create a valid KeyPair', () => { + it('from an empty value', async () => { + const keyPair = new KeyPair(); + should.exists(keyPair.getKeys().prv); + should.exists(keyPair.getKeys().pub); + const address = utils.getAddressFromPublicKey(keyPair.getKeys().pub); + should.exists(address); + }); + }); + + describe('Keypair from derived Public Key', () => { + it('should create keypair with just derived public key', () => { + const keyPair = new KeyPair({ pub: rootPublicKey }); + keyPair.getKeys().pub.should.equal(rootPublicKey); + }); + + it('should derived ed25519 public key should be valid', () => { + utils.isValidPublicKey(rootPublicKey).should.be.true(); + }); + }); + + describe('Keypair from random seed', () => { + it('should generate a keypair from random seed', function () { + const keyPair = basecoin.generateKeyPair(); + keyPair.should.have.property('pub'); + keyPair.should.have.property('prv'); + if (keyPair.pub) { + basecoin.isValidPub(keyPair.pub).should.equal(true); + } + }); + }); + + describe('should fail to create a KeyPair', function () { + it('from an invalid public key', () => { + const source = { + pub: '01D63D', + }; + + assert.throws(() => new KeyPair(source)); + }); + }); +});