sliftutils 1.7.126 → 1.7.128

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.
@@ -3,8 +3,10 @@ import os from "os";
3
3
  import path from "path";
4
4
  import { runPromise } from "socket-function/src/runPromise";
5
5
  import { expandHome } from "../helpers/paths";
6
+ import { spawnPromise } from "../helpers/spawn";
6
7
  import { buildManifest, formatManifest, MANIFEST_NAME, SIGNATURE_NAME, SIGN_NAMESPACE } from "./manifest";
7
8
  import { revokedKeysInRepo } from "../authorizedKeys/unrevoke";
9
+ import { findKeyProblems, normalizeKeys } from "../authorizedKeys/authorizedKeys";
8
10
 
9
11
  // A hardware backed key is the entire point. A key sitting on disk is compromised the moment the
10
12
  // machine is, and then the signature proves nothing, so this is what we make when asked to make one.
@@ -58,6 +60,27 @@ async function publicKeyOf(keyPath: string) {
58
60
  return `${keyType} ${keyBody}`;
59
61
  }
60
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;
69
+ }
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
+
61
84
  /** Signing a keys repo that still holds a revoked key would publish it back to every machine that
62
85
  took it out. Only applies to a repo that holds keys and has a revoke repo to check - signing
63
86
  anything else is none of this function's business. */
@@ -65,8 +88,10 @@ async function refuseRevokedKeys(repoPath: string) {
65
88
  if (!await pathExists(path.join(repoPath, "authorized_keys"))) {
66
89
  return;
67
90
  }
68
- let originURL = (await runPromise("git remote get-url origin", { cwd: repoPath, quiet: true, nothrow: true })).trim();
69
- if (!originURL) {
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) {
70
95
  return;
71
96
  }
72
97
  let revoked;
@@ -96,19 +121,16 @@ function parseArgs(argv: string[]) {
96
121
  return { keyPath: positional[0], pushToGit };
97
122
  }
98
123
 
99
- export async function main() {
100
- let { keyPath, pushToGit } = parseArgs(process.argv.slice(2));
101
-
102
- let repoPath = (await runPromise("git rev-parse --show-toplevel", { quiet: true })).trim();
103
- if (!repoPath) {
104
- throw new Error(`Expected the current directory to be inside a git repo, it is not.\n${USAGE}`);
105
- }
106
-
107
- 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();
108
129
  if (!await pathExists(signingKey)) {
109
130
  throw new Error(`Expected a signing key at ${signingKey}, no such file exists`);
110
131
  }
111
132
 
133
+ await refuseUnusableKeys(repoPath);
112
134
  await refuseRevokedKeys(repoPath);
113
135
 
114
136
  let manifest = await buildManifest(repoPath);
@@ -132,6 +154,22 @@ export async function main() {
132
154
  await fs.copyFile(stagedSignature, path.join(repoPath, SIGNATURE_NAME));
133
155
  await fs.rm(stagingDirectory, { recursive: true, force: true });
134
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 });
135
173
 
136
174
  if (!pushToGit) {
137
175
  console.log(`Commit and push ${MANIFEST_NAME} and ${SIGNATURE_NAME} for anything to see them.`);
@@ -140,7 +178,8 @@ export async function main() {
140
178
  await runPromise(`git add -A`, { cwd: repoPath });
141
179
  // Nothing staged is not worth stopping on. git commit calls that a failure, but it only means
142
180
  // the signature matches the one already committed, so there is nothing to deploy.
143
- let staged = await runPromise(`git status --porcelain`, { cwd: repoPath, quiet: true });
181
+ let status = await spawnPromise({ command: "git", args: ["status", "--porcelain"], cwd: repoPath });
182
+ let staged = status.stdout;
144
183
  if (!staged.trim()) {
145
184
  console.log(`Nothing changed, ${MANIFEST_NAME} and ${SIGNATURE_NAME} are already committed.`);
146
185
  return;