Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add unit test for online tlog verification #296

Merged
2 changes: 1 addition & 1 deletion pkg/sign/transparency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
41 changes: 35 additions & 6 deletions pkg/testing/ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ 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"
"github.com/sigstore/rekor/pkg/types"
"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"
Expand Down Expand Up @@ -156,10 +158,16 @@ 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) AttestWithInclusionProof(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), true)
}

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
Expand Down Expand Up @@ -193,7 +201,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
}
Expand Down Expand Up @@ -245,7 +253,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
Expand All @@ -271,7 +279,7 @@ 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)
Expand All @@ -284,7 +292,27 @@ func (ca *VirtualSigstore) GenerateTlogEntry(leafCert *x509.Certificate, envelop
return nil, err
}

return tlog.NewEntry(rekorBodyRaw, integratedTime, logIndex, rekorLogIDRaw, set, nil)
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
}
var inclusionProof *models.InclusionProof
if generateInclusionProof {
inclusionProof = &models.InclusionProof{
TreeSize: swag.Int64(int64(1)),
RootHash: &encodedRootHash,
LogIndex: swag.Int64(0),
Hashes: nil,
Checkpoint: swag.String(string(scBytes)),
}
}
return tlog.NewEntry(rekorBodyRaw, integratedTime, logIndex, rekorLogIDRaw, set, inclusionProof)
vishal-chdhry marked this conversation as resolved.
Show resolved Hide resolved
}

func (ca *VirtualSigstore) generateTlogEntryHashedRekord(leafCert *x509.Certificate, artifact []byte, sig []byte, integratedTime int64) (*tlog.Entry, error) {
Expand Down Expand Up @@ -451,6 +479,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
}
Expand Down
6 changes: 5 additions & 1 deletion pkg/tlog/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ func NewEntry(body []byte, integratedTime int64, logIndex int64, logID []byte, s

if inclusionProof != nil {
entry.logEntryAnon.Verification = &models.LogEntryAnonVerification{
InclusionProof: inclusionProof,
InclusionProof: inclusionProof,
SignedEntryTimestamp: signedEntryTimestamp,
vishal-chdhry marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -248,6 +249,9 @@ func (entry *Entry) HasInclusionPromise() bool {
func (entry *Entry) HasInclusionProof() bool {
return entry.logEntryAnon.Verification != nil
}
func (entry *Entry) Verification() *models.LogEntryAnonVerification {
vishal-chdhry marked this conversation as resolved.
Show resolved Hide resolved
return entry.logEntryAnon.Verification
}

func VerifyInclusion(entry *Entry, verifier signature.Verifier) error {
err := rekorVerify.VerifyInclusion(context.TODO(), &entry.logEntryAnon)
Expand Down
43 changes: 39 additions & 4 deletions pkg/verify/tlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,39 @@ import (

const maxAllowedTlogEntries = 32

type GetRekorClientFunc func(string) (*rekorGeneratedClient.Rekor, error)
type verifytlogOptions struct {
getRekorClient GetRekorClientFunc
}

func makeOptions(opts ...TlogOption) verifytlogOptions {
opt := verifytlogOptions{}
for _, o := range opts {
o(&opt)
}
return opt
}

// TlogOption is a functional option for transparency log verification.
type TlogOption func(*verifytlogOptions)

// WithGetRekorClientFunc sets the function that will be used to fetch rekor client from base URL.
// If not provided, rekorClient.GetRekorClient is used.
func WithGetRekorClientFunc(f GetRekorClientFunc) TlogOption {
return func(opts *verifytlogOptions) {
opts.getRekorClient = f
}
}

// VerifyArtifactTransparencyLog verifies that the given entity has been logged
// in the transparency log and that the log entry is valid.
//
// The threshold parameter is the number of unique transparency log entries
// that must be verified.
//
// If online is true, the log entry is verified against the Rekor server.
func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.TrustedMaterial, logThreshold int, trustIntegratedTime, online bool) ([]Timestamp, error) { //nolint:revive
func VerifyArtifactTransparencyLog(entity SignedEntity, trustedMaterial root.TrustedMaterial, logThreshold int, trustIntegratedTime, online bool, opts ...TlogOption) ([]Timestamp, error) { //nolint:revive
vishal-chdhry marked this conversation as resolved.
Show resolved Hide resolved
options := makeOptions(opts...)
entries, err := entity.TlogEntries()
if err != nil {
return nil, err
Expand Down Expand Up @@ -118,10 +143,20 @@ 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)
if err != nil {
return nil, err
var client *rekorGeneratedClient.Rekor
var err error
if options.getRekorClient != nil {
client, err = options.getRekorClient(tlogVerifier.BaseURL)
if err != nil {
return nil, err
}
} else {
client, err = getRekorClient(tlogVerifier.BaseURL)
if err != nil {
return nil, err
}
}

verifier, err := getVerifier(tlogVerifier.PublicKey, tlogVerifier.SignatureHashFunc)
if err != nil {
return nil, err
Expand Down
74 changes: 73 additions & 1 deletion pkg/verify/tlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ package verify_test

import (
"encoding/base64"
"encoding/hex"
"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"
Expand Down Expand Up @@ -56,7 +63,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("[email protected]", "issuer", statement, time.Now().Add(30*time.Minute))
entity, err = virtualSigstore.AttestAtTime("[email protected]", "issuer", statement, time.Now().Add(30*time.Minute), false)
assert.NoError(t, err)

_, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, false)
Expand Down Expand Up @@ -221,3 +228,68 @@ 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 (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":{}}`)
entity, err := virtualSigstore.AttestWithInclusionProof("[email protected]", "issuer", statement)
assert.NoError(t, err)
tlogEntries, err := entity.TlogEntries()
assert.NoError(t, err)

tlogEntry := tlogEntries[0]
var logEntry models.LogEntry = make(models.LogEntry)
logEntry["foo"] = models.LogEntryAnon{
Body: tlogEntry.Body(),
IntegratedTime: swag.Int64(tlogEntry.IntegratedTime().Unix()),
LogIndex: swag.Int64(tlogEntry.LogIndex()),
LogID: swag.String(hex.EncodeToString([]byte(tlogEntry.LogKeyID()))),
Verification: tlogEntry.Verification(),
vishal-chdhry marked this conversation as resolved.
Show resolved Hide resolved
}
mockRekor := &rekorGeneratedClient.Rekor{
Entries: &mockEntriesClient{
Entries: []*models.LogEntry{&logEntry},
},
}

_, err = verify.VerifyArtifactTransparencyLog(entity, virtualSigstore, 1, true, true, verify.WithGetRekorClientFunc(func(_ string) (*rekorGeneratedClient.Rekor, error) { return mockRekor, nil }))
assert.NoError(t, err)
}