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
|
# Fsverify Partition
The FsVerify partition contains a header with the necessary metadata for the filesystem verification, and a bbolt database containing all File and Directory nodes to be checked.
## Partition Header
`<magic number> <untrusted signature hash> <trusted signature hash> <filesystem size> <filesystem unit> <table size> <table unit>`
Field|Size|Purpose|Value
-----|----|-------|-----
magic number|2 bytes|sanity check|0xACAB
untrusted signature hash|100 bytes|untrusted signature from minisign
trusted signature hash|88 bytes|trusted signature from minisign
filesystem size|4 bytes|size of the original filesystem in <table unit\>
filesystem unit|1 byte|unit of the filesystem size|0x0: bytes, 0x1: kilobytes, 0x2: megabytes, 0x3: gigabytes, 0x4: terabytes, 0x5: petabytes
table size|4 bytes| size of the table in <table unit\>
table unit|1 byte|unit of the table size|0x0: bytes, 0x1: kilobytes, 0x2: megabytes, 0x3: gigabytes, 0x4: terabytes, 0x5: petabytes
Due to the filesystem and table size field, which can go up to 0xFFFFFFFF (16777215), the maximum supported partition size and table size is 16777215pb
The entire Head should be a total of 200 bytes long, reaching from 0x0 to 0xC8
## Partition Contents / Database
The main database containing the checksums is a [bbolt](https://github.com/etcd-io/bbolt) datbase consisting of a single bucket called `Nodes`
Each element in this Bucket is a json serialization of the `Node` struck:
```go
type Node struct {
BlockStart int
BlockEnd int
BlockSum string
PrevNodeSum string
}
```
Field|Purpose
----|----
BlockStart|The Hex offset at which the block begins
BlockEnd|The Hex offset at which the block ends
BlockSum|The checksum of the block
PrevNodeSum|The checksum of all the fields of the previous field as a checksum and the identifier of the node
Each block is 2kb big, if the partition size does not allow an even split in 2kb sectors, the partition will be split as good as possible, with a smaller block as the last sector.
Beyond beign signed with minisign, each Node is verified through the PrevNodeSum Field, which gets generated by adding all fields of the previous block together and calculating the checksum of the resulting string:
```
+-----+ +------+ +------+ +------+
|0x000| |0xFA0 | |0x1F40| |0x3E80|
|0xFA0| --> |0x1F40| --> |0x3E80| -----> |0x4E20|
|aFcDb| |cDfaB | |4aD01 | |2FdCa |
| | |adBfa | |1Ab3d | |bAd31 |
+-----+ +------+ +------+ +------+
```
through this, the slightest change in one of the nodes will result in a wrong hash for every node following the modified one:
```
Checksums do not match
|
+-----+ +------+ +------+ | +------+
|0x000| |0xFA0 | |0x1F40| | |0x3E80|
|0xFA0| --> |0x1F40| --> |0x3E80| --|--> |0x4E20|
|aFcDb| |AAAAA | <-+ |4aD01 | | |2FdCa |
| | |adBfa | | |1Ab3d | <-+--> |bAd31 |
+-----+ +------+ | +------+ +------+
|
Modified value
```
The first Node will have `PrevNodeSum` as "Entrypoint" since the PrevNodeSum field is also used to access each node, using EntryPoint allows fsverify to start the verification by always being able to read the first node
# Verification Process
The verification step consists of multiple steps:
1. Reading the signature and public key
2. Reading the database
3. Verifying the database using the previously read keys
4. Verifying the target partition using the database
# Storing the Public key
The public key can be stored in multiple ways:
- Partition which has the key directly flashed onto it
- A text file
- TPM2
- A device that prints the key over usb serial
Fsverify does not verify the integrity or trustworthiness of these storage forms, so the integrity will have to be verified manually.
## Partition which has the key directly flashed onto it
This is quite simple, all thats required is to write the key directly into a block device:
Assuming that sda1 is the block device which should be used, it can be as simple as:
````bash
echo -n "public key" > /dev/sda1
````
It is important to make sure that only the public key is written, no extra newlines (hence the `-n`) or whitespace surrounding the key.
Once this is done, `config/config.go` can be modified to set the `KeyStore` value to `1` and `KeyLocation` to `/dev/sda1`
Any form of accessing the block device directly is viable, but it is recommended to specify the uuid of the device instead of using a label or name, as that would always verify that the right block device is used and makes spoofing the device harder.
## Text file
The simplest form, and only recommended to be used in a secureboot verified UKI or a device/partition that can always be trusted (i.e. it is always read-only)
All that is required is to write the public key into a text file:
```bash
echo -n "public key" > /path/to/publickey
```
It is important to make sure that only the public key is written, no extra newlines (hence the `-n`) or whitespace surrounding the key.
Once this is done, `config/config.go` can be modified to set the `KeyStore` value to `0` and `KeyLocation` to `/path/to/publickey`
## TPM2
to be done
## Device that prints the key over usb serial
This is the safest form of storing the key, if done properly.
Any device, like a microcontroller, that has the option to write into a serial tty (`/dev/ttyACM*`) with the baud rate `9600` can be used for this.
It is recommended to make sure that the microcontroller cannot be reflashed once the key is written successfully, to ensure that the device does not get overwritten with the key of an attacker.
An (unsafe) example using an arduino can look like this:
```C
// fsverify-serial.ino
void setup() {
Serial.setup(9600) // set up a serial tty with the baud rate 9600
Serial.print("\tpublic key\t") // Write the public key to the tty
}
void loop() {}
```
This will print the public key once the arduino is connected to the device, make sure that the public key is always surrounded by a `\t`, as the tab sequence is used by fsverify to figure out where a key starts and where it ends.
To ensure the safety of using this method, in addition to having the microcontroller be read only, it also has to be made sure that the microcontroller is embedded in the device in a way where it cannot be easily replaced since using an external arduino makes no checks to see if the ttyACM device being accessed is actually the legitimate device.
## Reading the Signature and Public Key
The header only contains parts of the signature, the Trusted Hash and the Untrusted Hash, using this a complete signature is constructed, this allows for easier storage of the signature as the full signature contains data that can change over time (but is not required for signing) and break the header by becoming too big.
The Public Key however is not stored in the partition, instead it can be stored in multiple ways
- A different partition that has been verified in a different way
- An external storage device that can always be trusted
- The TPM2
- Secureboot verified UKI
The most secure option for most average Desktop computers would be the TPM2 or a secureboot verified UKI, embedded devices however would greatly benefit from a partition that cannot be modified in any way, in which the key is stored
In the case that the hardware itself cannot be trusted, read-only external storage can be used to store the key, this can ensure that the public key is never modified, assuming the person carrying said storage device does not loose it.
## Reading the database
The Database is simply read from 0x13A until the size of the table is reached as specified in the headers. If the table is 1mb big, it would reach from 0xC8 until 0xF4308 (1000000bytes/1mb)
## Verifying the database using the previously read keys
Now that the signature, public key and database are read, they can be verified using [minisign](https://jedisct1.github.io/minisign/).
If the verification fails, a protected rootfs is used to display a warning to the user and/or completely shut down the device.
## Verifying the target partition using the database
If the database was verified succesfully, it can now be used to verify the actual partition.
Each entry in the database is read and the according data in the specified offset area is verified to the hash. At the same time, the database verifies itself by using the `PrevNodeSum` property of the `Node` object.
If the verification fails, be it due to a mismatch in `PrevNodeSum` or a mismatch in the Block checksum, the same protected rootfs as with the database verification is used to display a warning to the user and/or completely shut down the device.
# When/how to run fsverify
Fsverify can theoretically be run at any point of the boot process, however it is the most useful when run
- before the init system (systemd, openrc, runit, etc.) launches, but after the initramfs is exited
- while the system is still inside the initramfs and the real system is not even mounted yet
The second option is the preffered option, as it not only makes the modification of fsverify more difficult, but also, in the case that secureboot and UKI is set up correctly, can not be modified whatsoever without failing the secureboot checks, allowing for an extra layer of verification that the fsverify binary has not been manipulated.
If neither UKI nor secureboot are implemented however, the first option will work just as well and could possibly be easier to implement, as it simply takes an init script that executes the fsverify binary before the init system launches. This order is important, to ensure that no extra binaries are executed before it is ensured that they can be trusted.
|