Skip to content

Commit

Permalink
Add hard limits for number of TSA entries, Tlog entries, and attestat…
Browse files Browse the repository at this point in the history
…ion subjects/digests (#286)

* Merge commit from fork

This reduces the capability of an adversary to craft a malicious bundle containing large numbers of these data, which can result in a target verifier process consuming high CPU and memory resources, resulting in an "endless data attack", a type of DoS attack.

Fixes GHSA-cq38-jh5f-37mq

Signed-off-by: Cody Soyland <codysoyland@github.com>

* nolint in tests

Signed-off-by: Cody Soyland <codysoyland@github.com>

---------

Signed-off-by: Cody Soyland <codysoyland@github.com>
  • Loading branch information
codysoyland authored Sep 4, 2024
1 parent e50c2d7 commit 01e70e8
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 3 deletions.
22 changes: 22 additions & 0 deletions pkg/verify/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import (
"github.com/sigstore/sigstore/pkg/signature/options"
)

const maxAllowedSubjects = 1024
const maxAllowedSubjectDigests = 32

var ErrDSSEInvalidSignatureCount = errors.New("exactly one signature is required")

func VerifySignature(sigContent SignatureContent, verificationContent VerificationContent, trustedMaterial root.TrustedMaterial) error { // nolint: revive
Expand Down Expand Up @@ -179,8 +182,17 @@ func verifyEnvelopeWithArtifact(verifier signature.Verifier, envelope EnvelopeCo
}
artifactDigest = hasher.Sum(nil)

// limit the number of subjects to prevent DoS
if len(statement.Subject) > maxAllowedSubjects {
return fmt.Errorf("too many subjects: %d > %d", len(statement.Subject), maxAllowedSubjects)
}

// Look for artifact digest in statement
for _, subject := range statement.Subject {
// limit the number of digests to prevent DoS
if len(subject.Digest) > maxAllowedSubjectDigests {
return fmt.Errorf("too many digests: %d > %d", len(subject.Digest), maxAllowedSubjectDigests)
}
for alg, digest := range subject.Digest {
hexdigest, err := hex.DecodeString(digest)
if err != nil {
Expand All @@ -203,7 +215,17 @@ func verifyEnvelopeWithArtifactDigest(verifier signature.Verifier, envelope Enve
if err != nil {
return fmt.Errorf("could not verify artifact: unable to extract statement from envelope: %w", err)
}

// limit the number of subjects to prevent DoS
if len(statement.Subject) > maxAllowedSubjects {
return fmt.Errorf("too many subjects: %d > %d", len(statement.Subject), maxAllowedSubjects)
}

for _, subject := range statement.Subject {
// limit the number of digests to prevent DoS
if len(subject.Digest) > maxAllowedSubjectDigests {
return fmt.Errorf("too many digests: %d > %d", len(subject.Digest), maxAllowedSubjectDigests)
}
for alg, digest := range subject.Digest {
if alg == artifactDigestAlgorithm {
hexdigest, err := hex.DecodeString(digest)
Expand Down
63 changes: 62 additions & 1 deletion pkg/verify/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"testing"

"github.com/in-toto/in-toto-golang/in_toto"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
"github.com/sigstore/sigstore-go/pkg/testing/ca"
"github.com/sigstore/sigstore-go/pkg/verify"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -94,7 +97,7 @@ func TestSignatureVerifierMessageSignature(t *testing.T) {
virtualSigstore, err := ca.NewVirtualSigstore()
assert.NoError(t, err)

artifact := "Hi, I am an artifact!"
artifact := "Hi, I am an artifact!" //nolint:goconst
entity, err := virtualSigstore.Sign("foo@example.com", "issuer", []byte(artifact))
assert.NoError(t, err)

Expand All @@ -113,3 +116,61 @@ func TestSignatureVerifierMessageSignature(t *testing.T) {
assert.Error(t, err)
assert.Nil(t, result)
}

func TestTooManySubjects(t *testing.T) {
virtualSigstore, err := ca.NewVirtualSigstore()
assert.NoError(t, err)

tooManySubjectsStatement := in_toto.Statement{}
for i := 0; i < 1025; i++ {
tooManySubjectsStatement.Subject = append(tooManySubjectsStatement.Subject, in_toto.Subject{
Name: fmt.Sprintf("subject-%d", i),
Digest: map[string]string{
"sha256": "", // actual content of digest does not matter for this test
},
})
}

tooManySubjectsStatementBytes, err := json.Marshal(tooManySubjectsStatement)
assert.NoError(t, err)

tooManySubjectsEntity, err := virtualSigstore.Attest("foo@example.com", "issuer", tooManySubjectsStatementBytes)
assert.NoError(t, err)

verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
assert.NoError(t, err)

artifact := "Hi, I am an artifact!" //nolint:goconst
_, err = verifier.Verify(tooManySubjectsEntity, verify.NewPolicy(verify.WithArtifact(bytes.NewBufferString(artifact)), verify.WithoutIdentitiesUnsafe()))
assert.ErrorContains(t, err, "too many subjects")
}

func TestTooManyDigests(t *testing.T) {
virtualSigstore, err := ca.NewVirtualSigstore()
assert.NoError(t, err)

tooManyDigestsStatement := in_toto.Statement{}
tooManyDigestsStatement.Subject = []in_toto.Subject{
{
Name: fmt.Sprintf("subject"),
Digest: make(common.DigestSet),
},
}
tooManyDigestsStatement.Subject[0].Digest["sha512"] = "" // verifier requires that at least one known hash algorithm is present in the digest map
for i := 0; i < 32; i++ {
tooManyDigestsStatement.Subject[0].Digest[fmt.Sprintf("digest-%d", i)] = ""
}

tooManySubjectsStatementBytes, err := json.Marshal(tooManyDigestsStatement)
assert.NoError(t, err)

tooManySubjectsEntity, err := virtualSigstore.Attest("foo@example.com", "issuer", tooManySubjectsStatementBytes)
assert.NoError(t, err)

verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
assert.NoError(t, err)

artifact := "Hi, I am an artifact!" //nolint:goconst
_, err = verifier.Verify(tooManySubjectsEntity, verify.NewPolicy(verify.WithArtifact(bytes.NewBufferString(artifact)), verify.WithoutIdentitiesUnsafe()))
assert.ErrorContains(t, err, "too many digests")
}
7 changes: 7 additions & 0 deletions pkg/verify/tlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"github.com/sigstore/sigstore-go/pkg/util"
)

const maxAllowedTlogEntries = 32

// VerifyArtifactTransparencyLog verifies that the given entity has been logged
// in the transparency log and that the log entry is valid.
//
Expand All @@ -47,6 +49,11 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru
return nil, err
}

// limit the number of tlog entries to prevent DoS
if len(entries) > maxAllowedTlogEntries {
return nil, fmt.Errorf("too many tlog entries: %d > %d", len(entries), maxAllowedTlogEntries)
}

// disallow duplicate entries, as a malicious actor could use duplicates to bypass the threshold
for i := 0; i < len(entries); i++ {
for j := i + 1; j < len(entries); j++ {
Expand Down
30 changes: 29 additions & 1 deletion pkg/verify/tlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,33 @@ func TestDuplicateTlogEntries(t *testing.T) {
assert.NoError(t, err)

_, err = verify.VerifyArtifactTransparencyLog(&dupTlogEntity{entity}, virtualSigstore, 1, true, false)
assert.Error(t, err) // duplicate tlog entries should fail to verify
assert.ErrorContains(t, err, "duplicate tlog entries found") // duplicate tlog entries should fail to verify
}

type tooManyTlogEntriesEntity struct {
*ca.TestEntity
}

func (e *tooManyTlogEntriesEntity) TlogEntries() ([]*tlog.Entry, error) {
entries, err := e.TestEntity.TlogEntries()
if err != nil {
return nil, err
}
for i := 0; i < 32; i++ {
entries = append(entries, entries[0])
}

return entries, nil
}

func TestMaxAllowedTlogEntries(t *testing.T) {
virtualSigstore, err := ca.NewVirtualSigstore()
assert.NoError(t, err)

statement := []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`)
entity, err := virtualSigstore.Attest("foo@example.com", "issuer", statement)
assert.NoError(t, err)

_, err = verify.VerifyArtifactTransparencyLog(&tooManyTlogEntriesEntity{entity}, virtualSigstore, 1, true, false)
assert.ErrorContains(t, err, "too many tlog entries") // too many tlog entries should fail to verify
}
7 changes: 7 additions & 0 deletions pkg/verify/tsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"github.com/sigstore/sigstore-go/pkg/root"
)

const maxAllowedTimestamps = 32

// VerifyTimestampAuthority verifies that the given entity has been timestamped
// by a trusted timestamp authority and that the timestamp is valid.
func VerifyTimestampAuthority(entity SignedEntity, trustedMaterial root.TrustedMaterial) ([]time.Time, error) { //nolint:revive
Expand All @@ -34,6 +36,11 @@ func VerifyTimestampAuthority(entity SignedEntity, trustedMaterial root.TrustedM
return nil, err
}

// limit the number of timestamps to prevent DoS
if len(signedTimestamps) > maxAllowedTimestamps {
return nil, fmt.Errorf("too many signed timestamps: %d > %d", len(signedTimestamps), maxAllowedTimestamps)
}

// disallow duplicate timestamps, as a malicious actor could use duplicates to bypass the threshold
for i := 0; i < len(signedTimestamps); i++ {
for j := i + 1; j < len(signedTimestamps); j++ {
Expand Down
30 changes: 29 additions & 1 deletion pkg/verify/tsa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TestDuplicateTimestamps(t *testing.T) {
assert.NoError(t, err)

_, err = verify.VerifyTimestampAuthorityWithThreshold(&dupTimestampEntity{entity}, virtualSigstore, 1)
assert.Error(t, err) // duplicate timestamps should fail to verify
assert.ErrorContains(t, err, "duplicate timestamps found")
}

type badTSASignatureEntity struct {
Expand Down Expand Up @@ -223,3 +223,31 @@ func TestBadTSACertificateChainOutsideValidityPeriod(t *testing.T) {
})
}
}

type tooManyTimestampsEntity struct {
*ca.TestEntity
}

func (e *tooManyTimestampsEntity) Timestamps() ([][]byte, error) {
timestamps, err := e.TestEntity.Timestamps()
if err != nil {
return nil, err
}

for i := 0; i < 32; i++ {
timestamps = append(timestamps, timestamps[0])
}

return timestamps, nil
}

func TestTooManyTimestamps(t *testing.T) {
virtualSigstore, err := ca.NewVirtualSigstore()
assert.NoError(t, err)

entity, err := virtualSigstore.Attest("foo@example.com", "issuer", []byte("statement"))
assert.NoError(t, err)

_, err = verify.VerifyTimestampAuthorityWithThreshold(&tooManyTimestampsEntity{entity}, virtualSigstore, 1)
assert.ErrorContains(t, err, "too many signed timestamps")
}

0 comments on commit 01e70e8

Please sign in to comment.