blob: b53e06e529d31d37ecadbcec887792925daaa146 [file] [log] [blame] [edit]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package verity
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"fmt"
"io"
"os"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
dm "source.monogon.dev/metropolis/pkg/devicemapper"
)
const (
// testDataSize configures the size of Verity-protected data devices.
testDataSize int64 = 2 * 1024 * 1024
// accessMode configures new files' permission bits.
accessMode = 0600
)
// getRamdisk creates a device file pointing to an unused ramdisk.
// Returns a filesystem path.
func getRamdisk() (string, error) {
for i := 0; ; i++ {
path := fmt.Sprintf("/dev/ram%d", i)
dn := unix.Mkdev(1, uint32(i))
err := unix.Mknod(path, accessMode|unix.S_IFBLK, int(dn))
if os.IsExist(err) {
continue
}
if err != nil {
return "", err
}
return path, nil
}
}
// verityDMTarget returns a dm.Target based on a Verity mapping table.
func verityDMTarget(mt *MappingTable) *dm.Target {
return &dm.Target{
Type: "verity",
StartSector: 0,
Length: mt.Length(),
Parameters: mt.VerityParameterList(),
}
}
// devZeroReader is a helper type used by writeRandomBytes.
type devZeroReader struct{}
// Read implements io.Reader on devZeroReader, making it a source of zero
// bytes.
func (_ devZeroReader) Read(b []byte) (int, error) {
for i := range b {
b[i] = 0
}
return len(b), nil
}
// writeRandomBytes writes length pseudorandom bytes to a given io.Writer.
func writeRandomBytes(w io.Writer, length int64) error {
keyiv := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
blkCipher, err := aes.NewCipher(keyiv)
if err != nil {
return err
}
var z devZeroReader
c := cipher.StreamReader{S: cipher.NewCTR(blkCipher, keyiv), R: z}
_, err = io.CopyN(w, c, length)
return err
}
// fillVerityRamdisks fills a block device at dataDevPath with
// pseudorandom data and writes a complementary Verity hash device to
// a block device at hashDevPath. Returns a dm.Target configuring a
// resulting Verity device, and a buffer containing random data written
// the data device.
func fillVerityRamdisks(t *testing.T, dataDevPath, hashDevPath string) (*dm.Target, bytes.Buffer) {
// Open the data device for writing.
dfd, err := os.OpenFile(dataDevPath, os.O_WRONLY, accessMode)
require.NoError(t, err, "while opening the data device at %s", dataDevPath)
// Open the hash device for writing.
hfd, err := os.OpenFile(hashDevPath, os.O_WRONLY, accessMode)
require.NoError(t, err, "while opening the hash device at %s", hashDevPath)
// Create a Verity encoder, backed with hfd. Configure it to write the
// Verity superblock. Use 4096-byte blocks.
bs := uint32(4096)
verityEnc, err := NewEncoder(hfd, bs, bs, true)
require.NoError(t, err, "while creating a Verity encoder")
// Write pseudorandom data both to the Verity-protected data device, and
// into the Verity encoder, which in turn will write a resulting hash
// tree to hfd on Close().
var testData bytes.Buffer
tdw := io.MultiWriter(dfd, verityEnc, &testData)
err = writeRandomBytes(tdw, testDataSize)
require.NoError(t, err, "while writing test data")
// Close the file descriptors.
err = verityEnc.Close()
require.NoError(t, err, "while closing the Verity encoder")
err = hfd.Close()
require.NoError(t, err, "while closing the hash device descriptor")
err = dfd.Close()
require.NoError(t, err, "while closing the data device descriptor")
// Generate the Verity mapping table based on the encoder state, device
// file paths and the metadata starting block, then return it along with
// the test data buffer.
mt, err := verityEnc.MappingTable(dataDevPath, hashDevPath, 0)
require.NoError(t, err, "while building a Verity mapping table")
return verityDMTarget(mt), testData
}
// createVerityDevice maps a Verity device described by dmt while
// assigning it a name equal to devName. It returns a Verity device path.
func createVerityDevice(t *testing.T, dmt *dm.Target, devName string) string {
devNum, err := dm.CreateActiveDevice(devName, true, []dm.Target{*dmt})
require.NoError(t, err, "while creating a Verity device")
devPath := fmt.Sprintf("/dev/%s", devName)
err = unix.Mknod(devPath, accessMode|unix.S_IFBLK, int(devNum))
require.NoError(t, err, "while creating a Verity device file at %s", devPath)
return devPath
}
// cleanupVerityDevice deactivates a Verity device previously mapped by
// createVerityDevice, and removes an associated device file.
func cleanupVerityDevice(t *testing.T, devName string) {
err := dm.RemoveDevice(devName)
require.NoError(t, err, "while removing a Verity device %s", devName)
devPath := fmt.Sprintf("/dev/%s", devName)
err = os.Remove(devPath)
require.NoError(t, err, "while removing a Verity device file at %s", devPath)
}
// testRead compares contents of a block device at devPath with
// expectedData. The length of data read is equal to the length
// of expectedData.
// It returns 'false', if either data could not be read or it does not
// match expectedData, and 'true' otherwise.
func testRead(t *testing.T, devPath string, expectedData []byte) bool {
// Open the Verity device.
verityDev, err := os.Open(devPath)
require.NoError(t, err, "while opening a Verity device at %s", devPath)
defer verityDev.Close()
// Attempt to read the test data. Abort on read errors.
readData := make([]byte, len(expectedData))
_, err = io.ReadFull(verityDev, readData)
if err != nil {
return false
}
// Return true, if read data matches expectedData.
if bytes.Compare(expectedData, readData) == 0 {
return true
}
return false
}
// TestMakeAndRead attempts to create a Verity device, then verifies the
// integrity of its contents.
func TestMakeAndRead(t *testing.T) {
if os.Getenv("IN_KTEST") != "true" {
t.Skip("Not in ktest")
}
// Allocate block devices backing the Verity target.
dataDevPath, err := getRamdisk()
require.NoError(t, err, "while allocating a data device ramdisk")
hashDevPath, err := getRamdisk()
require.NoError(t, err, "while allocating a hash device ramdisk")
// Fill the data device with test data and write a corresponding Verity
// hash tree to the hash device.
dmTarget, expectedDataBuf := fillVerityRamdisks(t, dataDevPath, hashDevPath)
// Create a Verity device using dmTarget. Use the test name as a device
// handle. verityPath will point to a resulting new block device.
verityPath := createVerityDevice(t, dmTarget, t.Name())
defer cleanupVerityDevice(t, t.Name())
// Use testRead to compare Verity target device contents with test data
// written to the data block device at dataDevPath by fillVerityRamdisks.
if !testRead(t, verityPath, expectedDataBuf.Bytes()) {
t.Error("data read from the verity device doesn't match the source")
}
}
// TestMalformed checks whenever Verity would prevent reading from a
// target whose hash device contents have been corrupted, as is expected.
func TestMalformed(t *testing.T) {
if os.Getenv("IN_KTEST") != "true" {
t.Skip("Not in ktest")
}
// Allocate block devices backing the Verity target.
dataDevPath, err := getRamdisk()
require.NoError(t, err, "while allocating a data device ramdisk")
hashDevPath, err := getRamdisk()
require.NoError(t, err, "while allocating a hash device ramdisk")
// Fill the data device with test data and write a corresponding Verity
// hash tree to the hash device.
dmTarget, expectedDataBuf := fillVerityRamdisks(t, dataDevPath, hashDevPath)
// Corrupt the first hash device block before mapping the Verity target.
hfd, err := os.OpenFile(hashDevPath, os.O_RDWR, accessMode)
require.NoError(t, err, "while opening a hash device at %s", hashDevPath)
// Place an odd byte at the 256th byte of the first hash block, skipping
// a 4096-byte Verity superblock.
hfd.Seek(4096+256, io.SeekStart)
hfd.Write([]byte{'F'})
hfd.Close()
// Create a Verity device using dmTarget. Use the test name as a device
// handle. verityPath will point to a resulting new block device.
verityPath := createVerityDevice(t, dmTarget, t.Name())
defer cleanupVerityDevice(t, t.Name())
// Use testRead to compare Verity target device contents with test data
// written to the data block device at dataDevPath by fillVerityRamdisks.
// This step is expected to fail after an incomplete read.
if testRead(t, verityPath, expectedDataBuf.Bytes()) {
t.Error("data matches the source when it shouldn't")
}
}