@@ -6,9 +6,6 @@ import * as x509 from '@peculiar/x509';
66import * as asn1X509 from '@peculiar/asn1-x509' ;
77import * as asn1Schema from '@peculiar/asn1-schema' ;
88
9- import * as forge from 'node-forge' ;
10- const { asn1, pki, md, util } = forge ;
11-
129const crypto = globalThis . crypto ;
1310
1411export type CAOptions = ( CertDataOptions | CertPathOptions ) ;
@@ -73,6 +70,17 @@ function arrayBufferToPem(buffer: ArrayBuffer, label: string): string {
7370 return `-----BEGIN ${ label } -----\n${ lines . join ( '\n' ) } \n-----END ${ label } -----\n` ;
7471}
7572
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+
7684/**
7785 * Generate a CA certificate for mocking HTTPS.
7886 *
@@ -202,16 +210,10 @@ export async function generateCACertificate(options: {
202210 } ;
203211}
204212
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' ) ;
215217}
216218
217219// Generates a unique serial number for a certificate as a hex string:
@@ -249,39 +251,49 @@ export async function getCA(options: CAOptions): Promise<CA> {
249251// This would be a terrible idea for a real server, but for a mock server
250252// it's ok - if anybody can steal this, they can steal the CA cert anyway.
251253let KEY_PAIR : {
252- publicKey : forge . pki . rsa . PublicKey ,
253- privateKey : forge . pki . rsa . PrivateKey ,
254+ value : Promise < CryptoKeyPair > ,
254255 length : number
255256} | undefined ;
257+ const KEY_PAIR_ALGO = {
258+ name : "RSASSA-PKCS1-v1_5" ,
259+ hash : "SHA-256" ,
260+ publicExponent : new Uint8Array ( [ 1 , 0 , 1 ] )
261+ } ;
256262
257263export class CA {
258- private caCert : forge . pki . Certificate ;
259- private caKey : forge . pki . PrivateKey ;
264+ private caCert : x509 . X509Certificate ;
265+ private caKey : Promise < CryptoKey > ;
260266 private options : CertDataOptions ;
261267
262268 private certCache : { [ domain : string ] : GeneratedCertificate } ;
263269
264270 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 ( ) ) ;
267273 this . certCache = { } ;
268274 this . options = options ?? { } ;
269275
270276 const keyLength = options . keyLength || 2048 ;
271277
272278 if ( ! KEY_PAIR || KEY_PAIR . length < keyLength ) {
273279 // 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+ } ;
278288 }
279289 }
280290
281- generateCertificate ( domain : string ) : GeneratedCertificate {
291+ async generateCertificate ( domain : string ) : Promise < GeneratedCertificate > {
282292 // TODO: Expire domains from the cache? Based on their actual expiry?
283293 if ( this . certCache [ domain ] ) return this . certCache [ domain ] ;
284294
295+ const leafKeyPair = await KEY_PAIR ! . value ;
296+
285297 if ( domain . includes ( '_' ) ) {
286298 // TLS certificates cannot cover domains with underscores, bizarrely. More info:
287299 // https://www.digicert.com/kb/ssl-support/underscores-not-allowed-in-fqdns.htm
@@ -300,70 +312,80 @@ export class CA {
300312 domain = `*.${ otherParts . join ( '.' ) } ` ;
301313 }
302314
303- let cert = pki . createCertificate ( ) ;
315+ const subjectJsonNameParams : x509 . JsonNameParams = [ ] ;
316+ const subjectAttributes : Record < string , string > = { } ;
304317
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 ;
311334
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
315337
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
360340
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+ } ) ;
362381
363382 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" )
367389 } ;
368390
369391 this . certCache [ domain ] = generatedCertificate ;
0 commit comments