Skip to content

Commit

Permalink
s/integrity/dmverity: rework (canonical#14871)
Browse files Browse the repository at this point in the history
* s/i/dmverity: refactor getRootHashFromOutput

* s/i/dmverity/veritysetup: add ability to pass options to veritysetup format

also dmverity.Format will simply return the root hash instead of the old
Info struct since the new design doesn't have a need for a separate header.

* s/i/dmverity/veritysetup: add superblock parsing functionality

adding helpers for retrieving and parsing a dm-verity superblock from a
dm-verity hash device/file. This will be first consumed by the snap
integrity API which will need to detect the salt that was used for the
dm-verity data generation. Moreover callers to dmverity.Format will
need to have a way to retrieve the parameters used by veritysetup if
no parameters are passed (and veritysetup chooses default values).

* s/i/dmverity: add default dmverity format version

* s/i/dmverity: add default value for superblock version too

* s/i/dmverity: name Format()'s return arguments for clarity

* s/i/dmverity: rename VeritySuperBlock to VeritySuperblock

* s/i/dmverity: change VeritySuperblock fields to camelCase

* s/i/dmverity: add clarifying comments for test data generation

* s/i/dmverity: tests: fix failing tests

* s/i/dmverity: remove json tags from VeritySuperblock

* s/i/dmverity: rename field of getFieldFromOutput to val

* s/i/dmverity: use osutil.CombineStdOutErr for debug message

* s/i/dmverity: create testdisk file programmatically

* s/i/dmverity: rename ReadSuperBlock to ReadVeritySuperblock

* s/integrity: stop using old APIs and tests for the testsuite to run

* s/i/dmverity: rename ReadVeritySuperblock to ReadSuperblock

* s/i/dmverity: use errors.New instead of fmt.Errorf for static error messages

* s/i/dmverity: fix passing arguments to veritysetup
  • Loading branch information
sespiros authored Jan 24, 2025
1 parent 78f68f0 commit eb821e9
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 175 deletions.
Binary file not shown.
172 changes: 137 additions & 35 deletions snap/integrity/dmverity/veritysetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,62 +22,70 @@ package dmverity
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"unsafe"

"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
)

// Info represents the dm-verity related data that:
// 1. are not included in the superblock which is generated by default when running
// veritysetup.
// 2. need their authenticity verified prior to loading the integrity data into the
// kernel.
//
// For now, since we are keeping the superblock as it is, this only includes the root hash.
type Info struct {
RootHash string `json:"root-hash"`
}
const (
// DefaultVerityFormat corresponds to veritysetup's default option for the --format argument which
// currently is 1. This corresponds to the hash_type field of a dm-verity superblock.
DefaultVerityFormat = 1
// DefaultSuperblockVersion corresponds to the superblock version. Version 1 is the only one
// currently supported by veritysetup. This corresponds to the version field of a dm-verity superblock.
DefaultSuperblockVersion = 1
)

func getVal(line string) (string, error) {
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("internal error: unexpected veritysetup output format")
return "", errors.New("internal error: unexpected veritysetup output format")
}
return strings.TrimSpace(parts[1]), nil
}

func getRootHashFromOutput(output []byte) (rootHash string, err error) {
func getFieldFromOutput(output []byte, key string) (val string, err error) {
scanner := bufio.NewScanner(bytes.NewBuffer(output))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "Root hash") {
rootHash, err = getVal(line)
if strings.HasPrefix(line, key) {
val, err = getVal(line)
if err != nil {
return "", err
}
}
if strings.HasPrefix(line, "Hash algorithm") {
hashAlgo, err := getVal(line)
if err != nil {
return "", err
}
if hashAlgo != "sha256" {
return "", fmt.Errorf("internal error: unexpected hash algorithm")
}
}
}

if err = scanner.Err(); err != nil {
return "", err
}

return val, nil
}

func getRootHashFromOutput(output []byte) (rootHash string, err error) {
rootHash, err = getFieldFromOutput(output, "Root hash")
if err != nil {
return "", err
}
if len(rootHash) != 64 {
return "", fmt.Errorf("internal error: unexpected root hash length")
return "", errors.New("internal error: unexpected root hash length")
}

hashAlg, err := getFieldFromOutput(output, "Hash algorithm")
if err != nil {
return "", err
}
if hashAlg != "sha256" {
return "", errors.New("internal error: unexpected hash algorithm")
}

return rootHash, nil
Expand Down Expand Up @@ -122,33 +130,127 @@ func shouldApplyNewFileWorkaroundForOlderThan204() (bool, error) {
return true, nil
}

// Format runs "veritysetup format" and returns an Info struct which includes the
// root hash. "veritysetup format" calculates the hash verification data for
// dataDevice and stores them in hashDevice. The root hash is retrieved from
// the command's stdout.
func Format(dataDevice string, hashDevice string) (*Info, error) {
// DmVerityParams contains the options to veritysetup format.
type DmVerityParams struct {
Format uint8 `json:"format"`
Hash string `json:"hash"`
DataBlocks uint64 `json:"data-blocks"`
DataBlockSize uint64 `json:"data-block-size"`
HashBlockSize uint64 `json:"hash-block-size"`
Salt string `json:"salt"`
}

// appendArguments returns the options to veritysetup format as command line arguments.
func (p DmVerityParams) appendArguments(args []string) []string {

args = append(args, fmt.Sprintf("--format=%d", p.Format))
args = append(args, fmt.Sprintf("--hash=%s", p.Hash))
args = append(args, fmt.Sprintf("--data-blocks=%d", p.DataBlocks))
args = append(args, fmt.Sprintf("--data-block-size=%d", p.DataBlockSize))
args = append(args, fmt.Sprintf("--hash-block-size=%d", p.HashBlockSize))

if len(p.Salt) != 0 {
args = append(args, fmt.Sprintf("--salt=%s", p.Salt))
}

return args
}

// Format runs "veritysetup format" with the passed parameters and returns the dm-verity root hash.
//
// "veritysetup format" calculates the hash verification data for dataDevice and stores them in
// hashDevice including the dm-verity superblock. The root hash is retrieved from the command's stdout.
func Format(dataDevice string, hashDevice string, opts *DmVerityParams) (rootHash string, err error) {
// In older versions of cryptsetup there is a bug when cryptsetup writes
// its superblock header, and there isn't already preallocated space.
// Fixed in commit dc852a100f8e640dfdf4f6aeb86e129100653673 which is version 2.0.4
deploy, err := shouldApplyNewFileWorkaroundForOlderThan204()
if err != nil {
return nil, err
return "", err
} else if deploy {
space := make([]byte, 4096)
os.WriteFile(hashDevice, space, 0644)
}

output, stderr, err := osutil.RunSplitOutput("veritysetup", "format", dataDevice, hashDevice)
args := []string{
"format",
dataDevice,
hashDevice,
}

if opts != nil {
args = opts.appendArguments(args)
}

output, stderr, err := osutil.RunSplitOutput("veritysetup", args...)
if err != nil {
return nil, osutil.OutputErrCombine(output, stderr, err)
return "", osutil.OutputErrCombine(output, stderr, err)
}

logger.Debugf("cmd: 'veritysetup format %s %s':\n%s", dataDevice, hashDevice, string(output))
logger.Debugf("cmd: 'veritysetup format %s %s %s':\n%s", dataDevice, hashDevice, args, osutil.CombineStdOutErr(output, stderr))

rootHash, err := getRootHashFromOutput(output)
rootHash, err = getRootHashFromOutput(output)
if err != nil {
return "", err
}

return rootHash, nil
}

// VeritySuperblock represents the dm-verity superblock structure.
//
// It mirrors cryptsetup's verity_sb structure from
// https://gitlab.com/cryptsetup/cryptsetup/-/blob/main/lib/verity/verity.c?ref_type=heads#L25
type VeritySuperblock struct {
Signature [8]uint8 /* "verity\0\0" */
Version uint32 /* superblock version */
HashType uint32 /* 0 - Chrome OS, 1 - normal */
Uuid [16]uint8 /* UUID of hash device */
Algorithm [32]uint8 /* hash algorithm name */
DataBlockSize uint32 /* data block in bytes */
HashBlockSize uint32 /* hash block in bytes */
DataBlocks uint64 /* number of data blocks */
SaltSize uint16 /* salt size */
Pad1 [6]uint8
Salt [256]uint8 /* salt */
Pad2 [168]uint8
}

func (sb *VeritySuperblock) Size() int {
size := int(unsafe.Sizeof(*sb))
return size
}

// Validate will perform consistency checks over an extracted superblock to determine whether it's a valid
// superblock or not.
func (sb *VeritySuperblock) Validate() error {
if sb.Version != DefaultSuperblockVersion {
return errors.New("invalid dm-verity superblock version")
}

if sb.HashType != DefaultVerityFormat {
return errors.New("invalid dm-verity hash type")
}

return nil
}

// ReadSuperblock reads the dm-verity superblock from a dm-verity hash file.
func ReadSuperblock(filename string) (*VeritySuperblock, error) {
hashFile, err := os.Open(filename)
if err != nil {
return nil, err
}
defer hashFile.Close()
var sb VeritySuperblock
verity_sb := make([]byte, sb.Size())
if _, err := hashFile.Read(verity_sb); err != nil {
return nil, err
}
err = binary.Read(bytes.NewReader(verity_sb), binary.LittleEndian, &sb)
if err != nil {
return nil, err
}

return &Info{RootHash: rootHash}, nil
return &sb, nil
}
65 changes: 58 additions & 7 deletions snap/integrity/dmverity/veritysetup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
package dmverity_test

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -103,24 +106,28 @@ case "$1" in
format)
cp %[1]s %[1]s.verity
echo VERITY header information for %[1]s.verity
echo "UUID: 97d80536-aad9-4f25-a528-5319c038c0c4"
echo "UUID: 93740d5e-9039-4a07-9219-bd355882b64b"
echo "Hash type: 1"
echo "Data blocks: 1"
echo "Data blocks: 2048"
echo "Data block size: 4096"
echo "Hash blocks: 17"
echo "Hash block size: 4096"
echo "Hash algorithm: sha256"
echo "Salt: c0234a906cfde0d5ffcba25038c240a98199cbc1d8fbd388a41e8faa02239c08"
echo "Root hash: e48cfc4df6df0f323bcf67f17b659a5074bec3afffe28f0b3b4db981d78d2e3e"
echo "Salt: 46aee3affbd0455623e907bb7fc622999bac4c86fa263808ac15240b16286458"
echo "Root hash: 9257053cde92608d275cd912c031c40dd9d8820e4645f0774ec2d4403f19f840"
echo "Hash device size: 73728 [bytes]"
;;
esac
`, snapPath))
defer vscmd.Restore()

_, err := dmverity.Format(snapPath, snapPath+".verity")
rootHash, err := dmverity.Format(snapPath, snapPath+".verity", nil)
c.Assert(err, IsNil)
c.Assert(vscmd.Calls(), HasLen, 2)
c.Check(vscmd.Calls()[0], DeepEquals, []string{"veritysetup", "--version"})
c.Check(vscmd.Calls()[1], DeepEquals, []string{"veritysetup", "format", snapPath, snapPath + ".verity"})

c.Check(rootHash, Equals, "9257053cde92608d275cd912c031c40dd9d8820e4645f0774ec2d4403f19f840")
}

func (s *VerityTestSuite) TestFormatSuccessWithWorkaround(c *C) {
Expand Down Expand Up @@ -152,7 +159,7 @@ esac
`, snapPath))
defer vscmd.Restore()

_, err := dmverity.Format(snapPath, snapPath+".verity")
_, err := dmverity.Format(snapPath, snapPath+".verity", nil)
c.Assert(err, IsNil)
c.Assert(vscmd.Calls(), HasLen, 2)
c.Check(vscmd.Calls()[0], DeepEquals, []string{"veritysetup", "--version"})
Expand All @@ -175,7 +182,8 @@ esac
`)
defer vscmd.Restore()

_, err := dmverity.Format(snapPath, "")
rootHash, err := dmverity.Format(snapPath, "", nil)
c.Assert(rootHash, Equals, "")
c.Check(err, ErrorMatches, "Cannot create hash image for writing.")
}

Expand Down Expand Up @@ -205,3 +213,46 @@ func (s *VerityTestSuite) TestVerityVersionDetect(c *C) {
c.Check(deploy, Equals, t.deploy, Commentf("test failed for version: %s", t.ver))
}
}

func (s *VerityTestSuite) TestReadSuperblockSuccess(c *C) {
// testdisk.verity is generated by:
// - dd if=/dev/zero of=testdisk bs=8M count=1
// - veritysetup format testdisk testdisk.verity
sb, err := dmverity.ReadSuperblock("testdata/testdisk.verity")
c.Check(err, IsNil)

sbJson, _ := json.Marshal(sb)
expectedSb := "{\"Signature\":[118,101,114,105,116,121,0,0]," +
"\"Version\":1," +
"\"HashType\":1," +
"\"Uuid\":[147,116,13,94,144,57,74,7,146,25,189,53,88,130,182,75]," +
"\"Algorithm\":[115,104,97,50,53,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]," +
"\"DataBlockSize\":4096," +
"\"HashBlockSize\":4096," +
"\"DataBlocks\":2048," +
"\"SaltSize\":32," +
"\"Pad1\":[0,0,0,0,0,0]," +
"\"Salt\":[70,174,227,175,251,208,69,86,35,233,7,187,127,198,34,153,155,172,76,134,250,38,56,8,172,21,36,11,22,40,100,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]," +
"\"Pad2\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}"
c.Check(string(sbJson), Equals, expectedSb)
}

func (s *VerityTestSuite) TestReadEmptySuperBlockError(c *C) {
// Attempt to read an empty disk

// create empty file
testDiskPath := filepath.Join(c.MkDir(), "testdisk")
f, err := os.Create(testDiskPath)
c.Assert(err, IsNil)
defer f.Close()

err = os.Truncate(testDiskPath, 8*1024*1024)
c.Assert(err, IsNil)

// attempt to read superblock from it
sb, err := dmverity.ReadSuperblock(testDiskPath)
c.Assert(err, IsNil)

err = sb.Validate()
c.Check(err, ErrorMatches, "invalid dm-verity superblock version")
}
12 changes: 6 additions & 6 deletions snap/integrity/integrity.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ func align(size uint64) uint64 {
// IntegrityDataHeader gets appended first at the end of a squashfs packed snap
// before the dm-verity data. Size field includes the header size
type IntegrityDataHeader struct {
Type string `json:"type"`
Size uint64 `json:"size,string"`
DmVerity dmverity.Info `json:"dm-verity"`
Type string `json:"type"`
Size uint64 `json:"size,string"`
RootHash string `json:"dm-verity"`
}

// newIntegrityDataHeader constructs a new IntegrityDataHeader struct from a dmverity.Info struct.
func newIntegrityDataHeader(dmVerityBlock *dmverity.Info, integrityDataSize uint64) *IntegrityDataHeader {
func newIntegrityDataHeader(rootHash string, integrityDataSize uint64) *IntegrityDataHeader {
return &IntegrityDataHeader{
Type: "integrity",
Size: HeaderSize + integrityDataSize,
DmVerity: *dmVerityBlock,
RootHash: rootHash,
}
}

Expand Down Expand Up @@ -115,7 +115,7 @@ func (integrityDataHeader *IntegrityDataHeader) Decode(input []byte) error {
func GenerateAndAppend(snapPath string) (err error) {
// Generate verity metadata
hashFileName := snapPath + ".verity"
dmVerityBlock, err := dmverity.Format(snapPath, hashFileName)
dmVerityBlock, err := dmverity.Format(snapPath, hashFileName, nil)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit eb821e9

Please sign in to comment.