From 98a149fa01d5b19671c53fafe323b2ee7368b408 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Fri, 15 Dec 2023 00:02:10 +0000 Subject: [PATCH 1/6] Don't fail on Rekor entry verification for untrusted entries Like #45, as long as the threshold of expected timestamps is met, then verification should succeed. Otherwise, entries without trust root material should be skipped. One benefit of having the log key ID be used to look up the correct trust root material is that we can still error out if the signature is invalid. Ref: #43 Signed-off-by: Hayden Blauzvern --- pkg/verify/tlog.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg/verify/tlog.go b/pkg/verify/tlog.go index f1f843f0..67a0ac15 100644 --- a/pkg/verify/tlog.go +++ b/pkg/verify/tlog.go @@ -75,6 +75,7 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru if err != nil { return nil, err } + inclusionVerified := false if !online { if !entry.HasInclusionPromise() && !entry.HasInclusionProof() { @@ -152,24 +153,27 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru return nil, err } } + inclusionVerified = true 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") - } + if inclusionVerified { + // Ensure entry signature matches signature from bundle + if !bytes.Equal(entry.Signature(), entitySignature) { + return nil, errors.New("transparency log signature does not match") + } - // Ensure entry certificate matches bundle certificate - if !verificationContent.CompareKey(entry.PublicKey(), trustedMaterial) { - return nil, errors.New("transparency log certificate does not match") - } + // Ensure entry certificate matches bundle certificate + if !verificationContent.CompareKey(entry.PublicKey(), trustedMaterial) { + return nil, errors.New("transparency log certificate does not match") + } - // TODO: if you have access to artifact, check that it matches body subject + // TODO: if you have access to artifact, check that it matches body subject - // Check tlog entry time against bundle certificates - if !verificationContent.ValidAtTime(entry.IntegratedTime(), trustedMaterial) { - return nil, errors.New("integrated time outside certificate validity") + // Check tlog entry time against bundle certificates + if !verificationContent.ValidAtTime(entry.IntegratedTime(), trustedMaterial) { + return nil, errors.New("integrated time outside certificate validity") + } } } From b89050282be3ca1908810fbd3ca046e595816020 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Fri, 15 Dec 2023 21:33:44 +0000 Subject: [PATCH 2/6] Address comments Signed-off-by: Hayden Blauzvern --- pkg/verify/tlog.go | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pkg/verify/tlog.go b/pkg/verify/tlog.go index 67a0ac15..f1f843f0 100644 --- a/pkg/verify/tlog.go +++ b/pkg/verify/tlog.go @@ -75,7 +75,6 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru if err != nil { return nil, err } - inclusionVerified := false if !online { if !entry.HasInclusionPromise() && !entry.HasInclusionProof() { @@ -153,27 +152,24 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru return nil, err } } - inclusionVerified = true verifiedTimestamps = append(verifiedTimestamps, entry.IntegratedTime()) } - if inclusionVerified { - // Ensure entry signature matches signature from bundle - if !bytes.Equal(entry.Signature(), entitySignature) { - return nil, errors.New("transparency log signature does not match") - } + // Ensure entry signature matches signature from bundle + if !bytes.Equal(entry.Signature(), entitySignature) { + return nil, errors.New("transparency log signature does not match") + } - // Ensure entry certificate matches bundle certificate - if !verificationContent.CompareKey(entry.PublicKey(), trustedMaterial) { - return nil, errors.New("transparency log certificate does not match") - } + // Ensure entry certificate matches bundle certificate + if !verificationContent.CompareKey(entry.PublicKey(), trustedMaterial) { + return nil, errors.New("transparency log certificate does not match") + } - // TODO: if you have access to artifact, check that it matches body subject + // TODO: if you have access to artifact, check that it matches body subject - // Check tlog entry time against bundle certificates - if !verificationContent.ValidAtTime(entry.IntegratedTime(), trustedMaterial) { - return nil, errors.New("integrated time outside certificate validity") - } + // Check tlog entry time against bundle certificates + if !verificationContent.ValidAtTime(entry.IntegratedTime(), trustedMaterial) { + return nil, errors.New("integrated time outside certificate validity") } } From a35edb0ca3b14da9b960683c3af731a1ebc2476b Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Mon, 18 Dec 2023 21:00:11 +0000 Subject: [PATCH 3/6] Add option to unify signed and log timestamps This adds the WithObserverTimestamp option to verify that a threshold is met using both RFC3161 signed timestamps and log integrated timestamps. This updates the verification API for the log to only verify log inclusion proofs or SETs, so that log and timestamp verification are not conflated. This also fixes a bug where the integrated time is used with an inclusion proof. This is not safe to do, since the integrated time is not authenticated metadata without a SignedEntryTimestamp signature. Signed-off-by: Hayden Blauzvern --- pkg/verify/signature_test.go | 4 +- pkg/verify/signed_entity.go | 124 +++++++++++++++++++++++++------ pkg/verify/signed_entity_test.go | 10 +-- pkg/verify/tlog.go | 24 ++++-- pkg/verify/tlog_test.go | 18 ++--- pkg/verify/tsa.go | 21 ++++-- pkg/verify/tsa_test.go | 16 ++-- 7 files changed, 159 insertions(+), 58 deletions(-) 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..55650eda 100644 --- a/pkg/verify/signed_entity.go +++ b/pkg/verify/signed_entity.go @@ -35,13 +35,38 @@ 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 + // trustIntegratedTime, when used along with requireObserverTimestamps, + // lets log integrated timestamps count towards the observer timestamp + // threshold + // trustIntegratedTime bool + // 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 +129,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 { @@ -149,10 +189,13 @@ func WithoutAnyObserverTimestampsInsecure() VerifierOption { } } +// TODO: Update with requireObserverTimestamps 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()") + // TODO: Fix up + if !c.weExpectSignedTimestamps && !c.requireObserverTimestamps && !c.weDoNotExpectAnyObserverTimestamps { + return errors.New("when initializing a new SignedEntityVerifier, you must specify at least one, or both, of WithSignedTimestamps() or WithObserverTimestamps()") } + // TODO: require WithTransparencyLog? return nil } @@ -416,11 +459,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 +595,67 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V return result, nil } +// 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 { + verifiedTlogTimestamps, err := VerifyArtifactTransparencyLog(entity, v.trustedMaterial, v.config.tlogEntriesThreshold, 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 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. // In order to be verifiable, a SignedEntity must have at least one verified // "observer timestamp". -func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity) ([]TimestampVerificationResult, error) { +// TODO: Update comment saying logTimestamps is populated when observertimestamps are used +func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, logTimestamps []TimestampVerificationResult) ([]TimestampVerificationResult, error) { verifiedTimestamps := []TimestampVerificationResult{} + fmt.Println(len(logTimestamps)) + // 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.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 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..ad3003cf 100644 --- a/pkg/verify/signed_entity_test.go +++ b/pkg/verify/signed_entity_test.go @@ -61,7 +61,7 @@ func TestEntitySignedByPublicGoodWithTlogVerifiesSuccessfully(t *testing.T) { tr := data.PublicGoodTrustedMaterialRoot(t) entity := data.SigstoreJS200ProvenanceBundle(t) - v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1)) + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) assert.Nil(t, err) res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) @@ -92,7 +92,7 @@ func TestEntitySignedByPublicGoodWithHighTlogThresholdFails(t *testing.T) { tr := data.PublicGoodTrustedMaterialRoot(t) entity := data.SigstoreJS200ProvenanceBundle(t) - v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(2)) + v, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(2), verify.WithObserverTimestamps(1)) assert.Nil(t, err) res, err := v.Verify(entity, SkipArtifactAndIdentitiesPolicy) @@ -118,7 +118,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 +200,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 +248,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..3d948a07 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: Change from search by index to search by hash? 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,11 +177,17 @@ 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) } + // 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 9e4efe7f..3c481863 100644 --- a/pkg/verify/tlog_test.go +++ b/pkg/verify/tlog_test.go @@ -34,13 +34,13 @@ 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) + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false) assert.NoError(t, err) 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 +50,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 +87,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 +129,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 +153,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 +181,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..90ade9c3 100644 --- a/pkg/verify/tsa_test.go +++ b/pkg/verify/tsa_test.go @@ -31,22 +31,22 @@ 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 } @@ -89,7 +89,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 +108,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 +141,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 +191,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 { From 65e1e366f41e5649b8eda1c38a5cf84b208b66ac Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Tue, 19 Dec 2023 00:26:14 +0000 Subject: [PATCH 4/6] Fix conformance test Signed-off-by: Hayden Blauzvern --- cmd/conformance/main.go | 2 +- cmd/sigstore-go/main.go | 1 + examples/oci-image-verification/main.go | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) 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..ff34c330 100644 --- a/cmd/sigstore-go/main.go +++ b/cmd/sigstore-go/main.go @@ -101,6 +101,7 @@ func run() error { verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1)) + // TODO: Add flag for requiring any timestamp if *requireTSA { verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1)) } diff --git a/examples/oci-image-verification/main.go b/examples/oci-image-verification/main.go index 89df1f56..0db9af77 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 requiring any timestamp if *requireTSA { verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1)) } From 1aeef18498126569ed952822365d3491129ef402 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Tue, 19 Dec 2023 20:43:49 +0000 Subject: [PATCH 5/6] Add WithIntegratedTimestamps Signed-off-by: Hayden Blauzvern --- pkg/verify/signed_entity.go | 43 ++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/pkg/verify/signed_entity.go b/pkg/verify/signed_entity.go index 55650eda..1675702a 100644 --- a/pkg/verify/signed_entity.go +++ b/pkg/verify/signed_entity.go @@ -44,10 +44,12 @@ type VerifierConfig struct { // nolint: revive // signedTimestampThreshold is the minimum number of verified // RFC3161 timestamps in a bundle signedTimestampThreshold int - // trustIntegratedTime, when used along with requireObserverTimestamps, - // lets log integrated timestamps count towards the observer timestamp - // threshold - // trustIntegratedTime bool + // 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 @@ -159,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(). @@ -189,13 +202,11 @@ func WithoutAnyObserverTimestampsInsecure() VerifierOption { } } -// TODO: Update with requireObserverTimestamps func (c *VerifierConfig) Validate() error { - // TODO: Fix up - if !c.weExpectSignedTimestamps && !c.requireObserverTimestamps && !c.weDoNotExpectAnyObserverTimestamps { - return errors.New("when initializing a new SignedEntityVerifier, you must specify at least one, or both, of WithSignedTimestamps() or WithObserverTimestamps()") + 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()") } - // TODO: require WithTransparencyLog? return nil } @@ -603,7 +614,9 @@ func (v *SignedEntityVerifier) VerifyTransparencyLogInclusion(entity SignedEntit verifiedTimestamps := []TimestampVerificationResult{} if v.config.weExpectTlogEntries { - verifiedTlogTimestamps, err := VerifyArtifactTransparencyLog(entity, v.trustedMaterial, v.config.tlogEntriesThreshold, v.config.requireObserverTimestamps, v.config.performOnlineVerification) + // 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 } @@ -625,8 +638,6 @@ func (v *SignedEntityVerifier) VerifyTransparencyLogInclusion(entity SignedEntit func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, logTimestamps []TimestampVerificationResult) ([]TimestampVerificationResult, error) { verifiedTimestamps := []TimestampVerificationResult{} - fmt.Println(len(logTimestamps)) - // From spec: // > … if verification or timestamp parsing fails, the Verifier MUST abort if v.config.weExpectSignedTimestamps { @@ -639,6 +650,12 @@ func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, log } } + 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) + } + } + if v.config.requireObserverTimestamps { verifiedSignedTimestamps, err := VerifyTimestampAuthority(entity, v.trustedMaterial) if err != nil { @@ -648,7 +665,7 @@ func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, log // 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 timestamps: %d < %d", + return nil, fmt.Errorf("threshold not met for verified signed & log entry integrated timestamps: %d < %d", tsCount, v.config.observerTimestampThreshold) } From 8684030bcc34417d4d61e94c804e8e3a6ca21f12 Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Tue, 19 Dec 2023 23:50:46 +0000 Subject: [PATCH 6/6] Complete TODOs, add tests Signed-off-by: Hayden Blauzvern --- cmd/sigstore-go/main.go | 13 ++- examples/oci-image-verification/main.go | 2 +- pkg/verify/signed_entity.go | 10 ++- pkg/verify/signed_entity_test.go | 103 ++++++++++++++++++++++-- pkg/verify/tlog.go | 5 +- pkg/verify/tlog_test.go | 11 ++- pkg/verify/tsa_test.go | 23 ++++++ 7 files changed, 148 insertions(+), 19 deletions(-) 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