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.
- package/bin/addmachine.js +1 -5
- package/bin/derivekey.js +2 -7
- package/bin/portsecuredaemon.js +1 -8
- package/bin/securessh.js +2 -7
- package/bin/setupnotify.js +2 -6
- package/bin/signfiles.js +2 -7
- package/bin/testnotify.js +2 -6
- package/bin/unrevoke.js +1 -5
- package/misc/dist/ownIPs.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +12 -9
- package/misc/https/dist/persistentLocalStorage.ts.cache +10 -4
- package/package.json +1 -1
- package/security/authorizedKeys/daemon/daemon.ts +142 -7
- package/security/authorizedKeys/daemon/dist/changes.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/git.ts.cache +26 -8
- package/security/authorizedKeys/daemon/dist/notify.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/paths.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/repoFiles.ts.cache +4 -6
- package/security/authorizedKeys/daemon/dist/revocation.ts.cache +0 -0
- package/security/authorizedKeys/daemon/dist/sessions.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/state.ts.cache +2 -2
- package/security/authorizedKeys/daemon/dist/trust.ts.cache +85 -265
- package/security/authorizedKeys/daemon/git.d.ts +8 -0
- package/security/authorizedKeys/daemon/git.ts +22 -4
- package/security/authorizedKeys/daemon/repoFiles.ts +2 -4
- package/security/authorizedKeys/daemon/trust.d.ts +25 -19
- package/security/authorizedKeys/daemon/trust.ts +91 -278
- package/security/authorizedKeys/dist/authorizedKeys.ts.cache +2 -2
- package/security/authorizedKeys/dist/keysRepo.ts.cache +2 -2
- package/security/authorizedKeys/dist/revokeSource.ts.cache +2 -2
- package/security/authorizedKeys/dist/sources.ts.cache +2 -2
- package/security/authorizedKeys/secureSSH.ts +6 -1
- package/security/authorizedKeys/unrevoke.ts +2 -198
- package/security/authorizedKeys/unrevokeCli.ts +204 -0
- package/security/helpers/dist/paths.ts.cache +2 -2
- package/security/helpers/dist/remoteSSH.ts.cache +2 -2
- package/security/helpers/dist/spawn.ts.cache +2 -2
- package/security/helpers/remoteSSH.d.ts +35 -0
- package/security/keys/deriveKey.d.ts +0 -1
- package/security/keys/deriveKey.ts +1 -56
- package/security/keys/deriveKeyCli.ts +62 -0
- package/security/keys/dist/deriveKey.ts.cache +4 -51
- package/security/keys/dist/sshKeyFile.ts.cache +5 -3
- package/security/machines/addMachine.ts +31 -61
- package/security/machines/dist/addMachine.ts.cache +72 -57
- package/security/machines/dist/machines.ts.cache +134 -55
- package/security/machines/machines.d.ts +11 -0
- package/security/machines/machines.ts +134 -57
- package/security/notifications/dist/discord.ts.cache +2 -2
- package/security/notifications/setupNotify.ts +6 -1
- package/security/notifications/testNotify.ts +6 -1
- package/security/signedFiles/dist/manifest.ts.cache +2 -2
- package/security/signedFiles/dist/sshsig.ts.cache +138 -0
- package/security/signedFiles/signFiles.ts +11 -52
- package/security/signedFiles/signFilesCli.ts +54 -0
- package/security/signedFiles/sshsig.d.ts +13 -0
- package/security/signedFiles/sshsig.ts +141 -0
- 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
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
}
|