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

Support for multi-subject attestations using different hash algorithms #361

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
170 changes: 137 additions & 33 deletions pkg/verify/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
codysoyland marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
continue
}
if bytes.Equal(artifactDigest, hexdigest) {
return nil
}
}
}
}
Expand Down Expand Up @@ -269,3 +254,122 @@ 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
}

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) {
codysoyland marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we err out if digest is empty? This is the current behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interpreting the spec to say the subject is invalid if the digest is the empty string seems pretty defensible to me / 👍 to erroring out on empty digests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little iffy on this, because verification may still pass if there exists a subject with the given digest, even if a subject exists that does not contain a digest. I'm not sure it's the responsibility of the verifier to make sure every subject contains a digest.

hf, err := algStringToHashFunc(alg)
if err != nil {
continue
}
algorithmCounts[hf]++
codysoyland marked this conversation as resolved.
Show resolved Hide resolved
}
}
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}
}
40 changes: 40 additions & 0 deletions pkg/verify/signature_internal_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
Loading