sliftutils 1.7.125 → 1.7.126

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.
Files changed (41) hide show
  1. package/bin/derivekey.js +10 -0
  2. package/bin/portsecuredaemon.js +13 -0
  3. package/bin/unrevoke.js +9 -0
  4. package/package.json +8 -3
  5. package/security/README.md +66 -2
  6. package/security/authorizedKeys/authorizedKeys.ts +23 -0
  7. package/security/authorizedKeys/daemon/authLog.ts +117 -0
  8. package/security/authorizedKeys/daemon/daemon.ts +308 -0
  9. package/security/authorizedKeys/daemon/git.ts +119 -0
  10. package/security/authorizedKeys/daemon/notify.ts +25 -0
  11. package/security/authorizedKeys/daemon/paths.ts +26 -0
  12. package/security/authorizedKeys/daemon/portsecure.service +3 -2
  13. package/security/authorizedKeys/daemon/revocation.ts +306 -0
  14. package/security/authorizedKeys/daemon/rootKeys.ts +135 -0
  15. package/security/authorizedKeys/daemon/sshdConfig.ts +85 -0
  16. package/security/authorizedKeys/daemon/state.ts +108 -0
  17. package/security/authorizedKeys/daemon/trust.ts +291 -0
  18. package/security/authorizedKeys/daemon/userKeys.ts +76 -0
  19. package/security/authorizedKeys/dist/authorizedKeys.ts.cache +73 -0
  20. package/security/authorizedKeys/dist/revokeSource.ts.cache +44 -0
  21. package/security/authorizedKeys/dist/secureSSH.ts.cache +552 -0
  22. package/security/authorizedKeys/dist/sources.ts.cache +24 -0
  23. package/security/authorizedKeys/dist/unrevoke.ts.cache +145 -0
  24. package/security/authorizedKeys/revokeSource.ts +40 -0
  25. package/security/authorizedKeys/secureSSH.ts +201 -15
  26. package/security/authorizedKeys/unrevoke.ts +149 -0
  27. package/security/helpers/dist/paths.ts.cache +28 -0
  28. package/security/helpers/dist/remoteSSH.ts.cache +90 -0
  29. package/security/helpers/dist/spawn.ts.cache +34 -0
  30. package/security/helpers/remoteSSH.ts +3 -2
  31. package/security/helpers/spawn.ts +5 -1
  32. package/security/keys/deriveKey.ts +72 -0
  33. package/security/keys/dist/deriveKey.ts.cache +72 -0
  34. package/security/keys/dist/sshKeyFile.ts.cache +153 -0
  35. package/security/keys/sshKeyFile.ts +156 -0
  36. package/security/notifications/dist/discord.ts.cache +180 -0
  37. package/security/signedFiles/dist/manifest.ts.cache +68 -0
  38. package/security/signedFiles/dist/signFiles.ts.cache +146 -0
  39. package/security/signedFiles/manifest.ts +19 -6
  40. package/security/signedFiles/signFiles.ts +78 -43
  41. package/security/authorizedKeys/daemon/portsecureDaemon.js +0 -1032
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ // PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js contains a plain JS port of everything here, so it can
3
+ // resolve the same locations with no dependencies. Both sides must derive identical paths from a
4
+ // repo url - if you change one, make the matching change in the other.
5
+ Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
6
+ //exports.sourceRepoPath = exports.sourceKeyPath = exports.sourceName = exports.REPOS_DIR = exports.REPO_KEYS_DIR = void 0;
7
+ exports.REPO_KEYS_DIR = "/etc/portsecure/repo-keys";
8
+ exports.REPOS_DIR = "/var/lib/portsecure/authorized-keys-repos";
9
+ /** A repo url reduced to something usable as a file name. Derived rather than configured, so the
10
+ daemon and the deploy script always agree on where a source's key and checkout live. */
11
+ function sourceName(repoURL) {
12
+ return repoURL.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
13
+ }
14
+ exports.sourceName = sourceName;
15
+ function sourceKeyPath(repoURL) {
16
+ return `${exports.REPO_KEYS_DIR}/${sourceName(repoURL)}`;
17
+ }
18
+ exports.sourceKeyPath = sourceKeyPath;
19
+ function sourceRepoPath(repoURL) {
20
+ return `${exports.REPOS_DIR}/${sourceName(repoURL)}`;
21
+ }
22
+ exports.sourceRepoPath = sourceRepoPath;
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic291cmNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNvdXJjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLHlIQUF5SDtBQUN6SCxpR0FBaUc7QUFDakcsdUVBQXVFOzs7QUFFMUQsUUFBQSxhQUFhLEdBQUcsMkJBQTJCLENBQUM7QUFDNUMsUUFBQSxTQUFTLEdBQUcsMkNBQTJDLENBQUM7QUFFckU7MkZBQzJGO0FBQzNGLFNBQWdCLFVBQVUsQ0FBQyxPQUFlO0lBQ3RDLE9BQU8sT0FBTyxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLEVBQUUsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDO0FBQ3hGLENBQUM7QUFGRCxnQ0FFQztBQUVELFNBQWdCLGFBQWEsQ0FBQyxPQUFlO0lBQ3pDLE9BQU8sR0FBRyxxQkFBYSxJQUFJLFVBQVUsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO0FBQ3JELENBQUM7QUFGRCxzQ0FFQztBQUVELFNBQWdCLGNBQWMsQ0FBQyxPQUFlO0lBQzFDLE9BQU8sR0FBRyxpQkFBUyxJQUFJLFVBQVUsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO0FBQ2pELENBQUM7QUFGRCx3Q0FFQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIFBPUlRFRCBDT0RFOiBzZWN1cml0eS9hdXRob3JpemVkS2V5cy9kYWVtb24vcG9ydHNlY3VyZURhZW1vbi5qcyBjb250YWlucyBhIHBsYWluIEpTIHBvcnQgb2YgZXZlcnl0aGluZyBoZXJlLCBzbyBpdCBjYW5cbi8vIHJlc29sdmUgdGhlIHNhbWUgbG9jYXRpb25zIHdpdGggbm8gZGVwZW5kZW5jaWVzLiBCb3RoIHNpZGVzIG11c3QgZGVyaXZlIGlkZW50aWNhbCBwYXRocyBmcm9tIGFcbi8vIHJlcG8gdXJsIC0gaWYgeW91IGNoYW5nZSBvbmUsIG1ha2UgdGhlIG1hdGNoaW5nIGNoYW5nZSBpbiB0aGUgb3RoZXIuXG5cbmV4cG9ydCBjb25zdCBSRVBPX0tFWVNfRElSID0gXCIvZXRjL3BvcnRzZWN1cmUvcmVwby1rZXlzXCI7XG5leHBvcnQgY29uc3QgUkVQT1NfRElSID0gXCIvdmFyL2xpYi9wb3J0c2VjdXJlL2F1dGhvcml6ZWQta2V5cy1yZXBvc1wiO1xuXG4vKiogQSByZXBvIHVybCByZWR1Y2VkIHRvIHNvbWV0aGluZyB1c2FibGUgYXMgYSBmaWxlIG5hbWUuIERlcml2ZWQgcmF0aGVyIHRoYW4gY29uZmlndXJlZCwgc28gdGhlXG4gICAgZGFlbW9uIGFuZCB0aGUgZGVwbG95IHNjcmlwdCBhbHdheXMgYWdyZWUgb24gd2hlcmUgYSBzb3VyY2UncyBrZXkgYW5kIGNoZWNrb3V0IGxpdmUuICovXG5leHBvcnQgZnVuY3Rpb24gc291cmNlTmFtZShyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gcmVwb1VSTC5yZXBsYWNlKC9bXkEtWmEtejAtOV0rL2csIFwiLVwiKS5yZXBsYWNlKC9eLSt8LSskL2csIFwiXCIpLnRvTG93ZXJDYXNlKCk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBzb3VyY2VLZXlQYXRoKHJlcG9VUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgJHtSRVBPX0tFWVNfRElSfS8ke3NvdXJjZU5hbWUocmVwb1VSTCl9YDtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNvdXJjZVJlcG9QYXRoKHJlcG9VUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgJHtSRVBPU19ESVJ9LyR7c291cmNlTmFtZShyZXBvVVJMKX1gO1xufVxuIl19
24
+ /* _JS_SOURCE_HASH = "d4a51163d16ac57a8fa5966ca3b09eaebcbef4ca2200c706a7e125220c7db7b8"; */
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
6
+ //exports.main = exports.revokedKeysInRepo = exports.readRemoteRevocations = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const runPromise_1 = require("socket-function/src/runPromise");
11
+ const authorizedKeys_1 = require("./authorizedKeys");
12
+ const revokeSource_1 = require("./revokeSource");
13
+ const authorizedKeys_2 = require("./authorizedKeys");
14
+ const paths_1 = require("../helpers/paths");
15
+ const spawn_1 = require("../helpers/spawn");
16
+ const UNREVOKES_DIR = "unrevoked";
17
+ const REVOCATIONS_DIR = "revocations";
18
+ const USAGE = `Usage: yarn unrevoke [keys-repo]
19
+
20
+ Run this in a keys repo, or name one. It reads that repo's revoke repo and writes one unrevoke file
21
+ naming every revocation in it, so the keys are accepted again once each machine's hour long wait
22
+ passes.
23
+
24
+ Keys that were revoked should normally be deleted from the repo instead. Unrevoking only matters
25
+ for a key you still want.`;
26
+ /** Every revocation the revoke repo lists. Cloned read only into a temp directory, with whatever
27
+ credentials this machine already has - the derived deploy key is for servers, and a person
28
+ running this has their own access to the repo. */
29
+ async function readRemoteRevocations(config) {
30
+ let { sourceURL } = config;
31
+ let temporaryDirectory = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "unrevoke-"));
32
+ let repoPath = path_1.default.join(temporaryDirectory, "repo");
33
+ let clone = await (0, spawn_1.spawnPromise)({
34
+ command: "git",
35
+ args: ["clone", "--depth", "1", (0, revokeSource_1.revokeRepoURL)(sourceURL), repoPath],
36
+ });
37
+ if (clone.status !== 0) {
38
+ await promises_1.default.rm(temporaryDirectory, { recursive: true, force: true });
39
+ throw new Error(`Expected to be able to read ${(0, revokeSource_1.revokeRepoURL)(sourceURL)}.\n`
40
+ + `${(clone.stdout + clone.stderr).trim()}`);
41
+ }
42
+ let revocations = [];
43
+ let directory = path_1.default.join(repoPath, REVOCATIONS_DIR);
44
+ try {
45
+ for (let name of (await promises_1.default.readdir(directory)).sort()) {
46
+ if (name.endsWith(".json")) {
47
+ revocations.push(JSON.parse(await promises_1.default.readFile(path_1.default.join(directory, name), "utf8")));
48
+ }
49
+ }
50
+ }
51
+ catch (e) {
52
+ // No revocations directory at all just means nothing has ever been revoked.
53
+ }
54
+ await promises_1.default.rm(temporaryDirectory, { recursive: true, force: true });
55
+ return revocations;
56
+ }
57
+ exports.readRemoteRevocations = readRemoteRevocations;
58
+ /** Which keys in a repo are revoked. What signfiles and securessh refuse over. */
59
+ async function revokedKeysInRepo(config) {
60
+ let { repoPath, sourceURL } = config;
61
+ let revocations = await readRemoteRevocations({ sourceURL });
62
+ let revokedFingerprints = new Set(revocations.map(revocation => revocation.fingerprint));
63
+ let unrevoked = new Set();
64
+ try {
65
+ let directory = path_1.default.join(repoPath, UNREVOKES_DIR);
66
+ for (let name of await promises_1.default.readdir(directory)) {
67
+ if (!name.endsWith(".json")) {
68
+ continue;
69
+ }
70
+ let parsed = JSON.parse(await promises_1.default.readFile(path_1.default.join(directory, name), "utf8"));
71
+ for (let revocationId of parsed.revocationIds || []) {
72
+ unrevoked.add(revocationId);
73
+ }
74
+ }
75
+ }
76
+ catch (e) {
77
+ // Nothing has been unrevoked.
78
+ }
79
+ let stillRevoked = revocations.filter(revocation => !unrevoked.has(revocation.revocationId));
80
+ let keys = (0, authorizedKeys_1.normalizeKeys)(await promises_1.default.readFile(path_1.default.join(repoPath, "authorized_keys"), "utf8").catch(() => ""));
81
+ return stillRevoked
82
+ .filter(revocation => keys.some(key => (0, authorizedKeys_1.keyFingerprint)(key) === revocation.fingerprint))
83
+ .map(revocation => ({ revocation, key: keys.find(key => (0, authorizedKeys_1.keyFingerprint)(key) === revocation.fingerprint) || "" }));
84
+ }
85
+ exports.revokedKeysInRepo = revokedKeysInRepo;
86
+ /** The repo named, or the one we are standing in. Checked the same way securessh checks it: a git
87
+ repo that actually holds keys and has an origin to clone from. */
88
+ async function resolveKeysRepo(named) {
89
+ let cwd = named && (0, paths_1.expandHome)(named) || undefined;
90
+ let repoPath = (await (0, runPromise_1.runPromise)("git rev-parse --show-toplevel", { quiet: true, cwd })).trim();
91
+ if (!repoPath) {
92
+ throw new Error(`Expected ${named || "the current directory"} to be inside a git repo, it is not.\n${USAGE}`);
93
+ }
94
+ try {
95
+ await (0, authorizedKeys_2.readRepoKeys)(repoPath);
96
+ }
97
+ catch (e) {
98
+ throw new Error(`Expected ${repoPath} to be a keys repo, it holds no keys.\n${e}\n${USAGE}`);
99
+ }
100
+ let originURL = (await (0, runPromise_1.runPromise)("git remote get-url origin", { quiet: true, cwd: repoPath })).trim();
101
+ if (!originURL) {
102
+ throw new Error(`Expected ${repoPath} to have an origin remote, it has none.\n${USAGE}`);
103
+ }
104
+ return { repoPath, originURL };
105
+ }
106
+ async function main() {
107
+ var _a;
108
+ let argv = process.argv.slice(2);
109
+ if (argv.length > 1) {
110
+ throw new Error(`Expected at most a keys repo, was ${argv.length} argument(s)\n${USAGE}`);
111
+ }
112
+ let { repoPath, originURL } = await resolveKeysRepo(argv[0]);
113
+ let revocations = await readRemoteRevocations({ sourceURL: originURL });
114
+ if (!revocations.length) {
115
+ console.log(`${(0, revokeSource_1.revokeRepoURL)(originURL)} lists no revocations, so there is nothing to undo.`);
116
+ return;
117
+ }
118
+ let directory = path_1.default.join(repoPath, UNREVOKES_DIR);
119
+ await promises_1.default.mkdir(directory, { recursive: true });
120
+ let stamp = new Date().toISOString().replace(/[:.]/g, "-");
121
+ let unrevokeId = `${stamp}-unrevoke`;
122
+ await promises_1.default.writeFile(path_1.default.join(directory, `${unrevokeId}.json`), JSON.stringify({
123
+ unrevokeId,
124
+ createdAt: new Date().toISOString(),
125
+ // Named one by one rather than as a blanket "allow everything again", so this file only
126
+ // ever undoes the revocations that existed when it was written.
127
+ revocationIds: revocations.map(revocation => revocation.revocationId),
128
+ revocations: revocations.map(revocation => ({
129
+ revocationId: revocation.revocationId,
130
+ fingerprint: revocation.fingerprint,
131
+ revokedAt: revocation.revokedAt,
132
+ revokedBy: revocation.revokedBy,
133
+ attempt: revocation.attempt,
134
+ })),
135
+ }, undefined, 4) + "\n");
136
+ console.log(`Wrote ${UNREVOKES_DIR}/${unrevokeId}.json covering ${revocations.length} revocation(s):`);
137
+ for (let revocation of revocations) {
138
+ console.log(` ${revocation.fingerprint} revoked by ${revocation.revokedBy || "?"} from ${((_a = revocation.attempt) === null || _a === void 0 ? void 0 : _a.ip) || "?"}`);
139
+ }
140
+ console.log(`Each machine waits an hour after seeing it before the keys work again.`);
141
+ console.log(`Sign and publish it with:\n yarn signfiles git`);
142
+ }
143
+ exports.main = main;
144
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidW5yZXZva2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJ1bnJldm9rZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSwyREFBNkI7QUFDN0IsNENBQW9CO0FBQ3BCLGdEQUF3QjtBQUN4QiwrREFBNEQ7QUFDNUQscURBQWlFO0FBQ2pFLGlEQUErQztBQUMvQyxxREFBZ0Q7QUFDaEQsNENBQThDO0FBQzlDLDRDQUFnRDtBQUVoRCxNQUFNLGFBQWEsR0FBRyxXQUFXLENBQUM7QUFDbEMsTUFBTSxlQUFlLEdBQUcsYUFBYSxDQUFDO0FBQ3RDLE1BQU0sS0FBSyxHQUFHOzs7Ozs7OzBCQU9ZLENBQUM7QUFXM0I7O3FEQUVxRDtBQUM5QyxLQUFLLFVBQVUscUJBQXFCLENBQUMsTUFBNkI7SUFDckUsSUFBSSxFQUFFLFNBQVMsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUMzQixJQUFJLGtCQUFrQixHQUFHLE1BQU0sa0JBQUUsQ0FBQyxPQUFPLENBQUMsY0FBSSxDQUFDLElBQUksQ0FBQyxZQUFFLENBQUMsTUFBTSxFQUFFLEVBQUUsV0FBVyxDQUFDLENBQUMsQ0FBQztJQUMvRSxJQUFJLFFBQVEsR0FBRyxjQUFJLENBQUMsSUFBSSxDQUFDLGtCQUFrQixFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3JELElBQUksS0FBSyxHQUFHLE1BQU0sSUFBQSxvQkFBWSxFQUFDO1FBQzNCLE9BQU8sRUFBRSxLQUFLO1FBQ2QsSUFBSSxFQUFFLENBQUMsT0FBTyxFQUFFLFNBQVMsRUFBRSxHQUFHLEVBQUUsSUFBQSw0QkFBYSxFQUFDLFNBQVMsQ0FBQyxFQUFFLFFBQVEsQ0FBQztLQUN0RSxDQUFDLENBQUM7SUFDSCxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDckIsTUFBTSxrQkFBRSxDQUFDLEVBQUUsQ0FBQyxrQkFBa0IsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFD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QUFDLE1BQU0saUJBQWlCLENBQUMsQ0FBQztJQUN2RyxLQUFLLElBQUksVUFBVSxJQUFJLFdBQVcsRUFBRSxDQUFDO1FBQ2pDLE9BQU8sQ0FBQyxHQUFHLENBQUMsS0FBSyxVQUFVLENBQUMsV0FBVyxlQUFlLFVBQVUsQ0FBQyxTQUFTLElBQUksR0FBRyxTQUFTLENBQUEsTUFBQSxVQUFVLENBQUMsT0FBTywwQ0FBRSxFQUFFLEtBQUksR0FBRyxFQUFFLENBQUMsQ0FBQztJQUMvSCxDQUFDO0lBQ0QsT0FBTyxDQUFDLEdBQUcsQ0FBQyx3RUFBd0UsQ0FBQyxDQUFDO0lBQ3RGLE9BQU8sQ0FBQyxHQUFHLENBQUMsaURBQWlELENBQUMsQ0FBQztBQUNuRSxDQUFDO0FBdENELG9CQXNDQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnMvcHJvbWlzZXNcIjtcbmltcG9ydCBvcyBmcm9tIFwib3NcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5pbXBvcnQgeyBydW5Qcm9taXNlIH0gZnJvbSBcInNvY2tldC1mdW5jdGlvbi9zcmMvcnVuUHJvbWlzZVwiO1xuaW1wb3J0IHsga2V5RmluZ2VycHJpbnQsIG5vcm1hbGl6ZUtleXMgfSBmcm9tIFwiLi9hdXRob3JpemVkS2V5c1wiO1xuaW1wb3J0IHsgcmV2b2tlUmVwb1VSTCB9IGZyb20gXCIuL3Jldm9rZVNvdXJjZVwiO1xuaW1wb3J0IHsgcmVhZFJlcG9LZXlzIH0gZnJvbSBcIi4vYXV0aG9yaXplZEtleXNcIjtcbmltcG9ydCB7IGV4cGFuZEhvbWUgfSBmcm9tIFwiLi4vaGVscGVycy9wYXRoc1wiO1xuaW1wb3J0IHsgc3Bhd25Qcm9taXNlIH0gZnJvbSBcIi4uL2hlbHBlcnMvc3Bhd25cIjtcblxuY29uc3QgVU5SRVZPS0VTX0RJUiA9IFwidW5yZXZva2VkXCI7XG5jb25zdCBSRVZPQ0FUSU9OU19ESVIgPSBcInJldm9jYXRpb25zXCI7XG5jb25zdCBVU0FHRSA9IGBVc2FnZTogeWFybiB1bnJldm9rZSBba2V5cy1yZXBvXVxuXG5SdW4gdGhpcyBpbiBhIGtleXMgcmVwbywgb3IgbmFtZSBvbmUuIEl0IHJlYWRzIHRoYXQgcmVwbydzIHJldm9rZSByZXBvIGFuZCB3cml0ZXMgb25lIHVucmV2b2tlIGZpbGVcbm5hbWluZyBldmVyeSByZXZvY2F0aW9uIGluIGl0LCBzbyB0aGUga2V5cyBhcmUgYWNjZXB0ZWQgYWdhaW4gb25jZSBlYWNoIG1hY2hpbmUncyBob3VyIGxvbmcgd2FpdFxucGFzc2VzLlxuXG5LZXlzIHRoYXQgd2VyZSByZXZva2VkIHNob3VsZCBub3JtYWxseSBiZSBkZWxldGVkIGZyb20gdGhlIHJlcG8gaW5zdGVhZC4gVW5yZXZva2luZyBvbmx5IG1hdHRlcnNcbmZvciBhIGtleSB5b3Ugc3RpbGwgd2FudC5gO1xuXG5leHBvcnQgdHlwZSBSZXZvY2F0aW9uID0ge1xuICAgIHJldm9jYXRpb25JZDogc3RyaW5nO1xuICAgIGZpbmdlcnByaW50OiBzdHJpbmc7XG4gICAga2V5Pzogc3RyaW5nO1xuICAgIHJldm9rZWRBdD86IHN0cmluZztcbiAgICByZXZva2VkQnk/OiBzdHJpbmc7XG4gICAgYXR0ZW1wdD86IHsgaXA/OiBzdHJpbmc7IHVzZXI/OiBzdHJpbmc7IHJlcXVpcmVkPzogc3RyaW5nIH07XG59O1xuXG4vKiogRXZlcnkgcmV2b2NhdGlvbiB0aGUgcmV2b2tlIHJlcG8gbGlzdHMuIENsb25lZCByZWFkIG9ubHkgaW50byBhIHRlbXAgZGlyZWN0b3J5LCB3aXRoIHdoYXRldmVyXG4gICAgY3JlZGVudGlhbHMgdGhpcyBtYWNoaW5lIGFscmVhZHkgaGFzIC0gdGhlIGRlcml2ZWQgZGVwbG95IGtleSBpcyBmb3Igc2VydmVycywgYW5kIGEgcGVyc29uXG4gICAgcnVubmluZyB0aGlzIGhhcyB0aGVpciBvd24gYWNjZXNzIHRvIHRoZSByZXBvLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHJlYWRSZW1vdGVSZXZvY2F0aW9ucyhjb25maWc6IHsgc291cmNlVVJMOiBzdHJpbmcgfSkge1xuICAgIGxldCB7IHNvdXJjZVVSTCB9ID0gY29uZmlnO1xuICAgIGxldCB0ZW1wb3JhcnlEaXJlY3RvcnkgPSBhd2FpdCBmcy5ta2R0ZW1wKHBhdGguam9pbihvcy50bXBkaXIoKSwgXCJ1bnJldm9rZS1cIikpO1xuICAgIGxldCByZXBvUGF0aCA9IHBhdGguam9pbih0ZW1wb3JhcnlEaXJlY3RvcnksIFwicmVwb1wiKTtcbiAgICBsZXQgY2xvbmUgPSBhd2FpdCBzcGF3blByb21pc2Uoe1xuICAgICAgICBjb21tYW5kOiBcImdpdFwiLFxuICAgICAgICBhcmdzOiBbXCJjbG9uZVwiLCBcIi0tZGVwdGhcIiwgXCIxXCIsIHJldm9rZVJlcG9VUkwoc291cmNlVVJMKSwgcmVwb1BhdGhdLFxuICAgIH0pO1xuICAgIGlmIChjbG9uZS5zdGF0dXMgIT09IDApIHtcbiAgICAgICAgYXdhaXQgZnMucm0odGVtcG9yYXJ5RGlyZWN0b3J5LCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSk7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCB0byBiZSBhYmxlIHRvIHJlYWQgJHtyZXZva2VSZXBvVVJMKHNvdXJjZVVSTCl9LlxcbmBcbiAgICAgICAgICAgICsgYCR7KGNsb25lLnN0ZG91dCArIGNsb25lLnN0ZGVycikudHJpbSgpfWBcbiAgICAgICAgKTtcbiAgICB9XG4gICAgbGV0IHJldm9jYXRpb25zOiBSZXZvY2F0aW9uW10gPSBbXTtcbiAgICBsZXQgZGlyZWN0b3J5ID0gcGF0aC5qb2luKHJlcG9QYXRoLCBSRVZPQ0FUSU9OU19ESVIpO1xuICAgIHRyeSB7XG4gICAgICAgIGZvciAobGV0IG5hbWUgb2YgKGF3YWl0IGZzLnJlYWRkaXIoZGlyZWN0b3J5KSkuc29ydCgpKSB7XG4gICAgICAgICAgICBpZiAobmFtZS5lbmRzV2l0aChcIi5qc29uXCIpKSB7XG4gICAgICAgICAgICAgICAgcmV2b2NhdGlvbnMucHVzaChKU09OLnBhcnNlKGF3YWl0IGZzLnJlYWRGaWxlKHBhdGguam9pbihkaXJlY3RvcnksIG5hbWUpLCBcInV0ZjhcIikpKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgLy8gTm8gcmV2b2NhdGlvbnMgZGlyZWN0b3J5IGF0IGFsbCBqdXN0IG1lYW5zIG5vdGhpbmcgaGFzIGV2ZXIgYmVlbiByZXZva2VkLlxuICAgIH1cbiAgICBhd2FpdCBmcy5ybSh0ZW1wb3JhcnlEaXJlY3RvcnksIHsgcmVjdXJzaXZlOiB0cnVlLCBmb3JjZTogdHJ1ZSB9KTtcbiAgICByZXR1cm4gcmV2b2NhdGlvbnM7XG59XG5cbi8qKiBXaGljaCBrZXlzIGluIGEgcmVwbyBhcmUgcmV2b2tlZC4gV2hhdCBzaWduZmlsZXMgYW5kIHNlY3VyZXNzaCByZWZ1c2Ugb3Zlci4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZXZva2VkS2V5c0luUmVwbyhjb25maWc6IHsgcmVwb1BhdGg6IHN0cmluZzsgc291cmNlVVJMOiBzdHJpbmcgfSkge1xuICAgIGxldCB7IHJlcG9QYXRoLCBzb3VyY2VVUkwgfSA9IGNvbmZpZztcbiAgICBsZXQgcmV2b2NhdGlvbnMgPSBhd2FpdCByZWFkUmVtb3RlUmV2b2NhdGlvbnMoeyBzb3VyY2VVUkwgfSk7XG4gICAgbGV0IHJldm9rZWRGaW5nZXJwcmludHMgPSBuZXcgU2V0KHJldm9jYXRpb25zLm1hcChyZXZvY2F0aW9uID0+IHJldm9jYXRpb24uZmluZ2VycHJpbnQpKTtcbiAgICBsZXQgdW5yZXZva2VkID0gbmV3IFNldDxzdHJpbmc+KCk7XG4gICAgdHJ5IHtcbiAgICAgICAgbGV0IGRpcmVjdG9yeSA9IHBhdGguam9pbihyZXBvUGF0aCwgVU5SRVZPS0VTX0RJUik7XG4gICAgICAgIGZvciAobGV0IG5hbWUgb2YgYXdhaXQgZnMucmVhZGRpcihkaXJlY3RvcnkpKSB7XG4gICAgICAgICAgICBpZiAoIW5hbWUuZW5kc1dpdGgoXCIuanNvblwiKSkge1xuICAgICAgICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgbGV0IHBhcnNlZCA9IEpTT04ucGFyc2UoYXdhaXQgZnMucmVhZEZpbGUocGF0aC5qb2luKGRpcmVjdG9yeSwgbmFtZSksIFwidXRmOFwiKSk7XG4gICAgICAgICAgICBmb3IgKGxldCByZXZvY2F0aW9uSWQgb2YgcGFyc2VkLnJldm9jYXRpb25JZHMgfHwgW10pIHtcbiAgICAgICAgICAgICAgICB1bnJldm9rZWQuYWRkKHJldm9jYXRpb25JZCk7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIC8vIE5vdGhpbmcgaGFzIGJlZW4gdW5yZXZva2VkLlxuICAgIH1cbiAgICBsZXQgc3RpbGxSZXZva2VkID0gcmV2b2NhdGlvbnMuZmlsdGVyKHJldm9jYXRpb24gPT4gIXVucmV2b2tlZC5oYXMocmV2b2NhdGlvbi5yZXZvY2F0aW9uSWQpKTtcbiAgICBsZXQga2V5cyA9IG5vcm1hbGl6ZUtleXMoYXdhaXQgZnMucmVhZEZpbGUocGF0aC5qb2luKHJlcG9QYXRoLCBcImF1dGhvcml6ZWRfa2V5c1wiKSwgXCJ1dGY4XCIpLmNhdGNoKCgpID0+IFwiXCIpKTtcbiAgICByZXR1cm4gc3RpbGxSZXZva2VkXG4gICAgICAgIC5maWx0ZXIocmV2b2NhdGlvbiA9PiBrZXlzLnNvbWUoa2V5ID0+IGtleUZpbmdlcnByaW50KGtleSkgPT09IHJldm9jYXRpb24uZmluZ2VycHJpbnQpKVxuICAgICAgICAubWFwKHJldm9jYXRpb24gPT4gKHsgcmV2b2NhdGlvbiwga2V5OiBrZXlzLmZpbmQoa2V5ID0+IGtleUZpbmdlcnByaW50KGtleSkgPT09IHJldm9jYXRpb24uZmluZ2VycHJpbnQpIHx8IFwiXCIgfSkpO1xufVxuXG4vKiogVGhlIHJlcG8gbmFtZWQsIG9yIHRoZSBvbmUgd2UgYXJlIHN0YW5kaW5nIGluLiBDaGVja2VkIHRoZSBzYW1lIHdheSBzZWN1cmVzc2ggY2hlY2tzIGl0OiBhIGdpdFxuICAgIHJlcG8gdGhhdCBhY3R1YWxseSBob2xkcyBrZXlzIGFuZCBoYXMgYW4gb3JpZ2luIHRvIGNsb25lIGZyb20uICovXG5hc3luYyBmdW5jdGlvbiByZXNvbHZlS2V5c1JlcG8obmFtZWQ6IHN0cmluZyB8IHVuZGVmaW5lZCkge1xuICAgIGxldCBjd2QgPSBuYW1lZCAmJiBleHBhbmRIb21lKG5hbWVkKSB8fCB1bmRlZmluZWQ7XG4gICAgbGV0IHJlcG9QYXRoID0gKGF3YWl0IHJ1blByb21pc2UoXCJnaXQgcmV2LXBhcnNlIC0tc2hvdy10b3BsZXZlbFwiLCB7IHF1aWV0OiB0cnVlLCBjd2QgfSkpLnRyaW0oKTtcbiAgICBpZiAoIXJlcG9QYXRoKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgJHtuYW1lZCB8fCBcInRoZSBjdXJyZW50IGRpcmVjdG9yeVwifSB0byBiZSBpbnNpZGUgYSBnaXQgcmVwbywgaXQgaXMgbm90LlxcbiR7VVNBR0V9YCk7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHJlYWRSZXBvS2V5cyhyZXBvUGF0aCk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkICR7cmVwb1BhdGh9IHRvIGJlIGEga2V5cyByZXBvLCBpdCBob2xkcyBubyBrZXlzLlxcbiR7ZX1cXG4ke1VTQUdFfWApO1xuICAgIH1cbiAgICBsZXQgb3JpZ2luVVJMID0gKGF3YWl0IHJ1blByb21pc2UoXCJnaXQgcmVtb3RlIGdldC11cmwgb3JpZ2luXCIsIHsgcXVpZXQ6IHRydWUsIGN3ZDogcmVwb1BhdGggfSkpLnRyaW0oKTtcbiAgICBpZiAoIW9yaWdpblVSTCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkICR7cmVwb1BhdGh9IHRvIGhhdmUgYW4gb3JpZ2luIHJlbW90ZSwgaXQgaGFzIG5vbmUuXFxuJHtVU0FHRX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIHsgcmVwb1BhdGgsIG9yaWdpblVSTCB9O1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbWFpbigpIHtcbiAgICBsZXQgYXJndiA9IHByb2Nlc3MuYXJndi5zbGljZSgyKTtcbiAgICBpZiAoYXJndi5sZW5ndGggPiAxKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgYXQgbW9zdCBhIGtleXMgcmVwbywgd2FzICR7YXJndi5sZW5ndGh9IGFyZ3VtZW50KHMpXFxuJHtVU0FHRX1gKTtcbiAgICB9XG4gICAgbGV0IHsgcmVwb1BhdGgsIG9yaWdpblVSTCB9ID0gYXdhaXQgcmVzb2x2ZUtleXNSZXBvKGFyZ3ZbMF0pO1xuXG4gICAgbGV0IHJldm9jYXRpb25zID0gYXdhaXQgcmVhZFJlbW90ZVJldm9jYXRpb25zKHsgc291cmNlVVJMOiBvcmlnaW5VUkwgfSk7XG4gICAgaWYgKCFyZXZvY2F0aW9ucy5sZW5ndGgpIHtcbiAgICAgICAgY29uc29sZS5sb2coYCR7cmV2b2tlUmVwb1VSTChvcmlnaW5VUkwpfSBsaXN0cyBubyByZXZvY2F0aW9ucywgc28gdGhlcmUgaXMgbm90aGluZyB0byB1bmRvLmApO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgbGV0IGRpcmVjdG9yeSA9IHBhdGguam9pbihyZXBvUGF0aCwgVU5SRVZPS0VTX0RJUik7XG4gICAgYXdhaXQgZnMubWtkaXIoZGlyZWN0b3J5LCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcbiAgICBsZXQgc3RhbXAgPSBuZXcgRGF0ZSgpLnRvSVNPU3RyaW5nKCkucmVwbGFjZSgvWzouXS9nLCBcIi1cIik7XG4gICAgbGV0IHVucmV2b2tlSWQgPSBgJHtzdGFtcH0tdW5yZXZva2VgO1xuICAgIGF3YWl0IGZzLndyaXRlRmlsZShwYXRoLmpvaW4oZGlyZWN0b3J5LCBgJHt1bnJldm9rZUlkfS5qc29uYCksIEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgdW5yZXZva2VJZCxcbiAgICAgICAgY3JlYXRlZEF0OiBuZXcgRGF0ZSgpLnRvSVNPU3RyaW5nKCksXG4gICAgICAgIC8vIE5hbWVkIG9uZSBieSBvbmUgcmF0aGVyIHRoYW4gYXMgYSBibGFua2V0IFwiYWxsb3cgZXZlcnl0aGluZyBhZ2FpblwiLCBzbyB0aGlzIGZpbGUgb25seVxuICAgICAgICAvLyBldmVyIHVuZG9lcyB0aGUgcmV2b2NhdGlvbnMgdGhhdCBleGlzdGVkIHdoZW4gaXQgd2FzIHdyaXR0ZW4uXG4gICAgICAgIHJldm9jYXRpb25JZHM6IHJldm9jYXRpb25zLm1hcChyZXZvY2F0aW9uID0+IHJldm9jYXRpb24ucmV2b2NhdGlvbklkKSxcbiAgICAgICAgcmV2b2NhdGlvbnM6IHJldm9jYXRpb25zLm1hcChyZXZvY2F0aW9uID0+ICh7XG4gICAgICAgICAgICByZXZvY2F0aW9uSWQ6IHJldm9jYXRpb24ucmV2b2NhdGlvbklkLFxuICAgICAgICAgICAgZmluZ2VycHJpbnQ6IHJldm9jYXRpb24uZmluZ2VycHJpbnQsXG4gICAgICAgICAgICByZXZva2VkQXQ6IHJldm9jYXRpb24ucmV2b2tlZEF0LFxuICAgICAgICAgICAgcmV2b2tlZEJ5OiByZXZvY2F0aW9uLnJldm9rZWRCeSxcbiAgICAgICAgICAgIGF0dGVtcHQ6IHJldm9jYXRpb24uYXR0ZW1wdCxcbiAgICAgICAgfSkpLFxuICAgIH0sIHVuZGVmaW5lZCwgNCkgKyBcIlxcblwiKTtcblxuICAgIGNvbnNvbGUubG9nKGBXcm90ZSAke1VOUkVWT0tFU19ESVJ9LyR7dW5yZXZva2VJZH0uanNvbiBjb3ZlcmluZyAke3Jldm9jYXRpb25zLmxlbmd0aH0gcmV2b2NhdGlvbihzKTpgKTtcbiAgICBmb3IgKGxldCByZXZvY2F0aW9uIG9mIHJldm9jYXRpb25zKSB7XG4gICAgICAgIGNvbnNvbGUubG9nKGAgICR7cmV2b2NhdGlvbi5maW5nZXJwcmludH0gcmV2b2tlZCBieSAke3Jldm9jYXRpb24ucmV2b2tlZEJ5IHx8IFwiP1wifSBmcm9tICR7cmV2b2NhdGlvbi5hdHRlbXB0Py5pcCB8fCBcIj9cIn1gKTtcbiAgICB9XG4gICAgY29uc29sZS5sb2coYEVhY2ggbWFjaGluZSB3YWl0cyBhbiBob3VyIGFmdGVyIHNlZWluZyBpdCBiZWZvcmUgdGhlIGtleXMgd29yayBhZ2Fpbi5gKTtcbiAgICBjb25zb2xlLmxvZyhgU2lnbiBhbmQgcHVibGlzaCBpdCB3aXRoOlxcbiAgeWFybiBzaWduZmlsZXMgZ2l0YCk7XG59XG4iXX0=
145
+ /* _JS_SOURCE_HASH = "1f43574c62196a1eb6f7594da6497ffb17128b3c8914274f810f020ee0cc4224"; */
@@ -0,0 +1,40 @@
1
+ import { deriveEd25519Key } from "../keys/deriveKey";
2
+ import { formatOpenSSHPrivateKey, parseOpenSSHPrivateKey } from "../keys/sshKeyFile";
3
+ import { REPO_KEYS_DIR, REPOS_DIR, sourceName } from "./sources";
4
+
5
+ // A source's revocations live in a repo beside it. Derived rather than configured, so adding a
6
+ // source brings its revoke repo with it and there is nothing extra to pass on the command line.
7
+ const REVOKED_SUFFIX = "_revoked";
8
+ // GitHub will not take the same public key as a deploy key on two repos, so the revoke repo is
9
+ // reached with a key derived from the source's, under this label.
10
+ export const REVOKE_KEY_LABEL = "revokegithubkey";
11
+
12
+ export function revokeRepoURL(sourceURL: string) {
13
+ let withoutGit = sourceURL.replace(/\.git$/, "");
14
+ return `${withoutGit}${REVOKED_SUFFIX}.git`;
15
+ }
16
+
17
+ export function revokeKeyPath(sourceURL: string) {
18
+ return `${REPO_KEYS_DIR}/${sourceName(sourceURL)}${REVOKED_SUFFIX}`;
19
+ }
20
+
21
+ export function revokeRepoPath(sourceURL: string) {
22
+ return `${REPOS_DIR}/${sourceName(sourceURL)}${REVOKED_SUFFIX}`;
23
+ }
24
+
25
+ /** The revoke repo's key, worked out from the source repo's key. Anything holding the source key
26
+ can produce it, so it never has to be stored anywhere separately or handed around. */
27
+ export function deriveRevokeKey(sourcePrivateKey: string) {
28
+ let source = parseOpenSSHPrivateKey(sourcePrivateKey);
29
+ let derived = deriveEd25519Key({ seed: source.seed, label: REVOKE_KEY_LABEL });
30
+ return {
31
+ publicKey: `ssh-ed25519 ${Buffer.concat([
32
+ Buffer.from([0, 0, 0, 11]), Buffer.from("ssh-ed25519"),
33
+ Buffer.from([0, 0, 0, 32]), derived.publicKey,
34
+ ]).toString("base64")}`,
35
+ privateKeyFile: formatOpenSSHPrivateKey({
36
+ seed: derived.seed,
37
+ comment: `${source.comment || "portsecure"} ${REVOKE_KEY_LABEL}`,
38
+ }),
39
+ };
40
+ }
@@ -4,24 +4,30 @@ import path from "path";
4
4
  import { DEFAULT_WEBHOOK_FILE_PATH, parseWebhookFile } from "../notifications/discord";
5
5
  import { normalizeKeys, readRepoKeys, summarizeKey } from "./authorizedKeys";
6
6
  import { sourceKeyPath, sourceRepoPath } from "./sources";
7
+ import { deriveRevokeKey, REVOKE_KEY_LABEL, revokeRepoURL } from "./revokeSource";
8
+ import { revokedKeysInRepo } from "./unrevoke";
7
9
  import { expandHome } from "../helpers/paths";
8
10
  import { spawnPromise } from "../helpers/spawn";
9
11
  import { readRemoteFile, remoteCommandExists, runOverSSH, SUDO_PREAMBLE, writeRemoteFile } from "../helpers/remoteSSH";
10
12
 
11
- const DAEMON_SOURCE = path.join(__dirname, "daemon", "portsecureDaemon.js");
12
13
  const SERVICE_SOURCE = path.join(__dirname, "daemon", "portsecure.service");
13
- const REMOTE_DAEMON_PATH = "/opt/portsecure/portsecure-daemon.js";
14
+ // The daemon runs from a checkout of this repo on the host, rather than from a copy we upload, so
15
+ // a host updates itself from github the same way anything else does. sliftutils is public, so this
16
+ // needs no key.
17
+ const SLIFTUTILS_URL = "https://github.com/sliftist/sliftutils.git";
18
+ const REMOTE_CHECKOUT_PATH = "/opt/portsecure/sliftutils";
14
19
  const REMOTE_SERVICE_PATH = "/etc/systemd/system/portsecure.service";
15
20
  const REMOTE_CONFIG_PATH = "/etc/portsecure/daemon.json";
16
21
  const ROOT_AUTHORIZED_KEYS = "/root/.ssh/authorized_keys";
17
22
  const SERVICE_NAME = "portsecure";
18
23
  const MAX_ERROR_BODY_LENGTH = 500;
19
- const VERBS = ["add", "remove", "list"];
24
+ const VERBS = ["add", "remove", "list", "update"];
20
25
  // The repo url is optional, and defaults to the repo the command is run from.
21
26
  const USAGE = `Usage:
22
27
  yarn securessh <host> add <repo-private-key> [repo-url]
23
28
  yarn securessh <host> remove [repo-url]
24
- yarn securessh <host> list`;
29
+ yarn securessh <host> list
30
+ yarn securessh <host> update`;
25
31
 
26
32
  async function pathExists(filePath: string) {
27
33
  try {
@@ -41,7 +47,7 @@ async function runLocal(config: { command: string; args: string[]; cwd?: string;
41
47
  if (result.status !== 0 && !allowFailure) {
42
48
  throw new Error(
43
49
  `Expected ${command} ${args.join(" ")} to exit 0, was ${result.status}. `
44
- + `${(result.stderr || "").slice(0, MAX_ERROR_BODY_LENGTH)}`
50
+ + `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
45
51
  );
46
52
  }
47
53
  return result;
@@ -69,7 +75,7 @@ async function gitWithKey(config: { keyPath: string; args: string[]; cwd?: strin
69
75
  if (result.status !== 0 && !allowFailure) {
70
76
  throw new Error(
71
77
  `Expected git ${args.join(" ")} to exit 0, was ${result.status}. `
72
- + `${(result.stderr || "").slice(0, MAX_ERROR_BODY_LENGTH)}`
78
+ + `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
73
79
  );
74
80
  }
75
81
  return result;
@@ -158,6 +164,101 @@ async function resolveRepoURL(passedURL: string | undefined) {
158
164
  return repoURL;
159
165
  }
160
166
 
167
+ /** Every source's revoke repo has to exist and hold at least one commit, or the hosts using it
168
+ cannot record a revocation. Checked with this machine's own git credentials, since update is
169
+ not given any deploy key, and an empty repo is initialised rather than merely complained about.
170
+
171
+ A host that cannot write a revocation silently keeps accepting a key it just saw being misused,
172
+ which is the one failure this whole thing exists to prevent, so it is checked on every deploy
173
+ and not only when a source is first added. */
174
+ async function ensureRevokeReposExist(repoSources: string[]) {
175
+ for (let repoURL of repoSources) {
176
+ let revokeURL = revokeRepoURL(repoURL);
177
+ let temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "portsecure-revoke-"));
178
+ let checkoutPath = path.join(temporaryDirectory, "repo");
179
+ let clone = await runLocal({ command: "git", args: ["clone", revokeURL, checkoutPath], allowFailure: true });
180
+ if (clone.status !== 0) {
181
+ await fs.rm(temporaryDirectory, { recursive: true, force: true });
182
+ throw new Error(
183
+ `Expected ${revokeURL} to exist, it does not, so ${repoURL} has nowhere to record a`
184
+ + ` revocation.\nCreate it, then run "yarn securessh <host> add" for that source to`
185
+ + ` register its deploy key.\n${(clone.stdout + clone.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
186
+ );
187
+ }
188
+ let head = await runLocal({ command: "git", args: ["-C", checkoutPath, "rev-parse", "HEAD"], allowFailure: true });
189
+ if (head.status !== 0) {
190
+ console.log(`${revokeURL} is empty, giving it a first commit`);
191
+ await fs.writeFile(path.join(checkoutPath, "README.md"),
192
+ `# revoked keys\n\nWritten by portsecure. Each file under revocations/ is one key that was used from an\n`
193
+ + `address it is not allowed from, and is no longer accepted anywhere.\n`);
194
+ for (let args of [
195
+ ["-C", checkoutPath, "add", "-A"],
196
+ ["-C", checkoutPath, "-c", "user.email=portsecure@localhost", "-c", "user.name=portsecure", "commit", "-m", "initialise revoke repo"],
197
+ ["-C", checkoutPath, "push", "origin", "HEAD"],
198
+ ]) {
199
+ await runLocal({ command: "git", args });
200
+ }
201
+ }
202
+ await fs.rm(temporaryDirectory, { recursive: true, force: true });
203
+ console.log(`${revokeURL} is ready`);
204
+ }
205
+ }
206
+
207
+ /** The revoke repo has to exist and be writable before a host is set up, because a host that
208
+ cannot write a revocation cannot revoke a key that is being misused. The key for it is derived
209
+ from the source's, since github will not take one public key on two repos. */
210
+ async function ensureRevokeRepo(config: { keyPath: string; repoURL: string }) {
211
+ let { keyPath, repoURL } = config;
212
+ let revokeURL = revokeRepoURL(repoURL);
213
+ let derived = deriveRevokeKey(await fs.readFile(keyPath, "utf8"));
214
+
215
+ let temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "portsecure-revoke-"));
216
+ let derivedKeyPath = path.join(temporaryDirectory, "key");
217
+ await fs.writeFile(derivedKeyPath, derived.privateKeyFile, { mode: 0o600 });
218
+ let checkoutPath = path.join(temporaryDirectory, "repo");
219
+
220
+ let explain = (problem: string) => new Error(
221
+ `${problem}\n`
222
+ + `Create ${revokeURL} and add this as a deploy key WITH WRITE ACCESS:\n`
223
+ + ` ${derived.publicKey} ${REVOKE_KEY_LABEL}\n`
224
+ + `It has to be this key: it is derived from ${keyPath}, and github will not accept the same`
225
+ + ` public key on two repositories.`
226
+ );
227
+
228
+ console.log(`Checking ${revokeURL}`);
229
+ let clone = await gitWithKey({ keyPath: derivedKeyPath, args: ["clone", revokeURL, checkoutPath], allowFailure: true });
230
+ if (clone.status !== 0) {
231
+ throw explain(`Expected ${revokeURL} to be readable with the derived key, it is not.`);
232
+ }
233
+
234
+ // An empty repo has no branch for the daemon to clone, so it gets its first commit here. That
235
+ // doubles as the proof that we can write to it.
236
+ let head = await gitWithKey({ keyPath: derivedKeyPath, args: ["rev-parse", "HEAD"], cwd: checkoutPath, allowFailure: true });
237
+ if (head.status !== 0) {
238
+ await fs.writeFile(path.join(checkoutPath, "README.md"),
239
+ `# revoked keys\n\nWritten by portsecure. Each file under revocations/ is one key that was used from an\n`
240
+ + `address it is not allowed from, and is no longer accepted anywhere.\n`);
241
+ for (let args of [
242
+ ["add", "-A"],
243
+ ["-c", "user.email=portsecure@localhost", "-c", "user.name=portsecure", "commit", "-m", "initialise revoke repo"],
244
+ ]) {
245
+ await gitWithKey({ keyPath: derivedKeyPath, args, cwd: checkoutPath });
246
+ }
247
+ let push = await gitWithKey({ keyPath: derivedKeyPath, args: ["push", "origin", "HEAD"], cwd: checkoutPath, allowFailure: true });
248
+ if (push.status !== 0) {
249
+ throw explain(`Expected write access to ${revokeURL}, the first push was refused.\n${(push.stdout + push.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`);
250
+ }
251
+ } else {
252
+ let dryRun = await gitWithKey({ keyPath: derivedKeyPath, args: ["push", "--dry-run", "origin", "HEAD"], cwd: checkoutPath, allowFailure: true });
253
+ if (dryRun.status !== 0) {
254
+ throw explain(`Expected write access to ${revokeURL}, a dry run push was refused.\n${(dryRun.stdout + dryRun.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`);
255
+ }
256
+ }
257
+
258
+ await fs.rm(temporaryDirectory, { recursive: true, force: true });
259
+ console.log(`${revokeURL} is writable`);
260
+ }
261
+
161
262
  async function readRemoteConfig(host: string) {
162
263
  let contents = await readRemoteFile({ host, filePath: REMOTE_CONFIG_PATH });
163
264
  if (!contents) {
@@ -187,11 +288,31 @@ fi`,
187
288
 
188
289
  async function installDaemon(config: { host: string; hostLabel: string; repoSources: string[] }) {
189
290
  let { host, hostLabel, repoSources } = config;
190
- for (let command of ["node", "git"]) {
291
+ for (let command of ["node", "git", "yarn"]) {
191
292
  if (!await remoteCommandExists({ host, command })) {
192
293
  throw new Error(`Expected ${command} to be installed on ${host}, it is not. Install it and rerun.`);
193
294
  }
194
295
  }
296
+
297
+ // The checkout is brought to the latest commit rather than a copy being pushed, so what runs on
298
+ // the host is exactly what is on github.
299
+ console.log(`Updating ${REMOTE_CHECKOUT_PATH} on ${host}`);
300
+ await runOverSSH({
301
+ host,
302
+ script: `${SUDO_PREAMBLE}
303
+ set -e
304
+ $SUDO mkdir -p "${path.posix.dirname(REMOTE_CHECKOUT_PATH)}"
305
+ if $SUDO test -d "${REMOTE_CHECKOUT_PATH}/.git"; then
306
+ $SUDO git -C "${REMOTE_CHECKOUT_PATH}" fetch --prune origin
307
+ $SUDO git -C "${REMOTE_CHECKOUT_PATH}" reset --hard origin/HEAD
308
+ else
309
+ $SUDO rm -rf "${REMOTE_CHECKOUT_PATH}"
310
+ $SUDO git clone "${SLIFTUTILS_URL}" "${REMOTE_CHECKOUT_PATH}"
311
+ fi
312
+ $SUDO yarn --cwd "${REMOTE_CHECKOUT_PATH}" install --production --non-interactive
313
+ # The single file daemon this replaced, left over on a host set up by an older version.
314
+ $SUDO rm -f /opt/portsecure/portsecure-daemon.js`,
315
+ });
195
316
  await writeRemoteFile({
196
317
  host,
197
318
  filePath: REMOTE_CONFIG_PATH,
@@ -201,13 +322,6 @@ async function installDaemon(config: { host: string; hostLabel: string; repoSour
201
322
  fileMode: "600",
202
323
  directoryMode: "700",
203
324
  });
204
- await writeRemoteFile({
205
- host,
206
- filePath: REMOTE_DAEMON_PATH,
207
- contents: await fs.readFile(DAEMON_SOURCE, "utf8"),
208
- fileMode: "755",
209
- directoryMode: "755",
210
- });
211
325
  await writeRemoteFile({
212
326
  host,
213
327
  filePath: REMOTE_SERVICE_PATH,
@@ -271,7 +385,8 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
271
385
  if (reachable.status !== 0) {
272
386
  throw new Error(
273
387
  `Expected ${repoURL} to be reachable with ${keyPath}, git ls-remote failed.`
274
- + ` The daemon would have no way to fetch keys.\n${reachable.stderr.slice(0, MAX_ERROR_BODY_LENGTH)}`
388
+ + ` The daemon would have no way to fetch keys.\n`
389
+ + `${(reachable.stdout + reachable.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
275
390
  );
276
391
  }
277
392
 
@@ -299,6 +414,19 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
299
414
  }
300
415
  console.log(`Our key ${ourFingerprint} is in the merged keys, access will survive.`);
301
416
 
417
+ await ensureRevokeRepo({ keyPath, repoURL });
418
+
419
+ // Deploying a repo that still holds a revoked key would hand it back to every machine.
420
+ let revoked = await revokedKeysInRepo({ repoPath: inspectionPath, sourceURL: repoURL });
421
+ if (revoked.length) {
422
+ throw new Error(
423
+ `Expected ${repoURL} to hold no revoked keys, it holds ${revoked.length}:\n`
424
+ + revoked.map(entry => ` ${entry.revocation.fingerprint} revoked by`
425
+ + ` ${entry.revocation.revokedBy || "?"} after use from ${entry.revocation.attempt?.ip || "?"}`).join("\n")
426
+ + `\nDelete them from the repo, or run "yarn unrevoke" there to allow them again.`
427
+ );
428
+ }
429
+
302
430
  let webhookURL = await requireRemoteWebhook(host);
303
431
  console.log(`${host} notifies ${webhookURL}`);
304
432
 
@@ -362,6 +490,57 @@ $SUDO rm -rf "${sourceRepoPath(repoURL)}"`,
362
490
 
363
491
  /** Answers "who can log into this box, and which repo says so". The paths the daemon uses are
364
492
  left out on purpose, they are plumbing rather than something to act on. */
493
+ /** The daemon is installed from this checkout, so the checkout is brought up to date first.
494
+ Anything that stops the pull - a conflict, local commits, no upstream - stops the update too,
495
+ rather than quietly installing whatever happened to be on disk. */
496
+ async function pullLocalCheckout() {
497
+ let topLevel = await spawnPromise({ command: "git", args: ["rev-parse", "--show-toplevel"], cwd: __dirname });
498
+ if (topLevel.status !== 0) {
499
+ throw new Error(
500
+ `Expected ${__dirname} to be inside a git checkout, it is not.`
501
+ + ` update installs the daemon from this checkout, so it has to be one.`
502
+ );
503
+ }
504
+ let repoPath = topLevel.stdout.trim();
505
+ let pull = await spawnPromise({ command: "git", args: ["pull", "--ff-only"], cwd: repoPath });
506
+ if (pull.status !== 0) {
507
+ throw new Error(
508
+ `Expected git pull in ${repoPath} to succeed, it did not, so nothing was installed.\n`
509
+ + `${(pull.stdout + pull.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
510
+ );
511
+ }
512
+ // The host installs from github, not from here, so local commits that have not been pushed
513
+ // are not what it will run.
514
+ let local = await spawnPromise({ command: "git", args: ["rev-parse", "HEAD"], cwd: repoPath });
515
+ let remote = await spawnPromise({ command: "git", args: ["rev-parse", "@{u}"], cwd: repoPath });
516
+ if (local.stdout.trim() !== remote.stdout.trim()) {
517
+ console.log(`WARNING: ${repoPath} has commits that are not pushed. The host installs from github,`);
518
+ console.log(` so it will run the pushed version, not what is here.`);
519
+ }
520
+ console.log(`Pulled ${repoPath}`);
521
+ }
522
+
523
+ /** Pushes the current daemon onto a host that already has one, for when this code has moved on.
524
+ Nothing about which keys the host trusts is touched. */
525
+ async function updateDaemon(host: string) {
526
+ // Pulled before anything else, so a checkout that cannot be brought up to date fails here
527
+ // rather than after we have already started changing the host.
528
+ await pullLocalCheckout();
529
+ let contents = await readRemoteFile({ host, filePath: REMOTE_CONFIG_PATH });
530
+ if (!contents) {
531
+ throw new Error(
532
+ `Expected ${host} to already have portsecure, ${REMOTE_CONFIG_PATH} does not exist.\n`
533
+ + `Set it up with:\n yarn securessh ${host} add <repo-private-key> [repo-url]`
534
+ );
535
+ }
536
+ let parsed = JSON.parse(contents) as { hostLabel?: string; repoSources?: string[] };
537
+ let repoSources = parsed.repoSources || [];
538
+ await requireRemoteWebhook(host);
539
+ await ensureRevokeReposExist(repoSources);
540
+ await installDaemon({ host, hostLabel: parsed.hostLabel || host, repoSources });
541
+ console.log(`Updated the daemon on ${host}. ${repoSources.length} source(s), unchanged.`);
542
+ }
543
+
365
544
  async function listSources(host: string) {
366
545
  let remoteConfig = await readRemoteConfig(host);
367
546
  if (!remoteConfig.repoSources.length) {
@@ -413,6 +592,13 @@ export async function main() {
413
592
  await listSources(host);
414
593
  return;
415
594
  }
595
+ if (verb === "update") {
596
+ if (rest.length) {
597
+ throw new Error(`Expected nothing after update, was ${rest.length} argument(s)\n${USAGE}`);
598
+ }
599
+ await updateDaemon(host);
600
+ return;
601
+ }
416
602
  if (verb === "add") {
417
603
  if (!rest.length || rest.length > 2) {
418
604
  throw new Error(`Expected a private key and optionally a repo url, was ${rest.length} argument(s)\n${USAGE}`);