@consiliency/agent-board-cli 1.23.0 → 1.25.0
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/dist/assets.d.ts +22 -0
- package/dist/assets.js +61 -0
- package/dist/assets.json +6 -0
- package/dist/cli.d.ts +37 -3
- package/dist/cli.js +68 -26
- package/dist/config_edit.d.ts +37 -0
- package/dist/config_edit.js +549 -0
- package/dist/doctor.d.ts +46 -3
- package/dist/doctor.js +242 -7
- package/dist/doctor_scopes.d.ts +162 -2
- package/dist/doctor_scopes.js +794 -16
- package/dist/identity_writer.d.ts +9 -7
- package/dist/identity_writer.js +18 -30
- package/dist/setup.d.ts +116 -0
- package/dist/setup.js +603 -0
- package/dist/setup_storage.d.ts +117 -0
- package/dist/setup_storage.js +447 -0
- package/dist/setup_writers.d.ts +128 -0
- package/dist/setup_writers.js +653 -0
- package/keyslot/agent-board-keyslot-load@.service +20 -0
- package/keyslot/agent-board-keyslot@.service +20 -0
- package/keyslot/constants.sh +73 -0
- package/keyslot/keygen.mjs +67 -0
- package/keyslot/keyslot-load.sh +78 -0
- package/keyslot/lib.sh +599 -0
- package/keyslot/optin.sh +185 -0
- package/keyslot/probe.sh +135 -0
- package/keyslot/provision.sh +530 -0
- package/keyslot/seam.sh +48 -0
- package/keyslot/status.sh +254 -0
- package/package.json +8 -4
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# provision.sh --harness <h> [--slot <s>] [--rotate] [--allow-unsealed-key] | [--refresh-loader]:
|
|
3
|
+
# put a key into slot <s> and load it into the slot agent (IF-0-KEYSEAL-1, IF-0-KEYSEAL-6).
|
|
4
|
+
# <s> defaults to <h>.
|
|
5
|
+
#
|
|
6
|
+
# Prints ONE line on success, the reference enrollment consumes:
|
|
7
|
+
# agent_socket:$XDG_RUNTIME_DIR/agent-board/<s>.sock#<pub43>
|
|
8
|
+
# Exit 0 provisioned, 1 failed or refused, 2 capability absent.
|
|
9
|
+
#
|
|
10
|
+
# MODE (A1), from probe.sh's `outcome`, never its exit code alone:
|
|
11
|
+
# capable sealed. --allow-unsealed-key is REFUSED (exit 1): unsealed is never
|
|
12
|
+
# chosen on a host that can seal.
|
|
13
|
+
# incapable unsealed, only with --allow-unsealed-key; without it exit 2, unless
|
|
14
|
+
# the slot already holds a committed unsealed key (then maintained).
|
|
15
|
+
# incapable-for-keyslot exit 2, the fix named: no mode survives a reboot.
|
|
16
|
+
# error exit 1 probe_error. An error is never treated as incapable.
|
|
17
|
+
# No downgrade: a slot with a <s>.cred is never turned unsealed; remove the pair first, then
|
|
18
|
+
# provision with a fresh flag. --rotate on an unsealed slot rotates INTO sealed mode, and only
|
|
19
|
+
# on a capable host.
|
|
20
|
+
#
|
|
21
|
+
# SEALED: the private key exists only in keygen.mjs's memory, the pipe into `systemd-creds
|
|
22
|
+
# encrypt`, and later the loader's pipe into ssh-add. It is never a file.
|
|
23
|
+
# UNSEALED: keygen.mjs's PEM goes straight into <s>.key (0600, noclobber); that file is the
|
|
24
|
+
# only plaintext this tool ever writes, and only on the opt-in path. Then the opt-in record
|
|
25
|
+
# <s>.unsealed-optin, then the pin <s>.pub: the pin's rename is the commit.
|
|
26
|
+
#
|
|
27
|
+
# LOADER (A1): each version of the install set lives in an immutable
|
|
28
|
+
# ~/.local/libexec/agent-board/v-<sha16>/; the template runs slot-%i/keyslot-load.sh through a
|
|
29
|
+
# per-slot symlink slot-<s> -> v-<sha16>, recorded in <s>.loader-sha256. Every run points THIS
|
|
30
|
+
# slot at the running tooling's version (restarting its loader when that changed) and leaves
|
|
31
|
+
# every other slot's version alone. --refresh-loader does only that, plus steps 5 and 6, and
|
|
32
|
+
# never re-keys. The first run under this layout migrates the flat layout (c5e358f), all
|
|
33
|
+
# slots under one lock (.loader.lock), template swap last, legacy files removed only after a
|
|
34
|
+
# successful daemon-reload.
|
|
35
|
+
#
|
|
36
|
+
# Transactional: under a per-slot lock, the recovery table below is evaluated first, so an
|
|
37
|
+
# interruption at any step leaves a state the next run finishes. A committed slot is NEVER
|
|
38
|
+
# deleted; only staging files and uncommitted generations are.
|
|
39
|
+
#
|
|
40
|
+
# found action
|
|
41
|
+
# nothing fresh: steps 3, 4, 5, 6
|
|
42
|
+
# *.staging only, or staged beside a committed slot delete the staging files, re-evaluate
|
|
43
|
+
# SEALED
|
|
44
|
+
# <s>.cred + <s>.pub.staging deriving from it half-committed: mv the pub, then
|
|
45
|
+
# (the old <s>.pub may exist) steps 3, 5 (with a restart), 6
|
|
46
|
+
# <s>.cred + <s>.pub deriving from it committed: steps 3, 5, 6; with
|
|
47
|
+
# --rotate 3, 4, 5 (restart), 6
|
|
48
|
+
# UNSEALED
|
|
49
|
+
# <s>.key + valid opt-in + <s>.pub deriving from key committed: steps 3, 5, 6; with --rotate
|
|
50
|
+
# on a capable host 3, 4 sealed, 5, 6
|
|
51
|
+
# <s>.key with no <s>.pub but a <s>.pub.staging, or an uncommitted unsealed generation
|
|
52
|
+
# an empty <s>.key alone (with any opt-in files) (never printed): delete it, re-evaluate
|
|
53
|
+
# UNSEALED -> SEALED (--rotate)
|
|
54
|
+
# <s>.cred + <s>.pub.staging deriving from it, with half-rotated: mv the pub, re-evaluate
|
|
55
|
+
# the old <s>.key and opt-in
|
|
56
|
+
# <s>.cred + <s>.pub deriving from it, with the old rotated: delete the old <s>.key and
|
|
57
|
+
# <s>.key (not deriving to the pin) and/or opt-in opt-in, re-evaluate
|
|
58
|
+
# anything else slot_inconsistent, exit 1
|
|
59
|
+
set -euo pipefail
|
|
60
|
+
|
|
61
|
+
here=${BASH_SOURCE[0]%/*}
|
|
62
|
+
[[ $here == "${BASH_SOURCE[0]}" ]] && here=.
|
|
63
|
+
# shellcheck source=lib.sh
|
|
64
|
+
. "$here/lib.sh"
|
|
65
|
+
|
|
66
|
+
usage="usage: provision.sh --harness <h> [--slot <s>] [--rotate] [--allow-unsealed-key] | [--refresh-loader]"
|
|
67
|
+
harness= slot= rotate=no allow_unsealed=no refresh=no
|
|
68
|
+
while (($#)); do
|
|
69
|
+
case $1 in
|
|
70
|
+
--harness) harness=${2-}; shift 2 || keyseal_die "usage: --harness <h>" ;;
|
|
71
|
+
--slot) slot=${2-}; shift 2 || keyseal_die "usage: --slot <s>" ;;
|
|
72
|
+
--rotate) rotate=yes; shift ;;
|
|
73
|
+
--allow-unsealed-key) allow_unsealed=yes; shift ;;
|
|
74
|
+
--refresh-loader) refresh=yes; shift ;;
|
|
75
|
+
*) keyseal_die "$usage" ;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
[[ $harness =~ $KEYSEAL_SLOT_RE ]] || keyseal_die "bad_harness"
|
|
79
|
+
slot=${slot:-$harness}
|
|
80
|
+
[[ $slot =~ $KEYSEAL_SLOT_RE ]] || keyseal_die "bad_slot"
|
|
81
|
+
if [[ $refresh == yes && ($rotate == yes || $allow_unsealed == yes) ]]; then
|
|
82
|
+
keyseal_die "--refresh-loader never re-keys: it takes no --rotate or --allow-unsealed-key"
|
|
83
|
+
fi
|
|
84
|
+
: "${XDG_RUNTIME_DIR:?provision: XDG_RUNTIME_DIR unset}"
|
|
85
|
+
: "${HOME:?provision: HOME unset}"
|
|
86
|
+
keyseal_resolve_bins || exit 1
|
|
87
|
+
seam_active && echo "provision: seam=true" >&2
|
|
88
|
+
|
|
89
|
+
K=$(keyseal_key_dir)
|
|
90
|
+
cred=$K/$slot.cred pub=$K/$slot.pub key=$K/$slot.key optin=$K/$slot.unsealed-optin
|
|
91
|
+
cred_s=$K/$slot.cred.staging pub_s=$K/$slot.pub.staging optin_s=$K/$slot.unsealed-optin.staging
|
|
92
|
+
base_unit=$KEYSEAL_BASE_UNIT@$slot.service
|
|
93
|
+
load_unit=$KEYSEAL_LOAD_UNIT@$slot.service
|
|
94
|
+
|
|
95
|
+
# Capability first, before any file exists (KS-9). --refresh-loader never generates a key, so
|
|
96
|
+
# it does not need one.
|
|
97
|
+
want=maintain
|
|
98
|
+
if [[ $refresh == no ]]; then
|
|
99
|
+
probe_out=$("$here/probe.sh" --json 2>/dev/null) || true
|
|
100
|
+
outcome=
|
|
101
|
+
[[ $probe_out =~ \"outcome\":\"([a-z-]+)\" ]] && outcome=${BASH_REMATCH[1]}
|
|
102
|
+
case $outcome in
|
|
103
|
+
capable)
|
|
104
|
+
[[ $allow_unsealed == no ]] || keyseal_die "allow_unsealed_refused: probe.sh reports this host capable of sealing; an unsealed key is never written where a sealed one can be. An existing unsealed slot is turned sealed with provision.sh --harness $harness --slot $slot --rotate (no --allow-unsealed-key)"
|
|
105
|
+
want=sealed
|
|
106
|
+
;;
|
|
107
|
+
incapable)
|
|
108
|
+
if [[ $allow_unsealed == yes ]]; then
|
|
109
|
+
want=unsealed
|
|
110
|
+
elif [[ -e $key || -L $key ]]; then
|
|
111
|
+
want=maintain
|
|
112
|
+
else
|
|
113
|
+
echo "provision: capability_absent (probe.sh: incapable; this host cannot seal). To accept an unsealed 0600 key for this slot, re-run with --allow-unsealed-key" >&2
|
|
114
|
+
exit 2
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
incapable-for-keyslot)
|
|
118
|
+
echo "provision: incapable_for_keyslot (probe.sh: no key slot survives a reboot here; run probe.sh for the fix)" >&2
|
|
119
|
+
exit 2
|
|
120
|
+
;;
|
|
121
|
+
*)
|
|
122
|
+
echo "provision: probe_error (probe.sh outcome '${outcome:-unreadable}'); refusing, and an error never admits an unsealed key" >&2
|
|
123
|
+
exit 1
|
|
124
|
+
;;
|
|
125
|
+
esac
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# The loader runs under the user manager, which resolves K from ITS environment.
|
|
129
|
+
keyseal_config_dir_check || keyseal_die "config_dir_mismatch: XDG_CONFIG_HOME/HOME differ from the user manager's (systemctl --user show-environment)"
|
|
130
|
+
|
|
131
|
+
umask 077
|
|
132
|
+
mkdir -p -m 0700 "$K"
|
|
133
|
+
exec {lock_fd}>"$K/$slot.lock"
|
|
134
|
+
"$FLOCK" -n "$lock_fd" || keyseal_die "slot_busy"
|
|
135
|
+
keyseal_seam_point locked
|
|
136
|
+
|
|
137
|
+
# Principle 4 on the key directory BEFORE any plaintext can exist (review round 1, Opus B1): an
|
|
138
|
+
# opt-in under a symlinked, foreign-owned or group-writable ancestor could never validate, so
|
|
139
|
+
# the unsealed key it guards would be stranded at rest.
|
|
140
|
+
if [[ $want == unsealed ]] && ! problems=$(keyseal_trusted_ancestors "$K"); then
|
|
141
|
+
keyseal_die "unsafe_key_dir: $K fails principle 4, so no unsealed key is written: ${problems//$'\n'/; }"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# No downgrade, judged BEFORE the recovery table: on a host that cannot decrypt, the table
|
|
145
|
+
# could not even derive the committed pin, and nothing here may depend on it.
|
|
146
|
+
if [[ $want == unsealed ]] && [[ -e $cred || -L $cred ]] && [[ -e $pub || -L $pub || -e $pub_s || -L $pub_s ]]; then
|
|
147
|
+
keyseal_die "downgrade_refused: slot $slot holds a sealed key ($slot.cred + $slot.pub); it is never turned unsealed. Remove that pair first, then provision with a fresh --allow-unsealed-key"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# step 2: recovery
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
has() { [[ -e $1 || -L $1 ]]; }
|
|
154
|
+
optin_valid() { keyseal_optin_check "$slot" >/dev/null; }
|
|
155
|
+
key_pub() { keyseal_keyfile_pub "$key" 2>/dev/null; }
|
|
156
|
+
|
|
157
|
+
table_row() {
|
|
158
|
+
local derived= kp=
|
|
159
|
+
if ! has "$cred" && ! has "$pub" && ! has "$key" && ! has "$optin" && ! has "$cred_s" && ! has "$pub_s" && ! has "$optin_s"; then
|
|
160
|
+
echo empty; return
|
|
161
|
+
fi
|
|
162
|
+
if has "$key" || has "$optin"; then
|
|
163
|
+
if has "$cred"; then
|
|
164
|
+
# unsealed -> sealed rotation, past the blob's rename.
|
|
165
|
+
derived=$(keyseal_derive_pub "$slot") || derived=
|
|
166
|
+
[[ -n $derived ]] || { echo inconsistent; return; }
|
|
167
|
+
if has "$pub_s" && ! has "$cred_s" && [[ $(keyseal_read_pin "$pub_s" || true) == "$derived" ]]; then
|
|
168
|
+
echo half-rotated; return
|
|
169
|
+
fi
|
|
170
|
+
if has "$pub" && [[ $(keyseal_read_pin "$pub" || true) == "$derived" ]]; then
|
|
171
|
+
kp=$(key_pub) || kp=
|
|
172
|
+
if [[ -n $kp && $kp == "$derived" ]]; then echo inconsistent; return; fi
|
|
173
|
+
echo rotated; return
|
|
174
|
+
fi
|
|
175
|
+
echo inconsistent; return
|
|
176
|
+
fi
|
|
177
|
+
if ! has "$pub"; then
|
|
178
|
+
# The pin's rename is the unsealed commit: before it, nothing was ever printed.
|
|
179
|
+
if has "$pub_s" || { has "$key" && [[ -f $key && ! -L $key && ! -s $key ]] && ! has "$optin"; }; then
|
|
180
|
+
echo uncommitted-unsealed; return
|
|
181
|
+
fi
|
|
182
|
+
echo inconsistent; return
|
|
183
|
+
fi
|
|
184
|
+
kp=$(key_pub) || kp=
|
|
185
|
+
if [[ -n $kp && $(keyseal_read_pin "$pub" || true) == "$kp" ]] && optin_valid; then
|
|
186
|
+
if has "$cred_s" || has "$pub_s" || has "$optin_s"; then echo staging; else echo committed-unsealed; fi
|
|
187
|
+
return
|
|
188
|
+
fi
|
|
189
|
+
echo inconsistent; return
|
|
190
|
+
fi
|
|
191
|
+
if ! has "$cred" && ! has "$pub"; then echo staging; return; fi
|
|
192
|
+
has "$cred" && derived=$(keyseal_derive_pub "$slot") || derived=
|
|
193
|
+
if [[ -n $derived ]] && has "$pub_s" && ! has "$cred_s" && [[ $(keyseal_read_pin "$pub_s" || true) == "$derived" ]]; then
|
|
194
|
+
echo half; return
|
|
195
|
+
fi
|
|
196
|
+
if [[ -n $derived ]] && has "$pub" && [[ $(keyseal_read_pin "$pub" || true) == "$derived" ]]; then
|
|
197
|
+
if has "$cred_s" || has "$pub_s" || has "$optin_s"; then echo staging; else echo committed; fi
|
|
198
|
+
return
|
|
199
|
+
fi
|
|
200
|
+
echo inconsistent
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# `finished`: this run completed an interrupted commit, so --rotate does not rotate again.
|
|
204
|
+
restart=no generate=no finished=no
|
|
205
|
+
while :; do
|
|
206
|
+
row=$(table_row)
|
|
207
|
+
case $row in
|
|
208
|
+
empty)
|
|
209
|
+
case $want in
|
|
210
|
+
sealed) generate=sealed ;;
|
|
211
|
+
unsealed) generate=unsealed ;;
|
|
212
|
+
*)
|
|
213
|
+
if [[ $refresh == yes ]]; then keyseal_die "nothing_to_refresh: slot $slot holds no committed key"; fi
|
|
214
|
+
echo "provision: capability_absent (probe.sh: incapable, and slot $slot holds no committed key). Re-run with --allow-unsealed-key to accept an unsealed 0600 key" >&2
|
|
215
|
+
exit 2
|
|
216
|
+
;;
|
|
217
|
+
esac
|
|
218
|
+
break
|
|
219
|
+
;;
|
|
220
|
+
staging) rm -f -- "$cred_s" "$pub_s" "$optin_s"; sync -- "$K" ;;
|
|
221
|
+
uncommitted-unsealed) rm -f -- "$key" "$pub_s" "$optin" "$optin_s" "$cred_s"; sync -- "$K" ;;
|
|
222
|
+
half | half-rotated)
|
|
223
|
+
mv -f -- "$pub_s" "$pub"
|
|
224
|
+
sync -- "$K"
|
|
225
|
+
restart=yes finished=yes
|
|
226
|
+
;;
|
|
227
|
+
rotated)
|
|
228
|
+
rm -f -- "$optin" "$key"
|
|
229
|
+
sync -- "$K"
|
|
230
|
+
restart=yes finished=yes
|
|
231
|
+
;;
|
|
232
|
+
committed)
|
|
233
|
+
# (want=unsealed cannot reach here: the downgrade refusal above ran first.)
|
|
234
|
+
if [[ $rotate == yes && $finished == no ]]; then
|
|
235
|
+
[[ $want == sealed ]] || keyseal_die "rotate_needs_capable: this host cannot seal a new key (probe.sh)"
|
|
236
|
+
generate=sealed restart=yes
|
|
237
|
+
fi
|
|
238
|
+
break
|
|
239
|
+
;;
|
|
240
|
+
committed-unsealed)
|
|
241
|
+
if [[ $rotate == yes ]]; then
|
|
242
|
+
[[ $want == sealed ]] || keyseal_die "rotate_needs_capable: --rotate turns an unsealed slot sealed, which needs probe.sh capable; this host is not"
|
|
243
|
+
generate=sealed restart=yes
|
|
244
|
+
elif [[ $want == sealed ]]; then
|
|
245
|
+
echo "provision: upgrade_available: this host can now seal; provision.sh --harness $harness --slot $slot --rotate, then re-enroll and revoke the old identity" >&2
|
|
246
|
+
fi
|
|
247
|
+
break
|
|
248
|
+
;;
|
|
249
|
+
*) keyseal_die "slot_inconsistent: $K holds a state the recovery table does not name; nothing was deleted" ;;
|
|
250
|
+
esac
|
|
251
|
+
done
|
|
252
|
+
keyseal_seam_point recovered
|
|
253
|
+
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
# step 3: install, migrate, reload, and check the chain BEFORE any key is loaded. Every
|
|
256
|
+
# layout change holds .loader.lock (outside the slot grammar), across all slots.
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
unit_dir=$(keyseal_unit_dir)
|
|
259
|
+
libexec=$(keyseal_libexec_dir)
|
|
260
|
+
load_tpl=$unit_dir/$KEYSEAL_LOAD_UNIT@.service
|
|
261
|
+
mkdir -p -- "$unit_dir" "$libexec"
|
|
262
|
+
exec {layout_fd}>"$K/.loader.lock"
|
|
263
|
+
"$FLOCK" -w 60 "$layout_fd" || keyseal_die "loader_busy: another provision holds $K/.loader.lock"
|
|
264
|
+
keyseal_seam_point layout-locked
|
|
265
|
+
|
|
266
|
+
if [[ -e $unit_dir/$KEYSEAL_BASE_UNIT@.service ]]; then
|
|
267
|
+
# Adoption (KS-7): an existing base unit is kept only if its directive multiset equals the
|
|
268
|
+
# shipped one; otherwise refuse, and leave it byte-identical.
|
|
269
|
+
if [[ $(keyseal_node directives "$unit_dir/$KEYSEAL_BASE_UNIT@.service") != \
|
|
270
|
+
"$(keyseal_node directives "$here/$KEYSEAL_BASE_UNIT@.service")" ]]; then
|
|
271
|
+
keyseal_die "base_unit_mismatch: $unit_dir/$KEYSEAL_BASE_UNIT@.service differs from the shipped directive set; refusing to adopt or replace it"
|
|
272
|
+
fi
|
|
273
|
+
else
|
|
274
|
+
install -m 0644 -- "$here/$KEYSEAL_BASE_UNIT@.service" "$unit_dir/"
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# Install the set in $1 as its immutable version directory, or verify the one already there.
|
|
278
|
+
# Prints the set sha.
|
|
279
|
+
install_version() {
|
|
280
|
+
local src=$1 sha d tmp f
|
|
281
|
+
shift
|
|
282
|
+
sha=$(keyseal_files_sha "$src" "$@") || keyseal_die "install_set_incomplete in $src"
|
|
283
|
+
d=$libexec/$(keyseal_version_dirname "$sha")
|
|
284
|
+
if has "$d"; then
|
|
285
|
+
[[ -d $d && ! -L $d && $(keyseal_dir_sha "$d" || true) == "$sha" ]] ||
|
|
286
|
+
keyseal_die "loader_version_mismatch: $d exists and does not hold version $sha; refusing to overwrite an immutable version"
|
|
287
|
+
else
|
|
288
|
+
tmp=$(mktemp -d "$libexec/.v-staging.XXXXXX")
|
|
289
|
+
for f in "$@"; do install -m 0755 -- "$src/$f" "$tmp/"; done
|
|
290
|
+
chmod 0755 "$tmp"
|
|
291
|
+
sync -- "$tmp"
|
|
292
|
+
mv -T -- "$tmp" "$d"
|
|
293
|
+
sync -- "$libexec"
|
|
294
|
+
fi
|
|
295
|
+
printf '%s' "$sha"
|
|
296
|
+
}
|
|
297
|
+
# Point slot $1 at version $2 (atomic rename of a fresh symlink), and record it.
|
|
298
|
+
point_slot() {
|
|
299
|
+
local s=$1 sha=$2 link tmp
|
|
300
|
+
link=$(keyseal_slot_link "$s")
|
|
301
|
+
if has "$link" && [[ ! -L $link ]]; then keyseal_die "slot_link_not_a_symlink: $link"; fi
|
|
302
|
+
tmp=$libexec/.$KEYSEAL_SLOT_LINK_PREFIX$s.staging
|
|
303
|
+
rm -f -- "$tmp"
|
|
304
|
+
ln -s -- "$(keyseal_version_dirname "$sha")" "$tmp"
|
|
305
|
+
mv -T -f -- "$tmp" "$link"
|
|
306
|
+
sync -- "$libexec"
|
|
307
|
+
}
|
|
308
|
+
record_slot() {
|
|
309
|
+
local s=$1 sha=$2 rec
|
|
310
|
+
rec=$(keyseal_loader_record "$s")
|
|
311
|
+
printf '%s\n' "$sha" >"$rec.staging"
|
|
312
|
+
sync -- "$rec.staging"
|
|
313
|
+
mv -f -- "$rec.staging" "$rec"
|
|
314
|
+
sync -- "$K"
|
|
315
|
+
}
|
|
316
|
+
template_is() { [[ -f $load_tpl ]] && cmp -s -- "$1" "$load_tpl"; }
|
|
317
|
+
template_is_legacy() {
|
|
318
|
+
[[ -f $load_tpl ]] || return 1
|
|
319
|
+
local line
|
|
320
|
+
while IFS= read -r line; do
|
|
321
|
+
line=${line#"${line%%[![:space:]]*}"}
|
|
322
|
+
[[ $line == "ExecStart=$KEYSEAL_LEGACY_LOAD_EXEC" ]] && return 0
|
|
323
|
+
done <"$load_tpl"
|
|
324
|
+
return 1
|
|
325
|
+
}
|
|
326
|
+
swap_template() {
|
|
327
|
+
install -m 0644 -- "$here/$KEYSEAL_LOAD_UNIT@.service" "$load_tpl.staging"
|
|
328
|
+
mv -f -- "$load_tpl.staging" "$load_tpl"
|
|
329
|
+
sync -- "$unit_dir"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Every slot the template serves: each with committed material in K (a blob, pin or key; enabled
|
|
333
|
+
# or not, running or not) and each enabled loader instance (review round 1, adversarial 2).
|
|
334
|
+
all_slots() {
|
|
335
|
+
local f s
|
|
336
|
+
{
|
|
337
|
+
keyseal_enabled_slots
|
|
338
|
+
for f in "$K"/*.cred "$K"/*.pub "$K"/*.key; do
|
|
339
|
+
[[ -e $f ]] || continue
|
|
340
|
+
s=${f##*/}
|
|
341
|
+
s=${s%.*}
|
|
342
|
+
[[ $s =~ $KEYSEAL_SLOT_RE ]] && printf '%s\n' "$s"
|
|
343
|
+
done
|
|
344
|
+
} | sort -u
|
|
345
|
+
}
|
|
346
|
+
# The slots that hold a key: the ones legacy removal must not strand.
|
|
347
|
+
committed_slots() {
|
|
348
|
+
local f s
|
|
349
|
+
for f in "$K"/*.cred "$K"/*.pub "$K"/*.key; do
|
|
350
|
+
[[ -e $f ]] || continue
|
|
351
|
+
s=${f##*/}
|
|
352
|
+
s=${s%.*}
|
|
353
|
+
[[ $s =~ $KEYSEAL_SLOT_RE ]] && printf '%s\n' "$s"
|
|
354
|
+
done | sort -u
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Migration from the flat layout: every slot onto a version directory holding the flat files it
|
|
358
|
+
# runs today, THEN the template, THEN the legacy files. Only the flat set c5e358f shipped is
|
|
359
|
+
# migrated: unknown content is never laundered into a recorded version (review round 1, S1).
|
|
360
|
+
if template_is_legacy; then
|
|
361
|
+
keyseal_legacy_present || keyseal_die "legacy_layout_incomplete: $load_tpl runs the flat loader but $libexec holds no flat install set"
|
|
362
|
+
keyseal_legacy_known || keyseal_die "legacy_loader_unknown: the flat loader files in $libexec are not the set c5e358f shipped; refusing to migrate them (status.sh reports the chain)"
|
|
363
|
+
legacy_sha=$(install_version "$libexec" "${KEYSEAL_LEGACY_INSTALL_SET[@]}")
|
|
364
|
+
keyseal_seam_point migrate-install
|
|
365
|
+
mapfile -t migrating < <(all_slots)
|
|
366
|
+
for s in "${migrating[@]}"; do point_slot "$s" "$legacy_sha"; done
|
|
367
|
+
keyseal_seam_point migrate-link
|
|
368
|
+
for s in "${migrating[@]}"; do record_slot "$s" "$legacy_sha"; done
|
|
369
|
+
keyseal_seam_point migrate-record
|
|
370
|
+
swap_template
|
|
371
|
+
keyseal_seam_point migrate-swap
|
|
372
|
+
"$SYSTEMCTL" --user daemon-reload || keyseal_die "daemon_reload_failed after the template swap; the legacy files are kept"
|
|
373
|
+
keyseal_seam_point migrate-reload
|
|
374
|
+
elif ! template_is "$here/$KEYSEAL_LOAD_UNIT@.service"; then
|
|
375
|
+
swap_template
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
sha=$(install_version "$here" "${KEYSEAL_INSTALL_SET[@]}")
|
|
379
|
+
loader_changed=no
|
|
380
|
+
[[ $(keyseal_read_loader_record "$slot" || true) == "$sha" && $(readlink -- "$(keyseal_slot_link "$slot")" || true) == "$(keyseal_version_dirname "$sha")" ]] || loader_changed=yes
|
|
381
|
+
point_slot "$slot" "$sha"
|
|
382
|
+
keyseal_seam_point link-pointed
|
|
383
|
+
record_slot "$slot" "$sha"
|
|
384
|
+
"$SYSTEMCTL" --user daemon-reload || keyseal_die "daemon_reload_failed"
|
|
385
|
+
keyseal_seam_point pointed
|
|
386
|
+
|
|
387
|
+
# Legacy flat files: removed only once every slot holding a key has its symlink and
|
|
388
|
+
# daemon-reload has succeeded (above, in this run). A missing symlink is a DELETION
|
|
389
|
+
# precondition, never an error: the files stay, status reports legacy_loader_present (2), and
|
|
390
|
+
# `provision.sh --refresh-loader` for that slot (which has a key, so it can) links it and
|
|
391
|
+
# removes them (review round 1, Gemini 1). An enabled slot with no key cannot load anything,
|
|
392
|
+
# before or after, so it never holds them.
|
|
393
|
+
if keyseal_legacy_present && ! template_is_legacy; then
|
|
394
|
+
unlinked=
|
|
395
|
+
while IFS= read -r s; do
|
|
396
|
+
[[ -z $s || -L $(keyseal_slot_link "$s") ]] || unlinked+=" $s"
|
|
397
|
+
done < <(committed_slots)
|
|
398
|
+
if [[ -z $unlinked ]]; then
|
|
399
|
+
for f in "${KEYSEAL_LEGACY_INSTALL_SET[@]}"; do rm -f -- "${libexec:?}/$f"; done
|
|
400
|
+
sync -- "$libexec"
|
|
401
|
+
keyseal_seam_point migrate-cleanup
|
|
402
|
+
else
|
|
403
|
+
echo "provision: legacy loader files kept: slot(s)$unlinked have no ${KEYSEAL_SLOT_LINK_PREFIX}<s> symlink yet; provision.sh --refresh-loader for each removes them" >&2
|
|
404
|
+
fi
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# A version directory goes only when no slot symlink references it; staging leftovers go.
|
|
408
|
+
declare -A referenced=()
|
|
409
|
+
for l in "$libexec/$KEYSEAL_SLOT_LINK_PREFIX"*; do
|
|
410
|
+
[[ -L $l ]] && referenced[$(readlink -- "$l")]=1
|
|
411
|
+
done
|
|
412
|
+
for d in "$libexec/$KEYSEAL_VERSION_PREFIX"* "$libexec"/.v-staging.*; do
|
|
413
|
+
[[ -d $d && ! -L $d ]] || continue
|
|
414
|
+
[[ -n ${referenced[${d##*/}]-} ]] || rm -rf -- "$d"
|
|
415
|
+
done
|
|
416
|
+
|
|
417
|
+
keyseal_chain_check "$slot" "$here" || keyseal_die "chain check failed before loading any key:"$'\n'"$KEYSEAL_CHAIN_PROBLEMS"
|
|
418
|
+
[[ $KEYSEAL_LOADER_STATE == current && $KEYSEAL_CHAIN_LEGACY == no ]] ||
|
|
419
|
+
keyseal_die "loader not current after install ($KEYSEAL_LOADER_STATE)"
|
|
420
|
+
exec {layout_fd}>&-
|
|
421
|
+
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
# step 4: generate and commit
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
if [[ $generate == sealed ]]; then
|
|
426
|
+
rm -f -- "$cred_s" "$pub_s"
|
|
427
|
+
# pipefail: a keygen that fails after printing a whole PEM still fails this step (KS-4).
|
|
428
|
+
node "$here/keygen.mjs" --pub-out "$pub_s" \
|
|
429
|
+
| "$CREDS" --user encrypt --name="$(keyseal_cred_name "$slot")" - "$cred_s" \
|
|
430
|
+
|| keyseal_die "keygen_or_encrypt_failed; the staging files are deleted by the next run"
|
|
431
|
+
keyseal_seam_point keygen
|
|
432
|
+
keyseal_seam_point encrypt
|
|
433
|
+
sync -- "$cred_s" "$pub_s"
|
|
434
|
+
mv -f -- "$cred_s" "$cred"
|
|
435
|
+
sync -- "$K"
|
|
436
|
+
keyseal_seam_point cred-rename
|
|
437
|
+
mv -f -- "$pub_s" "$pub"
|
|
438
|
+
sync -- "$K"
|
|
439
|
+
keyseal_seam_point pub-rename
|
|
440
|
+
if has "$optin" || has "$key"; then
|
|
441
|
+
# An unsealed slot rotated into sealed mode: its old key goes, opt-in first.
|
|
442
|
+
rm -f -- "$optin"
|
|
443
|
+
sync -- "$K"
|
|
444
|
+
keyseal_seam_point optin-remove
|
|
445
|
+
rm -f -- "$key"
|
|
446
|
+
sync -- "$K"
|
|
447
|
+
keyseal_seam_point key-remove
|
|
448
|
+
fi
|
|
449
|
+
elif [[ $generate == unsealed ]]; then
|
|
450
|
+
rm -f -- "$pub_s" "$optin_s"
|
|
451
|
+
# The opt-in path: the PEM goes into <s>.key, 0600 (umask 077), never over an existing file.
|
|
452
|
+
(set -C && node "$here/keygen.mjs" --pub-out "$pub_s" >"$key") ||
|
|
453
|
+
keyseal_die "keygen_failed; the uncommitted key is deleted by the next run"
|
|
454
|
+
keyseal_seam_point keygen
|
|
455
|
+
sync -- "$key" "$pub_s"
|
|
456
|
+
mid=$(keyseal_machine_id) || keyseal_die "machine_id_unreadable: $MACHINE_ID_FILE"
|
|
457
|
+
{
|
|
458
|
+
printf 'version=1\n'
|
|
459
|
+
printf 'machine_id=%s\n' "$mid"
|
|
460
|
+
printf 'uid=%s\n' "$(keyseal_uid)"
|
|
461
|
+
printf 'slot=%s\n' "$slot"
|
|
462
|
+
printf 'pub_sha256=%s\n' "$(keyseal_sha256_file "$pub_s")"
|
|
463
|
+
printf 'granted_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
464
|
+
} >"$optin_s"
|
|
465
|
+
sync -- "$optin_s"
|
|
466
|
+
mv -f -- "$optin_s" "$optin"
|
|
467
|
+
sync -- "$K"
|
|
468
|
+
keyseal_seam_point optin-rename
|
|
469
|
+
# BEFORE the pin's rename (the commit), re-read both halves under the loader's own predicate:
|
|
470
|
+
# the opt-in against the staged pin, the key's public key against it. A failure is still an
|
|
471
|
+
# uncommitted generation, nothing was printed, so it is deleted now (review round 1, B1).
|
|
472
|
+
kp=$(keyseal_keyfile_pub "$key") || kp=
|
|
473
|
+
if ! problems=$(keyseal_optin_verify "$optin" "$pub_s" "$slot" "$(keyseal_uid)" "$mid") ||
|
|
474
|
+
[[ $kp != "$(keyseal_read_pin "$pub_s" || true)" ]]; then
|
|
475
|
+
rm -f -- "$key" "$optin" "$pub_s"
|
|
476
|
+
sync -- "$K"
|
|
477
|
+
keyseal_die "slot_inconsistent: the opt-in does not validate against the staged pin (${problems//$'\n'/; }); the uncommitted key is deleted"
|
|
478
|
+
fi
|
|
479
|
+
mv -f -- "$pub_s" "$pub"
|
|
480
|
+
sync -- "$K"
|
|
481
|
+
keyseal_seam_point pub-rename
|
|
482
|
+
echo "provision: slot $slot is UNSEALED by opt-in: $key is a plaintext 0600 key (probe.sh: incapable). status.sh reports it; provision.sh --rotate once this host can seal" >&2
|
|
483
|
+
fi
|
|
484
|
+
pin=$(keyseal_read_pin "$pub") || keyseal_die "slot_inconsistent: no pin after commit"
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# step 5: enable and start; restart on rotate or recovery, reload on a new loader version
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
"$SYSTEMCTL" --user enable --now "$base_unit" "$load_unit" >/dev/null 2>&1 || true
|
|
490
|
+
for u in "$base_unit" "$load_unit"; do
|
|
491
|
+
[[ $("$SYSTEMCTL" --user is-enabled "$u" 2>/dev/null || true) == enabled ]] || keyseal_die "not_enabled: $u"
|
|
492
|
+
done
|
|
493
|
+
keyseal_seam_point enable
|
|
494
|
+
if [[ $restart == yes ]]; then
|
|
495
|
+
"$SYSTEMCTL" --user restart "$base_unit" >/dev/null 2>&1 || true
|
|
496
|
+
elif [[ $loader_changed == yes ]]; then
|
|
497
|
+
"$SYSTEMCTL" --user restart "$load_unit" >/dev/null 2>&1 || true
|
|
498
|
+
fi
|
|
499
|
+
keyseal_seam_point load
|
|
500
|
+
|
|
501
|
+
# ---------------------------------------------------------------------------
|
|
502
|
+
# step 6: every path ends with the agent holding exactly the pin
|
|
503
|
+
# ---------------------------------------------------------------------------
|
|
504
|
+
# Waits while the loader may still be running; a loader that has finished (active or
|
|
505
|
+
# failed) with the wrong key set is a mismatch now, not in 5 s.
|
|
506
|
+
holds_pin() {
|
|
507
|
+
local i
|
|
508
|
+
for ((i = 0; i < KEYSEAL_SOCK_WAIT_STEPS; i++)); do
|
|
509
|
+
keyseal_agent_holds_exactly "$slot" "$pin" && return 0
|
|
510
|
+
case $("$SYSTEMCTL" --user is-active "$load_unit" 2>/dev/null || true) in
|
|
511
|
+
active | failed) return 1 ;;
|
|
512
|
+
esac
|
|
513
|
+
"$SLEEP" 0.1
|
|
514
|
+
done
|
|
515
|
+
return 1
|
|
516
|
+
}
|
|
517
|
+
if ! holds_pin; then
|
|
518
|
+
"$SYSTEMCTL" --user restart "$base_unit" >/dev/null 2>&1 || true
|
|
519
|
+
holds_pin || keyseal_die "pin_not_loaded: the slot agent does not hold exactly the committed key after one restart"
|
|
520
|
+
fi
|
|
521
|
+
|
|
522
|
+
printf 'agent_socket:%s#%s\n' "$(keyseal_sock_path "$slot")" "$pin"
|
|
523
|
+
if [[ $rotate == yes ]]; then
|
|
524
|
+
cat >&2 <<EOT
|
|
525
|
+
provision: slot $slot rotated. Next, in order:
|
|
526
|
+
1. re-enroll the new key (owner): it is not yet known to the board;
|
|
527
|
+
2. re-point the harness at the new reference printed above;
|
|
528
|
+
3. revoke the old key (owner) once the harness signs with the new one.
|
|
529
|
+
EOT
|
|
530
|
+
fi
|
package/keyslot/seam.sh
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# The test seam. An override is honoured only under KEYSEAL_TEST_ROOT, and only when it
|
|
3
|
+
# resolves inside that root; anything else is refused, never silently replaced by the
|
|
4
|
+
# default, so a stray variable in a real environment cannot redirect a binary.
|
|
5
|
+
|
|
6
|
+
# 0 when the seam is active (a test run), else 1. Scripts stamp their output with it.
|
|
7
|
+
seam_active() { [[ -n ${KEYSEAL_TEST_ROOT-} ]]; }
|
|
8
|
+
|
|
9
|
+
# seam_resolve <VAR> <default>: print the value of $VAR when it is set and confined to
|
|
10
|
+
# KEYSEAL_TEST_ROOT, else <default> when $VAR is unset or empty; otherwise print
|
|
11
|
+
# seam_override_refused to stderr and return 1.
|
|
12
|
+
seam_resolve() {
|
|
13
|
+
local var=$1 default=$2 override root real
|
|
14
|
+
override=${!var-}
|
|
15
|
+
if [[ -z $override ]]; then
|
|
16
|
+
printf '%s' "$default"
|
|
17
|
+
return 0
|
|
18
|
+
fi
|
|
19
|
+
if ! seam_active; then
|
|
20
|
+
printf 'seam_override_refused: %s is set without KEYSEAL_TEST_ROOT\n' "$var" >&2
|
|
21
|
+
return 1
|
|
22
|
+
fi
|
|
23
|
+
# realpath only on this branch: the production path never depends on it.
|
|
24
|
+
root=$(/usr/bin/realpath -m -- "$KEYSEAL_TEST_ROOT") || root=
|
|
25
|
+
real=$(/usr/bin/realpath -m -- "$override") || real=
|
|
26
|
+
if [[ -z $root || $root == / || -z $real || $real/ != "$root"/* ]]; then
|
|
27
|
+
printf 'seam_override_refused: %s is outside KEYSEAL_TEST_ROOT\n' "$var" >&2
|
|
28
|
+
return 1
|
|
29
|
+
fi
|
|
30
|
+
printf '%s' "$override"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# seam_pause <point> <sleep-binary>: under KEYSEAL_TEST_ROOT only, when KEYSEAL_TEST_FAIL_AT is
|
|
34
|
+
# <point> and KEYSEAL_TEST_FAIL_MODE is `pause`, create $KEYSEAL_TEST_ROOT/paused.<point> and wait
|
|
35
|
+
# (at most 60 s) for $KEYSEAL_TEST_ROOT/resume. The loader's only seam: it never moves, writes or
|
|
36
|
+
# deletes anything else, so the TEST does whatever the pause is for. A no-op in production.
|
|
37
|
+
seam_pause() {
|
|
38
|
+
seam_active || return 0
|
|
39
|
+
[[ ${KEYSEAL_TEST_FAIL_AT-} == "$1" && ${KEYSEAL_TEST_FAIL_MODE-} == pause ]] || return 0
|
|
40
|
+
: >"$KEYSEAL_TEST_ROOT/paused.$1"
|
|
41
|
+
local i
|
|
42
|
+
for ((i = 0; i < 1200; i++)); do
|
|
43
|
+
[[ -e $KEYSEAL_TEST_ROOT/resume ]] && return 0
|
|
44
|
+
"$2" 0.05
|
|
45
|
+
done
|
|
46
|
+
printf 'seam_pause_timeout %s\n' "$1" >&2
|
|
47
|
+
return 1
|
|
48
|
+
}
|