From 45bb45d42b6c96cbfcab7242d5cc366fb34481f1 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 18 Feb 2023 17:06:26 +0100 Subject: [PATCH] refactor(node): have node:crypto deal with x509 parsing --- src/key/import.ts | 97 ++----------------------------------- src/runtime/browser/asn1.ts | 91 +++++++++++++++++++++++++++++++++- src/runtime/node/asn1.ts | 7 +++ 3 files changed, 102 insertions(+), 93 deletions(-) diff --git a/src/key/import.ts b/src/key/import.ts index 954950f325..f092295a60 100644 --- a/src/key/import.ts +++ b/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 @@ -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) } /** @@ -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) } /** @@ -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) } /** diff --git a/src/runtime/browser/asn1.ts b/src/runtime/browser/asn1.ts index 7e0610f046..be2b8dfeb6 100644 --- a/src/runtime/browser/asn1.ts +++ b/src/runtime/browser/asn1.ts @@ -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' @@ -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) +} diff --git a/src/runtime/node/asn1.ts b/src/runtime/node/asn1.ts index 1a64d7aeb0..0dcae992a8 100644 --- a/src/runtime/node/asn1.ts +++ b/src/runtime/node/asn1.ts @@ -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', + })