diff --git a/cmd/conformance/main.go b/cmd/conformance/main.go index 18f376c0..0d17e347 100644 --- a/cmd/conformance/main.go +++ b/cmd/conformance/main.go @@ -237,7 +237,7 @@ func main() { // Check bundle and trusted root for Tlog information if len(tr.TlogAuthorities()) > 0 && b.HasInclusionPromise() { - verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1)) + verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) } sev, err := verify.NewSignedEntityVerifier(tr, verifierConfig...) diff --git a/cmd/sigstore-go/main.go b/cmd/sigstore-go/main.go index 3e6ef65c..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)") @@ -105,6 +109,14 @@ func run() error { 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 89df1f56..67e03ab1 100644 --- a/examples/oci-image-verification/main.go +++ b/examples/oci-image-verification/main.go @@ -121,6 +121,7 @@ func run() error { verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1)) + // TODO: Add flag for allowing observer timestamp once merged if *requireTSA { verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1)) } diff --git a/pkg/verify/signature_test.go b/pkg/verify/signature_test.go index a565e851..cff81500 100644 --- a/pkg/verify/signature_test.go +++ b/pkg/verify/signature_test.go @@ -69,7 +69,7 @@ func TestEnvelopeSubject(t *testing.T) { entity, err := virtualSigstore.Attest("foo@example.com", "issuer", statement) assert.NoError(t, err) - verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1)) + verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1), verify.WithSignedTimestamps(1)) assert.NoError(t, err) _, err = verifier.Verify(entity, SkipArtifactAndIdentitiesPolicy) @@ -98,7 +98,7 @@ func TestSignatureVerifierMessageSignature(t *testing.T) { entity, err := virtualSigstore.Sign("foofighters@example.com", "issuer", []byte(artifact)) assert.NoError(t, err) - verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1)) + verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) assert.NoError(t, err) result, err := verifier.Verify(entity, verify.NewPolicy(verify.WithArtifact(bytes.NewBufferString(artifact)), verify.WithoutIdentitiesUnsafe())) diff --git a/pkg/verify/signed_entity.go b/pkg/verify/signed_entity.go index bb3efc10..b865d01e 100644 --- a/pkg/verify/signed_entity.go +++ b/pkg/verify/signed_entity.go @@ -35,13 +35,40 @@ type SignedEntityVerifier struct { } type VerifierConfig struct { // nolint: revive - performOnlineVerification bool - weExpectSignedTimestamps bool - signedTimestampThreshold int - weExpectTlogEntries bool - tlogEntriesThreshold int - weExpectSCTs bool - ctlogEntriesThreshold int + // performOnlineVerification queries logs during verification. + // Default is offline + performOnlineVerification bool + // weExpectSignedTimestamps requires RFC3161 timestamps to verify + // short-lived certificates + weExpectSignedTimestamps bool + // signedTimestampThreshold is the minimum number of verified + // RFC3161 timestamps in a bundle + signedTimestampThreshold int + // requireIntegratedTimestamps requires log entry integrated timestamps to + // verify short-lived certificates + requireIntegratedTimestamps bool + // integratedTimeThreshold is the minimum number of log entry + // integrated timestamps in a bundle + integratedTimeThreshold int + // requireObserverTimestamps requires RFC3161 timestamps and/or log + // integrated timestamps to verify short-lived certificates + requireObserverTimestamps bool + // observerTimestampThreshold is the minimum number of verified + // RFC3161 timestamps and/or log integrated timestamps in a bundle + observerTimestampThreshold int + // weExpectTlogEntries requires log inclusion proofs in a bundle + weExpectTlogEntries bool + // tlogEntriesThreshold is the minimum number of verified inclusion + // proofs in a bundle + tlogEntriesThreshold int + // weExpectSCTs requires SCTs in Fulcio certificates + weExpectSCTs bool + // ctlogEntriesTreshold is the minimum number of verified SCTs in + // a Fulcio certificate + ctlogEntriesThreshold int + // weDoNotExpectAnyObserverTimestamps uses the certificate's lifetime + // rather than a provided signed or log timestamp. Most workflows will + // not use this option weDoNotExpectAnyObserverTimestamps bool } @@ -104,10 +131,25 @@ func WithSignedTimestamps(threshold int) VerifierOption { } } +// WithObserverTimestamps configures the SignedEntityVerifier to expect +// timestamps from either an RFC3161 timestamp authority or a log's +// SignedEntryTimestamp. These are verified using the TrustedMaterial's +// TSACertificateAuthorities() or TlogAuthorities(), and used to verify +// the Fulcio certificate. +func WithObserverTimestamps(threshold int) VerifierOption { + return func(c *VerifierConfig) error { + if threshold < 1 { + return errors.New("observer timestamp threshold must be at least 1") + } + c.requireObserverTimestamps = true + c.observerTimestampThreshold = threshold + return nil + } +} + // WithTransparencyLog configures the SignedEntityVerifier to expect -// Transparency Log entries, verify them using the TrustedMaterial's -// TlogAuthorities(), and, if it exists, use the resulting Inclusion timestamp(s) -// to verify the Fulcio certificate. +// Transparency Log inclusion proofs or SignedEntryTimestamps, verifying them +// using the TrustedMaterial's TlogAuthorities(). func WithTransparencyLog(threshold int) VerifierOption { return func(c *VerifierConfig) error { if threshold < 1 { @@ -119,6 +161,17 @@ func WithTransparencyLog(threshold int) VerifierOption { } } +// WithIntegratedTimestamps configures the SignedEntityVerifier to +// expect log entry integrated timestamps from either SignedEntryTimestamps +// or live log lookups. +func WithIntegratedTimestamps(threshold int) VerifierOption { + return func(c *VerifierConfig) error { + c.requireIntegratedTimestamps = true + c.integratedTimeThreshold = threshold + return nil + } +} + // WithSignedCertificateTimestamps configures the SignedEntityVerifier to // expect the Fulcio certificate to have a SignedCertificateTimestamp, and // verify it using the TrustedMaterial's CTLogAuthorities(). @@ -150,8 +203,9 @@ func WithoutAnyObserverTimestampsInsecure() VerifierOption { } func (c *VerifierConfig) Validate() error { - if !c.weExpectSignedTimestamps && !c.weExpectTlogEntries && !c.weDoNotExpectAnyObserverTimestamps { - return errors.New("when initializing a new SignedEntityVerifier, you must specify at least one, or both, of WithSignedTimestamps() or WithTransparencyLog()") + if !c.requireObserverTimestamps && !c.weExpectSignedTimestamps && !c.requireIntegratedTimestamps && !c.weDoNotExpectAnyObserverTimestamps { + return errors.New("when initializing a new SignedEntityVerifier, you must specify at least one of " + + "WithObserverTimestamps(), WithSignedTimestamps(), WithIntegratedTimestamps(), or WithoutAnyObserverTimestampsInsecure()") } return nil @@ -416,11 +470,16 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V return nil, fmt.Errorf("failed to build policy: %w", err) } - // Let's go by the spec: https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit#heading=h.msyyz1cr5bcs + // Let's go by the spec: https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit#heading=h.g11ovq2s1jxh + // > ## Transparency Log Entry + verifiedTlogTimestamps, err := v.VerifyTransparencyLogInclusion(entity) + if err != nil { + return nil, fmt.Errorf("failed to verify log inclusion: %w", err) + } + // > ## Establishing a Time for the Signature // > First, establish a time for the signature. This timestamp is required to validate the certificate chain, so this step comes first. - - verifiedTimestamps, err := v.VerifyObserverTimestamps(entity) + verifiedTimestamps, err := v.VerifyObserverTimestamps(entity, verifiedTlogTimestamps) if err != nil { return nil, fmt.Errorf("failed to verify timestamps: %w", err) } @@ -547,35 +606,75 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V return result, 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. +// VerifyTransparencyLogInclusion verifies TlogEntries if expected. Optionally returns +// a list of verified timestamps from the log integrated timestamps when verifying +// with observer timestamps. +// TODO: Return a different verification result for logs specifically (also for #48) +func (v *SignedEntityVerifier) VerifyTransparencyLogInclusion(entity SignedEntity) ([]TimestampVerificationResult, error) { + verifiedTimestamps := []TimestampVerificationResult{} + + if v.config.weExpectTlogEntries { + // log timestamps should be verified if with WithIntegratedTimestamps or WithObserverTimestamps is used + verifiedTlogTimestamps, err := VerifyArtifactTransparencyLog(entity, v.trustedMaterial, v.config.tlogEntriesThreshold, + v.config.requireIntegratedTimestamps || v.config.requireObserverTimestamps, v.config.performOnlineVerification) + if err != nil { + return nil, err + } + + for _, vts := range verifiedTlogTimestamps { + verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "Tlog", URI: "TODO", Timestamp: vts}) + } + } + + return verifiedTimestamps, nil +} + +// 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". -func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity) ([]TimestampVerificationResult, error) { +func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, logTimestamps []TimestampVerificationResult) ([]TimestampVerificationResult, error) { verifiedTimestamps := []TimestampVerificationResult{} // From spec: // > … if verification or timestamp parsing fails, the Verifier MUST abort if v.config.weExpectSignedTimestamps { - verifiedSignedTimestamps, err := VerifyTimestampAuthority(entity, v.trustedMaterial, v.config.signedTimestampThreshold) + verifiedSignedTimestamps, err := VerifyTimestampAuthorityWithThreshold(entity, v.trustedMaterial, v.config.signedTimestampThreshold) if err != nil { return nil, err } - for _, vts := range verifiedSignedTimestamps { verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "TimestampAuthority", URI: "TODO", Timestamp: vts}) } } - if v.config.weExpectTlogEntries { - verifiedTlogTimestamps, err := VerifyArtifactTransparencyLog(entity, v.trustedMaterial, v.config.tlogEntriesThreshold, v.config.performOnlineVerification) + if v.config.requireIntegratedTimestamps { + 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 { + verifiedSignedTimestamps, err := VerifyTimestampAuthority(entity, v.trustedMaterial) if err != nil { return nil, err } - for _, vts := range verifiedTlogTimestamps { - verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "Tlog", URI: "TODO", Timestamp: vts}) + // check threshold for both RFC3161 and log timestamps + tsCount := len(verifiedSignedTimestamps) + len(logTimestamps) + if tsCount < v.config.observerTimestampThreshold { + return nil, fmt.Errorf("threshold not met for verified signed & log entry integrated timestamps: %d < %d", + tsCount, v.config.observerTimestampThreshold) + } + + // append all timestamps + verifiedTimestamps = append(verifiedTimestamps, logTimestamps...) + for _, vts := range verifiedSignedTimestamps { + verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "TimestampAuthority", URI: "TODO", Timestamp: vts}) } } diff --git a/pkg/verify/signed_entity_test.go b/pkg/verify/signed_entity_test.go index e3617500..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 @@ -61,11 +80,11 @@ func TestEntitySignedByPublicGoodWithTlogVerifiesSuccessfully(t *testing.T) { tr := data.PublicGoodTrustedMaterialRoot(t) entity := data.SigstoreJS200ProvenanceBundle(t) - v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1)) - assert.Nil(t, err) + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + 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) } @@ -92,12 +118,55 @@ func TestEntitySignedByPublicGoodWithHighTlogThresholdFails(t *testing.T) { tr := data.PublicGoodTrustedMaterialRoot(t) entity := data.SigstoreJS200ProvenanceBundle(t) - v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(2)) - assert.Nil(t, err) + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(2), verify.WithObserverTimestamps(1)) + 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: @@ -118,7 +205,7 @@ func TestVerifyPolicyOptionErors(t *testing.T) { tr := data.PublicGoodTrustedMaterialRoot(t) entity := data.SigstoreJS200ProvenanceBundle(t) - verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1)) + verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) assert.Nil(t, err) goodCertID, err := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", "", verify.SigstoreSanRegex) @@ -200,7 +287,7 @@ func TestEntitySignedByPublicGoodWithCertificateIdentityVerifiesSuccessfully(t * goodCI, _ := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", "", verify.SigstoreSanRegex) badCI, _ := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "BadSANValue", "", "") - verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1)) + verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) assert.Nil(t, err) @@ -248,7 +335,7 @@ func TestThatAllTheJSONKeysStartWithALowerCase(t *testing.T) { tr := data.PublicGoodTrustedMaterialRoot(t) entity := data.SigstoreJS200ProvenanceBundle(t) - verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1)) + verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) assert.Nil(t, err) res, err := verifier.Verify(entity, SkipArtifactAndIdentitiesPolicy) diff --git a/pkg/verify/tlog.go b/pkg/verify/tlog.go index f1f843f0..1414cc54 100644 --- a/pkg/verify/tlog.go +++ b/pkg/verify/tlog.go @@ -41,7 +41,7 @@ import ( // that must be verified. // // If online is true, the log entry is verified against the Rekor server. -func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.TrustedMaterial, threshold int, online bool) ([]time.Time, error) { //nolint:revive +func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.TrustedMaterial, logThreshold int, trustIntegratedTime, online bool) ([]time.Time, error) { //nolint:revive entries, err := entity.TlogEntries() if err != nil { return nil, err @@ -69,6 +69,7 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru } verifiedTimestamps := []time.Time{} + logEntriesVerified := 0 for _, entry := range entries { err := tlog.ValidateEntry(entry) @@ -86,6 +87,9 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru // skip entries the trust root cannot verify continue } + if trustIntegratedTime { + verifiedTimestamps = append(verifiedTimestamps, entry.IntegratedTime()) + } } if entity.HasInclusionProof() { keyID := entry.LogKeyID() @@ -105,8 +109,8 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru if err != nil { return nil, err } + // DO NOT use timestamp with only an inclusion proof, because it is not signed metadata } - verifiedTimestamps = append(verifiedTimestamps, entry.IntegratedTime()) } else { keyID := entry.LogKeyID() hex64Key := hex.EncodeToString([]byte(keyID)) @@ -127,6 +131,7 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru logIndex := entry.LogIndex() + // TODO(issue#52): Change to GetLogEntryByIndex searchParams := rekorEntries.NewSearchLogQueryParams() searchLogQuery := rekorModels.SearchLogQuery{} searchLogQuery.LogIndexes = []*int64{&logIndex} @@ -152,9 +157,10 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru return nil, err } } - verifiedTimestamps = append(verifiedTimestamps, entry.IntegratedTime()) + if trustIntegratedTime { + verifiedTimestamps = append(verifiedTimestamps, entry.IntegratedTime()) + } } - // Ensure entry signature matches signature from bundle if !bytes.Equal(entry.Signature(), entitySignature) { return nil, errors.New("transparency log signature does not match") @@ -171,10 +177,13 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru if !verificationContent.ValidAtTime(entry.IntegratedTime(), trustedMaterial) { return nil, errors.New("integrated time outside certificate validity") } + + // successful log entry verification + logEntriesVerified++ } - if len(verifiedTimestamps) < threshold { - return nil, fmt.Errorf("not enough verified timestamps from transparency log entries: %d < %d", len(verifiedTimestamps), threshold) + if logEntriesVerified < logThreshold { + return nil, fmt.Errorf("not enough verified log entries from transparency log: %d < %d", logEntriesVerified, logThreshold) } return verifiedTimestamps, nil diff --git a/pkg/verify/tlog_test.go b/pkg/verify/tlog_test.go index 9e4efe7f..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,13 +35,21 @@ func TestTlogVerifier(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", statement) assert.NoError(t, err) - _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, 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) - _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore2, 1, false) + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore2, 1, true, false) assert.Error(t, err) // different sigstore instance should fail to verify // Attempt to use tlog with integrated time outside certificate validity. @@ -50,7 +59,7 @@ func TestTlogVerifier(t *testing.T) { entity, err = virtualSigstore.AttestAtTime("foo@fighters.com", "issuer", statement, time.Now().Add(30*time.Minute)) assert.NoError(t, err) - _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, false) + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false) assert.Error(t, err) } @@ -87,11 +96,11 @@ func TestIgnoredTLogEntries(t *testing.T) { assert.NoError(t, err) // success: entry that cannot be verified is ignored - _, err = verify.VerifyArtifactTransparencyLog(&oneTrustedOneUntrustedLogEntry{entity, untrustedEntity}, virtualSigstore, 1, false) + _, err = verify.VerifyArtifactTransparencyLog(&oneTrustedOneUntrustedLogEntry{entity, untrustedEntity}, virtualSigstore, 1, true, false) assert.NoError(t, err) // failure: threshold of 2 is not met since 1 untrusted entry is ignored - _, err = verify.VerifyArtifactTransparencyLog(&oneTrustedOneUntrustedLogEntry{entity, untrustedEntity}, virtualSigstore, 2, false) + _, err = verify.VerifyArtifactTransparencyLog(&oneTrustedOneUntrustedLogEntry{entity, untrustedEntity}, virtualSigstore, 2, true, false) assert.Error(t, err) } @@ -129,7 +138,7 @@ func TestInvalidTLogEntries(t *testing.T) { assert.NoError(t, err) // failure: threshold of 1 is not met with invalid entry - _, err = verify.VerifyArtifactTransparencyLog(&invalidTLogEntity{entity}, virtualSigstore, 1, false) + _, err = verify.VerifyArtifactTransparencyLog(&invalidTLogEntity{entity}, virtualSigstore, 1, true, false) assert.Error(t, err) if err.Error() != "entry must contain an inclusion proof and/or promise" { t.Errorf("expected error with missing proof/promises, got: %v", err.Error()) @@ -153,9 +162,9 @@ func TestNoTLogEntries(t *testing.T) { assert.NoError(t, err) // failure: threshold of 1 is not met with no entries - _, err = verify.VerifyArtifactTransparencyLog(&noTLogEntity{entity}, virtualSigstore, 1, false) + _, err = verify.VerifyArtifactTransparencyLog(&noTLogEntity{entity}, virtualSigstore, 1, true, false) assert.Error(t, err) - if !strings.Contains(err.Error(), "not enough verified timestamps from transparency log") { + if !strings.Contains(err.Error(), "not enough verified log entries from transparency log") { t.Errorf("expected error with timestamp threshold, got: %v", err.Error()) } } @@ -181,6 +190,6 @@ func TestDuplicateTlogEntries(t *testing.T) { entity, err := virtualSigstore.Attest("foofighters@example.com", "issuer", statement) assert.NoError(t, err) - _, err = verify.VerifyArtifactTransparencyLog(&dupTlogEntity{entity}, virtualSigstore, 1, false) + _, err = verify.VerifyArtifactTransparencyLog(&dupTlogEntity{entity}, virtualSigstore, 1, true, false) assert.Error(t, err) // duplicate tlog entries should fail to verify } diff --git a/pkg/verify/tsa.go b/pkg/verify/tsa.go index 687a88c9..e6ed48fa 100644 --- a/pkg/verify/tsa.go +++ b/pkg/verify/tsa.go @@ -28,10 +28,7 @@ import ( // VerifyTimestampAuthority verifies that the given entity has been timestamped // by a trusted timestamp authority and that the timestamp is valid. -// -// The threshold parameter is the number of unique timestamps that must be -// verified. -func VerifyTimestampAuthority(entity SignedEntity, trustedMaterial root.TrustedMaterial, threshold int) ([]time.Time, error) { //nolint:revive +func VerifyTimestampAuthority(entity SignedEntity, trustedMaterial root.TrustedMaterial) ([]time.Time, error) { //nolint:revive signedTimestamps, err := entity.Timestamps() if err != nil { return nil, err @@ -70,10 +67,22 @@ func VerifyTimestampAuthority(entity SignedEntity, trustedMaterial root.TrustedM verifiedTimestamps = append(verifiedTimestamps, verifiedSignedTimestamp) } + return verifiedTimestamps, nil +} + +// VerifyTimestampAuthority verifies that the given entity has been timestamped +// by a trusted timestamp authority and that the timestamp is valid. +// +// The threshold parameter is the number of unique timestamps that must be +// verified. +func VerifyTimestampAuthorityWithThreshold(entity SignedEntity, trustedMaterial root.TrustedMaterial, threshold int) ([]time.Time, error) { //nolint:revive + verifiedTimestamps, err := VerifyTimestampAuthority(entity, trustedMaterial) + if err != nil { + return nil, err + } if len(verifiedTimestamps) < threshold { - return nil, fmt.Errorf("not enough verified timestamps: %d < %d", len(verifiedTimestamps), threshold) + return nil, fmt.Errorf("threshold not met for verified signed timestamps: %d < %d", len(verifiedTimestamps), threshold) } - return verifiedTimestamps, nil } diff --git a/pkg/verify/tsa_test.go b/pkg/verify/tsa_test.go index 37061632..6d37b1e3 100644 --- a/pkg/verify/tsa_test.go +++ b/pkg/verify/tsa_test.go @@ -31,25 +31,48 @@ func TestTimestampAuthorityVerifier(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", []byte("statement")) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(entity, virtualSigstore, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(entity, virtualSigstore, 1) assert.NoError(t, err) virtualSigstore2, err := ca.NewVirtualSigstore() assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(entity, virtualSigstore2, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(entity, virtualSigstore2, 1) assert.Error(t, err) // different sigstore instance should fail to verify untrustedEntity, err := virtualSigstore2.Attest("foo@fighters.com", "issuer", []byte("statement")) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(&oneTrustedOneUntrustedTimestampEntity{entity, untrustedEntity}, virtualSigstore, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(&oneTrustedOneUntrustedTimestampEntity{entity, untrustedEntity}, virtualSigstore, 1) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(&oneTrustedOneUntrustedTimestampEntity{entity, untrustedEntity}, virtualSigstore, 2) + _, err = verify.VerifyTimestampAuthorityWithThreshold(&oneTrustedOneUntrustedTimestampEntity{entity, untrustedEntity}, virtualSigstore, 2) 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 @@ -89,7 +112,7 @@ func TestDuplicateTimestamps(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", []byte("statement")) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(&dupTimestampEntity{entity}, virtualSigstore, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(&dupTimestampEntity{entity}, virtualSigstore, 1) assert.Error(t, err) // duplicate timestamps should fail to verify } @@ -108,7 +131,7 @@ func TestBadTSASignature(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", []byte("statement")) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(&badTSASignatureEntity{entity}, virtualSigstore, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(&badTSASignatureEntity{entity}, virtualSigstore, 1) assert.Error(t, err) } @@ -141,7 +164,7 @@ func TestBadTSACertificateChain(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", []byte("statement")) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(entity, &customTSAChainTrustedMaterial{VirtualSigstore: virtualSigstore, tsaChain: []root.CertificateAuthority{badChain}}, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(entity, &customTSAChainTrustedMaterial{VirtualSigstore: virtualSigstore, tsaChain: []root.CertificateAuthority{badChain}}, 1) assert.Error(t, err) } @@ -191,7 +214,7 @@ func TestBadTSACertificateChainOutsideValidityPeriod(t *testing.T) { entity, err := virtualSigstore.Attest("foo@fighters.com", "issuer", []byte("statement")) assert.NoError(t, err) - _, err = verify.VerifyTimestampAuthority(entity, &customTSAChainTrustedMaterial{VirtualSigstore: virtualSigstore, tsaChain: []root.CertificateAuthority{test.ca}}, 1) + _, err = verify.VerifyTimestampAuthorityWithThreshold(entity, &customTSAChainTrustedMaterial{VirtualSigstore: virtualSigstore, tsaChain: []root.CertificateAuthority{test.ca}}, 1) if test.err { assert.Error(t, err) } else {