sliftutils 1.7.125 → 1.7.127

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 (47) hide show
  1. package/bin/derivekey.js +10 -0
  2. package/bin/portsecuredaemon.js +13 -0
  3. package/bin/testnotify.js +9 -0
  4. package/bin/unrevoke.js +9 -0
  5. package/package.json +10 -3
  6. package/security/README.md +66 -2
  7. package/security/authorizedKeys/authorizedKeys.ts +68 -5
  8. package/security/authorizedKeys/daemon/authLog.ts +186 -0
  9. package/security/authorizedKeys/daemon/changes.ts +26 -0
  10. package/security/authorizedKeys/daemon/daemon.ts +335 -0
  11. package/security/authorizedKeys/daemon/git.ts +119 -0
  12. package/security/authorizedKeys/daemon/notify.ts +25 -0
  13. package/security/authorizedKeys/daemon/paths.ts +26 -0
  14. package/security/authorizedKeys/daemon/portsecure.service +3 -2
  15. package/security/authorizedKeys/daemon/revocation.ts +329 -0
  16. package/security/authorizedKeys/daemon/rootKeys.ts +198 -0
  17. package/security/authorizedKeys/daemon/sessions.ts +139 -0
  18. package/security/authorizedKeys/daemon/sshdConfig.ts +85 -0
  19. package/security/authorizedKeys/daemon/state.ts +122 -0
  20. package/security/authorizedKeys/daemon/trust.ts +291 -0
  21. package/security/authorizedKeys/daemon/userKeys.ts +76 -0
  22. package/security/authorizedKeys/dist/authorizedKeys.ts.cache +111 -0
  23. package/security/authorizedKeys/dist/revokeSource.ts.cache +44 -0
  24. package/security/authorizedKeys/dist/secureSSH.ts.cache +539 -0
  25. package/security/authorizedKeys/dist/sources.ts.cache +24 -0
  26. package/security/authorizedKeys/dist/unrevoke.ts.cache +167 -0
  27. package/security/authorizedKeys/revokeSource.ts +40 -0
  28. package/security/authorizedKeys/secureSSH.ts +240 -38
  29. package/security/authorizedKeys/unrevoke.ts +175 -0
  30. package/security/helpers/dist/paths.ts.cache +28 -0
  31. package/security/helpers/dist/remoteSSH.ts.cache +90 -0
  32. package/security/helpers/dist/spawn.ts.cache +34 -0
  33. package/security/helpers/remoteSSH.ts +18 -6
  34. package/security/helpers/spawn.ts +5 -1
  35. package/security/keys/deriveKey.ts +72 -0
  36. package/security/keys/dist/deriveKey.ts.cache +72 -0
  37. package/security/keys/dist/sshKeyFile.ts.cache +153 -0
  38. package/security/keys/sshKeyFile.ts +156 -0
  39. package/security/notifications/discord.ts +18 -1
  40. package/security/notifications/dist/discord.ts.cache +197 -0
  41. package/security/notifications/setupNotify.ts +17 -8
  42. package/security/notifications/testNotify.ts +48 -0
  43. package/security/signedFiles/dist/manifest.ts.cache +85 -0
  44. package/security/signedFiles/dist/signFiles.ts.cache +181 -0
  45. package/security/signedFiles/manifest.ts +38 -8
  46. package/security/signedFiles/signFiles.ts +123 -49
  47. package/security/authorizedKeys/daemon/portsecureDaemon.js +0 -1032
@@ -0,0 +1,181 @@
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.signRepo = 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 paths_1 = require("../helpers/paths");
12
+ const spawn_1 = require("../helpers/spawn");
13
+ const manifest_1 = require("./manifest");
14
+ const unrevoke_1 = require("../authorizedKeys/unrevoke");
15
+ const authorizedKeys_1 = require("../authorizedKeys/authorizedKeys");
16
+ // A hardware backed key is the entire point. A key sitting on disk is compromised the moment the
17
+ // machine is, and then the signature proves nothing, so this is what we make when asked to make one.
18
+ const DEFAULT_KEY_TYPE = "ed25519-sk";
19
+ const DEFAULT_KEY_PATH = "~/.ssh/signfiles_ed25519_sk";
20
+ const GIT_KEYWORD = "git";
21
+ const COMMIT_MESSAGE = "deploying signed files";
22
+ const MAX_ERROR_BODY_LENGTH = 500;
23
+ const USAGE = `Usage: yarn signfiles [signing-key] [${GIT_KEYWORD}]
24
+
25
+ Signs the files of the repo in the current directory. With no key, a hardware backed
26
+ ${DEFAULT_KEY_TYPE} key at ${DEFAULT_KEY_PATH} is used, and created if it does not exist.
27
+ Pass ${GIT_KEYWORD} to also commit and push the result.`;
28
+ /** runPromise takes a command line rather than an argument list, so anything holding a path has to
29
+ survive the shell. Double quotes work on both cmd and posix shells. */
30
+ function quote(value) {
31
+ return `"${value}"`;
32
+ }
33
+ async function pathExists(filePath) {
34
+ try {
35
+ await promises_1.default.access(filePath);
36
+ return true;
37
+ }
38
+ catch (e) {
39
+ return false;
40
+ }
41
+ }
42
+ /** Creating this needs the security key plugged in, and a touch. */
43
+ async function ensureDefaultKey() {
44
+ let keyPath = (0, paths_1.expandHome)(DEFAULT_KEY_PATH);
45
+ if (await pathExists(keyPath)) {
46
+ return keyPath;
47
+ }
48
+ console.log(`No signing key at ${keyPath}, creating an ${DEFAULT_KEY_TYPE} one.`);
49
+ console.log(`Plug your security key in - you will be asked to touch it.`);
50
+ await promises_1.default.mkdir(path_1.default.dirname(keyPath), { recursive: true, mode: 0o700 });
51
+ await (0, runPromise_1.runPromise)(`ssh-keygen -t ${DEFAULT_KEY_TYPE} -f ${quote(keyPath)} -N "" -C signfiles`);
52
+ return keyPath;
53
+ }
54
+ /** The public key in the form the daemon reports it, so what is printed here can be compared
55
+ against what arrives on Discord. */
56
+ async function publicKeyOf(keyPath) {
57
+ let contents = await promises_1.default.readFile(`${keyPath}.pub`, "utf8");
58
+ let [keyType, keyBody] = contents.trim().split(/\s+/);
59
+ if (!keyType || !keyBody) {
60
+ throw new Error(`Expected a public key in ${keyPath}.pub, was ${contents.slice(0, MAX_ERROR_BODY_LENGTH)}`);
61
+ }
62
+ return `${keyType} ${keyBody}`;
63
+ }
64
+ /** A keys repo has rules the rest of this cannot enforce afterwards, so they are enforced before it
65
+ is signed. Signing anything that is not a keys repo is none of this function's business. */
66
+ async function refuseUnusableKeys(repoPath) {
67
+ let authorizedPath = path_1.default.join(repoPath, "authorized_keys");
68
+ if (!await pathExists(authorizedPath)) {
69
+ return;
70
+ }
71
+ let problems = (0, authorizedKeys_1.findKeyProblems)((0, authorizedKeys_1.normalizeKeys)(await promises_1.default.readFile(authorizedPath, "utf8")));
72
+ if (!problems.length) {
73
+ return;
74
+ }
75
+ throw new Error(`Expected every key in ${authorizedPath} to be restricted and to belong to one person,`
76
+ + ` found ${problems.length} problem(s):\n`
77
+ + problems.map(problem => ` - ${problem}`).join("\n")
78
+ + `\n\nEvery key needs a from= naming the addresses it may be used from, and no two keys may`
79
+ + `\nname the same ones. Revoking a key that shares its addresses with another leaves that`
80
+ + `\nother key working, which defeats the point of revoking it.`);
81
+ }
82
+ /** Signing a keys repo that still holds a revoked key would publish it back to every machine that
83
+ took it out. Only applies to a repo that holds keys and has a revoke repo to check - signing
84
+ anything else is none of this function's business. */
85
+ async function refuseRevokedKeys(repoPath) {
86
+ if (!await pathExists(path_1.default.join(repoPath, "authorized_keys"))) {
87
+ return;
88
+ }
89
+ // Read for its value, so stdout has to be on its own. runPromise joins it with stderr.
90
+ let origin = await (0, spawn_1.spawnPromise)({ command: "git", args: ["remote", "get-url", "origin"], cwd: repoPath });
91
+ let originURL = origin.stdout.trim();
92
+ if (origin.status !== 0 || !originURL) {
93
+ return;
94
+ }
95
+ let revoked;
96
+ try {
97
+ revoked = await (0, unrevoke_1.revokedKeysInRepo)({ repoPath, sourceURL: originURL });
98
+ }
99
+ catch (e) {
100
+ // No revoke repo, or no access to it. Nothing has been revoked that we can see.
101
+ return;
102
+ }
103
+ if (!revoked.length) {
104
+ return;
105
+ }
106
+ throw new Error(`Expected ${repoPath} to hold no revoked keys, it holds ${revoked.length}:\n`
107
+ + revoked.map(entry => {
108
+ var _a;
109
+ return ` ${entry.revocation.fingerprint} revoked by ${entry.revocation.revokedBy || "?"}`
110
+ + ` after use from ${((_a = entry.revocation.attempt) === null || _a === void 0 ? void 0 : _a.ip) || "?"}`;
111
+ }).join("\n")
112
+ + `\nDelete them from authorized_keys, or run "yarn unrevoke" here to allow them again.`);
113
+ }
114
+ function parseArgs(argv) {
115
+ let pushToGit = argv.includes(GIT_KEYWORD);
116
+ let positional = argv.filter(arg => arg !== GIT_KEYWORD);
117
+ if (positional.length > 1) {
118
+ throw new Error(`Expected at most a signing key, was ${positional.length} argument(s): ${positional.join(" ")}\n${USAGE}`);
119
+ }
120
+ return { keyPath: positional[0], pushToGit };
121
+ }
122
+ /** Writes the manifest and its signature into a repo. Separate from the command so anything that
123
+ changes a keys repo can leave it signed, rather than telling someone to go and do it. */
124
+ async function signRepo(config) {
125
+ let { repoPath } = config;
126
+ let signingKey = config.keyPath && (0, paths_1.expandHome)(config.keyPath) || await ensureDefaultKey();
127
+ if (!await pathExists(signingKey)) {
128
+ throw new Error(`Expected a signing key at ${signingKey}, no such file exists`);
129
+ }
130
+ await refuseUnusableKeys(repoPath);
131
+ await refuseRevokedKeys(repoPath);
132
+ let manifest = await (0, manifest_1.buildManifest)(repoPath);
133
+ console.log(`${manifest_1.MANIFEST_NAME} covers ${manifest.files.length} file(s) in ${repoPath}`);
134
+ // The manifest is built and signed away from the repo, and only moved in once both exist.
135
+ // Landing a new manifest next to an old signature produces a pair that can never verify, and
136
+ // the daemon reading it can only treat that as tampering.
137
+ let stagingDirectory = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "signfiles-"));
138
+ let stagedManifest = path_1.default.join(stagingDirectory, manifest_1.MANIFEST_NAME);
139
+ let stagedSignature = `${stagedManifest}.sig`;
140
+ await promises_1.default.writeFile(stagedManifest, (0, manifest_1.formatManifest)(manifest));
141
+ // Signing happens before any git work, so a failed push never costs a second touch of the key.
142
+ await (0, runPromise_1.runPromise)(`ssh-keygen -Y sign -f ${quote(signingKey)} -n ${manifest_1.SIGN_NAMESPACE} ${quote(stagedManifest)}`);
143
+ // Checked here rather than left for a machine that has already pulled it to discover.
144
+ await (0, runPromise_1.runPromise)(`ssh-keygen -Y check-novalidate -n ${manifest_1.SIGN_NAMESPACE} -s ${quote(stagedSignature)} < ${quote(stagedManifest)}`);
145
+ await promises_1.default.copyFile(stagedManifest, path_1.default.join(repoPath, manifest_1.MANIFEST_NAME));
146
+ await promises_1.default.copyFile(stagedSignature, path_1.default.join(repoPath, manifest_1.SIGNATURE_NAME));
147
+ await promises_1.default.rm(stagingDirectory, { recursive: true, force: true });
148
+ console.log(`Signed with ${await publicKeyOf(signingKey)}`);
149
+ }
150
+ exports.signRepo = signRepo;
151
+ async function main() {
152
+ let { keyPath, pushToGit } = parseArgs(process.argv.slice(2));
153
+ // Read for its value, so stdout has to be on its own. runPromise joins it with stderr, and a
154
+ // git warning glued to the front of this becomes a directory that does not exist.
155
+ let topLevel = await (0, spawn_1.spawnPromise)({ command: "git", args: ["rev-parse", "--show-toplevel"] });
156
+ let repoPath = topLevel.stdout.trim();
157
+ if (topLevel.status !== 0 || !repoPath) {
158
+ throw new Error(`Expected the current directory to be inside a git repo, it is not.\n`
159
+ + `${(topLevel.stdout + topLevel.stderr).trim()}\n${USAGE}`);
160
+ }
161
+ await signRepo({ repoPath, keyPath });
162
+ if (!pushToGit) {
163
+ console.log(`Commit and push ${manifest_1.MANIFEST_NAME} and ${manifest_1.SIGNATURE_NAME} for anything to see them.`);
164
+ return;
165
+ }
166
+ await (0, runPromise_1.runPromise)(`git add -A`, { cwd: repoPath });
167
+ // Nothing staged is not worth stopping on. git commit calls that a failure, but it only means
168
+ // the signature matches the one already committed, so there is nothing to deploy.
169
+ let status = await (0, spawn_1.spawnPromise)({ command: "git", args: ["status", "--porcelain"], cwd: repoPath });
170
+ let staged = status.stdout;
171
+ if (!staged.trim()) {
172
+ console.log(`Nothing changed, ${manifest_1.MANIFEST_NAME} and ${manifest_1.SIGNATURE_NAME} are already committed.`);
173
+ return;
174
+ }
175
+ await (0, runPromise_1.runPromise)(`git commit -m ${quote(COMMIT_MESSAGE)}`, { cwd: repoPath });
176
+ await (0, runPromise_1.runPromise)(`git push`, { cwd: repoPath });
177
+ console.log(`Committed and pushed.`);
178
+ }
179
+ exports.main = main;
180
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2lnbkZpbGVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsic2lnbkZpbGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQUFBLDJEQUE2QjtBQUM3Qiw0Q0FBb0I7QUFDcEIsZ0RBQXdCO0FBQ3hCLCtEQUE0RDtBQUM1RCw0Q0FBOEM7QUFDOUMsNENBQWdEO0FBQ2hELHlDQUEwRztBQUMxRyx5REFBK0Q7QUFDL0QscUVBQWtGO0FBRWxGLGlHQUFpRztBQUNqRyxxR0FBcUc7QUFDckcsTUFBTSxnQkFBZ0IsR0FBRyxZQUFZLENBQUM7QUFDdEMsTUFBTSxnQkFBZ0IsR0FBRyw2QkFBNkIsQ0FBQztBQUN2RCxNQUFNLFdBQVcsR0FBRyxLQUFLLENBQUM7QUFDMUIsTUFBTSxjQUFjLEdBQUcsd0JBQXdCLENBQUM7QUFDaEQsTUFBTSxxQkFBcUIsR0FBRyxHQUFHLENBQUM7QUFDbEMsTUFBTSxLQUFLLEdBQUcsd0NBQXdDLFdBQVc7OztFQUcvRCxnQkFBZ0IsV0FBVyxnQkFBZ0I7T0FDdEMsV0FBVyxzQ0FBc0MsQ0FBQztBQUV6RDswRUFDMEU7QUFDMUUsU0FBUyxLQUFLLENBQUMsS0FBYTtJQUN4QixPQUFPLElBQUksS0FBSyxHQUFHLENBQUM7QUFDeEIsQ0FBQztBQUVELEtBQUssVUFBVSxVQUFVLENBQUMsUUFBZ0I7SUFDdEMsSUFBSSxDQUFDO1FBQ0QsTUFBTSxrQkFBRSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUMxQixPQUFPLElBQUksQ0FBQztJQUNoQixDQUFDO0lBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztRQUNULE9BQU8sS0FBSyxDQUFDO0lBQ2pCLENBQUM7QUFDTCxDQUFDO0FBRUQsb0VBQW9FO0FBQ3BFLEtBQUssVUFBVSxnQkFBZ0I7SUFDM0IsSUFBSSxPQUFPLEdBQUcsSUFBQSxrQkFBVSxFQUFDLGdCQUFnQixDQUFDLENBQUM7SUFDM0MsSUFBSSxNQUFNLFVBQVUsQ0FBQyxPQUFPLENBQUMsRUFBRSxD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QXlCLENBQUMsQ0FBQztRQUM5RixPQUFPO0lBQ1gsQ0FBQztJQUNELE1BQU0sSUFBQSx1QkFBVSxFQUFDLGlCQUFpQixLQUFLLENBQUMsY0FBYyxDQUFDLEVBQUUsRUFBRSxFQUFFLEdBQUcsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQzlFLE1BQU0sSUFBQSx1QkFBVSxFQUFDLFVBQVUsRUFBRSxFQUFFLEdBQUcsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQ2hELE9BQU8sQ0FBQyxHQUFHLENBQUMsdUJBQXVCLENBQUMsQ0FBQztBQUN6QyxDQUFDO0FBL0JELG9CQStCQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnMvcHJvbWlzZXNcIjtcbmltcG9ydCBvcyBmcm9tIFwib3NcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5pbXBvcnQgeyBydW5Qcm9taXNlIH0gZnJvbSBcInNvY2tldC1mdW5jdGlvbi9zcmMvcnVuUHJvbWlzZVwiO1xuaW1wb3J0IHsgZXhwYW5kSG9tZSB9IGZyb20gXCIuLi9oZWxwZXJzL3BhdGhzXCI7XG5pbXBvcnQgeyBzcGF3blByb21pc2UgfSBmcm9tIFwiLi4vaGVscGVycy9zcGF3blwiO1xuaW1wb3J0IHsgYnVpbGRNYW5pZmVzdCwgZm9ybWF0TWFuaWZlc3QsIE1BTklGRVNUX05BTUUsIFNJR05BVFVSRV9OQU1FLCBTSUdOX05BTUVTUEFDRSB9IGZyb20gXCIuL21hbmlmZXN0XCI7XG5pbXBvcnQgeyByZXZva2VkS2V5c0luUmVwbyB9IGZyb20gXCIuLi9hdXRob3JpemVkS2V5cy91bnJldm9rZVwiO1xuaW1wb3J0IHsgZmluZEtleVByb2JsZW1zLCBub3JtYWxpemVLZXlzIH0gZnJvbSBcIi4uL2F1dGhvcml6ZWRLZXlzL2F1dGhvcml6ZWRLZXlzXCI7XG5cbi8vIEEgaGFyZHdhcmUgYmFja2VkIGtleSBpcyB0aGUgZW50aXJlIHBvaW50LiBBIGtleSBzaXR0aW5nIG9uIGRpc2sgaXMgY29tcHJvbWlzZWQgdGhlIG1vbWVudCB0aGVcbi8vIG1hY2hpbmUgaXMsIGFuZCB0aGVuIHRoZSBzaWduYXR1cmUgcHJvdmVzIG5vdGhpbmcsIHNvIHRoaXMgaXMgd2hhdCB3ZSBtYWtlIHdoZW4gYXNrZWQgdG8gbWFrZSBvbmUuXG5jb25zdCBERUZBVUxUX0tFWV9UWVBFID0gXCJlZDI1NTE5LXNrXCI7XG5jb25zdCBERUZBVUxUX0tFWV9QQVRIID0gXCJ+Ly5zc2gvc2lnbmZpbGVzX2VkMjU1MTlfc2tcIjtcbmNvbnN0IEdJVF9LRVlXT1JEID0gXCJnaXRcIjtcbmNvbnN0IENPTU1JVF9NRVNTQUdFID0gXCJkZXBsb3lpbmcgc2lnbmVkIGZpbGVzXCI7XG5jb25zdCBNQVhfRVJST1JfQk9EWV9MRU5HVEggPSA1MDA7XG5jb25zdCBVU0FHRSA9IGBVc2FnZTogeWFybiBzaWduZmlsZXMgW3NpZ25pbmcta2V5XSBbJHtHSVRfS0VZV09SRH1dXG5cblNpZ25zIHRoZSBmaWxlcyBvZiB0aGUgcmVwbyBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkuIFdpdGggbm8ga2V5LCBhIGhhcmR3YXJlIGJhY2tlZFxuJHtERUZBVUxUX0tFWV9UWVBFfSBrZXkgYXQgJHtERUZBVUxUX0tFWV9QQVRIfSBpcyB1c2VkLCBhbmQgY3JlYXRlZCBpZiBpdCBkb2VzIG5vdCBleGlzdC5cblBhc3MgJHtHSVRfS0VZV09SRH0gdG8gYWxzbyBjb21taXQgYW5kIHB1c2ggdGhlIHJlc3VsdC5gO1xuXG4vKiogcnVuUHJvbWlzZSB0YWtlcyBhIGNvbW1hbmQgbGluZSByYXRoZXIgdGhhbiBhbiBhcmd1bWVudCBsaXN0LCBzbyBhbnl0aGluZyBob2xkaW5nIGEgcGF0aCBoYXMgdG9cbiAgICBzdXJ2aXZlIHRoZSBzaGVsbC4gRG91YmxlIHF1b3RlcyB3b3JrIG9uIGJvdGggY21kIGFuZCBwb3NpeCBzaGVsbHMuICovXG5mdW5jdGlvbiBxdW90ZSh2YWx1ZTogc3RyaW5nKSB7XG4gICAgcmV0dXJuIGBcIiR7dmFsdWV9XCJgO1xufVxuXG5hc3luYyBmdW5jdGlvbiBwYXRoRXhpc3RzKGZpbGVQYXRoOiBzdHJpbmcpIHtcbiAgICB0cnkge1xuICAgICAgICBhd2FpdCBmcy5hY2Nlc3MoZmlsZVBhdGgpO1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG59XG5cbi8qKiBDcmVhdGluZyB0aGlzIG5lZWRzIHRoZSBzZWN1cml0eSBrZXkgcGx1Z2dlZCBpbiwgYW5kIGEgdG91Y2guICovXG5hc3luYyBmdW5jdGlvbiBlbnN1cmVEZWZhdWx0S2V5KCkge1xuICAgIGxldCBrZXlQYXRoID0gZXhwYW5kSG9tZShERUZBVUxUX0tFWV9QQVRIKTtcbiAgICBpZiAoYXdhaXQgcGF0aEV4aXN0cyhrZXlQYXRoKSkge1xuICAgICAgICByZXR1cm4ga2V5UGF0aDtcbiAgICB9XG4gICAgY29uc29sZS5sb2coYE5vIHNpZ25pbmcga2V5IGF0ICR7a2V5UGF0aH0sIGNyZWF0aW5nIGFuICR7REVGQVVMVF9LRVlfVFlQRX0gb25lLmApO1xuICAgIGNvbnNvbGUubG9nKGBQbHVnIHlvdXIgc2VjdXJpdHkga2V5IGluIC0geW91IHdpbGwgYmUgYXNrZWQgdG8gdG91Y2ggaXQuYCk7XG4gICAgYXdhaXQgZnMubWtkaXIocGF0aC5kaXJuYW1lKGtleVBhdGgpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgbW9kZTogMG83MDAgfSk7XG4gICAgYXdhaXQgcnVuUHJvbWlzZShgc3NoLWtleWdlbiAtdCAke0RFRkFVTFRfS0VZX1RZUEV9IC1mICR7cXVvdGUoa2V5UGF0aCl9IC1OIFwiXCIgLUMgc2lnbmZpbGVzYCk7XG4gICAgcmV0dXJuIGtleVBhdGg7XG59XG5cbi8qKiBUaGUgcHVibGljIGtleSBpbiB0aGUgZm9ybSB0aGUgZGFlbW9uIHJlcG9ydHMgaXQsIHNvIHdoYXQgaXMgcHJpbnRlZCBoZXJlIGNhbiBiZSBjb21wYXJlZFxuICAgIGFnYWluc3Qgd2hhdCBhcnJpdmVzIG9uIERpc2NvcmQuICovXG5hc3luYyBmdW5jdGlvbiBwdWJsaWNLZXlPZihrZXlQYXRoOiBzdHJpbmcpIHtcbiAgICBsZXQgY29udGVudHMgPSBhd2FpdCBmcy5yZWFkRmlsZShgJHtrZXlQYXRofS5wdWJgLCBcInV0ZjhcIik7XG4gICAgbGV0IFtrZXlUeXBlLCBrZXlCb2R5XSA9IGNvbnRlbnRzLnRyaW0oKS5zcGxpdCgvXFxzKy8pO1xuICAgIGlmICgha2V5VHlwZSB8fCAha2V5Qm9keSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGEgcHVibGljIGtleSBpbiAke2tleVBhdGh9LnB1Yiwgd2FzICR7Y29udGVudHMuc2xpY2UoMCwgTUFYX0VSUk9SX0JPRFlfTEVOR1RIKX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIGAke2tleVR5cGV9ICR7a2V5Qm9keX1gO1xufVxuXG4vKiogQSBrZXlzIHJlcG8gaGFzIHJ1bGVzIHRoZSByZXN0IG9mIHRoaXMgY2Fubm90IGVuZm9yY2UgYWZ0ZXJ3YXJkcywgc28gdGhleSBhcmUgZW5mb3JjZWQgYmVmb3JlIGl0XG4gICAgaXMgc2lnbmVkLiBTaWduaW5nIGFueXRoaW5nIHRoYXQgaXMgbm90IGEga2V5cyByZXBvIGlzIG5vbmUgb2YgdGhpcyBmdW5jdGlvbidzIGJ1c2luZXNzLiAqL1xuYXN5bmMgZnVuY3Rpb24gcmVmdXNlVW51c2FibGVLZXlzKHJlcG9QYXRoOiBzdHJpbmcpIHtcbiAgICBsZXQgYXV0aG9yaXplZFBhdGggPSBwYXRoLmpvaW4ocmVwb1BhdGgsIFwiYXV0aG9yaXplZF9rZXlzXCIpO1xuICAgIGlmICghYXdhaXQgcGF0aEV4aXN0cyhhdXRob3JpemVkUGF0aCkpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBsZXQgcHJvYmxlbXMgPSBmaW5kS2V5UHJvYmxlbXMobm9ybWFsaXplS2V5cyhhd2FpdCBmcy5yZWFkRmlsZShhdXRob3JpemVkUGF0aCwgXCJ1dGY4XCIpKSk7XG4gICAgaWYgKCFwcm9ibGVtcy5sZW5ndGgpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgIGBFeHBlY3RlZCBldmVyeSBrZXkgaW4gJHthdXRob3JpemVkUGF0aH0gdG8gYmUgcmVzdHJpY3RlZCBhbmQgdG8gYmVsb25nIHRvIG9uZSBwZXJzb24sYFxuICAgICAgICArIGAgZm91bmQgJHtwcm9ibGVtcy5sZW5ndGh9IHByb2JsZW0ocyk6XFxuYFxuICAgICAgICArIHByb2JsZW1zLm1hcChwcm9ibGVtID0+IGAgIC0gJHtwcm9ibGVtfWApLmpvaW4oXCJcXG5cIilcbiAgICAgICAgKyBgXFxuXFxuRXZlcnkga2V5IG5lZWRzIGEgZnJvbT0gbmFtaW5nIHRoZSBhZGRyZXNzZXMgaXQgbWF5IGJlIHVzZWQgZnJvbSwgYW5kIG5vIHR3byBrZXlzIG1heWBcbiAgICAgICAgKyBgXFxubmFtZSB0aGUgc2FtZSBvbmVzLiBSZXZva2luZyBhIGtleSB0aGF0IHNoYXJlcyBpdHMgYWRkcmVzc2VzIHdpdGggYW5vdGhlciBsZWF2ZXMgdGhhdGBcbiAgICAgICAgKyBgXFxub3RoZXIga2V5IHdvcmtpbmcsIHdoaWNoIGRlZmVhdHMgdGhlIHBvaW50IG9mIHJldm9raW5nIGl0LmBcbiAgICApO1xufVxuXG4vKiogU2lnbmluZyBhIGtleXMgcmVwbyB0aGF0IHN0aWxsIGhvbGRzIGEgcmV2b2tlZCBrZXkgd291bGQgcHVibGlzaCBpdCBiYWNrIHRvIGV2ZXJ5IG1hY2hpbmUgdGhhdFxuICAgIHRvb2sgaXQgb3V0LiBPbmx5IGFwcGxpZXMgdG8gYSByZXBvIHRoYXQgaG9sZHMga2V5cyBhbmQgaGFzIGEgcmV2b2tlIHJlcG8gdG8gY2hlY2sgLSBzaWduaW5nXG4gICAgYW55dGhpbmcgZWxzZSBpcyBub25lIG9mIHRoaXMgZnVuY3Rpb24ncyBidXNpbmVzcy4gKi9cbmFzeW5jIGZ1bmN0aW9uIHJlZnVzZVJldm9rZWRLZXlzKHJlcG9QYXRoOiBzdHJpbmcpIHtcbiAgICBpZiAoIWF3YWl0IHBhdGhFeGlzdHMocGF0aC5qb2luKHJlcG9QYXRoLCBcImF1dGhvcml6ZWRfa2V5c1wiKSkpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICAvLyBSZWFkIGZvciBpdHMgdmFsdWUsIHNvIHN0ZG91dCBoYXMgdG8gYmUgb24gaXRzIG93bi4gcnVuUHJvbWlzZSBqb2lucyBpdCB3aXRoIHN0ZGVyci5cbiAgICBsZXQgb3JpZ2luID0gYXdhaXQgc3Bhd25Qcm9taXNlKHsgY29tbWFuZDogXCJnaXRcIiwgYXJnczogW1wicmVtb3RlXCIsIFwiZ2V0LXVybFwiLCBcIm9yaWdpblwiXSwgY3dkOiByZXBvUGF0aCB9KTtcbiAgICBsZXQgb3JpZ2luVVJMID0gb3JpZ2luLnN0ZG91dC50cmltKCk7XG4gICAgaWYgKG9yaWdpbi5zdGF0dXMgIT09IDAgfHwgIW9yaWdpblVSTCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGxldCByZXZva2VkO1xuICAgIHRyeSB7XG4gICAgICAgIHJldm9rZWQgPSBhd2FpdCByZXZva2VkS2V5c0luUmVwbyh7IHJlcG9QYXRoLCBzb3VyY2VVUkw6IG9yaWdpblVSTCB9KTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIC8vIE5vIHJldm9rZSByZXBvLCBvciBubyBhY2Nlc3MgdG8gaXQuIE5vdGhpbmcgaGFzIGJlZW4gcmV2b2tlZCB0aGF0IHdlIGNhbiBzZWUuXG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKCFyZXZva2VkLmxlbmd0aCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgYEV4cGVjdGVkICR7cmVwb1BhdGh9IHRvIGhvbGQgbm8gcmV2b2tlZCBrZXlzLCBpdCBob2xkcyAke3Jldm9rZWQubGVuZ3RofTpcXG5gXG4gICAgICAgICsgcmV2b2tlZC5tYXAoZW50cnkgPT4gYCAgJHtlbnRyeS5yZXZvY2F0aW9uLmZpbmdlcnByaW50fSByZXZva2VkIGJ5ICR7ZW50cnkucmV2b2NhdGlvbi5yZXZva2VkQnkgfHwgXCI/XCJ9YFxuICAgICAgICAgICAgKyBgIGFmdGVyIHVzZSBmcm9tICR7ZW50cnkucmV2b2NhdGlvbi5hdHRlbXB0Py5pcCB8fCBcIj9cIn1gKS5qb2luKFwiXFxuXCIpXG4gICAgICAgICsgYFxcbkRlbGV0ZSB0aGVtIGZyb20gYXV0aG9yaXplZF9rZXlzLCBvciBydW4gXCJ5YXJuIHVucmV2b2tlXCIgaGVyZSB0byBhbGxvdyB0aGVtIGFnYWluLmBcbiAgICApO1xufVxuXG5mdW5jdGlvbiBwYXJzZUFyZ3MoYXJndjogc3RyaW5nW10pIHtcbiAgICBsZXQgcHVzaFRvR2l0ID0gYXJndi5pbmNsdWRlcyhHSVRfS0VZV09SRCk7XG4gICAgbGV0IHBvc2l0aW9uYWwgPSBhcmd2LmZpbHRlcihhcmcgPT4gYXJnICE9PSBHSVRfS0VZV09SRCk7XG4gICAgaWYgKHBvc2l0aW9uYWwubGVuZ3RoID4gMSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGF0IG1vc3QgYSBzaWduaW5nIGtleSwgd2FzICR7cG9zaXRpb25hbC5sZW5ndGh9IGFyZ3VtZW50KHMpOiAke3Bvc2l0aW9uYWwuam9pbihcIiBcIil9XFxuJHtVU0FHRX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIHsga2V5UGF0aDogcG9zaXRpb25hbFswXSwgcHVzaFRvR2l0IH07XG59XG5cbi8qKiBXcml0ZXMgdGhlIG1hbmlmZXN0IGFuZCBpdHMgc2lnbmF0dXJlIGludG8gYSByZXBvLiBTZXBhcmF0ZSBmcm9tIHRoZSBjb21tYW5kIHNvIGFueXRoaW5nIHRoYXRcbiAgICBjaGFuZ2VzIGEga2V5cyByZXBvIGNhbiBsZWF2ZSBpdCBzaWduZWQsIHJhdGhlciB0aGFuIHRlbGxpbmcgc29tZW9uZSB0byBnbyBhbmQgZG8gaXQuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2lnblJlcG8oY29uZmlnOiB7IHJlcG9QYXRoOiBzdHJpbmc7IGtleVBhdGg/OiBzdHJpbmcgfSkge1xuICAgIGxldCB7IHJlcG9QYXRoIH0gPSBjb25maWc7XG4gICAgbGV0IHNpZ25pbmdLZXkgPSBjb25maWcua2V5UGF0aCAmJiBleHBhbmRIb21lKGNvbmZpZy5rZXlQYXRoKSB8fCBhd2FpdCBlbnN1cmVEZWZhdWx0S2V5KCk7XG4gICAgaWYgKCFhd2FpdCBwYXRoRXhpc3RzKHNpZ25pbmdLZXkpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgYSBzaWduaW5nIGtleSBhdCAke3NpZ25pbmdLZXl9LCBubyBzdWNoIGZpbGUgZXhpc3RzYCk7XG4gICAgfVxuXG4gICAgYXdhaXQgcmVmdXNlVW51c2FibGVLZXlzKHJlcG9QYXRoKTtcbiAgICBhd2FpdCByZWZ1c2VSZXZva2VkS2V5cyhyZXBvUGF0aCk7XG5cbiAgICBsZXQgbWFuaWZlc3QgPSBhd2FpdCBidWlsZE1hbmlmZXN0KHJlcG9QYXRoKTtcbiAgICBjb25zb2xlLmxvZyhgJHtNQU5JRkVTVF9OQU1FfSBjb3ZlcnMgJHttYW5pZmVzdC5maWxlcy5sZW5ndGh9IGZpbGUocykgaW4gJHtyZXBvUGF0aH1gKTtcblxuICAgIC8vIFRoZSBtYW5pZmVzdCBpcyBidWlsdCBhbmQgc2lnbmVkIGF3YXkgZnJvbSB0aGUgcmVwbywgYW5kIG9ubHkgbW92ZWQgaW4gb25jZSBib3RoIGV4aXN0LlxuICAgIC8vIExhbmRpbmcgYSBuZXcgbWFuaWZlc3QgbmV4dCB0byBhbiBvbGQgc2lnbmF0dXJlIHByb2R1Y2VzIGEgcGFpciB0aGF0IGNhbiBuZXZlciB2ZXJpZnksIGFuZFxuICAgIC8vIHRoZSBkYWVtb24gcmVhZGluZyBpdCBjYW4gb25seSB0cmVhdCB0aGF0IGFzIHRhbXBlcmluZy5cbiAgICBsZXQgc3RhZ2luZ0RpcmVjdG9yeSA9IGF3YWl0IGZzLm1rZHRlbXAocGF0aC5qb2luKG9zLnRtcGRpcigpLCBcInNpZ25maWxlcy1cIikpO1xuICAgIGxldCBzdGFnZWRNYW5pZmVzdCA9IHBhdGguam9pbihzdGFnaW5nRGlyZWN0b3J5LCBNQU5JRkVTVF9OQU1FKTtcbiAgICBsZXQgc3RhZ2VkU2lnbmF0dXJlID0gYCR7c3RhZ2VkTWFuaWZlc3R9LnNpZ2A7XG4gICAgYXdhaXQgZnMud3JpdGVGaWxlKHN0YWdlZE1hbmlmZXN0LCBmb3JtYXRNYW5pZmVzdChtYW5pZmVzdCkpO1xuXG4gICAgLy8gU2lnbmluZyBoYXBwZW5zIGJlZm9yZSBhbnkgZ2l0IHdvcmssIHNvIGEgZmFpbGVkIHB1c2ggbmV2ZXIgY29zdHMgYSBzZWNvbmQgdG91Y2ggb2YgdGhlIGtleS5cbiAgICBhd2FpdCBydW5Qcm9taXNlKGBzc2gta2V5Z2VuIC1ZIHNpZ24gLWYgJHtxdW90ZShzaWduaW5nS2V5KX0gLW4gJHtTSUdOX05BTUVTUEFDRX0gJHtxdW90ZShzdGFnZWRNYW5pZmVzdCl9YCk7XG4gICAgLy8gQ2hlY2tlZCBoZXJlIHJhdGhlciB0aGFuIGxlZnQgZm9yIGEgbWFjaGluZSB0aGF0IGhhcyBhbHJlYWR5IHB1bGxlZCBpdCB0byBkaXNjb3Zlci5cbiAgICBhd2FpdCBydW5Qcm9taXNlKFxuICAgICAgICBgc3NoLWtleWdlbiAtWSBjaGVjay1ub3ZhbGlkYXRlIC1uICR7U0lHTl9OQU1FU1BBQ0V9IC1zICR7cXVvdGUoc3RhZ2VkU2lnbmF0dXJlKX0gPCAke3F1b3RlKHN0YWdlZE1hbmlmZXN0KX1gXG4gICAgKTtcbiAgICBhd2FpdCBmcy5jb3B5RmlsZShzdGFnZWRNYW5pZmVzdCwgcGF0aC5qb2luKHJlcG9QYXRoLCBNQU5JRkVTVF9OQU1FKSk7XG4gICAgYXdhaXQgZnMuY29weUZpbGUoc3RhZ2VkU2lnbmF0dXJlLCBwYXRoLmpvaW4ocmVwb1BhdGgsIFNJR05BVFVSRV9OQU1FKSk7XG4gICAgYXdhaXQgZnMucm0oc3RhZ2luZ0RpcmVjdG9yeSwgeyByZWN1cnNpdmU6IHRydWUsIGZvcmNlOiB0cnVlIH0pO1xuICAgIGNvbnNvbGUubG9nKGBTaWduZWQgd2l0aCAke2F3YWl0IHB1YmxpY0tleU9mKHNpZ25pbmdLZXkpfWApO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbWFpbigpIHtcbiAgICBsZXQgeyBrZXlQYXRoLCBwdXNoVG9HaXQgfSA9IHBhcnNlQXJncyhwcm9jZXNzLmFyZ3Yuc2xpY2UoMikpO1xuXG4gICAgLy8gUmVhZCBmb3IgaXRzIHZhbHVlLCBzbyBzdGRvdXQgaGFzIHRvIGJlIG9uIGl0cyBvd24uIHJ1blByb21pc2Ugam9pbnMgaXQgd2l0aCBzdGRlcnIsIGFuZCBhXG4gICAgLy8gZ2l0IHdhcm5pbmcgZ2x1ZWQgdG8gdGhlIGZyb250IG9mIHRoaXMgYmVjb21lcyBhIGRpcmVjdG9yeSB0aGF0IGRvZXMgbm90IGV4aXN0LlxuICAgIGxldCB0b3BMZXZlbCA9IGF3YWl0IHNwYXduUHJvbWlzZSh7IGNvbW1hbmQ6IFwiZ2l0XCIsIGFyZ3M6IFtcInJldi1wYXJzZVwiLCBcIi0tc2hvdy10b3BsZXZlbFwiXSB9KTtcbiAgICBsZXQgcmVwb1BhdGggPSB0b3BMZXZlbC5zdGRvdXQudHJpbSgpO1xuICAgIGlmICh0b3BMZXZlbC5zdGF0dXMgIT09IDAgfHwgIXJlcG9QYXRoKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCB0aGUgY3VycmVudCBkaXJlY3RvcnkgdG8gYmUgaW5zaWRlIGEgZ2l0IHJlcG8sIGl0IGlzIG5vdC5cXG5gXG4gICAgICAgICAgICArIGAkeyh0b3BMZXZlbC5zdGRvdXQgKyB0b3BMZXZlbC5zdGRlcnIpLnRyaW0oKX1cXG4ke1VTQUdFfWBcbiAgICAgICAgKTtcbiAgICB9XG4gICAgYXdhaXQgc2lnblJlcG8oeyByZXBvUGF0aCwga2V5UGF0aCB9KTtcblxuICAgIGlmICghcHVzaFRvR2l0KSB7XG4gICAgICAgIGNvbnNvbGUubG9nKGBDb21taXQgYW5kIHB1c2ggJHtNQU5JRkVTVF9OQU1FfSBhbmQgJHtTSUdOQVRVUkVfTkFNRX0gZm9yIGFueXRoaW5nIHRvIHNlZSB0aGVtLmApO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGF3YWl0IHJ1blByb21pc2UoYGdpdCBhZGQgLUFgLCB7IGN3ZDogcmVwb1BhdGggfSk7XG4gICAgLy8gTm90aGluZyBzdGFnZWQgaXMgbm90IHdvcnRoIHN0b3BwaW5nIG9uLiBnaXQgY29tbWl0IGNhbGxzIHRoYXQgYSBmYWlsdXJlLCBidXQgaXQgb25seSBtZWFuc1xuICAgIC8vIHRoZSBzaWduYXR1cmUgbWF0Y2hlcyB0aGUgb25lIGFscmVhZHkgY29tbWl0dGVkLCBzbyB0aGVyZSBpcyBub3RoaW5nIHRvIGRlcGxveS5cbiAgICBsZXQgc3RhdHVzID0gYXdhaXQgc3Bhd25Qcm9taXNlKHsgY29tbWFuZDogXCJnaXRcIiwgYXJnczogW1wic3RhdHVzXCIsIFwiLS1wb3JjZWxhaW5cIl0sIGN3ZDogcmVwb1BhdGggfSk7XG4gICAgbGV0IHN0YWdlZCA9IHN0YXR1cy5zdGRvdXQ7XG4gICAgaWYgKCFzdGFnZWQudHJpbSgpKSB7XG4gICAgICAgIGNvbnNvbGUubG9nKGBOb3RoaW5nIGNoYW5nZWQsICR7TUFOSUZFU1RfTkFNRX0gYW5kICR7U0lHTkFUVVJFX05BTUV9IGFyZSBhbHJlYWR5IGNvbW1pdHRlZC5gKTtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBhd2FpdCBydW5Qcm9taXNlKGBnaXQgY29tbWl0IC1tICR7cXVvdGUoQ09NTUlUX01FU1NBR0UpfWAsIHsgY3dkOiByZXBvUGF0aCB9KTtcbiAgICBhd2FpdCBydW5Qcm9taXNlKGBnaXQgcHVzaGAsIHsgY3dkOiByZXBvUGF0aCB9KTtcbiAgICBjb25zb2xlLmxvZyhgQ29tbWl0dGVkIGFuZCBwdXNoZWQuYCk7XG59XG4iXX0=
181
+ /* _JS_SOURCE_HASH = "5656bac7a766bdb1101df9294951fc82d227c63f880590d3dd4eb51bd67c88d1"; */
@@ -19,7 +19,13 @@ export type Manifest = {
19
19
  files: { path: string; size: number; sha256: string }[];
20
20
  };
21
21
 
22
- /** Everything in the repo that is not ignored, which is exactly what a clone of it will contain.
22
+ /** Everything in the repo that is not ignored: what is in the index, plus what is not staged yet.
23
+ Git is asked because git is what knows how to apply the ignore rules.
24
+
25
+ What comes back is a hint that a file might be there, not that it is. A file deleted from the
26
+ working tree stays in the index until that deletion is staged, so git names it and there is
27
+ nothing to read, which is why each one is checked before it goes in.
28
+
23
29
  The manifest and its signature are left out, since they cannot describe themselves. */
24
30
  export async function listRepoFiles(repoPath: string) {
25
31
  let result = await spawnPromise({
@@ -28,24 +34,48 @@ export async function listRepoFiles(repoPath: string) {
28
34
  cwd: repoPath,
29
35
  });
30
36
  if (result.status !== 0) {
31
- throw new Error(`Expected to list the files in ${repoPath}, git ls-files exited ${result.status}. ${result.stderr}`);
37
+ throw new Error(
38
+ `Expected to list the files in ${repoPath}, git ls-files exited ${result.status}. `
39
+ + `${(result.stdout + result.stderr).trim()}`
40
+ );
32
41
  }
33
- return result.stdout.split("\n")
42
+ let listed = result.stdout.split("\n")
34
43
  .map(line => line.trim())
35
44
  .filter(line => line && line !== MANIFEST_NAME && line !== SIGNATURE_NAME)
36
45
  .sort();
46
+
47
+ let present: string[] = [];
48
+ for (let relativePath of listed) {
49
+ try {
50
+ await fs.access(path.join(repoPath, relativePath));
51
+ present.push(relativePath);
52
+ } catch (e) {
53
+ console.log(`Leaving ${relativePath} out of the manifest, it is gone from the working tree`);
54
+ }
55
+ }
56
+ return present;
57
+ }
58
+
59
+ /** A Windows checkout turns LF into CRLF, so the same commit hashes differently there than it
60
+ does on the machine that pulls it. Normalising first makes the digest describe the content
61
+ rather than whichever checkout produced it. latin1 round trips every byte, so this is safe on
62
+ files that are not text. */
63
+ export function normalizeContent(contents: Buffer) {
64
+ return Buffer.from(contents.toString("latin1").replace(/\r\n/g, "\n"), "latin1");
37
65
  }
38
66
 
39
- export async function hashFile(filePath: string) {
40
- return crypto.createHash("sha256").update(await fs.readFile(filePath)).digest("hex");
67
+ /** The size and hash of a file's normalised content. The size is the normalised one on purpose,
68
+ so it agrees with the hash rather than with whatever the local checkout happens to hold. */
69
+ export async function digestFile(filePath: string) {
70
+ let contents = normalizeContent(await fs.readFile(filePath));
71
+ return { size: contents.length, sha256: crypto.createHash("sha256").update(contents).digest("hex") };
41
72
  }
42
73
 
43
74
  export async function buildManifest(repoPath: string) {
44
75
  let files: Manifest["files"] = [];
45
76
  for (let relativePath of await listRepoFiles(repoPath)) {
46
- let fullPath = path.join(repoPath, relativePath);
47
- let stats = await fs.stat(fullPath);
48
- files.push({ path: relativePath, size: stats.size, sha256: await hashFile(fullPath) });
77
+ let digest = await digestFile(path.join(repoPath, relativePath));
78
+ files.push({ path: relativePath, size: digest.size, sha256: digest.sha256 });
49
79
  }
50
80
  return { version: MANIFEST_VERSION, files };
51
81
  }
@@ -1,9 +1,12 @@
1
1
  import fs from "fs/promises";
2
2
  import os from "os";
3
3
  import path from "path";
4
+ import { runPromise } from "socket-function/src/runPromise";
4
5
  import { expandHome } from "../helpers/paths";
5
6
  import { spawnPromise } from "../helpers/spawn";
6
7
  import { buildManifest, formatManifest, MANIFEST_NAME, SIGNATURE_NAME, SIGN_NAMESPACE } from "./manifest";
8
+ import { revokedKeysInRepo } from "../authorizedKeys/unrevoke";
9
+ import { findKeyProblems, normalizeKeys } from "../authorizedKeys/authorizedKeys";
7
10
 
8
11
  // A hardware backed key is the entire point. A key sitting on disk is compromised the moment the
9
12
  // machine is, and then the signature proves nothing, so this is what we make when asked to make one.
@@ -18,6 +21,12 @@ Signs the files of the repo in the current directory. With no key, a hardware ba
18
21
  ${DEFAULT_KEY_TYPE} key at ${DEFAULT_KEY_PATH} is used, and created if it does not exist.
19
22
  Pass ${GIT_KEYWORD} to also commit and push the result.`;
20
23
 
24
+ /** runPromise takes a command line rather than an argument list, so anything holding a path has to
25
+ survive the shell. Double quotes work on both cmd and posix shells. */
26
+ function quote(value: string) {
27
+ return `"${value}"`;
28
+ }
29
+
21
30
  async function pathExists(filePath: string) {
22
31
  try {
23
32
  await fs.access(filePath);
@@ -27,23 +36,7 @@ async function pathExists(filePath: string) {
27
36
  }
28
37
  }
29
38
 
30
- async function run(config: { command: string; args: string[]; cwd?: string; interactive?: boolean }) {
31
- let { command, args, cwd, interactive } = config;
32
- let result = await spawnPromise({ command, args, cwd, inheritStderr: interactive });
33
- if (result.error) {
34
- throw new Error(`Expected ${command} to run, failed with ${result.error.message}`);
35
- }
36
- if (result.status !== 0) {
37
- throw new Error(
38
- `Expected ${command} ${args.join(" ")} to exit 0, was ${result.status}. `
39
- + `${(result.stderr || "").slice(0, MAX_ERROR_BODY_LENGTH)}`
40
- );
41
- }
42
- return result;
43
- }
44
-
45
- /** Creating this needs the security key plugged in, and a touch, so its output goes straight to
46
- the terminal rather than being captured. */
39
+ /** Creating this needs the security key plugged in, and a touch. */
47
40
  async function ensureDefaultKey() {
48
41
  let keyPath = expandHome(DEFAULT_KEY_PATH);
49
42
  if (await pathExists(keyPath)) {
@@ -52,21 +45,71 @@ async function ensureDefaultKey() {
52
45
  console.log(`No signing key at ${keyPath}, creating an ${DEFAULT_KEY_TYPE} one.`);
53
46
  console.log(`Plug your security key in - you will be asked to touch it.`);
54
47
  await fs.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
55
- await run({
56
- command: "ssh-keygen",
57
- args: ["-t", DEFAULT_KEY_TYPE, "-f", keyPath, "-N", "", "-C", "signfiles"],
58
- interactive: true,
59
- });
48
+ await runPromise(`ssh-keygen -t ${DEFAULT_KEY_TYPE} -f ${quote(keyPath)} -N "" -C signfiles`);
60
49
  return keyPath;
61
50
  }
62
51
 
63
- async function fingerprintOf(keyPath: string) {
64
- let result = await run({ command: "ssh-keygen", args: ["-lf", `${keyPath}.pub`] });
65
- let match = result.stdout.match(/(SHA256:[A-Za-z0-9+/=]+)/);
66
- if (!match) {
67
- throw new Error(`Expected a fingerprint for ${keyPath}.pub, was ${result.stdout.slice(0, MAX_ERROR_BODY_LENGTH)}`);
52
+ /** The public key in the form the daemon reports it, so what is printed here can be compared
53
+ against what arrives on Discord. */
54
+ async function publicKeyOf(keyPath: string) {
55
+ let contents = await fs.readFile(`${keyPath}.pub`, "utf8");
56
+ let [keyType, keyBody] = contents.trim().split(/\s+/);
57
+ if (!keyType || !keyBody) {
58
+ throw new Error(`Expected a public key in ${keyPath}.pub, was ${contents.slice(0, MAX_ERROR_BODY_LENGTH)}`);
59
+ }
60
+ return `${keyType} ${keyBody}`;
61
+ }
62
+
63
+ /** A keys repo has rules the rest of this cannot enforce afterwards, so they are enforced before it
64
+ is signed. Signing anything that is not a keys repo is none of this function's business. */
65
+ async function refuseUnusableKeys(repoPath: string) {
66
+ let authorizedPath = path.join(repoPath, "authorized_keys");
67
+ if (!await pathExists(authorizedPath)) {
68
+ return;
68
69
  }
69
- return match[1];
70
+ let problems = findKeyProblems(normalizeKeys(await fs.readFile(authorizedPath, "utf8")));
71
+ if (!problems.length) {
72
+ return;
73
+ }
74
+ throw new Error(
75
+ `Expected every key in ${authorizedPath} to be restricted and to belong to one person,`
76
+ + ` found ${problems.length} problem(s):\n`
77
+ + problems.map(problem => ` - ${problem}`).join("\n")
78
+ + `\n\nEvery key needs a from= naming the addresses it may be used from, and no two keys may`
79
+ + `\nname the same ones. Revoking a key that shares its addresses with another leaves that`
80
+ + `\nother key working, which defeats the point of revoking it.`
81
+ );
82
+ }
83
+
84
+ /** Signing a keys repo that still holds a revoked key would publish it back to every machine that
85
+ took it out. Only applies to a repo that holds keys and has a revoke repo to check - signing
86
+ anything else is none of this function's business. */
87
+ async function refuseRevokedKeys(repoPath: string) {
88
+ if (!await pathExists(path.join(repoPath, "authorized_keys"))) {
89
+ return;
90
+ }
91
+ // Read for its value, so stdout has to be on its own. runPromise joins it with stderr.
92
+ let origin = await spawnPromise({ command: "git", args: ["remote", "get-url", "origin"], cwd: repoPath });
93
+ let originURL = origin.stdout.trim();
94
+ if (origin.status !== 0 || !originURL) {
95
+ return;
96
+ }
97
+ let revoked;
98
+ try {
99
+ revoked = await revokedKeysInRepo({ repoPath, sourceURL: originURL });
100
+ } catch (e) {
101
+ // No revoke repo, or no access to it. Nothing has been revoked that we can see.
102
+ return;
103
+ }
104
+ if (!revoked.length) {
105
+ return;
106
+ }
107
+ throw new Error(
108
+ `Expected ${repoPath} to hold no revoked keys, it holds ${revoked.length}:\n`
109
+ + revoked.map(entry => ` ${entry.revocation.fingerprint} revoked by ${entry.revocation.revokedBy || "?"}`
110
+ + ` after use from ${entry.revocation.attempt?.ip || "?"}`).join("\n")
111
+ + `\nDelete them from authorized_keys, or run "yarn unrevoke" here to allow them again.`
112
+ );
70
113
  }
71
114
 
72
115
  function parseArgs(argv: string[]) {
@@ -78,39 +121,70 @@ function parseArgs(argv: string[]) {
78
121
  return { keyPath: positional[0], pushToGit };
79
122
  }
80
123
 
81
- export async function main() {
82
- let { keyPath, pushToGit } = parseArgs(process.argv.slice(2));
83
-
84
- let topLevel = await spawnPromise({ command: "git", args: ["rev-parse", "--show-toplevel"] });
85
- if (topLevel.status !== 0) {
86
- throw new Error(`Expected the current directory to be inside a git repo, it is not.\n${USAGE}`);
87
- }
88
- let repoPath = topLevel.stdout.trim();
89
-
90
- let signingKey = keyPath && expandHome(keyPath) || await ensureDefaultKey();
124
+ /** Writes the manifest and its signature into a repo. Separate from the command so anything that
125
+ changes a keys repo can leave it signed, rather than telling someone to go and do it. */
126
+ export async function signRepo(config: { repoPath: string; keyPath?: string }) {
127
+ let { repoPath } = config;
128
+ let signingKey = config.keyPath && expandHome(config.keyPath) || await ensureDefaultKey();
91
129
  if (!await pathExists(signingKey)) {
92
130
  throw new Error(`Expected a signing key at ${signingKey}, no such file exists`);
93
131
  }
94
132
 
133
+ await refuseUnusableKeys(repoPath);
134
+ await refuseRevokedKeys(repoPath);
135
+
95
136
  let manifest = await buildManifest(repoPath);
96
- let manifestPath = path.join(repoPath, MANIFEST_NAME);
97
- await fs.writeFile(manifestPath, formatManifest(manifest));
98
137
  console.log(`${MANIFEST_NAME} covers ${manifest.files.length} file(s) in ${repoPath}`);
99
138
 
139
+ // The manifest is built and signed away from the repo, and only moved in once both exist.
140
+ // Landing a new manifest next to an old signature produces a pair that can never verify, and
141
+ // the daemon reading it can only treat that as tampering.
142
+ let stagingDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "signfiles-"));
143
+ let stagedManifest = path.join(stagingDirectory, MANIFEST_NAME);
144
+ let stagedSignature = `${stagedManifest}.sig`;
145
+ await fs.writeFile(stagedManifest, formatManifest(manifest));
146
+
100
147
  // Signing happens before any git work, so a failed push never costs a second touch of the key.
101
- await run({
102
- command: "ssh-keygen",
103
- args: ["-Y", "sign", "-f", signingKey, "-n", SIGN_NAMESPACE, manifestPath],
104
- interactive: true,
105
- });
106
- console.log(`Signed with ${await fingerprintOf(signingKey)}`);
148
+ await runPromise(`ssh-keygen -Y sign -f ${quote(signingKey)} -n ${SIGN_NAMESPACE} ${quote(stagedManifest)}`);
149
+ // Checked here rather than left for a machine that has already pulled it to discover.
150
+ await runPromise(
151
+ `ssh-keygen -Y check-novalidate -n ${SIGN_NAMESPACE} -s ${quote(stagedSignature)} < ${quote(stagedManifest)}`
152
+ );
153
+ await fs.copyFile(stagedManifest, path.join(repoPath, MANIFEST_NAME));
154
+ await fs.copyFile(stagedSignature, path.join(repoPath, SIGNATURE_NAME));
155
+ await fs.rm(stagingDirectory, { recursive: true, force: true });
156
+ console.log(`Signed with ${await publicKeyOf(signingKey)}`);
157
+ }
158
+
159
+ export async function main() {
160
+ let { keyPath, pushToGit } = parseArgs(process.argv.slice(2));
161
+
162
+ // Read for its value, so stdout has to be on its own. runPromise joins it with stderr, and a
163
+ // git warning glued to the front of this becomes a directory that does not exist.
164
+ let topLevel = await spawnPromise({ command: "git", args: ["rev-parse", "--show-toplevel"] });
165
+ let repoPath = topLevel.stdout.trim();
166
+ if (topLevel.status !== 0 || !repoPath) {
167
+ throw new Error(
168
+ `Expected the current directory to be inside a git repo, it is not.\n`
169
+ + `${(topLevel.stdout + topLevel.stderr).trim()}\n${USAGE}`
170
+ );
171
+ }
172
+ await signRepo({ repoPath, keyPath });
107
173
 
108
174
  if (!pushToGit) {
109
175
  console.log(`Commit and push ${MANIFEST_NAME} and ${SIGNATURE_NAME} for anything to see them.`);
110
176
  return;
111
177
  }
112
- await run({ command: "git", args: ["add", "-A"], cwd: repoPath, interactive: true });
113
- await run({ command: "git", args: ["commit", "-m", COMMIT_MESSAGE], cwd: repoPath, interactive: true });
114
- await run({ command: "git", args: ["push"], cwd: repoPath, interactive: true });
178
+ await runPromise(`git add -A`, { cwd: repoPath });
179
+ // Nothing staged is not worth stopping on. git commit calls that a failure, but it only means
180
+ // the signature matches the one already committed, so there is nothing to deploy.
181
+ let status = await spawnPromise({ command: "git", args: ["status", "--porcelain"], cwd: repoPath });
182
+ let staged = status.stdout;
183
+ if (!staged.trim()) {
184
+ console.log(`Nothing changed, ${MANIFEST_NAME} and ${SIGNATURE_NAME} are already committed.`);
185
+ return;
186
+ }
187
+ await runPromise(`git commit -m ${quote(COMMIT_MESSAGE)}`, { cwd: repoPath });
188
+ await runPromise(`git push`, { cwd: repoPath });
115
189
  console.log(`Committed and pushed.`);
116
190
  }