Skip to content

Commit 45bb45d

Browse files
committed
refactor(node): have node:crypto deal with x509 parsing
1 parent 7e93d4f commit 45bb45d

File tree

3 files changed

+102
-93
lines changed

3 files changed

+102
-93
lines changed

src/key/import.ts

Lines changed: 5 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,11 @@
1-
import { decode as decodeBase64URL, encodeBase64, decodeBase64 } from '../runtime/base64url.js'
2-
import { fromSPKI as importPublic } from '../runtime/asn1.js'
3-
import { fromPKCS8 as importPrivate } from '../runtime/asn1.js'
1+
import { decode as decodeBase64URL } from '../runtime/base64url.js'
2+
import { fromSPKI, fromPKCS8, fromX509 } from '../runtime/asn1.js'
43
import asKeyObject from '../runtime/jwk_to_key.js'
54

65
import { JOSENotSupported } from '../util/errors.js'
7-
import formatPEM from '../lib/format_pem.js'
86
import isObject from '../lib/is_object.js'
97
import type { JWK, KeyLike } from '../types.d'
108

11-
function getElement(seq: Uint8Array) {
12-
let result = []
13-
let next = 0
14-
15-
while (next < seq.length) {
16-
let nextPart = parseElement(seq.subarray(next))
17-
result.push(nextPart)
18-
next += nextPart.byteLength
19-
}
20-
return result
21-
}
22-
23-
function parseElement(bytes: Uint8Array) {
24-
let position = 0
25-
26-
// tag
27-
let tag = bytes[0] & 0x1f
28-
position++
29-
if (tag === 0x1f) {
30-
tag = 0
31-
while (bytes[position] >= 0x80) {
32-
tag = tag * 128 + bytes[position] - 0x80
33-
position++
34-
}
35-
tag = tag * 128 + bytes[position] - 0x80
36-
position++
37-
}
38-
39-
// length
40-
let length = 0
41-
if (bytes[position] < 0x80) {
42-
length = bytes[position]
43-
position++
44-
} else if (length === 0x80) {
45-
length = 0
46-
47-
while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {
48-
if (length > bytes.byteLength) {
49-
throw new TypeError('invalid indefinite form length')
50-
}
51-
length++
52-
}
53-
54-
const byteLength = position + length + 2
55-
return {
56-
byteLength,
57-
contents: bytes.subarray(position, position + length),
58-
raw: bytes.subarray(0, byteLength),
59-
}
60-
} else {
61-
let numberOfDigits = bytes[position] & 0x7f
62-
position++
63-
length = 0
64-
for (let i = 0; i < numberOfDigits; i++) {
65-
length = length * 256 + bytes[position]
66-
position++
67-
}
68-
}
69-
70-
const byteLength = position + length
71-
return {
72-
byteLength,
73-
contents: bytes.subarray(position, byteLength),
74-
raw: bytes.subarray(0, byteLength),
75-
}
76-
}
77-
78-
function spkiFromX509(buf: Uint8Array) {
79-
const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents)
80-
return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw)
81-
}
82-
83-
function getSPKI(x509: string): string {
84-
const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
85-
const raw = decodeBase64(pem)
86-
return formatPEM(spkiFromX509(raw), 'PUBLIC KEY')
87-
}
88-
899
export interface PEMImportOptions {
9010
/**
9111
* (Web Cryptography API specific) The value to use as
@@ -122,7 +42,7 @@ export async function importSPKI(
12242
if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) {
12343
throw new TypeError('"spki" must be SPKI formatted string')
12444
}
125-
return importPublic(spki, alg, options)
45+
return fromSPKI(spki, alg, options)
12646
}
12747

12848
/**
@@ -159,14 +79,7 @@ export async function importX509(
15979
if (typeof x509 !== 'string' || x509.indexOf('-----BEGIN CERTIFICATE-----') !== 0) {
16080
throw new TypeError('"x509" must be X.509 formatted string')
16181
}
162-
let spki: string
163-
try {
164-
spki = getSPKI(x509)
165-
} catch (cause) {
166-
// @ts-ignore
167-
throw new TypeError('failed to parse the X.509 certificate', { cause })
168-
}
169-
return importPublic(spki, alg, options)
82+
return fromX509(x509, alg, options)
17083
}
17184

17285
/**
@@ -197,7 +110,7 @@ export async function importPKCS8(
197110
if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) {
198111
throw new TypeError('"pkcs8" must be PKCS#8 formatted string')
199112
}
200-
return importPrivate(pkcs8, alg, options)
113+
return fromPKCS8(pkcs8, alg, options)
201114
}
202115

203116
/**

src/runtime/browser/asn1.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { isCloudflareWorkers } from './env.js'
22
import crypto, { isCryptoKey } from './webcrypto.js'
33
import type { PEMExportFunction, PEMImportFunction } from '../interfaces.d'
44
import invalidKeyInput from '../../lib/invalid_key_input.js'
5-
import { encodeBase64 } from './base64url.js'
5+
import { encodeBase64, decodeBase64 } from './base64url.js'
66
import formatPEM from '../../lib/format_pem.js'
77
import { JOSENotSupported } from '../../util/errors.js'
88
import { types } from './is_key_like.js'
@@ -177,3 +177,92 @@ export const fromPKCS8: PEMImportFunction = (pem, alg, options?) => {
177177
export const fromSPKI: PEMImportFunction = (pem, alg, options?) => {
178178
return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg, options)
179179
}
180+
181+
function getElement(seq: Uint8Array) {
182+
let result = []
183+
let next = 0
184+
185+
while (next < seq.length) {
186+
let nextPart = parseElement(seq.subarray(next))
187+
result.push(nextPart)
188+
next += nextPart.byteLength
189+
}
190+
return result
191+
}
192+
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++
204+
}
205+
tag = tag * 128 + bytes[position] - 0x80
206+
position++
207+
}
208+
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+
}
223+
224+
const byteLength = position + length + 2
225+
return {
226+
byteLength,
227+
contents: bytes.subarray(position, position + length),
228+
raw: bytes.subarray(0, byteLength),
229+
}
230+
} else {
231+
let 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+
}
238+
}
239+
240+
const byteLength = position + length
241+
return {
242+
byteLength,
243+
contents: bytes.subarray(position, byteLength),
244+
raw: bytes.subarray(0, byteLength),
245+
}
246+
}
247+
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)
251+
}
252+
253+
function getSPKI(x509: string): string {
254+
const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
255+
const raw = decodeBase64(pem)
256+
return formatPEM(spkiFromX509(raw), 'PUBLIC KEY')
257+
}
258+
259+
export const fromX509: PEMImportFunction = (pem, alg, options?) => {
260+
let spki: string
261+
try {
262+
spki = getSPKI(pem)
263+
} catch (cause) {
264+
// @ts-ignore
265+
throw new TypeError('failed to parse the X.509 certificate', { cause })
266+
}
267+
return fromSPKI(spki, alg, options)
268+
}

src/runtime/node/asn1.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@ export const fromSPKI: PEMImportFunction = (pem) =>
5151
type: 'spki',
5252
format: 'der',
5353
})
54+
55+
export const fromX509: PEMImportFunction = (pem) =>
56+
createPublicKey({
57+
key: pem,
58+
type: 'spki',
59+
format: 'pem',
60+
})

0 commit comments

Comments
 (0)