From 84e47128097f2f0c91c8723929f135055da3256b Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Wed, 14 Aug 2024 16:10:05 -0700 Subject: [PATCH] Upgrade to TUF v2 client Swap the use of the go-tuf v0.7.0 client from sigstore/sigstore to the v2.0.0 client from sigstore/sigstore-go. This change strictly adds logic to attempt to use the sigstore-go TUF client if possible, and falls back to the old TUF client. The new client can only fetch targets by name, not by custom metadata. This means that, on its own, the new client cannot support renaming/rotating keys, so the old client must be used to support that case. A future change will add support for fetching or reading a trusted_root.json file, which has better support for rotating keys. Once this later change is introduced, using the old client can be deprecated. The logic in this change works as follows: - if a path fo a key is provided by a SIGSTORE_ environment variable, read that file and use it (same as previously) - if new environment variables TUF_MIRROR and TUF_ROOT_JSON are set, use those to instantiate a TUF client that fetches keys from the given mirror - else, try reading the mirror URL from remote.json, which set set by `cosign initialize`, and try reading the root.json from the mirror's cache directory which may have been created by a previous TUF v2 run - if fetching keys using the new client with the given mirror did not work, fallback to the v1 client Also not that the use of the "status" field in the custom TUF metadata is removed, as it was only used for human-readable feedback. TODO: - e2e tests Signed-off-by: Colleen Murphy --- cmd/cosign/cli/verify/verify.go | 2 +- cmd/cosign/cli/verify/verify_attestation.go | 2 +- cmd/cosign/cli/verify/verify_blob.go | 2 +- .../cli/verify/verify_blob_attestation.go | 2 +- .../cosign/fulcio/fulcioroots/fulcioroots.go | 45 +---- pkg/cosign/ctlog.go | 46 +++-- pkg/cosign/fulcio.go | 137 ++++++++++++++ pkg/cosign/tlog.go | 73 ++++---- pkg/cosign/tlog_test.go | 5 +- pkg/cosign/tsa.go | 175 +++++++++++------- pkg/cosign/tsa_test.go | 6 +- pkg/cosign/tuf.go | 138 ++++++++++++++ pkg/cosign/verify.go | 6 +- pkg/cosign/verify_sct.go | 8 - pkg/cosign/verify_test.go | 9 +- 15 files changed, 456 insertions(+), 200 deletions(-) create mode 100644 pkg/cosign/fulcio.go create mode 100644 pkg/cosign/tuf.go diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 17fd63e83303..4c7b226ec5b8 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -88,7 +88,7 @@ func (c *VerifyCommand) loadTSACertificates(ctx context.Context) (*cosign.TSACer if c.TSACertChainPath == "" && !c.UseSignedTimestamps { return nil, fmt.Errorf("TSA certificate chain path not provided and use-signed-timestamps not set") } - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath) if err != nil { return nil, fmt.Errorf("unable to load TSA certificates: %w", err) } diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 93c27690455e..9abf537cffea 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -76,7 +76,7 @@ func (c *VerifyAttestationCommand) loadTSACertificates(ctx context.Context) (*co if c.TSACertChainPath == "" && !c.UseSignedTimestamps { return nil, fmt.Errorf("TSA certificate chain path not provided and use-signed-timestamps not set") } - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath) if err != nil { return nil, fmt.Errorf("unable to load TSA certificates: %w", err) } diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80b..873b70f7a3f3 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -73,7 +73,7 @@ func (c *VerifyBlobCmd) loadTSACertificates(ctx context.Context) (*cosign.TSACer if c.TSACertChainPath == "" && !c.UseSignedTimestamps { return nil, fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set") } - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath) if err != nil { return nil, fmt.Errorf("unable to load TSA certificates: %w", err) } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 3f2c33cc63bf..dac8e01cb6a0 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -160,7 +160,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } if c.TSACertChainPath != "" || c.UseSignedTimestamps { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath) if err != nil { return fmt.Errorf("unable to load or get TSA certificates: %w", err) } diff --git a/internal/pkg/cosign/fulcio/fulcioroots/fulcioroots.go b/internal/pkg/cosign/fulcio/fulcioroots/fulcioroots.go index 3b44da884201..7f8acc2853c9 100644 --- a/internal/pkg/cosign/fulcio/fulcioroots/fulcioroots.go +++ b/internal/pkg/cosign/fulcio/fulcioroots/fulcioroots.go @@ -16,15 +16,10 @@ package fulcioroots import ( - "bytes" "crypto/x509" - "fmt" - "os" "sync" - "github.com/sigstore/cosign/v2/pkg/cosign/env" - "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/fulcioroots" + "github.com/sigstore/cosign/v2/pkg/cosign" ) var ( @@ -64,41 +59,5 @@ func ReInit() error { } func initRoots() (*x509.CertPool, *x509.CertPool, error) { - rootPool := x509.NewCertPool() - // intermediatePool should be nil if no intermediates are found - var intermediatePool *x509.CertPool - - rootEnv := env.Getenv(env.VariableSigstoreRootFile) - if rootEnv != "" { - raw, err := os.ReadFile(rootEnv) - if err != nil { - return nil, nil, fmt.Errorf("error reading root PEM file: %w", err) - } - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(raw) - if err != nil { - return nil, nil, fmt.Errorf("error unmarshalling certificates: %w", err) - } - for _, cert := range certs { - // root certificates are self-signed - if bytes.Equal(cert.RawSubject, cert.RawIssuer) { - rootPool.AddCert(cert) - } else { - if intermediatePool == nil { - intermediatePool = x509.NewCertPool() - } - intermediatePool.AddCert(cert) - } - } - } else { - var err error - rootPool, err = fulcioroots.Get() - if err != nil { - return nil, nil, err - } - intermediatePool, err = fulcioroots.GetIntermediates() - if err != nil { - return nil, nil, err - } - } - return rootPool, intermediatePool, nil + return cosign.GetFulcioCerts() } diff --git a/pkg/cosign/ctlog.go b/pkg/cosign/ctlog.go index 9f2ebc3d5ec2..73dd01a096e0 100644 --- a/pkg/cosign/ctlog.go +++ b/pkg/cosign/ctlog.go @@ -16,16 +16,17 @@ package cosign import ( "context" - "errors" "fmt" - "os" "github.com/sigstore/cosign/v2/pkg/cosign/env" - "github.com/sigstore/sigstore/pkg/tuf" + tufv1 "github.com/sigstore/sigstore/pkg/tuf" ) // This is the CT log public key target name -var ctPublicKeyStr = `ctfe.pub` +var ( + ctPublicKeyStr = `ctfe.pub` + ctPublicKeyDesc = `CT log public key` +) // GetCTLogPubs retrieves trusted CTLog public keys from the embedded or cached // TUF root. If expired, makes a network call to retrieve the updated targets. @@ -37,32 +38,27 @@ func GetCTLogPubs(ctx context.Context) (*TrustedTransparencyLogPubKeys, error) { altCTLogPub := env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) if altCTLogPub != "" { - raw, err := os.ReadFile(altCTLogPub) - if err != nil { - return nil, fmt.Errorf("error reading alternate CTLog public key file: %w", err) - } - if err := publicKeys.AddTransparencyLogPubKey(raw, tuf.Active); err != nil { - return nil, fmt.Errorf("AddCTLogPubKey: %w", err) - } - } else { - tufClient, err := tuf.NewFromEnv(ctx) + err := addKeyFromFile(&publicKeys, altCTLogPub, ctPublicKeyDesc) if err != nil { - return nil, err - } - targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr}) - if err != nil { - return nil, err - } - for _, t := range targets { - if err := publicKeys.AddTransparencyLogPubKey(t.Target, t.Status); err != nil { - return nil, fmt.Errorf("AddCTLogPubKey: %w", err) - } + return nil, fmt.Errorf("error adding key from environment variable: %w", err) } + return &publicKeys, nil } - if len(publicKeys.Keys) == 0 { - return nil, errors.New("none of the CTLog public keys have been found") + opts, err := setTUFOpts() + if err != nil { + return nil, err } + _ = addKeyFromTUF(&publicKeys, opts, ctPublicKeyStr, ctPublicKeyDesc) + if len(publicKeys.Keys) == 0 { + err = legacyAddKeyFromTUF(ctx, &publicKeys, tufv1.CTFE, []string{ctPublicKeyStr}, ctPublicKeyDesc) + if err != nil { + return nil, fmt.Errorf("error adding ct log public key: %w", err) + } + if len(publicKeys.Keys) == 0 { + return nil, fmt.Errorf("none of the CTLog public keys have been found") + } + } return &publicKeys, nil } diff --git a/pkg/cosign/fulcio.go b/pkg/cosign/fulcio.go new file mode 100644 index 000000000000..c7ef93df18e7 --- /dev/null +++ b/pkg/cosign/fulcio.go @@ -0,0 +1,137 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "bytes" + "crypto/x509" + "fmt" + "os" + + "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/fulcioroots" +) + +const ( + // This is the root in the fulcio project. + fulcioTargetStr = `fulcio.crt.pem` + // This is the v1 migrated root. + fulcioV1TargetStr = `fulcio_v1.crt.pem` + // This is the untrusted v1 intermediate CA certificate, used or chain building. + fulcioV1IntermediateTargetStr = `fulcio_intermediate_v1.crt.pem` +) + +func GetFulcioCerts() (*x509.CertPool, *x509.CertPool, error) { + rootEnv := env.Getenv(env.VariableSigstoreRootFile) + if rootEnv != "" { + return getFulcioCertsFromFile(rootEnv) + } + + opts, err := setTUFOpts() + if err != nil { + return nil, nil, err + } + + roots, intermediates, _ := getFulcioCertsFromTUF(opts) + if roots == nil { + roots, intermediates, err = legacyGetFulcioCertsFromTUF() + if err != nil { + return nil, nil, fmt.Errorf("error getting fulcio certs: %w", err) + } + } + return roots, intermediates, nil +} + +func getFulcioCertsFromFile(path string) (*x509.CertPool, *x509.CertPool, error) { + rootPool := x509.NewCertPool() + // intermediatePool should be nil if no intermediates are found + var intermediatePool *x509.CertPool + raw, err := os.ReadFile(path) + if err != nil { + return nil, nil, fmt.Errorf("error reading root PEM file: %w", err) + } + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(raw) + if err != nil { + return nil, nil, fmt.Errorf("error unmarshalling certificates: %w", err) + } + for _, cert := range certs { + // root certificates are self-signed + if bytes.Equal(cert.RawSubject, cert.RawIssuer) { + rootPool.AddCert(cert) + } else { + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } + intermediatePool.AddCert(cert) + } + } + return rootPool, intermediatePool, nil +} + +func getFulcioCertsFromTUF(opts *tuf.Options) (*x509.CertPool, *x509.CertPool, error) { + tufClient, err := tuf.New(opts) + if err != nil { + return nil, nil, fmt.Errorf("error creating TUF client: %w", err) + } + rootPool := x509.NewCertPool() + fulcioCertBytes, _ := tufClient.GetTarget(fulcioTargetStr) + fulcioV1CertBytes, _ := tufClient.GetTarget(fulcioV1TargetStr) + if len(fulcioCertBytes) > 0 { + fulcioCert, err := cryptoutils.UnmarshalCertificatesFromPEM(fulcioCertBytes) + if err != nil { + return nil, nil, fmt.Errorf("error unmarshalling fulcio cert: %w", err) + } + for _, c := range fulcioCert { + rootPool.AddCert(c) + } + } + if len(fulcioV1CertBytes) > 0 { + fulcioV1Cert, err := cryptoutils.UnmarshalCertificatesFromPEM(fulcioV1CertBytes) + if err != nil { + return nil, nil, fmt.Errorf("error unmarshalling fulcio v1 cert: %w", err) + } + for _, c := range fulcioV1Cert { + rootPool.AddCert(c) + } + } + + var intermediatePool *x509.CertPool + fulcioIntermediateBytes, _ := tufClient.GetTarget(fulcioV1IntermediateTargetStr) + if len(fulcioIntermediateBytes) == 0 { + fulcioIntermediate, err := cryptoutils.UnmarshalCertificatesFromPEM(fulcioIntermediateBytes) + if err != nil { + return nil, nil, fmt.Errorf("error unmarshalling fulcio intermediate cert: %w", err) + } + intermediatePool = x509.NewCertPool() + for _, c := range fulcioIntermediate { + intermediatePool.AddCert(c) + } + } + return rootPool, intermediatePool, nil +} + +func legacyGetFulcioCertsFromTUF() (*x509.CertPool, *x509.CertPool, error) { + roots, err := fulcioroots.Get() + if err != nil { + return nil, nil, fmt.Errorf("error getting fulcio roots: %w", err) + } + intermediates, err := fulcioroots.GetIntermediates() + if err != nil { + return nil, nil, fmt.Errorf("error getting fulcio intermediates: %w", err) + } + return roots, intermediates, err +} diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 83d6f61f1793..512284f46123 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -26,7 +26,6 @@ import ( "errors" "fmt" "hash" - "os" "strconv" "strings" @@ -48,17 +47,19 @@ import ( "github.com/sigstore/rekor/pkg/types/intoto" intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" + tufv1 "github.com/sigstore/sigstore/pkg/tuf" ) // This is the rekor transparency log public key target name -var rekorTargetStr = `rekor.pub` +var ( + rekorTargetStr = `rekor.pub` + rekorDesc = `rekor public key` +) // TransparencyLogPubKey contains the ECDSA verification key and the current status // of the key according to TUF metadata, whether it's active or expired. type TransparencyLogPubKey struct { PubKey crypto.PublicKey - Status tuf.StatusKind } // This is a map of TransparencyLog public keys indexed by log ID that's used @@ -124,33 +125,28 @@ func GetRekorPubs(ctx context.Context) (*TrustedTransparencyLogPubKeys, error) { altRekorPub := env.Getenv(env.VariableSigstoreRekorPublicKey) if altRekorPub != "" { - raw, err := os.ReadFile(altRekorPub) + err := addKeyFromFile(&publicKeys, altRekorPub, rekorDesc) if err != nil { - return nil, fmt.Errorf("error reading alternate Rekor public key file: %w", err) - } - if err := publicKeys.AddTransparencyLogPubKey(raw, tuf.Active); err != nil { - return nil, fmt.Errorf("AddRekorPubKey: %w", err) - } - } else { - tufClient, err := tuf.NewFromEnv(ctx) - if err != nil { - return nil, err - } - targets, err := tufClient.GetTargetsByMeta(tuf.Rekor, []string{rekorTargetStr}) - if err != nil { - return nil, err - } - for _, t := range targets { - if err := publicKeys.AddTransparencyLogPubKey(t.Target, t.Status); err != nil { - return nil, fmt.Errorf("AddRekorPubKey: %w", err) - } + return nil, fmt.Errorf("error adding key from environment variable: %w", err) } + return &publicKeys, nil } - if len(publicKeys.Keys) == 0 { - return nil, errors.New("none of the Rekor public keys have been found") + opts, err := setTUFOpts() + if err != nil { + return nil, err } + _ = addKeyFromTUF(&publicKeys, opts, rekorTargetStr, rekorDesc) + if len(publicKeys.Keys) == 0 { + err = legacyAddKeyFromTUF(ctx, &publicKeys, tufv1.Rekor, []string{rekorTargetStr}, rekorDesc) + if err != nil { + return nil, fmt.Errorf("error adding rekor key from TUF: %w", err) + } + if len(publicKeys.Keys) == 0 { + return nil, fmt.Errorf("no rekor public keys found") + } + } return &publicKeys, nil } @@ -163,7 +159,7 @@ func rekorPubsFromClient(rekorClient *client.Rekor) (*TrustedTransparencyLogPubK if err != nil { return nil, fmt.Errorf("unable to fetch rekor public key from rekor: %w", err) } - if err := publicKeys.AddTransparencyLogPubKey([]byte(pubOK.Payload), tuf.Active); err != nil { + if err := publicKeys.AddTransparencyLogPubKey([]byte(pubOK.Payload)); err != nil { return nil, fmt.Errorf("constructRekorPubKey: %w", err) } return &publicKeys, nil @@ -219,7 +215,7 @@ func doUpload(ctx context.Context, rekorClient *client.Rekor, pe models.Proposed if err != nil { return nil, err } - return e, VerifyTLogEntryOffline(ctx, e, rekorPubsFromAPI) + return e, VerifyTLogEntryOffline(e, rekorPubsFromAPI) } return nil, err } @@ -443,7 +439,7 @@ func FindTlogEntry(ctx context.Context, rekorClient *client.Rekor, // VerifyTLogEntryOffline verifies a TLog entry against a map of trusted rekorPubKeys indexed // by log id. -func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPubKeys *TrustedTransparencyLogPubKeys) error { +func VerifyTLogEntryOffline(e *models.LogEntryAnon, rekorPubKeys *TrustedTransparencyLogPubKeys) error { if e.Verification == nil || e.Verification.InclusionProof == nil { return errors.New("inclusion proof not provided") } @@ -493,9 +489,6 @@ func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPu if err != nil { return fmt.Errorf("verifying signedEntryTimestamp: %w", err) } - if pubKey.Status != tuf.Active { - ui.Infof(ctx, "Successfully verified Rekor entry using an expired verification key") - } return nil } @@ -504,16 +497,22 @@ func NewTrustedTransparencyLogPubKeys() TrustedTransparencyLogPubKeys { } // constructRekorPubkey returns a log ID and RekorPubKey from a given -// byte-array representing the PEM-encoded Rekor key and a status. -func (t *TrustedTransparencyLogPubKeys) AddTransparencyLogPubKey(pemBytes []byte, status tuf.StatusKind) error { - pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pemBytes) - if err != nil { - return err +// byte-array representing the PEM-encoded Rekor key. +func (t *TrustedTransparencyLogPubKeys) AddTransparencyLogPubKey(pem any) error { + var pubKey crypto.PublicKey + var err error + if pemBytes, ok := pem.([]byte); ok { + pubKey, err = cryptoutils.UnmarshalPEMToPublicKey(pemBytes) + if err != nil { + return err + } + } else { + pubKey = pem.(crypto.PublicKey) } keyID, err := GetTransparencyLogID(pubKey) if err != nil { return err } - t.Keys[keyID] = TransparencyLogPubKey{PubKey: pubKey, Status: status} + t.Keys[keyID] = TransparencyLogPubKey{PubKey: pubKey} return nil } diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index 55bd76bb9f6f..7e90c347e48d 100644 --- a/pkg/cosign/tlog_test.go +++ b/pkg/cosign/tlog_test.go @@ -28,7 +28,6 @@ import ( ttestdata "github.com/google/certificate-transparency-go/trillian/testdata" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" ) var ( @@ -169,11 +168,11 @@ func TestVerifyTLogEntryOfflineFailsWithInvalidPublicKey(t *testing.T) { t.Fatalf("Unable to marshal RSA test key: %v", err) } rekorPubKeys := NewTrustedTransparencyLogPubKeys() - if err = rekorPubKeys.AddTransparencyLogPubKey(rsaPEM, tuf.Active); err != nil { + if err = rekorPubKeys.AddTransparencyLogPubKey(rsaPEM); err != nil { t.Fatalf("failed to add RSA key to transparency log public keys: %v", err) } - err = VerifyTLogEntryOffline(context.Background(), &models.LogEntryAnon{Verification: &models.LogEntryAnonVerification{InclusionProof: &models.InclusionProof{}}}, &rekorPubKeys) + err = VerifyTLogEntryOffline(&models.LogEntryAnon{Verification: &models.LogEntryAnonVerification{InclusionProof: &models.InclusionProof{}}}, &rekorPubKeys) if err == nil { t.Fatal("Wanted error got none") } diff --git a/pkg/cosign/tsa.go b/pkg/cosign/tsa.go index 9d1c17a33393..c766a4fda6e6 100644 --- a/pkg/cosign/tsa.go +++ b/pkg/cosign/tsa.go @@ -22,8 +22,9 @@ import ( "os" "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore-go/pkg/tuf" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" + tufv1 "github.com/sigstore/sigstore/pkg/tuf" ) const ( @@ -38,95 +39,57 @@ type TSACertificates struct { RootCert []*x509.Certificate } -type GetTargetStub func(ctx context.Context, usage tuf.UsageKind, names []string) ([]byte, error) - -func GetTufTargets(ctx context.Context, usage tuf.UsageKind, names []string) ([]byte, error) { - tufClient, err := tuf.NewFromEnv(ctx) - if err != nil { - return nil, fmt.Errorf("error creating TUF client: %w", err) - } - targets, err := tufClient.GetTargetsByMeta(usage, names) - if err != nil { - return nil, fmt.Errorf("error fetching targets by metadata with usage %v: %w", usage, err) - } - - var buffer bytes.Buffer - for _, target := range targets { - buffer.Write(target.Target) - buffer.WriteByte('\n') - } - return buffer.Bytes(), nil -} - -func isTufTargetExist(ctx context.Context, name string) (bool, error) { - tufClient, err := tuf.NewFromEnv(ctx) - if err != nil { - return false, fmt.Errorf("error creating TUF client: %w", err) - } - _, err = tufClient.GetTarget(name) - if err != nil { - return false, nil - } - return true, nil -} - // GetTSACerts retrieves trusted TSA certificates from the embedded or cached // TUF root. If expired, makes a network call to retrieve the updated targets. // By default, the certificates come from TUF, but you can override this for test // purposes by using an env variable `SIGSTORE_TSA_CERTIFICATE_FILE` or a file path // specified in `TSACertChainPath`. If using an alternate, the file should be in PEM format. -func GetTSACerts(ctx context.Context, certChainPath string, fn GetTargetStub) (*TSACertificates, error) { +func GetTSACerts(ctx context.Context, certChainPath string) (*TSACertificates, error) { altTSACert := env.Getenv(env.VariableSigstoreTSACertificateFile) var raw []byte var err error - var exists bool switch { case altTSACert != "": raw, err = os.ReadFile(altTSACert) case certChainPath != "": raw, err = os.ReadFile(certChainPath) - default: - certNames := []string{tsaLeafCertStr, tsaRootCertStr} - for i := 0; ; i++ { - intermediateCertStr := fmt.Sprintf(tsaIntermediateCertStrPattern, i) - exists, err = isTufTargetExist(ctx, intermediateCertStr) - if err != nil { - return nil, fmt.Errorf("error fetching TSA certificates: %w", err) - } - if !exists { - break - } - certNames = append(certNames, intermediateCertStr) - } - raw, err = fn(ctx, tuf.TSA, certNames) - if err != nil { - return nil, fmt.Errorf("error fetching TSA certificates: %w", err) - } } - if err != nil { return nil, fmt.Errorf("error reading TSA certificate file: %w", err) } + if len(raw) > 0 { + leaves, intermediates, roots, err := splitPEMCertificateChain(raw) + if err != nil { + return nil, fmt.Errorf("error splitting TSA certificates: %w", err) + } + if len(leaves) != 1 { + return nil, fmt.Errorf("TSA certificate chain must contain exactly one leaf certificate") + } - leaves, intermediates, roots, err := splitPEMCertificateChain(raw) - if err != nil { - return nil, fmt.Errorf("error splitting TSA certificates: %w", err) + if len(roots) == 0 { + return nil, fmt.Errorf("TSA certificate chain must contain at least one root certificate") + } + return &TSACertificates{ + LeafCert: leaves[0], + IntermediateCerts: intermediates, + RootCert: roots, + }, nil } - if len(leaves) != 1 { - return nil, fmt.Errorf("TSA certificate chain must contain exactly one leaf certificate") + opts, err := setTUFOpts() + if err != nil { + return nil, err } - if len(roots) == 0 { - return nil, fmt.Errorf("TSA certificate chain must contain at least one root certificate") + certs, err := getTSAKeysFromTUF(opts) + if err != nil { + certs, err = legacyGetTSAKeysFromTUF(ctx) } - - return &TSACertificates{ - LeafCert: leaves[0], - IntermediateCerts: intermediates, - RootCert: roots, - }, nil + if err != nil { + return nil, fmt.Errorf("could not find TSA certificates: %w", err) + } + return certs, nil } // splitPEMCertificateChain returns a list of leaf (non-CA) certificates, a certificate pool for @@ -152,3 +115,83 @@ func splitPEMCertificateChain(pem []byte) (leaves, intermediates, roots []*x509. return leaves, intermediates, roots, nil } + +func getTSAKeysFromTUF(opts *tuf.Options) (*TSACertificates, error) { + tufClient, err := tuf.New(opts) + if err != nil { + return nil, fmt.Errorf("error creating TUF client: %w", err) + } + leafCertBytes, err := tufClient.GetTarget(tsaLeafCertStr) + if err != nil { + return nil, fmt.Errorf("error fetching TSA leaf cert: %w", err) + } + rootCertBytes, err := tufClient.GetTarget(tsaRootCertStr) + if err != nil { + return nil, fmt.Errorf("error fetching TSA root CA cert: %w", err) + } + var intermediateChainBytes []byte + for i := 0; ; i++ { + intermediateCertStr := fmt.Sprintf(tsaIntermediateCertStrPattern, i) + intermediateCertBytes, _ := tufClient.GetTarget(intermediateCertStr) + if len(intermediateCertBytes) == 0 { + break + } + intermediateChainBytes = append(intermediateChainBytes, intermediateCertBytes...) + } + leafCert, err := cryptoutils.UnmarshalCertificatesFromPEM(leafCertBytes) + if err != nil { + return nil, fmt.Errorf("error unmarshalling TSA leaf cert: %w", err) + } + rootCert, err := cryptoutils.UnmarshalCertificatesFromPEM(rootCertBytes) + if err != nil { + return nil, fmt.Errorf("error unmarshalling TSA root CA cert: %w", err) + } + var intermediates []*x509.Certificate + if len(intermediateChainBytes) > 0 { + intermediates, err = cryptoutils.UnmarshalCertificatesFromPEM(intermediateChainBytes) + if err != nil { + return nil, fmt.Errorf("error unmarshalling intermediate certs: %w", err) + } + } + return &TSACertificates{ + LeafCert: leafCert[0], + IntermediateCerts: intermediates, + RootCert: rootCert, + }, nil +} + +func legacyGetTSAKeysFromTUF(ctx context.Context) (*TSACertificates, error) { + tufClient, err := tufv1.NewFromEnv(ctx) + if err != nil { + return nil, fmt.Errorf("error creating legacy TUF client: %w", err) + } + targets, err := tufClient.GetTargetsByMeta(tufv1.TSA, []string{tsaLeafCertStr, tsaRootCertStr}) + if err != nil { + return nil, fmt.Errorf("error fetching TSA certs: %w", err) + } + var buffer bytes.Buffer + for _, t := range targets { + buffer.Write(t.Target) + buffer.WriteByte('\n') + } + for i := 0; ; i++ { + target, err := tufClient.GetTarget(fmt.Sprintf(tsaIntermediateCertStrPattern, i)) + if err != nil { + break + } + buffer.Write(target) + buffer.WriteByte('\n') + } + if buffer.Len() == 0 { + return nil, fmt.Errorf("could not find TSA keys") + } + leaves, intermediates, roots, err := splitPEMCertificateChain(buffer.Bytes()) + if err != nil { + return nil, fmt.Errorf("error unmarshalling TSA certs: %w", err) + } + return &TSACertificates{ + LeafCert: leaves[0], + IntermediateCerts: intermediates, + RootCert: roots, + }, nil +} diff --git a/pkg/cosign/tsa_test.go b/pkg/cosign/tsa_test.go index 9487d1d25fa9..4789d37cce5b 100644 --- a/pkg/cosign/tsa_test.go +++ b/pkg/cosign/tsa_test.go @@ -68,7 +68,7 @@ func TestGetTSACertsFromEnv(t *testing.T) { os.Setenv("SIGSTORE_TSA_CERTIFICATE_FILE", tempFile.Name()) defer os.Unsetenv("SIGSTORE_TSA_CERTIFICATE_FILE") - tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name(), GetTufTargets) + tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name()) if err != nil { t.Fatalf("Failed to get TSA certs from env: %v", err) } @@ -86,7 +86,7 @@ func TestGetTSACertsFromPath(t *testing.T) { _, err = tempFile.Write([]byte(testLeafCert + "\n" + testRootCert)) require.NoError(t, err) - tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name(), GetTufTargets) + tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name()) if err != nil { t.Fatalf("Failed to get TSA certs from path: %v", err) } @@ -108,7 +108,7 @@ func TestGetTSACertsFromTUF(t *testing.T) { _, err = tempFile.Write([]byte(testLeafCert + "\n" + testRootCert)) require.NoError(t, err) - tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name(), GetTufTargets) + tsaCerts, err := GetTSACerts(context.Background(), tempFile.Name()) if err != nil { t.Fatalf("Failed to get TSA certs from TUF: %v", err) } diff --git a/pkg/cosign/tuf.go b/pkg/cosign/tuf.go new file mode 100644 index 000000000000..386a165fb80d --- /dev/null +++ b/pkg/cosign/tuf.go @@ -0,0 +1,138 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/sigstore/sigstore-go/pkg/tuf" + tufv1 "github.com/sigstore/sigstore/pkg/tuf" +) + +const ( + tufMirrorEnvVar = "TUF_MIRROR" + tufRootDirEnvVar = "TUF_ROOT" + tufRootJSONEnvVar = "TUF_ROOT_JSON" +) + +func addKeyFromFile(publicKeys *TrustedTransparencyLogPubKeys, name, description string) error { + raw, err := os.ReadFile(name) + if err != nil { + return fmt.Errorf("error reading alternate %s file: %w", description, err) + } + if err := publicKeys.AddTransparencyLogPubKey(raw); err != nil { + return fmt.Errorf("error adding %s: %w", description, err) + } + return nil +} + +func setTUFOpts() (*tuf.Options, error) { + opts := tuf.DefaultOptions() + if tufCacheDir := os.Getenv(tufRootDirEnvVar); tufCacheDir != "" { //nolint:forbidigo + opts.CachePath = tufCacheDir + } + err := setTUFMirror(opts) + if err != nil { + return nil, fmt.Errorf("error setting TUF mirror: %w", err) + } + err = setTUFRootJSON(opts) + if err != nil { + return nil, fmt.Errorf("error setting root: %w", err) + } + return opts, nil +} + +func addKeyFromTUF(publicKeys *TrustedTransparencyLogPubKeys, opts *tuf.Options, name, description string) error { + tufClient, err := tuf.New(opts) + if err != nil { + return fmt.Errorf("error creating TUF client: %w", err) + } + pubKeyBytes, err := tufClient.GetTarget(name) + if err != nil { + return fmt.Errorf("error fetching %s: %w", description, err) + } + if err := publicKeys.AddTransparencyLogPubKey(pubKeyBytes); err != nil { + return fmt.Errorf("error adding %s: %w", description, err) + } + return nil +} + +func legacyAddKeyFromTUF(ctx context.Context, publicKeys *TrustedTransparencyLogPubKeys, kind tufv1.UsageKind, names []string, description string) error { + tufClient, err := tufv1.NewFromEnv(ctx) + if err != nil { + return fmt.Errorf("error creating legacy TUF client: %w", err) + } + targets, err := tufClient.GetTargetsByMeta(kind, names) + if err != nil { + return fmt.Errorf("error fetching %s: %w", description, err) + } + for _, t := range targets { + if err := publicKeys.AddTransparencyLogPubKey(t.Target); err != nil { + return fmt.Errorf("error adding %s: %w", description, err) + } + } + return nil +} + +func setTUFMirror(opts *tuf.Options) error { + if tufMirror := os.Getenv(tufMirrorEnvVar); tufMirror != "" { //nolint:forbidigo + opts.RepositoryBaseURL = tufMirror + return nil + } + // try using the mirror set by `cosign initialize` + cachedRemote := filepath.Join(opts.CachePath, "remote.json") + remoteBytes, err := os.ReadFile(cachedRemote) + if os.IsNotExist(err) { + // could not find remote.json, use default mirror + return nil + } + if err != nil { + return fmt.Errorf("error reading remote.json: %w", err) + } + remote := make(map[string]string) + err = json.Unmarshal(remoteBytes, &remote) + if err != nil { + return fmt.Errorf("error unmarshalling remote.json: %w", err) + } + opts.RepositoryBaseURL = remote["mirror"] + return nil +} + +func setTUFRootJSON(opts *tuf.Options) error { + // TUF root set by TUF_ROOT_JSON + if tufRootJSON := os.Getenv(tufRootJSONEnvVar); tufRootJSON != "" { //nolint:forbidigo + rootJSONBytes, err := os.ReadFile(tufRootJSON) + if err != nil { + return fmt.Errorf("error reading root.json given by TUF_ROOT_JSON") + } + opts.Root = rootJSONBytes + return nil + } + // Look for cached root.json + cachedRootJSON := filepath.Join(opts.CachePath, tuf.URLToPath(opts.RepositoryBaseURL), "root.json") + if _, err := os.Stat(cachedRootJSON); !os.IsNotExist(err) { + rootJSONBytes, err := os.ReadFile(cachedRootJSON) + if err != nil { + return fmt.Errorf("error reading cached root.json") + } + opts.Root = rootJSONBytes + } + // Use defaults + return nil +} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3ab5d76026ac..09d9c433126a 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -69,7 +69,6 @@ import ( "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" "github.com/sigstore/sigstore/pkg/signature/options" - "github.com/sigstore/sigstore/pkg/tuf" tsaverification "github.com/sigstore/timestamp-authority/pkg/verification" ) @@ -457,7 +456,7 @@ func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys * entryVerificationErrs := make([]string, 0) for _, e := range tlogEntries { entry := e - if err := VerifyTLogEntryOffline(ctx, &entry, rekorPubKeys); err != nil { + if err := VerifyTLogEntryOffline(&entry, rekorPubKeys); err != nil { entryVerificationErrs = append(entryVerificationErrs, err.Error()) continue } @@ -1088,9 +1087,6 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { if err != nil { return false, err } - if pubKey.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") - } payload, err := sig.Payload() if err != nil { diff --git a/pkg/cosign/verify_sct.go b/pkg/cosign/verify_sct.go index 1b904c2c4fd8..d3164ee0d4a7 100644 --- a/pkg/cosign/verify_sct.go +++ b/pkg/cosign/verify_sct.go @@ -21,7 +21,6 @@ import ( "encoding/json" "errors" "fmt" - "os" ct "github.com/google/certificate-transparency-go" ctx509 "github.com/google/certificate-transparency-go/x509" @@ -29,7 +28,6 @@ import ( "github.com/sigstore/cosign/v2/pkg/cosign/fulcioverifier/ctutil" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" ) // ContainsSCT checks if the certificate contains embedded SCTs. cert can either be @@ -110,9 +108,6 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru if err != nil { return fmt.Errorf("error verifying embedded SCT: %w", err) } - if pubKeyMetadata.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n") - } } return nil } @@ -134,9 +129,6 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru if err != nil { return fmt.Errorf("error verifying SCT") } - if pubKeyMetadata.Status != tuf.Active { - fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") - } return nil } diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 30586d4a7595..deeddc18a361 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -60,7 +60,6 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" - "github.com/sigstore/sigstore/pkg/tuf" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/transparency-dev/merkle/rfc6962" @@ -306,7 +305,7 @@ func TestVerifyImageSignatureWithNoChain(t *testing.T) { rekorBundle := CreateTestBundle(ctx, t, sv, leaf) pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) rekorPubKeys := NewTrustedTransparencyLogPubKeys() - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) + rekorPubKeys.AddTransparencyLogPubKey(pemBytes) opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) @@ -350,7 +349,7 @@ func TestVerifyImageSignatureWithInvalidPublicKeyType(t *testing.T) { pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) rekorPubKeys := NewTrustedTransparencyLogPubKeys() // Add one valid key here. - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) + rekorPubKeys.AddTransparencyLogPubKey(pemBytes) opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) @@ -371,7 +370,7 @@ func TestVerifyImageSignatureWithInvalidPublicKeyType(t *testing.T) { if err != nil { t.Fatalf("Unable to marshal RSA test key: %v", err) } - if err = rekorPubKeys.AddTransparencyLogPubKey(rsaPEM, tuf.Active); err != nil { + if err = rekorPubKeys.AddTransparencyLogPubKey(rsaPEM); err != nil { t.Fatalf("failed to add RSA key to transparency log public keys: %v", err) } verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, @@ -564,7 +563,6 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { Keys: map[string]TransparencyLogPubKey{ logID: { PubKey: rekorPublicKey, - Status: tuf.Active, }, }, } @@ -575,7 +573,6 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { Keys: map[string]TransparencyLogPubKey{ logID: { PubKey: nonMatchingPublicKey, - Status: tuf.Active, }, }, }