m/n/b/mkverity: produce a combined image

mkverity was updated to output a copy of the source image, with Verity
metadata appended to it, instead of a separate hash image. This is
needed by the upcoming verity rootfs implementation.

Change-Id: I2a311da6851dabf5a09d77551dc3e9d35bcc845f
Reviewed-on: https://review.monogon.dev/c/monogon/+/525
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/build/mkverity/BUILD.bazel b/metropolis/node/build/mkverity/BUILD.bazel
index 8d67da3..2fe6768 100644
--- a/metropolis/node/build/mkverity/BUILD.bazel
+++ b/metropolis/node/build/mkverity/BUILD.bazel
@@ -3,7 +3,10 @@
 go_binary(
     name = "mkverity",
     embed = [":go_default_library"],
-    visibility = ["//visibility:private"],
+    visibility = [
+        "//metropolis/installer/test/testos:__pkg__",
+        "//metropolis/node:__pkg__",
+    ],
 )
 
 go_library(
diff --git a/metropolis/node/build/mkverity/mkverity.go b/metropolis/node/build/mkverity/mkverity.go
index 272bc73..7300f49 100644
--- a/metropolis/node/build/mkverity/mkverity.go
+++ b/metropolis/node/build/mkverity/mkverity.go
@@ -24,6 +24,7 @@
 package main
 
 import (
+	"flag"
 	"fmt"
 	"io"
 	"log"
@@ -32,28 +33,55 @@
 	"source.monogon.dev/metropolis/pkg/verity"
 )
 
-// createHashImage creates a complete dm-verity hash image at
-// hashImagePath. Contents of the file at dataImagePath are accessed
-// read-only, hashed and written to the hash image in the process.
-// The verity superblock is written only if wsb is true.
-// It returns a string-convertible VerityMappingTable, or an error.
-func createHashImage(dataImagePath, hashImagePath string, wsb bool) (*verity.MappingTable, error) {
+// createImage creates a dm-verity target image by combining the input image
+// with Verity metadata. Contents of the data image are copied to the output
+// image. Then, the same contents are verity-encoded and appended to the
+// output image. The verity superblock is written only if wsb is true. It
+// returns either a dm-verity target table, or an error.
+func createImage(dataImagePath, outputImagePath string, wsb bool) (*verity.MappingTable, error) {
+	// Hardcode both the data block size and the hash block size as 4096 bytes.
+	bs := uint32(4096)
+
 	// Open the data image for reading.
 	dataImage, err := os.Open(dataImagePath)
 	if err != nil {
 		return nil, fmt.Errorf("while opening the data image: %w", err)
 	}
 	defer dataImage.Close()
+
+	// Check that the data image is well-formed.
+	ds, err := dataImage.Stat()
+	if err != nil {
+		return nil, fmt.Errorf("while stat-ing the data image: %w", err)
+	}
+	if !ds.Mode().IsRegular() {
+		return nil, fmt.Errorf("the data image must be a regular file")
+	}
+	if ds.Size()%int64(bs) != 0 {
+		return nil, fmt.Errorf("the data image must end on a %d-byte block boundary.", bs)
+	}
+
 	// Create an empty hash image file.
-	hashImage, err := os.OpenFile(hashImagePath, os.O_RDWR|os.O_CREATE, 0644)
+	outputImage, err := os.OpenFile(outputImagePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
 	if err != nil {
 		return nil, fmt.Errorf("while opening the hash image for writing: %w", err)
 	}
-	defer hashImage.Close()
+	defer outputImage.Close()
 
-	// Write hashImage contents. Start with initializing a verity encoder,
-	// seting hashImage as its output.
-	v, err := verity.NewEncoder(hashImage, wsb)
+	// Copy the input data into the output file, then rewind dataImage to be read
+	// again by the Verity encoder.
+	_, err = io.Copy(outputImage, dataImage)
+	if err != nil {
+		return nil, err
+	}
+	_, err = dataImage.Seek(0, os.SEEK_SET)
+	if err != nil {
+		return nil, err
+	}
+
+	// Write outputImage contents. Start with initializing a verity encoder,
+	// seting outputImage as its output.
+	v, err := verity.NewEncoder(outputImage, bs, bs, wsb)
 	if err != nil {
 		return nil, fmt.Errorf("while initializing a verity encoder: %w", err)
 	}
@@ -68,34 +96,57 @@
 		return nil, fmt.Errorf("while writing the hash image: %w", err)
 	}
 
-	// Return an encoder-generated verity mapping table, containing the salt
-	// and the root hash.
-	mt, err := v.MappingTable(dataImagePath, hashImagePath)
+	// Return an encoder-generated verity mapping table, containing the salt and
+	// the root hash. First, calculate the starting hash block by dividing the
+	// data image size by the encoder data block size.
+	hashStart := ds.Size() / int64(bs)
+	mt, err := v.MappingTable(dataImagePath, outputImagePath, hashStart)
 	if err != nil {
 		return nil, fmt.Errorf("while querying for the mapping table: %w", err)
 	}
 	return mt, nil
 }
 
-// usage prints program usage information.
-func usage(executable string) {
-	fmt.Println("Usage: ", executable, " <data image> <hash image>")
-}
+var (
+	input           = flag.String("input", "", "input disk image (required)")
+	output          = flag.String("output", "", "output disk image with Verity metadata appended (required)")
+	dataDeviceAlias = flag.String("data_alias", "", "data device alias used in the mapping table")
+	hashDeviceAlias = flag.String("hash_alias", "", "hash device alias used in the mapping table")
+	table           = flag.String("table", "", "a file the mapping table will be saved to; disables stdout")
+)
 
 func main() {
-	if len(os.Args) != 3 {
-		usage(os.Args[0])
-		os.Exit(2)
-	}
-	dataImagePath := os.Args[1]
-	hashImagePath := os.Args[2]
+	flag.Parse()
 
-	// Attempt to build a new Verity hash Image at hashImagePath, based on
-	// the data image at dataImagePath. Include the Verity superblock.
-	mt, err := createHashImage(dataImagePath, hashImagePath, true)
+	// Ensure that required parameters were provided before continuing.
+	if *input == "" {
+		log.Fatalf("-input must be set.")
+	}
+	if *output == "" {
+		log.Fatalf("-output must be set.")
+	}
+
+	// Build the image.
+	mt, err := createImage(*input, *output, false)
 	if err != nil {
 		log.Fatal(err)
 	}
-	// Print a Device Mapper compatible mapping table.
-	fmt.Println(mt)
+
+	// Patch the device names, if alternatives were provided.
+	if *dataDeviceAlias != "" {
+		mt.DataDevicePath = *dataDeviceAlias
+	}
+	if *hashDeviceAlias != "" {
+		mt.HashDevicePath = *hashDeviceAlias
+	}
+
+	// Print a DeviceMapper target table, or save it to a file, if the table
+	// parameter was specified.
+	if *table != "" {
+		if err := os.WriteFile(*table, []byte(mt.String()), 0644); err != nil {
+			log.Fatal(err)
+		}
+	} else {
+		fmt.Println(mt)
+	}
 }
diff --git a/metropolis/pkg/verity/encoder.go b/metropolis/pkg/verity/encoder.go
index 322acaa..e0124e2 100644
--- a/metropolis/pkg/verity/encoder.go
+++ b/metropolis/pkg/verity/encoder.go
@@ -305,14 +305,14 @@
 	// - hash algorithm used
 	// - cryptographic salt used
 	superblock *superblock
-	// dataDevicePath is the filesystem path of the data device used as part
+	// DataDevicePath is the filesystem path of the data device used as part
 	// of the Verity Device Mapper target.
-	dataDevicePath string
-	// hashDevicePath is the filesystem path of the hash device used as part
+	DataDevicePath string
+	// HashDevicePath is the filesystem path of the hash device used as part
 	// of the Verity Device Mapper target.
-	hashDevicePath string
-	// hashStart marks the starting block of the Verity hash tree.
-	hashStart int
+	HashDevicePath string
+	// HashStart marks the starting block of the Verity hash tree.
+	HashStart int64
 	// rootHash stores a cryptographic hash of the top hash tree block.
 	rootHash []byte
 }
@@ -322,12 +322,12 @@
 func (t *MappingTable) VerityParameterList() []string {
 	return []string{
 		"1",
-		t.dataDevicePath,
-		t.hashDevicePath,
+		t.DataDevicePath,
+		t.HashDevicePath,
 		strconv.FormatUint(uint64(t.superblock.dataBlockSize), 10),
 		strconv.FormatUint(uint64(t.superblock.hashBlockSize), 10),
 		strconv.FormatUint(uint64(t.superblock.dataBlocks), 10),
-		strconv.FormatInt(int64(t.hashStart), 10),
+		strconv.FormatInt(int64(t.HashStart), 10),
 		t.superblock.algorithmName(),
 		hex.EncodeToString(t.rootHash),
 		hex.EncodeToString(t.superblock.salt()),
@@ -445,11 +445,13 @@
 // encoder will write to the given io.Writer object.
 // A verity superblock will be written, preceding the hash tree, if
 // writeSb is true.
-func NewEncoder(out io.Writer, writeSb bool) (*encoder, error) {
+func NewEncoder(out io.Writer, dataBlockSize, hashBlockSize uint32, writeSb bool) (*encoder, error) {
 	sb, err := newSuperblock()
 	if err != nil {
 		return nil, fmt.Errorf("while creating a superblock: %w", err)
 	}
+	sb.dataBlockSize = dataBlockSize
+	sb.hashBlockSize = hashBlockSize
 
 	e := encoder{
 		out:     out,
@@ -514,17 +516,19 @@
 	}
 
 	// Reset the encoder.
-	e, err = NewEncoder(e.out, e.writeSb)
+	e, err = NewEncoder(e.out, e.sb.dataBlockSize, e.sb.hashBlockSize, e.writeSb)
 	if err != nil {
 		return fmt.Errorf("while resetting an encoder: %w", err)
 	}
 	return nil
 }
 
-// MappingTable returns a string-convertible Verity target mapping table
-// for use with Device Mapper, or an error. Close must be called on the
-// encoder before calling this function.
-func (e *encoder) MappingTable(dataDevicePath, hashDevicePath string) (*MappingTable, error) {
+// MappingTable returns a complete, string-convertible Verity target mapping
+// table for use with Device Mapper, or an error. Close must be called on the
+// encoder before calling this function. dataDevicePath, hashDevicePath, and
+// hashStart parameters are parts of the mapping table. See:
+// https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html
+func (e *encoder) MappingTable(dataDevicePath, hashDevicePath string, hashStart int64) (*MappingTable, error) {
 	if e.rootHash == nil {
 		if e.bottom.Len() != 0 {
 			return nil, fmt.Errorf("encoder wasn't closed.")
@@ -532,17 +536,15 @@
 		return nil, fmt.Errorf("encoder is empty.")
 	}
 
-	var hs int
 	if e.writeSb {
-		// Account for the superblock by setting the hash tree starting block
-		// to 1 instead of 0.
-		hs = 1
+		// Account for the superblock.
+		hashStart += 1
 	}
 	return &MappingTable{
 		superblock:     e.sb,
-		dataDevicePath: dataDevicePath,
-		hashDevicePath: hashDevicePath,
-		hashStart:      hs,
+		DataDevicePath: dataDevicePath,
+		HashDevicePath: hashDevicePath,
+		HashStart:      hashStart,
 		rootHash:       e.rootHash,
 	}, nil
 }
diff --git a/metropolis/pkg/verity/encoder_test.go b/metropolis/pkg/verity/encoder_test.go
index c3a39fb..b53e06e 100644
--- a/metropolis/pkg/verity/encoder_test.go
+++ b/metropolis/pkg/verity/encoder_test.go
@@ -104,8 +104,9 @@
 	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.
-	verityEnc, err := NewEncoder(hfd, true)
+	// 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
@@ -124,9 +125,10 @@
 	err = dfd.Close()
 	require.NoError(t, err, "while closing the data device descriptor")
 
-	// Generate the Verity mapping table based on the encoder state and
-	// device file paths, then return it along with the test data buffer.
-	mt, err := verityEnc.MappingTable(dataDevPath, hashDevPath)
+	// 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
 }