@@ -6,9 +6,6 @@ import * as x509 from '@peculiar/x509';
6
6
import * as asn1X509 from '@peculiar/asn1-x509' ;
7
7
import * as asn1Schema from '@peculiar/asn1-schema' ;
8
8
9
- import * as forge from 'node-forge' ;
10
- const { asn1, pki, md, util } = forge ;
11
-
12
9
const crypto = globalThis . crypto ;
13
10
14
11
export type CAOptions = ( CertDataOptions | CertPathOptions ) ;
@@ -73,6 +70,17 @@ function arrayBufferToPem(buffer: ArrayBuffer, label: string): string {
73
70
return `-----BEGIN ${ label } -----\n${ lines . join ( '\n' ) } \n-----END ${ label } -----\n` ;
74
71
}
75
72
73
+ async function pemToCryptoKey ( pem : string ) {
74
+ const derKey = x509 . PemConverter . decodeFirst ( pem ) ;
75
+ return await crypto . subtle . importKey (
76
+ "pkcs8" ,
77
+ derKey ,
78
+ { name : "RSASSA-PKCS1-v1_5" , hash : "SHA-256" } ,
79
+ true , // Extractable
80
+ [ "sign" ]
81
+ ) ;
82
+ }
83
+
76
84
/**
77
85
* Generate a CA certificate for mocking HTTPS.
78
86
*
@@ -202,16 +210,10 @@ export async function generateCACertificate(options: {
202
210
} ;
203
211
}
204
212
205
-
206
- export function generateSPKIFingerprint ( certPem : PEM ) {
207
- let cert = pki . certificateFromPem ( certPem . toString ( 'utf8' ) ) ;
208
- return util . encode64 (
209
- pki . getPublicKeyFingerprint ( cert . publicKey , {
210
- type : 'SubjectPublicKeyInfo' ,
211
- md : md . sha256 . create ( ) ,
212
- encoding : 'binary'
213
- } )
214
- ) ;
213
+ export async function generateSPKIFingerprint ( certPem : string ) : Promise < string > {
214
+ const cert = new x509 . X509Certificate ( certPem ) ;
215
+ const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , cert . publicKey . rawData ) ;
216
+ return Buffer . from ( hashBuffer ) . toString ( 'base64' ) ;
215
217
}
216
218
217
219
// Generates a unique serial number for a certificate as a hex string:
@@ -249,39 +251,49 @@ export async function getCA(options: CAOptions): Promise<CA> {
249
251
// This would be a terrible idea for a real server, but for a mock server
250
252
// it's ok - if anybody can steal this, they can steal the CA cert anyway.
251
253
let KEY_PAIR : {
252
- publicKey : forge . pki . rsa . PublicKey ,
253
- privateKey : forge . pki . rsa . PrivateKey ,
254
+ value : Promise < CryptoKeyPair > ,
254
255
length : number
255
256
} | undefined ;
257
+ const KEY_PAIR_ALGO = {
258
+ name : "RSASSA-PKCS1-v1_5" ,
259
+ hash : "SHA-256" ,
260
+ publicExponent : new Uint8Array ( [ 1 , 0 , 1 ] )
261
+ } ;
256
262
257
263
export class CA {
258
- private caCert : forge . pki . Certificate ;
259
- private caKey : forge . pki . PrivateKey ;
264
+ private caCert : x509 . X509Certificate ;
265
+ private caKey : Promise < CryptoKey > ;
260
266
private options : CertDataOptions ;
261
267
262
268
private certCache : { [ domain : string ] : GeneratedCertificate } ;
263
269
264
270
constructor ( options : CertDataOptions ) {
265
- this . caKey = pki . privateKeyFromPem ( options . key . toString ( ) ) ;
266
- this . caCert = pki . certificateFromPem ( options . cert . toString ( ) ) ;
271
+ this . caKey = pemToCryptoKey ( options . key . toString ( ) ) ;
272
+ this . caCert = new x509 . X509Certificate ( options . cert . toString ( ) ) ;
267
273
this . certCache = { } ;
268
274
this . options = options ?? { } ;
269
275
270
276
const keyLength = options . keyLength || 2048 ;
271
277
272
278
if ( ! KEY_PAIR || KEY_PAIR . length < keyLength ) {
273
279
// If we have no key, or not a long enough one, generate one.
274
- KEY_PAIR = Object . assign (
275
- pki . rsa . generateKeyPair ( keyLength ) ,
276
- { length : keyLength }
277
- ) ;
280
+ KEY_PAIR = {
281
+ length : keyLength ,
282
+ value : crypto . subtle . generateKey (
283
+ { ...KEY_PAIR_ALGO , modulusLength : keyLength } ,
284
+ true ,
285
+ [ "sign" , "verify" ]
286
+ )
287
+ } ;
278
288
}
279
289
}
280
290
281
- generateCertificate ( domain : string ) : GeneratedCertificate {
291
+ async generateCertificate ( domain : string ) : Promise < GeneratedCertificate > {
282
292
// TODO: Expire domains from the cache? Based on their actual expiry?
283
293
if ( this . certCache [ domain ] ) return this . certCache [ domain ] ;
284
294
295
+ const leafKeyPair = await KEY_PAIR ! . value ;
296
+
285
297
if ( domain . includes ( '_' ) ) {
286
298
// TLS certificates cannot cover domains with underscores, bizarrely. More info:
287
299
// https://www.digicert.com/kb/ssl-support/underscores-not-allowed-in-fqdns.htm
@@ -300,70 +312,80 @@ export class CA {
300
312
domain = `*.${ otherParts . join ( '.' ) } ` ;
301
313
}
302
314
303
- let cert = pki . createCertificate ( ) ;
315
+ const subjectJsonNameParams : x509 . JsonNameParams = [ ] ;
316
+ const subjectAttributes : Record < string , string > = { } ;
304
317
305
- cert . publicKey = KEY_PAIR ! . publicKey ;
306
- cert . serialNumber = generateSerialNumber ( ) ;
307
-
308
- cert . validity . notBefore = new Date ( ) ;
309
- // Make it valid for the last 24h - helps in cases where clocks slightly disagree.
310
- cert . validity . notBefore . setDate ( cert . validity . notBefore . getDate ( ) - 1 ) ;
318
+ if ( domain [ 0 ] !== '*' ) { // Skip this for wildcards as CN cannot use them
319
+ subjectAttributes [ 'commonName' ] = domain ;
320
+ }
321
+ subjectAttributes [ 'countryName' ] = this . options . countryName ?? 'XX' ;
322
+ // Most other subject attributes aren't allowed here by BR.
323
+
324
+ // Apply BR-required order
325
+ const orderedSubjectKeys = [ "countryName" , "organizationName" , "localityName" , "commonName" ] ;
326
+ for ( const key of orderedSubjectKeys ) {
327
+ if ( subjectAttributes [ key ] ) {
328
+ const mappedKey = SUBJECT_NAME_MAP [ key ] || key ;
329
+ subjectJsonNameParams . push ( { [ mappedKey ] : [ subjectAttributes [ key ] ] } ) ;
330
+ }
331
+ }
332
+ const subjectDistinguishedName = new x509 . Name ( subjectJsonNameParams ) . toString ( ) ;
333
+ const issuerDistinguishedName = this . caCert . subject ;
311
334
312
- cert . validity . notAfter = new Date ( ) ;
313
- // Valid for the next year by default. TODO: Shorten (and expire the cache) automatically.
314
- cert . validity . notAfter . setFullYear ( cert . validity . notAfter . getFullYear ( ) + 1 ) ;
335
+ const notBefore = new Date ( ) ;
336
+ notBefore . setDate ( notBefore . getDate ( ) - 1 ) ; // Valid from 24 hours ago
315
337
316
- cert . setSubject ( [
317
- ...( domain [ 0 ] === '*'
318
- ? [ ] // We skip the CN (deprecated, rarely used) for wildcards, since they can't be used here.
319
- : [ { name : 'commonName' , value : domain } ]
320
- ) ,
321
- { name : 'countryName' , value : this . options ?. countryName ?? 'XX' } , // ISO-3166-1 alpha-2 'unknown country' code
322
- { name : 'localityName' , value : this . options ?. localityName ?? 'Unknown' } ,
323
- { name : 'organizationName' , value : this . options ?. organizationName ?? 'Mockttp Cert - DO NOT TRUST' }
324
- ] ) ;
325
- cert . setIssuer ( this . caCert . subject . attributes ) ;
326
-
327
- const policyList = forge . asn1 . create ( forge . asn1 . Class . UNIVERSAL , forge . asn1 . Type . SEQUENCE , true , [
328
- forge . asn1 . create ( forge . asn1 . Class . UNIVERSAL , forge . asn1 . Type . SEQUENCE , true , [
329
- forge . asn1 . create (
330
- forge . asn1 . Class . UNIVERSAL ,
331
- forge . asn1 . Type . OID ,
332
- false ,
333
- forge . asn1 . oidToDer ( '2.5.29.32.0' ) . getBytes ( ) // Mark all as Domain Verified
334
- )
335
- ] )
336
- ] ) ;
337
-
338
- cert . setExtensions ( [
339
- { name : 'basicConstraints' , cA : false , critical : true } ,
340
- { name : 'keyUsage' , digitalSignature : true , keyEncipherment : true , critical : true } ,
341
- { name : 'extKeyUsage' , serverAuth : true , clientAuth : true } ,
342
- {
343
- name : 'subjectAltName' ,
344
- altNames : [ {
345
- type : 2 ,
346
- value : domain
347
- } ]
348
- } ,
349
- { name : 'certificatePolicies' , value : policyList } ,
350
- { name : 'subjectKeyIdentifier' } ,
351
- {
352
- name : 'authorityKeyIdentifier' ,
353
- // We have to calculate this ourselves due to
354
- // https://github.com/digitalbazaar/forge/issues/462
355
- keyIdentifier : (
356
- this . caCert as any // generateSubjectKeyIdentifier is missing from node-forge types
357
- ) . generateSubjectKeyIdentifier ( ) . getBytes ( )
358
- }
359
- ] ) ;
338
+ const notAfter = new Date ( ) ;
339
+ notAfter . setFullYear ( notAfter . getFullYear ( ) + 1 ) ; // Valid for 1 year
360
340
361
- cert . sign ( this . caKey , md . sha256 . create ( ) ) ;
341
+ const extensions : x509 . Extension [ ] = [ ] ;
342
+ extensions . push ( new x509 . BasicConstraintsExtension ( false , undefined , true ) ) ;
343
+ extensions . push ( new x509 . KeyUsagesExtension (
344
+ x509 . KeyUsageFlags . digitalSignature | x509 . KeyUsageFlags . keyEncipherment ,
345
+ true
346
+ ) ) ;
347
+ extensions . push ( new x509 . ExtendedKeyUsageExtension (
348
+ [ asn1X509 . id_kp_serverAuth , asn1X509 . id_kp_clientAuth ] ,
349
+ false
350
+ ) ) ;
351
+
352
+ extensions . push ( new x509 . SubjectAlternativeNameExtension (
353
+ [ { type : "dns" , value : domain } ] ,
354
+ false
355
+ ) ) ;
356
+
357
+ const policyInfo = new asn1X509 . PolicyInformation ( {
358
+ policyIdentifier : '2.23.140.1.2.1' // Domain validated
359
+ } ) ;
360
+ const certificatePoliciesValue = new asn1X509 . CertificatePolicies ( [ policyInfo ] ) ;
361
+ extensions . push ( new x509 . Extension (
362
+ asn1X509 . id_ce_certificatePolicies ,
363
+ false ,
364
+ asn1Schema . AsnConvert . serialize ( certificatePoliciesValue )
365
+ ) ) ;
366
+
367
+ // We don't include SubjectKeyIdentifierExtension as that's no longer recommended
368
+ extensions . push ( await x509 . AuthorityKeyIdentifierExtension . create ( this . caCert , false ) ) ;
369
+
370
+ const certificate = await x509 . X509CertificateGenerator . create ( {
371
+ serialNumber : generateSerialNumber ( ) ,
372
+ subject : subjectDistinguishedName ,
373
+ issuer : issuerDistinguishedName ,
374
+ notBefore,
375
+ notAfter,
376
+ signingAlgorithm : KEY_PAIR_ALGO ,
377
+ publicKey : leafKeyPair . publicKey ,
378
+ signingKey : await this . caKey ,
379
+ extensions
380
+ } ) ;
362
381
363
382
const generatedCertificate = {
364
- key : pki . privateKeyToPem ( KEY_PAIR ! . privateKey ) ,
365
- cert : pki . certificateToPem ( cert ) ,
366
- ca : pki . certificateToPem ( this . caCert )
383
+ key : arrayBufferToPem (
384
+ await crypto . subtle . exportKey ( "pkcs8" , leafKeyPair . privateKey as CryptoKey ) ,
385
+ "RSA PRIVATE KEY"
386
+ ) ,
387
+ cert : certificate . toString ( "pem" ) ,
388
+ ca : this . caCert . toString ( "pem" )
367
389
} ;
368
390
369
391
this . certCache [ domain ] = generatedCertificate ;
0 commit comments