|
| 1 | +import { decodeBase64 } from './base64'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Parses a sequence of ASN.1 elements from a given Uint8Array. |
| 5 | + * Internally, this function repeatedly calls `parseElement` on |
| 6 | + * the subarray until the entire sequence is consumed, returning |
| 7 | + * an array of parsed elements. |
| 8 | + */ |
| 9 | +function getElement(seq: Uint8Array) { |
| 10 | + const result = []; |
| 11 | + let next = 0; |
| 12 | + |
| 13 | + while (next < seq.length) { |
| 14 | + // Parse one ASN.1 element from the remaining subarray |
| 15 | + const nextPart = parseElement(seq.subarray(next)); |
| 16 | + result.push(nextPart); |
| 17 | + // Advance the pointer by the element's total byte length |
| 18 | + next += nextPart.byteLength; |
| 19 | + } |
| 20 | + return result; |
| 21 | +} |
| 22 | + |
| 23 | +/** |
| 24 | + * Parses a single ASN.1 element (in DER encoding) from the given byte array. |
| 25 | + * |
| 26 | + * Each element consists of: |
| 27 | + * 1) Tag (possibly multiple bytes if 0x1f is encountered) |
| 28 | + * 2) Length (short form or long form, possibly indefinite) |
| 29 | + * 3) Contents (the data payload) |
| 30 | + * |
| 31 | + * Returns an object containing: |
| 32 | + * - byteLength: total size (in bytes) of this element (including tag & length) |
| 33 | + * - contents: Uint8Array of just the element's contents |
| 34 | + * - raw: Uint8Array of the entire element (tag + length + contents) |
| 35 | + */ |
| 36 | +function parseElement(bytes: Uint8Array) { |
| 37 | + let position = 0; |
| 38 | + |
| 39 | + // --- Parse Tag --- |
| 40 | + // The tag is in the lower 5 bits (0x1f). If it's 0x1f, it indicates a multi-byte tag. |
| 41 | + let tag = bytes[0] & 0x1f; |
| 42 | + position++; |
| 43 | + if (tag === 0x1f) { |
| 44 | + tag = 0; |
| 45 | + // Continue reading the tag bytes while each byte >= 0x80 |
| 46 | + while (bytes[position] >= 0x80) { |
| 47 | + tag = tag * 128 + bytes[position] - 0x80; |
| 48 | + position++; |
| 49 | + } |
| 50 | + tag = tag * 128 + bytes[position] - 0x80; |
| 51 | + position++; |
| 52 | + } |
| 53 | + |
| 54 | + // --- Parse Length --- |
| 55 | + let length = 0; |
| 56 | + // Short-form length: if less than 0x80, it's the length itself |
| 57 | + if (bytes[position] < 0x80) { |
| 58 | + length = bytes[position]; |
| 59 | + position++; |
| 60 | + } else if (length === 0x80) { |
| 61 | + // Indefinite length form: scan until 0x00 0x00 |
| 62 | + length = 0; |
| 63 | + while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) { |
| 64 | + if (length > bytes.byteLength) { |
| 65 | + throw new TypeError('invalid indefinite form length'); |
| 66 | + } |
| 67 | + length++; |
| 68 | + } |
| 69 | + const byteLength = position + length + 2; |
| 70 | + return { |
| 71 | + byteLength, |
| 72 | + contents: bytes.subarray(position, position + length), |
| 73 | + raw: bytes.subarray(0, byteLength), |
| 74 | + }; |
| 75 | + } else { |
| 76 | + // Long-form length: the lower 7 bits of this byte indicates how many bytes follow for length |
| 77 | + const numberOfDigits = bytes[position] & 0x7f; |
| 78 | + position++; |
| 79 | + length = 0; |
| 80 | + // Accumulate the length from these "numberOfDigits" bytes |
| 81 | + for (let i = 0; i < numberOfDigits; i++) { |
| 82 | + length = length * 256 + bytes[position]; |
| 83 | + position++; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + // The total byte length of this element (tag + length + contents) |
| 88 | + const byteLength = position + length; |
| 89 | + return { |
| 90 | + byteLength, |
| 91 | + contents: bytes.subarray(position, byteLength), |
| 92 | + raw: bytes.subarray(0, byteLength), |
| 93 | + }; |
| 94 | +} |
| 95 | + |
| 96 | +/** |
| 97 | + * Extracts the SubjectPublicKeyInfo (SPKI) portion from a DER-encoded X.509 certificate. |
| 98 | + * |
| 99 | + * Steps: |
| 100 | + * 1) Parse the entire certificate as an ASN.1 SEQUENCE. |
| 101 | + * 2) Retrieve the TBS (To-Be-Signed) Certificate, which is the first element. |
| 102 | + * 3) Parse the TBS Certificate to get its internal fields (version, serial, issuer, etc.). |
| 103 | + * 4) Depending on whether the version field is present (tag = 0xa0), the SPKI is either |
| 104 | + * at index 6 or 5 (skipping version if absent). |
| 105 | + * 5) Finally, encode the raw SPKI bytes in CryptoKey and return. |
| 106 | + */ |
| 107 | +async function spkiFromX509(buf: Uint8Array): Promise<CryptoKey> { |
| 108 | + // Parse the top-level ASN.1 structure, then get the top-level contents |
| 109 | + // which typically contain [ TBS Certificate, signatureAlgorithm, signature ]. |
| 110 | + // Retrieve TBS Certificate as [0], then parse TBS Certificate further. |
| 111 | + const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents); |
| 112 | + |
| 113 | + // In the TBS Certificate, check whether the first element (index 0) is a version field (tag=0xa0). |
| 114 | + // If it is, the SubjectPublicKeyInfo is the 7th element (index 6). |
| 115 | + // Otherwise, it is the 6th element (index 5). |
| 116 | + const spki = tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw; |
| 117 | + return await crypto.subtle.importKey( |
| 118 | + 'spki', |
| 119 | + spki, |
| 120 | + { |
| 121 | + name: 'RSASSA-PKCS1-v1_5', |
| 122 | + hash: 'SHA-256', |
| 123 | + }, |
| 124 | + true, |
| 125 | + ['verify'] |
| 126 | + ); |
| 127 | +} |
| 128 | + |
| 129 | +export async function jwkFromX509(kid: string, x509: string): Promise<JsonWebKeyWithKid> { |
| 130 | + const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, ''); |
| 131 | + const raw = decodeBase64(pem); |
| 132 | + const spki = await spkiFromX509(raw); |
| 133 | + const { kty, alg, n, e } = await crypto.subtle.exportKey('jwk', spki); |
| 134 | + return { |
| 135 | + kid, |
| 136 | + use: 'sig', |
| 137 | + kty, |
| 138 | + alg, |
| 139 | + n, |
| 140 | + e, |
| 141 | + }; |
| 142 | +} |
0 commit comments