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
10export 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
20const 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. */
28export const tlv = (tag: number, body: Uint8Array): Uint8Array =>
29 concat(Uint8Array.of(tag), encodeLength(body.length), body);
30
31export const sequence = (...items: ReadonlyArray<Uint8Array>) =>
32 tlv(0x30, concat(...items));
33export const set = (...items: ReadonlyArray<Uint8Array>) =>
34 tlv(0x31, concat(...items));
35export const octetString = (body: Uint8Array) => tlv(0x04, body);
36export const bitString = (body: Uint8Array) =>
37 tlv(0x03, concat(Uint8Array.of(0), body));
38export const nullValue = () => Uint8Array.of(0x05, 0x00);
39export const ia5String = (text: string) =>
40 tlv(0x16, new TextEncoder().encode(text));
41export 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). */
44export const contextTag = (n: number, body: Uint8Array) => tlv(0xa0 | n, body);
45/** Context-specific primitive tag `[n]` (e.g. GeneralName dNSName is `[2]`). */
46export const contextPrimitive = (n: number, body: Uint8Array) =>
47 tlv(0x80 | n, body);
48
49/** INTEGER from a non-negative number or big-endian magnitude bytes. */
50export 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. */
72export 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
91export 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
100export 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. */
113export 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
124export const content = (node: Node): Uint8Array =>
125 node.bytes.subarray(node.start, node.end);
126
127export 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`). */
148export 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
166export const toHex = (bytes: Uint8Array): string =>
167 Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");