@@ -7,6 +7,9 @@ import * as x509 from '@peculiar/x509';
7
7
import * as asn1X509 from '@peculiar/asn1-x509' ;
8
8
import * as asn1Schema from '@peculiar/asn1-schema' ;
9
9
10
+ // Import for PKCS#8 structure
11
+ import { PrivateKeyInfo } from '@peculiar/asn1-pkcs8' ;
12
+
10
13
const crypto = globalThis . crypto ;
11
14
12
15
export type CAOptions = ( CertDataOptions | CertPathOptions ) ;
@@ -71,11 +74,50 @@ function arrayBufferToPem(buffer: ArrayBuffer, label: string): string {
71
74
return `-----BEGIN ${ label } -----\n${ lines . join ( '\n' ) } \n-----END ${ label } -----\n` ;
72
75
}
73
76
77
+ // OID for rsaEncryption - used to wrap PKCS#1 keys into PKCS#8 below:
78
+ const rsaEncryptionOid = "1.2.840.113549.1.1.1" ;
79
+
74
80
async function pemToCryptoKey ( pem : string ) {
75
- const derKey = x509 . PemConverter . decodeFirst ( pem ) ;
81
+ // The PEM might be PKCS#8 ("BEGIN PRIVATE KEY") or PKCS#1 ("BEGIN
82
+ // RSA PRIVATE KEY"). We want to transparently accept both, but
83
+ // we can only import PKCS#8, so we detect & convert if required.
84
+
85
+ const keyData = x509 . PemConverter . decodeFirst ( pem ) ;
86
+ let pkcs8KeyData : ArrayBuffer ;
87
+
88
+ try {
89
+ // Try to parse the PEM as PKCS#8 PrivateKeyInfo - if it works,
90
+ // we can just use it directly as-is:
91
+ asn1Schema . AsnConvert . parse ( keyData , PrivateKeyInfo ) ;
92
+ pkcs8KeyData = keyData ;
93
+ } catch ( e : any ) {
94
+ // If parsing as PKCS#8 fails, assume it's PKCS#1 (RSAPrivateKey)
95
+ // and proceed to wrap it as an RSA key in a PrivateKeyInfo structure.
96
+ const rsaPrivateKeyDer = keyData ;
97
+
98
+ try {
99
+ const privateKeyInfo = new PrivateKeyInfo ( {
100
+ version : 0 ,
101
+ privateKeyAlgorithm : new asn1X509 . AlgorithmIdentifier ( {
102
+ algorithm : rsaEncryptionOid
103
+ } ) ,
104
+ privateKey : new asn1Schema . OctetString ( rsaPrivateKeyDer )
105
+ } ) ;
106
+ pkcs8KeyData = asn1Schema . AsnConvert . serialize ( privateKeyInfo ) ;
107
+ } catch ( conversionError : any ) {
108
+ throw new Error (
109
+ `Unsupported or malformed key format. Failed to parse as PKCS#8 with ${
110
+ e . message || e . toString ( )
111
+ } and failed to convert to PKCS#1 with ${
112
+ conversionError . message || conversionError . toString ( )
113
+ } `
114
+ ) ;
115
+ }
116
+ }
117
+
76
118
return await crypto . subtle . importKey (
77
- "pkcs8" ,
78
- derKey ,
119
+ "pkcs8" , // N.b, pkcs1 is not supported, which is why we need the above
120
+ pkcs8KeyData ,
79
121
{ name : "RSASSA-PKCS1-v1_5" , hash : "SHA-256" } ,
80
122
true , // Extractable
81
123
[ "sign" ]
@@ -243,7 +285,10 @@ export async function getCA(options: CAOptions): Promise<CA> {
243
285
throw new Error ( 'Unrecognized https options: you need to provide either a keyPath & certPath, or a key & cert.' )
244
286
}
245
287
246
- return new CA ( certOptions ) ;
288
+ const caCert = new x509 . X509Certificate ( certOptions . cert . toString ( ) ) ;
289
+ const caKey = await pemToCryptoKey ( certOptions . key . toString ( ) ) ;
290
+
291
+ return new CA ( caCert , caKey , options ) ;
247
292
}
248
293
249
294
// We share a single keypair across all certificates in this process, and
@@ -261,20 +306,22 @@ const KEY_PAIR_ALGO = {
261
306
publicExponent : new Uint8Array ( [ 1 , 0 , 1 ] )
262
307
} ;
263
308
264
- export class CA {
265
- private caCert : x509 . X509Certificate ;
266
- private caKey : Promise < CryptoKey > ;
267
- private options : CertDataOptions ;
309
+ export type { CA } ;
310
+
311
+ class CA {
312
+ private options : BaseCAOptions ;
268
313
269
314
private certCache : { [ domain : string ] : GeneratedCertificate } ;
270
315
271
- constructor ( options : CertDataOptions ) {
272
- this . caKey = pemToCryptoKey ( options . key . toString ( ) ) ;
273
- this . caCert = new x509 . X509Certificate ( options . cert . toString ( ) ) ;
316
+ constructor (
317
+ private caCert : x509 . X509Certificate ,
318
+ private caKey : CryptoKey ,
319
+ options ?: BaseCAOptions
320
+ ) {
274
321
this . certCache = { } ;
275
322
this . options = options ?? { } ;
276
323
277
- const keyLength = options . keyLength || 2048 ;
324
+ const keyLength = this . options . keyLength || 2048 ;
278
325
279
326
if ( ! KEY_PAIR || KEY_PAIR . length < keyLength ) {
280
327
// If we have no key, or not a long enough one, generate one.
@@ -376,7 +423,7 @@ export class CA {
376
423
notAfter,
377
424
signingAlgorithm : KEY_PAIR_ALGO ,
378
425
publicKey : leafKeyPair . publicKey ,
379
- signingKey : await this . caKey ,
426
+ signingKey : this . caKey ,
380
427
extensions
381
428
} ) ;
382
429
0 commit comments