Skip to content

Commit

Permalink
asserts: snap integrity assertion (#14870)
Browse files Browse the repository at this point in the history
* asserts/header_checks.go: add checkDigestWhatDec

checkDigestWhatDec also accepts a function used for the decoding.

* asserts/snap_asserts*: change format of snap-revision's snap integrity stanza

With the new design, dm-verity data can be generated on installation
using the parameters from the snap-revision assertion which also
contains the dm-verity root hash.

Multiple variants of integrity data are possible for a single
snap-revision to allow for future per-device optimization and
configuration.

* asserts/snap_asserts*: add support for separate block sizes for data/hash dm-verity devices

* asserts: change function argument's name in checkDigestWhatDec helper

* asserts: rename checkDigest{WhatDec,DecWhat} and remove checkDigestWhat

* asserts: modify checkDigestDecWhat to return the encoded string

* asserts: change argument order in check checkDigestDecWhat

* asserts: rename snap integrity's field hash-alg to hash-algorithm

* asserts: minor refactor in integrity related checks and more consistent error messages

* asserts: reduced list of supported algorithms to only sha256

* asserts: remove unused algorithms from toHash helper and address minor comment
  • Loading branch information
sespiros authored Jan 23, 2025
1 parent 3acaf01 commit e7a49b9
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 64 deletions.
16 changes: 8 additions & 8 deletions asserts/header_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,24 @@ func checkUintWhat(headers map[string]interface{}, name string, bitSize int, wha
return value, nil
}

func checkDigest(headers map[string]interface{}, name string, h crypto.Hash) ([]byte, error) {
return checkDigestWhat(headers, name, h, "header")
func checkDigest(headers map[string]interface{}, name string, h crypto.Hash) (string, error) {
return checkDigestDecWhat(headers, name, h, base64.RawURLEncoding.DecodeString, "header")
}

func checkDigestWhat(headers map[string]interface{}, name string, h crypto.Hash, what string) ([]byte, error) {
func checkDigestDecWhat(headers map[string]interface{}, name string, h crypto.Hash, decode func(string) ([]byte, error), what string) (string, error) {
digestStr, err := checkNotEmptyStringWhat(headers, name, what)
if err != nil {
return nil, err
return "", err
}
b, err := base64.RawURLEncoding.DecodeString(digestStr)
b, err := decode(digestStr)
if err != nil {
return nil, fmt.Errorf("%q %s cannot be decoded: %v", name, what, err)
return "", fmt.Errorf("%q %s cannot be decoded: %v", name, what, err)
}
if len(b) != h.Size() {
return nil, fmt.Errorf("%q %s does not have the expected bit length: %d", name, what, len(b)*8)
return "", fmt.Errorf("%q %s does not have the expected bit length: %d", name, what, len(b)*8)
}

return b, nil
return digestStr, nil
}

// checkStringListInMap returns the `name` entry in the `m` map as a (possibly nil) `[]string`
Expand Down
185 changes: 150 additions & 35 deletions asserts/snap_asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ package asserts
import (
"bytes"
"crypto"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"

// expected for digests
Expand Down Expand Up @@ -464,11 +466,54 @@ func (ra *RevisionAuthority) CheckResourceRevision(resrev *SnapResourceRevision,
return ra.checkProvenanceAndRevision(resrev, "resource", resrev.ResourceRevision(), model, store)
}

// SnapIntegrity holds information about integrity data included in a revision
// for a given snap.
type SnapIntegrity struct {
SHA3_384 string
Size uint64
var validSnapIntegrityTypes = []string{"dm-verity"}

var validVersionsForIntegrityType = map[string][]int{
// version 1 corresponds to dm-verity format 1
"dm-verity": {1},
}

var validHashAlgorithmsForIntegrityType = map[string][]string{
// kernel supported algorithms:
// https://gitlab.com/cryptsetup/cryptsetup/-/blob/main/lib/crypto_backend/crypto_kernel.c?ref_type=heads#L35
// Go crypto's supported algorithms:
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/crypto/crypto.go;l=68
"dm-verity": {
"sha256",
},
}

func contains[V int | string](l []V, i V) bool {
for _, v := range l {
if v == i {
return true
}
}

return false
}

func toHash(s string) crypto.Hash {
switch s {
case "sha256":
return crypto.SHA256
default:
return 0
}
}

// SnapIntegrityData holds information about integrity data of a specific type included in a snap's revision.
//
// A single snap revision can have multiple variants of integrity data which are represented as an array in the
// snap revision assertion.
type SnapIntegrityData struct {
Type string
Version uint
HashAlg string
DataBlockSize uint
HashBlockSize uint
Digest string
Salt string
}

// SnapFileSHA3_384 computes the SHA3-384 digest of the given snap file.
Expand Down Expand Up @@ -561,7 +606,7 @@ type SnapRevision struct {
snapRevision int
timestamp time.Time

snapIntegrity *SnapIntegrity
snapIntegrityData []SnapIntegrityData
}

// SnapSHA3_384 returns the SHA3-384 digest of the snap.
Expand Down Expand Up @@ -601,9 +646,9 @@ func (snaprev *SnapRevision) Timestamp() time.Time {
return snaprev.timestamp
}

// SnapIntegrity returns the snap integrity data associated with the snap revision if any.
func (snaprev *SnapRevision) SnapIntegrity() *SnapIntegrity {
return snaprev.snapIntegrity
// SnapIntegrityData returns the snap integrity data associated with the snap revision if any.
func (snaprev *SnapRevision) SnapIntegrityData() []SnapIntegrityData {
return snaprev.snapIntegrityData
}

// Implement further consistency checks.
Expand Down Expand Up @@ -683,6 +728,96 @@ func checkOptionalSnapRevisionWhat(headers map[string]interface{}, name, what st
return checkSnapRevisionWhat(headers, name, what)
}

func checkSnapIntegrity(headers map[string]interface{}) ([]SnapIntegrityData, error) {
value, ok := headers["integrity"]
if !ok {
// integrity stanzas are optional
return nil, nil
}

integrityList, ok := value.([]interface{})
if !ok {
return nil, fmt.Errorf(`"integrity" header must contain a list of integrity data`)
}
if len(integrityList) == 0 {
return nil, nil
}

var snapIntegrityDataList []SnapIntegrityData

for i, il := range integrityList {
id, ok := il.(map[string]interface{})
if !ok {
return nil, fmt.Errorf(`"integrity" header must contain a list of integrity data`)
}

what := fmt.Sprintf("of integrity data [%d]", i)
typ, err := checkExistsStringWhat(id, "type", what)
if err != nil {
return nil, err
}

if !contains(validSnapIntegrityTypes, typ) {
return nil, fmt.Errorf("\"type\" of integrity data [%d] must be one of (%s)", i, strings.Join(validSnapIntegrityTypes, "|"))
}

what = fmt.Sprintf("of integrity data [%d] of type %q", i, typ)
version, err := checkUintWhat(id, "version", 64, what)
if err != nil {
return nil, err
}

if !contains(validVersionsForIntegrityType[typ], int(version)) {
return nil, fmt.Errorf(`version of integrity data [%d] of type %q must be one of %v`, i, typ, validVersionsForIntegrityType[typ])
}

alg, err := checkExistsStringWhat(id, "hash-algorithm", what)
if err != nil {
return nil, err
}

if !contains(validHashAlgorithmsForIntegrityType[typ], alg) {
return nil, fmt.Errorf(`hash algorithm of integrity data [%d] of type %q must be one of %v`, i, typ, validHashAlgorithmsForIntegrityType[typ])
}

what = fmt.Sprintf("of integrity data [%d] of type %q (%s)", i, typ, alg)
dataBlockSize, err := checkUintWhat(id, "data-block-size", 64, what)
if err != nil {
return nil, err
}

hashBlockSize, err := checkUintWhat(id, "hash-block-size", 64, what)
if err != nil {
return nil, err
}

h := toHash(alg)
encDigest, err := checkDigestDecWhat(id, "digest", h, hex.DecodeString, what)
if err != nil {
return nil, err
}

encSalt, err := checkDigestDecWhat(id, "salt", h, hex.DecodeString, what)
if err != nil {
return nil, err
}

snapIntegrityData := SnapIntegrityData{
Type: typ,
Version: uint(version),
HashAlg: alg,
DataBlockSize: uint(dataBlockSize),
HashBlockSize: uint(hashBlockSize),
Digest: encDigest,
Salt: encSalt,
}

snapIntegrityDataList = append(snapIntegrityDataList, snapIntegrityData)
}

return snapIntegrityDataList, nil
}

func assembleSnapRevision(assert assertionBase) (Assertion, error) {
_, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384)
if err != nil {
Expand Down Expand Up @@ -719,37 +854,17 @@ func assembleSnapRevision(assert assertionBase) (Assertion, error) {
return nil, err
}

integrityMap, err := checkMap(assert.headers, "integrity")
snapIntegrityData, err := checkSnapIntegrity(assert.headers)
if err != nil {
return nil, err
}

var snapIntegrity *SnapIntegrity

if integrityMap != nil {
// TODO: this will change again to support format agility
_, err := checkDigestWhat(integrityMap, "sha3-384", crypto.SHA3_384, "of integrity header")
if err != nil {
return nil, err
}

size, err := checkUintWhat(integrityMap, "size", 64, "of integrity header")
if err != nil {
return nil, err
}

snapIntegrity = &SnapIntegrity{
SHA3_384: integrityMap["sha3-384"].(string),
Size: size,
}
}

return &SnapRevision{
assertionBase: assert,
snapSize: snapSize,
snapRevision: snapRevision,
timestamp: timestamp,
snapIntegrity: snapIntegrity,
assertionBase: assert,
snapSize: snapSize,
snapRevision: snapRevision,
timestamp: timestamp,
snapIntegrityData: snapIntegrityData,
}, nil
}

Expand Down
84 changes: 63 additions & 21 deletions asserts/snap_asserts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"encoding/base64"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -853,6 +854,7 @@ func (sbs *snapBuildSuite) SetUpSuite(c *C) {

const (
blobSHA3_384 = "QlqR0uAWEAWF5Nwnzj5kqmmwFslYPu1IL16MKtLKhwhv0kpBv5wKZ_axf_nf_2cL"
hexSHA256 = "e2926364a8b1242d92fb1b56081e1ddb86eba35411961252a103a1c083c2be6d"
)

func (sbs *snapBuildSuite) TestDecodeOK(c *C) {
Expand Down Expand Up @@ -1023,15 +1025,23 @@ func (srs *snapRevSuite) makeValidEncoded() string {
}

func (srs *snapRevSuite) makeValidEncodedWithIntegrity() string {
integrityData := "integrity:\n" +
" -\n" +
" type: dm-verity\n" +
" digest: " + hexSHA256 + "\n" +
" version: 1\n" +
" hash-algorithm: sha256\n" +
" data-block-size: 4096\n" +
" hash-block-size: 4096\n" +
" salt: " + hexSHA256 + "\n"

return "type: snap-revision\n" +
"authority-id: store-id1\n" +
"snap-sha3-384: " + blobSHA3_384 + "\n" +
"snap-id: snap-id-1\n" +
"snap-size: 123\n" +
"snap-revision: 1\n" +
"integrity:\n" +
" sha3-384: " + blobSHA3_384 + "\n" +
" size: 128\n" +
integrityData +
"developer-id: dev-id1\n" +
"revision: 1\n" +
srs.tsLine +
Expand Down Expand Up @@ -1112,8 +1122,13 @@ func (srs *snapRevSuite) TestDecodeOKWithIntegrity(c *C) {
c.Check(snapRev.DeveloperID(), Equals, "dev-id1")
c.Check(snapRev.Revision(), Equals, 1)
c.Check(snapRev.Provenance(), Equals, "global-upload")
c.Check(snapRev.SnapIntegrity().SHA3_384, Equals, blobSHA3_384)
c.Check(snapRev.SnapIntegrity().Size, Equals, uint64(128))
c.Check(snapRev.SnapIntegrityData()[0].Type, Equals, "dm-verity")
c.Check(snapRev.SnapIntegrityData()[0].Version, Equals, uint(1))
c.Check(snapRev.SnapIntegrityData()[0].HashAlg, Equals, "sha256")
c.Check(snapRev.SnapIntegrityData()[0].DataBlockSize, Equals, uint(4096))
c.Check(snapRev.SnapIntegrityData()[0].HashBlockSize, Equals, uint(4096))
c.Check(snapRev.SnapIntegrityData()[0].Digest, Equals, hexSHA256)
c.Check(snapRev.SnapIntegrityData()[0].Salt, Equals, hexSHA256)
}

const (
Expand Down Expand Up @@ -1160,22 +1175,49 @@ func (srs *snapRevSuite) TestDecodeInvalidWithIntegrity(c *C) {
encoded := srs.makeValidEncodedWithIntegrity()

integrityHdr := "integrity:\n" +
" sha3-384: " + blobSHA3_384 + "\n" +
" size: 128\n"

integrityShaHdr := " sha3-384: " + blobSHA3_384 + "\n"

integritySizeHdr := " size: 128\n"

invalidTests := []struct{ original, invalid, expectedErr string }{
{integrityHdr, "integrity: \n", `"integrity" header must be a map`},
{integrityShaHdr, " sha3-384: \n", `"sha3-384" of integrity header should not be empty`},
{integrityShaHdr, " sha3-384: #\n", `"sha3-384" of integrity header cannot be decoded:.*`},
{integrityShaHdr, " sha3-384: eHl6\n", `"sha3-384" of integrity header does not have the expected bit length: 24`},
{integritySizeHdr, "", `"size" of integrity header is mandatory`},
{integritySizeHdr, " size: \n", `"size" of integrity header should not be empty`},
{integritySizeHdr, " size: -1\n", `"size" of integrity header is not an unsigned integer: -1`},
{integritySizeHdr, " size: zzz\n", `"size" of integrity header is not an unsigned integer: zzz`},
" -\n" +
" type: dm-verity\n" +
" digest: " + hexSHA256 + "\n" +
" version: 1\n" +
" hash-algorithm: sha256\n" +
" data-block-size: 4096\n" +
" hash-block-size: 4096\n" +
" salt: " + hexSHA256 + "\n"

integrityTypeHdr := " type: dm-verity\n"
integrityVersionHdr := " version: 1\n"
integrityHashAlgHdr := " hash-algorithm: sha256\n"
integrityDataBlockSizeHdr := " data-block-size: 4096\n"
integrityHashBlockSizeHdr := " hash-block-size: 4096\n"
integrityDigestHdr := " digest: " + hexSHA256 + "\n"
integritySaltHdr := " salt: " + hexSHA256 + "\n"

invalidTests := []struct {
original,
invalid,
expectedErr string
}{
{integrityHdr, "integrity: test\n", `"integrity" header must contain a list of integrity data`},
{integrityTypeHdr, "", `"type" of integrity data \[0\] is mandatory`},
{integrityTypeHdr, " type: foo\n", `"type" of integrity data \[0\] must be one of \(dm-verity\)`},
{integrityVersionHdr, "", `"version" of integrity data \[0\] of type "dm-verity" is mandatory`},
{integrityVersionHdr, " version: a\n", `"version" of integrity data \[0\] of type "dm-verity" is not an unsigned integer: a`},
{integrityVersionHdr, " version: 2\n", `version of integrity data \[0\] of type "dm-verity" must be one of ` + regexp.QuoteMeta("[1]")},
{integrityHashAlgHdr, "", `"hash-algorithm" of integrity data \[0\] of type "dm-verity" is mandatory`},
{integrityHashAlgHdr, " hash-algorithm: 0\n", `hash algorithm of integrity data \[0\] of type "dm-verity" must be one of .*`},
{integrityHashAlgHdr, " hash-algorithm: a\n", `hash algorithm of integrity data \[0\] of type "dm-verity" must be one of .*`},
{integrityHashAlgHdr, " hash-algorithm: sha384\n", `hash algorithm of integrity data \[0\] of type "dm-verity" must be one of .*`},
{integrityHashAlgHdr, " hash-algorithm: sm3\n", `hash algorithm of integrity data \[0\] of type "dm-verity" must be one of .*`},
{integrityDataBlockSizeHdr, "", `"data-block-size" of integrity data \[0\] of type "dm-verity" \(sha256\) is mandatory`},
{integrityDataBlockSizeHdr, " data-block-size: a\n", `"data-block-size" of integrity data \[0\] of type "dm-verity" \(sha256\) is not an unsigned integer: a`},
{integrityHashBlockSizeHdr, "", `"hash-block-size" of integrity data \[0\] of type "dm-verity" \(sha256\) is mandatory`},
{integrityHashBlockSizeHdr, " hash-block-size: a\n", `"hash-block-size" of integrity data \[0\] of type "dm-verity" \(sha256\) is not an unsigned integer: a`},
{integrityDigestHdr, "", `"digest" of integrity data \[0\] of type "dm-verity" \(sha256\) is mandatory`},
{integrityDigestHdr, " digest: a\n", `"digest" of integrity data \[0\] of type "dm-verity" \(sha256\) cannot be decoded: encoding/hex: odd length hex string`},
{integrityDigestHdr, " digest: ab\n", `"digest" of integrity data \[0\] of type "dm-verity" \(sha256\) does not have the expected bit length: 8`},
{integritySaltHdr, "", `"salt" of integrity data \[0\] of type "dm-verity" \(sha256\) is mandatory`},
{integritySaltHdr, " salt: a\n", `"salt" of integrity data \[0\] of type "dm-verity" \(sha256\) cannot be decoded: encoding/hex: odd length hex string`},
{integritySaltHdr, " salt: ab\n", `"salt" of integrity data \[0\] of type "dm-verity" \(sha256\) does not have the expected bit length: 8`},
}

for _, test := range invalidTests {
Expand Down

0 comments on commit e7a49b9

Please sign in to comment.