blob: 21c4623eb028e054810b4c5ffe792033a4ff5720 [file] [log] [blame]
Mateusz Zalega356b8962021-08-10 17:27:15 +02001package verity
2
3import (
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
18const (
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.
27func 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.
43func 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.
53type devZeroReader struct{}
54
55// Read implements io.Reader on devZeroReader, making it a source of zero
56// bytes.
57func (_ 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.
65func 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.
82func 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.
120func 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.
132func 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.
146func 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.
168func 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.
197func 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}