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