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/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)
+	}
 }