Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Cardano key derivation according to CIP3-Icarus #158

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e656167
Add `cip3Icarus` fixtures
PeterBenc Mar 6, 2024
d51f073
Add `ed25519Bip32` derivation curve
PeterBenc Mar 6, 2024
d054935
Differentiate master node derivation based on `curve` `masterNodeGene…
PeterBenc Mar 6, 2024
996576d
Support 64 byte long private keys
PeterBenc Mar 6, 2024
03034a5
Support fingerprints from 32 byte long public keys
PeterBenc Mar 6, 2024
b8b70a8
Allow `littleEndian` in `numberToUint32` util
PeterBenc Mar 6, 2024
be62e89
Add `cip3Icarus` deriver
PeterBenc Mar 6, 2024
e6cf55b
Add `CIP3IcarusNode`
PeterBenc Mar 6, 2024
c22acb3
Generate and test `cip3Icarus` vectors
PeterBenc Mar 6, 2024
48d0dfa
Add `cip3Icarus` test vectors
PeterBenc Mar 6, 2024
f7a56f2
Rename `cip3Icarus` to `cip3`
PeterBenc Mar 6, 2024
97c8050
fixup! rename `cip3Icarus` to `cip3` in derivation test vectors
PeterBenc Mar 6, 2024
c719f5a
Add `compressedPublicKeyLength` curve parameter
PeterBenc Mar 6, 2024
f18654a
Refactor `bip39` deriveChildKey `masterNodeGenerationSpec` condition …
PeterBenc Mar 6, 2024
8d16e64
fixup! curve specification to separate type
PeterBenc Mar 6, 2024
004ed52
fixup! remove underscore
PeterBenc Mar 6, 2024
8a51dd8
fixup! inline unnecessary functions
PeterBenc Mar 6, 2024
23bca32
fixup! add JSDOCs to ed25519Bip32 file
PeterBenc Mar 6, 2024
70d89a3
fixup! quotes in src/curves/ed25519Bip32.test.ts
PeterBenc Mar 6, 2024
8ce367b
fixup! quotes in src/curves/ed25519Bip32.test.ts
PeterBenc Mar 6, 2024
2ebaf4e
fixyup! empty line in src/derivation.test.ts
PeterBenc Mar 6, 2024
44bec07
fixup! `eslint-enable no-bitwise` in src/derivers/bip39.ts
PeterBenc Mar 6, 2024
963fd3a
fixup! add empty lines between test in `cip3` file
PeterBenc Mar 6, 2024
08288d3
fixup! add JSDOCs to `cip3` file
PeterBenc Mar 6, 2024
94ff5d3
fixup! `_` in src/curves/ed25519Bip32.ts
PeterBenc Mar 6, 2024
269db31
fixup! improve error message in src/derivers/cip3.ts
PeterBenc Mar 6, 2024
ab81cfa
fixup! improve error message in src/utils.ts
PeterBenc Mar 6, 2024
f2233e5
fixup! fix quotes and test in fixes from github
PeterBenc Mar 6, 2024
68f5ade
Test all fns in `ed25519Bip32`
PeterBenc Mar 6, 2024
af22882
Test all fns in `cip3`
PeterBenc Mar 6, 2024
e6b759a
fixup! add test to have 100% coverage
PeterBenc Mar 6, 2024
31f072b
fixup! test coverage
PeterBenc Mar 7, 2024
5979566
fixup! lint
PeterBenc Mar 7, 2024
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
20 changes: 13 additions & 7 deletions scripts/generate-vectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
ed25519,
MAX_BIP_32_INDEX,
secp256k1,
ed25519Bip32,
} from '../src';
import type { Curve } from '../src/curves';
import { createBip39KeyFromSeed } from '../src/derivers/bip39';
import { type Curve } from '../src/curves';
import { deriveChildKey } from '../src/derivers/bip39';

/**
* Get a random boolean value.
Expand Down Expand Up @@ -58,7 +59,7 @@ function getRandomSeed(length = randomInt(16, 64)) {
* @returns A random BIP-32 path.
*/
function getRandomPath(
spec: 'bip32' | 'slip10',
spec: 'bip32' | 'slip10' | 'cip3',
length = randomInt(1, 20),
hardened?: boolean,
) {
Expand All @@ -84,7 +85,7 @@ function getRandomPath(
*/
async function getRandomKeyVector(
node: SLIP10Node,
spec: 'bip32' | 'slip10',
spec: 'bip32' | 'slip10' | 'cip3',
hardened?: boolean,
) {
const path = getRandomPath(spec, undefined, hardened);
Expand Down Expand Up @@ -116,13 +117,13 @@ async function getRandomKeyVector(
* @param curve - The curve to use. Defaults to secp256k1.
*/
async function getRandomVector(
spec: 'bip32' | 'slip10',
spec: 'bip32' | 'slip10' | 'cip3',
amount = 10,
hardened?: boolean,
curve: Curve = secp256k1,
) {
const seed = getRandomSeed();
const node = await createBip39KeyFromSeed(seed, curve);
const node = await deriveChildKey({ path: seed, curve });

return {
hexSeed: bytesToHex(seed),
Expand Down Expand Up @@ -153,7 +154,7 @@ async function getRandomVector(
* @returns The random vectors.
*/
async function getRandomVectors(
spec: 'bip32' | 'slip10',
spec: 'bip32' | 'slip10' | 'cip3',
amount = 10,
hardened?: boolean,
curve: Curve = secp256k1,
Expand Down Expand Up @@ -185,6 +186,11 @@ async function getOutput() {
unhardened: await getRandomVectors('slip10', 50, false),
mixed: await getRandomVectors('bip32', 50),
},
cip3: {
hardened: await getRandomVectors('cip3', 50, true, ed25519Bip32),
unhardened: await getRandomVectors('cip3', 50, false, ed25519Bip32),
mixed: await getRandomVectors('cip3', 50, undefined, ed25519Bip32),
},
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/SLIP10Node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ describe('SLIP10Node', () => {
specification: 'bip32',
}),
).rejects.toThrow(
'Invalid curve: Only the following curves are supported: secp256k1, ed25519.',
'Invalid curve: Only the following curves are supported: secp256k1, ed25519, ed25519Bip32.',
);
});
});
Expand Down
10 changes: 8 additions & 2 deletions src/SLIP10Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ export class SLIP10Node implements SLIP10NodeInterface {
const curveObject = getCurveByName(curve);

if (privateKey) {
const privateKeyBytes = getBytesUnsafe(privateKey, BYTES_KEY_LENGTH);
const privateKeyBytes = getBytesUnsafe(
privateKey,
curveObject.privateKeyLength,
);
assert(
curveObject.isValidPrivateKey(privateKeyBytes),
`Invalid private key: Value is not a valid ${curve} private key.`,
Expand Down Expand Up @@ -344,7 +347,10 @@ export class SLIP10Node implements SLIP10NodeInterface {
}

public get fingerprint(): number {
return getFingerprint(this.compressedPublicKeyBytes);
return getFingerprint(
this.compressedPublicKeyBytes,
getCurveByName(this.curve).compressedPublicKeyLength,
);
}

/**
Expand Down
18 changes: 16 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export type HardenedSLIP10Node = `slip10:${number}'`;
export type UnhardenedSLIP10Node = `slip10:${number}`;
export type SLIP10PathNode = HardenedSLIP10Node | UnhardenedSLIP10Node;

export type HardenedCIP3Node = `cip3:${number}'`;
export type UnhardenedCIP3Node = `cip3:${number}`;
export type CIP3PathNode = HardenedCIP3Node | UnhardenedCIP3Node;

export const BIP44PurposeNodeToken = `bip32:44'`;

export const UNPREFIXED_PATH_REGEX = /^\d+$/u;
Expand All @@ -59,6 +63,13 @@ export const BIP_32_PATH_REGEX = /^bip32:\d+'?$/u;
*/
export const SLIP_10_PATH_REGEX = /^slip10:\d+'?$/u;

/**
* e.g.
* - cip3:0
* - cip3:0'
*/
export const CIP_3_PATH_REGEX = /^cip3:\d+'?$/u;

/**
* bip39:<SPACE_DELMITED_SEED_PHRASE>
*
Expand Down Expand Up @@ -164,10 +175,13 @@ export type HDPathTuple = RootedHDPathTuple | PartialHDPathTuple;

export type RootedSLIP10PathTuple = readonly [
BIP39Node,
...(BIP32Node[] | SLIP10PathNode[]),
...(BIP32Node[] | SLIP10PathNode[] | CIP3PathNode[]),
];

export type SLIP10PathTuple = readonly BIP32Node[] | readonly SLIP10PathNode[];
export type SLIP10PathTuple =
| readonly BIP32Node[]
| readonly SLIP10PathNode[]
| readonly CIP3PathNode[];
export type SLIP10Path = RootedSLIP10PathTuple | SLIP10PathTuple;

export type FullHDPathTuple = RootedHDPathTuple5;
17 changes: 15 additions & 2 deletions src/curves/curve.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import * as ed25519 from './ed25519';
import * as ed25519Bip32 from './ed25519Bip32';
import * as secp256k1 from './secp256k1';

export type SupportedCurve = keyof typeof curves;

export const curves = {
secp256k1,
ed25519,
ed25519Bip32,
};

type CurveSpecification =
| {
masterNodeGenerationSpec: 'slip10';
name: Extract<SupportedCurve, 'secp256k1' | 'ed25519'>;
}
| {
name: Extract<SupportedCurve, 'ed25519Bip32'>;
masterNodeGenerationSpec: 'cip3';
};

export type Curve = {
name: SupportedCurve;
secret: Uint8Array;
deriveUnhardenedKeys: boolean;
publicKeyLength: number;
Expand All @@ -24,7 +35,9 @@ export type Curve = {
publicAdd: (publicKey: Uint8Array, tweak: Uint8Array) => Uint8Array;
compressPublicKey: (publicKey: Uint8Array) => Uint8Array;
decompressPublicKey: (publicKey: Uint8Array) => Uint8Array;
};
privateKeyLength: number;
compressedPublicKeyLength: number;
} & CurveSpecification;

/**
* Get a curve by name.
Expand Down
6 changes: 6 additions & 0 deletions src/curves/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ export const decompressPublicKey = (publicKey: Uint8Array): Uint8Array => {
// Ed25519 public keys don't have a compressed form.
return publicKey;
};

export const privateKeyLength = 32;

export const masterNodeGenerationSpec = 'slip10';

export const compressedPublicKeyLength = 33;
90 changes: 90 additions & 0 deletions src/curves/ed25519Bip32.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { bytesToHex, hexToBytes } from '@metamask/utils';

import { ed25519Bip32 } from '.';
import fixtures from '../../test/fixtures';
import {
bytesToNumberLE,
compressPublicKey,
decompressPublicKey,
isValidPrivateKey,
multiplyWithBase,
} from './ed25519Bip32';

describe('getPublicKey', () => {
fixtures.cip3.forEach((fixture) => {
Object.values(fixture.nodes).forEach((node) => {
it('returns correct public key from private key', async () => {
const publicKey = await ed25519Bip32.getPublicKey(
hexToBytes(node.privateKey),
);

expect(bytesToHex(publicKey)).toBe(node.publicKey);
});
});
});
});

describe('publicAdd', () => {
it('returns correct public key from private key', async () => {
const publicKey = hexToBytes(fixtures.cip3[0].nodes.bip39Node.publicKey);
const tweak = hexToBytes(fixtures.cip3[0].nodes.purposeNode.publicKey);
const added = ed25519Bip32.publicAdd(publicKey, tweak);

expect(bytesToHex(added)).toBe(
'0xf78d2a445afe9c961ac196fbac282b499d9ab6bbe8801354ee06fc22d46503e2',
);
});
});

describe('isValidPrivateKey', () => {
it('returns true for bigint input', () => {
const { privateKey } = fixtures.cip3[0].nodes.bip39Node;
expect(isValidPrivateKey(privateKey)).toBe(true);
});
});

describe('compressPublicKey', () => {
it('returns the same Uint8Array that was input', () => {
const publicKey = Uint8Array.from(
Buffer.from(fixtures.cip3[0].nodes.bip39Node.publicKey, 'hex'),
);
expect(compressPublicKey(publicKey)).toStrictEqual(publicKey);
});
});

describe('decompressPublicKey', () => {
it('returns the same Uint8Array that was input', () => {
const publicKey = Uint8Array.from(
Buffer.from(fixtures.cip3[0].nodes.bip39Node.publicKey, 'hex'),
);
expect(decompressPublicKey(publicKey)).toStrictEqual(publicKey);
});
});

describe('bytesToNumberLE', () => {
it('converts bytes to little endian bignumber', () => {
const bytes = Uint8Array.from([
240, 230, 228, 13, 229, 184, 174, 13, 156, 72, 248, 206, 127, 130, 146,
49, 175, 244, 32, 215, 146, 255, 153, 93, 197, 96, 64, 249, 123, 140, 119,
72,
]);
expect(bytesToNumberLE(bytes)).toBe(
32777749485515042639882960539696351427945957558989008047469858024981459691248n,
);
});
});

describe('multiplyWithBase', () => {
it('multiplies bytes with the curve base', () => {
const bytes = Uint8Array.from([
240, 230, 228, 13, 229, 184, 174, 13, 156, 72, 248, 206, 127, 130, 146,
49, 175, 244, 32, 215, 146, 255, 153, 93, 197, 96, 64, 249, 123, 140, 119,
72,
]);
const expectedResult = Uint8Array.from([
64, 197, 223, 88, 143, 127, 45, 60, 205, 81, 148, 125, 195, 249, 173, 214,
27, 176, 227, 21, 216, 243, 146, 168, 189, 206, 85, 135, 89, 11, 210, 27,
]);
expect(multiplyWithBase(bytes)).toStrictEqual(expectedResult);
});
});
Loading
Loading