blob: 2c9c35b71cff71fbd226fc45e6386ce0c5cdf170 [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)
Jan Schär3871fa12025-07-09 17:30:00 +000096 salt := []byte("testsalt")
97 verityEnc, err := NewEncoder(hfd, bs, bs, salt, true)
Mateusz Zalega356b8962021-08-10 17:27:15 +020098 require.NoError(t, err, "while creating a Verity encoder")
99
100 // Write pseudorandom data both to the Verity-protected data device, and
101 // into the Verity encoder, which in turn will write a resulting hash
102 // tree to hfd on Close().
103 var testData bytes.Buffer
104 tdw := io.MultiWriter(dfd, verityEnc, &testData)
105 err = writeRandomBytes(tdw, testDataSize)
106 require.NoError(t, err, "while writing test data")
107
108 // Close the file descriptors.
109 err = verityEnc.Close()
110 require.NoError(t, err, "while closing the Verity encoder")
111 err = hfd.Close()
112 require.NoError(t, err, "while closing the hash device descriptor")
113 err = dfd.Close()
114 require.NoError(t, err, "while closing the data device descriptor")
115
Mateusz Zalegaba1da9d2022-01-25 19:12:02 +0100116 // Generate the Verity mapping table based on the encoder state, device
117 // file paths and the metadata starting block, then return it along with
118 // the test data buffer.
119 mt, err := verityEnc.MappingTable(dataDevPath, hashDevPath, 0)
Mateusz Zalega356b8962021-08-10 17:27:15 +0200120 require.NoError(t, err, "while building a Verity mapping table")
121 return verityDMTarget(mt), testData
122}
123
124// createVerityDevice maps a Verity device described by dmt while
125// assigning it a name equal to devName. It returns a Verity device path.
126func createVerityDevice(t *testing.T, dmt *dm.Target, devName string) string {
127 devNum, err := dm.CreateActiveDevice(devName, true, []dm.Target{*dmt})
128 require.NoError(t, err, "while creating a Verity device")
129
130 devPath := fmt.Sprintf("/dev/%s", devName)
131 err = unix.Mknod(devPath, accessMode|unix.S_IFBLK, int(devNum))
132 require.NoError(t, err, "while creating a Verity device file at %s", devPath)
133 return devPath
134}
135
136// cleanupVerityDevice deactivates a Verity device previously mapped by
137// createVerityDevice, and removes an associated device file.
138func cleanupVerityDevice(t *testing.T, devName string) {
139 err := dm.RemoveDevice(devName)
140 require.NoError(t, err, "while removing a Verity device %s", devName)
141
142 devPath := fmt.Sprintf("/dev/%s", devName)
143 err = os.Remove(devPath)
144 require.NoError(t, err, "while removing a Verity device file at %s", devPath)
145}
146
147// testRead compares contents of a block device at devPath with
148// expectedData. The length of data read is equal to the length
149// of expectedData.
150// It returns 'false', if either data could not be read or it does not
151// match expectedData, and 'true' otherwise.
152func testRead(t *testing.T, devPath string, expectedData []byte) bool {
153 // Open the Verity device.
154 verityDev, err := os.Open(devPath)
155 require.NoError(t, err, "while opening a Verity device at %s", devPath)
156 defer verityDev.Close()
157
158 // Attempt to read the test data. Abort on read errors.
159 readData := make([]byte, len(expectedData))
160 _, err = io.ReadFull(verityDev, readData)
161 if err != nil {
162 return false
163 }
164
165 // Return true, if read data matches expectedData.
Tim Windelschmidta2eea162024-04-18 23:39:38 +0200166 if bytes.Equal(expectedData, readData) {
Mateusz Zalega356b8962021-08-10 17:27:15 +0200167 return true
168 }
169 return false
170}
171
172// TestMakeAndRead attempts to create a Verity device, then verifies the
173// integrity of its contents.
174func TestMakeAndRead(t *testing.T) {
175 if os.Getenv("IN_KTEST") != "true" {
176 t.Skip("Not in ktest")
177 }
178
179 // Allocate block devices backing the Verity target.
180 dataDevPath, err := getRamdisk()
181 require.NoError(t, err, "while allocating a data device ramdisk")
182 hashDevPath, err := getRamdisk()
183 require.NoError(t, err, "while allocating a hash device ramdisk")
184
185 // Fill the data device with test data and write a corresponding Verity
186 // hash tree to the hash device.
187 dmTarget, expectedDataBuf := fillVerityRamdisks(t, dataDevPath, hashDevPath)
188
189 // Create a Verity device using dmTarget. Use the test name as a device
190 // handle. verityPath will point to a resulting new block device.
191 verityPath := createVerityDevice(t, dmTarget, t.Name())
192 defer cleanupVerityDevice(t, t.Name())
193
194 // Use testRead to compare Verity target device contents with test data
195 // written to the data block device at dataDevPath by fillVerityRamdisks.
196 if !testRead(t, verityPath, expectedDataBuf.Bytes()) {
197 t.Error("data read from the verity device doesn't match the source")
198 }
199}
200
201// TestMalformed checks whenever Verity would prevent reading from a
202// target whose hash device contents have been corrupted, as is expected.
203func TestMalformed(t *testing.T) {
204 if os.Getenv("IN_KTEST") != "true" {
205 t.Skip("Not in ktest")
206 }
207
208 // Allocate block devices backing the Verity target.
209 dataDevPath, err := getRamdisk()
210 require.NoError(t, err, "while allocating a data device ramdisk")
211 hashDevPath, err := getRamdisk()
212 require.NoError(t, err, "while allocating a hash device ramdisk")
213
214 // Fill the data device with test data and write a corresponding Verity
215 // hash tree to the hash device.
216 dmTarget, expectedDataBuf := fillVerityRamdisks(t, dataDevPath, hashDevPath)
217
218 // Corrupt the first hash device block before mapping the Verity target.
219 hfd, err := os.OpenFile(hashDevPath, os.O_RDWR, accessMode)
220 require.NoError(t, err, "while opening a hash device at %s", hashDevPath)
221 // Place an odd byte at the 256th byte of the first hash block, skipping
222 // a 4096-byte Verity superblock.
223 hfd.Seek(4096+256, io.SeekStart)
224 hfd.Write([]byte{'F'})
225 hfd.Close()
226
227 // Create a Verity device using dmTarget. Use the test name as a device
228 // handle. verityPath will point to a resulting new block device.
229 verityPath := createVerityDevice(t, dmTarget, t.Name())
230 defer cleanupVerityDevice(t, t.Name())
231
232 // Use testRead to compare Verity target device contents with test data
233 // written to the data block device at dataDevPath by fillVerityRamdisks.
234 // This step is expected to fail after an incomplete read.
235 if testRead(t, verityPath, expectedDataBuf.Bytes()) {
236 t.Error("data matches the source when it shouldn't")
237 }
238}