Skip to content

Commit d2d4e80

Browse files
committed
WIP
1 parent e4be27d commit d2d4e80

File tree

2 files changed

+661
-67
lines changed

2 files changed

+661
-67
lines changed

test-keygen-webcrypto.ts

Lines changed: 358 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1-
import { webcrypto } from 'crypto';
1+
import type { X509Certificate } from '@peculiar/x509';
2+
import { Crypto } from '@peculiar/webcrypto';
3+
import * as x509 from '@peculiar/x509';
4+
5+
/**
6+
* WebCrypto polyfill from @peculiar/webcrypto
7+
* This behaves differently with respect to Ed25519 keys
8+
* See: https://github.com/PeculiarVentures/webcrypto/issues/55
9+
*/
10+
const webcrypto = new Crypto();
11+
12+
/**
13+
* Monkey patches the global crypto object polyfill
14+
*/
15+
globalThis.crypto = webcrypto;
16+
17+
x509.cryptoProvider.set(webcrypto);
218

319
async function generateKeyPairRSA(): Promise<{
420
publicKey: JsonWebKey;
@@ -62,14 +78,15 @@ async function generateKeyPairEd25519(): Promise<{
6278
}> {
6379
const keyPair = await webcrypto.subtle.generateKey(
6480
{
65-
name: 'ED25519',
81+
name: 'EdDSA',
82+
namedCurve: 'Ed25519'
6683
},
6784
true,
6885
[
6986
'sign',
7087
'verify'
7188
]
72-
) as webcrypto.CryptoKeyPair;
89+
) as CryptoKeyPair;
7390
return {
7491
publicKey: await webcrypto.subtle.exportKey(
7592
'jwk',
@@ -82,10 +99,345 @@ async function generateKeyPairEd25519(): Promise<{
8299
};
83100
}
84101

102+
/**
103+
* Imports public key.
104+
* This uses `@peculiar/webcrypto` API for Ed25519 keys.
105+
*/
106+
async function importPublicKey(publicKey: JsonWebKey): Promise<CryptoKey> {
107+
let algorithm;
108+
switch (publicKey.kty) {
109+
case 'RSA':
110+
switch(publicKey.alg) {
111+
case 'RS256':
112+
algorithm = {
113+
name: 'RSASSA-PKCS1-v1_5',
114+
hash: 'SHA-256'
115+
};
116+
break;
117+
case 'RS384':
118+
algorithm = {
119+
name: 'RSASSA-PKCS1-v1_5',
120+
hash: 'SHA-384'
121+
};
122+
break;
123+
case 'RS512':
124+
algorithm = {
125+
name: 'RSASSA-PKCS1-v1_5',
126+
hash: 'SHA-512'
127+
};
128+
break;
129+
default:
130+
throw new Error(`Unsupported algorithm ${publicKey.alg}`);
131+
}
132+
break;
133+
case 'EC':
134+
switch(publicKey.crv) {
135+
case 'P-256':
136+
algorithm = {
137+
name: 'ECDSA',
138+
namedCurve: 'P-256',
139+
};
140+
break;
141+
case 'P-384':
142+
algorithm = {
143+
name: 'ECDSA',
144+
namedCurve: 'P-384',
145+
};
146+
break;
147+
case 'P-521':
148+
algorithm = {
149+
name: 'ECDSA',
150+
namedCurve: 'P-521',
151+
};
152+
break;
153+
default:
154+
throw new Error(`Unsupported curve ${publicKey.crv}`);
155+
}
156+
break;
157+
case 'OKP':
158+
algorithm = {
159+
name: 'EdDSA',
160+
namedCurve: 'Ed25519',
161+
};
162+
break;
163+
default:
164+
throw new Error(`Unsupported key type ${publicKey.kty}`);
165+
}
166+
return await webcrypto.subtle.importKey(
167+
'jwk',
168+
publicKey,
169+
algorithm,
170+
true,
171+
['verify']
172+
);
173+
}
174+
175+
/**
176+
* Imports private key.
177+
* This uses `@peculiar/webcrypto` API for Ed25519 keys.
178+
*/
179+
async function importPrivateKey(privateKey: JsonWebKey): Promise<CryptoKey> {
180+
let algorithm;
181+
switch (privateKey.kty) {
182+
case 'RSA':
183+
switch(privateKey.alg) {
184+
case 'RS256':
185+
algorithm = {
186+
name: 'RSASSA-PKCS1-v1_5',
187+
hash: 'SHA-256'
188+
};
189+
break;
190+
case 'RS384':
191+
algorithm = {
192+
name: 'RSASSA-PKCS1-v1_5',
193+
hash: 'SHA-384'
194+
};
195+
break;
196+
case 'RS512':
197+
algorithm = {
198+
name: 'RSASSA-PKCS1-v1_5',
199+
hash: 'SHA-512'
200+
};
201+
break;
202+
default:
203+
throw new Error(`Unsupported algorithm ${privateKey.alg}`);
204+
}
205+
break;
206+
case 'EC':
207+
switch(privateKey.crv) {
208+
case 'P-256':
209+
algorithm = {
210+
name: 'ECDSA',
211+
namedCurve: 'P-256',
212+
};
213+
break;
214+
case 'P-384':
215+
algorithm = {
216+
name: 'ECDSA',
217+
namedCurve: 'P-384',
218+
};
219+
break;
220+
case 'P-521':
221+
algorithm = {
222+
name: 'ECDSA',
223+
namedCurve: 'P-521',
224+
};
225+
break;
226+
default:
227+
throw new Error(`Unsupported curve ${privateKey.crv}`);
228+
}
229+
break;
230+
case 'OKP':
231+
algorithm = {
232+
name: 'EdDSA',
233+
namedCurve: 'Ed25519',
234+
};
235+
break;
236+
default:
237+
throw new Error(`Unsupported key type ${privateKey.kty}`);
238+
}
239+
return await webcrypto.subtle.importKey(
240+
'jwk',
241+
privateKey,
242+
algorithm,
243+
true,
244+
['sign']
245+
);
246+
}
247+
248+
const extendedKeyUsageFlags = {
249+
serverAuth: '1.3.6.1.5.5.7.3.1',
250+
clientAuth: '1.3.6.1.5.5.7.3.2',
251+
codeSigning: '1.3.6.1.5.5.7.3.3',
252+
emailProtection: '1.3.6.1.5.5.7.3.4',
253+
timeStamping: '1.3.6.1.5.5.7.3.8',
254+
ocspSigning: '1.3.6.1.5.5.7.3.9',
255+
};
256+
257+
/**
258+
* Generate x509 certificate.
259+
* Duration is in seconds.
260+
* X509 certificates currently use `UTCTime` format for `notBefore` and `notAfter`.
261+
* This means:
262+
* - Only second resolution.
263+
* - Minimum date for validity is 1970-01-01T00:00:00Z (inclusive).
264+
* - Maximum date for valdity is 2049-12-31T23:59:59Z (inclusive).
265+
*/
266+
async function generateCertificate({
267+
certId,
268+
subjectKeyPair,
269+
issuerPrivateKey,
270+
duration,
271+
subjectAttrsExtra = [],
272+
issuerAttrsExtra = [],
273+
now = new Date(),
274+
}: {
275+
certId: string;
276+
subjectKeyPair: {
277+
publicKey: JsonWebKey;
278+
privateKey: JsonWebKey;
279+
};
280+
issuerPrivateKey: JsonWebKey;
281+
duration: number;
282+
subjectAttrsExtra?: Array<{ [key: string]: Array<string> }>;
283+
issuerAttrsExtra?: Array<{ [key: string]: Array<string> }>;
284+
now?: Date;
285+
}): Promise<X509Certificate> {
286+
const certIdNum = parseInt(certId);
287+
const iss = certIdNum === 0 ? certIdNum : certIdNum - 1;
288+
const sub = certIdNum;
289+
const subjectPublicCryptoKey = await importPublicKey(
290+
subjectKeyPair.publicKey,
291+
);
292+
const subjectPrivateCryptoKey = await importPrivateKey(
293+
subjectKeyPair.privateKey,
294+
);
295+
const issuerPrivateCryptoKey = await importPrivateKey(issuerPrivateKey);
296+
if (duration < 0) {
297+
throw new RangeError('`duration` must be positive');
298+
}
299+
// X509 `UTCTime` format only has resolution of seconds
300+
// this truncates to second resolution
301+
const notBeforeDate = new Date(now.getTime() - (now.getTime() % 1000));
302+
const notAfterDate = new Date(now.getTime() - (now.getTime() % 1000));
303+
// If the duration is 0, then only the `now` is valid
304+
notAfterDate.setSeconds(notAfterDate.getSeconds() + duration);
305+
if (notBeforeDate < new Date(0)) {
306+
throw new RangeError(
307+
'`notBeforeDate` cannot be before 1970-01-01T00:00:00Z',
308+
);
309+
}
310+
if (notAfterDate > new Date(new Date('2050').getTime() - 1)) {
311+
throw new RangeError('`notAfterDate` cannot be after 2049-12-31T23:59:59Z');
312+
}
313+
const serialNumber = certId;
314+
// The entire subject attributes and issuer attributes
315+
// is constructed via `x509.Name` class
316+
// By default this supports on a limited set of names:
317+
// CN, L, ST, O, OU, C, DC, E, G, I, SN, T
318+
// If custom names are desired, this needs to change to constructing
319+
// `new x509.Name('FOO=BAR', { FOO: '1.2.3.4' })` manually
320+
// And each custom attribute requires a registered OID
321+
// Because the OID is what is encoded into ASN.1
322+
const subjectAttrs = [
323+
{
324+
CN: [`${sub}`],
325+
},
326+
// Filter out conflicting CN attributes
327+
...subjectAttrsExtra.filter((attr) => !('CN' in attr)),
328+
];
329+
const issuerAttrs = [
330+
{
331+
CN: [`${iss}`],
332+
},
333+
// Filter out conflicting CN attributes
334+
...issuerAttrsExtra.filter((attr) => !('CN' in attr)),
335+
];
336+
const signingAlgorithm: any = issuerPrivateCryptoKey.algorithm;
337+
if (signingAlgorithm.name === 'ECDSA') {
338+
switch(signingAlgorithm.namedCurve) {
339+
case 'P-256':
340+
signingAlgorithm.hash = 'SHA-256';
341+
break;
342+
case 'P-384':
343+
signingAlgorithm.hash = 'SHA-384';
344+
break;
345+
case 'P-521':
346+
signingAlgorithm.hash = 'SHA-512';
347+
break;
348+
default:
349+
throw new TypeError(
350+
`Issuer private key has an unsupported curve: ${signingAlgorithm.namedCurve}`
351+
);
352+
}
353+
}
354+
const certConfig = {
355+
serialNumber,
356+
notBefore: notBeforeDate,
357+
notAfter: notAfterDate,
358+
subject: subjectAttrs,
359+
issuer: issuerAttrs,
360+
signingAlgorithm,
361+
publicKey: subjectPublicCryptoKey,
362+
signingKey: subjectPrivateCryptoKey,
363+
extensions: [
364+
new x509.BasicConstraintsExtension(true, undefined, true),
365+
new x509.KeyUsagesExtension(
366+
x509.KeyUsageFlags.keyCertSign |
367+
x509.KeyUsageFlags.cRLSign |
368+
x509.KeyUsageFlags.digitalSignature |
369+
x509.KeyUsageFlags.nonRepudiation |
370+
x509.KeyUsageFlags.keyAgreement |
371+
x509.KeyUsageFlags.keyEncipherment |
372+
x509.KeyUsageFlags.dataEncipherment,
373+
true,
374+
),
375+
new x509.ExtendedKeyUsageExtension([
376+
extendedKeyUsageFlags.serverAuth,
377+
extendedKeyUsageFlags.clientAuth,
378+
extendedKeyUsageFlags.codeSigning,
379+
extendedKeyUsageFlags.emailProtection,
380+
extendedKeyUsageFlags.timeStamping,
381+
extendedKeyUsageFlags.ocspSigning,
382+
]),
383+
await x509.SubjectKeyIdentifierExtension.create(subjectPublicCryptoKey),
384+
] as Array<x509.Extension>,
385+
};
386+
certConfig.signingKey = issuerPrivateCryptoKey;
387+
return await x509.X509CertificateGenerator.create(certConfig);
388+
}
389+
85390
async function main() {
86-
console.log(await generateKeyPairRSA());
87-
console.log(await generateKeyPairECDSA());
88-
console.log(await generateKeyPairEd25519());
391+
const keyPairRSA = await generateKeyPairRSA();
392+
const keyPairECDSA = await generateKeyPairECDSA();
393+
const keyPairEd25519 = await generateKeyPairEd25519();
394+
395+
console.log(keyPairRSA);
396+
console.log(keyPairECDSA);
397+
console.log(keyPairEd25519);
398+
399+
const publicKeyRSA = await importPublicKey(keyPairRSA.publicKey);
400+
const publicKeyECDSA = await importPublicKey(keyPairECDSA.publicKey);
401+
const publicKeyEd25519 = await importPublicKey(keyPairEd25519.publicKey);
402+
403+
console.log(publicKeyRSA);
404+
console.log(publicKeyECDSA);
405+
console.log(publicKeyEd25519);
406+
407+
const privateKeyRSA = await importPrivateKey(keyPairRSA.privateKey);
408+
const privateKeyECDSA = await importPrivateKey(keyPairECDSA.privateKey);
409+
const privateKeyEd25519 = await importPrivateKey(keyPairEd25519.privateKey);
410+
411+
console.log(privateKeyRSA);
412+
console.log(privateKeyECDSA);
413+
console.log(privateKeyEd25519);
414+
415+
416+
const certRSA = await generateCertificate({
417+
certId: '0',
418+
subjectKeyPair: keyPairRSA,
419+
issuerPrivateKey: keyPairRSA.privateKey,
420+
duration: 60 * 60 * 24 * 365 * 10,
421+
});
422+
423+
const certECDSA = await generateCertificate({
424+
certId: '0',
425+
subjectKeyPair: keyPairECDSA,
426+
issuerPrivateKey: keyPairECDSA.privateKey,
427+
duration: 60 * 60 * 24 * 365 * 10,
428+
});
429+
430+
const certEd25519 = await generateCertificate({
431+
certId: '0',
432+
subjectKeyPair: keyPairEd25519,
433+
issuerPrivateKey: keyPairEd25519.privateKey,
434+
duration: 60 * 60 * 24 * 365 * 10,
435+
});
436+
437+
console.log(certRSA);
438+
console.log(certECDSA);
439+
console.log(certEd25519);
440+
89441
}
90442

91443
void main();

0 commit comments

Comments
 (0)