From e92142f0734064ebf6001f188b7330a1212245fe Mon Sep 17 00:00:00 2001 From: Vishal Choudhary Date: Fri, 18 Oct 2024 04:53:29 +0530 Subject: [PATCH] feat: add unit test for online tlog verification (#296) * feat: add unit test for online tlog verification Signed-off-by: Vishal Choudhary * feat: add unfinished test Signed-off-by: Vishal Choudhary * chore: remove online test with network calls Signed-off-by: Vishal Choudhary * fix: inclusion and checkpoint verification in tlog Signed-off-by: Vishal Choudhary * fix: rekor test Signed-off-by: Vishal Choudhary * Update pkg/verify/tlog_test.go Co-authored-by: Colleen Murphy Signed-off-by: Vishal Choudhary * fix: seperate attest func with inclusion proof Signed-off-by: Vishal Choudhary * fix: add changes requested Signed-off-by: Vishal Choudhary * feat: add tests Signed-off-by: Vishal Choudhary * fix: remove unnecessary assignment Signed-off-by: Vishal Choudhary * feat: add test TestNoInclusionProofOnline Signed-off-by: Vishal Choudhary * fix: cleanup Signed-off-by: Vishal Choudhary --------- Signed-off-by: Vishal Choudhary Signed-off-by: Vishal Choudhary Co-authored-by: Colleen Murphy --- pkg/sign/transparency_test.go | 2 +- pkg/testing/ca/ca.go | 58 ++++++++++-- pkg/verify/tlog.go | 9 +- pkg/verify/tlog_test.go | 173 +++++++++++++++++++++++++++++++++- 4 files changed, 227 insertions(+), 15 deletions(-) diff --git a/pkg/sign/transparency_test.go b/pkg/sign/transparency_test.go index b4948d70..f34f5a1a 100644 --- a/pkg/sign/transparency_test.go +++ b/pkg/sign/transparency_test.go @@ -70,7 +70,7 @@ func (m *mockRekor) CreateLogEntry(_ *entries.CreateLogEntryParams, _ ...entries return nil, err } - entry, err := virtualSigstore.GenerateTlogEntry(leafCert, envelope, signature, time.Now().Unix()) + entry, err := virtualSigstore.GenerateTlogEntry(leafCert, envelope, signature, time.Now().Unix(), false) if err != nil { return nil, err } diff --git a/pkg/testing/ca/ca.go b/pkg/testing/ca/ca.go index 3a3e6ad4..92ee5851 100644 --- a/pkg/testing/ca/ca.go +++ b/pkg/testing/ca/ca.go @@ -36,6 +36,7 @@ import ( "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/digitorus/timestamp" "github.com/go-openapi/runtime" + "github.com/go-openapi/swag" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/pki" @@ -43,6 +44,7 @@ import ( "github.com/sigstore/rekor/pkg/types/hashedrekord" "github.com/sigstore/rekor/pkg/types/intoto" "github.com/sigstore/rekor/pkg/types/rekord" + "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore-go/pkg/tlog" @@ -121,7 +123,11 @@ func getLogID(pub crypto.PublicKey) (string, error) { return hex.EncodeToString(digest[:]), nil } -func (ca *VirtualSigstore) rekorSignPayload(payload tlog.RekorPayload) ([]byte, error) { +func (ca *VirtualSigstore) RekorLogID() (string, error) { + return getLogID(ca.rekorKey.Public()) +} + +func (ca *VirtualSigstore) RekorSignPayload(payload tlog.RekorPayload) ([]byte, error) { jsonPayload, err := json.Marshal(payload) if err != nil { return nil, err @@ -156,10 +162,10 @@ func (ca *VirtualSigstore) GenerateLeafCert(identity, issuer string) (*x509.Cert func (ca *VirtualSigstore) Attest(identity, issuer string, envelopeBody []byte) (*TestEntity, error) { // The timing here is important. We need to attest at a time when the leaf // certificate is valid, so we match what GenerateLeafCert() does, above - return ca.AttestAtTime(identity, issuer, envelopeBody, time.Now().Add(5*time.Minute)) + return ca.AttestAtTime(identity, issuer, envelopeBody, time.Now().Add(5*time.Minute), false) } -func (ca *VirtualSigstore) AttestAtTime(identity, issuer string, envelopeBody []byte, integratedTime time.Time) (*TestEntity, error) { +func (ca *VirtualSigstore) AttestAtTime(identity, issuer string, envelopeBody []byte, integratedTime time.Time, generateInclusionProof bool) (*TestEntity, error) { leafCert, leafPrivKey, err := ca.GenerateLeafCert(identity, issuer) if err != nil { return nil, err @@ -193,7 +199,7 @@ func (ca *VirtualSigstore) AttestAtTime(identity, issuer string, envelopeBody [] return nil, err } - entry, err := ca.GenerateTlogEntry(leafCert, envelope, sig, integratedTime.Unix()) + entry, err := ca.GenerateTlogEntry(leafCert, envelope, sig, integratedTime.Unix(), generateInclusionProof) if err != nil { return nil, err } @@ -245,7 +251,7 @@ func (ca *VirtualSigstore) SignAtTime(identity, issuer string, artifact []byte, }, nil } -func (ca *VirtualSigstore) GenerateTlogEntry(leafCert *x509.Certificate, envelope *dsse.Envelope, sig []byte, integratedTime int64) (*tlog.Entry, error) { +func (ca *VirtualSigstore) GenerateTlogEntry(leafCert *x509.Certificate, envelope *dsse.Envelope, sig []byte, integratedTime int64, generateInclusionProof bool) (*tlog.Entry, error) { leafCertPem, err := cryptoutils.MarshalCertificateToPEM(leafCert) if err != nil { return nil, err @@ -271,10 +277,10 @@ func (ca *VirtualSigstore) GenerateTlogEntry(leafCert *x509.Certificate, envelop return nil, err } - logIndex := int64(1000) + logIndex := int64(0) b := createRekorBundle(rekorLogID, integratedTime, logIndex, rekorBody) - set, err := ca.rekorSignPayload(*b) + set, err := ca.RekorSignPayload(*b) if err != nil { return nil, err } @@ -284,7 +290,35 @@ func (ca *VirtualSigstore) GenerateTlogEntry(leafCert *x509.Certificate, envelop return nil, err } - return tlog.NewEntry(rekorBodyRaw, integratedTime, logIndex, rekorLogIDRaw, set, nil) + var inclusionProof *models.InclusionProof + if generateInclusionProof { + inclusionProof, err = ca.GetInclusionProof(rekorBodyRaw) + if err != nil { + return nil, err + } + } + return tlog.NewEntry(rekorBodyRaw, integratedTime, logIndex, rekorLogIDRaw, set, inclusionProof) +} + +func (ca *VirtualSigstore) GetInclusionProof(rekorBodyRaw []byte) (*models.InclusionProof, error) { + signer, err := signature.LoadECDSASignerVerifier(ca.rekorKey, crypto.SHA256) + if err != nil { + return nil, err + } + rootHash := sha256.Sum256(append([]byte("\000"), rekorBodyRaw...)) + encodedRootHash := hex.EncodeToString(rootHash[:]) + scBytes, err := util.CreateAndSignCheckpoint(context.TODO(), "rekor.localhost", int64(123), uint64(42), rootHash[:], signer) + if err != nil { + return nil, err + } + + return &models.InclusionProof{ + TreeSize: swag.Int64(int64(1)), + RootHash: &encodedRootHash, + LogIndex: swag.Int64(0), + Hashes: nil, + Checkpoint: swag.String(string(scBytes)), + }, nil } func (ca *VirtualSigstore) generateTlogEntryHashedRekord(leafCert *x509.Certificate, artifact []byte, sig []byte, integratedTime int64) (*tlog.Entry, error) { @@ -311,7 +345,7 @@ func (ca *VirtualSigstore) generateTlogEntryHashedRekord(leafCert *x509.Certific logIndex := int64(1000) b := createRekorBundle(rekorLogID, integratedTime, logIndex, rekorBody) - set, err := ca.rekorSignPayload(*b) + set, err := ca.RekorSignPayload(*b) if err != nil { return nil, err } @@ -451,6 +485,7 @@ func (ca *VirtualSigstore) RekorLogs() map[string]*root.TransparencyLog { ValidityPeriodEnd: time.Now().Add(time.Hour), HashFunc: crypto.SHA256, PublicKey: ca.rekorKey.Public(), + SignatureHashFunc: crypto.SHA256, } return verifiers } @@ -489,6 +524,11 @@ func (e *TestEntity) HasInclusionPromise() bool { } func (e *TestEntity) HasInclusionProof() bool { + for _, tlog := range e.tlogEntries { + if tlog.HasInclusionProof() { + return true + } + } return false } diff --git a/pkg/verify/tlog.go b/pkg/verify/tlog.go index c54aff03..ca2f2d9d 100644 --- a/pkg/verify/tlog.go +++ b/pkg/verify/tlog.go @@ -35,6 +35,8 @@ import ( const maxAllowedTlogEntries = 32 +var RekorClientGetter = getRekorClient + // VerifyArtifactTransparencyLog verifies that the given entity has been logged // in the transparency log and that the log entry is valid. // @@ -118,10 +120,11 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru // DO NOT use timestamp with only an inclusion proof, because it is not signed metadata } } else { - client, err := getRekorClient(tlogVerifier.BaseURL) + client, err := RekorClientGetter(tlogVerifier.BaseURL) if err != nil { return nil, err } + verifier, err := getVerifier(tlogVerifier.PublicKey, tlogVerifier.SignatureHashFunc) if err != nil { return nil, err @@ -141,9 +144,7 @@ func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.Tru return nil, fmt.Errorf("unable to locate log entry %d", logIndex) } - logEntry := resp.Payload - - for _, v := range logEntry { + for _, v := range resp.Payload { v := v err = rekorVerify.VerifyLogEntry(context.TODO(), &v, *verifier) if err != nil { diff --git a/pkg/verify/tlog_test.go b/pkg/verify/tlog_test.go index be8d60bc..4afe2bd2 100644 --- a/pkg/verify/tlog_test.go +++ b/pkg/verify/tlog_test.go @@ -16,10 +16,16 @@ package verify_test import ( "encoding/base64" + "errors" "strings" "testing" "time" + "github.com/go-openapi/runtime" + "github.com/go-openapi/swag" + rekorGeneratedClient "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/testing/ca" "github.com/sigstore/sigstore-go/pkg/tlog" "github.com/sigstore/sigstore-go/pkg/verify" @@ -56,7 +62,7 @@ func TestTlogVerifier(t *testing.T) { // // This time was chosen assuming the Fulcio signing certificate expires // after 5 minutes, but while the TSA intermediate is still valid (2 hours). - entity, err = virtualSigstore.AttestAtTime("foo@example.com", "issuer", statement, time.Now().Add(30*time.Minute)) + entity, err = virtualSigstore.AttestAtTime("foo@example.com", "issuer", statement, time.Now().Add(30*time.Minute), false) assert.NoError(t, err) _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false) @@ -221,3 +227,168 @@ func TestMaxAllowedTlogEntries(t *testing.T) { _, err = verify.VerifyArtifactTransparencyLog(&tooManyTlogEntriesEntity{entity}, virtualSigstore, 1, true, false) assert.ErrorContains(t, err, "too many tlog entries") // too many tlog entries should fail to verify } + +type mockEntriesClient struct { + Entries []*models.LogEntry +} + +func newMockRekorEntriesClient(virtualSigstore ca.VirtualSigstore, statement []byte, integratedTime time.Time) (*mockEntriesClient, error) { + var err error + base64Statement := base64.StdEncoding.EncodeToString(statement) + verification := &models.LogEntryAnonVerification{} + verification.InclusionProof, err = virtualSigstore.GetInclusionProof(statement) + if err != nil { + return nil, err + } + logID, err := virtualSigstore.RekorLogID() + if err != nil { + return nil, err + } + bundle := &tlog.RekorPayload{ + LogID: logID, + IntegratedTime: integratedTime.Unix(), + LogIndex: 0, + Body: base64Statement, + } + verification.SignedEntryTimestamp, err = virtualSigstore.RekorSignPayload(*bundle) + if err != nil { + return nil, err + } + + var logEntry models.LogEntry = make(models.LogEntry) + logEntry["foo"] = models.LogEntryAnon{ + Body: base64Statement, + IntegratedTime: swag.Int64(integratedTime.Unix()), + LogIndex: swag.Int64(0), + LogID: swag.String(logID), + Verification: verification, + } + mockRekor := &mockEntriesClient{ + Entries: []*models.LogEntry{&logEntry}, + } + return mockRekor, nil +} + +func (m *mockEntriesClient) CreateLogEntry(_ *entries.CreateLogEntryParams, _ ...entries.ClientOption) (*entries.CreateLogEntryCreated, error) { + return nil, errors.New("not implemented") +} + +func (m *mockEntriesClient) GetLogEntryByIndex(params *entries.GetLogEntryByIndexParams, _ ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) { + resp := &entries.GetLogEntryByIndexOK{} + if len(m.Entries) != 0 { + for _, e := range m.Entries { + for _, i := range *e { + if *i.LogIndex == params.LogIndex { + resp.Payload = *e + } + } + } + + if resp.Payload == nil { + resp.Payload = *m.Entries[0] + } + } + return resp, nil +} + +func (m *mockEntriesClient) GetLogEntryByUUID(_ *entries.GetLogEntryByUUIDParams, _ ...entries.ClientOption) (*entries.GetLogEntryByUUIDOK, error) { + return nil, errors.New("not implemented") +} + +func (m *mockEntriesClient) SearchLogQuery(_ *entries.SearchLogQueryParams, _ ...entries.ClientOption) (*entries.SearchLogQueryOK, error) { + return nil, errors.New("not implemented") +} + +func (m *mockEntriesClient) SetTransport(_ runtime.ClientTransport) {} + +func TestOnlineVerification(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + statement := []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`) + integratedTime := time.Now().Add(5 * time.Minute) + entity, err := virtualSigstore.AttestAtTime("foo@example.com", "issuer", statement, integratedTime, true) + assert.NoError(t, err) + + entriesClient, err := newMockRekorEntriesClient(*virtualSigstore, statement, integratedTime) + assert.NoError(t, err) + mockRekor := &rekorGeneratedClient.Rekor{ + Entries: entriesClient, + } + + oldRekorClientGetter := verify.RekorClientGetter + verify.RekorClientGetter = func(_ string) (*rekorGeneratedClient.Rekor, error) { return mockRekor, nil } + defer func() { verify.RekorClientGetter = oldRekorClientGetter }() + + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, true) + assert.NoError(t, err) +} + +func TestNoInclusionProofOnline(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + statement := []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`) + integratedTime := time.Now().Add(5 * time.Minute) + entity, err := virtualSigstore.AttestAtTime("foo@example.com", "issuer", statement, integratedTime, false) + assert.NoError(t, err) + + entriesClient, err := newMockRekorEntriesClient(*virtualSigstore, statement, integratedTime) + assert.NoError(t, err) + mockRekor := &rekorGeneratedClient.Rekor{ + Entries: entriesClient, + } + + oldRekorClientGetter := verify.RekorClientGetter + verify.RekorClientGetter = func(_ string) (*rekorGeneratedClient.Rekor, error) { return mockRekor, nil } + defer func() { verify.RekorClientGetter = oldRekorClientGetter }() + + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, true) + assert.NoError(t, err) +} + +func TestNoInclusionProofAndRekorVerification(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + statement := []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`) + integratedTime := time.Now().Add(5 * time.Minute) + entity, err := virtualSigstore.AttestAtTime("foo@example.com", "issuer", statement, integratedTime, true) + assert.NoError(t, err) + + logID, err := virtualSigstore.RekorLogID() + assert.NoError(t, err) + + var logEntry models.LogEntry = make(models.LogEntry) + logEntry["foo"] = models.LogEntryAnon{ + Body: base64.StdEncoding.EncodeToString(statement), + IntegratedTime: swag.Int64(integratedTime.Unix()), + LogIndex: swag.Int64(0), + LogID: swag.String(logID), + } + mockRekor := &rekorGeneratedClient.Rekor{ + Entries: &mockEntriesClient{ + Entries: []*models.LogEntry{&logEntry}, + }, + } + + oldRekorClientGetter := verify.RekorClientGetter + verify.RekorClientGetter = func(_ string) (*rekorGeneratedClient.Rekor, error) { return mockRekor, nil } + defer func() { verify.RekorClientGetter = oldRekorClientGetter }() + + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, true) + assert.ErrorContains(t, err, "inclusion proof not provided") +} + +func TestOfflineInclusionProofVerification(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + statement := []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}`) + integratedTime := time.Now().Add(5 * time.Minute) + entity, err := virtualSigstore.AttestAtTime("foo@example.com", "issuer", statement, integratedTime, true) + assert.NoError(t, err) + + _, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false) + assert.NoError(t, err) +}