@runuai/host 0.9.147 → 0.9.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +137 -13
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +17 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +125 -58
- package/lib/agents/types.ts +3 -3
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +1681 -138
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/lib/ulid.ts +10 -1
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +941 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2239 -15
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root-side steps that put a running task behind the secrets proxy
|
|
3
|
+
* (ADR-153 §3 "Order"), run through the task environment's bounded exec:
|
|
4
|
+
* the CA bundle into its trust store, then the pinned names and the NAT
|
|
5
|
+
* chain. task-up runs the same scripts at a container start; these run them
|
|
6
|
+
* on a running task — after a runner restart, when the CA bundle changes, and
|
|
7
|
+
* when the task's proxied domains do.
|
|
8
|
+
*
|
|
9
|
+
* Every step is a reconciliation (safe to repeat), and every failure throws:
|
|
10
|
+
* the caller withholds the task's proxied keys until a step succeeds, since a
|
|
11
|
+
* placeholder whose domain is not pinned, or whose CA the task does not
|
|
12
|
+
* trust, would only fail later and less clearly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { TaskEnvironmentExecResult, TaskEnvironmentHandle } from "../task-environment/types";
|
|
16
|
+
import { ROOT_SCRIPT_PATH, caInstallScript, hostsReconcileScript, redirectChainScript } from "./task-scripts";
|
|
17
|
+
|
|
18
|
+
const STEP_TIMEOUT_MS = 60_000;
|
|
19
|
+
const STEP_OUTPUT_BYTES = 64 * 1024;
|
|
20
|
+
/** The shell itself starts clean, not only the script's children: the
|
|
21
|
+
* inherited PATH and loader variables can name task-writable files. */
|
|
22
|
+
const ROOT_STEP_ENV: Readonly<Record<string, string>> = Object.freeze({
|
|
23
|
+
PATH: ROOT_SCRIPT_PATH,
|
|
24
|
+
LD_PRELOAD: "",
|
|
25
|
+
LD_AUDIT: "",
|
|
26
|
+
LD_LIBRARY_PATH: "",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
type Exec = Pick<TaskEnvironmentHandle<unknown>, "exec">;
|
|
30
|
+
|
|
31
|
+
function checked(step: string, result: TaskEnvironmentExecResult): void {
|
|
32
|
+
if (result.exitCode === 0 && result.signal === null && !result.stdoutTruncated && !result.stderrTruncated) return;
|
|
33
|
+
const detail =
|
|
34
|
+
Buffer.from(result.stderr).toString("utf8").trim().slice(-512) ||
|
|
35
|
+
(result.signal === null ? `exit ${String(result.exitCode)}` : `signal ${result.signal}`);
|
|
36
|
+
throw new Error(`secrets proxy: ${step} failed in the task: ${detail}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Installs the bundle (`SecretsProxyRuntime.caBundlePem()`) as root. The
|
|
40
|
+
* PEM travels on stdin; it is public, but argv stays short and fixed. */
|
|
41
|
+
export async function installTaskTrust(environment: Exec, bundlePem: string): Promise<void> {
|
|
42
|
+
const result = await environment.exec({
|
|
43
|
+
argv: ["/bin/sh", "-c", caInstallScript()],
|
|
44
|
+
user: "root",
|
|
45
|
+
env: ROOT_STEP_ENV,
|
|
46
|
+
projectEnvProfile: "maintenance",
|
|
47
|
+
stdin: Buffer.from(bundlePem, "utf8"),
|
|
48
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
49
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
50
|
+
});
|
|
51
|
+
checked("installing the proxy CA", result);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Where a task's NET_ADMIN comes from: each privileged exec (Docker); the
|
|
55
|
+
* task VM itself, fixed when it was created (Apple); or the root that runs
|
|
56
|
+
* the step (a machine's sidecar, root on the task's own machine). */
|
|
57
|
+
export type TaskNetAdmin = "per-exec" | "from-creation" | "root";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pins `domains` to the task-local listener: the NAT chain that sends the pin
|
|
61
|
+
* address's :443 to `listenPort` (the task forwarder), and the /etc/hosts
|
|
62
|
+
* block that points the names there. The chain needs NET_ADMIN: on Docker
|
|
63
|
+
* the one privileged exec Uai makes; an Apple task's VM holds it from its
|
|
64
|
+
* creation, so there a root exec has it (and lacking it proves the VM never
|
|
65
|
+
* had it). Ordered so a failure part-way never leaves a name pointing at a
|
|
66
|
+
* pin with nothing behind it: when pinning, the chain goes first; when
|
|
67
|
+
* unpinning, the names go first. With no domains the block is removed and
|
|
68
|
+
* the chain emptied.
|
|
69
|
+
*/
|
|
70
|
+
export async function reconcileTaskPins(
|
|
71
|
+
environment: Exec,
|
|
72
|
+
options: {
|
|
73
|
+
domains: readonly string[];
|
|
74
|
+
listenPort: number;
|
|
75
|
+
netAdmin: TaskNetAdmin;
|
|
76
|
+
/** Redirect only this user's traffic (a machine, where the sidecar's own
|
|
77
|
+
* upstream connections share the network); everyone's otherwise. */
|
|
78
|
+
uidOwner?: string;
|
|
79
|
+
/** Pin the names in this view of the hosts, built from `from`, rather
|
|
80
|
+
* than in /etc/hosts itself (a machine, whose /etc/hosts the root
|
|
81
|
+
* sidecar shares). The pins count only while `proven` says every task
|
|
82
|
+
* process sees the view. */
|
|
83
|
+
hostsView?: { readonly path: string; readonly from: string; readonly proven: () => boolean };
|
|
84
|
+
},
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const enabled = options.domains.length > 0;
|
|
87
|
+
// Both generated first: a domain that is not canonical and proxiable is
|
|
88
|
+
// refused before anything runs.
|
|
89
|
+
const hosts =
|
|
90
|
+
options.hostsView === undefined
|
|
91
|
+
? hostsReconcileScript(options.domains)
|
|
92
|
+
: hostsReconcileScript(options.domains, options.hostsView.path, { sourcePath: options.hostsView.from });
|
|
93
|
+
const chain = redirectChainScript({
|
|
94
|
+
toPort: options.listenPort,
|
|
95
|
+
enabled,
|
|
96
|
+
netAdminFromCreation: options.netAdmin === "from-creation",
|
|
97
|
+
...(options.uidOwner === undefined ? {} : { uidOwner: options.uidOwner }),
|
|
98
|
+
});
|
|
99
|
+
const hostsStep = async (): Promise<void> => {
|
|
100
|
+
const pinned = await environment.exec({
|
|
101
|
+
argv: ["/bin/sh", "-c", hosts],
|
|
102
|
+
user: "root",
|
|
103
|
+
env: ROOT_STEP_ENV,
|
|
104
|
+
projectEnvProfile: "maintenance",
|
|
105
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
106
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
107
|
+
});
|
|
108
|
+
checked("pinning the proxied names", pinned);
|
|
109
|
+
};
|
|
110
|
+
const chainStep = async (): Promise<void> => {
|
|
111
|
+
const redirected = await environment.exec({
|
|
112
|
+
argv: ["/bin/sh", "-c", chain],
|
|
113
|
+
user: "root",
|
|
114
|
+
env: ROOT_STEP_ENV,
|
|
115
|
+
...(options.netAdmin === "per-exec" ? { privileged: true as const } : {}),
|
|
116
|
+
projectEnvProfile: "maintenance",
|
|
117
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
118
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
119
|
+
});
|
|
120
|
+
checked("redirecting the proxied names", redirected);
|
|
121
|
+
};
|
|
122
|
+
if (enabled) {
|
|
123
|
+
await chainStep();
|
|
124
|
+
await hostsStep();
|
|
125
|
+
// With a view, pins count only while every task process sees it: one
|
|
126
|
+
// a runner without the view started resolves the real names and goes
|
|
127
|
+
// around the proxy. Until then the step fails and the keys stay withheld.
|
|
128
|
+
if (options.hostsView !== undefined && !options.hostsView.proven()) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"secrets proxy: processes this task started before its hosts view may still run; " +
|
|
131
|
+
"its proxied keys stay withheld until its machine reboots",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
await hostsStep();
|
|
136
|
+
await chainStep();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal DER encoder (ADR-153 §3 "The CA") — exactly the X.690 subset an
|
|
3
|
+
* X.509 v3 certificate needs, so the runner mints its proxy certificates with
|
|
4
|
+
* Node built-ins and no new dependency.
|
|
5
|
+
*
|
|
6
|
+
* Every function returns one complete TLV. Nothing here ever sees key
|
|
7
|
+
* material: ca.ts hands the finished TBSCertificate to a signing callback and
|
|
8
|
+
* only embeds the signature it gets back. Inputs that DER cannot represent
|
|
9
|
+
* exactly throw instead of being silently adjusted, so a bad certificate can
|
|
10
|
+
* only fail to mint — never be minted subtly wrong.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CONTEXT_SPECIFIC = 0x80;
|
|
14
|
+
const CONSTRUCTED = 0x20;
|
|
15
|
+
/** Low-tag-number form only (X.690 §8.1.2.2); X.509 never needs more. */
|
|
16
|
+
const MAX_TAG_NUMBER = 30;
|
|
17
|
+
|
|
18
|
+
const TAG = {
|
|
19
|
+
boolean: 0x01,
|
|
20
|
+
integer: 0x02,
|
|
21
|
+
bitString: 0x03,
|
|
22
|
+
octetString: 0x04,
|
|
23
|
+
null: 0x05,
|
|
24
|
+
objectIdentifier: 0x06,
|
|
25
|
+
utf8String: 0x0c,
|
|
26
|
+
printableString: 0x13,
|
|
27
|
+
ia5String: 0x16,
|
|
28
|
+
utcTime: 0x17,
|
|
29
|
+
generalizedTime: 0x18,
|
|
30
|
+
sequence: 0x30,
|
|
31
|
+
set: 0x31,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/** Definite length: short form below 128, long form (0x80 | n, then n
|
|
35
|
+
* big-endian octets, no leading zero) above (X.690 §8.1.3, §10.1). */
|
|
36
|
+
export function encodeLength(length: number): Buffer {
|
|
37
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
38
|
+
throw new RangeError("DER length must be a non-negative safe integer");
|
|
39
|
+
}
|
|
40
|
+
if (length < 0x80) return Buffer.from([length]);
|
|
41
|
+
const octets: number[] = [];
|
|
42
|
+
for (let rest = length; rest > 0; rest = Math.floor(rest / 256)) {
|
|
43
|
+
octets.unshift(rest % 256);
|
|
44
|
+
}
|
|
45
|
+
return Buffer.from([0x80 | octets.length, ...octets]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tlv(tag: number, content: Uint8Array): Buffer {
|
|
49
|
+
return Buffer.concat([Buffer.from([tag]), encodeLength(content.length), content]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sequence(...items: readonly Buffer[]): Buffer {
|
|
53
|
+
return tlv(TAG.sequence, Buffer.concat(items));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** X.690 §11.6: a DER SET OF orders its elements by their encodings, compared
|
|
57
|
+
* as octet strings with the shorter one padded with trailing zero octets. */
|
|
58
|
+
function compareEncodings(left: Buffer, right: Buffer): number {
|
|
59
|
+
const length = Math.max(left.length, right.length);
|
|
60
|
+
for (let index = 0; index < length; index += 1) {
|
|
61
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
62
|
+
if (difference !== 0) return difference;
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function set(...items: readonly Buffer[]): Buffer {
|
|
68
|
+
return tlv(TAG.set, Buffer.concat([...items].sort(compareEncodings)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A non-negative INTEGER. A Buffer is read as an unsigned big-endian
|
|
73
|
+
* magnitude (a certificate serial); a number must be a safe integer. DER
|
|
74
|
+
* wants the minimal two's-complement form: redundant leading zero octets are
|
|
75
|
+
* dropped, and one is added back when the top bit would read as a sign.
|
|
76
|
+
*/
|
|
77
|
+
export function integer(value: Buffer | number): Buffer {
|
|
78
|
+
let magnitude: Buffer;
|
|
79
|
+
if (typeof value === "number") {
|
|
80
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
81
|
+
throw new RangeError("DER INTEGER must be a non-negative safe integer");
|
|
82
|
+
}
|
|
83
|
+
const octets: number[] = [];
|
|
84
|
+
for (let rest = value; rest > 0; rest = Math.floor(rest / 256)) {
|
|
85
|
+
octets.unshift(rest % 256);
|
|
86
|
+
}
|
|
87
|
+
magnitude = Buffer.from(octets);
|
|
88
|
+
} else {
|
|
89
|
+
magnitude = value;
|
|
90
|
+
}
|
|
91
|
+
let start = 0;
|
|
92
|
+
while (start < magnitude.length - 1 && magnitude[start] === 0) start += 1;
|
|
93
|
+
let content = magnitude.length === 0 ? Buffer.from([0]) : magnitude.subarray(start);
|
|
94
|
+
if (((content[0] ?? 0) & 0x80) !== 0) {
|
|
95
|
+
content = Buffer.concat([Buffer.from([0]), content]);
|
|
96
|
+
}
|
|
97
|
+
return tlv(TAG.integer, content);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function base128(arc: bigint): number[] {
|
|
101
|
+
const octets = [Number(arc & 0x7fn)];
|
|
102
|
+
for (let rest = arc >> 7n; rest > 0n; rest >>= 7n) {
|
|
103
|
+
octets.unshift(Number(rest & 0x7fn) | 0x80);
|
|
104
|
+
}
|
|
105
|
+
return octets;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Dotted-decimal OID. Arcs are BigInt so a 2.25.<uuid> arc stays exact. */
|
|
109
|
+
export function objectIdentifier(oid: string): Buffer {
|
|
110
|
+
if (!/^[0-2](?:\.(?:0|[1-9][0-9]*))+$/.test(oid)) {
|
|
111
|
+
throw new Error(`invalid object identifier: ${oid}`);
|
|
112
|
+
}
|
|
113
|
+
const [first, second, ...rest] = oid.split(".").map((arc) => BigInt(arc));
|
|
114
|
+
if (first === undefined || second === undefined) {
|
|
115
|
+
throw new Error(`invalid object identifier: ${oid}`);
|
|
116
|
+
}
|
|
117
|
+
if (first < 2n && second >= 40n) {
|
|
118
|
+
throw new Error(`invalid object identifier: ${oid}`);
|
|
119
|
+
}
|
|
120
|
+
const octets = [first * 40n + second, ...rest].flatMap(base128);
|
|
121
|
+
return tlv(TAG.objectIdentifier, Buffer.from(octets));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function nullValue(): Buffer {
|
|
125
|
+
return Buffer.from([TAG.null, 0x00]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** DER encodes TRUE as 0xFF only (X.690 §11.1). */
|
|
129
|
+
export function boolean(value: boolean): Buffer {
|
|
130
|
+
return Buffer.from([TAG.boolean, 0x01, value ? 0xff : 0x00]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A BIT STRING whose final `unusedBits` bits are padding, which DER requires
|
|
134
|
+
* to be zero (X.690 §11.2.1). */
|
|
135
|
+
export function bitString(bytes: Buffer, unusedBits = 0): Buffer {
|
|
136
|
+
if (!Number.isInteger(unusedBits) || unusedBits < 0 || unusedBits > 7) {
|
|
137
|
+
throw new RangeError("BIT STRING unused bits must be 0..7");
|
|
138
|
+
}
|
|
139
|
+
if (bytes.length === 0 && unusedBits !== 0) {
|
|
140
|
+
throw new RangeError("an empty BIT STRING has no unused bits");
|
|
141
|
+
}
|
|
142
|
+
const last = bytes[bytes.length - 1] ?? 0;
|
|
143
|
+
if ((last & ((1 << unusedBits) - 1)) !== 0) {
|
|
144
|
+
throw new RangeError("DER requires the unused bits of a BIT STRING to be zero");
|
|
145
|
+
}
|
|
146
|
+
return tlv(TAG.bitString, Buffer.concat([Buffer.from([unusedBits]), bytes]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A NamedBitList BIT STRING such as KeyUsage: bit 0 is the most significant
|
|
151
|
+
* bit of the first octet, and DER drops trailing zero bits (X.690 §11.2.2),
|
|
152
|
+
* so keyCertSign|cRLSign encodes as `03 02 01 06`, not `03 02 00 06`.
|
|
153
|
+
*/
|
|
154
|
+
export function namedBitString(bits: readonly number[]): Buffer {
|
|
155
|
+
if (bits.length === 0) return bitString(Buffer.alloc(0));
|
|
156
|
+
for (const bit of bits) {
|
|
157
|
+
if (!Number.isInteger(bit) || bit < 0 || bit > 255) {
|
|
158
|
+
throw new RangeError("named bits must be integers 0..255");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const highest = Math.max(...bits);
|
|
162
|
+
const bytes = Buffer.alloc((highest >> 3) + 1);
|
|
163
|
+
for (const bit of bits) {
|
|
164
|
+
bytes[bit >> 3] = (bytes[bit >> 3] ?? 0) | (0x80 >> (bit & 7));
|
|
165
|
+
}
|
|
166
|
+
return bitString(bytes, 7 - (highest & 7));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function octetString(bytes: Buffer): Buffer {
|
|
170
|
+
return tlv(TAG.octetString, bytes);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function utf8String(value: string): Buffer {
|
|
174
|
+
const bytes = Buffer.from(value, "utf8");
|
|
175
|
+
// Buffer.from silently turns a lone surrogate into U+FFFD; refuse instead.
|
|
176
|
+
if (bytes.toString("utf8") !== value) {
|
|
177
|
+
throw new Error("UTF8String must be well-formed Unicode");
|
|
178
|
+
}
|
|
179
|
+
return tlv(TAG.utf8String, bytes);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function printableString(value: string): Buffer {
|
|
183
|
+
if (!/^[A-Za-z0-9 '()+,\-./:=?]*$/.test(value)) {
|
|
184
|
+
throw new Error("value is outside the PrintableString alphabet");
|
|
185
|
+
}
|
|
186
|
+
return tlv(TAG.printableString, Buffer.from(value, "ascii"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function ia5String(value: string): Buffer {
|
|
190
|
+
if (!/^[\x00-\x7f]*$/.test(value)) {
|
|
191
|
+
throw new Error("value is outside the IA5String alphabet");
|
|
192
|
+
}
|
|
193
|
+
return tlv(TAG.ia5String, Buffer.from(value, "ascii"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function wholeSeconds(date: Date): void {
|
|
197
|
+
if (!Number.isFinite(date.getTime())) throw new RangeError("invalid date");
|
|
198
|
+
// X.509 times carry no fraction (RFC 5280 §4.1.2.5); refuse rather than
|
|
199
|
+
// truncate, so the caller's notBefore/notAfter is exactly what is signed.
|
|
200
|
+
if (date.getUTCMilliseconds() !== 0) {
|
|
201
|
+
throw new RangeError("certificate times must be whole seconds");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pad(value: number, width: number): string {
|
|
206
|
+
return String(value).padStart(width, "0");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function monthToSecond(date: Date): string {
|
|
210
|
+
return (
|
|
211
|
+
pad(date.getUTCMonth() + 1, 2) +
|
|
212
|
+
pad(date.getUTCDate(), 2) +
|
|
213
|
+
pad(date.getUTCHours(), 2) +
|
|
214
|
+
pad(date.getUTCMinutes(), 2) +
|
|
215
|
+
pad(date.getUTCSeconds(), 2)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** YYMMDDHHMMSSZ — RFC 5280 reads YY ≥ 50 as 19YY, so only 1950–2049 fit. */
|
|
220
|
+
export function utcTime(date: Date): Buffer {
|
|
221
|
+
wholeSeconds(date);
|
|
222
|
+
const year = date.getUTCFullYear();
|
|
223
|
+
if (year < 1950 || year > 2049) {
|
|
224
|
+
throw new RangeError("UTCTime covers 1950 through 2049 only");
|
|
225
|
+
}
|
|
226
|
+
return tlv(TAG.utcTime, Buffer.from(`${pad(year % 100, 2)}${monthToSecond(date)}Z`, "ascii"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** YYYYMMDDHHMMSSZ, no fractional seconds (RFC 5280 §4.1.2.5.2). */
|
|
230
|
+
export function generalizedTime(date: Date): Buffer {
|
|
231
|
+
wholeSeconds(date);
|
|
232
|
+
const year = date.getUTCFullYear();
|
|
233
|
+
if (year < 0 || year > 9999) {
|
|
234
|
+
throw new RangeError("GeneralizedTime covers years 0000 through 9999");
|
|
235
|
+
}
|
|
236
|
+
return tlv(TAG.generalizedTime, Buffer.from(`${pad(year, 4)}${monthToSecond(date)}Z`, "ascii"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** RFC 5280 §4.1.2.5: UTCTime through 2049, GeneralizedTime from 2050 on. */
|
|
240
|
+
export function time(date: Date): Buffer {
|
|
241
|
+
const year = date.getUTCFullYear();
|
|
242
|
+
return year >= 1950 && year <= 2049 ? utcTime(date) : generalizedTime(date);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function checkTagNumber(tagNumber: number): number {
|
|
246
|
+
if (!Number.isInteger(tagNumber) || tagNumber < 0 || tagNumber > MAX_TAG_NUMBER) {
|
|
247
|
+
throw new RangeError(`context tag number must be 0..${MAX_TAG_NUMBER}`);
|
|
248
|
+
}
|
|
249
|
+
return tagNumber;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** `[n] EXPLICIT`: a constructed context-specific TLV wrapping the whole inner
|
|
253
|
+
* TLV (e.g. the certificate version `A0 03 02 01 02`). */
|
|
254
|
+
export function explicit(tagNumber: number, inner: Buffer): Buffer {
|
|
255
|
+
return tlv(CONTEXT_SPECIFIC | CONSTRUCTED | checkTagNumber(tagNumber), inner);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* `[n] IMPLICIT`: the context-specific tag REPLACES the inner type's own tag,
|
|
260
|
+
* keeping its length and content octets and its constructed bit — e.g. a
|
|
261
|
+
* SAN dNSName is `[2] IMPLICIT IA5String`, `82 len name`. Takes the encoded
|
|
262
|
+
* inner TLV so callers reuse the typed encoders above.
|
|
263
|
+
*/
|
|
264
|
+
export function implicit(tagNumber: number, inner: Buffer): Buffer {
|
|
265
|
+
const identifier = inner[0];
|
|
266
|
+
if (identifier === undefined || inner.length < 2 || (identifier & 0x1f) === 0x1f) {
|
|
267
|
+
throw new Error("implicit tagging needs a complete low-tag-number TLV");
|
|
268
|
+
}
|
|
269
|
+
return Buffer.concat([
|
|
270
|
+
Buffer.from([CONTEXT_SPECIFIC | (identifier & CONSTRUCTED) | checkTagNumber(tagNumber)]),
|
|
271
|
+
inner.subarray(1),
|
|
272
|
+
]);
|
|
273
|
+
}
|