Skip to content

Commit

Permalink
ed25519 tests
Browse files Browse the repository at this point in the history
  • Loading branch information
krpeacock committed Dec 18, 2023
1 parent dd3f9d3 commit f712a67
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 48 deletions.
3 changes: 2 additions & 1 deletion packages/agent/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export type Signature = ArrayBuffer & { __signature__: void };
*/
export interface PublicKey {
toDer(): DerEncodedPublicKey;
// rawKey and derKey are optional for backwards compatibility.
// rawKey, toRaw, and derKey are optional for backwards compatibility.
toRaw?(): ArrayBuffer;
rawKey?: ArrayBuffer;
derKey?: DerEncodedPublicKey;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/utils/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function toHex(buffer: ArrayBuffer): string {
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
}

const hexRe = new RegExp(/^([0-9A-F]{2})*$/i);
const hexRe = new RegExp(/^[0-9a-fA-F]+$/);

/**
* Transforms a hexadecimal string into an array buffer.
Expand Down
15 changes: 0 additions & 15 deletions packages/identity-secp256k1/src/buffer.ts

This file was deleted.

29 changes: 12 additions & 17 deletions packages/identity-secp256k1/src/secp256k1.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { DerEncodedPublicKey, PublicKey } from '@dfinity/agent';
import { toHexString } from '@dfinity/candid/lib/cjs/utils/buffer';
import { DerEncodedPublicKey, PublicKey, fromHex, toHex } from '@dfinity/agent';
import { randomBytes } from 'crypto';
import { sha256 } from '@noble/hashes/sha256';
import { secp256k1 } from '@noble/curves/secp256k1';
import { Secp256k1KeyIdentity, Secp256k1PublicKey } from './secp256k1';

function fromHexString(hexString: string): ArrayBuffer {
return new Uint8Array((hexString.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16))).buffer;
}

// DER KEY SECP256K1 PREFIX = 3056301006072a8648ce3d020106052b8104000a03420004
// These test vectors contain the hex encoding of the corresponding raw and DER versions
// of secp256k1 keys that were generated using OpenSSL as follows:
Expand Down Expand Up @@ -40,7 +35,7 @@ const goldenSeed = '8caa0410fa5955c05d6877801806f627e5dd313957a59c70f8d8ef252a48
describe('Secp256k1PublicKey Tests', () => {
test('create from an existing public key', () => {
testVectors.forEach(([rawPublicKeyHex]) => {
const publicKey: PublicKey = Secp256k1PublicKey.fromRaw(fromHexString(rawPublicKeyHex));
const publicKey: PublicKey = Secp256k1PublicKey.fromRaw(fromHex(rawPublicKeyHex));

const newKey = Secp256k1PublicKey.from(publicKey);
expect(newKey).toMatchSnapshot();
Expand All @@ -49,8 +44,8 @@ describe('Secp256k1PublicKey Tests', () => {

test('DER encoding of SECP256K1 keys', async () => {
testVectors.forEach(([rawPublicKeyHex, derEncodedPublicKeyHex]) => {
const publicKey = Secp256k1PublicKey.fromRaw(fromHexString(rawPublicKeyHex));
const expectedDerPublicKey = fromHexString(derEncodedPublicKeyHex);
const publicKey = Secp256k1PublicKey.fromRaw(fromHex(rawPublicKeyHex));
const expectedDerPublicKey = fromHex(derEncodedPublicKeyHex);
expect(publicKey.toDer()).toEqual(expectedDerPublicKey);
});
});
Expand All @@ -59,15 +54,15 @@ describe('Secp256k1PublicKey Tests', () => {
// Too short.
expect(() => {
Secp256k1PublicKey.fromDer(
fromHexString(
fromHex(
'3056301006072a8648ce3d020106052b8104000a0342000401ec030acd7d1199f73ae3469329c114944e0693c89502f850bcc6bad397a5956767c79b410c29ac6f587eec84878020fdb54ba002a79b02aa153fe47b6',
) as DerEncodedPublicKey,
);
}).toThrowError('DER payload mismatch: Expected length 65 actual length 63');
// Too long.
expect(() => {
Secp256k1PublicKey.fromDer(
fromHexString(
fromHex(
'3056301006072a8648ce3d020106052b8104000a0342000401ec030acd7d1199f73ae3469329c114944e0693c89502f850bcc6bad397a5956767c79b410c29ac6f587eec84878020fdb54ba002a79b02aa153fe47b6ffd33' +
'1b42211ce',
) as DerEncodedPublicKey,
Expand All @@ -77,7 +72,7 @@ describe('Secp256k1PublicKey Tests', () => {
// Invalid DER-encoding.
expect(() => {
Secp256k1PublicKey.fromDer(
fromHexString(
fromHex(
'0693c89502f850bcc6bad397a5956767c79b410c29ac6f54fdac09ea93a1b9b744b5f19f091ada7978ceb2f045875bca8ef9b75fa8061704e76de023c6a23d77a118c5c8d0f5efaf0dbbfcc3702d5590604717f639f6f00d',
) as DerEncodedPublicKey,
);
Expand Down Expand Up @@ -131,11 +126,11 @@ describe('Secp256k1KeyIdentity Tests', () => {
});

test('generation from a seed should be supported', () => {
const seed = new Uint8Array(fromHexString(goldenSeed));
const seed = new Uint8Array(fromHex(goldenSeed));
const identity = Secp256k1KeyIdentity.generate(seed);
const publicKey = identity.getKeyPair().publicKey as Secp256k1PublicKey;
publicKey.toRaw();
expect(toHexString(publicKey.toRaw())).toEqual(
expect(toHex(publicKey.toRaw())).toEqual(
'04e2abe3b762fe0553f690d25f5100259b7eaeb3e476df6bd3dfc1d27e5dae56dad5a84f70bd87acc95ad54af0285f28c2be1e3b2f62a28a2fbad9fe44c84dc904',
);
});
Expand Down Expand Up @@ -195,7 +190,7 @@ describe('Secp256k1KeyIdentity Tests', () => {
});
});

describe('public key serialization from', () => {
describe('public key serialization from various types', () => {
it('should serialize from an existing public key', () => {
const baseKey = Secp256k1KeyIdentity.generate();
const publicKey: PublicKey = baseKey.getPublicKey();
Expand Down Expand Up @@ -225,7 +220,7 @@ describe('public key serialization from', () => {
});
it('should serialize from a hex string', () => {
const baseKey = Secp256k1KeyIdentity.generate();
const publicKey = toHexString(baseKey.getPublicKey().toRaw());
const publicKey = toHex(baseKey.getPublicKey().toRaw());
const newKey = Secp256k1PublicKey.from(publicKey);
expect(newKey).toBeDefined();
});
Expand All @@ -235,6 +230,6 @@ describe('public key serialization from', () => {
expect(shouldFail).toThrow('Cannot construct Secp256k1PublicKey from the provided key.');

const shouldFailHex = () => Secp256k1PublicKey.from('not a hex string');
expect(shouldFailHex).toThrow('A Secp256k1 public key must be exactly 33 or 65 bytes long');
expect(shouldFailHex).toThrow('Invalid hexadecimal string');
});
});
18 changes: 8 additions & 10 deletions packages/identity-secp256k1/src/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
Signature,
uint8ToBuf,
bufFromBufLike,
fromHex,
toHex,
} from '@dfinity/agent';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { randomBytes } from '@noble/hashes/utils';
import hdkey from 'hdkey';
import { mnemonicToSeedSync } from 'bip39';
import { PublicKey, SignIdentity } from '@dfinity/agent';
import { fromHexString, toHexString } from './buffer';
import { SECP256K1_OID, unwrapDER, wrapDER } from './der';

declare type PublicKeyHex = string;
Expand Down Expand Up @@ -41,7 +42,7 @@ export class Secp256k1PublicKey implements PublicKey {
*/
public static from(maybeKey: unknown): Secp256k1PublicKey {
if (typeof maybeKey === 'string') {
const key = fromHexString(maybeKey);
const key = fromHex(maybeKey);
return this.fromRaw(key);
} else if (isObject(maybeKey)) {
const key = maybeKey as KeyLike;
Expand Down Expand Up @@ -87,9 +88,6 @@ export class Secp256k1PublicKey implements PublicKey {

// `fromRaw` and `fromDer` should be used for instantiation, not this constructor.
private constructor(key: ArrayBuffer) {
if (key.byteLength !== 33 && key.byteLength !== 65) {
throw new Error('A Secp256k1 public key must be exactly 33 or 65 bytes long');
}
this.#rawKey = bufFromBufLike(key);
this.#derKey = Secp256k1PublicKey.derEncode(key);
}
Expand Down Expand Up @@ -141,8 +139,8 @@ export class Secp256k1KeyIdentity extends SignIdentity {
public static fromParsedJson(obj: JsonableSecp256k1Identity): Secp256k1KeyIdentity {
const [publicKeyRaw, privateKeyRaw] = obj;
return new Secp256k1KeyIdentity(
Secp256k1PublicKey.fromRaw(fromHexString(publicKeyRaw)),
fromHexString(privateKeyRaw),
Secp256k1PublicKey.fromRaw(fromHex(publicKeyRaw)),
fromHex(privateKeyRaw),
);
}

Expand Down Expand Up @@ -216,7 +214,7 @@ export class Secp256k1KeyIdentity extends SignIdentity {
* @returns {JsonableSecp256k1Identity}
*/
public toJSON(): JsonableSecp256k1Identity {
return [toHexString(this._publicKey.toRaw()), toHexString(this._privateKey)];
return [toHex(this._publicKey.toRaw()), toHex(this._privateKey)];
}

/**
Expand All @@ -232,9 +230,9 @@ export class Secp256k1KeyIdentity extends SignIdentity {

/**
* Return the public key.
* @returns {Secp256k1PublicKey}
* @returns {Required<PublicKey>}
*/
public getPublicKey(): Secp256k1PublicKey {
public getPublicKey(): Required<PublicKey> {
return this._publicKey;
}

Expand Down
46 changes: 45 additions & 1 deletion packages/identity/src/identity/ed25519.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DerEncodedPublicKey, fromHex } from '@dfinity/agent';
import { DerEncodedPublicKey, PublicKey, fromHex, toHex } from '@dfinity/agent';
import { Ed25519KeyIdentity, Ed25519PublicKey } from './ed25519';

const testVectors: Array<[string, string]> = [
Expand Down Expand Up @@ -129,3 +129,47 @@ test('from JSON', async () => {
const isValid = Ed25519KeyIdentity.verify(msg, signature, identity.getPublicKey().rawKey);
expect(isValid).toBe(true);
});

describe('public key serialization from various types', () => {
it('should serialize from an existing public key', () => {
const baseKey = Ed25519KeyIdentity.generate();
const publicKey: PublicKey = baseKey.getPublicKey();
const newKey = Ed25519PublicKey.from(publicKey);
expect(newKey).toBeDefined();
});
it('should serialize from a raw key', () => {
const baseKey = Ed25519KeyIdentity.generate();
const publicKey = baseKey.getPublicKey().rawKey;
ArrayBuffer.isView(publicKey); //?
publicKey instanceof ArrayBuffer; //?

const newKey = Ed25519PublicKey.from(publicKey);
expect(newKey).toBeDefined();
});
it('should serialize from a DER key', () => {
const baseKey = Ed25519KeyIdentity.generate();
const publicKey = baseKey.getPublicKey().derKey;
const newKey = Ed25519PublicKey.from(publicKey);
expect(newKey).toBeDefined();
});
it('should serialize from a Uint8Array', () => {
const baseKey = Ed25519KeyIdentity.generate();
const publicKey = new Uint8Array(baseKey.getPublicKey().toRaw());
const newKey = Ed25519PublicKey.from(publicKey);
expect(newKey).toBeDefined();
});
it('should serialize from a hex string', () => {
const baseKey = Ed25519KeyIdentity.generate();
const publicKey = toHex(baseKey.getPublicKey().toRaw());
const newKey = Ed25519PublicKey.from(publicKey);
expect(newKey).toBeDefined();
});
it('should fail to parse an invalid key', () => {
const baseKey = 7;
const shouldFail = () => Ed25519PublicKey.from(baseKey as unknown);
expect(shouldFail).toThrow('Cannot construct Ed25519PublicKey from the provided key.');

const shouldFailHex = () => Ed25519PublicKey.from('not a hex string');
expect(shouldFailHex).toThrow('Invalid hexadecimal string');
});
});
35 changes: 32 additions & 3 deletions packages/identity/src/identity/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,39 @@ import {
wrapDER,
fromHex,
toHex,
bufFromBufLike,
} from '@dfinity/agent';
import { ed25519 } from '@noble/curves/ed25519';

declare type KeyLike = PublicKey | DerEncodedPublicKey | ArrayBuffer | ArrayBufferView;

function isObject(value: unknown) {
return value !== null && typeof value === 'object';
}

export class Ed25519PublicKey implements PublicKey {
public static from(key: PublicKey): Ed25519PublicKey {
return this.fromDer(key.toDer());
public static from(maybeKey: unknown): Ed25519PublicKey {
if (typeof maybeKey === 'string') {
const key = fromHex(maybeKey);
return this.fromRaw(key);
} else if (isObject(maybeKey)) {
const key = maybeKey as KeyLike;
if (isObject(key) && Object.hasOwnProperty.call(key, '__derEncodedPublicKey__')) {
return this.fromDer(key as DerEncodedPublicKey);
} else if (ArrayBuffer.isView(key)) {
const view = key as ArrayBufferView;
return this.fromRaw(bufFromBufLike(view.buffer));
} else if (key instanceof ArrayBuffer) {
return this.fromRaw(key);
} else if ('rawKey' in key) {
return this.fromRaw(key.rawKey as ArrayBuffer);
} else if ('derKey' in key) {
return this.fromDer(key.derKey as DerEncodedPublicKey);
} else if ('toDer' in key) {
return this.fromDer(key.toDer() as ArrayBuffer);
}
}
throw new Error('Cannot construct Ed25519PublicKey from the provided key.');
}

public static fromRaw(rawKey: ArrayBuffer): Ed25519PublicKey {
Expand All @@ -30,7 +57,9 @@ export class Ed25519PublicKey implements PublicKey {
private static RAW_KEY_LENGTH = 32;

private static derEncode(publicKey: ArrayBuffer): DerEncodedPublicKey {
return wrapDER(publicKey, ED25519_OID).buffer as DerEncodedPublicKey;
const key = wrapDER(publicKey, ED25519_OID).buffer as DerEncodedPublicKey;
key.__derEncodedPublicKey__ = undefined;
return key;
}

private static derDecode(key: DerEncodedPublicKey): ArrayBuffer {
Expand Down

0 comments on commit f712a67

Please sign in to comment.