diff --git a/snap/integrity/dmverity/testdata/testdisk.verity b/snap/integrity/dmverity/testdata/testdisk.verity new file mode 100644 index 00000000000..53ac8243969 Binary files /dev/null and b/snap/integrity/dmverity/testdata/testdisk.verity differ diff --git a/snap/integrity/dmverity/veritysetup.go b/snap/integrity/dmverity/veritysetup.go index 8e677b4a290..94509860a5c 100644 --- a/snap/integrity/dmverity/veritysetup.go +++ b/snap/integrity/dmverity/veritysetup.go @@ -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 @@ -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 } diff --git a/snap/integrity/dmverity/veritysetup_test.go b/snap/integrity/dmverity/veritysetup_test.go index 428917dca16..d540edd2709 100644 --- a/snap/integrity/dmverity/veritysetup_test.go +++ b/snap/integrity/dmverity/veritysetup_test.go @@ -20,7 +20,10 @@ package dmverity_test import ( + "encoding/json" "fmt" + "os" + "path/filepath" "strings" "testing" @@ -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) { @@ -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"}) @@ -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.") } @@ -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") +} diff --git a/snap/integrity/integrity.go b/snap/integrity/integrity.go index 1ed6f135ae3..7732ebfbdd9 100644 --- a/snap/integrity/integrity.go +++ b/snap/integrity/integrity.go @@ -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, } } @@ -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 } diff --git a/snap/integrity/integrity_test.go b/snap/integrity/integrity_test.go index a91cd19406f..0aa621d575b 100644 --- a/snap/integrity/integrity_test.go +++ b/snap/integrity/integrity_test.go @@ -21,17 +21,12 @@ package integrity_test import ( "encoding/json" - "fmt" - "io" - "os" "strings" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/snap/integrity" - "github.com/snapcore/snapd/snap/integrity/dmverity" - "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" ) @@ -69,37 +64,6 @@ func (s *IntegrityTestSuite) TestAlign(c *C) { } } -func (s *IntegrityTestSuite) TestIntegrityHeaderMarshalJSON(c *C) { - dmVerityBlock := &dmverity.Info{} - integrityDataHeader := integrity.NewIntegrityDataHeader(dmVerityBlock, 4096) - - jsonHeader, err := json.Marshal(integrityDataHeader) - c.Assert(err, IsNil) - - c.Check(json.Valid(jsonHeader), Equals, true) - - expected := []byte(`{"type":"integrity","size":"8192","dm-verity":{"root-hash":""}}`) - c.Check(jsonHeader, DeepEquals, expected) -} - -func (s *IntegrityTestSuite) TestIntegrityHeaderUnmarshalJSON(c *C) { - var integrityDataHeader integrity.IntegrityDataHeader - integrityHeaderJSON := `{ - "type": "integrity", - "size": "4096", - "dm-verity": { - "root-hash": "00000000000000000000000000000000" - } - }` - - err := json.Unmarshal([]byte(integrityHeaderJSON), &integrityDataHeader) - c.Assert(err, IsNil) - - c.Check(integrityDataHeader.Type, Equals, "integrity") - c.Check(integrityDataHeader.Size, Equals, uint64(4096)) - c.Check(integrityDataHeader.DmVerity.RootHash, Equals, "00000000000000000000000000000000") -} - func (s *IntegrityTestSuite) TestIntegrityHeaderEncode(c *C) { var integrityDataHeader integrity.IntegrityDataHeader magic := integrity.Magic @@ -124,31 +88,6 @@ func (s *IntegrityTestSuite) TestIntegrityHeaderEncodeInvalidSize(c *C) { c.Assert(err, ErrorMatches, "internal error: invalid integrity data header: wrong size") } -func (s *IntegrityTestSuite) TestIntegrityHeaderDecode(c *C) { - var integrityDataHeader integrity.IntegrityDataHeader - magic := integrity.Magic - - integrityHeaderJSON := `{ - "type": "integrity", - "size": "4096", - "dm-verity": { - "root-hash": "00000000000000000000000000000000" - } - }` - header := append(magic, integrityHeaderJSON...) - header = append(header, 0) - - headerBlock := make([]byte, 4096) - copy(headerBlock, header) - - err := integrityDataHeader.Decode(headerBlock) - c.Assert(err, IsNil) - - c.Check(integrityDataHeader.Type, Equals, "integrity") - c.Check(integrityDataHeader.Size, Equals, uint64(4096)) - c.Check(integrityDataHeader.DmVerity.RootHash, Equals, "00000000000000000000000000000000") -} - func (s *IntegrityTestSuite) TestIntegrityHeaderDecodeInvalidMagic(c *C) { var integrityDataHeader integrity.IntegrityDataHeader magic := []byte("invalid") @@ -212,69 +151,3 @@ func (s *IntegrityTestSuite) TestIntegrityHeaderDecodeInvalidTermination(c *C) { err := integrityDataHeader.Decode(headerBlock) c.Check(err, ErrorMatches, "invalid integrity data header: no null byte found at end of input") } - -func (s *IntegrityTestSuite) TestGenerateAndAppendSuccess(c *C) { - blockSize := uint64(integrity.BlockSize) - - snapPath, _ := snaptest.MakeTestSnapInfoWithFiles(c, "name: foo\nversion: 1.0", nil, nil) - - // 8192 is the hash size that is created when running 'veritysetup format' - // on a minimally sized snap. there is not an easy way to calculate this - // value dynamically. - const verityHashSize = 8192 - - // mock the verity-setup command, what it does is make a copy of the snap - // and then returns pre-calculated output - vscmd := testutil.MockCommand(c, "veritysetup", fmt.Sprintf(` -case "$1" in - --version) - echo "veritysetup 2.2.6" - exit 0 - ;; - format) - truncate -s %[1]d %[2]s.verity - echo "VERITY header information for %[2]s.verity" - echo "UUID: f8b4f201-fe4e-41a2-9f1d-4908d3c76632" - echo "Hash type: 1" - echo "Data blocks: 4" - echo "Data block size: 4096" - echo "Hash block size: 4096" - echo "Hash algorithm: sha256" - echo "Salt: f1a7f87b88692b388f47dbda4a3bdf790f5adc3104b325f8772aee593488bf15" - echo "Root hash: e2926364a8b1242d92fb1b56081e1ddb86eba35411961252a103a1c083c2be6d" - ;; -esac -`, verityHashSize, snapPath)) - defer vscmd.Restore() - - snapFileInfo, err := os.Stat(snapPath) - c.Assert(err, IsNil) - orig_size := snapFileInfo.Size() - - err = integrity.GenerateAndAppend(snapPath) - c.Assert(err, IsNil) - - snapFile, err := os.Open(snapPath) - c.Assert(err, IsNil) - defer snapFile.Close() - - // check integrity header - _, err = snapFile.Seek(orig_size, io.SeekStart) - c.Assert(err, IsNil) - - header := make([]byte, blockSize-1) - n, err := snapFile.Read(header) - c.Assert(err, IsNil) - c.Assert(n, Equals, int(blockSize)-1) - - var integrityDataHeader integrity.IntegrityDataHeader - err = integrityDataHeader.Decode(header) - c.Check(err, IsNil) - c.Check(integrityDataHeader.Type, Equals, "integrity") - c.Check(integrityDataHeader.Size, Equals, uint64(verityHashSize+integrity.HeaderSize)) - c.Check(integrityDataHeader.DmVerity.RootHash, HasLen, 64) - - 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"}) -}