Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

snap/integrity: new API #14872

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added snap/integrity/dmverity/testdata/testdisk
Binary file not shown.
Binary file not shown.
158 changes: 127 additions & 31 deletions snap/integrity/dmverity/veritysetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,21 @@ package dmverity
import (
"bufio"
"bytes"
"encoding/binary"
"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 = 1
)

func getVal(line string) (string, error) {
parts := strings.SplitN(line, ":", 2)
Expand All @@ -51,35 +46,42 @@ func getVal(line string) (string, error) {
return strings.TrimSpace(parts[1]), nil
}

func getRootHashFromOutput(output []byte) (rootHash string, err error) {
func getFieldFromOutput(output []byte, key string) (rootHash string, err error) {
scanner := bufio.NewScanner(bytes.NewBuffer(output))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "Root hash") {
if strings.HasPrefix(line, key) {
rootHash, 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 rootHash, 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")
}

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

return rootHash, nil
}

Expand Down Expand Up @@ -122,33 +124,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) (string, 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, string(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 `json:"-"` /* "verity\0\0" */
Version uint32 `json:"version"` /* superblock version */
Hash_type uint32 `json:"hash_type"` /* 0 - Chrome OS, 1 - normal */
Uuid [16]uint8 `json:"uuid"` /* UUID of hash device */
Algorithm [32]uint8 `json:"algorithm"` /* hash algorithm name */
Data_block_size uint32 `json:"data_block_size"` /* data block in bytes */
Hash_block_size uint32 `json:"hash_block_size"` /* hash block in bytes */
Data_blocks uint64 `json:"data_blocks"` /* number of data blocks */
Salt_size uint16 `json:"salt_size"` /* salt size */
Pad1 [6]uint8 `json:"-"`
Salt [256]uint8 `json:"salt"` /* salt */
Pad2 [168]uint8 `json:"-"`
}

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 != DefaultVerityFormat {
return fmt.Errorf("invalid dm-verity version")
}

if sb.Hash_type != DefaultVerityFormat {
return fmt.Errorf("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
}
35 changes: 28 additions & 7 deletions snap/integrity/dmverity/veritysetup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package dmverity_test

import (
"encoding/json"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -103,24 +104,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 +157,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 +180,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 +211,18 @@ 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) {
sb, err := dmverity.ReadSuperBlock("testdata/testdisk.verity")
c.Check(err, IsNil)

sbJson, _ := json.Marshal(sb)
expectedSb := `{"version":1,"hash_type":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],"data_block_size":4096,"hash_block_size":4096,"data_blocks":2048,"salt_size":32,"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]}`
c.Check(string(sbJson), Equals, expectedSb)
}

func (s *VerityTestSuite) TestReadSuperBlockError(c *C) {
// Attempt to read an empty disk
_, err := dmverity.ReadSuperBlock("testdata/testdisk")
c.Check(err, ErrorMatches, "invalid dm-verity superblock header")
}
27 changes: 21 additions & 6 deletions snap/integrity/export_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2023 Canonical Ltd
* Copyright (C) 2023-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand All @@ -19,9 +19,24 @@

package integrity

var (
Align = align
BlockSize = blockSize
Magic = magic
NewIntegrityDataHeader = newIntegrityDataHeader
import (
"github.com/snapcore/snapd/snap/integrity/dmverity"
)

func MockVeritysetupFormat(fn func(string, string, *dmverity.DmVerityParams) (string, error)) (restore func()) {
origVeritysetupFormat := veritysetupFormat
veritysetupFormat = fn
return func() {
veritysetupFormat = origVeritysetupFormat
}
}

func MockReadSuperBlockFromFile(sb *dmverity.VeritySuperBlock) (restore func()) {
origReadSuperBlockFromFile := readDmVeritySuperBlock
readDmVeritySuperBlock = func(filename string) (*dmverity.VeritySuperBlock, error) {
return sb, nil
}
return func() {
readDmVeritySuperBlock = origReadSuperBlockFromFile
}
}
Loading
Loading