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