@appforge-ci/core 0.2.3 → 0.3.1
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 +240 -1
- package/dist/agent-readiness.d.ts.map +1 -1
- package/dist/agent-readiness.js +304 -1
- package/dist/agent-readiness.js.map +1 -1
- package/dist/agent-readiness.test.js +277 -19
- package/dist/agent-readiness.test.js.map +1 -1
- package/dist/api-client.d.ts +80 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +79 -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/appforge-verify.test.d.ts +2 -0
- package/dist/appforge-verify.test.d.ts.map +1 -0
- package/dist/appforge-verify.test.js +958 -0
- package/dist/appforge-verify.test.js.map +1 -0
- package/dist/attestation-keys.d.ts +20 -0
- package/dist/attestation-keys.d.ts.map +1 -0
- package/dist/attestation-keys.js +27 -0
- package/dist/attestation-keys.js.map +1 -0
- package/dist/attestation-keys.test.d.ts +2 -0
- package/dist/attestation-keys.test.d.ts.map +1 -0
- package/dist/attestation-keys.test.js +45 -0
- package/dist/attestation-keys.test.js.map +1 -0
- package/dist/attestation-signing.d.ts +226 -0
- package/dist/attestation-signing.d.ts.map +1 -0
- package/dist/attestation-signing.js +276 -0
- package/dist/attestation-signing.js.map +1 -0
- package/dist/attestation-signing.test.d.ts +2 -0
- package/dist/attestation-signing.test.d.ts.map +1 -0
- package/dist/attestation-signing.test.js +250 -0
- package/dist/attestation-signing.test.js.map +1 -0
- package/dist/attestation-workerd.test.d.ts +2 -0
- package/dist/attestation-workerd.test.d.ts.map +1 -0
- package/dist/attestation-workerd.test.js +183 -0
- package/dist/attestation-workerd.test.js.map +1 -0
- package/dist/attestation.d.ts +412 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +353 -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 +20 -0
- package/dist/build-vpn.d.ts.map +1 -0
- package/dist/build-vpn.js +50 -0
- package/dist/build-vpn.js.map +1 -0
- package/dist/fleet-vpn.d.ts +64 -0
- package/dist/fleet-vpn.d.ts.map +1 -0
- package/dist/fleet-vpn.js +88 -0
- package/dist/fleet-vpn.js.map +1 -0
- package/dist/fleet-vpn.test.d.ts +2 -0
- package/dist/fleet-vpn.test.d.ts.map +1 -0
- package/dist/fleet-vpn.test.js +69 -0
- package/dist/fleet-vpn.test.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas-repo-url.test.d.ts +2 -0
- package/dist/schemas-repo-url.test.d.ts.map +1 -0
- package/dist/schemas-repo-url.test.js +67 -0
- package/dist/schemas-repo-url.test.js.map +1 -0
- package/dist/schemas.d.ts +360 -7
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +148 -12
- package/dist/schemas.js.map +1 -1
- package/dist/ssh-deploy-key.d.ts +59 -0
- package/dist/ssh-deploy-key.d.ts.map +1 -0
- package/dist/ssh-deploy-key.js +124 -0
- package/dist/ssh-deploy-key.js.map +1 -0
- package/dist/ssh-deploy-key.test.d.ts +2 -0
- package/dist/ssh-deploy-key.test.d.ts.map +1 -0
- package/dist/ssh-deploy-key.test.js +117 -0
- package/dist/ssh-deploy-key.test.js.map +1 -0
- package/dist/types.d.ts +204 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +1 -1
- package/dist/vpn-attestation-workerd.test.d.ts +2 -0
- package/dist/vpn-attestation-workerd.test.d.ts.map +1 -0
- package/dist/vpn-attestation-workerd.test.js +184 -0
- package/dist/vpn-attestation-workerd.test.js.map +1 -0
- package/dist/vpn-attestation.d.ts +1015 -0
- package/dist/vpn-attestation.d.ts.map +1 -0
- package/dist/vpn-attestation.js +963 -0
- package/dist/vpn-attestation.js.map +1 -0
- package/dist/vpn-attestation.test.d.ts +2 -0
- package/dist/vpn-attestation.test.d.ts.map +1 -0
- package/dist/vpn-attestation.test.js +613 -0
- package/dist/vpn-attestation.test.js.map +1 -0
- 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-sandbox.d.ts +67 -0
- package/dist/vpn-sandbox.d.ts.map +1 -0
- package/dist/vpn-sandbox.js +22 -0
- package/dist/vpn-sandbox.js.map +1 -0
- package/dist/vpn-sandbox.test.d.ts +2 -0
- package/dist/vpn-sandbox.test.d.ts.map +1 -0
- package/dist/vpn-sandbox.test.js +34 -0
- package/dist/vpn-sandbox.test.js.map +1 -0
- package/dist/vpn-ssh.d.ts +138 -0
- package/dist/vpn-ssh.d.ts.map +1 -0
- package/dist/vpn-ssh.js +320 -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 +274 -0
- package/dist/vpn-ssh.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between the control plane and an agent about the regulated-build sandbox
|
|
3
|
+
* (macOS Seatbelt, packages/agent/src/lib/sandbox): ONE definition of the profile generation and of
|
|
4
|
+
* which `requires` entries make a sandbox mandatory, shared by the server that decides and the agent
|
|
5
|
+
* that enforces so the two can never drift. (The agent refuses a job whose profile version is not
|
|
6
|
+
* the one it implements, and refuses a job that `requires` a sandbox without carrying this field —
|
|
7
|
+
* it never runs such a build unsandboxed.)
|
|
8
|
+
*/
|
|
9
|
+
/** The profile generation agents implement. Bump it only together with a new profile template. */
|
|
10
|
+
export declare const SANDBOX_PROFILE_VERSION = 2;
|
|
11
|
+
/** `requires` entries that mean "a sandbox is mandatory" for the build. */
|
|
12
|
+
export declare const SANDBOX_IMPLYING_REQUIRES: ReadonlySet<string>;
|
|
13
|
+
/** The job's `sandbox` field (what the agent's `parseSandboxRequirement` reads). */
|
|
14
|
+
export interface SandboxJobRequirement {
|
|
15
|
+
profileVersion: number;
|
|
16
|
+
/** The org that owns the build; scopes the warm caches (`~/appforge/cache.noindex/<orgId>`). */
|
|
17
|
+
orgId: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What a regulated build left behind on its Mac, as the agent observed it AFTER the steps ended
|
|
21
|
+
* (agent: `packages/agent/src/lib/sandbox/hygiene.ts`). It is evidence for the attestation, not a
|
|
22
|
+
* guarantee, and each part has a limit the reader must keep:
|
|
23
|
+
*
|
|
24
|
+
* - `processes`: a best-effort sweep of leftover processes of the agent's own uid (the build shares
|
|
25
|
+
* that uid until a dedicated build user exists). `methods` lists the detection methods that ran
|
|
26
|
+
* (`env-marker`: the process environment carries this build's `APPFORGE_JOB_DIR`; `cwd-job-dir` /
|
|
27
|
+
* `cwd-org-cache`: the working directory is inside the build's job directory / the org's cache).
|
|
28
|
+
* A hostile build can hide from both (scrub its environment and `chdir` elsewhere), and macOS hides
|
|
29
|
+
* the environment of Apple's own binaries from `ps`, so `remaining: 0` means "nothing the sweep could
|
|
30
|
+
* see was left", never "nothing is running". `remaining > 0` means a process of the build is still
|
|
31
|
+
* alive after SIGKILL: the host must not be trusted for the next tenant.
|
|
32
|
+
* - `wipe`: after the job directory was removed, did it (or the agent's trash copy of it) still
|
|
33
|
+
* exist, and how many entries (the directories themselves included) were left. 0 and `true` is the
|
|
34
|
+
* only clean result. It says nothing about copies made elsewhere (backups, snapshots, the index).
|
|
35
|
+
* - `dataAtRest`: whether the jobs and cache roots are kept out of Spotlight (`excluded`: both paths
|
|
36
|
+
* carry a directory name ending in `.noindex`, the rule Spotlight applies, checked by name and not
|
|
37
|
+
* observed in the index; `not_verified`: they do not, for example because the operator overrode
|
|
38
|
+
* the directories) and out of Time Machine backups (`tmutil isexcluded`; `unknown` when it could not
|
|
39
|
+
* be asked). APFS local snapshots are not covered by either.
|
|
40
|
+
*/
|
|
41
|
+
export type SandboxHygieneEvidence = {
|
|
42
|
+
processes: {
|
|
43
|
+
scanned: number;
|
|
44
|
+
found: number;
|
|
45
|
+
killed: number;
|
|
46
|
+
remaining: number;
|
|
47
|
+
methods: string[];
|
|
48
|
+
};
|
|
49
|
+
wipe: {
|
|
50
|
+
jobDirRemoved: boolean;
|
|
51
|
+
entriesRemaining: number;
|
|
52
|
+
};
|
|
53
|
+
dataAtRest: {
|
|
54
|
+
spotlight: "excluded" | "not_verified";
|
|
55
|
+
timeMachine: "excluded" | "included" | "unknown";
|
|
56
|
+
};
|
|
57
|
+
/** ISO timestamp of the moment the wipe was verified (the last step of the record). */
|
|
58
|
+
completedAt: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The `sandbox` field to put in a lease for a build whose tunnel needs these capabilities, or
|
|
62
|
+
* `undefined` when none of them implies a sandbox (a standard or strict-egress-only tunnel). Derived from
|
|
63
|
+
* the SAME `requires` tokens the scheduler matched the Mac against, so a build is sandboxed exactly when
|
|
64
|
+
* the Mac was chosen for its sandbox.
|
|
65
|
+
*/
|
|
66
|
+
export declare function sandboxRequirementForJob(requires: readonly string[], orgId: string): SandboxJobRequirement | undefined;
|
|
67
|
+
//# sourceMappingURL=vpn-sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-sandbox.d.ts","sourceRoot":"","sources":["../src/vpn-sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,2EAA2E;AAC3E,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAA6E,CAAC;AAExI,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACpG,IAAI,EAAE;QAAE,aAAa,EAAE,OAAO,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,UAAU,EAAE;QAAE,SAAS,EAAE,UAAU,GAAG,cAAc,CAAC;QAAC,WAAW,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAA;KAAE,CAAC;IACzG,uFAAuF;IACvF,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAEtH"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between the control plane and an agent about the regulated-build sandbox
|
|
3
|
+
* (macOS Seatbelt, packages/agent/src/lib/sandbox): ONE definition of the profile generation and of
|
|
4
|
+
* which `requires` entries make a sandbox mandatory, shared by the server that decides and the agent
|
|
5
|
+
* that enforces so the two can never drift. (The agent refuses a job whose profile version is not
|
|
6
|
+
* the one it implements, and refuses a job that `requires` a sandbox without carrying this field —
|
|
7
|
+
* it never runs such a build unsandboxed.)
|
|
8
|
+
*/
|
|
9
|
+
/** The profile generation agents implement. Bump it only together with a new profile template. */
|
|
10
|
+
export const SANDBOX_PROFILE_VERSION = 2;
|
|
11
|
+
/** `requires` entries that mean "a sandbox is mandatory" for the build. */
|
|
12
|
+
export const SANDBOX_IMPLYING_REQUIRES = new Set(["sandbox-v1", "enforcement:sandbox", "enforcement:sandbox_uid"]);
|
|
13
|
+
/**
|
|
14
|
+
* The `sandbox` field to put in a lease for a build whose tunnel needs these capabilities, or
|
|
15
|
+
* `undefined` when none of them implies a sandbox (a standard or strict-egress-only tunnel). Derived from
|
|
16
|
+
* the SAME `requires` tokens the scheduler matched the Mac against, so a build is sandboxed exactly when
|
|
17
|
+
* the Mac was chosen for its sandbox.
|
|
18
|
+
*/
|
|
19
|
+
export function sandboxRequirementForJob(requires, orgId) {
|
|
20
|
+
return requires.some((r) => SANDBOX_IMPLYING_REQUIRES.has(r)) ? { profileVersion: SANDBOX_PROFILE_VERSION, orgId } : undefined;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=vpn-sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-sandbox.js","sourceRoot":"","sources":["../src/vpn-sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,kGAAkG;AAClG,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,qBAAqB,EAAE,yBAAyB,CAAC,CAAC,CAAC;AAuCxI;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,QAA2B,EAAE,KAAa;IACjF,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,uBAAuB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACjI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-sandbox.test.d.ts","sourceRoot":"","sources":["../src/vpn-sandbox.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { SANDBOX_IMPLYING_REQUIRES, SANDBOX_PROFILE_VERSION, sandboxRequirementForJob } from "./vpn-sandbox.js";
|
|
4
|
+
test("a build is sandboxed exactly when its requires imply a sandbox", () => {
|
|
5
|
+
assert.equal(sandboxRequirementForJob([], "org-a"), undefined);
|
|
6
|
+
assert.equal(sandboxRequirementForJob(["egress-policy-v1"], "org-a"), undefined, "strict egress alone is the helper's job, not the sandbox's");
|
|
7
|
+
assert.equal(sandboxRequirementForJob(["ssh-v1", "egress-policy-v1"], "org-a"), undefined);
|
|
8
|
+
for (const r of ["sandbox-v1", "enforcement:sandbox", "enforcement:sandbox_uid"]) {
|
|
9
|
+
assert.deepEqual(sandboxRequirementForJob(["egress-policy-v1", r], "org-a"), { profileVersion: SANDBOX_PROFILE_VERSION, orgId: "org-a" }, r);
|
|
10
|
+
}
|
|
11
|
+
assert.equal(sandboxRequirementForJob(["enforcement:none"], "org-a"), undefined);
|
|
12
|
+
});
|
|
13
|
+
test("the implying set is exactly the three entries the agent honours, and the profile version is a positive integer", () => {
|
|
14
|
+
assert.deepEqual([...SANDBOX_IMPLYING_REQUIRES].sort(), ["enforcement:sandbox", "enforcement:sandbox_uid", "sandbox-v1"]);
|
|
15
|
+
assert.ok(Number.isInteger(SANDBOX_PROFILE_VERSION) && SANDBOX_PROFILE_VERSION > 0);
|
|
16
|
+
});
|
|
17
|
+
test("SandboxHygieneEvidence is the one record of what a regulated build left on its host, with exactly these fields (the agent writes it, the attestation reads it)", () => {
|
|
18
|
+
// A compile-time contract: renaming or dropping a field here breaks the agent's hygiene.ts and the later attestation consumer.
|
|
19
|
+
const record = {
|
|
20
|
+
processes: { scanned: 412, found: 2, killed: 2, remaining: 0, methods: ["env-marker", "cwd-job-dir", "cwd-org-cache"] },
|
|
21
|
+
wipe: { jobDirRemoved: true, entriesRemaining: 0 },
|
|
22
|
+
dataAtRest: { spotlight: "excluded", timeMachine: "excluded" },
|
|
23
|
+
completedAt: "2026-10-03T12:00:00.000Z",
|
|
24
|
+
};
|
|
25
|
+
assert.deepEqual(Object.keys(record).sort(), ["completedAt", "dataAtRest", "processes", "wipe"]);
|
|
26
|
+
assert.deepEqual(Object.keys(record.processes).sort(), ["found", "killed", "methods", "remaining", "scanned"]);
|
|
27
|
+
assert.deepEqual(Object.keys(record.wipe).sort(), ["entriesRemaining", "jobDirRemoved"]);
|
|
28
|
+
assert.deepEqual(Object.keys(record.dataAtRest).sort(), ["spotlight", "timeMachine"]);
|
|
29
|
+
// The states an honest record can carry for what could not be proven.
|
|
30
|
+
const unproven = { spotlight: "not_verified", timeMachine: "unknown" };
|
|
31
|
+
const included = { spotlight: "not_verified", timeMachine: "included" };
|
|
32
|
+
assert.notDeepEqual(unproven, included);
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=vpn-sandbox.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpn-sandbox.test.js","sourceRoot":"","sources":["../src/vpn-sandbox.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
/** At most this many comma-separated host patterns on one known_hosts line. */
|
|
26
|
+
export declare const MAX_KNOWN_HOSTS_PATTERNS_PER_LINE = 16;
|
|
27
|
+
export interface SanitizedKnownHosts {
|
|
28
|
+
/** Normalised, de-duplicated lines, safe to write to a known_hosts file. */
|
|
29
|
+
lines: string[];
|
|
30
|
+
/** 1-based indexes of input lines that were refused. Contents are never echoed. */
|
|
31
|
+
rejected: number[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validates customer-supplied known_hosts lines. Comments (`# ...`) and blank
|
|
35
|
+
* lines are dropped silently (`ssh-keyscan` output has them); duplicates are
|
|
36
|
+
* dropped; every other line must be exactly one recognised host-key (or
|
|
37
|
+
* `@cert-authority`) line whose key blob really is of the stated type, or it is
|
|
38
|
+
* listed in `rejected`. Input past `MAX_KNOWN_HOSTS_LINES` accepted lines is
|
|
39
|
+
* rejected, not silently truncated.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sanitizeKnownHosts(input: readonly unknown[]): SanitizedKnownHosts;
|
|
42
|
+
/**
|
|
43
|
+
* A cheap shape check for an OpenSSH/PEM private key: one BEGIN/END pair of a
|
|
44
|
+
* `... PRIVATE KEY` block, bounded size, no NUL. Not a proof of validity.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isPlausibleSshPrivateKey(value: unknown): value is string;
|
|
47
|
+
/** The agent feature that makes a lease's ssh material usable (`appforge-vpn connect` + per-build ssh state). */
|
|
48
|
+
export declare const SSH_FEATURE = "ssh-v1";
|
|
49
|
+
export interface ValidatedKnownHosts {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
/** Normalised lines, ready to store (empty when `ok` is false). */
|
|
52
|
+
lines: string[];
|
|
53
|
+
/** One human-readable problem per rejected line or per input-level fault. Never echoes line content. */
|
|
54
|
+
errors: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The strict front door for `sshKnownHosts` from a client: `sanitizeKnownHosts`
|
|
58
|
+
* plus an all-or-nothing verdict. A line the sanitizer drops is an ERROR here
|
|
59
|
+
* (the customer must see that part of their paste was refused, instead of the
|
|
60
|
+
* build later failing "No host key is known" for a server they thought was
|
|
61
|
+
* pinned), and nothing is stored when any line is bad. An empty list is valid
|
|
62
|
+
* and means "no ssh through this tunnel". Error text carries line numbers and
|
|
63
|
+
* the rule broken, never the offending text (a private key pasted by mistake
|
|
64
|
+
* must not travel into an error, a log or an audit entry).
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateKnownHostsInput(input: unknown): ValidatedKnownHosts;
|
|
67
|
+
/** What the dashboard and CLI show for one pinned host-key line. */
|
|
68
|
+
export interface SshHostKeyEntry {
|
|
69
|
+
/** The line's host-pattern field, e.g. `git.corp.test,10.20.0.5` or `*.corp.test`. */
|
|
70
|
+
hosts: string;
|
|
71
|
+
/** Key algorithm, e.g. `ssh-ed25519`. */
|
|
72
|
+
keyType: string;
|
|
73
|
+
/** OpenSSH-style fingerprint of the key blob: `SHA256:<base64 without padding>`. */
|
|
74
|
+
fingerprint: string;
|
|
75
|
+
/** True for an `@cert-authority` line: the key is a host CA, not a server's own key. */
|
|
76
|
+
certAuthority: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** `SHA256:<base64>` of an SSH wire-format key (the blob in a known_hosts or `.pub` line), as `ssh-keygen -l` prints it. */
|
|
79
|
+
export declare function sshKeyFingerprint(keyBase64: string): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Fingerprints of already-validated known_hosts lines, computed server-side so a
|
|
82
|
+
* customer compares what AppForge will actually pin with what their server
|
|
83
|
+
* prints (`ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`). Lines that do not
|
|
84
|
+
* parse (should not happen for stored data) are skipped rather than thrown on,
|
|
85
|
+
* so a display can never be broken by a corrupt row.
|
|
86
|
+
*/
|
|
87
|
+
export declare function knownHostsFingerprints(lines: readonly string[]): Promise<SshHostKeyEntry[]>;
|
|
88
|
+
/** The fingerprints of two stored lists compared, for the audit entry of a change: fingerprints only, never the lines. */
|
|
89
|
+
export declare function diffFingerprints(before: readonly SshHostKeyEntry[], after: readonly SshHostKeyEntry[]): {
|
|
90
|
+
added: string[];
|
|
91
|
+
removed: string[];
|
|
92
|
+
};
|
|
93
|
+
/** A git remote an app may be built from. */
|
|
94
|
+
export interface GitRemote {
|
|
95
|
+
/** `https`: the transport every app has always used. `ssh`: only through a VPN tunnel with pinned host keys. */
|
|
96
|
+
kind: "https" | "ssh";
|
|
97
|
+
/** `url` for `https://` and `ssh://`, `scp` for `user@host:path`. */
|
|
98
|
+
form: "url" | "scp";
|
|
99
|
+
user: string | null;
|
|
100
|
+
host: string;
|
|
101
|
+
port: number | null;
|
|
102
|
+
path: string;
|
|
103
|
+
}
|
|
104
|
+
/** Longest accepted `ssh://` / scp-like remote. */
|
|
105
|
+
export declare const MAX_SSH_REMOTE_LENGTH = 1024;
|
|
106
|
+
/**
|
|
107
|
+
* Parses a git remote into one of the two shapes an app may use, or null.
|
|
108
|
+
*
|
|
109
|
+
* - `https://…` exactly as before (the WHATWG URL parser, scheme `https:`).
|
|
110
|
+
* - `ssh://user@host[:port]/path` and the scp-like `user@host:path`: a user name
|
|
111
|
+
* is REQUIRED (without it ssh would use the Mac's own account name), the host
|
|
112
|
+
* is a plain DNS name or IPv4 address, the path is a conservative character
|
|
113
|
+
* set with no `..` segment. Anything an ssh client could read as an option or
|
|
114
|
+
* a command (leading `-`, `%`, quotes, `$`, backticks, `;`, whitespace,
|
|
115
|
+
* control characters, a `user:password@` form, an IPv6 literal) does not parse.
|
|
116
|
+
*
|
|
117
|
+
* This is the SYNTAX gate. Whether a given app may use an ssh remote at all
|
|
118
|
+
* (useVpn, an active tunnel with pinned host keys) is the control plane's rule,
|
|
119
|
+
* enforced by the routes and again at lease time.
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseGitRemote(value: unknown): GitRemote | null;
|
|
122
|
+
/** True when `value` is a (valid) ssh git remote: `ssh://` or scp-like. */
|
|
123
|
+
export declare function isSshGitRemote(value: unknown): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* True when `value` is shaped like it wants a non-https, ssh transport (an ssh
|
|
126
|
+
* form, whether or not it is well-formed). The agent uses it to refuse, closed,
|
|
127
|
+
* anything that should have gone through the ssh path but cannot be proven
|
|
128
|
+
* safe, instead of letting git interpret it.
|
|
129
|
+
*/
|
|
130
|
+
export declare function looksLikeSshGitRemote(value: unknown): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* What git over SSH through the tunnel does and does NOT give, one line each, shown
|
|
133
|
+
* by the CLI and the dashboard next to the host-key and deploy-key settings. Kept
|
|
134
|
+
* here so both surfaces (and docs/vpn.md) say the same, honest thing. Do not add a
|
|
135
|
+
* claim the code does not back: this is not compliance wording.
|
|
136
|
+
*/
|
|
137
|
+
export declare const SSH_LIMITS: readonly string[];
|
|
138
|
+
//# 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,+EAA+E;AAC/E,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAmCpD,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;AAOD,iHAAiH;AACjH,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,mEAAmE;IACnE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,wGAAwG;IACxG,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAU3E;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,aAAa,EAAE,OAAO,CAAC;CACxB;AAgBD,4HAA4H;AAC5H,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1E;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAYjG;AAED,0HAA0H;AAC1H,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,EAAE,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAI9I;AAID,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB,gHAAgH;IAChH,IAAI,EAAE,OAAO,GAAG,KAAK,CAAC;IACtB,qEAAqE;IACrE,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mDAAmD;AACnD,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAe1C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CA0B/D;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM7D;AAID;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAMvC,CAAC"}
|
package/dist/vpn-ssh.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
/** At most this many comma-separated host patterns on one known_hosts line. */
|
|
40
|
+
export const MAX_KNOWN_HOSTS_PATTERNS_PER_LINE = 16;
|
|
41
|
+
/**
|
|
42
|
+
* True when a line's host-pattern field names something specific: at most
|
|
43
|
+
* `MAX_KNOWN_HOSTS_PATTERNS_PER_LINE` patterns, none empty, at least one of them
|
|
44
|
+
* a positive (not `!`-negated) pattern, and every positive pattern carries at
|
|
45
|
+
* least one literal character. A bare `*`, `?`, `*.*` or `[*]:22` would pin ONE
|
|
46
|
+
* key as the identity of EVERY server, which defeats pinning (and, as a
|
|
47
|
+
* `@cert-authority` line, would trust a CA for every host); a line of only
|
|
48
|
+
* negations matches nothing and is a typo. Hashed (`|1|salt|hash`) patterns are
|
|
49
|
+
* opaque and always count as specific.
|
|
50
|
+
*/
|
|
51
|
+
function hostPatternsAreSpecific(field) {
|
|
52
|
+
const patterns = field.split(",");
|
|
53
|
+
if (patterns.length > MAX_KNOWN_HOSTS_PATTERNS_PER_LINE)
|
|
54
|
+
return false;
|
|
55
|
+
let positives = 0;
|
|
56
|
+
for (const p of patterns) {
|
|
57
|
+
if (p === "" || p === "!")
|
|
58
|
+
return false;
|
|
59
|
+
if (p.startsWith("|")) {
|
|
60
|
+
positives++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (p.startsWith("!"))
|
|
64
|
+
continue;
|
|
65
|
+
positives++;
|
|
66
|
+
// `[host]:port` pins a port: the literal part is the host, never the port digits.
|
|
67
|
+
// For a non-default port ssh looks a host up as `[host]:PORT`, so an unbracketed
|
|
68
|
+
// `*:2222` or `*]:2222` would match every host on that port: strip any trailing
|
|
69
|
+
// `]:port` / `:port` suffix(es) before looking for a literal character.
|
|
70
|
+
const bracket = /^\[(.*)\](?::\d+)?$/.exec(p);
|
|
71
|
+
const host = (bracket ? bracket[1] : p).replace(/(\]?:\d+)+$/, "");
|
|
72
|
+
if (!/[A-Za-z0-9]/.test(host.replace(/[*?.\-_]/g, "")))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return positives > 0;
|
|
76
|
+
}
|
|
77
|
+
/** True when the base64 blob is a well-formed SSH wire key of type `keyType`:
|
|
78
|
+
* a sequence of length-prefixed strings that consumes the blob exactly (so a
|
|
79
|
+
* truncated or padded blob is refused), the first of which is the type name
|
|
80
|
+
* (what ssh itself checks first), followed by at least one key field. */
|
|
81
|
+
function blobMatchesType(b64, keyType) {
|
|
82
|
+
let bin;
|
|
83
|
+
try {
|
|
84
|
+
bin = atob(b64);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
let pos = 0;
|
|
90
|
+
const fields = [];
|
|
91
|
+
while (pos < bin.length) {
|
|
92
|
+
if (pos + 4 > bin.length)
|
|
93
|
+
return false;
|
|
94
|
+
const len = (bin.charCodeAt(pos) * 0x1000000) + ((bin.charCodeAt(pos + 1) << 16) | (bin.charCodeAt(pos + 2) << 8) | bin.charCodeAt(pos + 3));
|
|
95
|
+
pos += 4;
|
|
96
|
+
if (len > bin.length - pos)
|
|
97
|
+
return false;
|
|
98
|
+
fields.push(bin.slice(pos, pos + len));
|
|
99
|
+
pos += len;
|
|
100
|
+
}
|
|
101
|
+
return fields.length >= 2 && fields[0] === keyType;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Validates customer-supplied known_hosts lines. Comments (`# ...`) and blank
|
|
105
|
+
* lines are dropped silently (`ssh-keyscan` output has them); duplicates are
|
|
106
|
+
* dropped; every other line must be exactly one recognised host-key (or
|
|
107
|
+
* `@cert-authority`) line whose key blob really is of the stated type, or it is
|
|
108
|
+
* listed in `rejected`. Input past `MAX_KNOWN_HOSTS_LINES` accepted lines is
|
|
109
|
+
* rejected, not silently truncated.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeKnownHosts(input) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
const rejected = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
input.forEach((raw, i) => {
|
|
116
|
+
if (typeof raw !== "string") {
|
|
117
|
+
rejected.push(i + 1);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const line = raw.trim();
|
|
121
|
+
if (line === "" || line.startsWith("#"))
|
|
122
|
+
return;
|
|
123
|
+
const m = line.length <= MAX_KNOWN_HOSTS_LINE_LENGTH ? KNOWN_HOSTS_LINE.exec(line) : null;
|
|
124
|
+
if (!m || !blobMatchesType(m[4], m[3]) || !hostPatternsAreSpecific(m[2]) || lines.length >= MAX_KNOWN_HOSTS_LINES) {
|
|
125
|
+
rejected.push(i + 1);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (seen.has(line))
|
|
129
|
+
return;
|
|
130
|
+
seen.add(line);
|
|
131
|
+
lines.push(line);
|
|
132
|
+
});
|
|
133
|
+
return { lines, rejected };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A cheap shape check for an OpenSSH/PEM private key: one BEGIN/END pair of a
|
|
137
|
+
* `... PRIVATE KEY` block, bounded size, no NUL. Not a proof of validity.
|
|
138
|
+
*/
|
|
139
|
+
export function isPlausibleSshPrivateKey(value) {
|
|
140
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SSH_DEPLOY_KEY_LENGTH)
|
|
141
|
+
return false;
|
|
142
|
+
if (value.includes("\u0000"))
|
|
143
|
+
return false;
|
|
144
|
+
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);
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Control-plane side (PR-G): validating what a customer pastes, showing it back
|
|
148
|
+
// with fingerprints, and deciding which git remotes may use ssh at all.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
/** The agent feature that makes a lease's ssh material usable (`appforge-vpn connect` + per-build ssh state). */
|
|
151
|
+
export const SSH_FEATURE = "ssh-v1";
|
|
152
|
+
/**
|
|
153
|
+
* The strict front door for `sshKnownHosts` from a client: `sanitizeKnownHosts`
|
|
154
|
+
* plus an all-or-nothing verdict. A line the sanitizer drops is an ERROR here
|
|
155
|
+
* (the customer must see that part of their paste was refused, instead of the
|
|
156
|
+
* build later failing "No host key is known" for a server they thought was
|
|
157
|
+
* pinned), and nothing is stored when any line is bad. An empty list is valid
|
|
158
|
+
* and means "no ssh through this tunnel". Error text carries line numbers and
|
|
159
|
+
* the rule broken, never the offending text (a private key pasted by mistake
|
|
160
|
+
* must not travel into an error, a log or an audit entry).
|
|
161
|
+
*/
|
|
162
|
+
export function validateKnownHostsInput(input) {
|
|
163
|
+
if (!Array.isArray(input))
|
|
164
|
+
return { ok: false, lines: [], errors: ["knownHosts must be a list of host-key lines"] };
|
|
165
|
+
// A hard ceiling BEFORE the per-line work, so an oversized paste is one cheap error.
|
|
166
|
+
if (input.length > MAX_KNOWN_HOSTS_LINES * 4) {
|
|
167
|
+
return { ok: false, lines: [], errors: [`knownHosts has too many lines (at most ${MAX_KNOWN_HOSTS_LINES} host keys are accepted)`] };
|
|
168
|
+
}
|
|
169
|
+
const { lines, rejected } = sanitizeKnownHosts(input);
|
|
170
|
+
const errors = rejected.slice(0, 20).map((n) => `line ${n} is not an acceptable host-key line (use "host[,host...] <key-type> <base64-key>" or "@cert-authority <host-pattern> <key-type> <base64-key>", as printed by ssh-keyscan; no options, no private keys, no catch-all host patterns)`);
|
|
171
|
+
if (rejected.length > 20)
|
|
172
|
+
errors.push(`${rejected.length - 20} more line(s) were also refused`);
|
|
173
|
+
return errors.length > 0 ? { ok: false, lines: [], errors } : { ok: true, lines, errors: [] };
|
|
174
|
+
}
|
|
175
|
+
function toB64NoPad(bytes) {
|
|
176
|
+
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
177
|
+
let bin = "";
|
|
178
|
+
for (const b of u8)
|
|
179
|
+
bin += String.fromCharCode(b);
|
|
180
|
+
return btoa(bin).replace(/=+$/, "");
|
|
181
|
+
}
|
|
182
|
+
function fromB64(b64) {
|
|
183
|
+
const bin = atob(b64);
|
|
184
|
+
const out = new Uint8Array(bin.length);
|
|
185
|
+
for (let i = 0; i < bin.length; i++)
|
|
186
|
+
out[i] = bin.charCodeAt(i);
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/** `SHA256:<base64>` of an SSH wire-format key (the blob in a known_hosts or `.pub` line), as `ssh-keygen -l` prints it. */
|
|
190
|
+
export async function sshKeyFingerprint(keyBase64) {
|
|
191
|
+
const digest = await crypto.subtle.digest("SHA-256", fromB64(keyBase64));
|
|
192
|
+
return `SHA256:${toB64NoPad(digest)}`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Fingerprints of already-validated known_hosts lines, computed server-side so a
|
|
196
|
+
* customer compares what AppForge will actually pin with what their server
|
|
197
|
+
* prints (`ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`). Lines that do not
|
|
198
|
+
* parse (should not happen for stored data) are skipped rather than thrown on,
|
|
199
|
+
* so a display can never be broken by a corrupt row.
|
|
200
|
+
*/
|
|
201
|
+
export async function knownHostsFingerprints(lines) {
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
const m = KNOWN_HOSTS_LINE.exec(line);
|
|
205
|
+
if (!m)
|
|
206
|
+
continue;
|
|
207
|
+
try {
|
|
208
|
+
out.push({ hosts: m[2], keyType: m[3], fingerprint: await sshKeyFingerprint(m[4]), certAuthority: m[1] !== undefined });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* corrupt stored line: not shown */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** The fingerprints of two stored lists compared, for the audit entry of a change: fingerprints only, never the lines. */
|
|
217
|
+
export function diffFingerprints(before, after) {
|
|
218
|
+
const b = new Set(before.map((e) => e.fingerprint));
|
|
219
|
+
const a = new Set(after.map((e) => e.fingerprint));
|
|
220
|
+
return { added: [...a].filter((f) => !b.has(f)).sort(), removed: [...b].filter((f) => !a.has(f)).sort() };
|
|
221
|
+
}
|
|
222
|
+
/** Longest accepted `ssh://` / scp-like remote. */
|
|
223
|
+
export const MAX_SSH_REMOTE_LENGTH = 1024;
|
|
224
|
+
const SSH_USER = String.raw `[A-Za-z0-9_][A-Za-z0-9._-]{0,63}`;
|
|
225
|
+
// A DNS name or an IPv4 address: letters, digits, dots and inner hyphens, never a leading
|
|
226
|
+
// hyphen (it would be read as an ssh option), never `%`, `:`, brackets or whitespace.
|
|
227
|
+
const SSH_HOST = String.raw `[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?`;
|
|
228
|
+
// Path characters git hosts actually use. No `-` first (option-looking), no `?`, `#`, `%`, `\`, quotes, `$`, `;`, `|`, `&`, backticks or whitespace.
|
|
229
|
+
const SSH_PATH = String.raw `[A-Za-z0-9_~+][A-Za-z0-9_~+./-]{0,500}`;
|
|
230
|
+
const SSH_URL_RE = new RegExp(String.raw `^ssh://(${SSH_USER})@(${SSH_HOST})(?::([0-9]{1,5}))?/(${SSH_PATH})$`);
|
|
231
|
+
const SCP_RE = new RegExp(String.raw `^(${SSH_USER})@(${SSH_HOST}):(/?${SSH_PATH})$`);
|
|
232
|
+
function safeRepoPath(path) {
|
|
233
|
+
return !path.split("/").some((seg) => seg === "..");
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parses a git remote into one of the two shapes an app may use, or null.
|
|
237
|
+
*
|
|
238
|
+
* - `https://…` exactly as before (the WHATWG URL parser, scheme `https:`).
|
|
239
|
+
* - `ssh://user@host[:port]/path` and the scp-like `user@host:path`: a user name
|
|
240
|
+
* is REQUIRED (without it ssh would use the Mac's own account name), the host
|
|
241
|
+
* is a plain DNS name or IPv4 address, the path is a conservative character
|
|
242
|
+
* set with no `..` segment. Anything an ssh client could read as an option or
|
|
243
|
+
* a command (leading `-`, `%`, quotes, `$`, backticks, `;`, whitespace,
|
|
244
|
+
* control characters, a `user:password@` form, an IPv6 literal) does not parse.
|
|
245
|
+
*
|
|
246
|
+
* This is the SYNTAX gate. Whether a given app may use an ssh remote at all
|
|
247
|
+
* (useVpn, an active tunnel with pinned host keys) is the control plane's rule,
|
|
248
|
+
* enforced by the routes and again at lease time.
|
|
249
|
+
*/
|
|
250
|
+
export function parseGitRemote(value) {
|
|
251
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_SSH_REMOTE_LENGTH)
|
|
252
|
+
return null;
|
|
253
|
+
if (value.startsWith("https://")) {
|
|
254
|
+
try {
|
|
255
|
+
const u = new URL(value);
|
|
256
|
+
if (u.protocol !== "https:")
|
|
257
|
+
return null;
|
|
258
|
+
return { kind: "https", form: "url", user: null, host: u.hostname, port: u.port ? Number(u.port) : null, path: u.pathname };
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// eslint-disable-next-line no-control-regex
|
|
265
|
+
if (/[\x00-\x20\x7f-\x9f]|\p{Zl}|\p{Zp}/u.test(value))
|
|
266
|
+
return null;
|
|
267
|
+
if (value.startsWith("ssh://")) {
|
|
268
|
+
const m = SSH_URL_RE.exec(value);
|
|
269
|
+
if (!m)
|
|
270
|
+
return null;
|
|
271
|
+
const port = m[3] === undefined ? null : Number(m[3]);
|
|
272
|
+
if (port !== null && (port < 1 || port > 65535))
|
|
273
|
+
return null;
|
|
274
|
+
if (!safeRepoPath(m[4]))
|
|
275
|
+
return null;
|
|
276
|
+
return { kind: "ssh", form: "url", user: m[1], host: m[2], port, path: `/${m[4]}` };
|
|
277
|
+
}
|
|
278
|
+
// scp-like: no scheme. `://` anywhere means some other scheme, never scp.
|
|
279
|
+
if (value.includes("://"))
|
|
280
|
+
return null;
|
|
281
|
+
const m = SCP_RE.exec(value);
|
|
282
|
+
if (!m || !safeRepoPath(m[3]))
|
|
283
|
+
return null;
|
|
284
|
+
return { kind: "ssh", form: "scp", user: m[1], host: m[2], port: null, path: m[3] };
|
|
285
|
+
}
|
|
286
|
+
/** True when `value` is a (valid) ssh git remote: `ssh://` or scp-like. */
|
|
287
|
+
export function isSshGitRemote(value) {
|
|
288
|
+
return parseGitRemote(value)?.kind === "ssh";
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* True when `value` is shaped like it wants a non-https, ssh transport (an ssh
|
|
292
|
+
* form, whether or not it is well-formed). The agent uses it to refuse, closed,
|
|
293
|
+
* anything that should have gone through the ssh path but cannot be proven
|
|
294
|
+
* safe, instead of letting git interpret it.
|
|
295
|
+
*/
|
|
296
|
+
export function looksLikeSshGitRemote(value) {
|
|
297
|
+
if (typeof value !== "string")
|
|
298
|
+
return false;
|
|
299
|
+
if (value.startsWith("ssh://"))
|
|
300
|
+
return true;
|
|
301
|
+
if (value.startsWith("https://") || value.includes("://"))
|
|
302
|
+
return false;
|
|
303
|
+
// `[user@]host:path` with no slash before the colon is git's own scp test.
|
|
304
|
+
return /^[^/:]+:/.test(value) && !/^[A-Za-z]:[\\/]/.test(value);
|
|
305
|
+
}
|
|
306
|
+
// --- customer-facing wording -------------------------------------------------
|
|
307
|
+
/**
|
|
308
|
+
* What git over SSH through the tunnel does and does NOT give, one line each, shown
|
|
309
|
+
* by the CLI and the dashboard next to the host-key and deploy-key settings. Kept
|
|
310
|
+
* here so both surfaces (and docs/vpn.md) say the same, honest thing. Do not add a
|
|
311
|
+
* claim the code does not back: this is not compliance wording.
|
|
312
|
+
*/
|
|
313
|
+
export const SSH_LIMITS = [
|
|
314
|
+
"Host keys are pinned, never trusted on first use: ssh connects only to servers whose key you registered here, and refuses an unknown or changed key. Compare the fingerprints with what your own server prints (ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub) before you rely on them.",
|
|
315
|
+
"AppForge generates each app's deploy key and shows you only the public half. Install it on your git server as a read-only deploy key. The private half is stored encrypted and is handed only to the Mac that runs that app's build.",
|
|
316
|
+
"During a build the key sits in a per-build ssh-agent: build scripts can ask it to sign for your registered servers, but cannot read the key out. It is never written to disk by AppForge. Build scripts run as the same macOS user as the build agent on that Mac, so treat the deploy key as readable by the code you build: keep it read-only and scoped to one repository.",
|
|
317
|
+
"git and rsync use the tunnel automatically. A bare 'ssh host' in a build script is NOT routed through it; use ssh -F \"$APPFORGE_SSH_CONFIG\" host. Where the build is not kernel-sandboxed, a script that bypasses that wiring is not stopped from connecting elsewhere by this feature.",
|
|
318
|
+
"Clone speed through the tunnel for large repositories has not been measured.",
|
|
319
|
+
];
|
|
320
|
+
//# 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":""}
|