@@ -6,6 +6,14 @@ import { isCryptoKey, isKeyObject } from './is_key_like.js'
66
77import type { KeyImportOptions } from '../key/import.js'
88
9+ /**
10+ * Formats a base64 string as a PEM-encoded key with proper line breaks and headers.
11+ *
12+ * @param b64 - Base64-encoded key data
13+ * @param descriptor - Key type descriptor (e.g., "PUBLIC KEY", "PRIVATE KEY")
14+ *
15+ * @returns PEM-formatted string
16+ */
917const formatPEM = ( b64 : string , descriptor : string ) => {
1018 const newlined = ( b64 . match ( / .{ 1 , 64 } / g) || [ ] ) . join ( '\n' )
1119 return `-----BEGIN ${ descriptor } -----\n${ newlined } \n-----END ${ descriptor } -----`
@@ -59,61 +67,64 @@ export const toPKCS8 = (key: unknown): Promise<string> => {
5967 return genericExport ( 'private' , 'pkcs8' , key )
6068}
6169
62- const findOid = ( keyData : Uint8Array , oid : number [ ] , from = 0 ) : boolean => {
63- if ( from === 0 ) {
64- oid . unshift ( oid . length )
65- oid . unshift ( 0x06 )
66- }
67- const i = keyData . indexOf ( oid [ 0 ] , from )
68- if ( i === - 1 ) return false
69- const sub = keyData . subarray ( i , i + oid . length )
70- if ( sub . length !== oid . length ) return false
71- return sub . every ( ( value , index ) => value === oid [ index ] ) || findOid ( keyData , oid , i + 1 )
72- }
73-
70+ /**
71+ * Detects the named curve from ECDH/ECDSA key data by searching for curve OID patterns.
72+ *
73+ * @param keyData - The key data to analyze
74+ *
75+ * @returns The curve name ('P-256', 'P-384', or 'P-521') or undefined if not found
76+ */
7477const getNamedCurve = ( keyData : Uint8Array ) : string | undefined => {
75- switch ( true ) {
76- case findOid ( keyData , [ 0x2a , 0x86 , 0x48 , 0xce , 0x3d , 0x03 , 0x01 , 0x07 ] ) :
77- return 'P-256'
78- case findOid ( keyData , [ 0x2b , 0x81 , 0x04 , 0x00 , 0x22 ] ) :
79- return 'P-384'
80- case findOid ( keyData , [ 0x2b , 0x81 , 0x04 , 0x00 , 0x23 ] ) :
81- return 'P-521'
82- default :
83- return undefined
78+ // OID patterns for NIST curves (Object Identifier byte sequences)
79+ const patterns = Object . entries ( {
80+ 'P-256' : [ 0x06 , 0x08 , 0x2a , 0x86 , 0x48 , 0xce , 0x3d , 0x03 , 0x01 , 0x07 ] ,
81+ 'P-384' : [ 0x06 , 0x05 , 0x2b , 0x81 , 0x04 , 0x00 , 0x22 ] ,
82+ 'P-521' : [ 0x06 , 0x05 , 0x2b , 0x81 , 0x04 , 0x00 , 0x23 ] ,
83+ } )
84+
85+ const maxPatternLen = Math . max ( ...patterns . map ( ( [ , bytes ] ) => bytes . length ) )
86+
87+ for ( let i = 0 ; i <= keyData . byteLength - maxPatternLen ; i ++ ) {
88+ for ( const [ curve , bytes ] of patterns ) {
89+ if ( i <= keyData . byteLength - bytes . length ) {
90+ if ( keyData . subarray ( i , i + bytes . length ) . every ( ( byte , idx ) => byte === bytes [ idx ] ) ) {
91+ return curve
92+ }
93+ }
94+ }
8495 }
96+
97+ return undefined
8598}
8699
87100const genericImport = async (
88- replace : RegExp ,
89101 keyFormat : 'spki' | 'pkcs8' ,
90- pem : string ,
102+ keyData : Uint8Array ,
91103 alg : string ,
92104 options ?: KeyImportOptions ,
93105) => {
94106 let algorithm : RsaHashedImportParams | EcKeyAlgorithm | Algorithm
95107 let keyUsages : KeyUsage [ ]
96108
97- const keyData = new Uint8Array (
98- atob ( pem . replace ( replace , '' ) )
99- . split ( '' )
100- . map ( ( c ) => c . charCodeAt ( 0 ) ) ,
101- )
102-
103109 const isPublic = keyFormat === 'spki'
104110
111+ // Helper functions for determining key usage based on key type
112+ const getSignatureUsages = ( ) : KeyUsage [ ] => ( isPublic ? [ 'verify' ] : [ 'sign' ] )
113+ const getEncryptionUsages = ( ) : KeyUsage [ ] =>
114+ isPublic ? [ 'encrypt' , 'wrapKey' ] : [ 'decrypt' , 'unwrapKey' ]
115+
105116 switch ( alg ) {
106117 case 'PS256' :
107118 case 'PS384' :
108119 case 'PS512' :
109120 algorithm = { name : 'RSA-PSS' , hash : `SHA-${ alg . slice ( - 3 ) } ` }
110- keyUsages = isPublic ? [ 'verify' ] : [ 'sign' ]
121+ keyUsages = getSignatureUsages ( )
111122 break
112123 case 'RS256' :
113124 case 'RS384' :
114125 case 'RS512' :
115126 algorithm = { name : 'RSASSA-PKCS1-v1_5' , hash : `SHA-${ alg . slice ( - 3 ) } ` }
116- keyUsages = isPublic ? [ 'verify' ] : [ 'sign' ]
127+ keyUsages = getSignatureUsages ( )
117128 break
118129 case 'RSA-OAEP' :
119130 case 'RSA-OAEP-256' :
@@ -123,33 +134,29 @@ const genericImport = async (
123134 name : 'RSA-OAEP' ,
124135 hash : `SHA-${ parseInt ( alg . slice ( - 3 ) , 10 ) || 1 } ` ,
125136 }
126- keyUsages = isPublic ? [ 'encrypt' , 'wrapKey' ] : [ 'decrypt' , 'unwrapKey' ]
137+ keyUsages = getEncryptionUsages ( )
127138 break
128139 case 'ES256' :
129- algorithm = { name : 'ECDSA' , namedCurve : 'P-256' }
130- keyUsages = isPublic ? [ 'verify' ] : [ 'sign' ]
131- break
132140 case 'ES384' :
133- algorithm = { name : 'ECDSA' , namedCurve : 'P-384' }
134- keyUsages = isPublic ? [ 'verify' ] : [ 'sign' ]
135- break
136- case 'ES512' :
137- algorithm = { name : 'ECDSA' , namedCurve : 'P-521' }
138- keyUsages = isPublic ? [ 'verify' ] : [ 'sign' ]
141+ case 'ES512' : {
142+ const curveMap = { ES256 : 'P-256' , ES384 : 'P-384' , ES512 : 'P-521' } as const
143+ algorithm = { name : 'ECDSA' , namedCurve : curveMap [ alg ] }
144+ keyUsages = getSignatureUsages ( )
139145 break
146+ }
140147 case 'ECDH-ES' :
141148 case 'ECDH-ES+A128KW' :
142149 case 'ECDH-ES+A192KW' :
143150 case 'ECDH-ES+A256KW' : {
144151 const namedCurve = getNamedCurve ( keyData )
145- algorithm = namedCurve ?. startsWith ( 'P-' ) ? { name : 'ECDH' , namedCurve } : { name : 'X25519' }
152+ algorithm = namedCurve ? { name : 'ECDH' , namedCurve } : { name : 'X25519' }
146153 keyUsages = isPublic ? [ ] : [ 'deriveBits' ]
147154 break
148155 }
149- case 'Ed25519' : // Fall through
156+ case 'Ed25519' :
150157 case 'EdDSA' :
151158 algorithm = { name : 'Ed25519' }
152- keyUsages = isPublic ? [ 'verify' ] : [ 'sign' ]
159+ keyUsages = getSignatureUsages ( )
153160 break
154161 default :
155162 throw new JOSENotSupported ( 'Invalid or unsupported "alg" (Algorithm) value' )
@@ -171,110 +178,100 @@ type PEMImportFunction = (
171178) => Promise < types . CryptoKey >
172179
173180export const fromPKCS8 : PEMImportFunction = ( pem , alg , options ?) => {
174- return genericImport ( / (?: - - - - - (?: B E G I N | E N D ) P R I V A T E K E Y - - - - - | \s ) / g, 'pkcs8' , pem , alg , options )
181+ const keyData = decodeBase64 ( pem . replace ( / (?: - - - - - (?: B E G I N | E N D ) P R I V A T E K E Y - - - - - | \s ) / g, '' ) )
182+ return genericImport ( 'pkcs8' , keyData , alg , options )
175183}
176184
177185export const fromSPKI : PEMImportFunction = ( pem , alg , options ?) => {
178- return genericImport ( / (?: - - - - - (?: B E G I N | E N D ) P U B L I C K E Y - - - - - | \s ) / g, 'spki' , pem , alg , options )
186+ const keyData = decodeBase64 ( pem . replace ( / (?: - - - - - (?: B E G I N | E N D ) P U B L I C K E Y - - - - - | \s ) / g, '' ) )
187+ return genericImport ( 'spki' , keyData , alg , options )
179188}
180189
181- function getElement ( seq : Uint8Array ) {
182- const result = [ ]
183- let next = 0
184-
185- while ( next < seq . length ) {
186- const nextPart = parseElement ( seq . subarray ( next ) )
187- result . push ( nextPart )
188- next += nextPart . byteLength
190+ /**
191+ * Extracts the Subject Public Key Info (SPKI) from an X.509 certificate. Parses the ASN.1 DER
192+ * structure to locate and extract the public key portion.
193+ *
194+ * @param buf - DER-encoded X.509 certificate bytes
195+ *
196+ * @returns SPKI structure as bytes
197+ */
198+ function spkiFromX509 ( buf : Uint8Array ) : Uint8Array {
199+ // Parse ASN.1 DER structure to extract SPKI from X.509 certificate
200+ let pos = 0
201+
202+ // Helper function to parse ASN.1 length encoding (both short and long form)
203+ const parseLength = ( ) : number => {
204+ const first = buf [ pos ++ ]
205+ if ( first & 0x80 ) {
206+ // Long form: first byte indicates number of subsequent length bytes
207+ const lengthOfLength = first & 0x7f
208+ let length = 0
209+ for ( let i = 0 ; i < lengthOfLength ; i ++ ) {
210+ length = ( length << 8 ) | buf [ pos ++ ]
211+ }
212+ return length
213+ }
214+ // Short form: length is encoded directly in first byte
215+ return first
189216 }
190- return result
191- }
192217
193- function parseElement ( bytes : Uint8Array ) {
194- let position = 0
195-
196- // tag
197- let tag = bytes [ 0 ] & 0x1f
198- position ++
199- if ( tag === 0x1f ) {
200- tag = 0
201- while ( bytes [ position ] >= 0x80 ) {
202- tag = tag * 128 + bytes [ position ] - 0x80
203- position ++
218+ // Helper function to skip ASN.1 elements (tag + length + content)
219+ const skipElement = ( count : number = 1 ) : void => {
220+ if ( count <= 0 ) return
221+ pos ++ // Skip tag byte
222+ const length = parseLength ( )
223+ pos += length // Skip content bytes
224+ if ( count > 1 ) {
225+ skipElement ( count - 1 ) // Recursively skip remaining elements
204226 }
205- tag = tag * 128 + bytes [ position ] - 0x80
206- position ++
207227 }
208228
209- // length
210- let length = 0
211- if ( bytes [ position ] < 0x80 ) {
212- length = bytes [ position ]
213- position ++
214- } else if ( length === 0x80 ) {
215- length = 0
216-
217- while ( bytes [ position + length ] !== 0 || bytes [ position + length + 1 ] !== 0 ) {
218- if ( length > bytes . byteLength ) {
219- throw new TypeError ( 'invalid indefinite form length' )
220- }
221- length ++
222- }
229+ // Parse outer certificate SEQUENCE
230+ if ( buf [ pos ++ ] !== 0x30 ) throw new Error ( 'Invalid certificate structure' )
231+ parseLength ( ) // Skip certificate length
223232
224- const byteLength = position + length + 2
225- return {
226- byteLength,
227- contents : bytes . subarray ( position , position + length ) ,
228- raw : bytes . subarray ( 0 , byteLength ) ,
229- }
233+ // Parse tbsCertificate (To Be Signed Certificate) SEQUENCE
234+ if ( buf [ pos ++ ] !== 0x30 ) throw new Error ( 'Invalid tbsCertificate structure' )
235+ parseLength ( ) // Skip tbsCertificate length
236+
237+ if ( buf [ pos ] === 0xa0 ) {
238+ // Optional version field present (context-specific [0])
239+ // Skip: version, serialNumber, signature algorithm, issuer, validity, subject
240+ skipElement ( 6 )
230241 } else {
231- const numberOfDigits = bytes [ position ] & 0x7f
232- position ++
233- length = 0
234- for ( let i = 0 ; i < numberOfDigits ; i ++ ) {
235- length = length * 256 + bytes [ position ]
236- position ++
237- }
242+ // No version field (defaults to v1)
243+ // Skip: serialNumber, signature algorithm, issuer, validity, subject
244+ skipElement ( 5 )
238245 }
239246
240- const byteLength = position + length
241- return {
242- byteLength,
243- contents : bytes . subarray ( position , byteLength ) ,
244- raw : bytes . subarray ( 0 , byteLength ) ,
245- }
246- }
247+ // Extract subjectPublicKeyInfo SEQUENCE
248+ const spkiStart = pos
249+ if ( buf [ pos ++ ] !== 0x30 ) throw new Error ( 'Invalid SPKI structure' )
250+ const spkiContentLength = parseLength ( )
247251
248- function spkiFromX509 ( buf : Uint8Array ) {
249- const tbsCertificate = getElement ( getElement ( parseElement ( buf ) . contents ) [ 0 ] . contents )
250- return encodeBase64 ( tbsCertificate [ tbsCertificate [ 0 ] . raw [ 0 ] === 0xa0 ? 6 : 5 ] . raw )
252+ // Return the complete SPKI structure (tag + length + content)
253+ return buf . subarray ( spkiStart , spkiStart + spkiContentLength + ( pos - spkiStart ) )
251254}
252255
253- let createPublicKey : any
254- function getSPKI ( x509 : string ) : string {
255- try {
256- // @ts -ignore
257- createPublicKey ??= globalThis . process ?. getBuiltinModule ?.( 'node:crypto' ) ?. createPublicKey
258- } catch {
259- createPublicKey = 0
260- }
261-
262- if ( createPublicKey ) {
263- try {
264- return createPublicKey ( x509 ) . export ( { format : 'pem' , type : 'spki' } )
265- } catch { }
266- }
267- const pem = x509 . replace ( / (?: - - - - - (?: B E G I N | E N D ) C E R T I F I C A T E - - - - - | \s ) / g, '' )
268- const raw = decodeBase64 ( pem )
269- return formatPEM ( spkiFromX509 ( raw ) , 'PUBLIC KEY' )
256+ /**
257+ * Extracts SPKI from a PEM-encoded X.509 certificate string.
258+ *
259+ * @param x509 - PEM-encoded X.509 certificate
260+ *
261+ * @returns SPKI structure as bytes
262+ */
263+ function extractX509SPKI ( x509 : string ) : Uint8Array {
264+ const base64Content = x509 . replace ( / (?: - - - - - (?: B E G I N | E N D ) C E R T I F I C A T E - - - - - | \s ) / g, '' )
265+ const derBytes = decodeBase64 ( base64Content )
266+ return spkiFromX509 ( derBytes )
270267}
271268
272269export const fromX509 : PEMImportFunction = ( pem , alg , options ?) => {
273- let spki : string
270+ let spki : Uint8Array
274271 try {
275- spki = getSPKI ( pem )
272+ spki = extractX509SPKI ( pem )
276273 } catch ( cause ) {
277274 throw new TypeError ( 'Failed to parse the X.509 certificate' , { cause } )
278275 }
279- return fromSPKI ( spki , alg , options )
276+ return genericImport ( 'spki' , spki , alg , options )
280277}
0 commit comments