Skip to content

Commit b2b611c

Browse files
authored
refactor: update asn1.ts helper functions
1 parent 61ded78 commit b2b611c

File tree

1 file changed

+123
-126
lines changed

1 file changed

+123
-126
lines changed

src/lib/asn1.ts

Lines changed: 123 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import { isCryptoKey, isKeyObject } from './is_key_like.js'
66

77
import 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+
*/
917
const 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+
*/
7477
const 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

87100
const 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

173180
export const fromPKCS8: PEMImportFunction = (pem, alg, options?) => {
174-
return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, 'pkcs8', pem, alg, options)
181+
const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, ''))
182+
return genericImport('pkcs8', keyData, alg, options)
175183
}
176184

177185
export const fromSPKI: PEMImportFunction = (pem, alg, options?) => {
178-
return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg, options)
186+
const keyData = decodeBase64(pem.replace(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\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(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\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(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
265+
const derBytes = decodeBase64(base64Content)
266+
return spkiFromX509(derBytes)
270267
}
271268

272269
export 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

Comments
 (0)