From 27e9c05a136b04514f89d663324b2ac2c90122d2 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Wed, 18 Dec 2024 15:00:13 -0500 Subject: [PATCH 1/6] Add multihasher for computing multiple hashes at once Signed-off-by: Cody Soyland --- pkg/verify/signature.go | 34 +++++++++++++++++++++++ pkg/verify/signature_internal_test.go | 40 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 pkg/verify/signature_internal_test.go diff --git a/pkg/verify/signature.go b/pkg/verify/signature.go index 1d96c12..054d887 100644 --- a/pkg/verify/signature.go +++ b/pkg/verify/signature.go @@ -269,3 +269,37 @@ func limitSubjects(statement *in_toto.Statement) error { } return nil } + +type multihasher struct { + hashfuncs []crypto.Hash + hashes []hash.Hash +} + +func newMultihasher(hashfuncs []crypto.Hash) *multihasher { + hashes := make([]hash.Hash, len(hashfuncs)) + for i := range hashfuncs { + hashes[i] = hashfuncs[i].New() + } + return &multihasher{ + hashfuncs: hashfuncs, + hashes: hashes, + } +} + +func (m *multihasher) Write(p []byte) (n int, err error) { + for i := range m.hashes { + n, err = m.hashes[i].Write(p) + if err != nil { + return + } + } + return +} + +func (m *multihasher) Sum(b []byte) map[crypto.Hash][]byte { + sums := make(map[crypto.Hash][]byte, len(m.hashes)) + for i := range m.hashes { + sums[m.hashfuncs[i]] = m.hashes[i].Sum(b) + } + return sums +} diff --git a/pkg/verify/signature_internal_test.go b/pkg/verify/signature_internal_test.go new file mode 100644 index 0000000..8388483 --- /dev/null +++ b/pkg/verify/signature_internal_test.go @@ -0,0 +1,40 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "crypto" + "crypto/sha256" + "crypto/sha512" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMultiHasher(t *testing.T) { + testBytes := []byte("Hello, world!") + hash256 := sha256.Sum256(testBytes) + hash512 := sha512.Sum512(testBytes) + + hasher := newMultihasher([]crypto.Hash{crypto.SHA256, crypto.SHA512}) + _, err := hasher.Write(testBytes) + assert.NoError(t, err) + + hashes := hasher.Sum(nil) + + assert.Equal(t, 2, len(hashes)) + assert.EqualValues(t, hash256[:], hashes[crypto.SHA256]) + assert.EqualValues(t, hash512[:], hashes[crypto.SHA512]) +} From afa1b7bdc58a24eaa20877ae5c20bcf9bcec3e3d Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 20 Dec 2024 07:37:33 -0500 Subject: [PATCH 2/6] Use multihasher to efficiently search for all required hashes to satisfy subject discovery in multi-subject attestations Signed-off-by: Cody Soyland --- pkg/verify/signature.go | 136 ++++++++++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/pkg/verify/signature.go b/pkg/verify/signature.go index 054d887..0edadbe 100644 --- a/pkg/verify/signature.go +++ b/pkg/verify/signature.go @@ -23,6 +23,7 @@ import ( "fmt" "hash" "io" + "slices" in_toto "github.com/in-toto/attestation/go/v1" "github.com/secure-systems-lab/go-securesystemslib/dsse" @@ -146,56 +147,40 @@ func verifyEnvelopeWithArtifact(verifier signature.Verifier, envelope EnvelopeCo if err = limitSubjects(statement); err != nil { return err } - - var artifactDigestAlgorithm string - var artifactDigest []byte - - // Determine artifact digest algorithm by looking at the first subject's - // digests. This assumes that if a statement contains multiple subjects, - // they all use the same digest algorithm(s). + // Sanity check (no subjects) if len(statement.Subject) == 0 { return errors.New("no subjects found in statement") } - if len(statement.Subject[0].Digest) == 0 { - return errors.New("no digests found in statement") - } - // Select the strongest digest algorithm available. - for _, alg := range []string{"sha512", "sha384", "sha256"} { - if _, ok := statement.Subject[0].Digest[alg]; ok { - artifactDigestAlgorithm = alg - continue - } - } - if artifactDigestAlgorithm == "" { - return errors.New("could not verify artifact: unsupported digest algorithm") + // determine which hash functions to use + hashFuncs, err := determineHashFunctions(statement) + if err != nil { + return fmt.Errorf("could not verify artifact: unable to determine hash functions: %w", err) } // Compute digest of the artifact. - var hasher hash.Hash - switch artifactDigestAlgorithm { - case "sha512": - hasher = crypto.SHA512.New() - case "sha384": - hasher = crypto.SHA384.New() - case "sha256": - hasher = crypto.SHA256.New() - } + hasher := newMultihasher(hashFuncs) _, err = io.Copy(hasher, artifact) if err != nil { return fmt.Errorf("could not verify artifact: unable to calculate digest: %w", err) } - artifactDigest = hasher.Sum(nil) + artifactDigests := hasher.Sum(nil) // Look for artifact digest in statement for _, subject := range statement.Subject { for alg, digest := range subject.Digest { - hexdigest, err := hex.DecodeString(digest) + hf, err := algStringToHashFunc(alg) if err != nil { - return fmt.Errorf("could not verify artifact: unable to decode subject digest: %w", err) + continue } - if alg == artifactDigestAlgorithm && bytes.Equal(artifactDigest, hexdigest) { - return nil + if artifactDigest, ok := artifactDigests[hf]; ok { + hexdigest, err := hex.DecodeString(digest) + if err != nil { + continue + } + if bytes.Equal(artifactDigest, hexdigest) { + return nil + } } } } @@ -303,3 +288,88 @@ func (m *multihasher) Sum(b []byte) map[crypto.Hash][]byte { } return sums } + +func algStringToHashFunc(alg string) (crypto.Hash, error) { + switch alg { + case "sha256": + return crypto.SHA256, nil + case "sha384": + return crypto.SHA384, nil + case "sha512": + return crypto.SHA512, nil + default: + return 0, errors.New("unsupported digest algorithm") + } +} + +func determineHashFunctions(statement *in_toto.Statement) ([]crypto.Hash, error) { + if len(statement.Subject) == 0 { + return nil, errors.New("no subjects found in statement") + } + + algorithmCounts := supportedHashFuncsCount() + for _, subject := range statement.Subject { + for alg := range subject.Digest { + hf, err := algStringToHashFunc(alg) + if err != nil { + continue + } + algorithmCounts[hf]++ + } + } + anyCompatibleAlgorithms := false + var mostCommonHashFunc crypto.Hash + largestCount := 0 + seenHashFuncs := make([]crypto.Hash, 0) + for hf, count := range algorithmCounts { + if count > 0 { + anyCompatibleAlgorithms = true + if !slices.Contains(seenHashFuncs, hf) { + seenHashFuncs = append(seenHashFuncs, hf) + } + } + // if this algorithm is supported by all subjects, we can use it alone + if count == len(statement.Subject) { + return []crypto.Hash{hf}, nil + } + if count > largestCount { + largestCount = count + mostCommonHashFunc = hf + } + } + if !anyCompatibleAlgorithms { + return nil, errors.New("no supported digest algorithms found in statement") + } + + // If we didn't find a common algorithm, see if we cover more digests by using all seen algorithms + countWithAllAlgorithms := 0 + for _, subject := range statement.Subject { + for alg := range subject.Digest { + hf, err := algStringToHashFunc(alg) + if err != nil { + continue + } + if slices.Contains(seenHashFuncs, hf) { + countWithAllAlgorithms++ + break + } + } + } + // No need to calculate all digests if the most common one covers the same number of subjects + if countWithAllAlgorithms > largestCount { + return seenHashFuncs, nil + } + return []crypto.Hash{mostCommonHashFunc}, nil +} + +func supportedHashFuncsCount() map[crypto.Hash]int { + counts := make(map[crypto.Hash]int) + for _, hf := range supportedHashFuncs() { + counts[hf] = 0 + } + return counts +} + +func supportedHashFuncs() []crypto.Hash { + return []crypto.Hash{crypto.SHA512, crypto.SHA384, crypto.SHA256} +} From 38c9e8f564105494f0dce661590e0866916851bd Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 20 Dec 2024 11:15:01 -0500 Subject: [PATCH 3/6] Rewrite and simplify hash function selection Signed-off-by: Cody Soyland --- pkg/verify/signature.go | 88 ++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/pkg/verify/signature.go b/pkg/verify/signature.go index 0edadbe..b5ca75a 100644 --- a/pkg/verify/signature.go +++ b/pkg/verify/signature.go @@ -153,7 +153,7 @@ func verifyEnvelopeWithArtifact(verifier signature.Verifier, envelope EnvelopeCo } // determine which hash functions to use - hashFuncs, err := determineHashFunctions(statement) + hashFuncs, err := getHashFunctions(statement) if err != nil { return fmt.Errorf("could not verify artifact: unable to determine hash functions: %w", err) } @@ -302,74 +302,62 @@ func algStringToHashFunc(alg string) (crypto.Hash, error) { } } -func determineHashFunctions(statement *in_toto.Statement) ([]crypto.Hash, error) { +// getHashFunctions returns the smallest subset of supported hash functions +// that are needed to verify all subjects in a statement. +func getHashFunctions(statement *in_toto.Statement) ([]crypto.Hash, error) { if len(statement.Subject) == 0 { return nil, errors.New("no subjects found in statement") } - algorithmCounts := supportedHashFuncsCount() - for _, subject := range statement.Subject { + supportedHashFuncs := []crypto.Hash{crypto.SHA512, crypto.SHA384, crypto.SHA256} + chosenHashFuncs := make([]crypto.Hash, 0, len(supportedHashFuncs)) + subjectHashFuncs := make([][]crypto.Hash, len(statement.Subject)) + + // go through the statement and make a simple data structure to hold the + // list of hash funcs for each subject (subjectHashFuncs) + for i, subject := range statement.Subject { for alg := range subject.Digest { hf, err := algStringToHashFunc(alg) if err != nil { continue } - algorithmCounts[hf]++ + subjectHashFuncs[i] = append(subjectHashFuncs[i], hf) } } - anyCompatibleAlgorithms := false - var mostCommonHashFunc crypto.Hash - largestCount := 0 - seenHashFuncs := make([]crypto.Hash, 0) - for hf, count := range algorithmCounts { - if count > 0 { - anyCompatibleAlgorithms = true - if !slices.Contains(seenHashFuncs, hf) { - seenHashFuncs = append(seenHashFuncs, hf) - } - } - // if this algorithm is supported by all subjects, we can use it alone - if count == len(statement.Subject) { - return []crypto.Hash{hf}, nil - } - if count > largestCount { - largestCount = count - mostCommonHashFunc = hf + + // for each subject, see if we have chosen a compatible hash func, and if + // not, add the first one that is supported + for _, hfs := range subjectHashFuncs { + // if any of the hash funcs are already in chosenHashFuncs, skip + if len(intersection(hfs, chosenHashFuncs)) > 0 { + continue } - } - if !anyCompatibleAlgorithms { - return nil, errors.New("no supported digest algorithms found in statement") - } - // If we didn't find a common algorithm, see if we cover more digests by using all seen algorithms - countWithAllAlgorithms := 0 - for _, subject := range statement.Subject { - for alg := range subject.Digest { - hf, err := algStringToHashFunc(alg) - if err != nil { - continue - } - if slices.Contains(seenHashFuncs, hf) { - countWithAllAlgorithms++ + // check each supported hash func and add chose it if the subject + // has a digest for it + for _, hf := range supportedHashFuncs { + if slices.Contains(hfs, hf) { + chosenHashFuncs = append(chosenHashFuncs, hf) break } } } - // No need to calculate all digests if the most common one covers the same number of subjects - if countWithAllAlgorithms > largestCount { - return seenHashFuncs, nil - } - return []crypto.Hash{mostCommonHashFunc}, nil -} -func supportedHashFuncsCount() map[crypto.Hash]int { - counts := make(map[crypto.Hash]int) - for _, hf := range supportedHashFuncs() { - counts[hf] = 0 + if len(chosenHashFuncs) == 0 { + return nil, errors.New("no supported digest algorithms found") } - return counts + + return chosenHashFuncs, nil } -func supportedHashFuncs() []crypto.Hash { - return []crypto.Hash{crypto.SHA512, crypto.SHA384, crypto.SHA256} +func intersection(a, b []crypto.Hash) []crypto.Hash { + var result []crypto.Hash + for _, x := range a { + for _, y := range b { + if x == y { + result = append(result, x) + } + } + } + return result } From 33d31c1b5f7a2e265312b38306be34a699bc08de Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 20 Dec 2024 11:15:20 -0500 Subject: [PATCH 4/6] Rename vars Signed-off-by: Cody Soyland --- pkg/verify/signature.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/verify/signature.go b/pkg/verify/signature.go index b5ca75a..9e1f451 100644 --- a/pkg/verify/signature.go +++ b/pkg/verify/signature.go @@ -168,17 +168,17 @@ func verifyEnvelopeWithArtifact(verifier signature.Verifier, envelope EnvelopeCo // Look for artifact digest in statement for _, subject := range statement.Subject { - for alg, digest := range subject.Digest { + for alg, hexdigest := range subject.Digest { hf, err := algStringToHashFunc(alg) if err != nil { continue } if artifactDigest, ok := artifactDigests[hf]; ok { - hexdigest, err := hex.DecodeString(digest) + digest, err := hex.DecodeString(hexdigest) if err != nil { continue } - if bytes.Equal(artifactDigest, hexdigest) { + if bytes.Equal(artifactDigest, digest) { return nil } } From c1108326df0552c41d343d95ef1b75359e048c01 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 20 Dec 2024 11:42:15 -0500 Subject: [PATCH 5/6] Add test for getHashFunctions Signed-off-by: Cody Soyland --- pkg/verify/signature_internal_test.go | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/pkg/verify/signature_internal_test.go b/pkg/verify/signature_internal_test.go index 8388483..68ca7fe 100644 --- a/pkg/verify/signature_internal_test.go +++ b/pkg/verify/signature_internal_test.go @@ -20,6 +20,7 @@ import ( "crypto/sha512" "testing" + in_toto "github.com/in-toto/attestation/go/v1" "github.com/stretchr/testify/assert" ) @@ -38,3 +39,70 @@ func TestMultiHasher(t *testing.T) { assert.EqualValues(t, hash256[:], hashes[crypto.SHA256]) assert.EqualValues(t, hash512[:], hashes[crypto.SHA512]) } + +func makeStatement(subjectalgs [][]string) *in_toto.Statement { + statement := &in_toto.Statement{ + Subject: make([]*in_toto.ResourceDescriptor, len(subjectalgs)), + } + for i, subjectAlg := range subjectalgs { + statement.Subject[i] = &in_toto.ResourceDescriptor{ + Digest: make(map[string]string), + } + for _, digest := range subjectAlg { + // content of digest doesn't matter for this test + statement.Subject[i].Digest[digest] = "foobar" + } + } + return statement +} + +func TestGetHashFunctions(t *testing.T) { + for _, test := range []struct { + name string + algs [][]string + expectOutput []crypto.Hash + expectError bool + }{ + { + name: "choose strongest algorithm", + algs: [][]string{{"sha256", "sha512"}}, + expectOutput: []crypto.Hash{crypto.SHA512}, + }, + { + name: "choose both algorithms", + algs: [][]string{{"sha256"}, {"sha512"}}, + expectOutput: []crypto.Hash{crypto.SHA256, crypto.SHA512}, + }, + { + name: "choose one algorithm", + algs: [][]string{{"sha512"}, {"sha256", "sha512"}}, + expectOutput: []crypto.Hash{crypto.SHA512}, + }, + { + name: "choose two algorithms", + algs: [][]string{{"sha256", "sha512"}, {"sha384", "sha512"}, {"sha256", "sha384"}}, + expectOutput: []crypto.Hash{crypto.SHA512, crypto.SHA384}, + }, + { + name: "ignore unknown algorithm", + algs: [][]string{{"md5", "sha512"}, {"sha256", "sha512"}}, + expectOutput: []crypto.Hash{crypto.SHA512}, + }, + { + name: "no recognized algorithms", + algs: [][]string{{"md5"}, {"sha1"}}, + expectError: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + statement := makeStatement(test.algs) + hfs, err := getHashFunctions(statement) + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.expectOutput, hfs) + }) + } +} From d08901398403c2527a2838183d928feda1eee92c Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 20 Dec 2024 13:41:27 -0500 Subject: [PATCH 6/6] Update pkg/verify/signature.go Co-authored-by: Hayden B Signed-off-by: Cody Soyland --- pkg/verify/signature.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/verify/signature.go b/pkg/verify/signature.go index 9e1f451..8d5569e 100644 --- a/pkg/verify/signature.go +++ b/pkg/verify/signature.go @@ -333,7 +333,7 @@ func getHashFunctions(statement *in_toto.Statement) ([]crypto.Hash, error) { continue } - // check each supported hash func and add chose it if the subject + // check each supported hash func and add it if the subject // has a digest for it for _, hf := range supportedHashFuncs { if slices.Contains(hfs, hf) {