Skip to content

Commit 693bbb2

Browse files
Support for multi-subject attestations using different hash algorithms (#361)
* Add multihasher for computing multiple hashes at once Signed-off-by: Cody Soyland <[email protected]> * Use multihasher to efficiently search for all required hashes to satisfy subject discovery in multi-subject attestations Signed-off-by: Cody Soyland <[email protected]> * Rewrite and simplify hash function selection Signed-off-by: Cody Soyland <[email protected]> * Rename vars Signed-off-by: Cody Soyland <[email protected]> * Add test for getHashFunctions Signed-off-by: Cody Soyland <[email protected]> * Update pkg/verify/signature.go Co-authored-by: Hayden B <[email protected]> Signed-off-by: Cody Soyland <[email protected]> * Use slices.Contains to simplify Signed-off-by: Cody Soyland <[email protected]> * Add error return from multiHasher, table based test Signed-off-by: Cody Soyland <[email protected]> --------- Signed-off-by: Cody Soyland <[email protected]> Co-authored-by: Hayden B <[email protected]>
1 parent 4c441c3 commit 693bbb2

File tree

2 files changed

+286
-33
lines changed

2 files changed

+286
-33
lines changed

pkg/verify/signature.go

+129-33
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"hash"
2525
"io"
26+
"slices"
2627

2728
in_toto "github.com/in-toto/attestation/go/v1"
2829
"github.com/secure-systems-lab/go-securesystemslib/dsse"
@@ -146,56 +147,43 @@ func verifyEnvelopeWithArtifact(verifier signature.Verifier, envelope EnvelopeCo
146147
if err = limitSubjects(statement); err != nil {
147148
return err
148149
}
149-
150-
var artifactDigestAlgorithm string
151-
var artifactDigest []byte
152-
153-
// Determine artifact digest algorithm by looking at the first subject's
154-
// digests. This assumes that if a statement contains multiple subjects,
155-
// they all use the same digest algorithm(s).
150+
// Sanity check (no subjects)
156151
if len(statement.Subject) == 0 {
157152
return errors.New("no subjects found in statement")
158153
}
159-
if len(statement.Subject[0].Digest) == 0 {
160-
return errors.New("no digests found in statement")
161-
}
162154

163-
// Select the strongest digest algorithm available.
164-
for _, alg := range []string{"sha512", "sha384", "sha256"} {
165-
if _, ok := statement.Subject[0].Digest[alg]; ok {
166-
artifactDigestAlgorithm = alg
167-
continue
168-
}
169-
}
170-
if artifactDigestAlgorithm == "" {
171-
return errors.New("could not verify artifact: unsupported digest algorithm")
155+
// determine which hash functions to use
156+
hashFuncs, err := getHashFunctions(statement)
157+
if err != nil {
158+
return fmt.Errorf("unable to determine hash functions: %w", err)
172159
}
173160

174161
// Compute digest of the artifact.
175-
var hasher hash.Hash
176-
switch artifactDigestAlgorithm {
177-
case "sha512":
178-
hasher = crypto.SHA512.New()
179-
case "sha384":
180-
hasher = crypto.SHA384.New()
181-
case "sha256":
182-
hasher = crypto.SHA256.New()
162+
hasher, err := newMultihasher(hashFuncs)
163+
if err != nil {
164+
return fmt.Errorf("could not verify artifact: unable to create hasher: %w", err)
183165
}
184166
_, err = io.Copy(hasher, artifact)
185167
if err != nil {
186168
return fmt.Errorf("could not verify artifact: unable to calculate digest: %w", err)
187169
}
188-
artifactDigest = hasher.Sum(nil)
170+
artifactDigests := hasher.Sum(nil)
189171

190172
// Look for artifact digest in statement
191173
for _, subject := range statement.Subject {
192-
for alg, digest := range subject.Digest {
193-
hexdigest, err := hex.DecodeString(digest)
174+
for alg, hexdigest := range subject.Digest {
175+
hf, err := algStringToHashFunc(alg)
194176
if err != nil {
195-
return fmt.Errorf("could not verify artifact: unable to decode subject digest: %w", err)
177+
continue
196178
}
197-
if alg == artifactDigestAlgorithm && bytes.Equal(artifactDigest, hexdigest) {
198-
return nil
179+
if artifactDigest, ok := artifactDigests[hf]; ok {
180+
digest, err := hex.DecodeString(hexdigest)
181+
if err != nil {
182+
continue
183+
}
184+
if bytes.Equal(artifactDigest, digest) {
185+
return nil
186+
}
199187
}
200188
}
201189
}
@@ -269,3 +257,111 @@ func limitSubjects(statement *in_toto.Statement) error {
269257
}
270258
return nil
271259
}
260+
261+
type multihasher struct {
262+
hashfuncs []crypto.Hash
263+
hashes []hash.Hash
264+
}
265+
266+
func newMultihasher(hashfuncs []crypto.Hash) (*multihasher, error) {
267+
if len(hashfuncs) == 0 {
268+
return nil, errors.New("no hash functions specified")
269+
}
270+
hashes := make([]hash.Hash, len(hashfuncs))
271+
for i := range hashfuncs {
272+
hashes[i] = hashfuncs[i].New()
273+
}
274+
return &multihasher{
275+
hashfuncs: hashfuncs,
276+
hashes: hashes,
277+
}, nil
278+
}
279+
280+
func (m *multihasher) Write(p []byte) (n int, err error) {
281+
for i := range m.hashes {
282+
n, err = m.hashes[i].Write(p)
283+
if err != nil {
284+
return
285+
}
286+
}
287+
return
288+
}
289+
290+
func (m *multihasher) Sum(b []byte) map[crypto.Hash][]byte {
291+
sums := make(map[crypto.Hash][]byte, len(m.hashes))
292+
for i := range m.hashes {
293+
sums[m.hashfuncs[i]] = m.hashes[i].Sum(b)
294+
}
295+
return sums
296+
}
297+
298+
func algStringToHashFunc(alg string) (crypto.Hash, error) {
299+
switch alg {
300+
case "sha256":
301+
return crypto.SHA256, nil
302+
case "sha384":
303+
return crypto.SHA384, nil
304+
case "sha512":
305+
return crypto.SHA512, nil
306+
default:
307+
return 0, errors.New("unsupported digest algorithm")
308+
}
309+
}
310+
311+
// getHashFunctions returns the smallest subset of supported hash functions
312+
// that are needed to verify all subjects in a statement.
313+
func getHashFunctions(statement *in_toto.Statement) ([]crypto.Hash, error) {
314+
if len(statement.Subject) == 0 {
315+
return nil, errors.New("no subjects found in statement")
316+
}
317+
318+
supportedHashFuncs := []crypto.Hash{crypto.SHA512, crypto.SHA384, crypto.SHA256}
319+
chosenHashFuncs := make([]crypto.Hash, 0, len(supportedHashFuncs))
320+
subjectHashFuncs := make([][]crypto.Hash, len(statement.Subject))
321+
322+
// go through the statement and make a simple data structure to hold the
323+
// list of hash funcs for each subject (subjectHashFuncs)
324+
for i, subject := range statement.Subject {
325+
for alg := range subject.Digest {
326+
hf, err := algStringToHashFunc(alg)
327+
if err != nil {
328+
continue
329+
}
330+
subjectHashFuncs[i] = append(subjectHashFuncs[i], hf)
331+
}
332+
}
333+
334+
// for each subject, see if we have chosen a compatible hash func, and if
335+
// not, add the first one that is supported
336+
for _, hfs := range subjectHashFuncs {
337+
// if any of the hash funcs are already in chosenHashFuncs, skip
338+
if len(intersection(hfs, chosenHashFuncs)) > 0 {
339+
continue
340+
}
341+
342+
// check each supported hash func and add it if the subject
343+
// has a digest for it
344+
for _, hf := range supportedHashFuncs {
345+
if slices.Contains(hfs, hf) {
346+
chosenHashFuncs = append(chosenHashFuncs, hf)
347+
break
348+
}
349+
}
350+
}
351+
352+
if len(chosenHashFuncs) == 0 {
353+
return nil, errors.New("no supported digest algorithms found")
354+
}
355+
356+
return chosenHashFuncs, nil
357+
}
358+
359+
func intersection(a, b []crypto.Hash) []crypto.Hash {
360+
var result []crypto.Hash
361+
for _, x := range a {
362+
if slices.Contains(b, x) {
363+
result = append(result, x)
364+
}
365+
}
366+
return result
367+
}

pkg/verify/signature_internal_test.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2024 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package verify
16+
17+
import (
18+
"crypto"
19+
"crypto/sha256"
20+
"crypto/sha512"
21+
"testing"
22+
23+
in_toto "github.com/in-toto/attestation/go/v1"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestMultiHasher(t *testing.T) {
28+
testBytes := []byte("Hello, world!")
29+
hash256 := sha256.Sum256(testBytes)
30+
hash384 := sha512.Sum384(testBytes)
31+
hash512 := sha512.Sum512(testBytes)
32+
33+
for _, tc := range []struct {
34+
name string
35+
hashes []crypto.Hash
36+
output map[crypto.Hash][]byte
37+
err bool
38+
}{
39+
{
40+
name: "one hash",
41+
hashes: []crypto.Hash{crypto.SHA256},
42+
output: map[crypto.Hash][]byte{
43+
crypto.SHA256: hash256[:],
44+
},
45+
},
46+
{
47+
name: "two hashes",
48+
hashes: []crypto.Hash{crypto.SHA256, crypto.SHA512},
49+
output: map[crypto.Hash][]byte{
50+
crypto.SHA256: hash256[:],
51+
crypto.SHA512: hash512[:],
52+
},
53+
},
54+
{
55+
name: "three hashes",
56+
hashes: []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512},
57+
output: map[crypto.Hash][]byte{
58+
crypto.SHA256: hash256[:],
59+
crypto.SHA384: hash384[:],
60+
crypto.SHA512: hash512[:],
61+
},
62+
},
63+
{
64+
name: "no hashes",
65+
hashes: []crypto.Hash{},
66+
output: nil,
67+
err: true,
68+
},
69+
} {
70+
t.Run(tc.name, func(t *testing.T) {
71+
hasher, err := newMultihasher(tc.hashes)
72+
if tc.err {
73+
assert.Error(t, err)
74+
return
75+
}
76+
assert.NoError(t, err)
77+
78+
_, err = hasher.Write(testBytes)
79+
assert.NoError(t, err)
80+
81+
hashes := hasher.Sum(nil)
82+
83+
assert.EqualValues(t, tc.output, hashes)
84+
assert.Equal(t, len(tc.hashes), len(hashes))
85+
for _, hash := range tc.hashes {
86+
assert.EqualValues(t, tc.output[hash], hashes[hash])
87+
}
88+
})
89+
}
90+
}
91+
92+
func makeStatement(subjectalgs [][]string) *in_toto.Statement {
93+
statement := &in_toto.Statement{
94+
Subject: make([]*in_toto.ResourceDescriptor, len(subjectalgs)),
95+
}
96+
for i, subjectAlg := range subjectalgs {
97+
statement.Subject[i] = &in_toto.ResourceDescriptor{
98+
Digest: make(map[string]string),
99+
}
100+
for _, digest := range subjectAlg {
101+
// content of digest doesn't matter for this test
102+
statement.Subject[i].Digest[digest] = "foobar"
103+
}
104+
}
105+
return statement
106+
}
107+
108+
func TestGetHashFunctions(t *testing.T) {
109+
for _, test := range []struct {
110+
name string
111+
algs [][]string
112+
expectOutput []crypto.Hash
113+
expectError bool
114+
}{
115+
{
116+
name: "choose strongest algorithm",
117+
algs: [][]string{{"sha256", "sha512"}},
118+
expectOutput: []crypto.Hash{crypto.SHA512},
119+
},
120+
{
121+
name: "choose both algorithms",
122+
algs: [][]string{{"sha256"}, {"sha512"}},
123+
expectOutput: []crypto.Hash{crypto.SHA256, crypto.SHA512},
124+
},
125+
{
126+
name: "choose one algorithm",
127+
algs: [][]string{{"sha512"}, {"sha256", "sha512"}},
128+
expectOutput: []crypto.Hash{crypto.SHA512},
129+
},
130+
{
131+
name: "choose two algorithms",
132+
algs: [][]string{{"sha256", "sha512"}, {"sha384", "sha512"}, {"sha256", "sha384"}},
133+
expectOutput: []crypto.Hash{crypto.SHA512, crypto.SHA384},
134+
},
135+
{
136+
name: "ignore unknown algorithm",
137+
algs: [][]string{{"md5", "sha512"}, {"sha256", "sha512"}},
138+
expectOutput: []crypto.Hash{crypto.SHA512},
139+
},
140+
{
141+
name: "no recognized algorithms",
142+
algs: [][]string{{"md5"}, {"sha1"}},
143+
expectError: true,
144+
},
145+
} {
146+
t.Run(test.name, func(t *testing.T) {
147+
statement := makeStatement(test.algs)
148+
hfs, err := getHashFunctions(statement)
149+
if test.expectError {
150+
assert.Error(t, err)
151+
} else {
152+
assert.NoError(t, err)
153+
}
154+
assert.Equal(t, test.expectOutput, hfs)
155+
})
156+
}
157+
}

0 commit comments

Comments
 (0)