sliftutils 1.7.133 → 1.7.134

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 (58) hide show
  1. package/bin/addmachine.js +1 -5
  2. package/bin/derivekey.js +2 -7
  3. package/bin/portsecuredaemon.js +1 -8
  4. package/bin/securessh.js +2 -7
  5. package/bin/setupnotify.js +2 -6
  6. package/bin/signfiles.js +2 -7
  7. package/bin/testnotify.js +2 -6
  8. package/bin/unrevoke.js +1 -5
  9. package/misc/dist/ownIPs.ts.cache +2 -2
  10. package/misc/https/dist/certs.ts.cache +12 -9
  11. package/misc/https/dist/persistentLocalStorage.ts.cache +10 -4
  12. package/package.json +1 -1
  13. package/security/authorizedKeys/daemon/daemon.ts +142 -7
  14. package/security/authorizedKeys/daemon/dist/changes.ts.cache +2 -2
  15. package/security/authorizedKeys/daemon/dist/git.ts.cache +26 -8
  16. package/security/authorizedKeys/daemon/dist/notify.ts.cache +2 -2
  17. package/security/authorizedKeys/daemon/dist/paths.ts.cache +2 -2
  18. package/security/authorizedKeys/daemon/dist/repoFiles.ts.cache +4 -6
  19. package/security/authorizedKeys/daemon/dist/revocation.ts.cache +0 -0
  20. package/security/authorizedKeys/daemon/dist/sessions.ts.cache +2 -2
  21. package/security/authorizedKeys/daemon/dist/state.ts.cache +2 -2
  22. package/security/authorizedKeys/daemon/dist/trust.ts.cache +85 -265
  23. package/security/authorizedKeys/daemon/git.d.ts +8 -0
  24. package/security/authorizedKeys/daemon/git.ts +22 -4
  25. package/security/authorizedKeys/daemon/repoFiles.ts +2 -4
  26. package/security/authorizedKeys/daemon/trust.d.ts +25 -19
  27. package/security/authorizedKeys/daemon/trust.ts +91 -278
  28. package/security/authorizedKeys/dist/authorizedKeys.ts.cache +2 -2
  29. package/security/authorizedKeys/dist/keysRepo.ts.cache +2 -2
  30. package/security/authorizedKeys/dist/revokeSource.ts.cache +2 -2
  31. package/security/authorizedKeys/dist/sources.ts.cache +2 -2
  32. package/security/authorizedKeys/secureSSH.ts +6 -1
  33. package/security/authorizedKeys/unrevoke.ts +2 -198
  34. package/security/authorizedKeys/unrevokeCli.ts +204 -0
  35. package/security/helpers/dist/paths.ts.cache +2 -2
  36. package/security/helpers/dist/remoteSSH.ts.cache +2 -2
  37. package/security/helpers/dist/spawn.ts.cache +2 -2
  38. package/security/helpers/remoteSSH.d.ts +35 -0
  39. package/security/keys/deriveKey.d.ts +0 -1
  40. package/security/keys/deriveKey.ts +1 -56
  41. package/security/keys/deriveKeyCli.ts +62 -0
  42. package/security/keys/dist/deriveKey.ts.cache +4 -51
  43. package/security/keys/dist/sshKeyFile.ts.cache +5 -3
  44. package/security/machines/addMachine.ts +31 -61
  45. package/security/machines/dist/addMachine.ts.cache +72 -57
  46. package/security/machines/dist/machines.ts.cache +134 -55
  47. package/security/machines/machines.d.ts +11 -0
  48. package/security/machines/machines.ts +134 -57
  49. package/security/notifications/dist/discord.ts.cache +2 -2
  50. package/security/notifications/setupNotify.ts +6 -1
  51. package/security/notifications/testNotify.ts +6 -1
  52. package/security/signedFiles/dist/manifest.ts.cache +2 -2
  53. package/security/signedFiles/dist/sshsig.ts.cache +138 -0
  54. package/security/signedFiles/signFiles.ts +11 -52
  55. package/security/signedFiles/signFilesCli.ts +54 -0
  56. package/security/signedFiles/sshsig.d.ts +13 -0
  57. package/security/signedFiles/sshsig.ts +141 -0
  58. package/storage/dist/backblaze.ts.cache +10 -2
@@ -4,23 +4,16 @@ import path from "path";
4
4
  import { runPromise } from "socket-function/src/runPromise";
5
5
  import { expandHome } from "../helpers/paths";
6
6
  import { spawnPromise } from "../helpers/spawn";
7
- import { buildManifest, formatManifest, MANIFEST_NAME, SIGNATURE_NAME, SIGN_NAMESPACE } from "./manifest";
7
+ import { buildManifest, formatManifest, MANIFEST_NAME, normalizeContent, SIGNATURE_NAME, SIGN_NAMESPACE } from "./manifest";
8
+ import { verifySSHSIG } from "./sshsig";
8
9
  import { revokedKeysInRepo } from "../authorizedKeys/unrevoke";
9
- import { resolveKeysRepo } from "../authorizedKeys/keysRepo";
10
10
  import { findKeyProblems, normalizeKeys } from "../authorizedKeys/authorizedKeys";
11
11
 
12
12
  // A hardware backed key is the entire point. A key sitting on disk is compromised the moment the
13
13
  // machine is, and then the signature proves nothing, so this is what we make when asked to make one.
14
- const DEFAULT_KEY_TYPE = "ed25519-sk";
15
- const DEFAULT_KEY_PATH = "~/.ssh/signfiles_ed25519_sk";
16
- const GIT_KEYWORD = "git";
17
- const COMMIT_MESSAGE = "deploying signed files";
14
+ export const DEFAULT_KEY_TYPE = "ed25519-sk";
15
+ export const DEFAULT_KEY_PATH = "~/.ssh/signfiles_ed25519_sk";
18
16
  const MAX_ERROR_BODY_LENGTH = 500;
19
- const USAGE = `Usage: yarn signfiles [signing-key] [${GIT_KEYWORD}]
20
-
21
- Signs the files of the repo in the current directory. With no key, a hardware backed
22
- ${DEFAULT_KEY_TYPE} key at ${DEFAULT_KEY_PATH} is used, and created if it does not exist.
23
- Pass ${GIT_KEYWORD} to also commit and push the result.`;
24
17
 
25
18
  /** runPromise takes a command line rather than an argument list, so anything holding a path has to
26
19
  survive the shell. Double quotes work on both cmd and posix shells. */
@@ -113,15 +106,6 @@ async function refuseRevokedKeys(repoPath: string) {
113
106
  );
114
107
  }
115
108
 
116
- function parseArgs(argv: string[]) {
117
- let pushToGit = argv.includes(GIT_KEYWORD);
118
- let positional = argv.filter(arg => arg !== GIT_KEYWORD);
119
- if (positional.length > 1) {
120
- throw new Error(`Expected at most a signing key, was ${positional.length} argument(s): ${positional.join(" ")}\n${USAGE}`);
121
- }
122
- return { keyPath: positional[0], pushToGit };
123
- }
124
-
125
109
  /** Writes the manifest and its signature into a repo. Separate from the command so anything that
126
110
  changes a keys repo can leave it signed, rather than telling someone to go and do it. */
127
111
  export async function signRepo(config: { repoPath: string; keyPath?: string }) {
@@ -147,40 +131,15 @@ export async function signRepo(config: { repoPath: string; keyPath?: string }) {
147
131
 
148
132
  // Signing happens before any git work, so a failed push never costs a second touch of the key.
149
133
  await runPromise(`ssh-keygen -Y sign -f ${quote(signingKey)} -n ${SIGN_NAMESPACE} ${quote(stagedManifest)}`);
150
- // Checked here rather than left for a machine that has already pulled it to discover.
151
- await runPromise(
152
- `ssh-keygen -Y check-novalidate -n ${SIGN_NAMESPACE} -s ${quote(stagedSignature)} < ${quote(stagedManifest)}`
153
- );
134
+ // Checked with the same code the daemon verifies with, over the same normalised bytes, so what
135
+ // is about to be committed is proven to pass the check every other machine will run on it.
136
+ verifySSHSIG({
137
+ signature: await fs.readFile(stagedSignature, "utf8"),
138
+ message: normalizeContent(await fs.readFile(stagedManifest)),
139
+ namespace: SIGN_NAMESPACE,
140
+ });
154
141
  await fs.copyFile(stagedManifest, path.join(repoPath, MANIFEST_NAME));
155
142
  await fs.copyFile(stagedSignature, path.join(repoPath, SIGNATURE_NAME));
156
143
  await fs.rm(stagingDirectory, { recursive: true, force: true });
157
144
  console.log(`Signed with ${await publicKeyOf(signingKey)}`);
158
145
  }
159
-
160
- export async function main() {
161
- let { keyPath, pushToGit } = parseArgs(process.argv.slice(2));
162
-
163
- // The keys repo, which is this one when it holds keys and this machine's otherwise. Signing
164
- // whatever repo the terminal happened to be in is how you end up signing something that is not
165
- // a keys repo at all.
166
- let { repoPath } = await resolveKeysRepo();
167
- console.log(`Signing ${repoPath}`);
168
- await signRepo({ repoPath, keyPath });
169
-
170
- if (!pushToGit) {
171
- console.log(`Commit and push ${MANIFEST_NAME} and ${SIGNATURE_NAME} for anything to see them.`);
172
- return;
173
- }
174
- await runPromise(`git add -A`, { cwd: repoPath });
175
- // Nothing staged is not worth stopping on. git commit calls that a failure, but it only means
176
- // the signature matches the one already committed, so there is nothing to deploy.
177
- let status = await spawnPromise({ command: "git", args: ["status", "--porcelain"], cwd: repoPath });
178
- let staged = status.stdout;
179
- if (!staged.trim()) {
180
- console.log(`Nothing changed, ${MANIFEST_NAME} and ${SIGNATURE_NAME} are already committed.`);
181
- return;
182
- }
183
- await runPromise(`git commit -m ${quote(COMMIT_MESSAGE)}`, { cwd: repoPath });
184
- await runPromise(`git push`, { cwd: repoPath });
185
- console.log(`Committed and pushed.`);
186
- }
@@ -0,0 +1,54 @@
1
+ import { runPromise } from "socket-function/src/runPromise";
2
+ import { spawnPromise } from "../helpers/spawn";
3
+ import { resolveKeysRepo } from "../authorizedKeys/keysRepo";
4
+ import { MANIFEST_NAME, SIGNATURE_NAME } from "./manifest";
5
+ import { DEFAULT_KEY_PATH, DEFAULT_KEY_TYPE, signRepo } from "./signFiles";
6
+
7
+ const GIT_KEYWORD = "git";
8
+ const COMMIT_MESSAGE = "deploying signed files";
9
+ const USAGE = `Usage: yarn signfiles [signing-key] [${GIT_KEYWORD}]
10
+
11
+ Signs the files of the repo in the current directory. With no key, a hardware backed
12
+ ${DEFAULT_KEY_TYPE} key at ${DEFAULT_KEY_PATH} is used, and created if it does not exist.
13
+ Pass ${GIT_KEYWORD} to also commit and push the result.`;
14
+
15
+ function parseArgs(argv: string[]) {
16
+ let pushToGit = argv.includes(GIT_KEYWORD);
17
+ let positional = argv.filter(arg => arg !== GIT_KEYWORD);
18
+ if (positional.length > 1) {
19
+ throw new Error(`Expected at most a signing key, was ${positional.length} argument(s): ${positional.join(" ")}\n${USAGE}`);
20
+ }
21
+ return { keyPath: positional[0], pushToGit };
22
+ }
23
+
24
+ async function main() {
25
+ let { keyPath, pushToGit } = parseArgs(process.argv.slice(2));
26
+
27
+ // The keys repo, which is this one when it holds keys and this machine's otherwise. Signing
28
+ // whatever repo the terminal happened to be in is how you end up signing something that is not
29
+ // a keys repo at all.
30
+ let { repoPath } = await resolveKeysRepo();
31
+ console.log(`Signing ${repoPath}`);
32
+ await signRepo({ repoPath, keyPath });
33
+
34
+ if (!pushToGit) {
35
+ console.log(`Commit and push ${MANIFEST_NAME} and ${SIGNATURE_NAME} for anything to see them.`);
36
+ return;
37
+ }
38
+ await runPromise(`git add -A`, { cwd: repoPath });
39
+ // Nothing staged is not worth stopping on. git commit calls that a failure, but it only means
40
+ // the signature matches the one already committed, so there is nothing to deploy.
41
+ let status = await spawnPromise({ command: "git", args: ["status", "--porcelain"], cwd: repoPath });
42
+ if (!status.stdout.trim()) {
43
+ console.log(`Nothing changed, ${MANIFEST_NAME} and ${SIGNATURE_NAME} are already committed.`);
44
+ return;
45
+ }
46
+ await runPromise(`git commit -m "${COMMIT_MESSAGE}"`, { cwd: repoPath });
47
+ await runPromise(`git push`, { cwd: repoPath });
48
+ console.log(`Committed and pushed.`);
49
+ }
50
+
51
+ main().catch(e => {
52
+ console.error(`${e}`);
53
+ process.exitCode = 1;
54
+ }).finally(() => process.exit());
@@ -0,0 +1,13 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /** Verifies an SSHSIG signature over a message, and that it was made for the given namespace.
4
+ Returns the signing public key and its fingerprint, in the forms ssh-keygen reports them.
5
+ Throws if it does not verify, or is not for this namespace. */
6
+ export declare function verifySSHSIG(config: {
7
+ signature: string;
8
+ message: Buffer;
9
+ namespace: string;
10
+ }): {
11
+ publicKey: string;
12
+ fingerprint: string;
13
+ };
@@ -0,0 +1,141 @@
1
+ import crypto from "crypto";
2
+
3
+ // Verifies an SSHSIG signature (the format ssh-keygen -Y sign produces) in process, so nothing has
4
+ // to shell out to ssh-keygen to check one. The wire format is PROTOCOL.sshsig, and the actual
5
+ // check is one ed25519 verification.
6
+ //
7
+ // Both ed25519 and the hardware sk-ed25519 variant are handled, since signfiles signs with a
8
+ // hardware ed25519-sk key. The sk variant signs a little more than the message - the authenticator
9
+ // flags and counter go in too - but it is still ed25519 underneath.
10
+
11
+ const MAGIC = "SSHSIG";
12
+ // An ed25519 public key as a DER SPKI is this fixed 12 byte header followed by the 32 byte key.
13
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
14
+
15
+ /** Reads the length prefixed fields SSH wire format is made of. */
16
+ class Reader {
17
+ private offset = 0;
18
+ constructor(private buffer: Buffer) { }
19
+ u8() {
20
+ return this.buffer.readUInt8(this.offset++);
21
+ }
22
+ u32() {
23
+ let value = this.buffer.readUInt32BE(this.offset);
24
+ this.offset += 4;
25
+ return value;
26
+ }
27
+ string() {
28
+ let length = this.u32();
29
+ let value = this.buffer.subarray(this.offset, this.offset + length);
30
+ this.offset += length;
31
+ return value;
32
+ }
33
+ remaining() {
34
+ return this.buffer.length - this.offset;
35
+ }
36
+ }
37
+
38
+ /** One length prefixed field, the way SSH writes them. */
39
+ function sshString(bytes: Buffer) {
40
+ let length = Buffer.alloc(4);
41
+ length.writeUInt32BE(bytes.length);
42
+ return Buffer.concat([length, bytes]);
43
+ }
44
+
45
+ function hash(algorithm: string, data: Buffer) {
46
+ // SSHSIG names sha256 or sha512; node uses the same names.
47
+ if (algorithm !== "sha256" && algorithm !== "sha512") {
48
+ throw new Error(`Unsupported SSHSIG hash algorithm ${JSON.stringify(algorithm)}`);
49
+ }
50
+ return crypto.createHash(algorithm).update(data).digest();
51
+ }
52
+
53
+ function ed25519PublicKey(rawKey: Buffer) {
54
+ if (rawKey.length !== 32) {
55
+ throw new Error(`Expected a 32 byte ed25519 key, was ${rawKey.length}`);
56
+ }
57
+ return crypto.createPublicKey({
58
+ key: Buffer.concat([ED25519_SPKI_PREFIX, rawKey]),
59
+ format: "der",
60
+ type: "spki",
61
+ });
62
+ }
63
+
64
+ /** Verifies an SSHSIG signature over a message, and that it was made for the given namespace.
65
+ Returns the signing public key and its fingerprint, in the forms ssh-keygen reports them.
66
+ Throws if it does not verify, or is not for this namespace. */
67
+ export function verifySSHSIG(config: { signature: string; message: Buffer; namespace: string }) {
68
+ let { signature, message, namespace } = config;
69
+
70
+ let body = signature.split("\n").filter(line => line && !line.startsWith("-----")).join("");
71
+ let blob = Buffer.from(body, "base64");
72
+ if (blob.subarray(0, MAGIC.length).toString() !== MAGIC) {
73
+ throw new Error(`Expected an ${MAGIC} signature, started with ${JSON.stringify(blob.subarray(0, MAGIC.length).toString())}`);
74
+ }
75
+
76
+ let reader = new Reader(blob.subarray(MAGIC.length));
77
+ let version = reader.u32();
78
+ if (version !== 1) {
79
+ throw new Error(`Expected SSHSIG version 1, was ${version}`);
80
+ }
81
+ let publicKeyBlob = reader.string();
82
+ let signatureNamespace = reader.string().toString();
83
+ reader.string(); // reserved
84
+ let hashAlgorithm = reader.string().toString();
85
+ let signatureBlob = reader.string();
86
+
87
+ if (signatureNamespace !== namespace) {
88
+ throw new Error(`Signature is for namespace ${JSON.stringify(signatureNamespace)}, expected ${JSON.stringify(namespace)}`);
89
+ }
90
+
91
+ let publicKeyReader = new Reader(publicKeyBlob);
92
+ let keyType = publicKeyReader.string().toString();
93
+ let rawKey = publicKeyReader.string();
94
+ let application = publicKeyReader.remaining() ? publicKeyReader.string() : Buffer.alloc(0);
95
+
96
+ let signatureReader = new Reader(signatureBlob);
97
+ let signatureType = signatureReader.string().toString();
98
+ let rawSignature = signatureReader.string();
99
+ if (signatureType !== keyType) {
100
+ throw new Error(`Signature type ${signatureType} does not match key type ${keyType}`);
101
+ }
102
+
103
+ // The bytes the signature is over: the magic, the namespace, the reserved field, the hash
104
+ // algorithm, and the hash of the message. Never the message itself, so a huge file still signs
105
+ // a fixed size blob.
106
+ let signed = Buffer.concat([
107
+ Buffer.from(MAGIC),
108
+ sshString(Buffer.from(signatureNamespace)),
109
+ sshString(Buffer.alloc(0)),
110
+ sshString(Buffer.from(hashAlgorithm)),
111
+ sshString(hash(hashAlgorithm, message)),
112
+ ]);
113
+
114
+ let toVerify: Buffer;
115
+ if (keyType === "ssh-ed25519") {
116
+ toVerify = signed;
117
+ } else if (keyType === "sk-ssh-ed25519@openssh.com") {
118
+ // A hardware key signs over what the authenticator saw, not the blob directly: the hash of
119
+ // the application it is scoped to, the flags and counter it returned, and the hash of the
120
+ // blob. Reconstructing that is the whole difference from a plain ed25519 signature.
121
+ let flags = signatureReader.u8();
122
+ let counter = signatureReader.u32();
123
+ let counterBytes = Buffer.alloc(4);
124
+ counterBytes.writeUInt32BE(counter);
125
+ toVerify = Buffer.concat([
126
+ hash("sha256", application),
127
+ Buffer.from([flags]),
128
+ counterBytes,
129
+ hash("sha256", signed),
130
+ ]);
131
+ } else {
132
+ throw new Error(`Unsupported SSHSIG key type ${JSON.stringify(keyType)}`);
133
+ }
134
+
135
+ if (!crypto.verify(null, toVerify, ed25519PublicKey(rawKey), rawSignature)) {
136
+ throw new Error(`The SSHSIG signature does not verify`);
137
+ }
138
+
139
+ let fingerprint = "SHA256:" + crypto.createHash("sha256").update(publicKeyBlob).digest("base64").replace(/=+$/, "");
140
+ return { publicKey: `${keyType} ${publicKeyBlob.toString("base64")}`, fingerprint };
141
+ }