wardensec 0.0.1 → 0.1.0-alpha.1
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/LICENSE +1 -0
- package/README.md +26 -3
- package/bin/wardensec.cjs +153 -0
- package/install.sh +710 -0
- package/package.json +24 -6
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright 2026 The Warden Authors. All rights reserved. Use is permitted only under the terms at https://wardensec.ai/legal/terms and a valid Warden licence file.
|
package/README.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wardensec
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installs Warden, the self-hosted AWS and Kubernetes security software, from a terminal where Node is installed.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```
|
|
6
|
+
npx wardensec@alpha
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
or `npm i -g wardensec@alpha && wardensec`.
|
|
10
|
+
|
|
11
|
+
This package carries one file that matters: `install.sh`, the same installer https://wardensec.ai/install.sh serves,
|
|
12
|
+
byte for byte. The command writes it to a private temporary directory and runs it with `sh`, passing your arguments on
|
|
13
|
+
(`--now`, `--root-sha256=<hex>`). The installer checks every release against its built-in root and ends in the same
|
|
14
|
+
launcher and the same one approval in your console as the `curl` line. The package adds no verifier and no root of its
|
|
15
|
+
own, opens no connection of its own, and runs no code when it is installed.
|
|
16
|
+
|
|
17
|
+
Check the bytes before you run them:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm pack wardensec@alpha
|
|
21
|
+
tar -xOf wardensec-*.tgz package/install.sh | sha256sum
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The result must equal an `installer sha256` line of the public trust record,
|
|
25
|
+
https://raw.githubusercontent.com/agent-labs-dev/warden-trust/main/ROOTS.txt. `npx wardensec@alpha --help` prints the
|
|
26
|
+
same hash and runs nothing.
|
|
27
|
+
|
|
28
|
+
It needs a POSIX shell: macOS, Linux, or WSL on Windows. Elsewhere it stops with one line and the `curl` command.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// The npm entry to the Warden install. It carries the one released install.sh, byte for byte, and runs it with sh.
|
|
5
|
+
// It holds no verifier and no root of its own: install.sh checks every release against its built-in root, and the
|
|
6
|
+
// bytes it runs are the ones the public trust record names. This file only carries them. It opens no connection;
|
|
7
|
+
// the installer makes every fetch.
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const os = require("node:os");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const crypto = require("node:crypto");
|
|
13
|
+
const childProcess = require("node:child_process");
|
|
14
|
+
|
|
15
|
+
// Filled in when the package is packed (tools/release/npm-pack.ts).
|
|
16
|
+
const BUILT_IN = { version: "0.1.0-alpha.1", installerSha256: "41db9d8809bbd9dab61f84753b3a6605115d6db9bd9a79f26a9e5c7e5629e9b1" };
|
|
17
|
+
|
|
18
|
+
const CURL_LINE = "curl -fsSL https://wardensec.ai/install | sh";
|
|
19
|
+
const TRUST_RECORD = "https://raw.githubusercontent.com/agent-labs-dev/warden-trust/main/ROOTS.txt";
|
|
20
|
+
const NO_SHELL = `Warden installs from a POSIX shell (macOS, Linux, or WSL on Windows). There, run: ${CURL_LINE}\n`;
|
|
21
|
+
|
|
22
|
+
function defaults() {
|
|
23
|
+
return {
|
|
24
|
+
argv: process.argv.slice(2),
|
|
25
|
+
platform: process.platform,
|
|
26
|
+
sh: "/bin/sh",
|
|
27
|
+
pkgDir: path.resolve(__dirname, ".."),
|
|
28
|
+
tmp: os.tmpdir(),
|
|
29
|
+
uid: typeof process.getuid === "function" ? process.getuid() : -1,
|
|
30
|
+
version: BUILT_IN.version,
|
|
31
|
+
installerSha256: BUILT_IN.installerSha256,
|
|
32
|
+
environ: process.env,
|
|
33
|
+
out: (s) => process.stdout.write(s),
|
|
34
|
+
err: (s) => process.stderr.write(s),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function helpText(e) {
|
|
39
|
+
return [
|
|
40
|
+
`wardensec ${e.version}: runs the Warden installer this package carries.`,
|
|
41
|
+
`install.sh sha256 ${e.installerSha256}; compare it with the installer lines of ${TRUST_RECORD}`,
|
|
42
|
+
"Any other arguments go to the installer: --now, --root-sha256=<hex>.",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `npx` puts every ./node_modules/.bin from here up the tree in front of PATH and adds npm_* and NODE_* variables.
|
|
48
|
+
// install.sh calls ssh-keygen, sha256sum, awk and curl through PATH, so a project folder must not choose them: the
|
|
49
|
+
// installer gets the PATH the user's shell had, and no variable that npm or node added.
|
|
50
|
+
function cleanEnv(environ) {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [k, v] of Object.entries(environ)) {
|
|
53
|
+
if (/^(npm_|NODE_)/i.test(k) || k === "NODE" || k === "INIT_CWD") continue;
|
|
54
|
+
out[k] = v;
|
|
55
|
+
}
|
|
56
|
+
const key = Object.keys(out).find((k) => k.toUpperCase() === "PATH");
|
|
57
|
+
if (key !== undefined && typeof out[key] === "string") {
|
|
58
|
+
out[key] = out[key]
|
|
59
|
+
.split(path.delimiter)
|
|
60
|
+
.filter((d) => !/(^|[\\/])node_modules[\\/]\.bin[\\/]*$/.test(d))
|
|
61
|
+
.join(path.delimiter);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasShell(sh) {
|
|
67
|
+
try {
|
|
68
|
+
fs.accessSync(sh, fs.constants.X_OK);
|
|
69
|
+
return fs.statSync(sh).isFile();
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function privateDir(e) {
|
|
76
|
+
const dir = fs.mkdtempSync(path.join(e.tmp, "wardensec-"));
|
|
77
|
+
try {
|
|
78
|
+
fs.chmodSync(dir, 0o700);
|
|
79
|
+
const st = fs.lstatSync(dir);
|
|
80
|
+
if (!st.isDirectory() || st.uid !== e.uid || (st.mode & 0o777) !== 0o700) throw new Error("not private");
|
|
81
|
+
} catch (err) {
|
|
82
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
return dir;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Runs the package. Returns { code } or { signal }; the caller exits with it. */
|
|
89
|
+
function main(env) {
|
|
90
|
+
const e = { ...defaults(), ...env };
|
|
91
|
+
if (e.argv.length === 1 && (e.argv[0] === "--help" || e.argv[0] === "--version")) {
|
|
92
|
+
e.out(helpText(e));
|
|
93
|
+
return { code: 0 };
|
|
94
|
+
}
|
|
95
|
+
if (e.platform === "win32" || !hasShell(e.sh)) {
|
|
96
|
+
e.err(NO_SHELL);
|
|
97
|
+
return { code: 1 };
|
|
98
|
+
}
|
|
99
|
+
let bytes;
|
|
100
|
+
try {
|
|
101
|
+
bytes = fs.readFileSync(path.join(e.pkgDir, "install.sh"));
|
|
102
|
+
} catch {
|
|
103
|
+
bytes = Buffer.alloc(0);
|
|
104
|
+
}
|
|
105
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== e.installerSha256) {
|
|
106
|
+
e.err(
|
|
107
|
+
`wardensec: the install.sh in this package does not match the sha256 it names; nothing ran. Reinstall it, or run: ${CURL_LINE}\n`,
|
|
108
|
+
);
|
|
109
|
+
return { code: 1 };
|
|
110
|
+
}
|
|
111
|
+
let dir;
|
|
112
|
+
try {
|
|
113
|
+
dir = privateDir(e);
|
|
114
|
+
} catch {
|
|
115
|
+
e.err("wardensec: could not make a private temporary directory; nothing ran.\n");
|
|
116
|
+
return { code: 1 };
|
|
117
|
+
}
|
|
118
|
+
const ignore = () => {
|
|
119
|
+
// Wait for the installer; the signal reaches it directly.
|
|
120
|
+
};
|
|
121
|
+
const signals = ["SIGINT", "SIGTERM"];
|
|
122
|
+
try {
|
|
123
|
+
const file = path.join(dir, "install.sh");
|
|
124
|
+
fs.writeFileSync(file, bytes, { flag: "wx", mode: 0o600 });
|
|
125
|
+
// The terminal sends Ctrl-C to the installer too; this process waits for it and reports how it ended.
|
|
126
|
+
for (const s of signals) process.on(s, ignore);
|
|
127
|
+
const r = childProcess.spawnSync(e.sh, [file, ...e.argv], { stdio: "inherit", env: cleanEnv(e.environ) });
|
|
128
|
+
if (r.error) {
|
|
129
|
+
e.err(`wardensec: could not start sh: ${r.error.message}\n`);
|
|
130
|
+
return { code: 1 };
|
|
131
|
+
}
|
|
132
|
+
if (r.signal) return { signal: r.signal };
|
|
133
|
+
return { code: r.status === null ? 1 : r.status };
|
|
134
|
+
} finally {
|
|
135
|
+
for (const s of signals) process.removeListener(s, ignore);
|
|
136
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Ends this process the way the installer ended: its exit code, or its signal re-raised. */
|
|
141
|
+
function finish(r, p) {
|
|
142
|
+
if (r.signal) {
|
|
143
|
+
p.kill(p.pid, r.signal);
|
|
144
|
+
// A signal this process ignores (SIGPIPE) leaves it running: report what a shell reports, 128 plus the number.
|
|
145
|
+
p.exitCode = 128 + (os.constants.signals[r.signal] || 0);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
p.exitCode = r.code === undefined ? 1 : r.code;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (require.main === module) finish(main(), process);
|
|
152
|
+
|
|
153
|
+
module.exports = { main, helpText, cleanEnv, finish };
|
package/install.sh
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Warden installer, served at https://wardensec.ai/install (distribution design 3.7).
|
|
3
|
+
#
|
|
4
|
+
# It fetches only from https://wardensec.ai/releases/, checks every record against the built-in
|
|
5
|
+
# root 1 with the shell verifier below, and installs one file, bin/warden, with its trust files
|
|
6
|
+
# under ${XDG_DATA_HOME:-$HOME/.local/share}/warden, plus at most one symlink in ~/.local/bin.
|
|
7
|
+
# No sudo, no package manager, no shell start-up file, no eval, nothing in the background, and
|
|
8
|
+
# nothing about this machine is sent anywhere. No byte runs before its hash matched a signed record.
|
|
9
|
+
#
|
|
10
|
+
# v1.0: the release log has no witnesses yet. This script requires the release signatures only and
|
|
11
|
+
# refuses a root that asks for witness cosignatures. It prints the root fingerprints so you can
|
|
12
|
+
# compare them with the ones published outside this website (https://wardensec.ai/docs/verify-install
|
|
13
|
+
# names where). Given --root-sha256=<hex>, it compares root 1 before it fetches anything. Every check
|
|
14
|
+
# here runs inside this script, so none of them can catch a tampered copy of it: compare this file's
|
|
15
|
+
# sha256 with the installer lines of the public trust record before you run it. It never starts setup.
|
|
16
|
+
#
|
|
17
|
+
# The whole body runs from main(), called on the last line, so a cut download runs nothing.
|
|
18
|
+
|
|
19
|
+
set -eu
|
|
20
|
+
|
|
21
|
+
# >>> sshsig_ok from launcher/sshsig-ok.sh sha256=73144cc52a1ff058ed24e2d460e3dbf914f4ce980055ec39706e617f9f2e334b (generated; edit the source, not this copy)
|
|
22
|
+
# shellcheck shell=sh
|
|
23
|
+
# sshsig_ok: the shell's one verdict on an SSHSIG signature (distribution 3.7 step 2, 12.3 L5).
|
|
24
|
+
#
|
|
25
|
+
# This file is the one source. tools/launcher.ts embeds it, byte for byte, in the launcher and the one-line
|
|
26
|
+
# installer; never edit an embedded copy. POSIX sh: it runs under dash and bash --posix, and needs only ssh-keygen,
|
|
27
|
+
# base64 (-d or -D), od, tr, wc, awk and mktemp.
|
|
28
|
+
#
|
|
29
|
+
# ssh-keygen -Y verify checks the cryptography, the namespace and the principal, but enforces neither user presence,
|
|
30
|
+
# user verification nor the signature counter, and tolerates armour and fields the TypeScript verifier
|
|
31
|
+
# (src/release/sshsig.ts) refuses. So this function also reads the signature itself: strict armour, the exact blob
|
|
32
|
+
# structure for the named key and namespace (version 1, empty reserved field, sha512, nothing trailing), and then the
|
|
33
|
+
# five bytes the authenticator signed after the signature: flags must be exactly UP|UV (0x05) and the counter above 0
|
|
34
|
+
# and above the stored one. src/release/sshsig.differential.test.ts holds this verdict equal to the TypeScript one.
|
|
35
|
+
#
|
|
36
|
+
# sshsig_ok NAMESPACE KID ALLOWED_SIGNERS SIG MESSAGE [STORED_COUNTER]
|
|
37
|
+
#
|
|
38
|
+
# ALLOWED_SIGNERS holds one line per key, `<kid> namespaces="<namespace>" <type> <base64 key>`, built by the caller
|
|
39
|
+
# from verified roots; KID names the one line whose key must have made SIG over the bytes of MESSAGE.
|
|
40
|
+
# STORED_COUNTER (decimal, default 0) is the counter already seen for KID; an index signature must exceed it.
|
|
41
|
+
# 0 the signature counts. SSHSIG_COUNTER holds its counter (empty for an ssh-ed25519 key, which has none), and
|
|
42
|
+
# SSHSIG_CLASS the key's class from its type alone: `software` for ssh-ed25519 (a password
|
|
43
|
+
# manager's SSH agent, a file, a witness), `security_key` for sk-ssh-ed25519@openssh.com. Empty on any refusal.
|
|
44
|
+
# 1 it does not. SSHSIG_REFUSED holds one closed code: TOOL ARMOUR OVERSIZE STRUCTURE KEY VERIFY FLAGS COUNTER
|
|
45
|
+
# COUNTER_STALE.
|
|
46
|
+
# 2 the caller's arguments are wrong (SSHSIG_REFUSED=ARGS): bad namespace or kid shape, no single matching
|
|
47
|
+
# allowed line, a stored counter that is not plain decimal, or a stored counter for a key that has none.
|
|
48
|
+
# It writes nothing to stdout or stderr and never evaluates what it reads.
|
|
49
|
+
|
|
50
|
+
SSHSIG_COUNTER=
|
|
51
|
+
SSHSIG_CLASS=
|
|
52
|
+
SSHSIG_REFUSED=
|
|
53
|
+
_wd_b64_flag=
|
|
54
|
+
|
|
55
|
+
# Chooses the decoder once per shell: `base64 -d` (GNU, BusyBox, recent macOS) or `base64 -D` (older macOS).
|
|
56
|
+
_wd_b64_init() {
|
|
57
|
+
[ -n "$_wd_b64_flag" ] && return 0
|
|
58
|
+
if [ "$(printf 'QQ==' | base64 -d 2>/dev/null)" = A ]; then
|
|
59
|
+
_wd_b64_flag=-d
|
|
60
|
+
elif [ "$(printf 'QQ==' | base64 -D 2>/dev/null)" = A ]; then
|
|
61
|
+
_wd_b64_flag=-D
|
|
62
|
+
else
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_wd_b64d() {
|
|
68
|
+
base64 "$_wd_b64_flag" 2>/dev/null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_wd_hex() {
|
|
72
|
+
od -An -tx1 -v | tr -d ' \t\n'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sshsig_ok() {
|
|
76
|
+
SSHSIG_COUNTER=
|
|
77
|
+
SSHSIG_CLASS=
|
|
78
|
+
SSHSIG_REFUSED=ARGS
|
|
79
|
+
[ "$#" -eq 5 ] || [ "$#" -eq 6 ] || return 2
|
|
80
|
+
_wd_ns=$1
|
|
81
|
+
_wd_kid=$2
|
|
82
|
+
_wd_as=$3
|
|
83
|
+
_wd_sig=$4
|
|
84
|
+
_wd_msg=$5
|
|
85
|
+
_wd_min=${6-0}
|
|
86
|
+
case $_wd_ns in
|
|
87
|
+
'' | *[!a-z0-9-]*) return 2 ;;
|
|
88
|
+
esac
|
|
89
|
+
case $_wd_kid in
|
|
90
|
+
wd[rw]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;;
|
|
91
|
+
*) return 2 ;;
|
|
92
|
+
esac
|
|
93
|
+
case $_wd_min in
|
|
94
|
+
'' | *[!0-9]* | 0?*) return 2 ;;
|
|
95
|
+
esac
|
|
96
|
+
[ "${#_wd_min}" -le 10 ] && [ "$_wd_min" -le 4294967295 ] || return 2
|
|
97
|
+
[ -f "$_wd_as" ] && [ -r "$_wd_as" ] && [ -f "$_wd_msg" ] && [ -r "$_wd_msg" ] || return 2
|
|
98
|
+
|
|
99
|
+
# The one allowed line for the kid, with exactly this namespace option.
|
|
100
|
+
_wd_line=$(LC_ALL=C awk -v k="$_wd_kid" -v o="namespaces=\"$_wd_ns\"" '
|
|
101
|
+
$1 == k { n++; if (NF == 4 && $2 == o) t = $3 " " $4; else t = "" }
|
|
102
|
+
END { if (n == 1 && t != "") print t }' "$_wd_as") || return 2
|
|
103
|
+
[ -n "$_wd_line" ] || return 2
|
|
104
|
+
_wd_type=${_wd_line%% *}
|
|
105
|
+
_wd_kb64=${_wd_line#* }
|
|
106
|
+
case $_wd_type in
|
|
107
|
+
sk-ssh-ed25519@openssh.com) _wd_sk=1 ;;
|
|
108
|
+
ssh-ed25519)
|
|
109
|
+
_wd_sk=0
|
|
110
|
+
[ "$_wd_min" = 0 ] || return 2
|
|
111
|
+
;;
|
|
112
|
+
*) return 2 ;;
|
|
113
|
+
esac
|
|
114
|
+
case $_wd_kb64 in
|
|
115
|
+
'' | *[!A-Za-z0-9+/=]*) return 2 ;;
|
|
116
|
+
esac
|
|
117
|
+
_wd_b64_init || {
|
|
118
|
+
SSHSIG_REFUSED=TOOL
|
|
119
|
+
return 1
|
|
120
|
+
}
|
|
121
|
+
_wd_khex=$(printf '%s' "$_wd_kb64" | _wd_b64d | _wd_hex) || return 2
|
|
122
|
+
_wd_thex=$(printf '%08x' "${#_wd_type}")$(printf '%s' "$_wd_type" | _wd_hex) || return 2
|
|
123
|
+
case $_wd_khex in
|
|
124
|
+
"$_wd_thex"00000020*) ;;
|
|
125
|
+
*) return 2 ;;
|
|
126
|
+
esac
|
|
127
|
+
if [ "$_wd_sk" = 1 ]; then
|
|
128
|
+
# string(type) string(32-byte key) string(application)
|
|
129
|
+
[ "${#_wd_khex}" -ge 140 ] || return 2
|
|
130
|
+
_wd_al=$(printf '%s' "$_wd_khex" | cut -c 133-140) || return 2
|
|
131
|
+
[ "${#_wd_khex}" -eq $((140 + 2 * 0x$_wd_al)) ] || return 2
|
|
132
|
+
else
|
|
133
|
+
[ "${#_wd_khex}" -eq 102 ] || return 2
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Strict armour: printable ASCII and LF only, exact header and footer, body lines of 1 to 76 base64 characters,
|
|
137
|
+
# canonical base64, at most 4096 bytes of text and 1024 bytes of blob.
|
|
138
|
+
[ -f "$_wd_sig" ] && [ -r "$_wd_sig" ] || {
|
|
139
|
+
SSHSIG_REFUSED=ARMOUR
|
|
140
|
+
return 1
|
|
141
|
+
}
|
|
142
|
+
_wd_size=$(wc -c <"$_wd_sig" | tr -d ' \t') || return 1
|
|
143
|
+
[ "$_wd_size" -le 4096 ] || {
|
|
144
|
+
SSHSIG_REFUSED=OVERSIZE
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
SSHSIG_REFUSED=ARMOUR
|
|
148
|
+
_wd_bad=$(LC_ALL=C tr -d '\n\040-\176' <"$_wd_sig" | wc -c | tr -d ' \t') || return 1
|
|
149
|
+
[ "$_wd_bad" = 0 ] || return 1
|
|
150
|
+
_wd_b64=$(LC_ALL=C awk '
|
|
151
|
+
{ l[NR] = $0 }
|
|
152
|
+
END {
|
|
153
|
+
if (NR < 3 || l[1] != "-----BEGIN SSH SIGNATURE-----" || l[NR] != "-----END SSH SIGNATURE-----") exit 1
|
|
154
|
+
s = ""
|
|
155
|
+
for (i = 2; i < NR; i++) {
|
|
156
|
+
if (length(l[i]) > 76 || l[i] !~ /^[A-Za-z0-9+\/=]+$/) exit 1
|
|
157
|
+
s = s l[i]
|
|
158
|
+
}
|
|
159
|
+
if (s !~ /^[A-Za-z0-9+\/]+(=|==)?$/ || length(s) % 4 != 0) exit 1
|
|
160
|
+
print s
|
|
161
|
+
}' "$_wd_sig") || return 1
|
|
162
|
+
[ -n "$_wd_b64" ] || return 1
|
|
163
|
+
case $_wd_b64 in
|
|
164
|
+
*==) _wd_pad=2 ;;
|
|
165
|
+
*=) _wd_pad=1 ;;
|
|
166
|
+
*) _wd_pad=0 ;;
|
|
167
|
+
esac
|
|
168
|
+
[ $((${#_wd_b64} / 4 * 3 - _wd_pad)) -le 1024 ] || {
|
|
169
|
+
SSHSIG_REFUSED=OVERSIZE
|
|
170
|
+
return 1
|
|
171
|
+
}
|
|
172
|
+
_wd_re=$(printf '%s' "$_wd_b64" | _wd_b64d | base64 | tr -d '\r\n') || return 1
|
|
173
|
+
[ "$_wd_re" = "$_wd_b64" ] || return 1
|
|
174
|
+
_wd_shex=$(printf '%s' "$_wd_b64" | _wd_b64d | _wd_hex) || return 1
|
|
175
|
+
|
|
176
|
+
# The exact structure: "SSHSIG", version 1, the named key, the namespace, an empty reserved field, sha512, then a
|
|
177
|
+
# signature blob of the key's type holding a 64-byte signature and, for an sk key, the flags and counter.
|
|
178
|
+
_wd_head=53534853494700000001
|
|
179
|
+
case $_wd_shex in
|
|
180
|
+
"$_wd_head"*) ;;
|
|
181
|
+
*)
|
|
182
|
+
SSHSIG_REFUSED=STRUCTURE
|
|
183
|
+
return 1
|
|
184
|
+
;;
|
|
185
|
+
esac
|
|
186
|
+
_wd_want=$_wd_head$(printf '%08x' $((${#_wd_khex} / 2)))$_wd_khex
|
|
187
|
+
case $_wd_shex in
|
|
188
|
+
"$_wd_want"*) ;;
|
|
189
|
+
*)
|
|
190
|
+
SSHSIG_REFUSED=KEY
|
|
191
|
+
return 1
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
if [ "$_wd_sk" = 1 ]; then
|
|
195
|
+
_wd_slen=103
|
|
196
|
+
_wd_tail=10
|
|
197
|
+
else
|
|
198
|
+
_wd_slen=83
|
|
199
|
+
_wd_tail=0
|
|
200
|
+
fi
|
|
201
|
+
_wd_nshex=$(printf '%s' "$_wd_ns" | _wd_hex) || return 1
|
|
202
|
+
_wd_want=$_wd_want$(printf '%08x' "${#_wd_ns}")$_wd_nshex
|
|
203
|
+
_wd_want=${_wd_want}00000000
|
|
204
|
+
_wd_want=${_wd_want}00000006736861353132
|
|
205
|
+
_wd_want=$_wd_want$(printf '%08x' "$_wd_slen")$_wd_thex
|
|
206
|
+
_wd_want=${_wd_want}00000040
|
|
207
|
+
SSHSIG_REFUSED=STRUCTURE
|
|
208
|
+
case $_wd_shex in
|
|
209
|
+
"$_wd_want"*) ;;
|
|
210
|
+
*) return 1 ;;
|
|
211
|
+
esac
|
|
212
|
+
[ "${#_wd_shex}" -eq $((${#_wd_want} + 128 + _wd_tail)) ] || return 1
|
|
213
|
+
|
|
214
|
+
SSHSIG_REFUSED=VERIFY
|
|
215
|
+
ssh-keygen -Y verify -n "$_wd_ns" -I "$_wd_kid" -f "$_wd_as" -s "$_wd_sig" <"$_wd_msg" >/dev/null 2>&1 || return 1
|
|
216
|
+
|
|
217
|
+
if [ "$_wd_sk" = 1 ]; then
|
|
218
|
+
# The last five bytes, covered by the signature ssh-keygen has just checked: flags, then the counter (big-endian).
|
|
219
|
+
_wd_fc=${_wd_shex#"${_wd_shex%??????????}"}
|
|
220
|
+
_wd_flags=${_wd_fc%????????}
|
|
221
|
+
_wd_ctr=$((0x${_wd_fc#??}))
|
|
222
|
+
SSHSIG_REFUSED=FLAGS
|
|
223
|
+
[ "$_wd_flags" = 05 ] || return 1
|
|
224
|
+
SSHSIG_REFUSED=COUNTER
|
|
225
|
+
[ "$_wd_ctr" -gt 0 ] || return 1
|
|
226
|
+
SSHSIG_REFUSED=COUNTER_STALE
|
|
227
|
+
[ "$_wd_ctr" -gt "$_wd_min" ] || return 1
|
|
228
|
+
SSHSIG_COUNTER=$_wd_ctr
|
|
229
|
+
SSHSIG_CLASS=security_key
|
|
230
|
+
else
|
|
231
|
+
SSHSIG_CLASS=software
|
|
232
|
+
fi
|
|
233
|
+
SSHSIG_REFUSED=
|
|
234
|
+
return 0
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# The self-test (3.7 step 2): four built-in vectors over one built-in sk-ssh-ed25519 key. Flags 0x05 with counter 7
|
|
238
|
+
# must count; flags 0x01, 0x04 and 0x00 must each be refused by the flag reader after ssh-keygen accepted them. It
|
|
239
|
+
# runs in a subshell, in a private mktemp directory removed on exit, and touches nothing else.
|
|
240
|
+
sshsig_selftest() (
|
|
241
|
+
_wd_d=$(mktemp -d 2>/dev/null) || exit 1
|
|
242
|
+
[ -n "$_wd_d" ] && [ -d "$_wd_d" ] || exit 1
|
|
243
|
+
trap 'rm -rf "$_wd_d"' EXIT
|
|
244
|
+
trap 'exit 1' HUP INT TERM
|
|
245
|
+
_wd_k=wdr-4943def14ec1d7f0
|
|
246
|
+
_wd_n=wardensec-release-v1
|
|
247
|
+
printf '%s\n' 'wardensec sshsig fixture' >"$_wd_d/m"
|
|
248
|
+
printf '%s\n' 'wdr-4943def14ec1d7f0 namespaces="wardensec-release-v1" sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAICia/v4KKmAwny1nGulB8OALFvkREDhrsovegylyCr/TAAAAFXNzaDp3YXJkZW5zZWMtcmVsZWFzZQ==' >"$_wd_d/a"
|
|
249
|
+
cat >"$_wd_d/05" <<'EOF'
|
|
250
|
+
-----BEGIN SSH SIGNATURE-----
|
|
251
|
+
U1NIU0lHAAAAAQAAAFsAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgKJr+/g
|
|
252
|
+
oqYDCfLWca6UHw4AsW+REQOGuyi96DKXIKv9MAAAAVc3NoOndhcmRlbnNlYy1yZWxlYXNl
|
|
253
|
+
AAAAFHdhcmRlbnNlYy1yZWxlYXNlLXYxAAAAAAAAAAZzaGE1MTIAAABnAAAAGnNrLXNzaC
|
|
254
|
+
1lZDI1NTE5QG9wZW5zc2guY29tAAAAQFYGQMLta9uAYU6ML3UfQn0c11b2h8NROW3c5uQb
|
|
255
|
+
Uo+vK7lr0FJN5FHPsM2546fb9VdU7AtL+M59u5y6rUCCfw8FAAAABw==
|
|
256
|
+
-----END SSH SIGNATURE-----
|
|
257
|
+
EOF
|
|
258
|
+
cat >"$_wd_d/01" <<'EOF'
|
|
259
|
+
-----BEGIN SSH SIGNATURE-----
|
|
260
|
+
U1NIU0lHAAAAAQAAAFsAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgKJr+/g
|
|
261
|
+
oqYDCfLWca6UHw4AsW+REQOGuyi96DKXIKv9MAAAAVc3NoOndhcmRlbnNlYy1yZWxlYXNl
|
|
262
|
+
AAAAFHdhcmRlbnNlYy1yZWxlYXNlLXYxAAAAAAAAAAZzaGE1MTIAAABnAAAAGnNrLXNzaC
|
|
263
|
+
1lZDI1NTE5QG9wZW5zc2guY29tAAAAQKYYoMWy25Ek/TRiZXZUtHEL2ACbqgLdakDZ2fjc
|
|
264
|
+
HX8hpsJH0QuDNmCaXkU1xjcP+5FFL9flyruPuVo8yASySw0BAAAABw==
|
|
265
|
+
-----END SSH SIGNATURE-----
|
|
266
|
+
EOF
|
|
267
|
+
cat >"$_wd_d/04" <<'EOF'
|
|
268
|
+
-----BEGIN SSH SIGNATURE-----
|
|
269
|
+
U1NIU0lHAAAAAQAAAFsAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgKJr+/g
|
|
270
|
+
oqYDCfLWca6UHw4AsW+REQOGuyi96DKXIKv9MAAAAVc3NoOndhcmRlbnNlYy1yZWxlYXNl
|
|
271
|
+
AAAAFHdhcmRlbnNlYy1yZWxlYXNlLXYxAAAAAAAAAAZzaGE1MTIAAABnAAAAGnNrLXNzaC
|
|
272
|
+
1lZDI1NTE5QG9wZW5zc2guY29tAAAAQLC5HvpXVdqOhDGufOQY88kLch8uMRI9QpX2sXL9
|
|
273
|
+
1n+vjo3Q33kwBMAzQHwxOPzTYNW0zqfgWjZQNfT8Ajstrg8EAAAABw==
|
|
274
|
+
-----END SSH SIGNATURE-----
|
|
275
|
+
EOF
|
|
276
|
+
cat >"$_wd_d/00" <<'EOF'
|
|
277
|
+
-----BEGIN SSH SIGNATURE-----
|
|
278
|
+
U1NIU0lHAAAAAQAAAFsAAAAac2stc3NoLWVkMjU1MTlAb3BlbnNzaC5jb20AAAAgKJr+/g
|
|
279
|
+
oqYDCfLWca6UHw4AsW+REQOGuyi96DKXIKv9MAAAAVc3NoOndhcmRlbnNlYy1yZWxlYXNl
|
|
280
|
+
AAAAFHdhcmRlbnNlYy1yZWxlYXNlLXYxAAAAAAAAAAZzaGE1MTIAAABnAAAAGnNrLXNzaC
|
|
281
|
+
1lZDI1NTE5QG9wZW5zc2guY29tAAAAQF4YP7F3B4WfhVoXkr7hjxzmjmcJb3RJ2bsCHV9t
|
|
282
|
+
RURUTTXWOcZJSiVHVf5bWaQdKYONT4/Bqttasw1XZtepUAwAAAAABw==
|
|
283
|
+
-----END SSH SIGNATURE-----
|
|
284
|
+
EOF
|
|
285
|
+
sshsig_ok "$_wd_n" "$_wd_k" "$_wd_d/a" "$_wd_d/05" "$_wd_d/m" || exit 1
|
|
286
|
+
[ "$SSHSIG_COUNTER" = 7 ] && [ "$SSHSIG_CLASS" = security_key ] || exit 1
|
|
287
|
+
for _wd_v in 01 04 00; do
|
|
288
|
+
if sshsig_ok "$_wd_n" "$_wd_k" "$_wd_d/a" "$_wd_d/$_wd_v" "$_wd_d/m"; then exit 1; fi
|
|
289
|
+
[ "$SSHSIG_REFUSED" = FLAGS ] || exit 1
|
|
290
|
+
done
|
|
291
|
+
exit 0
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# The self-test, with the one sentence a person sees when this machine cannot verify the way Warden needs.
|
|
295
|
+
sshsig_require_selftest() {
|
|
296
|
+
if sshsig_selftest; then
|
|
297
|
+
return 0
|
|
298
|
+
fi
|
|
299
|
+
printf '%s\n' "This machine's OpenSSH cannot verify security-key signatures the way Warden needs (8.2 or later is needed). Nothing was installed." >&2
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
# <<< sshsig_ok
|
|
303
|
+
|
|
304
|
+
# The software-key self-test (amendment 02 B4.5), beside the four sk vectors above: one ssh-ed25519 signature made by
|
|
305
|
+
# ssh-keygen must count, with no counter, and the same signature over other bytes must not.
|
|
306
|
+
in_selftest_soft() (
|
|
307
|
+
in_d=$(mktemp -d 2>/dev/null) && [ -n "$in_d" ] && [ -d "$in_d" ] || exit 1
|
|
308
|
+
trap 'rm -rf "$in_d"' EXIT
|
|
309
|
+
trap 'exit 1' HUP INT TERM
|
|
310
|
+
in_k=wdr-c956dff44e52ea56
|
|
311
|
+
printf '%s\n' 'wdr-c956dff44e52ea56 namespaces="wardensec-release-v1" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBEfZCqnq84xy0t2BUTKqofV0oE/Fk2TyEYTkeNB7dtm' >"$in_d/a"
|
|
312
|
+
printf '%s\n' 'wardensec sshsig fixture' >"$in_d/m"
|
|
313
|
+
cat >"$in_d/s" <<'EOF'
|
|
314
|
+
-----BEGIN SSH SIGNATURE-----
|
|
315
|
+
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgER9kKqerzjHLS3YFRMqqh9XSgT
|
|
316
|
+
8WTZPIRhOR40Ht22YAAAAUd2FyZGVuc2VjLXJlbGVhc2UtdjEAAAAAAAAABnNoYTUxMgAA
|
|
317
|
+
AFMAAAALc3NoLWVkMjU1MTkAAABAZKPSaQr/vz1A6RySaYLItCG/erBJGh4HTukig3Kd3J
|
|
318
|
+
i2K7suY9VntNz0sVc8egbHzqGfvOJyy731t7JdY/TuBg==
|
|
319
|
+
-----END SSH SIGNATURE-----
|
|
320
|
+
EOF
|
|
321
|
+
sshsig_ok wardensec-release-v1 "$in_k" "$in_d/a" "$in_d/s" "$in_d/m" && [ -z "$SSHSIG_COUNTER" ] || exit 1
|
|
322
|
+
printf '%s\n' 'wardensec sshsig fixturf' >"$in_d/m"
|
|
323
|
+
if sshsig_ok wardensec-release-v1 "$in_k" "$in_d/a" "$in_d/s" "$in_d/m"; then exit 1; fi
|
|
324
|
+
[ "$SSHSIG_REFUSED" = VERIFY ]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Root 1, built in. Every later root, index and release must chain to it.
|
|
328
|
+
in_root1() {
|
|
329
|
+
cat <<'WD_ROOT_1'
|
|
330
|
+
wardensec-root 1
|
|
331
|
+
serial 1
|
|
332
|
+
issued 2026-10-31T20:04:01Z
|
|
333
|
+
threshold root 2
|
|
334
|
+
threshold release 2
|
|
335
|
+
threshold witness 0
|
|
336
|
+
distinct-custody
|
|
337
|
+
key wdr-7a1d75603ba41504 root ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGJsDCHa2CqJAUYF2WaPSNHCj6UgNSN3oyrZHlYlfuI9
|
|
338
|
+
key wdr-dc7dffa7195fda90 root ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHY7aN44mL2Ubiw3L7sXXNQq/l5/9z03b5KZ4fZLlJsM
|
|
339
|
+
key wdr-617fb4b50c6681a6 release ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHgicMETDmigeeAMQHwWqaSIiPPraQLnf6Elq5bKoJZf
|
|
340
|
+
key wdr-3184059dcd400670 release ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH4/Hy5Gq9hkHeSPDkntlkqFdXVtOIK0NWFtwCakwtTq
|
|
341
|
+
custody wdr-7a1d75603ba41504 vault-root
|
|
342
|
+
custody wdr-dc7dffa7195fda90 kms-root
|
|
343
|
+
custody wdr-617fb4b50c6681a6 vault-a
|
|
344
|
+
custody wdr-3184059dcd400670 kms-1
|
|
345
|
+
end
|
|
346
|
+
WD_ROOT_1
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
in_say() { printf '%s\n' "$@"; }
|
|
350
|
+
|
|
351
|
+
# Every refusal is a closed code and a fixed sentence; nothing fetched is ever printed.
|
|
352
|
+
in_die() { printf '%s\n' "Warden install stopped ($1): $2" "Nothing was installed." >&2; exit 1; }
|
|
353
|
+
|
|
354
|
+
in_sha() {
|
|
355
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum <"$1"; else shasum -a 256 <"$1"; fi | awk '{ print $1 }'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
in_hex() { printf '%s\n' "$1" | LC_ALL=C grep -Eqx '[0-9a-f]{64}'; }
|
|
359
|
+
|
|
360
|
+
in_int() { case $1 in '' | *[!0-9]* | 0*) return 1 ;; esac && [ "${#1}" -le 9 ]; }
|
|
361
|
+
|
|
362
|
+
in_iso() { case $1 in [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]Z) ;; *) return 1 ;; esac; }
|
|
363
|
+
|
|
364
|
+
# A release name as the index may give it: v<major>.<minor>.<patch>, then only lowercase letters, digits, dots, dashes.
|
|
365
|
+
in_relname() { case $1 in v[0-9]*.[0-9]*.[0-9]*) case $1 in *[!0-9a-z.-]*) return 1 ;; esac ;; *) return 1 ;; esac; }
|
|
366
|
+
|
|
367
|
+
in_bad() { in_die "$1" "the $2 is not well formed."; }
|
|
368
|
+
|
|
369
|
+
# GET one release file into the work directory: one host, HTTPS only, no redirect, no query. in_code keeps the HTTP
|
|
370
|
+
# status the site answered (000 when no answer came); it is compared, never printed.
|
|
371
|
+
in_get() {
|
|
372
|
+
rm -f "$in_w/$2"
|
|
373
|
+
in_code=$(curl --proto '=https' --tlsv1.2 --max-redirs 0 --max-filesize 400000000 -fsS -A warden-installer \
|
|
374
|
+
-w '%{http_code}' -o "$in_w/$2" "https://wardensec.ai/releases/$1" 2>/dev/null)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# A text record: ASCII, LF only, single spaces, no blank line, at most 64 KiB, and `end` last.
|
|
378
|
+
in_record() {
|
|
379
|
+
[ -f "$1" ] && in_sz=$(wc -c <"$1" | tr -d ' ') && [ "$in_sz" -gt 0 ] && [ "$in_sz" -le 65536 ] || return 1
|
|
380
|
+
[ "$(LC_ALL=C tr -d '\n -~' <"$1" | wc -c | tr -d ' ')" = 0 ] || return 1
|
|
381
|
+
[ "$(tail -c 1 "$1" | od -An -tx1 | tr -d ' \n')" = 0a ] || return 1
|
|
382
|
+
LC_ALL=C awk '$0 == "" || /^ / || / $/ || / / { bad = 1 } { last = $0 } END { exit (bad || last != "end") }' "$1"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# The value of the one two-field line `KEY value`; fails when there is none or more than one.
|
|
386
|
+
in_val() { LC_ALL=C awk -v k="$2" '$1 == k { n++; v = $2; f = NF } END { if (n != 1 || f != 2) exit 1; print v }' "$1"; }
|
|
387
|
+
|
|
388
|
+
in_thr() { LC_ALL=C awk -v r="$2" '$1 == "threshold" && $2 == r { print $3 }' "$1"; }
|
|
389
|
+
|
|
390
|
+
# A root file's shape (amendment 02 B4.3): prints ok, test (its `test` line: never trusted), below (a root or release
|
|
391
|
+
# key under the class its `require` line asks), witness (it asks for cosignatures this installer cannot check yet) or
|
|
392
|
+
# bad. Lines come in the product's order. A key's class is its type: ssh-ed25519 is a software key (0),
|
|
393
|
+
# sk-ssh-ed25519@openssh.com a security key (1); no attestation is verified here, so none is ever class 2. The
|
|
394
|
+
# advisory feed's keys (amendment 05 3.4) are accepted and never counted: zero to four `key <wdc-kid> curator` lines
|
|
395
|
+
# after the witnesses, zero or two to four `spki <wdf-kid> feed <base64 DER>` lines after them, a `custody` line for
|
|
396
|
+
# each feed key (at least two labels among them) and optionally each curator, in key order, and the `distrust` and
|
|
397
|
+
# `retire <wdf-kid> feed-seq <n>` forms. The product's parity vectors (tests/fixtures/root-grammar-vectors.json)
|
|
398
|
+
# hold this grammar to its parser.
|
|
399
|
+
in_root_check() {
|
|
400
|
+
if ! in_record "$1" || [ "$(head -n 1 "$1")" != "wardensec-root 1" ] || [ "$(in_val "$1" serial)" != "$2" ]; then
|
|
401
|
+
echo bad && return 0
|
|
402
|
+
fi
|
|
403
|
+
LC_ALL=C awk '
|
|
404
|
+
BEGIN {
|
|
405
|
+
n = split("wardensec-root test serial issued threshold require distinct-custody delay key spki attest custody log distrust retire reset end", o, " ")
|
|
406
|
+
for (i = 1; i <= n; i++) rk[o[i]] = i
|
|
407
|
+
split("threshold key spki attest custody distrust retire", m, " "); for (i in m) multi[m[i]] = 1
|
|
408
|
+
split("root release witness", tr, " "); ro["root"] = 1; ro["release"] = 2; ro["witness"] = 3; ro["curator"] = 4
|
|
409
|
+
}
|
|
410
|
+
!($1 in rk) || rk[$1] < last || (rk[$1] == last && !($1 in multi)) { bad = 1 }
|
|
411
|
+
{ last = rk[$1] }
|
|
412
|
+
$1 == "test" || $1 == "distinct-custody" || $1 == "end" { if (NF != 1) bad = 1; f[$1] = 1 }
|
|
413
|
+
$1 == "issued" { iss = 1; if (NF != 2 || $2 !~ /^[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]Z$/) bad = 1 }
|
|
414
|
+
$1 == "threshold" { if (NF != 3 || $2 != tr[++nt] || $3 !~ /^(0|[1-9][0-9]?[0-9]?)$/) bad = 1; t[$2] = $3 + 0 }
|
|
415
|
+
$1 == "require" { need = $2 == "security-key" ? 1 : $2 == "attested-security-key" ? 2 : 0; if (NF != 2 || !need) bad = 1 }
|
|
416
|
+
$1 == "delay" { if (NF != 2 || $2 !~ /^[1-9][0-9]*$/ || length($2) > 3 || $2 + 0 > 168) bad = 1 }
|
|
417
|
+
$1 == "key" {
|
|
418
|
+
if (NF != 5 || ($2 in seen) || ($5 in blob) || length($2) != 20 || !($3 in ro) || ro[$3] < lr) bad = 1
|
|
419
|
+
seen[$2] = 1; blob[$5] = 1; c[$3]++; lr = ro[$3]
|
|
420
|
+
if ($3 == "witness") { if ($2 !~ /^wdw-[0-9a-f]+$/ || $4 != "ssh-ed25519") bad = 1 }
|
|
421
|
+
else if ($3 == "root" || $3 == "release") {
|
|
422
|
+
if ($2 !~ /^wdr-[0-9a-f]+$/ || ($4 != "ssh-ed25519" && $4 != "sk-ssh-ed25519@openssh.com")) bad = 1
|
|
423
|
+
cls[$2] = $4 == "ssh-ed25519" ? 0 : 1; idx[$2] = ++ns; ci[$2] = ++nc; rl[$2] = $3; if (cls[$2] < 1) low = 1
|
|
424
|
+
} else if ($3 == "curator") {
|
|
425
|
+
if ($2 !~ /^wdc-[0-9a-f]+$/ || ($4 != "ssh-ed25519" && $4 != "sk-ssh-ed25519@openssh.com")) bad = 1
|
|
426
|
+
ci[$2] = ++nc
|
|
427
|
+
} else bad = 1
|
|
428
|
+
}
|
|
429
|
+
$1 == "spki" {
|
|
430
|
+
if (NF != 4 || $3 != "feed" || $2 !~ /^wdf-[0-9a-f]+$/ || length($2) != 20 || ($2 in seen) || ($4 in blob) || $4 !~ /^[A-Za-z0-9+\/]+=*$/) bad = 1
|
|
431
|
+
seen[$2] = 1; blob[$4] = 1; fd[$2] = 1; nf++; ci[$2] = ++nc
|
|
432
|
+
}
|
|
433
|
+
$1 == "attest" { if (NF != 3 || !($2 in cls) || cls[$2] != 1 || idx[$2] <= la || $3 !~ /^sha256:[0-9a-f]+$/ || length($3) != 71) bad = 1; la = idx[$2] }
|
|
434
|
+
$1 == "custody" {
|
|
435
|
+
if (NF != 3 || !($2 in ci) || ci[$2] <= lc || $3 !~ /^[a-z0-9][a-z0-9.-]*$/ || length($3) > 32) bad = 1
|
|
436
|
+
lc = ci[$2]
|
|
437
|
+
if ($2 in cls) { cu[$2] = $3; if (!((rl[$2], $3) in dc)) { dc[rl[$2], $3] = 1; nd[rl[$2]]++ } }
|
|
438
|
+
else if ($2 in fd) { fcu[$2] = 1; if (!($3 in fl)) { fl[$3] = 1; nfl++ } }
|
|
439
|
+
}
|
|
440
|
+
$1 == "distrust" {
|
|
441
|
+
if (NF != 3 || $2 !~ /^wd[rwcf]-[0-9a-f]+$/ || length($2) != 20 || ($2 in dis)) bad = 1
|
|
442
|
+
if ($3 !~ /^[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]Z$/) bad = 1
|
|
443
|
+
dis[$2] = 1
|
|
444
|
+
}
|
|
445
|
+
$1 == "retire" && $2 ~ /^wdf-/ {
|
|
446
|
+
if ($2 !~ /^wdf-[0-9a-f]+$/ || length($2) != 20 || ($2 in seen) || ($2 in rt) || NF != 4 || $3 != "feed-seq" || $4 !~ /^[1-9][0-9]*$/ || length($4) > 15) bad = 1
|
|
447
|
+
rt[$2] = 1; next
|
|
448
|
+
}
|
|
449
|
+
$1 == "retire" {
|
|
450
|
+
if ($2 !~ /^wdr-[0-9a-f]+$/ || length($2) != 20 || ($2 in seen) || ($2 in rt)) bad = 1
|
|
451
|
+
if (!(NF == 3 && $3 ~ /^(0|[1-9][0-9]*)$/ && length($3) <= 10 && $3 + 0 <= 4294967295) && !(NF == 6 && $3 == "serial" && $4 ~ /^[1-9][0-9]*$/ && $5 == "seq" && $6 ~ /^[1-9][0-9]*$/)) bad = 1
|
|
452
|
+
rt[$2] = 1
|
|
453
|
+
}
|
|
454
|
+
END {
|
|
455
|
+
if (nt != 3 || !iss) bad = 1
|
|
456
|
+
if (c["curator"] > 4 || nf == 1 || nf > 4 || (nf > 0 && nfl < 2)) bad = 1
|
|
457
|
+
for (k in fd) if (!(k in fcu)) bad = 1
|
|
458
|
+
for (i = 1; i <= 2; i++) { r = tr[i]; if (t[r] < (r == "release" && c[r] == 1 ? 1 : 2) || t[r] > c[r]) bad = 1 }
|
|
459
|
+
if (t["witness"] > c["witness"] || (!f["test"] && c["witness"] > 0 && t["witness"] < 1)) bad = 1
|
|
460
|
+
if (f["distinct-custody"]) { for (k in cls) if (!(k in cu)) bad = 1; if (nd["root"] < t["root"] || nd["release"] < t["release"]) bad = 1 }
|
|
461
|
+
print (bad ? "bad" : need > 1 || (need && low) ? "below" : f["test"] ? "test" : t["witness"] > 0 ? "witness" : "ok")
|
|
462
|
+
}' "$1"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# Counts the distinct ROLE keys of ROOTFILE whose signature on MSG counts (sshsig_ok), dropping keys the newest root
|
|
466
|
+
# distrusts and retired keys outside their retire line: a security key above its last counter, a software key on an
|
|
467
|
+
# index serial or release seq above its bounds (AT is serial:<n>, seq:<n> or -, for a root). Sets in_got and writes
|
|
468
|
+
# `<kid> <counter or -> <custody or -> <software|security>` lines to $in_w/got. Args: NAMESPACE ROLE ROOTFILE SIGPATH
|
|
469
|
+
# MSG NEWEST AT, STATE, whose `counter` lines each security key's counter must exceed (an index on a re-install), and
|
|
470
|
+
# RETIRES, the root whose retire lines apply (NEWEST if empty): on a root, the previous one, as a root's own retire
|
|
471
|
+
# lines bind from the next root on (verifyRootChain).
|
|
472
|
+
in_count() {
|
|
473
|
+
awk -v r="$2" -v ns="$1" '$1 == "key" && $3 == r { print $2 " namespaces=\"" ns "\" " $4 " " $5 }' "$3" >"$in_w/as"
|
|
474
|
+
in_got=0
|
|
475
|
+
: >"$in_w/got"
|
|
476
|
+
for in_k in $(awk '{ print $1 }' "$in_w/as"); do
|
|
477
|
+
in_ret=$(awk -v k="$in_k" -v at="$7" 'BEGIN { split(at, a, ":") }
|
|
478
|
+
f == 1 && $1 == "distrust" && $2 == k { x = 1 }
|
|
479
|
+
f == 2 && $1 == "retire" && $2 == k { if (NF == 3) print $3; else if (!(a[1] == "serial" && a[2] + 0 <= $4 + 0) && !(a[1] == "seq" && a[2] + 0 <= $6 + 0)) x = 1 }
|
|
480
|
+
END { exit x }' f=1 "$6" f=2 "${9:-$6}") || continue
|
|
481
|
+
in_cl=$(awk -v k="$in_k" '$1 == k { print ($3 == "ssh-ed25519" ? "software" : "security") }' "$in_w/as")
|
|
482
|
+
in_old=
|
|
483
|
+
[ "$in_cl" = software ] || in_old=$(awk -v k="$in_k" '$1 == "counter" && $2 == k { print $3 }' "${8:-/dev/null}" 2>/dev/null) || in_old=
|
|
484
|
+
in_get "$4.$in_k.sig" sig && sshsig_ok "$1" "$in_k" "$in_w/as" "$in_w/sig" "$5" "${in_old:-0}" || continue
|
|
485
|
+
[ -z "$in_ret" ] || [ "$SSHSIG_COUNTER" -le "$in_ret" ] 2>/dev/null || continue
|
|
486
|
+
in_got=$((in_got + 1))
|
|
487
|
+
printf '%s %s %s %s\n' "$in_k" "${SSHSIG_COUNTER:--}" \
|
|
488
|
+
"$(awk -v k="$in_k" '$1 == "custody" && $2 == k { c = $3 } END { print (c == "" ? "-" : c) }' "$3")" "$in_cl" >>"$in_w/got"
|
|
489
|
+
done
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# Whether the signatures in_count counted meet ROOTFILE's threshold for ROLE, or stop with CODE and SENTENCE; under
|
|
493
|
+
# `distinct-custody` they must also come from that many distinct custodies (B-4: one stolen laptop is not enough).
|
|
494
|
+
in_need() {
|
|
495
|
+
in_t=$(in_thr "$1" "$2")
|
|
496
|
+
[ "$in_got" -ge "$in_t" ] || in_die "$3" "$4"
|
|
497
|
+
grep -qx distinct-custody "$1" || return 0
|
|
498
|
+
[ "$(awk '!s[$3]++ { n++ } END { print n + 0 }' "$in_w/got")" -ge "$in_t" ] ||
|
|
499
|
+
in_die ROOT_CUSTODY_DUPLICATE "the counted signatures come from too few distinct custodies."
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
# Root 1 to root N: each newer root needs `threshold root` of the previous root's root keys and
|
|
503
|
+
# of its own, and carries every distrust and retire line forward.
|
|
504
|
+
in_chain() {
|
|
505
|
+
in_n=1
|
|
506
|
+
while [ "$in_n" -lt "$1" ]; do
|
|
507
|
+
in_m=$((in_n + 1)) && in_r=$in_w/root-$in_m.txt
|
|
508
|
+
in_get "root/$in_m.txt" "root-$in_m.txt" || in_die ROOT_FETCH "root $in_m could not be fetched."
|
|
509
|
+
case $(in_root_check "$in_r" "$in_m") in
|
|
510
|
+
ok) ;;
|
|
511
|
+
witness) in_die WITNESS_REQUIRED "root $in_m asks for witness cosignatures, which this installer cannot check." ;;
|
|
512
|
+
test) in_die ROOT_TEST "root $in_m is a test root." ;;
|
|
513
|
+
below) in_die ROOT_KEY_CLASS_BELOW_REQUIRED "root $in_m holds a key below the class it requires." ;;
|
|
514
|
+
*) in_bad ROOT_GRAMMAR "root $in_m" ;;
|
|
515
|
+
esac
|
|
516
|
+
awk 'NR == FNR { if ($1 == "distrust" || $1 == "retire") need[$0] = 1; next } { delete need[$0] }
|
|
517
|
+
END { for (l in need) exit 1 }' "$in_w/root-$in_n.txt" "$in_r" || in_die ROOT_DISTRUST_DROPPED "root $in_m drops a distrust or retire line."
|
|
518
|
+
in_count wardensec-root-v1 root "$in_w/root-$in_n.txt" "root/$in_m.txt" "$in_r" "$in_r" - "" "$in_w/root-$in_n.txt"
|
|
519
|
+
in_need "$in_w/root-$in_n.txt" root ROOT_UNSIGNED_BY_PREVIOUS "root $in_m lacks the previous root's signatures."
|
|
520
|
+
in_count wardensec-root-v1 root "$in_r" "root/$in_m.txt" "$in_r" "$in_r" - "" "$in_w/root-$in_n.txt"
|
|
521
|
+
in_need "$in_r" root ROOT_UNSIGNED_BY_ITSELF "root $in_m lacks its own signatures."
|
|
522
|
+
in_n=$in_m
|
|
523
|
+
done
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# The newest stable release that is neither revoked nor withdrawn, at or above `minimum` and, unless --now, 72 hours
|
|
527
|
+
# old by its index date. Prints `<name> <seq> <sha256> <skipped>`; or `none <skipped> <name> <time>` when every such
|
|
528
|
+
# release is younger, naming the newest and when it is 72 hours old; or `none 0` when there is none; or `held` while
|
|
529
|
+
# the index is inside the root's publication delay (B-4: DELAY hours after ISSUED, which --now does not shorten).
|
|
530
|
+
in_pick() {
|
|
531
|
+
LC_ALL=C awk -v now="$(date -u +%s)" -v age="$1" -v min="$2" -v iss="$3" -v dl="$4" '
|
|
532
|
+
function ep(s, y, m) {
|
|
533
|
+
y = substr(s, 1, 4) + 0; m = substr(s, 6, 2) + 0
|
|
534
|
+
if (m <= 2) { y--; m += 12 }
|
|
535
|
+
return (365 * y + int(y / 4) - int(y / 100) + int(y / 400) + int((153 * (m - 3) + 2) / 5) + substr(s, 9, 2) - 719469) * 86400 + substr(s, 12, 2) * 3600 + substr(s, 15, 2) * 60 + substr(s, 18, 2)
|
|
536
|
+
}
|
|
537
|
+
function iso(t, d, x, z, e, doe, yoe, doy, mp, mm) {
|
|
538
|
+
d = int(t / 86400); x = t - d * 86400; z = d + 719468; e = int(z / 146097); doe = z - e * 146097; yoe = int((doe - int(doe / 1460) + int(doe / 36524) - int(doe / 146096)) / 365)
|
|
539
|
+
doy = doe - (365 * yoe + int(yoe / 4) - int(yoe / 100)); mp = int((5 * doy + 2) / 153); mm = mp < 10 ? mp + 3 : mp - 9
|
|
540
|
+
return sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", yoe + e * 400 + (mm <= 2), mm, doy - int((153 * mp + 2) / 5) + 1, int(x / 3600), int((x % 3600) / 60), x % 60)
|
|
541
|
+
}
|
|
542
|
+
$1 == "revoke" { gone[$2] = 1 }
|
|
543
|
+
$1 == "withdraw" { withdrawn[$2] = 1 }
|
|
544
|
+
$1 == "release" && NF == 6 { line[++n] = $0 }
|
|
545
|
+
END {
|
|
546
|
+
if (ep(iss) + dl * 3600 > now + 0) { print "held"; exit }
|
|
547
|
+
for (i = n; i > 0; i--) {
|
|
548
|
+
split(line[i], f, " ")
|
|
549
|
+
if (f[6] != "stable" || (f[2] in gone) || (f[3] in withdrawn) || f[3] + 0 < min + 0) continue
|
|
550
|
+
if (age && ep(f[5]) + 259200 > now + 0) { if (!young++) y = f[2] " " iso(ep(f[5]) + 259200); continue }
|
|
551
|
+
print f[2], f[3], f[4], young + 0
|
|
552
|
+
exit
|
|
553
|
+
}
|
|
554
|
+
print "none", young + 0, y
|
|
555
|
+
}' "$in_w/index.txt"
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# Writes FILE into the data directory as NAME, through tmp/ and one rename.
|
|
559
|
+
in_put() { cp -p "$1" "$in_data/tmp/f" && mv -f "$in_data/tmp/f" "$in_data/$2"; }
|
|
560
|
+
|
|
561
|
+
main() {
|
|
562
|
+
in_age=1 in_pin=
|
|
563
|
+
for in_a in "$@"; do
|
|
564
|
+
case $in_a in --now) in_age= ;; --root-sha256=*) in_pin=${in_a#*=} ;; *) in_die USAGE "the options are --now and --root-sha256=<hex>." ;; esac
|
|
565
|
+
done
|
|
566
|
+
[ "$(id -u)" != 0 ] || in_die ROOT_USER "run this as yourself, not as root."
|
|
567
|
+
in_data=${XDG_DATA_HOME:-$HOME/.local/share}/warden
|
|
568
|
+
[ ! -L "$in_data" ] || in_die DATA_SYMLINK "the data directory $in_data is a symlink."
|
|
569
|
+
umask 077
|
|
570
|
+
sshsig_require_selftest || exit 1
|
|
571
|
+
in_selftest_soft || in_die SSHSIG_SELFTEST "this machine's OpenSSH cannot verify software-key signatures the way Warden needs."
|
|
572
|
+
in_w=$(mktemp -d) && [ -d "$in_w" ] || in_die TMP "no private temporary directory."
|
|
573
|
+
trap 'rm -rf "$in_w"' EXIT
|
|
574
|
+
trap 'exit 1' HUP INT TERM
|
|
575
|
+
|
|
576
|
+
in_root1 >"$in_w/root-1.txt"
|
|
577
|
+
[ -z "$in_pin" ] || [ "$(in_sha "$in_w/root-1.txt")" = "$in_pin" ] || in_die ROOT_PIN "this script's root 1 is not the one you pinned with --root-sha256."
|
|
578
|
+
case $(in_root_check "$in_w/root-1.txt" 1) in
|
|
579
|
+
ok) ;;
|
|
580
|
+
test)
|
|
581
|
+
in_say "Warden is in early access and releases are rolling out. This installer found no release to verify, so it stopped before changing anything." \
|
|
582
|
+
"Read the install guide: https://wardensec.ai/docs/install" "Nothing was installed." >&2
|
|
583
|
+
exit 1
|
|
584
|
+
;;
|
|
585
|
+
*) in_bad ROOT_BUILT_IN "built-in root" ;;
|
|
586
|
+
esac
|
|
587
|
+
|
|
588
|
+
in_i=$in_w/index.txt
|
|
589
|
+
# The site answers 404 for the index only until the first release is published: that is not a network fault.
|
|
590
|
+
in_get index.txt index.txt || { [ "$in_code" != 404 ] || in_die NO_RELEASE_YET "Warden has not published a release yet. The first release is being signed; once it is, it becomes installable 72 hours later. Run the same command then."; in_die INDEX_FETCH "the release index could not be fetched."; }
|
|
591
|
+
in_record "$in_i" && [ "$(head -n 1 "$in_i")" = "wardensec-index 1" ] || in_bad INDEX_GRAMMAR "release index"
|
|
592
|
+
in_serial=$(in_val "$in_i" serial) || in_serial=
|
|
593
|
+
in_rootn=$(in_val "$in_i" root) || in_rootn=
|
|
594
|
+
in_exp=$(in_val "$in_i" expires) || in_exp=
|
|
595
|
+
in_inst=$(in_val "$in_i" installer) || in_inst=
|
|
596
|
+
in_min=$(in_val "$in_i" minimum) || in_min=
|
|
597
|
+
in_iss=$(in_val "$in_i" issued) || in_iss=
|
|
598
|
+
in_int "$in_serial" && in_int "$in_rootn" && [ "$in_rootn" -le 999 ] && in_hex "$in_inst" && in_iso "$in_exp" && in_iso "$in_iss" &&
|
|
599
|
+
{ [ "$in_min" = 0 ] || in_int "$in_min"; } || in_bad INDEX_GRAMMAR "release index"
|
|
600
|
+
|
|
601
|
+
in_chain "$in_rootn"
|
|
602
|
+
in_top=$in_w/root-$in_rootn.txt
|
|
603
|
+
in_st=$in_data/trust/state && [ -f "$in_st" ] || in_st=/dev/null
|
|
604
|
+
in_count wardensec-index-v1 release "$in_top" index.txt "$in_i" "$in_top" "serial:$in_serial" "$in_st"
|
|
605
|
+
in_need "$in_top" release INDEX_UNSIGNED "the release index lacks its signatures."
|
|
606
|
+
mv "$in_w/got" "$in_w/index-got"
|
|
607
|
+
# The seen state (B4.4), the counter of a key that has none: a key seen on this serial or a later one counts only
|
|
608
|
+
# on the bytes it signed then. Other keys' well-formed lines are carried.
|
|
609
|
+
LC_ALL=C awk -v s="$in_serial" -v h="$(in_sha "$in_i")" 'NR == FNR { k[$1] = 1; next }
|
|
610
|
+
$1 == "seen" && NF == 4 && $2 ~ /^wdr-[0-9a-f]+$/ && length($2) == 20 && $3 ~ /^[1-9][0-9]*$/ && length($3) < 10 &&
|
|
611
|
+
$4 ~ /^[0-9a-f]+$/ && length($4) == 64 && !d[$2]++ { if (!($2 in k)) print; else if ($3 + 0 >= s + 0 && $4 != h) exit 3 }
|
|
612
|
+
END { for (x in k) print "seen", x, s, h }' "$in_w/index-got" "$in_st" >"$in_w/seen" ||
|
|
613
|
+
in_die SIGNATURE_SERIAL_REGRESSED "a release key signed other bytes at an index serial it has already signed."
|
|
614
|
+
in_now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
615
|
+
awk -v a="$in_exp" -v b="$in_now" 'BEGIN { exit !(a > b) }' || in_die INDEX_EXPIRED "the release index has expired."
|
|
616
|
+
case $0 in sh | */sh | dash | */dash | bash | */bash | -*) in_file= ;; *) in_file=$0 ;; esac
|
|
617
|
+
if [ -n "$in_file" ] && [ -f "$in_file" ]; then
|
|
618
|
+
[ "$(in_sha "$in_file")" = "$in_inst" ] || in_die INSTALLER_MISMATCH "this script is not the installer the index names."
|
|
619
|
+
else
|
|
620
|
+
in_say "This script was piped to sh, so it could not check its own hash against the index."
|
|
621
|
+
fi
|
|
622
|
+
|
|
623
|
+
in_dl=$(in_val "$in_top" delay) || in_dl=0
|
|
624
|
+
in_pk=$(in_pick "$in_age" "$in_min" "$in_iss" "$in_dl")
|
|
625
|
+
[ "$in_pk" != held ] || in_die INDEX_NOT_YET_ADOPTABLE "the release index was issued less than $in_dl hours ago; its root holds every release that long."
|
|
626
|
+
# shellcheck disable=SC2086
|
|
627
|
+
set -f && set -- $in_pk && set +f
|
|
628
|
+
if [ "$1" = none ]; then
|
|
629
|
+
[ "$#" -eq 4 ] || in_die NO_RELEASE "no stable release can be installed yet."
|
|
630
|
+
in_relname "$3" && in_iso "$4" || in_bad INDEX_GRAMMAR "release index"
|
|
631
|
+
if [ -n "$in_file" ]; then in_how="add --now"; else in_how="end the line with sh -s -- --now in place of sh"; fi
|
|
632
|
+
in_die NO_RELEASE "no stable release is old enough to install yet: this installer waits 72 hours after a release is signed before it installs one, so a bad release can be caught and withdrawn before anyone runs it, and the newest, $3, becomes usable at $4. Run the same command then, or $in_how to install it today (--now turns off only that age floor)."
|
|
633
|
+
fi
|
|
634
|
+
[ "$#" -eq 4 ] || in_die NO_RELEASE "no stable release can be installed yet."
|
|
635
|
+
in_rel=$1 in_seq=$2 in_rsha=$3
|
|
636
|
+
in_relname "$in_rel" && in_int "$in_seq" && in_hex "$in_rsha" || in_bad INDEX_GRAMMAR "release index"
|
|
637
|
+
[ "$4" = 0 ] || in_say "Skipped $4 newer stable release(s) signed less than 72 hours ago."
|
|
638
|
+
[ -n "$in_age" ] || in_say "--now: the 72-hour age floor is off. A release signed in the last three days" \
|
|
639
|
+
"has had no time to be noticed if it is bad."
|
|
640
|
+
|
|
641
|
+
in_f=$in_w/release.txt
|
|
642
|
+
in_get "$in_rel/release.txt" release.txt || in_die RELEASE_FETCH "the release record could not be fetched."
|
|
643
|
+
[ "$(in_sha "$in_f")" = "$in_rsha" ] || in_die RELEASE_HASH "the release record does not match the index."
|
|
644
|
+
in_record "$in_f" && [ "$(head -n 1 "$in_f")" = "wardensec-release 2" ] && [ "$(in_val "$in_f" release)" = "$in_rel" ] &&
|
|
645
|
+
[ "$(in_val "$in_f" seq)" = "$in_seq" ] && [ "$(in_val "$in_f" channel)" = stable ] || in_bad RELEASE_GRAMMAR "release record"
|
|
646
|
+
in_rr=$(in_val "$in_f" root) || in_rr=
|
|
647
|
+
in_int "$in_rr" && [ "$in_rr" -le "$in_rootn" ] || in_bad RELEASE_GRAMMAR "release record"
|
|
648
|
+
in_count wardensec-release-v1 release "$in_w/root-$in_rr.txt" "$in_rel/release.txt" "$in_f" "$in_top" "seq:$in_seq"
|
|
649
|
+
in_need "$in_w/root-$in_rr.txt" release RELEASE_UNSIGNED "the release record lacks its signatures."
|
|
650
|
+
in_fl=$(awk '$1 == "file" && $2 == "warden" { n++; h = $3; s = $4; f = NF } END { if (n == 1 && f == 4) print h, s }' "$in_f")
|
|
651
|
+
in_fsha=${in_fl%% *} && in_fsha=${in_fsha#sha256:} && in_fsize=${in_fl#* }
|
|
652
|
+
case $in_fl in sha256:*) ;; *) in_fsha= ;; esac
|
|
653
|
+
in_hex "$in_fsha" && in_int "$in_fsize" || in_bad RELEASE_GRAMMAR "release record"
|
|
654
|
+
in_get "sha256/$in_fsha" warden || in_die FILE_FETCH "the warden file could not be fetched."
|
|
655
|
+
[ "$(wc -c <"$in_w/warden" | tr -d ' ')" = "$in_fsize" ] && [ "$(in_sha "$in_w/warden")" = "$in_fsha" ] ||
|
|
656
|
+
in_die FILE_HASH "the warden file does not match the signed release."
|
|
657
|
+
|
|
658
|
+
# Install: every file is written under tmp/ and renamed into place.
|
|
659
|
+
{
|
|
660
|
+
printf 'root-serial %s\nindex-serial %s\nindex-sha256 %s\n' "$in_rootn" "$in_serial" "$(in_sha "$in_i")"
|
|
661
|
+
printf 'seen-seq %s\n' "$(awk '$1 == "release" && $3 + 0 > m { m = $3 + 0 } END { print m + 0 }' "$in_i")"
|
|
662
|
+
awk '$2 != "-" { print "counter", $1, $2 }' "$in_w/index-got"
|
|
663
|
+
cat "$in_w/seen"
|
|
664
|
+
printf 'first-seen %s %s\n' "$in_serial" "$in_now"
|
|
665
|
+
} >"$in_w/state"
|
|
666
|
+
chmod 0755 "$in_w/warden"
|
|
667
|
+
mkdir -p "$in_data/bin" "$in_data/trust" "$in_data/tmp"
|
|
668
|
+
in_n=0
|
|
669
|
+
while [ "$in_n" -lt "$in_rootn" ]; do
|
|
670
|
+
in_n=$((in_n + 1)) && in_put "$in_w/root-$in_n.txt" "trust/root-$in_n.txt"
|
|
671
|
+
done
|
|
672
|
+
in_put "$in_i" trust/index.txt && in_put "$in_w/state" trust/state && in_put "$in_w/warden" bin/warden
|
|
673
|
+
rmdir "$in_data/tmp" 2>/dev/null || true
|
|
674
|
+
in_bin=$in_data/bin/warden in_lb=$HOME/.local/bin in_link=
|
|
675
|
+
|
|
676
|
+
case ":$PATH:" in
|
|
677
|
+
*":$in_lb:"*)
|
|
678
|
+
if [ -d "$in_lb" ] && { [ -L "$in_lb/warden" ] || [ ! -e "$in_lb/warden" ]; }; then
|
|
679
|
+
rm -f "$in_lb/warden" && ln -s "$in_bin" "$in_lb/warden" && in_link=$in_lb/warden
|
|
680
|
+
fi
|
|
681
|
+
;;
|
|
682
|
+
esac
|
|
683
|
+
|
|
684
|
+
in_say "" "Installed Warden $in_rel at $in_bin" ""
|
|
685
|
+
# The class of every key behind this release (B4.2): posture, printed, never a refusal.
|
|
686
|
+
awk -v t="$(in_thr "$in_w/root-$in_rr.txt" release)" '{ printf "release signed by %s, %s key%s, %d of %s needed\n", $1, $4,
|
|
687
|
+
($3 == "-" ? "" : ", custody " $3), NR, t }' "$in_w/got"
|
|
688
|
+
awk -v n="$in_rootn" 'function p(x) { return c[x] ? c[x] " " x (c[x] > 1 ? "s" : "") : "" }
|
|
689
|
+
$1 == "key" && $3 == "root" { c[$4 == "ssh-ed25519" ? "software key" : "security key"]++ }
|
|
690
|
+
$1 == "threshold" && $2 == "root" { t = $3 } $1 == "distinct-custody" { d = " from distinct custodies" } $1 == "require" { q = 1 }
|
|
691
|
+
END { a = p("software key"); b = p("security key"); printf "root %s: %s%s%s, %s needed%s; this root %s security keys\n", n, a,
|
|
692
|
+
(a != "" && b != "" ? " and " : ""), b, t, d, (q ? "requires" : "does not require") }' "$in_top"
|
|
693
|
+
in_say "Root keys: $(awk '$1 == "key" && $3 == "root" { printf "%s ", $2 }' "$in_top")"
|
|
694
|
+
for in_n in $(awk -v n="$in_rootn" 'BEGIN { for (i = 1; i <= n; i++) print i }'); do
|
|
695
|
+
in_say " sha256 $(in_sha "$in_data/trust/root-$in_n.txt") trust/root-$in_n.txt"
|
|
696
|
+
done
|
|
697
|
+
in_say "These came from this script, so they are only as good as the installer sha256 you compared with the public" \
|
|
698
|
+
"trust record before you ran it; https://wardensec.ai/docs/verify-install says how. To print them again:" \
|
|
699
|
+
" shasum -a 256 $in_data/trust/root-*.txt" ""
|
|
700
|
+
[ -n "$in_link" ] || in_say "To run it by name, add this line to your shell's start-up file:" \
|
|
701
|
+
" export PATH=\"$in_data/bin:\$PATH\""
|
|
702
|
+
in_cv=$(command -v warden 2>/dev/null || true)
|
|
703
|
+
if [ -n "$in_cv" ] && [ "$in_cv" != "$in_bin" ] && [ "$in_cv" != "$in_link" ]; then
|
|
704
|
+
in_say "Warning: another warden comes first on your PATH and shadows this one: $in_cv"
|
|
705
|
+
fi
|
|
706
|
+
[ -z "$in_pin" ] || in_say "Root 1 matches the sha256 you pinned."
|
|
707
|
+
in_say "Next: $in_bin setup"
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
main "$@"
|
package/package.json
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wardensec",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"description": "Warden
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Warden AI installer: runs the signed install.sh that https://wardensec.ai/install.sh serves, byte for byte",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"homepage": "https://wardensec.ai/install",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/agent-labs-dev/warden.git",
|
|
10
|
+
"directory": "launcher/npm"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"wardensec": "bin/wardensec.cjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/wardensec.cjs",
|
|
17
|
+
"install.sh",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
9
27
|
}
|