Skip to content

Commit b4f8fb3

Browse files
committed
refactor: update asn1.ts helpers
1 parent 413fa45 commit b4f8fb3

File tree

1 file changed

+188
-82
lines changed

1 file changed

+188
-82
lines changed

src/lib/asn1.ts

Lines changed: 188 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -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

100202
const 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+
180291
export const fromPKCS8: PEMImportFunction = (pem, alg, options?) => {
181-
const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, ''))
182-
return genericImport('pkcs8', keyData, alg, options)
292+
const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\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

185308
export const fromSPKI: PEMImportFunction = (pem, alg, options?) => {
186-
const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, ''))
187-
return genericImport('spki', keyData, alg, options)
309+
const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\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
*/
198333
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
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
*/
263370
function extractX509SPKI(x509: string): Uint8Array {
264-
const base64Content = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
265-
const derBytes = decodeBase64(base64Content)
371+
const derBytes = processPEMData(x509, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\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

Comments
 (0)