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
14 changes: 12 additions & 2 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StaticsBaseCoin>;
Expand Down Expand Up @@ -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 */
Expand Down
11 changes: 11 additions & 0 deletions modules/sdk-coin-canton/src/lib/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const CryptoKeyFormat = {
RAW: 2,
};

export const SigningKeySpec = {
EC_CURVE25519: 1,
};

export const SigningAlgorithmSpec = {
ED25519: 1,
};
25 changes: 21 additions & 4 deletions modules/sdk-coin-canton/src/lib/keyPair.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
69 changes: 67 additions & 2 deletions modules/sdk-coin-canton/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -18,7 +20,7 @@ export class Utils implements BaseUtils {

/** @inheritdoc */
isValidPublicKey(key: string): boolean {
throw new Error('Method not implemented.');
return isValidEd25519PublicKey(key);
}

/** @inheritdoc */
Expand All @@ -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();
Expand Down
77 changes: 77 additions & 0 deletions modules/sdk-coin-canton/test/unit/keyPair.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});
});