| 1 | /** |
| 2 | * A minimal DER encoder/decoder — just enough ASN.1 for a PKCS#10 |
| 3 | * certificate request (encode) and reading a certificate's serial, |
| 4 | * validity, issuer and subject alternative names (decode). Pure |
| 5 | * TypeScript; no native code, no dependency, works on workerd. |
| 6 | * |
| 7 | * @internal |
| 8 | */ |
| 9 | |
| 10 | export const concat = (...parts: ReadonlyArray<Uint8Array>): Uint8Array => { |
| 11 | const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); |
| 12 | let offset = 0; |
| 13 | for (const part of parts) { |
| 14 | out.set(part, offset); |
| 15 | offset += part.length; |
| 16 | } |
| 17 | return out; |
| 18 | }; |
| 19 | |
| 20 | const encodeLength = (n: number): Uint8Array => { |
| 21 | if (n < 0x80) return Uint8Array.of(n); |
| 22 | if (n < 0x100) return Uint8Array.of(0x81, n); |
| 23 | if (n < 0x10000) return Uint8Array.of(0x82, n >>> 8, n & 0xff); |
| 24 | return Uint8Array.of(0x83, n >>> 16, (n >>> 8) & 0xff, n & 0xff); |
| 25 | }; |
| 26 | |
| 27 | /** Tag-length-value. */ |
| 28 | export const tlv = (tag: number, body: Uint8Array): Uint8Array => |
| 29 | concat(Uint8Array.of(tag), encodeLength(body.length), body); |
| 30 | |
| 31 | export const sequence = (...items: ReadonlyArray<Uint8Array>) => |
| 32 | tlv(0x30, concat(...items)); |
| 33 | export const set = (...items: ReadonlyArray<Uint8Array>) => |
| 34 | tlv(0x31, concat(...items)); |
| 35 | export const octetString = (body: Uint8Array) => tlv(0x04, body); |
| 36 | export const bitString = (body: Uint8Array) => |
| 37 | tlv(0x03, concat(Uint8Array.of(0), body)); |
| 38 | export const nullValue = () => Uint8Array.of(0x05, 0x00); |
| 39 | export const ia5String = (text: string) => |
| 40 | tlv(0x16, new TextEncoder().encode(text)); |
| 41 | export const utf8String = (text: string) => |
| 42 | tlv(0x0c, new TextEncoder().encode(text)); |
| 43 | /** Context-specific constructed tag `[n]` (implicit or explicit, caller's choice of body). */ |
| 44 | export const contextTag = (n: number, body: Uint8Array) => tlv(0xa0 | n, body); |
| 45 | /** Context-specific primitive tag `[n]` (e.g. GeneralName dNSName is `[2]`). */ |
| 46 | export const contextPrimitive = (n: number, body: Uint8Array) => |
| 47 | tlv(0x80 | n, body); |
| 48 | |
| 49 | /** INTEGER from a non-negative number or big-endian magnitude bytes. */ |
| 50 | export const integer = (value: number | Uint8Array): Uint8Array => { |
| 51 | if (typeof value === "number") { |
| 52 | if (value === 0) return tlv(0x02, Uint8Array.of(0)); |
| 53 | const bytes: number[] = []; |
| 54 | let n = value; |
| 55 | while (n > 0) { |
| 56 | bytes.unshift(n & 0xff); |
| 57 | n = Math.floor(n / 256); |
| 58 | } |
| 59 | if (bytes[0]! & 0x80) bytes.unshift(0); |
| 60 | return tlv(0x02, Uint8Array.from(bytes)); |
| 61 | } |
| 62 | let start = 0; |
| 63 | while (start < value.length - 1 && value[start] === 0) start++; |
| 64 | const magnitude = value.subarray(start); |
| 65 | return tlv( |
| 66 | 0x02, |
| 67 | magnitude[0]! & 0x80 ? concat(Uint8Array.of(0), magnitude) : magnitude, |
| 68 | ); |
| 69 | }; |
| 70 | |
| 71 | /** OBJECT IDENTIFIER from dotted notation. */ |
| 72 | export const oid = (dotted: string): Uint8Array => { |
| 73 | const parts = dotted.split(".").map(Number); |
| 74 | const bytes: number[] = [parts[0]! * 40 + parts[1]!]; |
| 75 | for (const part of parts.slice(2)) { |
| 76 | const stack: number[] = [part & 0x7f]; |
| 77 | let n = Math.floor(part / 128); |
| 78 | while (n > 0) { |
| 79 | stack.unshift((n & 0x7f) | 0x80); |
| 80 | n = Math.floor(n / 128); |
| 81 | } |
| 82 | bytes.push(...stack); |
| 83 | } |
| 84 | return tlv(0x06, Uint8Array.from(bytes)); |
| 85 | }; |
| 86 | |
| 87 | // ============================================================================= |
| 88 | // Decoding |
| 89 | // ============================================================================= |
| 90 | |
| 91 | export interface Node { |
| 92 | readonly tag: number; |
| 93 | /** Offset of the first content byte. */ |
| 94 | readonly start: number; |
| 95 | /** Offset one past the last content byte. */ |
| 96 | readonly end: number; |
| 97 | readonly bytes: Uint8Array; |
| 98 | } |
| 99 | |
| 100 | export const read = (bytes: Uint8Array, offset: number): Node => { |
| 101 | const tag = bytes[offset]!; |
| 102 | let cursor = offset + 1; |
| 103 | let length = bytes[cursor++]!; |
| 104 | if (length & 0x80) { |
| 105 | const count = length & 0x7f; |
| 106 | length = 0; |
| 107 | for (let i = 0; i < count; i++) length = length * 256 + bytes[cursor++]!; |
| 108 | } |
| 109 | return { tag, start: cursor, end: cursor + length, bytes }; |
| 110 | }; |
| 111 | |
| 112 | /** The children of a constructed node, in order. */ |
| 113 | export const children = (node: Node): Node[] => { |
| 114 | const out: Node[] = []; |
| 115 | let cursor = node.start; |
| 116 | while (cursor < node.end) { |
| 117 | const child = read(node.bytes, cursor); |
| 118 | out.push(child); |
| 119 | cursor = child.end; |
| 120 | } |
| 121 | return out; |
| 122 | }; |
| 123 | |
| 124 | export const content = (node: Node): Uint8Array => |
| 125 | node.bytes.subarray(node.start, node.end); |
| 126 | |
| 127 | export const decodeOid = (node: Node): string => { |
| 128 | const body = content(node); |
| 129 | const first = body[0]!; |
| 130 | const parts = [Math.floor(first / 40), first % 40]; |
| 131 | let n = 0; |
| 132 | for (let i = 1; i < body.length; i++) { |
| 133 | n = n * 128 + (body[i]! & 0x7f); |
| 134 | if ((body[i]! & 0x80) === 0) { |
| 135 | parts.push(n); |
| 136 | n = 0; |
| 137 | } |
| 138 | } |
| 139 | // OIDs under joint-iso-itu-t(2) can have a first arc ≥ 2 with large second arcs. |
| 140 | if (first >= 80) { |
| 141 | parts[0] = 2; |
| 142 | parts[1] = first - 80; |
| 143 | } |
| 144 | return parts.join("."); |
| 145 | }; |
| 146 | |
| 147 | /** Parse UTCTime (`YYMMDDHHMMSSZ`) or GeneralizedTime (`YYYYMMDDHHMMSSZ`). */ |
| 148 | export const decodeTime = (node: Node): Date => { |
| 149 | const text = new TextDecoder().decode(content(node)); |
| 150 | const generalized = node.tag === 0x18; |
| 151 | const year = generalized |
| 152 | ? Number(text.slice(0, 4)) |
| 153 | : (() => { |
| 154 | const yy = Number(text.slice(0, 2)); |
| 155 | return yy < 50 ? 2000 + yy : 1900 + yy; |
| 156 | })(); |
| 157 | const rest = generalized ? text.slice(4) : text.slice(2); |
| 158 | const month = Number(rest.slice(0, 2)); |
| 159 | const day = Number(rest.slice(2, 4)); |
| 160 | const hour = Number(rest.slice(4, 6)); |
| 161 | const minute = Number(rest.slice(6, 8)); |
| 162 | const second = Number(rest.slice(8, 10)) || 0; |
| 163 | return new Date(Date.UTC(year, month - 1, day, hour, minute, second)); |
| 164 | }; |
| 165 | |
| 166 | export const toHex = (bytes: Uint8Array): string => |
| 167 | Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); |