Skip to content

Commit

Permalink
keychain: EC key name
Browse files Browse the repository at this point in the history
  • Loading branch information
yoursunny committed Oct 3, 2019
1 parent 45d741f commit a10445e
Show file tree
Hide file tree
Showing 15 changed files with 178 additions and 46 deletions.
1 change: 1 addition & 0 deletions packages/keychain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@ndn/l3pkt": "workspace:*",
"@ndn/name": "workspace:*",
"@ndn/naming-convention-03": "workspace:*",
"@peculiar/webcrypto": "^1.0.19",
"applymixins": "^1.1.0",
"idb-keyval": "^3.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/keychain/src/cert/build.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Data, LLSign } from "@ndn/l3pkt";

import { PrivateKey } from "../key";
import { CertificateName } from "../name";

import { ContentTypeKEY } from "./an";
import { Certificate } from "./certificate";
import { CertificateName } from "./name";
import { ValidityPeriod } from "./validity-period";

interface Options {
Expand Down
9 changes: 6 additions & 3 deletions packages/keychain/src/cert/certificate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Data } from "@ndn/l3pkt";

import { CertificateName } from "../name";

import { ContentTypeKEY } from "./an";
import { CertificateName } from "./name";
import { ValidityPeriod } from "./validity-period";

/**
Expand All @@ -10,14 +11,16 @@ import { ValidityPeriod } from "./validity-period";
* To create a new Certificate, use buildCertificate function.
*/
export class Certificate {
public readonly name: CertificateName;
public readonly certName: CertificateName;
public readonly validity: ValidityPeriod;

public get name() { return this.data.name; }

/** Public key in SubjectPublicKeyInfo binary format. */
public get publicKey() { return this.data.content; }

constructor(public readonly data: Data) {
this.name = CertificateName.from(data.name);
this.certName = CertificateName.from(data.name);
if (this.data.contentType !== ContentTypeKEY) {
throw new Error("ContentType must be KEY");
}
Expand Down
1 change: 0 additions & 1 deletion packages/keychain/src/cert/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./build";
export * from "./certificate";
export * from "./name";
export * from "./validity-period";
30 changes: 0 additions & 30 deletions packages/keychain/src/cert/name.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/keychain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./cert";
export * from "./key";
export * from "./name";
2 changes: 1 addition & 1 deletion packages/keychain/src/key/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Verifiable = LLVerify.Verifiable & Readonly<PacketWithSignature>;
const ISKEY = Symbol("KeyChain.IsKey");

class NamedKey {
public [ISKEY] = ISKEY;
public readonly [ISKEY] = ISKEY;

constructor(public readonly name: Name, public readonly sigType: number,
public readonly keyLocator: KeyLocator|undefined) {
Expand Down
13 changes: 10 additions & 3 deletions packages/keychain/src/key/ec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { SigInfo, SigType } from "@ndn/l3pkt";
import { Name, NameLike } from "@ndn/name";

import { KeyName } from "../name";
import { crypto } from "../platform";

import { PrivateKeyBase, PublicKeyBase } from "./base";

export type EcCurve = "P-256" | "P-384" | "P-521";
Expand Down Expand Up @@ -38,15 +40,20 @@ export class EcPublicKey extends PublicKeyBase {
}

export namespace EcPrivateKey {
/** Generate ECDSA key pair. */
export async function generate(name: NameLike, curve: EcCurve): Promise<[EcPrivateKey, EcPublicKey]> {
/**
* Generate ECDSA key pair.
* @param name Name or URI as subjectName, or KeyName instance.
* @param curve EC curve.
*/
export async function generate(name: NameLike|KeyName, curve: EcCurve): Promise<[EcPrivateKey, EcPublicKey]> {
const { publicKey: pub, privateKey: pvt }: CryptoKeyPair = await crypto.subtle.generateKey(
// tslint:disable-next-line object-literal-sort-keys
{ name: "ECDSA", namedCurve: curve } as EcKeyGenParams,
false,
["sign", "verify"],
);
const n = new Name(name);

const n = KeyName.create(name).toName();
return [new EcPrivateKey(n, pvt), new EcPublicKey(n, pub)];
}
}
76 changes: 76 additions & 0 deletions packages/keychain/src/name/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Component, ComponentLike, Name, NameLike, TT } from "@ndn/name";
import { Timestamp } from "@ndn/naming-convention-03";

const KEY = new Component(TT.GenericNameComponent, "KEY");

class KeyNameBase {
public readonly subjectName: Name;
public readonly keyId: Component;

constructor(subjectName: NameLike, keyId: ComponentLike) {
this.subjectName = new Name(subjectName);
this.keyId = Component.from(keyId);
}
}

/** Key name in NDN Certificate Format v2. */
export class KeyName extends KeyNameBase {
/**
* Create a KeyName from Name, URI, or KeyName.
* If input is not a KeyName, it's interpreted as subjectName.
*/
public static create(input: NameLike|KeyName): KeyName {
if (input instanceof KeyName) {
return input;
}
const name = new Name(input);
try { return KeyName.from(name); } catch {}
const timestamp = Timestamp.create(new Date());
return new KeyName(name, timestamp);
}

/** Parse key name. */
public static from(name: Name): KeyName {
if (!name.at(-2).equals(KEY)) {
throw new Error("invalid key name");
}
return new KeyName(name.getPrefix(-2), name.get(-1)!);
}

/** Retrieve complete name. */
public toName(): Name {
return this.subjectName.append(KEY, this.keyId);
}
}

/** Certificate name in NDN Certificate Format v2. */
export class CertificateName extends KeyNameBase {
/** Parse certificate name. */
public static from(name: Name): CertificateName {
if (!name.at(-4).equals(KEY)) {
throw new Error("invalid certificate name");
}
return new CertificateName(name.getPrefix(-4),
...(name.slice(-3).comps as [Component, Component, Component]));
}

public readonly issuerId: Component;
public readonly version: Component;

constructor(subjectName: NameLike, keyId: ComponentLike,
issuerId: ComponentLike, version: ComponentLike) {
super(subjectName, keyId);
this.issuerId = Component.from(issuerId);
this.version = Component.from(version);
}

/** Retrieve complete name. */
public toName(): Name {
return this.subjectName.append(KEY, this.keyId, this.issuerId, this.version);
}

/** Derive key name. */
public toKeyName(): KeyName {
return new KeyName(this.subjectName, this.keyId);
}
}
9 changes: 5 additions & 4 deletions packages/keychain/tests/cert/certificate.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ const NDN_TESTBED_ROOT_V2_NDNCERT = Buffer.from(`
test("decode ndn-testbed-root-v2.ndncert", () => {
const data = new Decoder(NDN_TESTBED_ROOT_V2_NDNCERT).decode(Data);
const cert = new Certificate(data);
expect(cert.name.subjectName.toString()).toBe("/ndn");
expect(cert.name.keyId.toString()).toBe("e%9D%7F%A5%C5%81%10%7D");
expect(cert.name.issuerId.toString()).toBe("ndn");
expect(cert.name.version.toString()).toBe("%FD%00%00%01%60qJQ%9B");
expect(cert.name.toString()).toBe("/ndn/KEY/e%9D%7F%A5%C5%81%10%7D/ndn/%FD%00%00%01%60qJQ%9B");
expect(cert.certName.subjectName.toString()).toBe("/ndn");
expect(cert.certName.keyId.toString()).toBe("e%9D%7F%A5%C5%81%10%7D");
expect(cert.certName.issuerId.toString()).toBe("ndn");
expect(cert.certName.version.toString()).toBe("%FD%00%00%01%60qJQ%9B");
expect(cert.validity.notBefore.getTime()).toBe(1513729179000);
expect(cert.validity.notAfter.getTime()).toBe(1609459199000);
expect(cert.publicKey).toEqualUint8Array(Buffer.from(`
Expand Down
10 changes: 7 additions & 3 deletions packages/keychain/tests/key/ec.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@ const TABLE = TestSignVerify.TABLE.flatMap((row) => [
]) as Row[];

test.each(TABLE)("%p", async ({ cls, curve }) => {
const [pvtA, pubA] = await EcPrivateKey.generate("/ECKEY-A", curve);
const [pvtB, pubB] = await EcPrivateKey.generate("/ECKEY-B", curve);
const [pvtA, pubA] = await EcPrivateKey.generate("/ECKEY-A/KEY/x", curve);
const [pvtB, pubB] = await EcPrivateKey.generate("/ECKEY-B/KEY/x", curve);
expect(PrivateKey.isPrivateKey(pvtA)).toBeTruthy();
expect(PrivateKey.isPrivateKey(pubA)).toBeFalsy();
expect(PublicKey.isPublicKey(pvtA)).toBeFalsy();
expect(PublicKey.isPublicKey(pubA)).toBeTruthy();
expect(pvtA.name.toString()).toBe("/ECKEY-A/KEY/x");
expect(pubA.name.toString()).toBe("/ECKEY-A/KEY/x");
expect(pvtB.name.toString()).toBe("/ECKEY-B/KEY/x");
expect(pubB.name.toString()).toBe("/ECKEY-B/KEY/x");

const record = await TestSignVerify.execute(cls, pvtA, pubA, pvtB, pubB);
TestSignVerify.check(record, false, false);
expect(record.sA0.sigInfo.type).toBe(SigType.Sha256WithEcdsa);
expect(record.sA0.sigInfo.keyLocator).toBeInstanceOf(Name);
expect((record.sA0.sigInfo.keyLocator as Name).toString()).toBe("/ECKEY-A");
expect((record.sA0.sigInfo.keyLocator as Name).toString()).toBe(pvtA.name.toString());
});
26 changes: 26 additions & 0 deletions packages/keychain/tests/name/cert-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Component, Name } from "@ndn/name";
import { Version } from "@ndn/naming-convention-03";

import { CertificateName } from "../../src";

test("construct", () => {
const cn = new CertificateName(new Name("/owner"), Component.from("keyid"),
Component.from("issuer"), Version.create(2));
expect(cn.subjectName.toString()).toBe("/owner");
expect(cn.keyId.toString()).toBe("keyid");
expect(cn.issuerId.toString()).toBe("issuer");
expect(cn.version.toString()).toBe("35=%02");
expect(cn.toName().toString()).toBe("/owner/KEY/keyid/issuer/35=%02");
expect(cn.toKeyName().toName().toString()).toBe("/owner/KEY/keyid");
});

test("from", () => {
const cn = CertificateName.from(new Name("/owner/KEY/keyid"));
expect(cn.subjectName.toString()).toBe("/owner");
expect(cn.keyId.toString()).toBe("keyid");
expect(cn.issuerId.toString()).toBe("issuer");
expect(cn.version.toString()).toBe("35=%02");
expect(cn.toName().toString()).toBe("/owner/KEY/keyid/issuer/35=%02");

expect(() => CertificateName.from(new Name("/owner/keyid/issuer/35=%02"))).toThrow(/invalid/);
});
41 changes: 41 additions & 0 deletions packages/keychain/tests/name/key-name.t.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Component, Name } from "@ndn/name";
import { Timestamp } from "@ndn/naming-convention-03";

import { KeyName } from "../../src";

test("construct", () => {
const kn = new KeyName(new Name("/owner"), Component.from("keyid"));
expect(kn.subjectName.toString()).toBe("/owner");
expect(kn.keyId.toString()).toBe("keyid");
expect(kn.toName().toString()).toBe("/owner/KEY/keyid");
});

test("from", () => {
const kn = KeyName.from(new Name("/owner/KEY/keyid"));
expect(kn.subjectName.toString()).toBe("/owner");
expect(kn.keyId.toString()).toBe("keyid");
expect(kn.toName().toString()).toBe("/owner/KEY/keyid");

expect(() => KeyName.from(new Name("/owner/keyid"))).toThrow(/invalid/);
});

test("create from subjectName", () => {
const kn = KeyName.create("/owner");
expect(kn.subjectName.toString()).toBe("/owner");
expect(kn.keyId.is(Timestamp)).toBeTruthy();

const name = kn.toName();
expect(name).toHaveLength(3);
expect(name.getPrefix(2).toString()).toBe("/owner/KEY");
expect(name.at(-1).is(Timestamp)).toBeTruthy();
});

test("create from keyName", () => {
const kn0 = new KeyName(new Name("/owner"), Component.from("keyid"));

const kn1 = KeyName.create(kn0);
expect(kn1.toName().toString()).toBe(kn0.toName().toString());

const kn2 = KeyName.create(kn0.toName());
expect(kn2.toName().toString()).toBe(kn0.toName().toString());
});
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"rules": {
"interface-name": [true, "never-prefix"],
"max-classes-per-file": false,
"no-empty": [true, "allow-empty-catch"],
"no-namespace": false,
"no-shadowed-variable": false,
"object-literal-sort-keys": [true, "match-declaration-order"],
Expand Down

0 comments on commit a10445e

Please sign in to comment.