-
Notifications
You must be signed in to change notification settings - Fork 3
/
functions
465 lines (425 loc) · 16.7 KB
/
functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
#! /bin/sh
EMBOOT_VERBOSE=${EMBOOT_VERBOSE:-${VERBOSE:-0}}
EMBOOT_ADD_TO_COUNTER=0
export EMBOOT_VERBOSE EMBOOT_ADD_TO_COUNTER
# Sets up a temporary working directory in the standard way
setup_tmp_dir() {
mktemp -d -t emboot-XXXXXXXXXX
}
# Basic shell quoting. Makes no attempt to ensure the output is human-readable.
# See the version in bash_functions for something nicer.
quote_args() {
local space=""
local w
for w; do
printf %s "$space$(printf \'; printf %s "$w" | sed -e "s/'/'\\\\''/g"; printf \')"
space=" "
done
}
# Quotes a basic regular expression. Care must be taken with strings with
# trailing newlines, as they will be stripped by command substitution. See
# `any_of_bre` for how to avoid this.
quote_bre() {
printf %s "$1" | sed -e 's/[]\\^$*.[]/\\\0/g'
}
# Returns a BRE matching any of the given arguments literally. (I.e., arguments
# are quoted.)
any_of_bre() {
local quoted=""
local oor=""
local w
for w; do
q=$(quote_bre "${w}x")
quoted=$quoted$oor${q%x}
oor='\|'
done
printf %s "$quoted"
}
# Parses YAML in the first argument, producing shell output that can be eval'ed
# to assign values to variables named for keys. Optional second argument adds a
# prefix to each key. Adapted from
# https://stackoverflow.com/questions/5014632/how-can-i-parse-a-yaml-file-from-a-linux-shell-script
# to support keys with spaces, which are valid YAML.
parse_yaml() {
local prefix=$2
local s='[[:space:]]*' w='[a-zA-Z0-9_ ]*' fs=$(echo @|tr @ '\034')
sed -n \
-e "s|^\($s\):|\1|" \
-e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 | \
awk -F$fs '{
indent = length($1)/2;
vname[indent] = $2;
for (i in vname) {if (i > indent) {delete vname[i]}}
if (length($3) > 0) {
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
gsub(/ /, "_", vn)
printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
}
}'
}
# Returns 0 for anything but "1" or strings starting with [nNfF]. The idea is
# to support yes/no, true/false, 0/1 in configuration files. There should be a
# convention for this.
is_true() {
case "$(printf %s "$1" | tr A-Z a-z)" in
y*|t*|1)
return 0
;;
n*|f*|0|'')
return 1
;;
esac
return 0
}
# Cleaner looking than putting `|| true` at the end of a command: keeps shell
# variables, temporarily disables errexit, and works on compound commands (with
# eval).
ignore_err() {
{ local do_nothing=$({ "$@"; } >&3 3>&-); } 3>&1
return 0
}
# Returns 0 if $EMBOOT_VERBOSE >= the given argument (or >= 1 if unspecified)
is_verbose() {
[ -z "$EMBOOT_VERBOSE" ] || [ "$EMBOOT_VERBOSE" -ge "${1:-1}" ]
}
# Runs the given command, ignoring errors, if `is_verbose [<value for -l>]`.
# Note that arguments will be expanded before even getting here, so use eval
# with a (single-quoted) string to avoid doing unnecessary work.
verbose_do() {
local OPTIND
local OPTARG
local level=1
while getopts 'l:' opt; do
case "$opt" in
l) level=$OPTARG ;;
?) return 1;;
esac
done
shift $((OPTIND-1))
if is_verbose $level; then
ignore_err "$@"
fi
}
# Categorizes the given file descriptor by type:
# $ = tty
# - = /dev/null or closed
# | = pipe
# * = regular file
# ? = anything else
fd_type() {
fd=$1
fdfile=/proc/self/fd/$fd
if [ -t "$fd" ]; then printf '$';
elif [ ! -e "$fdfile" -o "$(readlink -f "$fdfile")" = "/dev/null" ]; then printf '-';
elif [ -f "$fdfile" ]; then printf '*';
elif [ -p "$fdfile" ]; then printf '|';
else printf '?'; fi
return 0
}
# Additional info about the given file descriptor that is appended to the log
# line
fd_info() {
fd=$1
fdfile=/proc/self/fd/$fd
if [ "$(fd_type $1)" = '*' ]; then printf "%s" "$(readlink -f "$fdfile")"; fi
}
# Logs the given command to stderr if `is_verbose [<value for -l>]`. Runs the
# command regardless.
log_command() {
local OPTIND
local OPTARG
local level=1
while getopts 'l:' opt; do
case "$opt" in
l) level=$OPTARG ;;
?) return 1;;
esac
done
shift $((OPTIND-1))
if is_verbose $level; then
cmd=$(quote_args "$@")
fdi=$(fd_info 0)
printf " %s %s%s\n" "$(fd_type 0)" "$cmd" "${fdi:+" <$fdi"}" >&2
fi
"$@"
}
LL_TPM=3
LL_TPM_DEBUG=4
LL_CRYPT=3
LL_EFI=3
LL_MISC=4
lc_tpm() {
if ! is_verbose $LL_TPM_DEBUG && [ -z "${1##tpm2_*}" -a "$1" != "tpm2_nvreadpublic" -a "$1" != "tpm2_pcrread" ]; then
local cmd=$1
shift
set -- "$cmd" -Q "$@"
fi
log_command -l $LL_TPM "$@";
}
lc_crypt() { log_command -l $LL_CRYPT "$@"; }
lc_efi() { log_command -l $LL_EFI "$@"; }
lc_misc() { log_command -l $LL_MISC "$@"; }
# Self-explanatory. From
# https://github.com/mateusza/shellscripthttpd/blob/master/base64.sh
b64encode() {
local OPTIND
local OPTARG
local wrap=65
while getopts 'w:' opt; do
case "$opt" in
w) wrap=$OPTARG ;;
:) echo "$OPTARG requires an argument" >&2; return 1;;
?) echo "unknown argument" >&2; return 1;;
esac
done
if [ "$wrap" = 0 ]; then
wrap=''
fi
shift $((OPTIND-1))
hexdump -v -e '2/1 "%02x"' | \
sed -e 's/0/0000 /g;s/1/0001 /g;s/2/0010 /g;s/3/0011 /g;
s/4/0100 /g;s/5/0101 /g;s/6/0110 /g;s/7/0111 /g;
s/8/1000 /g;s/9/1001 /g;s/a/1010 /g;s/b/1011 /g;
s/c/1100 /g;s/d/1101 /g;s/e/1110 /g;s/f/1111 /g;' | \
tr -d ' ' | \
sed -e 's/[01]\{6\}/\0 /g' | \
sed -e 's_000000_A_g; s_000001_B_g; s_000010_C_g; s_000011_D_g;
s_000100_E_g; s_000101_F_g; s_000110_G_g; s_000111_H_g;
s_001000_I_g; s_001001_J_g; s_001010_K_g; s_001011_L_g;
s_001100_M_g; s_001101_N_g; s_001110_O_g; s_001111_P_g;
s_010000_Q_g; s_010001_R_g; s_010010_S_g; s_010011_T_g;
s_010100_U_g; s_010101_V_g; s_010110_W_g; s_010111_X_g;
s_011000_Y_g; s_011001_Z_g; s_011010_a_g; s_011011_b_g;
s_011100_c_g; s_011101_d_g; s_011110_e_g; s_011111_f_g;
s_100000_g_g; s_100001_h_g; s_100010_i_g; s_100011_j_g;
s_100100_k_g; s_100101_l_g; s_100110_m_g; s_100111_n_g;
s_101000_o_g; s_101001_p_g; s_101010_q_g; s_101011_r_g;
s_101100_s_g; s_101101_t_g; s_101110_u_g; s_101111_v_g;
s_110000_w_g; s_110001_x_g; s_110010_y_g; s_110011_z_g;
s_110100_0_g; s_110101_1_g; s_110110_2_g; s_110111_3_g;
s_111000_4_g; s_111001_5_g; s_111010_6_g; s_111011_7_g;
s_111100_8_g; s_111101_9_g; s_111110_+_g; s_111111_/_g;
s_0000_A=_g; s_0001_E=_g; s_0010_I=_g; s_0011_M=_g;
s_0100_Q=_g; s_0101_U=_g; s_0110_Y=_g; s_0111_c=_g;
s_1000_g=_g; s_1001_k=_g; s_1010_o=_g; s_1011_s=_g;
s_1100_w=_g; s_1101_0=_g; s_1110_4=_g; s_1111_8=_g;
s_00_A==_; s_01_Q==_; s_10_g==_; s_11_w==_;' | \
tr -d ' ' | \
if [ -n "$wrap" ]; then sed -e 's/.\{'"$wrap"'\}/\0\n/g'; else cat; fi
if [ -n "$wrap" ]; then
echo
fi
}
b64decode() {
/usr/bin/printf "$(
tr -d '\n' | \
sed -e 's_A==_@@_; s_Q==_@,_; s_g==_,@_; s_w==_,,_;
s_A=_@@@@_; s_E=_@@@,_; s_I=_@@,@_; s_M=_@@,,_;
s_Q=_@,@@_; s_U=_@,@,_; s_Y=_@,,@_; s_c=_@,,,_;
s_g=_,@@@_; s_k=_,@@,_; s_o=_,@,@_; s_s=_,@,,_;
s_w=_,,@@_; s_0=_,,@,_; s_4=_,,,@_; s_8=_,,,,_;
s_A_@@@@@@_g; s_B_@@@@@,_g; s_C_@@@@,@_g; s_D_@@@@,,_g;
s_E_@@@,@@_g; s_F_@@@,@,_g; s_G_@@@,,@_g; s_H_@@@,,,_g;
s_I_@@,@@@_g; s_J_@@,@@,_g; s_K_@@,@,@_g; s_L_@@,@,,_g;
s_M_@@,,@@_g; s_N_@@,,@,_g; s_O_@@,,,@_g; s_P_@@,,,,_g;
s_Q_@,@@@@_g; s_R_@,@@@,_g; s_S_@,@@,@_g; s_T_@,@@,,_g;
s_U_@,@,@@_g; s_V_@,@,@,_g; s_W_@,@,,@_g; s_X_@,@,,,_g;
s_Y_@,,@@@_g; s_Z_@,,@@,_g; s_a_@,,@,@_g; s_b_@,,@,,_g;
s_c_@,,,@@_g; s_d_@,,,@,_g; s_e_@,,,,@_g; s_f_@,,,,,_g;
s_g_,@@@@@_g; s_h_,@@@@,_g; s_i_,@@@,@_g; s_j_,@@@,,_g;
s_k_,@@,@@_g; s_l_,@@,@,_g; s_m_,@@,,@_g; s_n_,@@,,,_g;
s_o_,@,@@@_g; s_p_,@,@@,_g; s_q_,@,@,@_g; s_r_,@,@,,_g;
s_s_,@,,@@_g; s_t_,@,,@,_g; s_u_,@,,,@_g; s_v_,@,,,,_g;
s_w_,,@@@@_g; s_x_,,@@@,_g; s_y_,,@@,@_g; s_z_,,@@,,_g;
s_0_,,@,@@_g; s_1_,,@,@,_g; s_2_,,@,,@_g; s_3_,,@,,,_g;
s_4_,,,@@@_g; s_5_,,,@@,_g; s_6_,,,@,@_g; s_7_,,,@,,_g;
s_8_,,,,@@_g; s_9_,,,,@,_g; s_+_,,,,,@_g; s_/_,,,,,,_g;' | \
sed -e 's/[,@]\{4\}/\0 /g' | \
sed -e 's/@@@@/0/g; s/@@@,/1/g; s/@@,@/2/g; s/@@,,/3/g;
s/@,@@/4/g; s/@,@,/5/g; s/@,,@/6/g; s/@,,,/7/g;
s/,@@@/8/g; s/,@@,/9/g; s/,@,@/a/g; s/,@,,/b/g;
s/,,@@/c/g; s/,,@,/d/g; s/,,,@/e/g; s/,,,,/f/g;' | \
tr -d ' ' | \
sed -e 's/../\\x\0/g'
)"
}
# Outputs the path to the given loader filename as mounted on the filesystem
emboot_loader_unix_path() {
printf %s\\n "$EFI_MOUNT/EFI/$OS_SHORT_NAME/${1:-emboot.efi}"
}
# Outputs the UEFI runtime path to the loader (i.e., relative to the EFI system
# partition root, using backslashes instead of forward slashes)
emboot_loader_path() {
printf %s\\n '\EFI\'"$OS_SHORT_NAME"'\'"${1:-emboot.efi}"
}
# Converts a UEFI runtime path to a UNIX path relative to the filesystem root
efi_path_to_unix() {
printf %s "$EFI_MOUNT"
printf %s%s\\n "$1" | sed -e 's/\\/\//g'
}
# Outputs the kernel release for each given input kernel path. Requires that
# the kernel filenames match vmlinuz-<release>, and fails otherwise.
kernel_path_to_release() {
space=
for kpath; do
kfn=${kpath##*/}
case "$kfn" in
vmlinuz-*)
printf %s "$space${kfn##vmlinuz-}"
space=' '
;;
*)
return 1
;;
esac
done
}
# Builds an EFI app from the given files (kernel image, initrd, kernel command
# line, and kernel release number) along with the host OS release file and the
# configured EFI stub, and writes it to the given output filename
create_loader() {
local kernel=$1
local initrd=$2
local kcli=$3
local krel=$4
local output=$5
lc_efi objcopy --add-section .osrel="/usr/lib/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="$kcli" --change-section-vma .cmdline=0x30000 \
--add-section .krel="$krel" --change-section-vma .krel=0x40000 \
--add-section .linux="$kernel" --change-section-vma .linux=0x2000000 \
--add-section .initrd="$initrd" --change-section-vma .initrd=0x3000000 \
${EFI_STUB:-/usr/lib/systemd/boot/efi/linuxx64.efi.stub} "$output"
}
extract_krel_from_loader() {
local workdir=$1
local loader=$2
local of=$workdir/cur_krel.txt
rm -f "$of"
lc_efi objcopy -O binary --only-section=.krel "$loader" "$of"
krel=$(cat "$of")
rm -f "$of"
[ -n "$krel" ] && printf "%s" "$krel"
}
# Increments the monotonic counter
increment_counter() {
lc_tpm tpm2_nvincrement -C o "$COUNTER_HANDLE"
}
# Writes the current value of the monotonic counter to the argument filename
read_counter() {
[ -n "$1" ] || return 1
lc_tpm tpm2_nvread -C o "$COUNTER_HANDLE" -s 8 -o "$1"
}
# Writes the configured PCRs to the argument filename
read_pcrs() {
lc_tpm tpm2_pcrread ${1:+-Q} sha256:"$SEAL_PCRS" ${1:+-o "$1"}
}
# Writes future PCR predictions to the argument filename, passing the remaining
# arguments to tpm_futurepcr
predict_future_pcrs() {
local outf=$1
[ -n "$outf" ] || return 1
shift
lc_tpm /root/.local/bin/tpm_futurepcr -L "$SEAL_PCRS" -H sha256 "$@" -o "$outf"
}
# Splits a string into multiple newline-separated strings of at most the given
# length
split_by_length() {
local arg=$1
local len=$2
while [ -n "$arg" ]; do
local entry=$(printf "%s" "$arg" | head -c $len)
printf "%s\n" "$entry"
arg=${arg#$entry}
done
}
# Complexity required only because tpm2-tools provides no way of formatting
# binary PCR output as YAML
diff_pcrs() {
local expected_fn=$1
local expected_txt_fn=$(dirname $1)/expected_pcrs.txt
local current_txt_fn=$2
local expected_hex=$(xxd -p -c9999 <$expected_fn | tr a-f A-F)
eval set -- $(split_by_length "$expected_hex" 64)
local re=''
{
local IFS=,
for pcr in $SEAL_PCRS; do
re="$re"'s/^\(\s*'"$pcr"'\s*:\s*0x\).*/\1'"$(quote_bre "$1")"'/i; '
shift
done
}
sed -e "$re" <$current_txt_fn >$expected_txt_fn
diff -i -u -U 9999 "$expected_txt_fn" "$current_txt_fn"
}
# Outputs a space-separated list of token IDs associated with emboot, and
# kernel release (if specified)
list_luks_token_ids() {
local cryptdev=$1
local krel=$2
lc_crypt cryptsetup luksDump "$cryptdev" --dump-json-metadata | lc_misc jq -j '."tokens" | to_entries | map(select(."value"."type" == "emboot"'"${krel:+ and .\"value\".\"krel\" == \"$krel\"}"')) | map(.key) | join(" ")'
}
# Outputs the expected platform state and sealed data from a given token ID
# from the given LUKS device to the given working directory
export_luks_token() {
local workdir=${1:-.}
local cryptdev=$2
local token_id=$3
md=$(lc_crypt cryptsetup token export "$cryptdev" --token-id "$token_id" 2>/dev/null)
if [ -z "$md" ]; then
return 1
fi
for k in counter pcrs sealed.priv sealed.pub; do
rm -f "$workdir/$k"
printf %s "$md" | lc_misc jq -j ".\"$k\"" | b64decode >$workdir/$k
done
return 0
}
# Seals stdin to the (expected future) platform state (PCRs `pcrs` and
# monotonic counter value `counter`) from the argument working directory.
# Sealed data is written to sealed.{priv,pub} in that directory.
seal_data() {(
local workdir=${1:-.}
# Make sure we don't unintentionally send input to anything but
# tpm2_create. This is also the reason for the subshell: so we don't have
# to explicitly restore stdin for the caller.
exec 3<&0 </dev/null
trap 'rc=$?; trap - EXIT; lc_tpm tpm2_flushcontext -t; lc_tpm tpm2_flushcontext -s; exit $rc' EXIT
rm -f "$workdir"/sealed.pub "$workdir"/sealed.priv
local pctx="$workdir"/pctx.ctx
local sctx="$workdir"/session.ctx
local policy="$workdir"/policy
lc_tpm tpm2_createprimary -C o -g sha256 -G ecc256:null:aes128cfb -a 'fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|decrypt' -c "$pctx" && \
lc_tpm tpm2_startauthsession -S "$sctx" && \
lc_tpm tpm2_policypcr -S "$sctx" -l sha256:"$SEAL_PCRS" -f "$workdir"/pcrs && \
lc_tpm tpm2_policynv -S "$sctx" -C o -i "$workdir"/counter -L "$policy" "$COUNTER_HANDLE" ule && \
lc_tpm tpm2_create -C "$pctx" -g sha256 -a 'fixedtpm|fixedparent|adminwithpolicy|noda' -i - -L "$policy" -r "$workdir"/sealed.priv -u "$workdir"/sealed.pub <&3 3<&-
)}
# Attempts to unseal the sealed data (sealed.{priv,pub}) from the given working
# directory using the expected platform state (also from that working
# directory) and output the plaintext to stdout. Will use current PCR values
# and/or monotonic counter value in creating the policy session if the expected
# values are not available: on failure, this mainly impacts the error reported
# by the TPM.
unseal_data() {(
local workdir=${1:-.}
# Make sure we don't unintentionally send anything but the output of
# tpm2_unseal to stdout. This is also the reason for the subshell: so we
# don't have to explicitly restore stdout for the caller.
exec 3>&1 >&2
trap 'rc=$?; trap - EXIT; lc_tpm tpm2_flushcontext -t; lc_tpm tpm2_flushcontext -s; exit $rc' EXIT
if [ ! -r "$workdir"/pcrs ]; then
read_pcrs "$workdir"/pcrs || return 1
fi
if [ ! -r "$workdir"/counter ]; then
read_counter "$workdir"/counter || return 1
fi
local pctx="$workdir"/pctx.ctx
local sctx="$workdir"/session.ctx
lc_tpm tpm2_createprimary -C o -g sha256 -G ecc256:null:aes128cfb -a 'fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|decrypt' -c "$pctx" && \
lc_tpm tpm2_load -C "$pctx" -r "$workdir"/sealed.priv -u "$workdir"/sealed.pub -c "$workdir"/load.ctx && \
lc_tpm tpm2_startauthsession -S "$sctx" --policy-session && \
lc_tpm tpm2_policypcr -S "$sctx" -l sha256:"$SEAL_PCRS" -f "$workdir"/pcrs && \
lc_tpm tpm2_policynv -C o -S "$sctx" -i "$workdir"/counter "$COUNTER_HANDLE" ule && \
lc_tpm tpm2_unseal -c "$workdir"/load.ctx -p session:"$sctx" >&3 3>&-
)}