Mateusz Zalega | 356b896 | 2021-08-10 17:27:15 +0200 | [diff] [blame] | 1 | package verity |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "crypto/aes" |
| 6 | "crypto/cipher" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "os" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/stretchr/testify/require" |
| 13 | "golang.org/x/sys/unix" |
| 14 | |
| 15 | dm "source.monogon.dev/metropolis/pkg/devicemapper" |
| 16 | ) |
| 17 | |
| 18 | const ( |
| 19 | // testDataSize configures the size of Verity-protected data devices. |
| 20 | testDataSize int64 = 2 * 1024 * 1024 |
| 21 | // accessMode configures new files' permission bits. |
| 22 | accessMode = 0600 |
| 23 | ) |
| 24 | |
| 25 | // getRamdisk creates a device file pointing to an unused ramdisk. |
| 26 | // Returns a filesystem path. |
| 27 | func getRamdisk() (string, error) { |
| 28 | for i := 0; ; i++ { |
| 29 | path := fmt.Sprintf("/dev/ram%d", i) |
| 30 | dn := unix.Mkdev(1, uint32(i)) |
| 31 | err := unix.Mknod(path, accessMode|unix.S_IFBLK, int(dn)) |
| 32 | if os.IsExist(err) { |
| 33 | continue |
| 34 | } |
| 35 | if err != nil { |
| 36 | return "", err |
| 37 | } |
| 38 | return path, nil |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // verityDMTarget returns a dm.Target based on a Verity mapping table. |
| 43 | func verityDMTarget(mt *MappingTable) *dm.Target { |
| 44 | return &dm.Target{ |
| 45 | Type: "verity", |
| 46 | StartSector: 0, |
| 47 | Length: mt.Length(), |
| 48 | Parameters: mt.VerityParameterList(), |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | // devZeroReader is a helper type used by writeRandomBytes. |
| 53 | type devZeroReader struct{} |
| 54 | |
| 55 | // Read implements io.Reader on devZeroReader, making it a source of zero |
| 56 | // bytes. |
| 57 | func (_ devZeroReader) Read(b []byte) (int, error) { |
| 58 | for i := range b { |
| 59 | b[i] = 0 |
| 60 | } |
| 61 | return len(b), nil |
| 62 | } |
| 63 | |
| 64 | // writeRandomBytes writes length pseudorandom bytes to a given io.Writer. |
| 65 | func writeRandomBytes(w io.Writer, length int64) error { |
| 66 | keyiv := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} |
| 67 | blkCipher, err := aes.NewCipher(keyiv) |
| 68 | if err != nil { |
| 69 | return err |
| 70 | } |
| 71 | var z devZeroReader |
| 72 | c := cipher.StreamReader{S: cipher.NewCTR(blkCipher, keyiv), R: z} |
| 73 | _, err = io.CopyN(w, c, length) |
| 74 | return err |
| 75 | } |
| 76 | |
| 77 | // fillVerityRamdisks fills a block device at dataDevPath with |
| 78 | // pseudorandom data and writes a complementary Verity hash device to |
| 79 | // a block device at hashDevPath. Returns a dm.Target configuring a |
| 80 | // resulting Verity device, and a buffer containing random data written |
| 81 | // the data device. |
| 82 | func fillVerityRamdisks(t *testing.T, dataDevPath, hashDevPath string) (*dm.Target, bytes.Buffer) { |
| 83 | // Open the data device for writing. |
| 84 | dfd, err := os.OpenFile(dataDevPath, os.O_WRONLY, accessMode) |
| 85 | require.NoError(t, err, "while opening the data device at %s", dataDevPath) |
| 86 | // Open the hash device for writing. |
| 87 | hfd, err := os.OpenFile(hashDevPath, os.O_WRONLY, accessMode) |
| 88 | require.NoError(t, err, "while opening the hash device at %s", hashDevPath) |
| 89 | |
| 90 | // Create a Verity encoder, backed with hfd. Configure it to write the |
| 91 | // Verity superblock. |
| 92 | verityEnc, err := NewEncoder(hfd, true) |
| 93 | require.NoError(t, err, "while creating a Verity encoder") |
| 94 | |
| 95 | // Write pseudorandom data both to the Verity-protected data device, and |
| 96 | // into the Verity encoder, which in turn will write a resulting hash |
| 97 | // tree to hfd on Close(). |
| 98 | var testData bytes.Buffer |
| 99 | tdw := io.MultiWriter(dfd, verityEnc, &testData) |
| 100 | err = writeRandomBytes(tdw, testDataSize) |
| 101 | require.NoError(t, err, "while writing test data") |
| 102 | |
| 103 | // Close the file descriptors. |
| 104 | err = verityEnc.Close() |
| 105 | require.NoError(t, err, "while closing the Verity encoder") |
| 106 | err = hfd.Close() |
| 107 | require.NoError(t, err, "while closing the hash device descriptor") |
| 108 | err = dfd.Close() |
| 109 | require.NoError(t, err, "while closing the data device descriptor") |
| 110 | |
| 111 | // Generate the Verity mapping table based on the encoder state and |
| 112 | // device file paths, then return it along with the test data buffer. |
| 113 | mt, err := verityEnc.MappingTable(dataDevPath, hashDevPath) |
| 114 | require.NoError(t, err, "while building a Verity mapping table") |
| 115 | return verityDMTarget(mt), testData |
| 116 | } |
| 117 | |
| 118 | // createVerityDevice maps a Verity device described by dmt while |
| 119 | // assigning it a name equal to devName. It returns a Verity device path. |
| 120 | func createVerityDevice(t *testing.T, dmt *dm.Target, devName string) string { |
| 121 | devNum, err := dm.CreateActiveDevice(devName, true, []dm.Target{*dmt}) |
| 122 | require.NoError(t, err, "while creating a Verity device") |
| 123 | |
| 124 | devPath := fmt.Sprintf("/dev/%s", devName) |
| 125 | err = unix.Mknod(devPath, accessMode|unix.S_IFBLK, int(devNum)) |
| 126 | require.NoError(t, err, "while creating a Verity device file at %s", devPath) |
| 127 | return devPath |
| 128 | } |
| 129 | |
| 130 | // cleanupVerityDevice deactivates a Verity device previously mapped by |
| 131 | // createVerityDevice, and removes an associated device file. |
| 132 | func cleanupVerityDevice(t *testing.T, devName string) { |
| 133 | err := dm.RemoveDevice(devName) |
| 134 | require.NoError(t, err, "while removing a Verity device %s", devName) |
| 135 | |
| 136 | devPath := fmt.Sprintf("/dev/%s", devName) |
| 137 | err = os.Remove(devPath) |
| 138 | require.NoError(t, err, "while removing a Verity device file at %s", devPath) |
| 139 | } |
| 140 | |
| 141 | // testRead compares contents of a block device at devPath with |
| 142 | // expectedData. The length of data read is equal to the length |
| 143 | // of expectedData. |
| 144 | // It returns 'false', if either data could not be read or it does not |
| 145 | // match expectedData, and 'true' otherwise. |
| 146 | func testRead(t *testing.T, devPath string, expectedData []byte) bool { |
| 147 | // Open the Verity device. |
| 148 | verityDev, err := os.Open(devPath) |
| 149 | require.NoError(t, err, "while opening a Verity device at %s", devPath) |
| 150 | defer verityDev.Close() |
| 151 | |
| 152 | // Attempt to read the test data. Abort on read errors. |
| 153 | readData := make([]byte, len(expectedData)) |
| 154 | _, err = io.ReadFull(verityDev, readData) |
| 155 | if err != nil { |
| 156 | return false |
| 157 | } |
| 158 | |
| 159 | // Return true, if read data matches expectedData. |
| 160 | if bytes.Compare(expectedData, readData) == 0 { |
| 161 | return true |
| 162 | } |
| 163 | return false |
| 164 | } |
| 165 | |
| 166 | // TestMakeAndRead attempts to create a Verity device, then verifies the |
| 167 | // integrity of its contents. |
| 168 | func TestMakeAndRead(t *testing.T) { |
| 169 | if os.Getenv("IN_KTEST") != "true" { |
| 170 | t.Skip("Not in ktest") |
| 171 | } |
| 172 | |
| 173 | // Allocate block devices backing the Verity target. |
| 174 | dataDevPath, err := getRamdisk() |
| 175 | require.NoError(t, err, "while allocating a data device ramdisk") |
| 176 | hashDevPath, err := getRamdisk() |
| 177 | require.NoError(t, err, "while allocating a hash device ramdisk") |
| 178 | |
| 179 | // Fill the data device with test data and write a corresponding Verity |
| 180 | // hash tree to the hash device. |
| 181 | dmTarget, expectedDataBuf := fillVerityRamdisks(t, dataDevPath, hashDevPath) |
| 182 | |
| 183 | // Create a Verity device using dmTarget. Use the test name as a device |
| 184 | // handle. verityPath will point to a resulting new block device. |
| 185 | verityPath := createVerityDevice(t, dmTarget, t.Name()) |
| 186 | defer cleanupVerityDevice(t, t.Name()) |
| 187 | |
| 188 | // Use testRead to compare Verity target device contents with test data |
| 189 | // written to the data block device at dataDevPath by fillVerityRamdisks. |
| 190 | if !testRead(t, verityPath, expectedDataBuf.Bytes()) { |
| 191 | t.Error("data read from the verity device doesn't match the source") |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | // TestMalformed checks whenever Verity would prevent reading from a |
| 196 | // target whose hash device contents have been corrupted, as is expected. |
| 197 | func TestMalformed(t *testing.T) { |
| 198 | if os.Getenv("IN_KTEST") != "true" { |
| 199 | t.Skip("Not in ktest") |
| 200 | } |
| 201 | |
| 202 | // Allocate block devices backing the Verity target. |
| 203 | dataDevPath, err := getRamdisk() |
| 204 | require.NoError(t, err, "while allocating a data device ramdisk") |
| 205 | hashDevPath, err := getRamdisk() |
| 206 | require.NoError(t, err, "while allocating a hash device ramdisk") |
| 207 | |
| 208 | // Fill the data device with test data and write a corresponding Verity |
| 209 | // hash tree to the hash device. |
| 210 | dmTarget, expectedDataBuf := fillVerityRamdisks(t, dataDevPath, hashDevPath) |
| 211 | |
| 212 | // Corrupt the first hash device block before mapping the Verity target. |
| 213 | hfd, err := os.OpenFile(hashDevPath, os.O_RDWR, accessMode) |
| 214 | require.NoError(t, err, "while opening a hash device at %s", hashDevPath) |
| 215 | // Place an odd byte at the 256th byte of the first hash block, skipping |
| 216 | // a 4096-byte Verity superblock. |
| 217 | hfd.Seek(4096+256, io.SeekStart) |
| 218 | hfd.Write([]byte{'F'}) |
| 219 | hfd.Close() |
| 220 | |
| 221 | // Create a Verity device using dmTarget. Use the test name as a device |
| 222 | // handle. verityPath will point to a resulting new block device. |
| 223 | verityPath := createVerityDevice(t, dmTarget, t.Name()) |
| 224 | defer cleanupVerityDevice(t, t.Name()) |
| 225 | |
| 226 | // Use testRead to compare Verity target device contents with test data |
| 227 | // written to the data block device at dataDevPath by fillVerityRamdisks. |
| 228 | // This step is expected to fail after an incomplete read. |
| 229 | if testRead(t, verityPath, expectedDataBuf.Bytes()) { |
| 230 | t.Error("data matches the source when it shouldn't") |
| 231 | } |
| 232 | } |