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
319async 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+
85390async 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
91443void main ( ) ;
0 commit comments