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.
- package/bin/derivekey.js +10 -0
- package/bin/portsecuredaemon.js +13 -0
- package/bin/unrevoke.js +9 -0
- package/package.json +8 -3
- package/security/README.md +66 -2
- package/security/authorizedKeys/authorizedKeys.ts +23 -0
- package/security/authorizedKeys/daemon/authLog.ts +117 -0
- package/security/authorizedKeys/daemon/daemon.ts +308 -0
- package/security/authorizedKeys/daemon/git.ts +119 -0
- package/security/authorizedKeys/daemon/notify.ts +25 -0
- package/security/authorizedKeys/daemon/paths.ts +26 -0
- package/security/authorizedKeys/daemon/portsecure.service +3 -2
- package/security/authorizedKeys/daemon/revocation.ts +306 -0
- package/security/authorizedKeys/daemon/rootKeys.ts +135 -0
- package/security/authorizedKeys/daemon/sshdConfig.ts +85 -0
- package/security/authorizedKeys/daemon/state.ts +108 -0
- package/security/authorizedKeys/daemon/trust.ts +291 -0
- package/security/authorizedKeys/daemon/userKeys.ts +76 -0
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +73 -0
- package/security/authorizedKeys/dist/revokeSource.ts.cache +44 -0
- package/security/authorizedKeys/dist/secureSSH.ts.cache +552 -0
- package/security/authorizedKeys/dist/sources.ts.cache +24 -0
- package/security/authorizedKeys/dist/unrevoke.ts.cache +145 -0
- package/security/authorizedKeys/revokeSource.ts +40 -0
- package/security/authorizedKeys/secureSSH.ts +201 -15
- package/security/authorizedKeys/unrevoke.ts +149 -0
- package/security/helpers/dist/paths.ts.cache +28 -0
- package/security/helpers/dist/remoteSSH.ts.cache +90 -0
- package/security/helpers/dist/spawn.ts.cache +34 -0
- package/security/helpers/remoteSSH.ts +3 -2
- package/security/helpers/spawn.ts +5 -1
- package/security/keys/deriveKey.ts +72 -0
- package/security/keys/dist/deriveKey.ts.cache +72 -0
- package/security/keys/dist/sshKeyFile.ts.cache +153 -0
- package/security/keys/sshKeyFile.ts +156 -0
- package/security/notifications/dist/discord.ts.cache +180 -0
- package/security/signedFiles/dist/manifest.ts.cache +68 -0
- package/security/signedFiles/dist/signFiles.ts.cache +146 -0
- package/security/signedFiles/manifest.ts +19 -6
- package/security/signedFiles/signFiles.ts +78 -43
- 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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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}`);
|