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
|
# Tests that `cryptsetup luksSuspend` successfully wipes the volume key from memory.
{ pkgs, lib, ... }:
let
# Fixed 64-byte volume key.
# Chosen by fair dice roll. Guaranteed to be random. :-)
# (tr -dc '0-9A-Za-z' </dev/urandom | head -c 64)
volumeKey = "Okqf1piY8oebIL11l3x5lkqzyqLuY8X0aROgI6wMuUV99PoeqHbgsptcACs0ojaL";
luksUUID = "55555555-5555-5555-5555-555555555555";
# 1 GiB of RAM for the VM. Less than the 4 GiB PCI hole, so that a
# single `pmemsave 0 <MEM_BYTES>` captures all of guest RAM.
memorySizeMiB = 1024;
# A LUKS2 container with the known master key above.
luksImage =
pkgs.runCommand "luks-suspend-test-disk.img" { nativeBuildInputs = [ pkgs.cryptsetup ]; }
''
truncate -s 64M $out
echo -n abc | cryptsetup luksFormat \
--batch-mode \
--uuid ${luksUUID} \
--pbkdf pbkdf2 --pbkdf-force-iterations 1000 \
--volume-key-file <(echo -n "${volumeKey}") \
$out
'';
commonMachine =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.cryptsetup ];
virtualisation.memorySize = memorySizeMiB;
virtualisation.qemu.drives = [
{
name = "luks";
file = "${luksImage}";
driveExtraOpts = {
format = "raw";
readonly = "on";
};
}
];
};
in
{
name = "luks-suspend";
meta.maintainers = with lib.maintainers; [ iblech ];
nodes = {
# Variant 1: LUKS volume opened after boot via the cryptsetup CLI.
cli = commonMachine;
# Variant 2: LUKS volume opened during boot by systemd-cryptsetup,
# driven by the native NixOS LUKS module.
native =
{ pkgs, lib, ... }:
{
imports = [ commonMachine ];
boot.initrd.systemd.enable = true;
# The test framework's qemu-vm module force-clears
# boot.initrd.luks.devices via mkVMOverride (priority 10) when
# virtualisation.useDefaultFilesystems is on (the default).
# Our setting should have priority.
boot.initrd.luks.devices = lib.mkOverride 5 {
foo = {
device = "/dev/disk/by-uuid/${luksUUID}";
keyFile = "/etc/luks-foo.key";
};
};
# Will produce (harmless) warnings about a world-readable keyFile.
boot.initrd.systemd.contents."/etc/luks-foo.key".source = pkgs.writeText "luks-foo-key" "abc";
};
};
testScript = ''
import mmap
MEM_BYTES = ${toString memorySizeMiB} * 1024 * 1024
VOLUME_KEY = b"${volumeKey}"
LUKS_DEV = "/dev/disk/by-uuid/${luksUUID}"
def count_occurrences(dump_path, needle):
count = 0
with open(dump_path, "rb") as f:
with mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) as m:
i = m.find(needle)
while i != -1:
count += 1
i = m.find(needle, i + 1)
return count
def dump_and_count(machine, label):
path = machine.state_dir / f"ram-{machine.name}-{label}.raw"
reply = machine.send_monitor_command(f'pmemsave 0 {MEM_BYTES} "{path}"')
if not path.exists():
raise Exception(f"pmemsave wrote no dump; monitor replied: {reply!r}")
full = count_occurrences(path, VOLUME_KEY)
first = count_occurrences(path, VOLUME_KEY[:32])
second = count_occurrences(path, VOLUME_KEY[32:])
path.unlink()
print(
f"[{machine.name}/{label}] volume-key copies in RAM: "
f"full={full} first-half={first} second-half={second}"
)
return full, first, second
def check_suspend_wipes_key(machine):
# Precondition: `machine` reached multi-user.target with the LUKS
# volume open as /dev/mapper/foo.
machine.succeed("test -b /dev/mapper/foo")
# Give kernel-keyring garbage collection time to run.
machine.sleep(10)
before_full, before_first, before_second = dump_and_count(machine, "before-suspend")
assert (before_full, before_first, before_second) == (0, 1, 1), (
f"[{machine.name}] expected exactly one copy of the two parts of the volume key each "
f"while the volume is open, found {before_full} / {before_first} / {before_second}"
)
# Suspend the volume -- this must wipe the key from kernel memory.
machine.succeed("cryptsetup luksSuspend foo")
after_full, after_first, after_second = dump_and_count(machine, "after-suspend")
assert (after_full, after_first, after_second) == (0, 0, 0), (
f"[{machine.name}] volume key material still present in RAM after luksSuspend "
f"(full={after_full} first-half={after_first} second-half={after_second}) "
f"-- key-wipe bug is present"
)
# Variant 1
cli.start()
cli.wait_for_unit("multi-user.target")
cli.wait_for_file(LUKS_DEV)
cli.succeed(f"echo -n abc | cryptsetup luksOpen {LUKS_DEV} foo")
check_suspend_wipes_key(cli)
# Variant 2
native.start()
native.wait_for_unit("multi-user.target")
check_suspend_wipes_key(native)
'';
}
|