From da5582e42b6b6ab18a33e0a91598269d7ae3b126 Mon Sep 17 00:00:00 2001 From: Horiodino Date: Wed, 5 Feb 2025 22:15:57 +0530 Subject: [PATCH 1/5] Alert only for certificates issued from a set of trusted roots Signed-off-by: Horiodino --- pkg/ct/consistency.go | 17 +++++-- pkg/rekor/verifier.go | 115 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 11 deletions(-) diff --git a/pkg/ct/consistency.go b/pkg/ct/consistency.go index 1513f10e..2c76ee31 100644 --- a/pkg/ct/consistency.go +++ b/pkg/ct/consistency.go @@ -22,7 +22,7 @@ import ( ct "github.com/google/certificate-transparency-go" ctclient "github.com/google/certificate-transparency-go/client" "github.com/sigstore/rekor-monitor/pkg/util/file" - "github.com/sigstore/sigstore/pkg/cryptoutils" + sigstore_tuf "github.com/sigstore/sigstore-go/pkg/tuf" "github.com/transparency-dev/merkle/proof" "github.com/transparency-dev/merkle/rfc6962" ) @@ -41,12 +41,21 @@ func verifyCertificateTransparencyConsistency(logInfoFile string, logClient *ctc } if logClient.Verifier == nil { - // TODO: this public key is currently hardcoded- should be fetched from TUF repository instead - pubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(ctfe2022PubKey)) + client, err := sigstore_tuf.DefaultClient() if err != nil { - return nil, fmt.Errorf("error loading public key: %v", err) + return nil, fmt.Errorf("error creating TUF client: %v", err) } + + if err := client.Refresh(); err != nil { + return nil, fmt.Errorf("error refreshing TUF metadata: %v", err) + } + + pubKey, err := client.GetTarget("ctfe2022PubKey") + if err != nil { + return nil, fmt.Errorf("error fetching public key: %v", err) + } + logClient.Verifier = &ct.SignatureVerifier{ PubKey: pubKey, } diff --git a/pkg/rekor/verifier.go b/pkg/rekor/verifier.go index 2c2303e3..948fc3c6 100644 --- a/pkg/rekor/verifier.go +++ b/pkg/rekor/verifier.go @@ -15,10 +15,13 @@ package rekor import ( + "bytes" "context" "crypto" + "crypto/x509" "encoding/hex" "fmt" + "log" "os" "github.com/sigstore/rekor-monitor/pkg/util/file" @@ -26,28 +29,126 @@ import ( "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/util" "github.com/sigstore/rekor/pkg/verify" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" ) +// TrustRootConfig for trust roots (aka custom roots) +type TrustRootConfig struct { + CustomRoots []*x509.Certificate +} + // GetLogVerifier creates a verifier from the log's public key -// TODO: Fetch the public key from TUF -func GetLogVerifier(ctx context.Context, rekorClient *client.Rekor) (signature.Verifier, error) { - pemPubKey, err := GetPublicKey(ctx, rekorClient) +func GetLogVerifier(ctx context.Context, rekorClient *client.Rekor, trustRootConfig *TrustRootConfig) (signature.Verifier, error) { + trustedRoots, err := fetchTrustedRoots(trustRootConfig) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get trusted roots: %v", err) } - pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pemPubKey) + + certChain, err := getCertificateChain(ctx, rekorClient) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get certificate chain: %v", err) + } + + if err := verifyCertificateChain(certChain, trustedRoots); err != nil { + return nil, fmt.Errorf("certificate chain verification failed: %v", err) } + + // Extract and create verifier + pubKey := certChain[0].PublicKey verifier, err := signature.LoadVerifier(pubKey, crypto.SHA256) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to load verifier: %v", err) } + return verifier, nil } +func fetchTrustedRoots(config *TrustRootConfig) (*x509.CertPool, error) { + certPool := x509.NewCertPool() + + defaultRoots, err := root.FetchTrustedRoot() + if err != nil { + log.Printf("Warning: Failed to fetch trusted roots: %v", err) + } + + fulcioCAs := defaultRoots.FulcioCertificateAuthorities() + if len(fulcioCAs) == 0 { + log.Println("Warning: No Fulcio CAs found in TUF metadata") + } + + for _, ca := range fulcioCAs { + if fulcioCa, ok := ca.(*root.FulcioCertificateAuthority); ok { + if fulcioCa.Root != nil { + certPool.AddCert(fulcioCa.Root) + } + for _, intermediateCert := range fulcioCa.Intermediates { + certPool.AddCert(intermediateCert) + } + } + } + + // for custom roots (if any) + if len(config.CustomRoots) > 0 { + for _, rootCert := range config.CustomRoots { + certPool.AddCert(rootCert) + } + } + + return certPool, nil +} + +func verifyCertificateChain(certChain []*x509.Certificate, trustedRoots *x509.CertPool) error { + if len(certChain) == 0 { + return fmt.Errorf("empty certificate chain") + } + + intermediates := x509.NewCertPool() + for _, cert := range certChain[1:] { //skipp the first cert as it is the leaf cert + intermediates.AddCert(cert) + } + + // using intermediate CAs to verify the chain of trust so that we can support intermediate CAs and it won't fail + // should we? or should we just use the root CAs? let me know + opts := x509.VerifyOptions{ + Roots: trustedRoots, + Intermediates: intermediates, + } + + verifiedChains, err := certChain[0].Verify(opts) + if err != nil { + return fmt.Errorf("certificate chain verification failed: %v", err) + } + + for _, chain := range verifiedChains { + rootCert := chain[len(chain)-1] + if trustedRoots.Subjects() != nil { + for _, subject := range trustedRoots.Subjects() { + if bytes.Equal(rootCert.RawSubject, subject) { + return nil + } + } + } + } + + return fmt.Errorf("certificate chain does not terminate at a trusted root") +} + +func getCertificateChain(ctx context.Context, rekorClient *client.Rekor) ([]*x509.Certificate, error) { + pemPubKey, err := GetPublicKey(ctx, rekorClient) + if err != nil { + return nil, fmt.Errorf("failed to get public key: %v", err) + } + + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pemPubKey) + if err != nil || len(certs) == 0 { + return nil, fmt.Errorf("failed to parse certificates: %v", err) + } + + return certs, nil +} + // ReadLatestCheckpoint fetches the latest checkpoint from log info fetched from Rekor. // It returns the checkpoint if it successfully fetches one; otherwise, it returns an error. func ReadLatestCheckpoint(logInfo *models.LogInfo) (*util.SignedCheckpoint, error) { From b36d36cab20c83613cd756b50892b8ba21e6b7f8 Mon Sep 17 00:00:00 2001 From: Horiodino Date: Wed, 5 Feb 2025 22:18:03 +0530 Subject: [PATCH 2/5] updated test case Signed-off-by: Horiodino --- pkg/rekor/verifier_test.go | 115 ++++++++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/pkg/rekor/verifier_test.go b/pkg/rekor/verifier_test.go index 67145f20..ca686d7b 100644 --- a/pkg/rekor/verifier_test.go +++ b/pkg/rekor/verifier_test.go @@ -19,31 +19,128 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net/http" "testing" + "time" "github.com/sigstore/rekor-monitor/pkg/rekor/mock" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/sigstore/pkg/cryptoutils" ) -func TestGetLogVerifier(t *testing.T) { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - pemKey, err := cryptoutils.MarshalPublicKeyToPEM(key.Public()) +func fetchRealCertChain() ([]*x509.Certificate, error) { + resp, err := http.Get("https://rekor.sigstore.dev/api/v1/log/publicKey") if err != nil { - t.Fatalf("unexpected error marshalling key: %v", err) + return nil, fmt.Errorf("failed to fetch public key: %v", err) + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(bodyBytes) + if err == nil && len(certs) > 0 { + return certs, nil + } + + pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(bodyBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %v", err) + } + + dummyCert := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{CommonName: "Rekor Public Key"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + PublicKey: pubKey, + } + + return []*x509.Certificate{dummyCert}, nil +} + +func TestGetLogVerifierWithRealAndMockData(t *testing.T) { + realCertChain, err := fetchRealCertChain() + if err != nil { + t.Fatalf("failed to fetch certificate chain: %v", err) + } + if len(realCertChain) == 0 { + t.Fatalf("certificate chain is empty") + } + + realPubKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(realCertChain[0].PublicKey) + if err != nil { + t.Fatalf("failed to marshal real public key to PEM: %v", err) } var mClient client.Rekor mClient.Pubkey = &mock.PubkeyClient{ - PEMPubKey: string(pemKey), + PEMPubKey: string(realPubKeyPEM), + } + + realTrustRootConfig := &TrustRootConfig{} + + _, err = GetLogVerifier(context.Background(), &mClient, realTrustRootConfig) + if err == nil { + t.Fatalf("expected error due to unknown authority, but got a verifier") } - verifier, err := GetLogVerifier(context.Background(), &mClient) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - t.Fatalf("unexpected error getting log verifier: %v", err) + t.Fatalf("unexpected error generating key: %v", err) } - pubkey, _ := verifier.PublicKey() - if err := cryptoutils.EqualKeys(key.Public(), pubkey); err != nil { + + mockTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(12345), + Subject: pkix.Name{CommonName: "test.example.com"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + mockCertBytes, err := x509.CreateCertificate(rand.Reader, mockTemplate, mockTemplate, &key.PublicKey, key) + if err != nil { + t.Fatalf("unexpected error creating certificate: %v", err) + } + + mockCert, err := x509.ParseCertificate(mockCertBytes) + if err != nil { + t.Fatalf("failed to parse created certificate: %v", err) + } + + mockCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: mockCertBytes}) + + mockTrustRootConfig := &TrustRootConfig{ + CustomRoots: []*x509.Certificate{mockCert}, + } + + mClient.Pubkey = &mock.PubkeyClient{ + PEMPubKey: string(mockCertPEM), + } + + _, err = GetLogVerifier(context.Background(), &mClient, &TrustRootConfig{}) + if err == nil { + t.Fatalf("expected error due to unknown authority, but got a verifier") + } + + verifier, err := GetLogVerifier(context.Background(), &mClient, mockTrustRootConfig) + if err != nil { + t.Fatalf("unexpected error getting log verifier with mock certificate: %v", err) + } + + verifierPubKey, _ := verifier.PublicKey() + if err := cryptoutils.EqualKeys(key.Public(), verifierPubKey); err != nil { t.Fatalf("expected equal keys: %v", err) } + } From d1473cddf363dbec1242afb13120463b7006ff1e Mon Sep 17 00:00:00 2001 From: Horiodino Date: Wed, 5 Feb 2025 22:18:27 +0530 Subject: [PATCH 3/5] Update go.mod Signed-off-by: Horiodino --- go.mod | 7 +++++++ go.sum | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/go.mod b/go.mod index 74a8782a..76392144 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/sendgrid/sendgrid-go v3.16.0+incompatible github.com/sigstore/rekor v1.3.9 github.com/sigstore/sigstore v1.8.12 + github.com/sigstore/sigstore-go v0.7.0 github.com/transparency-dev/merkle v0.0.2 github.com/wneessen/go-mail v0.6.1 golang.org/x/mod v0.22.0 @@ -31,6 +32,8 @@ require ( github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect github.com/danieljoos/wincred v1.2.0 // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect @@ -82,6 +85,8 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/sigstore/protobuf-specs v0.3.3 // indirect + github.com/sigstore/timestamp-authority v1.2.4 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect @@ -90,6 +95,7 @@ require ( github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect + github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/veraison/go-cose v1.3.0 // indirect @@ -109,6 +115,7 @@ require ( golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.9.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/protobuf v1.36.4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 678bec3c..67dd90ba 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -139,6 +144,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -160,6 +167,8 @@ github.com/google/rpmpack v0.6.0 h1:LoQuqlw6kHRwg25n3M0xtYrW+z2pTkR0ae1xx11hRw8= github.com/google/rpmpack v0.6.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= +github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek= github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -293,10 +302,14 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g= +github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU= github.com/sigstore/rekor v1.3.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU= github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM= github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc= github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo= +github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48= +github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12/go.mod h1:aw60vs3crnQdM/DYH+yF2P0MVKtItwAX34nuaMrY7Lk= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY= @@ -305,6 +318,8 @@ github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12 h1:kweBChR6M9FEvmxN3B github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12/go.mod h1:6+d+A6oYt1W5OgtzgEVb21V7tAZ/C2Ihtzc5MNJbayY= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12 h1:jvY1B9bjP+tKzdKDyuq5K7O19CG2IKzGJNTy5tuL2Gs= github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12/go.mod h1:2uEeOb8xE2RC6OvzxKux1wkS39Zv8gA27z92m49xUTc= +github.com/sigstore/timestamp-authority v1.2.4 h1:RjXZxOWorEiem/uSr0pFHVtQpyzpcFxgugo5jVqm3mw= +github.com/sigstore/timestamp-authority v1.2.4/go.mod h1:ExrbobKdEuwuBptZIiKp1IaVBRiUeKbiuSyZTO8Okik= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -335,6 +350,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if370jjzO84SS+Ebo= +github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= From 3951b2afd3bc75267aed69f0560f83c5dfa1e572 Mon Sep 17 00:00:00 2001 From: Horiodino Date: Sat, 22 Feb 2025 22:23:47 +0530 Subject: [PATCH 4/5] verifying logEntry certChains Signed-off-by: Horiodino --- pkg/rekor/verifier.go | 325 +++++++++++++++++++++++++++++++++++------- 1 file changed, 274 insertions(+), 51 deletions(-) diff --git a/pkg/rekor/verifier.go b/pkg/rekor/verifier.go index 948fc3c6..5f8079c9 100644 --- a/pkg/rekor/verifier.go +++ b/pkg/rekor/verifier.go @@ -19,13 +19,18 @@ import ( "context" "crypto" "crypto/x509" + "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "log" "os" + "sync" + "time" "github.com/sigstore/rekor-monitor/pkg/util/file" "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/rekor/pkg/util" "github.com/sigstore/rekor/pkg/verify" @@ -34,69 +39,296 @@ import ( "github.com/sigstore/sigstore/pkg/signature" ) -// TrustRootConfig for trust roots (aka custom roots) +// TrustRootConfig defines the set of trusted roots for certificate verification type TrustRootConfig struct { - CustomRoots []*x509.Certificate + CustomRoots []*x509.Certificate + UseTUFDefault bool } -// GetLogVerifier creates a verifier from the log's public key -func GetLogVerifier(ctx context.Context, rekorClient *client.Rekor, trustRootConfig *TrustRootConfig) (signature.Verifier, error) { - trustedRoots, err := fetchTrustedRoots(trustRootConfig) - if err != nil { - return nil, fmt.Errorf("failed to get trusted roots: %v", err) +var certPoolMutex sync.Mutex + +// GetTrustedRoots builds a CertPool from the configured trusted roots +func GetTrustedRoots(ctx context.Context, config *TrustRootConfig, rekorClient *client.Rekor) (*x509.CertPool, error) { + certPool := x509.NewCertPool() + + if config.UseTUFDefault { + defaultRoots, err := root.FetchTrustedRoot() + if err != nil { + log.Printf("Critical: Failed to fetch TUF trusted roots: %v", err) + return nil, fmt.Errorf("failed to fetch TUF trusted roots: %v", err) + } + + fulcioCAs := defaultRoots.FulcioCertificateAuthorities() + if len(fulcioCAs) == 0 { + log.Println("Warning: No Fulcio CAs found in TUF metadata") + } + + for _, ca := range fulcioCAs { + if fulcioCa, ok := ca.(*root.FulcioCertificateAuthority); ok { + if fulcioCa.Root != nil { + certPool.AddCert(fulcioCa.Root) + } + for _, intermediateCert := range fulcioCa.Intermediates { + certPool.AddCert(intermediateCert) + } + } + } + } + + if len(config.CustomRoots) > 0 { + for _, rootCert := range config.CustomRoots { + if !isValidRoot(rootCert) { + log.Printf("Warning: Skipping invalid custom root: %v", rootCert.Subject.CommonName) + continue + } + certPoolMutex.Lock() + certPool.AddCert(rootCert) + certPoolMutex.Unlock() + } + } + + if len(certPool.Subjects()) == 0 { + return nil, fmt.Errorf("no valid trusted roots configured") } - certChain, err := getCertificateChain(ctx, rekorClient) + return certPool, nil +} + +// GetLogVerifier creates a verifier from the log's public key, ensuring it chains to trusted roots +func GetLogVerifier(ctx context.Context, rekorClient *client.Rekor, trustedRoots *x509.CertPool) (signature.Verifier, error) { + certChain, err := getCertificateChain(rekorClient) if err != nil { return nil, fmt.Errorf("failed to get certificate chain: %v", err) } + if len(certChain) == 0 { + return nil, fmt.Errorf("empty certificate chain") + } + if err := verifyCertificateChain(certChain, trustedRoots); err != nil { - return nil, fmt.Errorf("certificate chain verification failed: %v", err) + return nil, fmt.Errorf("certificate chain does not chain to a trusted root: %v", err) } - // Extract and create verifier pubKey := certChain[0].PublicKey verifier, err := signature.LoadVerifier(pubKey, crypto.SHA256) if err != nil { return nil, fmt.Errorf("failed to load verifier: %v", err) } - return verifier, nil } -func fetchTrustedRoots(config *TrustRootConfig) (*x509.CertPool, error) { - certPool := x509.NewCertPool() +// VerifyLogEntryCertChain verifies that a log entry's certificate chain chains up to a trusted root +func VerifyLogEntryCertChain(entry models.LogEntryAnon, certPool *x509.CertPool) bool { + certChain := extractCertChain(entry) + if len(certChain) == 0 { + logIndex := "" + if entry.LogIndex != nil { + logIndex = fmt.Sprintf("%d", *entry.LogIndex) + } + log.Printf("No certificate chain found in log entry: %s", logIndex) + return false + } - defaultRoots, err := root.FetchTrustedRoot() + cert, err := x509.ParseCertificate(certChain[0]) if err != nil { - log.Printf("Warning: Failed to fetch trusted roots: %v", err) + logIndex := "" + if entry.LogIndex != nil { + logIndex = fmt.Sprintf("%d", *entry.LogIndex) + } + log.Printf("Failed to parse end-entity certificate for entry %s: %v", logIndex, err) + return false } - fulcioCAs := defaultRoots.FulcioCertificateAuthorities() - if len(fulcioCAs) == 0 { - log.Println("Warning: No Fulcio CAs found in TUF metadata") + intermediates := x509.NewCertPool() + for _, der := range certChain[1:] { + inter, err := x509.ParseCertificate(der) + if err != nil { + logIndex := "" + if entry.LogIndex != nil { + logIndex = fmt.Sprintf("%d", *entry.LogIndex) + } + log.Printf("Failed to parse intermediate certificate for entry %s: %v", logIndex, err) + return false + } + intermediates.AddCert(inter) } - for _, ca := range fulcioCAs { - if fulcioCa, ok := ca.(*root.FulcioCertificateAuthority); ok { - if fulcioCa.Root != nil { - certPool.AddCert(fulcioCa.Root) - } - for _, intermediateCert := range fulcioCa.Intermediates { - certPool.AddCert(intermediateCert) - } + opts := x509.VerifyOptions{ + Roots: certPool, + Intermediates: intermediates, + CurrentTime: time.Now(), + } + + if _, err := cert.Verify(opts); err != nil { + logIndex := "" + if entry.LogIndex != nil { + logIndex = fmt.Sprintf("%d", *entry.LogIndex) } + log.Printf("Certificate chain verification failed for entry %s: %v", logIndex, err) + return false + } + return true +} + +func extractCertChain(entry models.LogEntryAnon) [][]byte { + if entry.Body == nil { + return [][]byte{} } - // for custom roots (if any) - if len(config.CustomRoots) > 0 { - for _, rootCert := range config.CustomRoots { - certPool.AddCert(rootCert) + bodyStr, ok := entry.Body.(string) + if !ok { + log.Printf("Failed to convert body to string") + return [][]byte{} + } + + decodedBody, err := base64.StdEncoding.DecodeString(bodyStr) + if err != nil { + log.Printf("Failed to decode base64 body: %v", err) + return [][]byte{} + } + + var rekord models.Rekord + if err := json.Unmarshal(decodedBody, &rekord); err != nil { + log.Printf("Failed to unmarshal rekord body: %v", err) + return [][]byte{} + } + + if rekord.APIVersion == nil || rekord.Spec == nil { + return [][]byte{} + } + + if *rekord.APIVersion != "0.0.1" { + log.Printf("Unsupported rekord API version: %s", *rekord.APIVersion) + return [][]byte{} + } + + specBytes, err := json.Marshal(rekord.Spec) + if err != nil { + log.Printf("Failed to marshal spec: %v", err) + return [][]byte{} + } + + var rekordSpec models.RekordV001Schema + if err := json.Unmarshal(specBytes, &rekordSpec); err != nil { + log.Printf("Failed to unmarshal rekord spec: %v", err) + return [][]byte{} + } + + if rekordSpec.Signature != nil && rekordSpec.Signature.PublicKey != nil && rekordSpec.Signature.PublicKey.Content != nil { + return [][]byte{*rekordSpec.Signature.PublicKey.Content} + } + + return [][]byte{} +} + +func ProcessLogEntries(ctx context.Context, config *TrustRootConfig, rekorClient *client.Rekor) error { + certPool, err := GetTrustedRoots(ctx, config, rekorClient) + if err != nil { + return fmt.Errorf("failed to get trusted roots: %v", err) + } + + logVerifier, err := GetLogVerifier(ctx, rekorClient, certPool) + if err != nil { + return fmt.Errorf("failed to get log verifier: %v", err) + } + + params := &entries.GetLogEntryByIndexParams{ + Context: ctx, + LogIndex: 0, + } + entries, err := rekorClient.Entries.GetLogEntryByIndex(params) + if err != nil { + return fmt.Errorf("failed to fetch log entries: %v", err) + } + + for _, entry := range entries.Payload { + logIndex := "" + if entry.LogIndex != nil { + logIndex = fmt.Sprintf("%d", *entry.LogIndex) + } + + if !VerifyLogEntryCertChain(entry, certPool) { + log.Printf("Skipping log entry %s due to invalid certificate chain", logIndex) + continue + } + + bodyStr, ok := entry.Body.(string) + if !ok { + log.Printf("Failed to convert body to string for entry %s", logIndex) + continue + } + + decodedBody, err := base64.StdEncoding.DecodeString(bodyStr) + if err != nil { + log.Printf("Failed to decode base64 body for entry %s: %v", logIndex, err) + continue + } + + var rekord models.Rekord + if err := json.Unmarshal(decodedBody, &rekord); err != nil { + log.Printf("Failed to unmarshal rekord body for entry %s: %v", logIndex, err) + continue + } + + specBytes, err := json.Marshal(rekord.Spec) + if err != nil { + log.Printf("Failed to marshal spec for entry %s: %v", logIndex, err) + continue + } + + var rekordSpec models.RekordV001Schema + if err := json.Unmarshal(specBytes, &rekordSpec); err != nil { + log.Printf("Failed to unmarshal rekord spec for entry %s: %v", logIndex, err) + continue } + + if rekordSpec.Signature == nil || rekordSpec.Signature.Content == nil || rekordSpec.Data == nil || rekordSpec.Data.Content == nil { + log.Printf("Missing signature or data in entry %s", logIndex) + continue + } + + signatureBytes := *rekordSpec.Signature.Content + signedData := rekordSpec.Data.Content + + h := crypto.SHA256.New() + h.Write(signedData) + hashedData := h.Sum(nil) + + // Verify the signature + err = logVerifier.VerifySignature(bytes.NewReader(hashedData), bytes.NewReader(signatureBytes)) + if err != nil { + log.Printf("Signature verification failed for entry %s: %v", logIndex, err) + continue + } + + log.Printf("Successfully verified signature for log entry %s", logIndex) } - return certPool, nil + return nil +} + +func isValidRoot(cert *x509.Certificate) bool { + if !cert.IsCA || !cert.BasicConstraintsValid { + log.Printf("Root certificate is not a valid CA: %v", cert.Subject.CommonName) + return false + } + + if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) { + log.Printf("Root certificate expired or not yet valid: %v", cert.Subject.CommonName) + return false + } + + if !isSelfSigned(cert) { + log.Printf("Root certificate is not self-signed: %v", cert.Subject.CommonName) + return false + } + + return true +} + +func isSelfSigned(cert *x509.Certificate) bool { + err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) + return err == nil } func verifyCertificateChain(certChain []*x509.Certificate, trustedRoots *x509.CertPool) error { @@ -104,48 +336,39 @@ func verifyCertificateChain(certChain []*x509.Certificate, trustedRoots *x509.Ce return fmt.Errorf("empty certificate chain") } + leafCert := certChain[0] intermediates := x509.NewCertPool() - for _, cert := range certChain[1:] { //skipp the first cert as it is the leaf cert + for _, cert := range certChain[1:] { intermediates.AddCert(cert) } - // using intermediate CAs to verify the chain of trust so that we can support intermediate CAs and it won't fail - // should we? or should we just use the root CAs? let me know opts := x509.VerifyOptions{ Roots: trustedRoots, Intermediates: intermediates, + CurrentTime: time.Now(), } - verifiedChains, err := certChain[0].Verify(opts) + _, err := leafCert.Verify(opts) if err != nil { return fmt.Errorf("certificate chain verification failed: %v", err) } + return nil +} - for _, chain := range verifiedChains { - rootCert := chain[len(chain)-1] - if trustedRoots.Subjects() != nil { - for _, subject := range trustedRoots.Subjects() { - if bytes.Equal(rootCert.RawSubject, subject) { - return nil - } - } - } +func getCertificateChain(rekorClient *client.Rekor) ([]*x509.Certificate, error) { + if rekorClient == nil || rekorClient.Pubkey == nil { + return nil, fmt.Errorf("invalid rekor client or pubkey client") } - return fmt.Errorf("certificate chain does not terminate at a trusted root") -} - -func getCertificateChain(ctx context.Context, rekorClient *client.Rekor) ([]*x509.Certificate, error) { - pemPubKey, err := GetPublicKey(ctx, rekorClient) + resp, err := rekorClient.Pubkey.GetPublicKey(nil) if err != nil { return nil, fmt.Errorf("failed to get public key: %v", err) } - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pemPubKey) + certs, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(resp.Payload)) if err != nil || len(certs) == 0 { return nil, fmt.Errorf("failed to parse certificates: %v", err) } - return certs, nil } From 5d90feeb6e227a85c4444663725e179b427b2be9 Mon Sep 17 00:00:00 2001 From: Horiodino Date: Sat, 22 Feb 2025 22:27:27 +0530 Subject: [PATCH 5/5] added test case Signed-off-by: Horiodino --- pkg/rekor/verifier_test.go | 593 ++++++++++++++++++++++++++++++++----- 1 file changed, 526 insertions(+), 67 deletions(-) diff --git a/pkg/rekor/verifier_test.go b/pkg/rekor/verifier_test.go index ca686d7b..1d82209f 100644 --- a/pkg/rekor/verifier_test.go +++ b/pkg/rekor/verifier_test.go @@ -15,132 +15,591 @@ package rekor import ( + "bytes" "context" + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" + "encoding/json" "encoding/pem" - "fmt" - "io" "math/big" - "net/http" "testing" "time" + "github.com/go-openapi/strfmt" "github.com/sigstore/rekor-monitor/pkg/rekor/mock" "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" ) -func fetchRealCertChain() ([]*x509.Certificate, error) { - resp, err := http.Get("https://rekor.sigstore.dev/api/v1/log/publicKey") - if err != nil { - return nil, fmt.Errorf("failed to fetch public key: %v", err) +func generateCert(template, parent *x509.Certificate, pubKey interface{}, privKey interface{}) ([]byte, error) { + if _, ok := privKey.(*ecdsa.PrivateKey); ok { + template.SignatureAlgorithm = x509.ECDSAWithSHA256 } - defer resp.Body.Close() + return x509.CreateCertificate(rand.Reader, template, parent, pubKey, privKey) +} - bodyBytes, err := io.ReadAll(resp.Body) +func TestGetLogVerifierWithRootVerification(t *testing.T) { + trustedRootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) + t.Fatalf("failed to generate trusted root key: %v", err) } - - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(bodyBytes) - if err == nil && len(certs) > 0 { - return certs, nil + trustedRootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Trusted Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + IsCA: true, + BasicConstraintsValid: true, + } + trustedRootCertBytes, err := generateCert(trustedRootTemplate, trustedRootTemplate, &trustedRootKey.PublicKey, trustedRootKey) + if err != nil { + t.Fatalf("failed to create trusted root certificate: %v", err) + } + trustedRootCert, err := x509.ParseCertificate(trustedRootCertBytes) + if err != nil { + t.Fatalf("failed to parse trusted root certificate: %v", err) } - pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(bodyBytes) + intermediateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, fmt.Errorf("failed to parse public key: %v", err) + t.Fatalf("failed to generate intermediate key: %v", err) + } + intermediateTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "Intermediate CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + IsCA: true, + BasicConstraintsValid: true, + } + intermediateCertBytes, err := generateCert(intermediateTemplate, trustedRootTemplate, &intermediateKey.PublicKey, trustedRootKey) + if err != nil { + t.Fatalf("failed to create intermediate certificate: %v", err) } - dummyCert := &x509.Certificate{ - SerialNumber: big.NewInt(time.Now().UnixNano()), - Subject: pkix.Name{CommonName: "Rekor Public Key"}, + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate leaf key: %v", err) + } + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: "rekor.sigstore.dev"}, NotBefore: time.Now().Add(-time.Hour), - NotAfter: time.Now().Add(time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, - PublicKey: pubKey, + } + leafCertBytes, err := generateCert(leafTemplate, intermediateTemplate, &leafKey.PublicKey, intermediateKey) + if err != nil { + t.Fatalf("failed to create leaf certificate: %v", err) } - return []*x509.Certificate{dummyCert}, nil -} + directLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate direct leaf key: %v", err) + } + directLeafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(6), + Subject: pkix.Name{CommonName: "direct-rekor.sigstore.dev"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + directLeafCertBytes, err := generateCert(directLeafTemplate, trustedRootTemplate, &directLeafKey.PublicKey, trustedRootKey) + if err != nil { + t.Fatalf("failed to create direct leaf certificate: %v", err) + } -func TestGetLogVerifierWithRealAndMockData(t *testing.T) { - realCertChain, err := fetchRealCertChain() + untrustedRootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - t.Fatalf("failed to fetch certificate chain: %v", err) + t.Fatalf("failed to generate untrusted root key: %v", err) + } + untrustedRootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(4), + Subject: pkix.Name{CommonName: "Untrusted Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + IsCA: true, + BasicConstraintsValid: true, } - if len(realCertChain) == 0 { - t.Fatalf("certificate chain is empty") + untrustedRootCertBytes, err := generateCert(untrustedRootTemplate, untrustedRootTemplate, &untrustedRootKey.PublicKey, untrustedRootKey) + if err != nil { + t.Fatalf("failed to create untrusted root certificate: %v", err) } - realPubKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(realCertChain[0].PublicKey) + untrustedLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate untrusted leaf key: %v", err) + } + untrustedLeafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(5), + Subject: pkix.Name{CommonName: "untrusted-rekor.sigstore.dev"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + untrustedLeafCertBytes, err := generateCert(untrustedLeafTemplate, untrustedRootTemplate, &untrustedLeafKey.PublicKey, untrustedRootKey) if err != nil { - t.Fatalf("failed to marshal real public key to PEM: %v", err) + t.Fatalf("failed to create untrusted leaf certificate: %v", err) } - var mClient client.Rekor - mClient.Pubkey = &mock.PubkeyClient{ - PEMPubKey: string(realPubKeyPEM), + tests := []struct { + name string + config *TrustRootConfig + setupClient func() *client.Rekor + expectSuccess bool + }{ + { + name: "Valid chain with custom root", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + chainPEM := append( + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCertBytes}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: intermediateCertBytes})..., + ) + chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: trustedRootCertBytes})...) + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: string(chainPEM), + }, + } + }, + expectSuccess: true, + }, + { + name: "Valid chain with direct root", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + chainPEM := append( + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: directLeafCertBytes}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: trustedRootCertBytes})..., + ) + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: string(chainPEM), + }, + } + }, + expectSuccess: true, + }, + { + name: "Untrusted root", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + chainPEM := append( + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: untrustedLeafCertBytes}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: untrustedRootCertBytes})..., + ) + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: string(chainPEM), + }, + } + }, + expectSuccess: false, + }, + { + name: "Missing intermediate cert", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + chainPEM := append( + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCertBytes}), + pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: trustedRootCertBytes})..., + ) + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: string(chainPEM), + }, + } + }, + expectSuccess: false, + }, + { + name: "Empty chain", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: "", + }, + } + }, + expectSuccess: false, + }, + { + name: "Only leaf certificate", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + chainPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCertBytes}) + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: string(chainPEM), + }, + } + }, + expectSuccess: false, + }, + { + name: "Malformed PEM data", + config: &TrustRootConfig{ + CustomRoots: []*x509.Certificate{trustedRootCert}, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: "invalid-pem-data", + }, + } + }, + expectSuccess: false, + }, + { + name: "No custom roots and TUF disabled", + config: &TrustRootConfig{ + CustomRoots: nil, + UseTUFDefault: false, + }, + setupClient: func() *client.Rekor { + chainPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCertBytes}) + return &client.Rekor{ + Pubkey: &mock.PubkeyClient{ + PEMPubKey: string(chainPEM), + }, + } + }, + expectSuccess: false, + }, } - realTrustRootConfig := &TrustRootConfig{} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mClient := tc.setupClient() + trustedRoots, err := GetTrustedRoots(context.Background(), tc.config, mClient) + if err != nil && tc.expectSuccess { + t.Fatalf("failed to get trusted roots: %v", err) + } + if !tc.expectSuccess && tc.name == "No custom roots and TUF disabled" { + if err == nil { + t.Errorf("expected error for no trusted roots but got none") + } + return + } + if trustedRoots == nil && tc.expectSuccess { + t.Fatalf("trusted roots should not be nil") + } - _, err = GetLogVerifier(context.Background(), &mClient, realTrustRootConfig) - if err == nil { - t.Fatalf("expected error due to unknown authority, but got a verifier") + verifier, err := GetLogVerifier(context.Background(), mClient, trustedRoots) + if tc.expectSuccess { + if err != nil { + t.Errorf("expected success but got error: %v", err) + } + if verifier == nil { + t.Error("expected verifier but got nil") + } else { + pubKey, err := verifier.PublicKey() + if err != nil { + t.Errorf("failed to get public key from verifier: %v", err) + } + if tc.name == "Valid chain with direct root" { + if err := cryptoutils.EqualKeys(directLeafKey.Public(), pubKey); err != nil { + t.Errorf("public keys don't match: %v", err) + } + } + } + } else { + if err == nil { + t.Errorf("expected error but got success") + } + } + }) } +} - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +func TestVerifyLogEntryCertChain(t *testing.T) { + trustedRootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate trusted root key: %v", err) + } + trustedRootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Trusted Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + IsCA: true, + BasicConstraintsValid: true, + } + trustedRootCertBytes, err := generateCert(trustedRootTemplate, trustedRootTemplate, &trustedRootKey.PublicKey, trustedRootKey) + if err != nil { + t.Fatalf("failed to create trusted root certificate: %v", err) + } + trustedRootCert, err := x509.ParseCertificate(trustedRootCertBytes) if err != nil { - t.Fatalf("unexpected error generating key: %v", err) + t.Fatalf("failed to parse trusted root certificate: %v", err) } - mockTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(12345), - Subject: pkix.Name{CommonName: "test.example.com"}, - NotBefore: time.Now(), + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate leaf key: %v", err) + } + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: "rekor.sigstore.dev"}, + NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, } - - mockCertBytes, err := x509.CreateCertificate(rand.Reader, mockTemplate, mockTemplate, &key.PublicKey, key) + leafCertBytes, err := generateCert(leafTemplate, trustedRootTemplate, &leafKey.PublicKey, trustedRootKey) if err != nil { - t.Fatalf("unexpected error creating certificate: %v", err) + t.Fatalf("failed to create leaf certificate: %v", err) } - mockCert, err := x509.ParseCertificate(mockCertBytes) + untrustedRootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - t.Fatalf("failed to parse created certificate: %v", err) + t.Fatalf("failed to generate untrusted root key: %v", err) } - - mockCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: mockCertBytes}) - - mockTrustRootConfig := &TrustRootConfig{ - CustomRoots: []*x509.Certificate{mockCert}, + untrustedRootTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(4), + Subject: pkix.Name{CommonName: "Untrusted Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + IsCA: true, + BasicConstraintsValid: true, } - - mClient.Pubkey = &mock.PubkeyClient{ - PEMPubKey: string(mockCertPEM), + untrustedRootCertBytes, err := generateCert(untrustedRootTemplate, untrustedRootTemplate, &untrustedRootKey.PublicKey, untrustedRootKey) + if err != nil { + t.Fatalf("failed to create untrusted root certificate: %v", err) } - - _, err = GetLogVerifier(context.Background(), &mClient, &TrustRootConfig{}) - if err == nil { - t.Fatalf("expected error due to unknown authority, but got a verifier") + untrustedRootCert, err := x509.ParseCertificate(untrustedRootCertBytes) + if err != nil { + t.Fatalf("failed to parse untrusted root certificate: %v", err) } - verifier, err := GetLogVerifier(context.Background(), &mClient, mockTrustRootConfig) + untrustedLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - t.Fatalf("unexpected error getting log verifier with mock certificate: %v", err) + t.Fatalf("failed to generate untrusted leaf key: %v", err) + } + untrustedLeafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(5), + Subject: pkix.Name{CommonName: "untrusted-rekor.sigstore.dev"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, } + untrustedLeafCertBytes, err := generateCert(untrustedLeafTemplate, untrustedRootTemplate, &untrustedLeafKey.PublicKey, untrustedRootKey) + if err != nil { + t.Fatalf("failed to create untrusted leaf certificate: %v", err) + } + + certPool := x509.NewCertPool() + certPool.AddCert(trustedRootCert) + + untrustedCertPool := x509.NewCertPool() + untrustedCertPool.AddCert(untrustedRootCert) - verifierPubKey, _ := verifier.PublicKey() - if err := cryptoutils.EqualKeys(key.Public(), verifierPubKey); err != nil { - t.Fatalf("expected equal keys: %v", err) + tests := []struct { + name string + entry models.LogEntryAnon + certPool *x509.CertPool + expectSuccess bool + }{ + { + name: "Valid certificate chain", + entry: func() models.LogEntryAnon { + apiVersion := "0.0.1" + content := leafCertBytes + dataContent := []byte("test data") + signer, err := signature.LoadECDSASignerVerifier(leafKey, crypto.SHA256) + if err != nil { + t.Fatalf("failed to create signer: %v", err) + } + sig, err := signer.SignMessage(bytes.NewReader(dataContent)) + if err != nil { + t.Fatalf("failed to sign data: %v", err) + } + contentBase64 := strfmt.Base64(content) + sigBase64 := strfmt.Base64(sig) + dataContentBase64 := strfmt.Base64(dataContent) + + spec := models.RekordV001Schema{ + Signature: &models.RekordV001SchemaSignature{ + Content: &sigBase64, + PublicKey: &models.RekordV001SchemaSignaturePublicKey{ + Content: &contentBase64, + }, + }, + Data: &models.RekordV001SchemaData{ + Content: dataContentBase64, + }, + } + rekord := models.Rekord{ + APIVersion: &apiVersion, + Spec: spec, + } + bodyBytes, err := json.Marshal(rekord) + if err != nil { + t.Fatalf("failed to marshal rekord: %v", err) + } + body := base64.StdEncoding.EncodeToString(bodyBytes) + logIndex := int64(1) + return models.LogEntryAnon{ + Body: body, + LogIndex: &logIndex, + } + }(), + certPool: certPool, + expectSuccess: true, + }, + { + name: "Untrusted certificate chain", + entry: func() models.LogEntryAnon { + apiVersion := "0.0.1" + content := untrustedLeafCertBytes + dataContent := []byte("test data") + signer, err := signature.LoadECDSASignerVerifier(untrustedLeafKey, crypto.SHA256) + if err != nil { + t.Fatalf("failed to create signer: %v", err) + } + sig, err := signer.SignMessage(bytes.NewReader(dataContent)) + if err != nil { + t.Fatalf("failed to sign data: %v", err) + } + contentBase64 := strfmt.Base64(content) + sigBase64 := strfmt.Base64(sig) + dataContentBase64 := strfmt.Base64(dataContent) + + spec := models.RekordV001Schema{ + Signature: &models.RekordV001SchemaSignature{ + Content: &sigBase64, + PublicKey: &models.RekordV001SchemaSignaturePublicKey{ + Content: &contentBase64, + }, + }, + Data: &models.RekordV001SchemaData{ + Content: dataContentBase64, + }, + } + specBytes, err := json.Marshal(spec) + if err != nil { + t.Fatalf("failed to marshal spec: %v", err) + } + rekord := models.Rekord{ + APIVersion: &apiVersion, + Spec: specBytes, + } + bodyBytes, err := json.Marshal(rekord) + if err != nil { + t.Fatalf("failed to marshal rekord: %v", err) + } + body := base64.StdEncoding.EncodeToString(bodyBytes) + logIndex := int64(2) + return models.LogEntryAnon{ + Body: body, + LogIndex: &logIndex, + } + }(), + certPool: certPool, + expectSuccess: false, + }, + { + name: "Empty certificate chain", + entry: func() models.LogEntryAnon { + apiVersion := "0.0.1" + rekord := models.Rekord{ + APIVersion: &apiVersion, + Spec: []byte(`{}`), + } + bodyBytes, err := json.Marshal(rekord) + if err != nil { + t.Fatalf("failed to marshal rekord: %v", err) + } + body := base64.StdEncoding.EncodeToString(bodyBytes) + logIndex := int64(0) + return models.LogEntryAnon{ + Body: body, + LogIndex: &logIndex, + } + }(), + certPool: certPool, + expectSuccess: false, + }, + { + name: "Invalid certificate data", + entry: func() models.LogEntryAnon { + apiVersion := "0.0.1" + invalidCert := strfmt.Base64([]byte("invalid-cert-data")) + sig := strfmt.Base64([]byte("dummy-signature")) + data := strfmt.Base64([]byte("dummy-data")) + spec := models.RekordV001Schema{ + Signature: &models.RekordV001SchemaSignature{ + Content: &sig, + PublicKey: &models.RekordV001SchemaSignaturePublicKey{ + Content: &invalidCert, + }, + }, + Data: &models.RekordV001SchemaData{ + Content: data, + }, + } + specBytes, err := json.Marshal(spec) + if err != nil { + t.Fatalf("failed to marshal spec: %v", err) + } + rekord := models.Rekord{ + APIVersion: &apiVersion, + Spec: specBytes, + } + bodyBytes, err := json.Marshal(rekord) + if err != nil { + t.Fatalf("failed to marshal rekord: %v", err) + } + body := base64.StdEncoding.EncodeToString(bodyBytes) + logIndex := int64(0) + return models.LogEntryAnon{ + Body: body, + LogIndex: &logIndex, + } + }(), + certPool: certPool, + expectSuccess: false, + }, } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := VerifyLogEntryCertChain(tc.entry, tc.certPool) + if result != tc.expectSuccess { + t.Errorf("expected %v but got %v", tc.expectSuccess, result) + } + }) + } }