@appforge-ci/core 0.2.3 → 0.3.0
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/dist/agent-readiness.d.ts +232 -1
- package/dist/agent-readiness.d.ts.map +1 -1
- package/dist/agent-readiness.js +294 -1
- package/dist/agent-readiness.js.map +1 -1
- package/dist/agent-readiness.test.js +253 -19
- package/dist/agent-readiness.test.js.map +1 -1
- package/dist/api-client.d.ts +42 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +27 -0
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.js +22 -0
- package/dist/api-client.test.js.map +1 -1
- package/dist/attestation.d.ts +265 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +327 -0
- package/dist/attestation.js.map +1 -0
- package/dist/attestation.test.d.ts +2 -0
- package/dist/attestation.test.d.ts.map +1 -0
- package/dist/attestation.test.js +218 -0
- package/dist/attestation.test.js.map +1 -0
- package/dist/build-vpn.d.ts +10 -0
- package/dist/build-vpn.d.ts.map +1 -0
- package/dist/build-vpn.js +23 -0
- package/dist/build-vpn.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +342 -7
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +92 -2
- package/dist/schemas.js.map +1 -1
- package/dist/types.d.ts +149 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vpn-egress-presets.d.ts +142 -0
- package/dist/vpn-egress-presets.d.ts.map +1 -0
- package/dist/vpn-egress-presets.js +318 -0
- package/dist/vpn-egress-presets.js.map +1 -0
- package/dist/vpn-egress-presets.test.d.ts +2 -0
- package/dist/vpn-egress-presets.test.d.ts.map +1 -0
- package/dist/vpn-egress-presets.test.js +210 -0
- package/dist/vpn-egress-presets.test.js.map +1 -0
- package/dist/vpn-egress.d.ts +57 -0
- package/dist/vpn-egress.d.ts.map +1 -0
- package/dist/vpn-egress.js +126 -0
- package/dist/vpn-egress.js.map +1 -0
- package/dist/vpn-egress.test.d.ts +2 -0
- package/dist/vpn-egress.test.d.ts.map +1 -0
- package/dist/vpn-egress.test.js +133 -0
- package/dist/vpn-egress.test.js.map +1 -0
- package/dist/vpn-ssh.d.ts +45 -0
- package/dist/vpn-ssh.d.ts.map +1 -0
- package/dist/vpn-ssh.js +108 -0
- package/dist/vpn-ssh.js.map +1 -0
- package/dist/vpn-ssh.test.d.ts +2 -0
- package/dist/vpn-ssh.test.d.ts.map +1 -0
- package/dist/vpn-ssh.test.js +75 -0
- package/dist/vpn-ssh.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict egress policy for the site-to-site VPN helper (`VpnJobConfig.egress`,
|
|
3
|
+
* see packages/vpn-proxy/PROTOCOL.md, "Strict egress"). Pure: validation, the
|
|
4
|
+
* canonical form and its SHA-256, so the control plane, the agent and the
|
|
5
|
+
* helper agree on exactly which policy a build ran under.
|
|
6
|
+
*
|
|
7
|
+
* This file is the TypeScript twin of `packages/vpn-proxy/internal/policy`.
|
|
8
|
+
* Both must accept and reject the same inputs and produce the same canonical
|
|
9
|
+
* bytes; `packages/vpn-proxy/testdata/egress-policy-vectors.json` is the shared
|
|
10
|
+
* proof and both test suites run it.
|
|
11
|
+
*
|
|
12
|
+
* Hosts are ASCII/punycode only (the control plane converts Unicode names
|
|
13
|
+
* before storing them): that keeps the two implementations free of IDNA and
|
|
14
|
+
* Unicode case-mapping differences.
|
|
15
|
+
*/
|
|
16
|
+
import type { VpnEgressPolicy } from "./types.js";
|
|
17
|
+
/** Mirrors `policy.MaxRules` / `policy.MaxPortsPerRule` / `policy.DefaultPort` / `policy.Version`. */
|
|
18
|
+
export declare const VPN_EGRESS_MAX_RULES = 512;
|
|
19
|
+
export declare const VPN_EGRESS_MAX_PORTS_PER_RULE = 16;
|
|
20
|
+
export declare const VPN_EGRESS_DEFAULT_PORT = 443;
|
|
21
|
+
export declare const VPN_EGRESS_POLICY_VERSION = 1;
|
|
22
|
+
/**
|
|
23
|
+
* Mirrors `config.MaxJobBytes`: the most stdin the helper reads for one job.
|
|
24
|
+
* It is sized so the largest valid policy (512 rules, 253-byte hosts, 16 ports
|
|
25
|
+
* each: about 190 KB as compact JSON) plus the rest of the job fits; the agent
|
|
26
|
+
* still checks the serialised job against it before spawning, because extra
|
|
27
|
+
* keys inside rules are ignored by validation but would count against the cap.
|
|
28
|
+
*/
|
|
29
|
+
export declare const VPN_JOB_MAX_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* Normalises one allow-list host: lowercase; either `name.tld` or `*.name.tld`
|
|
32
|
+
* with at least two labels after any `*.`; no IP literal in any spelling (a
|
|
33
|
+
* numeric last label is refused, so `10.0.0.1`, `2130706433`, `0x7f.1` all
|
|
34
|
+
* fail), no trailing dot, no port/scheme/path, no Unicode. Returns `null` when
|
|
35
|
+
* the host is not acceptable.
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeEgressHost(raw: unknown): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Every problem with an egress policy (an empty array means valid). Problem
|
|
40
|
+
* strings name the field, like the helper's own validation
|
|
41
|
+
* (`egress.allow[3].host`), and never echo a rejected value.
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateVpnEgressPolicy(policy: unknown): string[];
|
|
44
|
+
/**
|
|
45
|
+
* The exact bytes the helper hashes:
|
|
46
|
+
*
|
|
47
|
+
* {"v":1,"allow":[{"host":"*.googleapis.com","ports":[443]},{"host":"pub.dev","ports":[443]}]}
|
|
48
|
+
*
|
|
49
|
+
* Hosts lowercased; one entry per host (duplicates merge, ports unioned);
|
|
50
|
+
* sorted by host in byte order (so `*.` sorts before letters); ports ascending
|
|
51
|
+
* and deduplicated with the default 443 made explicit; no whitespace.
|
|
52
|
+
* Throws on an invalid policy (callers validate first and report the problems).
|
|
53
|
+
*/
|
|
54
|
+
export declare function canonicalVpnEgressPolicy(policy: VpnEgressPolicy): string;
|
|
55
|
+
/** Lowercase hex SHA-256 of `canonicalVpnEgressPolicy`: what the helper echoes as `policyHash`. */
|
|
56
|
+
export declare function vpnEgressPolicyHash(policy: VpnEgressPolicy): Promise<string>;
|
|
57
|
+
//# sourceMappingURL=vpn-egress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-egress.d.ts","sourceRoot":"","sources":["../src/vpn-egress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,sGAAsG;AACtG,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAChD,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,QAAa,CAAC;AAK5C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAe/D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAiCjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAmBxE;AAED,mGAAmG;AACnG,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAIlF"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** Mirrors `policy.MaxRules` / `policy.MaxPortsPerRule` / `policy.DefaultPort` / `policy.Version`. */
|
|
2
|
+
export const VPN_EGRESS_MAX_RULES = 512;
|
|
3
|
+
export const VPN_EGRESS_MAX_PORTS_PER_RULE = 16;
|
|
4
|
+
export const VPN_EGRESS_DEFAULT_PORT = 443;
|
|
5
|
+
export const VPN_EGRESS_POLICY_VERSION = 1;
|
|
6
|
+
/**
|
|
7
|
+
* Mirrors `config.MaxJobBytes`: the most stdin the helper reads for one job.
|
|
8
|
+
* It is sized so the largest valid policy (512 rules, 253-byte hosts, 16 ports
|
|
9
|
+
* each: about 190 KB as compact JSON) plus the rest of the job fits; the agent
|
|
10
|
+
* still checks the serialised job against it before spawning, because extra
|
|
11
|
+
* keys inside rules are ignored by validation but would count against the cap.
|
|
12
|
+
*/
|
|
13
|
+
export const VPN_JOB_MAX_BYTES = 256 * 1024;
|
|
14
|
+
/** Mirrors Go `route.Normalize`: LDH plus `_`, labels 1..63, total <= 253. */
|
|
15
|
+
const LABEL = /^[a-z0-9_-]{1,63}$/;
|
|
16
|
+
/**
|
|
17
|
+
* Normalises one allow-list host: lowercase; either `name.tld` or `*.name.tld`
|
|
18
|
+
* with at least two labels after any `*.`; no IP literal in any spelling (a
|
|
19
|
+
* numeric last label is refused, so `10.0.0.1`, `2130706433`, `0x7f.1` all
|
|
20
|
+
* fail), no trailing dot, no port/scheme/path, no Unicode. Returns `null` when
|
|
21
|
+
* the host is not acceptable.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeEgressHost(raw) {
|
|
24
|
+
if (typeof raw !== "string")
|
|
25
|
+
return null;
|
|
26
|
+
// ASCII only, BEFORE lowercasing: toLowerCase() folds the Kelvin sign (U+212A)
|
|
27
|
+
// to "k". Mirrors the Go implementation.
|
|
28
|
+
if (!/^[\x00-\x7f]*$/.test(raw))
|
|
29
|
+
return null;
|
|
30
|
+
const host = raw.toLowerCase();
|
|
31
|
+
const wild = host.startsWith("*.");
|
|
32
|
+
const name = wild ? host.slice(2) : host;
|
|
33
|
+
if (name === "" || name.length > 253 || name.endsWith(".") || name.includes("*"))
|
|
34
|
+
return null;
|
|
35
|
+
const labels = name.split(".");
|
|
36
|
+
if (labels.length < 2)
|
|
37
|
+
return null;
|
|
38
|
+
for (const l of labels)
|
|
39
|
+
if (!LABEL.test(l))
|
|
40
|
+
return null;
|
|
41
|
+
const last = labels[labels.length - 1];
|
|
42
|
+
if (last.startsWith("0x") || /^[0-9]+$/.test(last))
|
|
43
|
+
return null; // an IP in some spelling
|
|
44
|
+
return wild ? `*.${name}` : name;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every problem with an egress policy (an empty array means valid). Problem
|
|
48
|
+
* strings name the field, like the helper's own validation
|
|
49
|
+
* (`egress.allow[3].host`), and never echo a rejected value.
|
|
50
|
+
*/
|
|
51
|
+
export function validateVpnEgressPolicy(policy) {
|
|
52
|
+
const problems = [];
|
|
53
|
+
if (policy === null || typeof policy !== "object" || Array.isArray(policy))
|
|
54
|
+
return ["egress: must be an object"];
|
|
55
|
+
const allow = policy.allow;
|
|
56
|
+
if (!Array.isArray(allow))
|
|
57
|
+
return ["egress.allow: must be an array"];
|
|
58
|
+
if (allow.length > VPN_EGRESS_MAX_RULES)
|
|
59
|
+
return [`egress.allow: may contain at most ${VPN_EGRESS_MAX_RULES} entries`];
|
|
60
|
+
allow.forEach((rule, i) => {
|
|
61
|
+
const at = `egress.allow[${i}]`;
|
|
62
|
+
if (rule === null || typeof rule !== "object" || Array.isArray(rule)) {
|
|
63
|
+
problems.push(`${at}: must be an object`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { host, ports } = rule;
|
|
67
|
+
if (normalizeEgressHost(host) === null) {
|
|
68
|
+
problems.push(`${at}.host: must be a hostname like example.com or *.example.com (no IP literals, no bare *, no trailing dot)`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (ports === undefined || ports === null)
|
|
72
|
+
return;
|
|
73
|
+
if (!Array.isArray(ports)) {
|
|
74
|
+
problems.push(`${at}.ports: must be an array`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (ports.length > VPN_EGRESS_MAX_PORTS_PER_RULE) {
|
|
78
|
+
problems.push(`${at}.ports: may contain at most ${VPN_EGRESS_MAX_PORTS_PER_RULE} ports`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!ports.every((p) => typeof p === "number" && Number.isInteger(p) && p >= 1 && p <= 65535)) {
|
|
82
|
+
problems.push(`${at}.ports: each port must be 1-65535`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return problems;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The exact bytes the helper hashes:
|
|
89
|
+
*
|
|
90
|
+
* {"v":1,"allow":[{"host":"*.googleapis.com","ports":[443]},{"host":"pub.dev","ports":[443]}]}
|
|
91
|
+
*
|
|
92
|
+
* Hosts lowercased; one entry per host (duplicates merge, ports unioned);
|
|
93
|
+
* sorted by host in byte order (so `*.` sorts before letters); ports ascending
|
|
94
|
+
* and deduplicated with the default 443 made explicit; no whitespace.
|
|
95
|
+
* Throws on an invalid policy (callers validate first and report the problems).
|
|
96
|
+
*/
|
|
97
|
+
export function canonicalVpnEgressPolicy(policy) {
|
|
98
|
+
const problems = validateVpnEgressPolicy(policy);
|
|
99
|
+
if (problems.length > 0)
|
|
100
|
+
throw new Error(`invalid egress policy: ${problems.join("; ")}`);
|
|
101
|
+
const merged = new Map();
|
|
102
|
+
for (const rule of policy.allow) {
|
|
103
|
+
const host = normalizeEgressHost(rule.host);
|
|
104
|
+
let set = merged.get(host);
|
|
105
|
+
if (!set)
|
|
106
|
+
merged.set(host, (set = new Set()));
|
|
107
|
+
const ports = rule.ports && rule.ports.length > 0 ? rule.ports : [VPN_EGRESS_DEFAULT_PORT];
|
|
108
|
+
for (const p of ports)
|
|
109
|
+
set.add(p);
|
|
110
|
+
}
|
|
111
|
+
// Default Array.prototype.sort compares UTF-16 code units, which for ASCII is
|
|
112
|
+
// the same order as Go's byte-wise slices.Sort.
|
|
113
|
+
const hosts = [...merged.keys()].sort();
|
|
114
|
+
const entries = hosts.map((h) => {
|
|
115
|
+
const ports = [...merged.get(h)].sort((a, b) => a - b);
|
|
116
|
+
return `{"host":"${h}","ports":[${ports.join(",")}]}`;
|
|
117
|
+
});
|
|
118
|
+
return `{"v":${VPN_EGRESS_POLICY_VERSION},"allow":[${entries.join(",")}]}`;
|
|
119
|
+
}
|
|
120
|
+
/** Lowercase hex SHA-256 of `canonicalVpnEgressPolicy`: what the helper echoes as `policyHash`. */
|
|
121
|
+
export async function vpnEgressPolicyHash(policy) {
|
|
122
|
+
const bytes = new TextEncoder().encode(canonicalVpnEgressPolicy(policy));
|
|
123
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
124
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=vpn-egress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-egress.js","sourceRoot":"","sources":["../src/vpn-egress.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-egress.test.d.ts","sourceRoot":"","sources":["../src/vpn-egress.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { canonicalVpnEgressPolicy, normalizeEgressHost, validateVpnEgressPolicy, vpnEgressPolicyHash, VPN_EGRESS_MAX_PORTS_PER_RULE, VPN_EGRESS_MAX_RULES, VPN_JOB_MAX_BYTES, } from "./vpn-egress.js";
|
|
7
|
+
import { vpnEgressPolicySchema } from "./schemas.js";
|
|
8
|
+
// The Go implementation (packages/vpn-proxy/internal/policy) runs the same file.
|
|
9
|
+
const vectors = JSON.parse(readFileSync(fileURLToPath(new URL("../../vpn-proxy/testdata/egress-policy-vectors.json", import.meta.url)), "utf8"));
|
|
10
|
+
test("shared vectors: same validity, same canonical bytes, same hash as the Go helper", async () => {
|
|
11
|
+
assert.ok(vectors.length >= 20);
|
|
12
|
+
assert.ok(vectors.some((v) => v.valid) && vectors.some((v) => !v.valid));
|
|
13
|
+
for (const v of vectors) {
|
|
14
|
+
const policy = { allow: v.allow };
|
|
15
|
+
const problems = validateVpnEgressPolicy(policy);
|
|
16
|
+
if (!v.valid) {
|
|
17
|
+
assert.ok(problems.length > 0, `${v.name}: should be rejected`);
|
|
18
|
+
assert.throws(() => canonicalVpnEgressPolicy(policy), /invalid egress policy/, v.name);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
assert.deepEqual(problems, [], `${v.name}: should be valid`);
|
|
22
|
+
assert.equal(canonicalVpnEgressPolicy(policy), v.canonical, `${v.name}: canonical`);
|
|
23
|
+
assert.equal(createHash("sha256").update(v.canonical).digest("hex"), v.sha256, `${v.name}: vector hash`);
|
|
24
|
+
assert.equal(await vpnEgressPolicyHash(policy), v.sha256, `${v.name}: hash`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
test("canonical form is insensitive to order, case, duplicates and explicit default ports", () => {
|
|
28
|
+
const a = canonicalVpnEgressPolicy({
|
|
29
|
+
allow: [{ host: "B.example.com", ports: [8443, 443] }, { host: "a.example.com" }, { host: "*.c.example.com", ports: [443] }],
|
|
30
|
+
});
|
|
31
|
+
const b = canonicalVpnEgressPolicy({
|
|
32
|
+
allow: [
|
|
33
|
+
{ host: "*.C.example.com" },
|
|
34
|
+
{ host: "a.example.com", ports: [443] },
|
|
35
|
+
{ host: "b.example.com", ports: [443] },
|
|
36
|
+
{ host: "b.example.com", ports: [8443] },
|
|
37
|
+
{ host: "A.EXAMPLE.COM" },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
assert.equal(a, b);
|
|
41
|
+
assert.equal(a, '{"v":1,"allow":[{"host":"*.c.example.com","ports":[443]},{"host":"a.example.com","ports":[443]},{"host":"b.example.com","ports":[443,8443]}]}');
|
|
42
|
+
assert.equal(canonicalVpnEgressPolicy({ allow: [] }), '{"v":1,"allow":[]}');
|
|
43
|
+
assert.equal(canonicalVpnEgressPolicy({ allow: [{ host: "x.example.com", ports: [] }] }), '{"v":1,"allow":[{"host":"x.example.com","ports":[443]}]}');
|
|
44
|
+
});
|
|
45
|
+
test("the hash is the SHA-256 of the canonical bytes and changes with the policy", async () => {
|
|
46
|
+
const p = { allow: [{ host: "pub.dev" }] };
|
|
47
|
+
const h = await vpnEgressPolicyHash(p);
|
|
48
|
+
assert.match(h, /^[0-9a-f]{64}$/);
|
|
49
|
+
assert.equal(h, createHash("sha256").update(canonicalVpnEgressPolicy(p)).digest("hex"));
|
|
50
|
+
assert.notEqual(h, await vpnEgressPolicyHash({ allow: [{ host: "pub.dev", ports: [443, 80] }] }));
|
|
51
|
+
assert.notEqual(h, await vpnEgressPolicyHash({ allow: [] }));
|
|
52
|
+
await assert.rejects(() => vpnEgressPolicyHash({ allow: [{ host: "*" }] }), /invalid egress policy/);
|
|
53
|
+
});
|
|
54
|
+
test("normalizeEgressHost accepts hostnames and wildcards, refuses everything IP-shaped or overbroad", () => {
|
|
55
|
+
for (const [raw, want] of [
|
|
56
|
+
["pub.dev", "pub.dev"],
|
|
57
|
+
["Repo.Maven.Apache.ORG", "repo.maven.apache.org"],
|
|
58
|
+
["*.GoogleAPIs.com", "*.googleapis.com"],
|
|
59
|
+
["xn--bcher-kva.example", "xn--bcher-kva.example"],
|
|
60
|
+
["_dmarc.example.com", "_dmarc.example.com"],
|
|
61
|
+
]) {
|
|
62
|
+
assert.equal(normalizeEgressHost(raw), want, raw);
|
|
63
|
+
}
|
|
64
|
+
for (const bad of [
|
|
65
|
+
"", "*", "*.", "*.com", "intranet", "a.*.example.com", "*.*.example.com", "a*.example.com", "example.*",
|
|
66
|
+
"10.0.0.1", "*.10.0.0.1", "2130706433", "0x7f.0.0.1", "127.1", "::1", "[::1]", "example.com.", ".example.com",
|
|
67
|
+
"a..example.com", "https://example.com", "example.com/x", "example.com:443", "a@example.com", "a b.example.com",
|
|
68
|
+
"bücher.example", "EXAMPLE.com", "Kubernetes.io", "example.123", `${"a".repeat(64)}.example.com`,
|
|
69
|
+
`${"a.".repeat(130)}com`,
|
|
70
|
+
]) {
|
|
71
|
+
assert.equal(normalizeEgressHost(bad), null, JSON.stringify(bad));
|
|
72
|
+
}
|
|
73
|
+
for (const nonString of [undefined, null, 5, {}, []])
|
|
74
|
+
assert.equal(normalizeEgressHost(nonString), null);
|
|
75
|
+
});
|
|
76
|
+
test("validateVpnEgressPolicy reports every problem, names the field, never echoes the value", () => {
|
|
77
|
+
const secret = "SUPERSECRET";
|
|
78
|
+
const problems = validateVpnEgressPolicy({
|
|
79
|
+
allow: [
|
|
80
|
+
{ host: "ok.example.com" },
|
|
81
|
+
{ host: `${secret}/x` },
|
|
82
|
+
{ host: "10.0.0.1" },
|
|
83
|
+
{ host: "ok.example.com", ports: [0] },
|
|
84
|
+
{ host: "ok.example.com", ports: [65536] },
|
|
85
|
+
{ host: "ok.example.com", ports: [1.5] },
|
|
86
|
+
{ host: "ok.example.com", ports: ["443"] },
|
|
87
|
+
{ host: "ok.example.com", ports: "443" },
|
|
88
|
+
{ host: "ok.example.com", ports: Array.from({ length: VPN_EGRESS_MAX_PORTS_PER_RULE + 1 }, (_, i) => i + 1) },
|
|
89
|
+
null,
|
|
90
|
+
"pub.dev",
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
assert.equal(problems.length, 10);
|
|
94
|
+
assert.ok(problems[0].startsWith("egress.allow[1].host"));
|
|
95
|
+
assert.ok(problems.every((p) => p.startsWith("egress.allow[")));
|
|
96
|
+
assert.ok(!problems.some((p) => p.includes(secret)));
|
|
97
|
+
assert.deepEqual(validateVpnEgressPolicy({ allow: [] }), []);
|
|
98
|
+
assert.deepEqual(validateVpnEgressPolicy({ allow: [{ host: "pub.dev", ports: null }] }), []);
|
|
99
|
+
for (const bad of [null, undefined, 5, "x", [], {}, { allow: "pub.dev" }, { allow: null }]) {
|
|
100
|
+
assert.equal(validateVpnEgressPolicy(bad).length, 1, JSON.stringify(bad));
|
|
101
|
+
}
|
|
102
|
+
const tooMany = { allow: Array.from({ length: VPN_EGRESS_MAX_RULES + 1 }, (_, i) => ({ host: `h${i}.example.com` })) };
|
|
103
|
+
assert.equal(validateVpnEgressPolicy(tooMany).length, 1);
|
|
104
|
+
assert.deepEqual(validateVpnEgressPolicy({ allow: tooMany.allow.slice(0, VPN_EGRESS_MAX_RULES) }), []);
|
|
105
|
+
});
|
|
106
|
+
test("vpnEgressPolicySchema bounds shape and size", () => {
|
|
107
|
+
assert.ok(vpnEgressPolicySchema.safeParse({ allow: [] }).success);
|
|
108
|
+
assert.ok(vpnEgressPolicySchema.safeParse({ allow: [{ host: "pub.dev" }, { host: "*.example.com", ports: [443, 8443] }] }).success);
|
|
109
|
+
for (const bad of [
|
|
110
|
+
{}, { allow: "x" }, { allow: [{}] }, { allow: [{ host: "" }] }, { allow: [{ host: "a.example.com", ports: [0] }] },
|
|
111
|
+
{ allow: [{ host: "a.example.com", ports: [1.5] }] }, { allow: [{ host: "a.example.com", ports: [70000] }] },
|
|
112
|
+
{ allow: Array.from({ length: 513 }, () => ({ host: "a.example.com" })) },
|
|
113
|
+
]) {
|
|
114
|
+
assert.ok(!vpnEgressPolicySchema.safeParse(bad).success, JSON.stringify(bad).slice(0, 60));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
test("wrapper shape: missing or null allow, and wrong-case keys, are invalid (the Go decoder agrees)", () => {
|
|
118
|
+
for (const bad of [{}, { allow: null }, { Allow: [] }, { allow: [{ Host: "pub.dev" }] }, { allow: [null] }]) {
|
|
119
|
+
assert.ok(validateVpnEgressPolicy(bad).length > 0, JSON.stringify(bad));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
test("the largest valid policy fits in the helper's stdin cap with room for the rest of the job", () => {
|
|
123
|
+
const label = (c, n) => c.repeat(n);
|
|
124
|
+
const allow = Array.from({ length: VPN_EGRESS_MAX_RULES }, (_, i) => ({
|
|
125
|
+
host: `${String(i).padStart(3, "0")}${"a".repeat(60)}.${label("b", 63)}.${label("c", 63)}.${label("d", 61)}`,
|
|
126
|
+
ports: Array.from({ length: VPN_EGRESS_MAX_PORTS_PER_RULE }, (_, k) => 60000 + k),
|
|
127
|
+
}));
|
|
128
|
+
assert.equal(allow[0].host.length, 253);
|
|
129
|
+
assert.deepEqual(validateVpnEgressPolicy({ allow }), []);
|
|
130
|
+
const bytes = Buffer.byteLength(JSON.stringify({ egress: { allow } }));
|
|
131
|
+
assert.ok(bytes + 8 * 1024 < VPN_JOB_MAX_BYTES, `${bytes} bytes leaves too little room under ${VPN_JOB_MAX_BYTES}`);
|
|
132
|
+
});
|
|
133
|
+
//# sourceMappingURL=vpn-egress.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-egress.test.js","sourceRoot":"","sources":["../src/vpn-egress.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH material for a VPN build (`VpnJobConfig.sshKnownHosts` / `sshDeployKey`,
|
|
3
|
+
* the "git over SSH through the tunnel" part of the regulated-VPN program).
|
|
4
|
+
*
|
|
5
|
+
* Pure validation shared by whoever stores the values (the control plane) and
|
|
6
|
+
* whoever applies them (the agent, `packages/agent/src/lib/vpn-ssh.ts`), so the
|
|
7
|
+
* two can never disagree about what is acceptable:
|
|
8
|
+
*
|
|
9
|
+
* - `sanitizeKnownHosts`: customer-supplied public host keys (or host CAs).
|
|
10
|
+
* Free text in the dashboard, so anything that is not exactly one recognised
|
|
11
|
+
* host-key line is refused rather than repaired, and a rejected line is never
|
|
12
|
+
* echoed back (a pasted private key must not travel into a log or an error).
|
|
13
|
+
* There is no trust-on-first-use anywhere: these lines are the ONLY host
|
|
14
|
+
* keys a VPN build's ssh will accept.
|
|
15
|
+
* - `isPlausibleSshPrivateKey`: a cheap shape check for the deploy key. It
|
|
16
|
+
* does not prove the key is usable (ssh-add does that); it keeps obviously
|
|
17
|
+
* wrong input (a public key, a huge blob, binary) out of the lease.
|
|
18
|
+
*/
|
|
19
|
+
/** At most this many known_hosts lines are accepted per tunnel. */
|
|
20
|
+
export declare const MAX_KNOWN_HOSTS_LINES = 64;
|
|
21
|
+
/** Longest accepted known_hosts line (a 4096-bit RSA key with a comment is ~800). */
|
|
22
|
+
export declare const MAX_KNOWN_HOSTS_LINE_LENGTH = 2048;
|
|
23
|
+
/** Longest accepted deploy key (an RSA-4096 OpenSSH key is ~3.4 KB). */
|
|
24
|
+
export declare const MAX_SSH_DEPLOY_KEY_LENGTH: number;
|
|
25
|
+
export interface SanitizedKnownHosts {
|
|
26
|
+
/** Normalised, de-duplicated lines, safe to write to a known_hosts file. */
|
|
27
|
+
lines: string[];
|
|
28
|
+
/** 1-based indexes of input lines that were refused. Contents are never echoed. */
|
|
29
|
+
rejected: number[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validates customer-supplied known_hosts lines. Comments (`# ...`) and blank
|
|
33
|
+
* lines are dropped silently (`ssh-keyscan` output has them); duplicates are
|
|
34
|
+
* dropped; every other line must be exactly one recognised host-key (or
|
|
35
|
+
* `@cert-authority`) line whose key blob really is of the stated type, or it is
|
|
36
|
+
* listed in `rejected`. Input past `MAX_KNOWN_HOSTS_LINES` accepted lines is
|
|
37
|
+
* rejected, not silently truncated.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sanitizeKnownHosts(input: readonly unknown[]): SanitizedKnownHosts;
|
|
40
|
+
/**
|
|
41
|
+
* A cheap shape check for an OpenSSH/PEM private key: one BEGIN/END pair of a
|
|
42
|
+
* `... PRIVATE KEY` block, bounded size, no NUL. Not a proof of validity.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isPlausibleSshPrivateKey(value: unknown): value is string;
|
|
45
|
+
//# sourceMappingURL=vpn-ssh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-ssh.d.ts","sourceRoot":"","sources":["../src/vpn-ssh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,qFAAqF;AACrF,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAChD,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAoBnD,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA0BD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,mBAAmB,CAqBjF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAIxE"}
|
package/dist/vpn-ssh.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH material for a VPN build (`VpnJobConfig.sshKnownHosts` / `sshDeployKey`,
|
|
3
|
+
* the "git over SSH through the tunnel" part of the regulated-VPN program).
|
|
4
|
+
*
|
|
5
|
+
* Pure validation shared by whoever stores the values (the control plane) and
|
|
6
|
+
* whoever applies them (the agent, `packages/agent/src/lib/vpn-ssh.ts`), so the
|
|
7
|
+
* two can never disagree about what is acceptable:
|
|
8
|
+
*
|
|
9
|
+
* - `sanitizeKnownHosts`: customer-supplied public host keys (or host CAs).
|
|
10
|
+
* Free text in the dashboard, so anything that is not exactly one recognised
|
|
11
|
+
* host-key line is refused rather than repaired, and a rejected line is never
|
|
12
|
+
* echoed back (a pasted private key must not travel into a log or an error).
|
|
13
|
+
* There is no trust-on-first-use anywhere: these lines are the ONLY host
|
|
14
|
+
* keys a VPN build's ssh will accept.
|
|
15
|
+
* - `isPlausibleSshPrivateKey`: a cheap shape check for the deploy key. It
|
|
16
|
+
* does not prove the key is usable (ssh-add does that); it keeps obviously
|
|
17
|
+
* wrong input (a public key, a huge blob, binary) out of the lease.
|
|
18
|
+
*/
|
|
19
|
+
/** At most this many known_hosts lines are accepted per tunnel. */
|
|
20
|
+
export const MAX_KNOWN_HOSTS_LINES = 64;
|
|
21
|
+
/** Longest accepted known_hosts line (a 4096-bit RSA key with a comment is ~800). */
|
|
22
|
+
export const MAX_KNOWN_HOSTS_LINE_LENGTH = 2048;
|
|
23
|
+
/** Longest accepted deploy key (an RSA-4096 OpenSSH key is ~3.4 KB). */
|
|
24
|
+
export const MAX_SSH_DEPLOY_KEY_LENGTH = 16 * 1024;
|
|
25
|
+
const KEY_TYPES = [
|
|
26
|
+
"ssh-ed25519",
|
|
27
|
+
"ecdsa-sha2-nistp256",
|
|
28
|
+
"ecdsa-sha2-nistp384",
|
|
29
|
+
"ecdsa-sha2-nistp521",
|
|
30
|
+
"ssh-rsa",
|
|
31
|
+
"sk-ssh-ed25519@openssh.com",
|
|
32
|
+
"sk-ecdsa-sha2-nistp256@openssh.com",
|
|
33
|
+
];
|
|
34
|
+
// [@cert-authority ]<host patterns> <keytype> <base64> [comment]. No other
|
|
35
|
+
// markers (@revoked is not accepted: the customer revokes by removing the key),
|
|
36
|
+
// no leading ssh option fields (`command=...`), single spaces only.
|
|
37
|
+
const HOSTS = String.raw `[A-Za-z0-9.*?!,:_\-\[\]|=+/]+`;
|
|
38
|
+
const KNOWN_HOSTS_LINE = new RegExp(`^(@cert-authority )?(${HOSTS}) (${KEY_TYPES.map((t) => t.replace(/[.]/g, "\\.")).join("|")}) ([A-Za-z0-9+/]{20,}={0,3})( [\\x20-\\x7e]{0,200})?$`);
|
|
39
|
+
/** True when the base64 blob is a well-formed SSH wire key of type `keyType`:
|
|
40
|
+
* a sequence of length-prefixed strings that consumes the blob exactly (so a
|
|
41
|
+
* truncated or padded blob is refused), the first of which is the type name
|
|
42
|
+
* (what ssh itself checks first), followed by at least one key field. */
|
|
43
|
+
function blobMatchesType(b64, keyType) {
|
|
44
|
+
let bin;
|
|
45
|
+
try {
|
|
46
|
+
bin = atob(b64);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
let pos = 0;
|
|
52
|
+
const fields = [];
|
|
53
|
+
while (pos < bin.length) {
|
|
54
|
+
if (pos + 4 > bin.length)
|
|
55
|
+
return false;
|
|
56
|
+
const len = (bin.charCodeAt(pos) * 0x1000000) + ((bin.charCodeAt(pos + 1) << 16) | (bin.charCodeAt(pos + 2) << 8) | bin.charCodeAt(pos + 3));
|
|
57
|
+
pos += 4;
|
|
58
|
+
if (len > bin.length - pos)
|
|
59
|
+
return false;
|
|
60
|
+
fields.push(bin.slice(pos, pos + len));
|
|
61
|
+
pos += len;
|
|
62
|
+
}
|
|
63
|
+
return fields.length >= 2 && fields[0] === keyType;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validates customer-supplied known_hosts lines. Comments (`# ...`) and blank
|
|
67
|
+
* lines are dropped silently (`ssh-keyscan` output has them); duplicates are
|
|
68
|
+
* dropped; every other line must be exactly one recognised host-key (or
|
|
69
|
+
* `@cert-authority`) line whose key blob really is of the stated type, or it is
|
|
70
|
+
* listed in `rejected`. Input past `MAX_KNOWN_HOSTS_LINES` accepted lines is
|
|
71
|
+
* rejected, not silently truncated.
|
|
72
|
+
*/
|
|
73
|
+
export function sanitizeKnownHosts(input) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
const rejected = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
input.forEach((raw, i) => {
|
|
78
|
+
if (typeof raw !== "string") {
|
|
79
|
+
rejected.push(i + 1);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const line = raw.trim();
|
|
83
|
+
if (line === "" || line.startsWith("#"))
|
|
84
|
+
return;
|
|
85
|
+
const m = line.length <= MAX_KNOWN_HOSTS_LINE_LENGTH ? KNOWN_HOSTS_LINE.exec(line) : null;
|
|
86
|
+
if (!m || !blobMatchesType(m[4], m[3]) || lines.length >= MAX_KNOWN_HOSTS_LINES) {
|
|
87
|
+
rejected.push(i + 1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (seen.has(line))
|
|
91
|
+
return;
|
|
92
|
+
seen.add(line);
|
|
93
|
+
lines.push(line);
|
|
94
|
+
});
|
|
95
|
+
return { lines, rejected };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A cheap shape check for an OpenSSH/PEM private key: one BEGIN/END pair of a
|
|
99
|
+
* `... PRIVATE KEY` block, bounded size, no NUL. Not a proof of validity.
|
|
100
|
+
*/
|
|
101
|
+
export function isPlausibleSshPrivateKey(value) {
|
|
102
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SSH_DEPLOY_KEY_LENGTH)
|
|
103
|
+
return false;
|
|
104
|
+
if (value.includes("\u0000"))
|
|
105
|
+
return false;
|
|
106
|
+
return /^-----BEGIN [A-Z0-9 ]{0,30}PRIVATE KEY-----\r?\n[A-Za-z0-9+/=\r\n: ,.-]+\r?\n-----END [A-Z0-9 ]{0,30}PRIVATE KEY-----\s*$/.test(value);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=vpn-ssh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-ssh.js","sourceRoot":"","sources":["../src/vpn-ssh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-ssh.test.d.ts","sourceRoot":"","sources":["../src/vpn-ssh.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { isPlausibleSshPrivateKey, MAX_KNOWN_HOSTS_LINES, sanitizeKnownHosts } from "./vpn-ssh.js";
|
|
4
|
+
const ED = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK91pDfs6YOw64Pk/1OQrblxsDl0obAVDxwCRQdDWeVM";
|
|
5
|
+
const ECDSA = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBvuulkZYdqW+h/n3m7I39sGrSATHP+KNj8oDXKvXuio3bne5GoiF6ePP8jCxnH/fhJ3rMy4Sidwg3aYllX4110=";
|
|
6
|
+
test("sanitizeKnownHosts accepts host-key and cert-authority lines, drops comments, blanks and duplicates", () => {
|
|
7
|
+
const r = sanitizeKnownHosts([
|
|
8
|
+
"# ssh-keyscan -t ed25519 git.corp.test",
|
|
9
|
+
"",
|
|
10
|
+
" ",
|
|
11
|
+
`git.corp.test ${ED}`,
|
|
12
|
+
`[git.corp.test]:2222,10.20.0.5 ${ED} root@host`,
|
|
13
|
+
`git.corp.test ${ED}`,
|
|
14
|
+
`@cert-authority *.corp.test ${ED}`,
|
|
15
|
+
`|1|c2FsdHNhbHRzYWx0c2FsdA==|aGFzaGhhc2hoYXNoaGFzaA== ${ED}`,
|
|
16
|
+
`git2.corp.test ${ECDSA}`,
|
|
17
|
+
]);
|
|
18
|
+
assert.equal(r.lines.length, 5);
|
|
19
|
+
assert.deepEqual(r.rejected, []);
|
|
20
|
+
assert.equal(r.lines[0], `git.corp.test ${ED}`);
|
|
21
|
+
});
|
|
22
|
+
test("sanitizeKnownHosts refuses anything that is not exactly one host-key line, without echoing it", () => {
|
|
23
|
+
const r = sanitizeKnownHosts([
|
|
24
|
+
"-----BEGIN OPENSSH PRIVATE KEY-----",
|
|
25
|
+
`@revoked git.corp.test ${ED}`,
|
|
26
|
+
`command="rm -rf /" git.corp.test ${ED}`,
|
|
27
|
+
"git.corp.test ssh-dss AAAAB3NzaC1kc3MAAACBAP",
|
|
28
|
+
`git.corp.test ${ED}\nsecond line smuggled`,
|
|
29
|
+
`git.corp.test ${ED} ` + "x".repeat(3000),
|
|
30
|
+
"git.corp.test",
|
|
31
|
+
`git.corp.test ${ED}`, // two spaces
|
|
32
|
+
`git.corp.test\t${ED}`, // tab
|
|
33
|
+
`git.corp.test ${ED} comment\u0000with-nul`,
|
|
34
|
+
`git.corp.test ${ED} comment\u2028with-separator`,
|
|
35
|
+
42,
|
|
36
|
+
null,
|
|
37
|
+
]);
|
|
38
|
+
assert.deepEqual(r.lines, []);
|
|
39
|
+
assert.deepEqual(r.rejected, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
|
|
40
|
+
});
|
|
41
|
+
test("sanitizeKnownHosts requires the key blob to really be of the stated type", () => {
|
|
42
|
+
const mismatched = "ssh-ed25519 " + ECDSA.split(" ")[1];
|
|
43
|
+
const garbage = "ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
44
|
+
const notBase64Length = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK91pDfs6YOw64Pk/1OQrblxsDl0obAVDxwCRQdDWeV";
|
|
45
|
+
const r = sanitizeKnownHosts([`h ${mismatched}`, `h ${garbage}`, `h ${notBase64Length}`, `h ${ED}`]);
|
|
46
|
+
assert.deepEqual(r.lines, [`h ${ED}`]);
|
|
47
|
+
assert.deepEqual(r.rejected, [1, 2, 3]);
|
|
48
|
+
});
|
|
49
|
+
test("sanitizeKnownHosts caps the number of lines and rejects (not truncates) the rest", () => {
|
|
50
|
+
const many = Array.from({ length: 100 }, (_, i) => `h${i}.corp.test ${ED}`);
|
|
51
|
+
const r = sanitizeKnownHosts(many);
|
|
52
|
+
assert.equal(r.lines.length, MAX_KNOWN_HOSTS_LINES);
|
|
53
|
+
assert.equal(r.rejected.length, 36);
|
|
54
|
+
assert.equal(r.rejected[0], 65);
|
|
55
|
+
});
|
|
56
|
+
test("isPlausibleSshPrivateKey: a PEM/OpenSSH private-key block of sane size, nothing else", () => {
|
|
57
|
+
const body = "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQ";
|
|
58
|
+
assert.equal(isPlausibleSshPrivateKey(`-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n-----END OPENSSH PRIVATE KEY-----\n`), true);
|
|
59
|
+
assert.equal(isPlausibleSshPrivateKey(`-----BEGIN RSA PRIVATE KEY-----\r\n${body}\r\n-----END RSA PRIVATE KEY-----`), true);
|
|
60
|
+
for (const bad of [
|
|
61
|
+
"",
|
|
62
|
+
ED, // a PUBLIC key
|
|
63
|
+
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
|
|
64
|
+
`-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\u0000\n-----END OPENSSH PRIVATE KEY-----\n`,
|
|
65
|
+
`-----BEGIN OPENSSH PRIVATE KEY-----\n${"A".repeat(20000)}\n-----END OPENSSH PRIVATE KEY-----\n`,
|
|
66
|
+
`-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n`,
|
|
67
|
+
`x-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n-----END OPENSSH PRIVATE KEY-----\n`,
|
|
68
|
+
42,
|
|
69
|
+
null,
|
|
70
|
+
undefined,
|
|
71
|
+
]) {
|
|
72
|
+
assert.equal(isPlausibleSshPrivateKey(bad), false, String(bad).slice(0, 40));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=vpn-ssh.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-ssh.test.js","sourceRoot":"","sources":["../src/vpn-ssh.test.ts"],"names":[],"mappings":"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"}
|