Skip to content

Commit

Permalink
refactor(node): have node:crypto deal with x509 parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 18, 2023
1 parent 7e93d4f commit 45bb45d
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 93 deletions.
97 changes: 5 additions & 92 deletions src/key/import.ts
@@ -1,91 +1,11 @@
import { decode as decodeBase64URL, encodeBase64, decodeBase64 } from '../runtime/base64url.js'
import { fromSPKI as importPublic } from '../runtime/asn1.js'
import { fromPKCS8 as importPrivate } from '../runtime/asn1.js'
import { decode as decodeBase64URL } from '../runtime/base64url.js'
import { fromSPKI, fromPKCS8, fromX509 } from '../runtime/asn1.js'
import asKeyObject from '../runtime/jwk_to_key.js'

import { JOSENotSupported } from '../util/errors.js'
import formatPEM from '../lib/format_pem.js'
import isObject from '../lib/is_object.js'
import type { JWK, KeyLike } from '../types.d'

function getElement(seq: Uint8Array) {
let result = []
let next = 0

while (next < seq.length) {
let nextPart = parseElement(seq.subarray(next))
result.push(nextPart)
next += nextPart.byteLength
}
return result
}

function parseElement(bytes: Uint8Array) {
let position = 0

// tag
let tag = bytes[0] & 0x1f
position++
if (tag === 0x1f) {
tag = 0
while (bytes[position] >= 0x80) {
tag = tag * 128 + bytes[position] - 0x80
position++
}
tag = tag * 128 + bytes[position] - 0x80
position++
}

// length
let length = 0
if (bytes[position] < 0x80) {
length = bytes[position]
position++
} else if (length === 0x80) {
length = 0

while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {
if (length > bytes.byteLength) {
throw new TypeError('invalid indefinite form length')
}
length++
}

const byteLength = position + length + 2
return {
byteLength,
contents: bytes.subarray(position, position + length),
raw: bytes.subarray(0, byteLength),
}
} else {
let numberOfDigits = bytes[position] & 0x7f
position++
length = 0
for (let i = 0; i < numberOfDigits; i++) {
length = length * 256 + bytes[position]
position++
}
}

const byteLength = position + length
return {
byteLength,
contents: bytes.subarray(position, byteLength),
raw: bytes.subarray(0, byteLength),
}
}

function spkiFromX509(buf: Uint8Array) {
const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents)
return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw)
}

function getSPKI(x509: string): string {
const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
const raw = decodeBase64(pem)
return formatPEM(spkiFromX509(raw), 'PUBLIC KEY')
}

export interface PEMImportOptions {
/**
* (Web Cryptography API specific) The value to use as
Expand Down Expand Up @@ -122,7 +42,7 @@ export async function importSPKI(
if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) {
throw new TypeError('"spki" must be SPKI formatted string')
}
return importPublic(spki, alg, options)
return fromSPKI(spki, alg, options)
}

/**
Expand Down Expand Up @@ -159,14 +79,7 @@ export async function importX509(
if (typeof x509 !== 'string' || x509.indexOf('-----BEGIN CERTIFICATE-----') !== 0) {
throw new TypeError('"x509" must be X.509 formatted string')
}
let spki: string
try {
spki = getSPKI(x509)
} catch (cause) {
// @ts-ignore
throw new TypeError('failed to parse the X.509 certificate', { cause })
}
return importPublic(spki, alg, options)
return fromX509(x509, alg, options)
}

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

/**
Expand Down
91 changes: 90 additions & 1 deletion src/runtime/browser/asn1.ts
Expand Up @@ -2,7 +2,7 @@ import { isCloudflareWorkers } from './env.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import type { PEMExportFunction, PEMImportFunction } from '../interfaces.d'
import invalidKeyInput from '../../lib/invalid_key_input.js'
import { encodeBase64 } from './base64url.js'
import { encodeBase64, decodeBase64 } from './base64url.js'
import formatPEM from '../../lib/format_pem.js'
import { JOSENotSupported } from '../../util/errors.js'
import { types } from './is_key_like.js'
Expand Down Expand Up @@ -177,3 +177,92 @@ export const fromPKCS8: PEMImportFunction = (pem, alg, options?) => {
export const fromSPKI: PEMImportFunction = (pem, alg, options?) => {
return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg, options)
}

function getElement(seq: Uint8Array) {
let result = []
let next = 0

while (next < seq.length) {
let nextPart = parseElement(seq.subarray(next))
result.push(nextPart)
next += nextPart.byteLength
}
return result
}

function parseElement(bytes: Uint8Array) {
let position = 0

// tag
let tag = bytes[0] & 0x1f
position++
if (tag === 0x1f) {
tag = 0
while (bytes[position] >= 0x80) {
tag = tag * 128 + bytes[position] - 0x80
position++
}
tag = tag * 128 + bytes[position] - 0x80
position++
}

// length
let length = 0
if (bytes[position] < 0x80) {
length = bytes[position]
position++
} else if (length === 0x80) {
length = 0

while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {
if (length > bytes.byteLength) {
throw new TypeError('invalid indefinite form length')
}
length++
}

const byteLength = position + length + 2
return {
byteLength,
contents: bytes.subarray(position, position + length),
raw: bytes.subarray(0, byteLength),
}
} else {
let numberOfDigits = bytes[position] & 0x7f
position++
length = 0
for (let i = 0; i < numberOfDigits; i++) {
length = length * 256 + bytes[position]
position++
}
}

const byteLength = position + length
return {
byteLength,
contents: bytes.subarray(position, byteLength),
raw: bytes.subarray(0, byteLength),
}
}

function spkiFromX509(buf: Uint8Array) {
const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents)
return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw)
}

function getSPKI(x509: string): string {
const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
const raw = decodeBase64(pem)
return formatPEM(spkiFromX509(raw), 'PUBLIC KEY')
}

export const fromX509: PEMImportFunction = (pem, alg, options?) => {
let spki: string
try {
spki = getSPKI(pem)
} catch (cause) {
// @ts-ignore
throw new TypeError('failed to parse the X.509 certificate', { cause })
}
return fromSPKI(spki, alg, options)
}
7 changes: 7 additions & 0 deletions src/runtime/node/asn1.ts
Expand Up @@ -51,3 +51,10 @@ export const fromSPKI: PEMImportFunction = (pem) =>
type: 'spki',
format: 'der',
})

export const fromX509: PEMImportFunction = (pem) =>
createPublicKey({
key: pem,
type: 'spki',
format: 'pem',
})

0 comments on commit 45bb45d

Please sign in to comment.