@@ -67,64 +67,166 @@ export const toPKCS8 = (key: unknown): Promise<string> => {
6767 return genericExport ( 'private' , 'pkcs8' , key )
6868}
6969
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- */
77- const getNamedCurve = ( keyData : Uint8Array ) : string | 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- }
70+ /** Helper function to compare two byte arrays for equality */
71+ const bytesEqual = ( a : Uint8Array , b : readonly number [ ] ) : boolean => {
72+ if ( a . byteLength !== b . length ) return false
73+ for ( let i = 0 ; i < a . byteLength ; i ++ ) {
74+ if ( a [ i ] !== b [ i ] ) return false
75+ }
76+ return true
77+ }
78+
79+ /** ASN.1 DER parsing state */
80+ interface ASN1State {
81+ readonly data : Uint8Array
82+ pos : number
83+ }
84+
85+ /** Creates ASN.1 parsing state */
86+ const createASN1State = ( data : Uint8Array ) : ASN1State => ( { data, pos : 0 } )
87+
88+ /** Parses ASN.1 length encoding (both short and long form) */
89+ const parseLength = ( state : ASN1State ) : number => {
90+ const first = state . data [ state . pos ++ ]
91+ if ( first & 0x80 ) {
92+ // Long form: first byte indicates number of subsequent length bytes
93+ const lengthOfLen = first & 0x7f
94+ let length = 0
95+ for ( let i = 0 ; i < lengthOfLen ; i ++ ) {
96+ length = ( length << 8 ) | state . data [ state . pos ++ ]
97+ }
98+ return length
99+ }
100+ // Short form: length is encoded directly in first byte
101+ return first
102+ }
103+
104+ /** Skips ASN.1 elements (tag + length + content) */
105+ const skipElement = ( state : ASN1State , count : number = 1 ) : void => {
106+ if ( count <= 0 ) return
107+ state . pos ++ // Skip tag byte
108+ const length = parseLength ( state )
109+ state . pos += length // Skip content bytes
110+ if ( count > 1 ) {
111+ skipElement ( state , count - 1 ) // Recursively skip remaining elements
112+ }
113+ }
114+
115+ /** Expects a specific tag and throws if not found */
116+ const expectTag = ( state : ASN1State , expectedTag : number , errorMessage : string ) : void => {
117+ if ( state . data [ state . pos ++ ] !== expectedTag ) {
118+ throw new Error ( errorMessage )
119+ }
120+ }
121+
122+ /** Gets a subarray from current position */
123+ const getSubarray = ( state : ASN1State , length : number ) : Uint8Array => {
124+ const result = state . data . subarray ( state . pos , state . pos + length )
125+ state . pos += length
126+ return result
127+ }
128+
129+ /** Parses algorithm OID and returns the OID bytes */
130+ const parseAlgorithmOID = ( state : ASN1State ) : Uint8Array => {
131+ expectTag ( state , 0x06 , 'Expected algorithm OID' )
132+ const oidLen = parseLength ( state )
133+ return getSubarray ( state , oidLen )
134+ }
135+
136+ /** Parses a PKCS#8 private key structure up to the privateKey field */
137+ function parsePKCS8Header ( state : ASN1State ) {
138+ // Parse outer SEQUENCE (PrivateKeyInfo)
139+ expectTag ( state , 0x30 , 'Invalid PKCS#8 structure' )
140+ parseLength ( state ) // Skip outer length
141+
142+ // Skip version (INTEGER)
143+ expectTag ( state , 0x02 , 'Expected version field' )
144+ const verLen = parseLength ( state )
145+ state . pos += verLen
146+
147+ // Parse privateKeyAlgorithm (AlgorithmIdentifier SEQUENCE)
148+ expectTag ( state , 0x30 , 'Expected algorithm identifier' )
149+ const algIdLen = parseLength ( state )
150+ const algIdStart = state . pos
151+
152+ return { algIdStart, algIdLength : algIdLen }
153+ }
154+
155+ /** Parses an SPKI structure up to the subjectPublicKey field */
156+ function parseSPKIHeader ( state : ASN1State ) {
157+ // Parse outer SEQUENCE (SubjectPublicKeyInfo)
158+ expectTag ( state , 0x30 , 'Invalid SPKI structure' )
159+ parseLength ( state ) // Skip outer length
160+
161+ // Parse algorithm identifier (AlgorithmIdentifier SEQUENCE)
162+ expectTag ( state , 0x30 , 'Expected algorithm identifier' )
163+ const algIdLen = parseLength ( state )
164+ const algIdStart = state . pos
165+
166+ return { algIdStart, algIdLength : algIdLen }
167+ }
168+
169+ /** Parses algorithm identifier and returns curve name for EC/ECDH keys */
170+ const parseECAlgorithmIdentifier = ( state : ASN1State ) : string => {
171+ const algOid = parseAlgorithmOID ( state )
172+
173+ // id-x25519
174+ if ( bytesEqual ( algOid , [ 0x2b , 0x65 , 0x6e ] ) ) {
175+ return 'X25519'
176+ }
177+
178+ // id-ecPublicKey 1.2.840.10045.2.1
179+ if ( ! bytesEqual ( algOid , [ 0x2a , 0x86 , 0x48 , 0xce , 0x3d , 0x02 , 0x01 ] ) ) {
180+ throw new Error ( 'Unsupported key algorithm' )
181+ }
182+
183+ // Parse curve parameters (should be an OID for named curves)
184+ expectTag ( state , 0x06 , 'Expected curve OID' )
185+ const curveOidLen = parseLength ( state )
186+ const curveOid = getSubarray ( state , curveOidLen )
187+
188+ // Compare with known curve OIDs - NIST curves inlined
189+ for ( const { name, oid } of [
190+ { name : 'P-256' , oid : [ 0x2a , 0x86 , 0x48 , 0xce , 0x3d , 0x03 , 0x01 , 0x07 ] } , // 1.2.840.10045.3.1.7
191+ { name : 'P-384' , oid : [ 0x2b , 0x81 , 0x04 , 0x00 , 0x22 ] } , // 1.3.132.0.34
192+ { name : 'P-521' , oid : [ 0x2b , 0x81 , 0x04 , 0x00 , 0x23 ] } , // 1.3.132.0.35
193+ ] as const ) {
194+ if ( bytesEqual ( curveOid , oid ) ) {
195+ return name
94196 }
95197 }
96198
97- return undefined
199+ throw new Error ( 'Unsupported named curve' )
98200}
99201
100202const genericImport = async (
101203 keyFormat : 'spki' | 'pkcs8' ,
102204 keyData : Uint8Array ,
103205 alg : string ,
104- options ?: KeyImportOptions ,
206+ options ?: KeyImportOptions & { getNamedCurve ?: ( keyData : Uint8Array ) => string } ,
105207) => {
106208 let algorithm : RsaHashedImportParams | EcKeyAlgorithm | Algorithm
107209 let keyUsages : KeyUsage [ ]
108210
109211 const isPublic = keyFormat === 'spki'
110212
111213 // Helper functions for determining key usage based on key type
112- const getSignatureUsages = ( ) : KeyUsage [ ] => ( isPublic ? [ 'verify' ] : [ 'sign' ] )
113- const getEncryptionUsages = ( ) : KeyUsage [ ] =>
214+ const getSigUsages = ( ) : KeyUsage [ ] => ( isPublic ? [ 'verify' ] : [ 'sign' ] )
215+ const getEncUsages = ( ) : KeyUsage [ ] =>
114216 isPublic ? [ 'encrypt' , 'wrapKey' ] : [ 'decrypt' , 'unwrapKey' ]
115217
116218 switch ( alg ) {
117219 case 'PS256' :
118220 case 'PS384' :
119221 case 'PS512' :
120222 algorithm = { name : 'RSA-PSS' , hash : `SHA-${ alg . slice ( - 3 ) } ` }
121- keyUsages = getSignatureUsages ( )
223+ keyUsages = getSigUsages ( )
122224 break
123225 case 'RS256' :
124226 case 'RS384' :
125227 case 'RS512' :
126228 algorithm = { name : 'RSASSA-PKCS1-v1_5' , hash : `SHA-${ alg . slice ( - 3 ) } ` }
127- keyUsages = getSignatureUsages ( )
229+ keyUsages = getSigUsages ( )
128230 break
129231 case 'RSA-OAEP' :
130232 case 'RSA-OAEP-256' :
@@ -134,29 +236,33 @@ const genericImport = async (
134236 name : 'RSA-OAEP' ,
135237 hash : `SHA-${ parseInt ( alg . slice ( - 3 ) , 10 ) || 1 } ` ,
136238 }
137- keyUsages = getEncryptionUsages ( )
239+ keyUsages = getEncUsages ( )
138240 break
139241 case 'ES256' :
140242 case 'ES384' :
141243 case 'ES512' : {
142244 const curveMap = { ES256 : 'P-256' , ES384 : 'P-384' , ES512 : 'P-521' } as const
143245 algorithm = { name : 'ECDSA' , namedCurve : curveMap [ alg ] }
144- keyUsages = getSignatureUsages ( )
246+ keyUsages = getSigUsages ( )
145247 break
146248 }
147249 case 'ECDH-ES' :
148250 case 'ECDH-ES+A128KW' :
149251 case 'ECDH-ES+A192KW' :
150252 case 'ECDH-ES+A256KW' : {
151- const namedCurve = getNamedCurve ( keyData )
152- algorithm = namedCurve ? { name : 'ECDH' , namedCurve } : { name : 'X25519' }
253+ try {
254+ const namedCurve = options ! . getNamedCurve ! ( keyData )
255+ algorithm = namedCurve === 'X25519' ? { name : 'X25519' } : { name : 'ECDH' , namedCurve }
256+ } catch ( cause ) {
257+ throw new JOSENotSupported ( 'Invalid or unsupported key format' )
258+ }
153259 keyUsages = isPublic ? [ ] : [ 'deriveBits' ]
154260 break
155261 }
156262 case 'Ed25519' :
157263 case 'EdDSA' :
158264 algorithm = { name : 'Ed25519' }
159- keyUsages = getSignatureUsages ( )
265+ keyUsages = getSigUsages ( )
160266 break
161267 default :
162268 throw new JOSENotSupported ( 'Invalid or unsupported "alg" (Algorithm) value' )
@@ -177,14 +283,43 @@ type PEMImportFunction = (
177283 options ?: KeyImportOptions ,
178284) => Promise < types . CryptoKey >
179285
286+ /** Helper function to process PEM-encoded data */
287+ const processPEMData = ( pem : string , pattern : RegExp ) : Uint8Array => {
288+ return decodeBase64 ( pem . replace ( pattern , '' ) )
289+ }
290+
180291export const fromPKCS8 : PEMImportFunction = ( 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 )
292+ const keyData = processPEMData ( pem , / (?: - - - - - (?: B E G I N | E N D ) P R I V A T E K E Y - - - - - | \s ) / g)
293+
294+ let opts : Parameters < typeof genericImport > [ 3 ] = options
295+
296+ if ( alg ?. startsWith ?.( 'ECDH-ES' ) ) {
297+ opts ||= { }
298+ opts . getNamedCurve = ( keyData : Uint8Array ) => {
299+ const state = createASN1State ( keyData )
300+ parsePKCS8Header ( state )
301+ return parseECAlgorithmIdentifier ( state )
302+ }
303+ }
304+
305+ return genericImport ( 'pkcs8' , keyData , alg , opts )
183306}
184307
185308export const fromSPKI : PEMImportFunction = ( 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 )
309+ const keyData = processPEMData ( pem , / (?: - - - - - (?: B E G I N | E N D ) P U B L I C K E Y - - - - - | \s ) / g)
310+
311+ let opts : Parameters < typeof genericImport > [ 3 ] = options
312+
313+ if ( alg ?. startsWith ?.( 'ECDH-ES' ) ) {
314+ opts ||= { }
315+ opts . getNamedCurve = ( keyData : Uint8Array ) => {
316+ const state = createASN1State ( keyData )
317+ parseSPKIHeader ( state )
318+ return parseECAlgorithmIdentifier ( state )
319+ }
320+ }
321+
322+ return genericImport ( 'spki' , keyData , alg , opts )
188323}
189324
190325/**
@@ -196,61 +331,33 @@ export const fromSPKI: PEMImportFunction = (pem, alg, options?) => {
196331 * @returns SPKI structure as bytes
197332 */
198333function 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
216- }
217-
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
226- }
227- }
334+ const state = createASN1State ( buf )
228335
229336 // Parse outer certificate SEQUENCE
230- if ( buf [ pos ++ ] !== 0x30 ) throw new Error ( 'Invalid certificate structure' )
231- parseLength ( ) // Skip certificate length
337+ expectTag ( state , 0x30 , 'Invalid certificate structure' )
338+ parseLength ( state ) // Skip certificate length
232339
233340 // Parse tbsCertificate (To Be Signed Certificate) SEQUENCE
234- if ( buf [ pos ++ ] !== 0x30 ) throw new Error ( 'Invalid tbsCertificate structure' )
235- parseLength ( ) // Skip tbsCertificate length
341+ expectTag ( state , 0x30 , 'Invalid tbsCertificate structure' )
342+ parseLength ( state ) // Skip tbsCertificate length
236343
237- if ( buf [ pos ] === 0xa0 ) {
344+ if ( buf [ state . pos ] === 0xa0 ) {
238345 // Optional version field present (context-specific [0])
239346 // Skip: version, serialNumber, signature algorithm, issuer, validity, subject
240- skipElement ( 6 )
347+ skipElement ( state , 6 )
241348 } else {
242349 // No version field (defaults to v1)
243350 // Skip: serialNumber, signature algorithm, issuer, validity, subject
244- skipElement ( 5 )
351+ skipElement ( state , 5 )
245352 }
246353
247354 // Extract subjectPublicKeyInfo SEQUENCE
248- const spkiStart = pos
249- if ( buf [ pos ++ ] !== 0x30 ) throw new Error ( 'Invalid SPKI structure' )
250- const spkiContentLength = parseLength ( )
355+ const spkiStart = state . pos
356+ expectTag ( state , 0x30 , 'Invalid SPKI structure' )
357+ const spkiContentLen = parseLength ( state )
251358
252359 // Return the complete SPKI structure (tag + length + content)
253- return buf . subarray ( spkiStart , spkiStart + spkiContentLength + ( pos - spkiStart ) )
360+ return buf . subarray ( spkiStart , spkiStart + spkiContentLen + ( state . pos - spkiStart ) )
254361}
255362
256363/**
@@ -261,8 +368,7 @@ function spkiFromX509(buf: Uint8Array): Uint8Array {
261368 * @returns SPKI structure as bytes
262369 */
263370function 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 )
371+ const derBytes = processPEMData ( x509 , / (?: - - - - - (?: B E G I N | E N D ) C E R T I F I C A T E - - - - - | \s ) / g)
266372 return spkiFromX509 ( derBytes )
267373}
268374
@@ -273,5 +379,5 @@ export const fromX509: PEMImportFunction = (pem, alg, options?) => {
273379 } catch ( cause ) {
274380 throw new TypeError ( 'Failed to parse the X.509 certificate' , { cause } )
275381 }
276- return genericImport ( ' spki' , spki , alg , options )
382+ return fromSPKI ( formatPEM ( encodeBase64 ( spki ) , 'PUBLIC KEY' ) , alg , options )
277383}
0 commit comments