diff --git a/cmd/sigstore-go/main.go b/cmd/sigstore-go/main.go index ff34c330..380f4cb7 100644 --- a/cmd/sigstore-go/main.go +++ b/cmd/sigstore-go/main.go @@ -41,6 +41,8 @@ var expectedOIDIssuer *string var expectedSAN *string var expectedSANRegex *string var requireTSA *bool +var requireIntegratedTs *bool +var requireTimestamp *bool var requireTlog *bool var minBundleVersion *string var onlineTlog *bool @@ -57,6 +59,8 @@ func init() { expectedSAN = flag.String("expectedSAN", "", "The expected identity in the signing certificate's SAN extension") expectedSANRegex = flag.String("expectedSANRegex", "", "The expected identity in the signing certificate's SAN extension") requireTSA = flag.Bool("requireTSA", false, "Require RFC 3161 signed timestamp") + requireIntegratedTs = flag.Bool("requireIntegratedTs", false, "Require log entry integrated timestamp") + requireTimestamp = flag.Bool("requireTimestamp", false, "Require either an RFC3161 signed timestamp or log entry integrated timestamp") requireTlog = flag.Bool("requireTlog", true, "Require Artifact Transparency log entry (Rekor)") minBundleVersion = flag.String("minBundleVersion", "", "Minimum acceptable bundle version (e.g. '0.1')") onlineTlog = flag.Bool("onlineTlog", false, "Verify Artifact Transparency log entry online (Rekor)") @@ -101,11 +105,18 @@ func run() error { verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1)) - // TODO: Add flag for requiring any timestamp if *requireTSA { verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1)) } + if *requireIntegratedTs { + verifierConfig = append(verifierConfig, verify.WithIntegratedTimestamps(1)) + } + + if *requireTimestamp { + verifierConfig = append(verifierConfig, verify.WithObserverTimestamps(1)) + } + if *requireTlog { verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1)) } diff --git a/examples/oci-image-verification/main.go b/examples/oci-image-verification/main.go index 0db9af77..67e03ab1 100644 --- a/examples/oci-image-verification/main.go +++ b/examples/oci-image-verification/main.go @@ -121,7 +121,7 @@ func run() error { verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1)) - // TODO: Add flag for requiring any timestamp + // TODO: Add flag for allowing observer timestamp once merged if *requireTSA { verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1)) } diff --git a/pkg/verify/signed_entity.go b/pkg/verify/signed_entity.go index 1675702a..b865d01e 100644 --- a/pkg/verify/signed_entity.go +++ b/pkg/verify/signed_entity.go @@ -629,12 +629,13 @@ func (v *SignedEntityVerifier) VerifyTransparencyLogInclusion(entity SignedEntit return verifiedTimestamps, nil } -// VerifyObserverTimestamps verifies TlogEntries and SignedTimestamps, if we -// expect them, and returns a slice of verified results, which embed the actual -// time.Time value. This value can then be used to verify certificates, if any. +// VerifyObserverTimestamps verifies RFC3161 signed timestamps, and verifies +// that timestamp thresholds are met with log entry integrated timestamps, +// signed timestamps, or a combination of both. The returned timestamps +// can be used to verify short-lived certificates. +// logTimestamps may be populated with verified log entry integrated timestamps // In order to be verifiable, a SignedEntity must have at least one verified // "observer timestamp". -// TODO: Update comment saying logTimestamps is populated when observertimestamps are used func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, logTimestamps []TimestampVerificationResult) ([]TimestampVerificationResult, error) { verifiedTimestamps := []TimestampVerificationResult{} @@ -654,6 +655,7 @@ func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, log if len(logTimestamps) < v.config.integratedTimeThreshold { return nil, fmt.Errorf("threshold not met for verified log entry integrated timestamps: %d < %d", len(logTimestamps), v.config.integratedTimeThreshold) } + verifiedTimestamps = append(verifiedTimestamps, logTimestamps...) } if v.config.requireObserverTimestamps { diff --git a/pkg/verify/signed_entity_test.go b/pkg/verify/signed_entity_test.go index ad3003cf..2477368d 100644 --- a/pkg/verify/signed_entity_test.go +++ b/pkg/verify/signed_entity_test.go @@ -52,6 +52,25 @@ func TestSignedEntityVerifierInitialization(t *testing.T) { assert.Error(t, err) } +func TestSignedEntityVerifierInitRequiresTimestamp(t *testing.T) { + tr := data.PublicGoodTrustedMaterialRoot(t) + + _, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1)) + assert.Error(t, err) + if !strings.Contains(err.Error(), "you must specify at least one of") { + t.Errorf("expected error missing timestamp verifier, got: %v", err) + } + + _, err = verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(1)) + assert.NoError(t, err) + _, err = verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithSignedTimestamps(1)) + assert.NoError(t, err) + _, err = verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + assert.NoError(t, err) + _, err = verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithoutAnyObserverTimestampsInsecure()) + assert.NoError(t, err) +} + // Testing a bundle: // - signed by public good // - one tlog entry @@ -62,10 +81,10 @@ func TestEntitySignedByPublicGoodWithTlogVerifiesSuccessfully(t *testing.T) { entity := data.SigstoreJS200ProvenanceBundle(t) v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) - assert.Nil(t, err) + assert.NoError(t, err) res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, res) assert.NotNil(t, res.Statement) @@ -74,6 +93,13 @@ func TestEntitySignedByPublicGoodWithTlogVerifiesSuccessfully(t *testing.T) { assert.NotNil(t, res.Signature.Certificate) assert.Equal(t, "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main", res.Signature.Certificate.SubjectAlternativeName.Value) assert.NotEmpty(t, res.VerifiedTimestamps) + + // verifies with integrated timestamp threshold too + v, err = verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(1)) + assert.NoError(t, err) + res, err = v.Verify(entity, SkipArtifactAndIdentitiesPolicy) + assert.NoError(t, err) + assert.NotNil(t, res) } func TestEntitySignedByPublicGoodWithoutTimestampsVerifiesSuccessfully(t *testing.T) { @@ -81,10 +107,10 @@ func TestEntitySignedByPublicGoodWithoutTimestampsVerifiesSuccessfully(t *testin entity := data.SigstoreJS200ProvenanceBundle(t) v, err := verify.NewSignedEntityVerifier(tr, verify.WithoutAnyObserverTimestampsInsecure()) - assert.Nil(t, err) + assert.NoError(t, err) res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, res) } @@ -93,11 +119,54 @@ func TestEntitySignedByPublicGoodWithHighTlogThresholdFails(t *testing.T) { entity := data.SigstoreJS200ProvenanceBundle(t) v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(2), verify.WithObserverTimestamps(1)) - assert.Nil(t, err) + assert.NoError(t, err) res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) - assert.NotNil(t, err) + assert.Error(t, err) + assert.Nil(t, res) + if !strings.Contains(err.Error(), "not enough verified log entries from transparency log") { + t.Errorf("expected error not meeting log entry threshold, got: %v", err) + } +} + +func TestEntitySignedByPublicGoodWithoutVerifyingLogEntryFails(t *testing.T) { + tr := data.PublicGoodTrustedMaterialRoot(t) + entity := data.SigstoreJS200ProvenanceBundle(t) + + v, err := verify.NewSignedEntityVerifier(tr, verify.WithObserverTimestamps(1)) + assert.NoError(t, err) + + res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) + assert.Error(t, err) + assert.Nil(t, res) + if !strings.Contains(err.Error(), "threshold not met for verified signed & log entry integrated timestamps") { + t.Errorf("expected error not meeting timestamp threshold without entry verification, got: %v", err) + } + + // also fails trying to use integrated timestamps without verifying the log + v, err = verify.NewSignedEntityVerifier(tr, verify.WithIntegratedTimestamps(1)) + assert.NoError(t, err) + res, err = v.Verify(entity, SkipArtifactAndIdentitiesPolicy) + assert.Error(t, err) + assert.Nil(t, res) + if !strings.Contains(err.Error(), "threshold not met for verified log entry integrated timestamps") { + t.Errorf("expected error not meeting integrated timestamp threshold without entry verification, got: %v", err) + } +} + +func TestEntitySignedByPublicGoodWithHighLogTimestampThresholdFails(t *testing.T) { + tr := data.PublicGoodTrustedMaterialRoot(t) + entity := data.SigstoreJS200ProvenanceBundle(t) + + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(2)) + assert.NoError(t, err) + + res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) + assert.Error(t, err) assert.Nil(t, res) + if !strings.Contains(err.Error(), "threshold not met for verified log entry integrated timestamps") { + t.Errorf("expected error not meeting log entry integrated timestamp threshold, got: %v", err) + } } func TestEntitySignedByPublicGoodExpectingTSAFails(t *testing.T) { @@ -105,11 +174,29 @@ func TestEntitySignedByPublicGoodExpectingTSAFails(t *testing.T) { entity := data.SigstoreJS200ProvenanceBundle(t) v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithSignedTimestamps(1)) - assert.Nil(t, err) + assert.NoError(t, err) res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) - assert.NotNil(t, err) + assert.Error(t, err) + assert.Nil(t, res) + if !strings.Contains(err.Error(), "threshold not met for verified signed timestamps") { + t.Errorf("expected error not meeting signed timestamp threshold, got: %v", err) + } +} + +func TestEntitySignedByPublicGoodWithHighObserverTimestampThresholdFails(t *testing.T) { + tr := data.PublicGoodTrustedMaterialRoot(t) + entity := data.SigstoreJS200ProvenanceBundle(t) + + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(2)) + assert.NoError(t, err) + + res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) + assert.Error(t, err) assert.Nil(t, res) + if !strings.Contains(err.Error(), "threshold not met for verified signed & log entry integrated timestamps") { + t.Errorf("expected error not meeting observer timestamp threshold, got: %v", err) + } } // Now we test policy: diff --git a/pkg/verify/tlog.go b/pkg/verify/tlog.go index 3d948a07..1414cc54 100644 --- a/pkg/verify/tlog.go +++ b/pkg/verify/tlog.go @@ -131,7 +131,7 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru logIndex := entry.LogIndex() - // TODO: Change from search by index to search by hash? + // TODO(issue#52): Change to GetLogEntryByIndex searchParams := rekorEntries.NewSearchLogQueryParams() searchLogQuery := rekorModels.SearchLogQuery{} searchLogQuery.LogIndexes = []*int64{&logIndex} @@ -185,9 +185,6 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru if logEntriesVerified < logThreshold { return nil, fmt.Errorf("not enough verified log entries from transparency log: %d < %d", logEntriesVerified, logThreshold) } - // if len(verifiedTimestamps) < tsThreshold { - // return nil, fmt.Errorf("not enough verified timestamps from transparency log entries: %d < %d", len(verifiedTimestamps), tsThreshold) - // } return verifiedTimestamps, nil } diff --git a/pkg/verify/tlog_test.go b/pkg/verify/tlog_test.go index 3c481863..3ff4845c 100644 --- a/pkg/verify/tlog_test.go +++ b/pkg/verify/tlog_test.go @@ -26,6 +26,7 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO(issue#53): Add unit tests for online log verification and inclusion proofs func TestTlogVerifier(t *testing.T) { virtualSigstore, err := ca.NewVirtualSigstore() assert.NoError(t, err) @@ -34,8 +35,16 @@ func TestTlogVerifier(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", statement) assert.NoError(t, err) - _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false) + var ts []time.Time + ts, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false) + assert.NoError(t, err) + // 1 verified timestamp + assert.Len(t, ts, 1) + + ts, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, false, false) assert.NoError(t, err) + // 0 verified timestamps, since integrated timestamps are ignored + assert.Len(t, ts, 0) virtualSigstore2, err := ca.NewVirtualSigstore() assert.NoError(t, err) diff --git a/pkg/verify/tsa_test.go b/pkg/verify/tsa_test.go index 90ade9c3..6d37b1e3 100644 --- a/pkg/verify/tsa_test.go +++ b/pkg/verify/tsa_test.go @@ -50,6 +50,29 @@ func TestTimestampAuthorityVerifier(t *testing.T) { assert.Error(t, err) // only 1 trusted should not meet threshold of 2 } +func TestTimestampAuthorityVerifierWithoutThreshold(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", []byte("statement")) + assert.NoError(t, err) + + virtualSigstore2, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + var ts []time.Time + + // expect one verified timestamp + ts, err = verify.VerifyTimestampAuthority(entity, virtualSigstore) + assert.NoError(t, err) + assert.Len(t, ts, 1) + + // no failure, but also no verified timestamps + ts, err = verify.VerifyTimestampAuthority(entity, virtualSigstore2) + assert.NoError(t, err) + assert.Empty(t, ts) +} + type oneTrustedOneUntrustedTimestampEntity struct { *ca.TestEntity UntrustedTestEntity *ca.TestEntity