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