Skip to content

Commit df20551

Browse files
committed
ndndsec: avoid Node.js Crypto for PKCS#1 and SEC#1
1 parent 8472d40 commit df20551

14 files changed

Lines changed: 150 additions & 112 deletions

File tree

pkg/keychain/src/algo/ecdsa.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,32 @@ import { asBufferSource, assert, fromHex, toHex } from "@ndn/util";
33
import * as asn1 from "@yoursunny/asn1";
44

55
import type { CryptoAlgorithm, SigningAlgorithm } from "../key/mod";
6-
import { assertSpkiAlgorithm } from "./impl-spki";
6+
import { assertSpkiAlgorithm, toPkcs8 } from "./impl-asn1";
77

88
const SignVerifyParams: EcdsaParams = { name: "ECDSA", hash: "SHA-256" };
99

1010
function makeGenParams(curve: EcCurve): EcKeyGenParams & EcKeyImportParams {
1111
return { name: "ECDSA", namedCurve: curve };
1212
}
1313

14-
const PointSizes = {
15-
"P-256": 32,
16-
"P-384": 48,
17-
"P-521": 66,
14+
const EcPublicKeyOid = "2A8648CE3D0201"; // 1.2.840.10045.2.1
15+
16+
const NamedCurveInfo = {
17+
"P-256": [32, "2A8648CE3D030107"], // 1.2.840.10045.3.1.7
18+
"P-384": [48, "2B81040022"], // 1.3.132.0.34
19+
"P-521": [66, "2B81040023"], // 1.3.132.0.35
1820
} as const;
1921

20-
const NamedCurveOids: Record<string, EcCurve> = {
21-
"2A8648CE3D030107": "P-256", // 1.2.840.10045.3.1.7
22-
"2B81040022": "P-384", // 1.3.132.0.34
23-
"2B81040023": "P-521", // 1.3.132.0.35
24-
};
22+
const NamedCurveOids: Record<string, EcCurve> = Object.fromEntries(Object.entries(NamedCurveInfo).map(([curve, [,oid]]) => [oid, curve as EcCurve]));
2523

26-
export type EcCurve = keyof typeof PointSizes;
24+
export type EcCurve = keyof typeof NamedCurveInfo;
2725
export namespace EcCurve {
2826
export const Default: EcCurve = "P-256";
29-
export const Choices = Object.keys(PointSizes) as readonly EcCurve[];
27+
export const Choices = Object.keys(NamedCurveInfo) as readonly EcCurve[];
3028

3129
/** Detect EcCurve from SubjectPublicKeyInfo. */
3230
export function detectFromSpki(der: asn1.ElementBuffer): EcCurve {
33-
assertSpkiAlgorithm(der, "ECDSA", "2A8648CE3D0201"); // 1.2.840.10045.2.1
31+
assertSpkiAlgorithm(der, "ECDSA", EcPublicKeyOid);
3432

3533
// SubjectPublicKeyInfo.algorithm.parameter
3634
const ecp = der.children?.[0]?.children?.[1];
@@ -51,6 +49,26 @@ function toUintHex(array: Uint8Array): string {
5149
return toHex(array.subarray(msb));
5250
}
5351

52+
function stripSec1Parameters(input: Uint8Array): Uint8Array {
53+
// https://datatracker.ietf.org/doc/html/rfc5915#section-3
54+
// Delete ECParameters field from SEC#1 data.
55+
const sec1 = asn1.parseVerbose(input);
56+
let index: number;
57+
if (sec1.children && (index = sec1.children.findIndex((child) => child.type === 160)) >= 0) {
58+
sec1.children.splice(index, 1);
59+
}
60+
return asn1.pack(sec1);
61+
}
62+
63+
function loadPkcs8(pkcs8: Uint8Array, spki: Uint8Array, curve: EcCurve, extractable: boolean) {
64+
const params = makeGenParams(curve);
65+
return Promise.all([
66+
params, // eslint-disable-line @typescript-eslint/await-thenable
67+
crypto.subtle.importKey("pkcs8", asBufferSource(pkcs8), params, extractable, ECDSA.keyUsages.private),
68+
crypto.subtle.importKey("spki", asBufferSource(spki), params, true, ECDSA.keyUsages.public),
69+
]);
70+
}
71+
5472
/** Sha256WithEcdsa signing algorithm. */
5573
export const ECDSA: SigningAlgorithm<ECDSA.Info, true, ECDSA.GenParams> = {
5674
uuid: "a81b3696-65e5-4f4c-bb45-14125472321b",
@@ -60,18 +78,25 @@ export const ECDSA: SigningAlgorithm<ECDSA.Info, true, ECDSA.GenParams> = {
6078
public: ["verify"],
6179
},
6280

63-
async cryptoGenerate({ curve, importPkcs8 }: ECDSA.GenParams, extractable: boolean) {
81+
async cryptoGenerate({ curve, importPkcs8, importSec1 }: ECDSA.GenParams, extractable: boolean) {
6482
let params: ReturnType<typeof makeGenParams>;
6583
let privateKey: CryptoKey;
6684
let publicKey: CryptoKey;
6785
if (importPkcs8) {
6886
const [pkcs8, spki] = importPkcs8;
6987
curve ??= EcCurve.detectFromSpki(asn1.parseVerbose(spki));
70-
params = makeGenParams(curve);
71-
[privateKey, publicKey] = await Promise.all([
72-
crypto.subtle.importKey("pkcs8", asBufferSource(pkcs8), params, extractable, this.keyUsages.private),
73-
crypto.subtle.importKey("spki", asBufferSource(spki), params, true, this.keyUsages.public),
74-
]);
88+
[params, privateKey, publicKey] = await loadPkcs8(pkcs8, spki, curve, extractable);
89+
} else if (importSec1) {
90+
const [sec1, spki] = importSec1;
91+
curve ??= EcCurve.detectFromSpki(asn1.parseVerbose(spki));
92+
const pkcs8 = toPkcs8(
93+
[
94+
asn1.Any("06", EcPublicKeyOid), // OID
95+
asn1.Any("06", NamedCurveInfo[curve][1]),
96+
],
97+
stripSec1Parameters(sec1),
98+
);
99+
[params, privateKey, publicKey] = await loadPkcs8(pkcs8, spki, curve, extractable);
75100
} else {
76101
curve ??= EcCurve.Default;
77102
params = makeGenParams(curve);
@@ -105,7 +130,7 @@ export const ECDSA: SigningAlgorithm<ECDSA.Info, true, ECDSA.GenParams> = {
105130
makeLLSign({ privateKey, info: { curve } }: CryptoAlgorithm.PrivateKey<ECDSA.Info>) {
106131
return async (input) => {
107132
const raw = await crypto.subtle.sign(SignVerifyParams, privateKey, asBufferSource(input));
108-
const pointSize = PointSizes[curve];
133+
const pointSize = NamedCurveInfo[curve][0];
109134
return fromHex(asn1.Any(
110135
"30",
111136
asn1.UInt(toUintHex(new Uint8Array(raw, 0, pointSize))),
@@ -116,7 +141,7 @@ export const ECDSA: SigningAlgorithm<ECDSA.Info, true, ECDSA.GenParams> = {
116141

117142
makeLLVerify({ publicKey, info: { curve } }: CryptoAlgorithm.PublicKey<ECDSA.Info>) {
118143
return async (input, sig) => {
119-
const pointSize = PointSizes[curve];
144+
const pointSize = NamedCurveInfo[curve][0];
120145

121146
const der = asn1.parseVerbose(sig);
122147
const r = der.children?.[0]?.value;
@@ -153,6 +178,14 @@ export namespace ECDSA {
153178
* If {@link curve} is also specified, it must match the SPKI public key.
154179
*/
155180
importPkcs8?: [pkcs8: Uint8Array, spki: Uint8Array];
181+
182+
/**
183+
* Import SEC#1 private key and SPKI public key instead of generating.
184+
*
185+
* If {@link curve} is also specified, it must match the SPKI public key.
186+
* If {@link importPkcs8} is also specified, this field is ignored.
187+
*/
188+
importSec1?: [sec1: Uint8Array, spki: Uint8Array];
156189
}
157190

158191
export interface Info {

pkg/keychain/src/algo/ed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type * as asn1 from "@yoursunny/asn1";
44
import { Ed25519Algorithm, ponyfillEd25519 } from "@yoursunny/webcrypto-ed25519";
55

66
import type { CryptoAlgorithm, SigningAlgorithm } from "../key/mod";
7-
import { assertSpkiAlgorithm } from "./impl-spki";
7+
import { assertSpkiAlgorithm } from "./impl-asn1";
88

99
const subtle = ponyfillEd25519();
1010

pkg/keychain/src/algo/impl-asn1.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { fromHex, toHex } from "@ndn/util";
2+
import * as asn1 from "@yoursunny/asn1";
3+
4+
/**
5+
* Require SubjectPublicKeyInfo.algorithm.algorithm to have specific OID.
6+
* @param der - SubjectPublicKeyInfo.
7+
* @param algoName - Textual algorithm name.
8+
* @param oid - OID hex string (upper case).
9+
*/
10+
export function assertSpkiAlgorithm(der: asn1.ElementBuffer, algoName: string, oid: string): void {
11+
const algo = der.children?.[0]?.children?.[0];
12+
if (algo?.type === 0x06 && algo.value && toHex(algo.value) === oid) {
13+
return;
14+
}
15+
throw new Error(`not ${algoName} public key`);
16+
}
17+
18+
/**
19+
* Convert to ASN.1 PrivateKeyInfo (PKCS#8) format.
20+
* @param privateKeyAlgorithm - AlgorithmIdentifier elements.
21+
* @param privateKey - Value of private key.
22+
* @returns PKCS#8 buffer.
23+
*/
24+
export function toPkcs8(privateKeyAlgorithm: string[], privateKey: Uint8Array): Uint8Array {
25+
// https://datatracker.ietf.org/doc/html/rfc5208#section-5
26+
return fromHex(asn1.Any(
27+
"30", // PrivateKeyInfo
28+
asn1.UInt("00"), // Version 0
29+
asn1.Any( // PrivateKeyAlgorithmIdentifier
30+
"30",
31+
...privateKeyAlgorithm,
32+
),
33+
asn1.Any("04", toHex(privateKey)), // PrivateKey
34+
));
35+
}

pkg/keychain/src/algo/impl-spki.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

pkg/keychain/src/algo/rsa-common.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { asBufferSource } from "@ndn/util";
2-
import type * as asn1 from "@yoursunny/asn1";
2+
import * as asn1 from "@yoursunny/asn1";
33

44
import type { CryptoAlgorithm } from "../key/mod";
5-
import { assertSpkiAlgorithm } from "./impl-spki";
5+
import { assertSpkiAlgorithm, toPkcs8 } from "./impl-asn1";
66
import type { RSA } from "./rsa";
77

8+
const RsaOid = "2A864886F70D010101"; // 1.2.840.113549.1.1.1
9+
810
export type RsaModulusLength = (typeof RsaModulusLength.Choices)[number];
911
export namespace RsaModulusLength {
1012
export const Default: RsaModulusLength = 2048;
@@ -26,15 +28,21 @@ export abstract class RsaCommon implements CryptoAlgorithm<{}, true, RSA.GenPara
2628
protected readonly importParams: RsaHashedImportParams;
2729
protected readonly genParams: RsaHashedKeyGenParams;
2830

29-
async cryptoGenerate({ modulusLength = RsaModulusLength.Default, importPkcs8 }: RSA.GenParams, extractable: boolean) {
31+
async cryptoGenerate({ modulusLength = RsaModulusLength.Default, importPkcs8, importPkcs1 }: RSA.GenParams, extractable: boolean) {
3032
let privateKey: CryptoKey;
3133
let publicKey: CryptoKey;
3234
if (importPkcs8) {
33-
const [pkcs8, spki] = importPkcs8;
34-
[privateKey, publicKey] = await Promise.all([
35-
crypto.subtle.importKey("pkcs8", asBufferSource(pkcs8), this.importParams, extractable, this.keyUsages.private),
36-
crypto.subtle.importKey("spki", asBufferSource(spki), this.importParams, true, this.keyUsages.public),
37-
]);
35+
[privateKey, publicKey] = await this.importPkcs8(...importPkcs8, extractable);
36+
} else if (importPkcs1) {
37+
const [pkcs1, spki] = importPkcs1;
38+
const pkcs8 = toPkcs8(
39+
[
40+
asn1.Any("06", RsaOid), // OID
41+
asn1.Any("05"), // Parameters NULL
42+
],
43+
pkcs1,
44+
);
45+
[privateKey, publicKey] = await this.importPkcs8(pkcs8, spki, extractable);
3846
} else {
3947
const genParams: RsaHashedKeyGenParams = {
4048
...this.genParams,
@@ -56,8 +64,15 @@ export abstract class RsaCommon implements CryptoAlgorithm<{}, true, RSA.GenPara
5664
};
5765
}
5866

67+
private importPkcs8(pkcs8: Uint8Array, spki: Uint8Array, extractable: boolean) {
68+
return Promise.all([
69+
crypto.subtle.importKey("pkcs8", asBufferSource(pkcs8), this.importParams, extractable, this.keyUsages.private),
70+
crypto.subtle.importKey("spki", asBufferSource(spki), this.importParams, true, this.keyUsages.public),
71+
]);
72+
}
73+
5974
public async importSpki(spki: Uint8Array, der: asn1.ElementBuffer) {
60-
assertSpkiAlgorithm(der, "RSA", "2A864886F70D010101"); // 1.2.840.113549.1.1.1
75+
assertSpkiAlgorithm(der, "RSA", RsaOid);
6176
const key = await crypto.subtle.importKey("spki", asBufferSource(spki), this.importParams, true, this.keyUsages.public);
6277
return {
6378
publicKey: key,

pkg/keychain/src/algo/rsa.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,12 @@ export namespace RSA {
3535

3636
/** Import PKCS#8 private key and SPKI public key instead of generating. */
3737
importPkcs8?: [pkcs8: Uint8Array, spki: Uint8Array];
38+
39+
/**
40+
* Import PKCS#1 private key and SPKI public key instead of generating.
41+
*
42+
* If {@link importPkcs8} is also specified, this field is ignored.
43+
*/
44+
importPkcs1?: [pkcs1: Uint8Array, spki: Uint8Array];
3845
}
3946
}

pkg/keychain/src/store/key-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { StoreBase, type StoreProvider } from "./store-base";
66

77
/** KV store of named key pairs. */
88
export class KeyStore extends StoreBase<KeyStore.StoredKey> {
9-
constructor(provider: StoreProvider<KeyStore.StoredKey>, algoList: readonly CryptoAlgorithm[]) {
9+
constructor(provider: StoreProvider<KeyStore.StoredKey>, public readonly algoList: readonly CryptoAlgorithm[]) {
1010
super(provider);
1111
this.loader = new KeyStore.Loader(false, algoList);
1212
}

pkg/keychain/src/store/keychain-external.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export abstract class KeyChainExternal extends KeyChainSerialized {
1414
private cached?: KeyChain;
1515

1616
protected constructor(
17-
protected readonly algoList: readonly CryptoAlgorithm[],
17+
public readonly algoList: readonly CryptoAlgorithm[],
1818
public override readonly needJwk = true,
1919
) {
2020
super();

pkg/keychain/src/store/keychain.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { openStores } from "./stores_node";
1313

1414
/** Storage of own private keys and certificates. */
1515
export abstract class KeyChain {
16+
/** Retrieve the algorithm list used to construct this KeyChain. */
17+
public abstract readonly algoList: readonly CryptoAlgorithm[];
18+
1619
/** Return whether `.insertKey()` method expects JsonWebKey instead of CryptoKey. */
1720
public abstract readonly needJwk: boolean;
1821

@@ -283,6 +286,8 @@ class KeyChainImpl extends KeyChainSerialized {
283286
super();
284287
}
285288

289+
public override get algoList() { return this.keys.algoList; }
290+
286291
public override get needJwk() { return !this.keys.canSClone; }
287292

288293
protected override async sListKeys(prefix: Name): Promise<Name[]> {

pkg/ndndsec/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ This package implements basic support for [NDNd keychain](https://github.com/nam
99
`parseCert` function loads certificate file generated by `ndnsec sec sign-cert` command.
1010

1111
`UnencryptedPrivateKey` type, returned by `parseKey`, allows importing the key pair into NDNts keychain.
12-
This only works in Node.js, because Web Crypto API does not support PKCS#1 or SEC1 private key formats.

0 commit comments

Comments
 (0)