From e7a49b9fedeacf78105bc304edfee49c9650d91b Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Thu, 23 Jan 2025 11:36:25 +0200 Subject: [PATCH] asserts: snap integrity assertion (#14870) * 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 --- asserts/header_checks.go | 16 +-- asserts/snap_asserts.go | 185 ++++++++++++++++++++++++++++------- asserts/snap_asserts_test.go | 84 ++++++++++++---- 3 files changed, 221 insertions(+), 64 deletions(-) diff --git a/asserts/header_checks.go b/asserts/header_checks.go index 5b55ed8f5dd..455d54835dd 100644 --- a/asserts/header_checks.go +++ b/asserts/header_checks.go @@ -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` diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go index 84dcbe09a6e..5720c4d901a 100644 --- a/asserts/snap_asserts.go +++ b/asserts/snap_asserts.go @@ -22,8 +22,10 @@ package asserts import ( "bytes" "crypto" + "encoding/hex" "errors" "fmt" + "strings" "time" // expected for digests @@ -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. @@ -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. @@ -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. @@ -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 { @@ -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 } diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go index 6532c3aa29a..cfaffc7cb89 100644 --- a/asserts/snap_asserts_test.go +++ b/asserts/snap_asserts_test.go @@ -23,6 +23,7 @@ import ( "encoding/base64" "os" "path/filepath" + "regexp" "sort" "strings" "time" @@ -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) { @@ -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 + @@ -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 ( @@ -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 {