From e59faeef1ce90c58dbcdb23da20305ff89ca6db0 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. Sigstore-go provides a way to check for a trusted root and automatically use it if available, but can also fetch individual targets as needed if the provided TUF mirror does not supply a trusted_root.json. This change is intended to be backwards compatible and transparent. TODO: - e2e tests - deprecate `cosign initialize` Signed-off-by: Colleen Murphy --- .../fulcio/fulcioverifier/fulcioverifier.go | 2 +- cmd/cosign/cli/sign/sign.go | 2 +- cmd/cosign/cli/verify/verify.go | 6 +- cmd/cosign/cli/verify/verify_attestation.go | 6 +- cmd/cosign/cli/verify/verify_blob.go | 6 +- .../cli/verify/verify_blob_attestation.go | 6 +- .../cosign/fulcio/fulcioroots/fulcioroots.go | 45 +----- pkg/cosign/ctlog.go | 47 +++--- pkg/cosign/ctlog_test.go | 5 +- pkg/cosign/fulcio.go | 138 +++++++++++++++++ pkg/cosign/tlog.go | 69 +++++---- pkg/cosign/tlog_test.go | 5 +- pkg/cosign/tsa.go | 142 ++++++++++-------- pkg/cosign/tsa_test.go | 7 +- pkg/cosign/tuf.go | 91 +++++++++++ pkg/cosign/verify.go | 3 +- pkg/cosign/verify_sct.go | 5 +- pkg/cosign/verify_sct_test.go | 12 +- pkg/cosign/verify_test.go | 11 +- 19 files changed, 401 insertions(+), 207 deletions(-) create mode 100644 pkg/cosign/fulcio.go create mode 100644 pkg/cosign/tuf.go diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 8646bb298bde..b00566064ba0 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -33,7 +33,7 @@ func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerV } // Grab the PublicKeys for the CTFE, either from tuf or env. - pubKeys, err := cosign.GetCTLogPubs(ctx) + pubKeys, err := cosign.GetCTLogPubs() if err != nil { return nil, fmt.Errorf("getting CTFE public keys: %w", err) } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1289e7b1bb49..b5041a08650e 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -505,7 +505,7 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin return nil, err } if contains { - pubKeys, err := cosign.GetCTLogPubs(ctx) + pubKeys, err := cosign.GetCTLogPubs() if err != nil { return nil, fmt.Errorf("getting CTLog public keys: %w", err) } diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 17fd63e83303..429a7b8d36de 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(c.TSACertChainPath) if err != nil { return nil, fmt.Errorf("unable to load TSA certificates: %w", err) } @@ -169,7 +169,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } // This performs an online fetch of the Rekor public keys, but this is needed // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + co.RekorPubKeys, err = cosign.GetRekorPubs() if err != nil { return fmt.Errorf("getting Rekor public keys: %w", err) } @@ -185,7 +185,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { // Ignore Signed Certificate Timestamp if the flag is set or a key is provided if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + co.CTLogPubKeys, err = cosign.GetCTLogPubs() if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) } diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 93c27690455e..466cc1d7f18e 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(c.TSACertChainPath) if err != nil { return nil, fmt.Errorf("unable to load TSA certificates: %w", err) } @@ -125,7 +125,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } // Ignore Signed Certificate Timestamp if the flag is set or a key is provided if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + co.CTLogPubKeys, err = cosign.GetCTLogPubs() if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) } @@ -151,7 +151,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } // This performs an online fetch of the Rekor public keys, but this is needed // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + co.RekorPubKeys, err = cosign.GetRekorPubs() if err != nil { return fmt.Errorf("getting Rekor public keys: %w", err) } diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80b..ff394a22f89f 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(c.TSACertChainPath) if err != nil { return nil, fmt.Errorf("unable to load TSA certificates: %w", err) } @@ -161,7 +161,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } // This performs an online fetch of the Rekor public keys, but this is needed // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + co.RekorPubKeys, err = cosign.GetRekorPubs() if err != nil { return fmt.Errorf("getting Rekor public keys: %w", err) } @@ -294,7 +294,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { // Ignore Signed Certificate Timestamp if the flag is set or a key is provided if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + co.CTLogPubKeys, err = cosign.GetCTLogPubs() if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 3f2c33cc63bf..75f018a8bebb 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(c.TSACertChainPath) if err != nil { return fmt.Errorf("unable to load or get TSA certificates: %w", err) } @@ -179,7 +179,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } // This performs an online fetch of the Rekor public keys, but this is needed // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + co.RekorPubKeys, err = cosign.GetRekorPubs() if err != nil { return fmt.Errorf("getting Rekor public keys: %w", err) } @@ -192,7 +192,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st // Ignore Signed Certificate Timestamp if the flag is set or a key is provided if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + co.CTLogPubKeys, err = cosign.GetCTLogPubs() if err != nil { return fmt.Errorf("getting ctlog public keys: %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..9b064a996381 100644 --- a/pkg/cosign/ctlog.go +++ b/pkg/cosign/ctlog.go @@ -15,13 +15,11 @@ package cosign import ( - "context" "errors" "fmt" - "os" "github.com/sigstore/cosign/v2/pkg/cosign/env" - "github.com/sigstore/sigstore/pkg/tuf" + "github.com/sigstore/sigstore-go/pkg/root" ) // This is the CT log public key target name @@ -32,37 +30,36 @@ var ctPublicKeyStr = `ctfe.pub` // By default the public keys comes from TUF, but you can override this for test // purposes by using an env variable `SIGSTORE_CT_LOG_PUBLIC_KEY_FILE`. If using // an alternate, the file can be PEM, or DER format. -func GetCTLogPubs(ctx context.Context) (*TrustedTransparencyLogPubKeys, error) { +func GetCTLogPubs() (*TrustedTransparencyLogPubKeys, error) { publicKeys := NewTrustedTransparencyLogPubKeys() 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) - 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 addKeyFromFile(&publicKeys, altCTLogPub, "CT log public key") + } + + opts, err := setTUFOpts() + if err != nil { + return nil, err + } + + // Try getting keys from trusted_root.json + trustedRoot, _ := root.NewLiveTrustedRoot(opts) + if trustedRoot == nil { + // The TUF repository didn't have a trusted_root.json, try getting the individual target + return addKeyFromTUF(&publicKeys, opts, ctPublicKeyStr, "CT log public key") + } + + ctlogs := trustedRoot.CTLogs() + for _, ct := range ctlogs { + validity := checkValidityPeriod(ct.ValidityPeriodStart, ct.ValidityPeriodEnd) + if err := publicKeys.AddTransparencyLogPubKey(ct.PublicKey, validity); err != nil { + return nil, fmt.Errorf("error adding CT log public key: %w", err) } } if len(publicKeys.Keys) == 0 { return nil, errors.New("none of the CTLog public keys have been found") } - return &publicKeys, nil } diff --git a/pkg/cosign/ctlog_test.go b/pkg/cosign/ctlog_test.go index 2abb48f6b60c..18e4a5cda1b9 100644 --- a/pkg/cosign/ctlog_test.go +++ b/pkg/cosign/ctlog_test.go @@ -15,7 +15,6 @@ package cosign import ( - "context" "os" "testing" ) @@ -31,7 +30,7 @@ Nmo7M3bN7+dQddw9Ibc2R3SV8tzBZw0rST8FKcn4apJepcKM4qUpYUeNfw== ) func TestGetCTLogPubKeys(t *testing.T) { - keys, err := GetCTLogPubs(context.Background()) + keys, err := GetCTLogPubs() if err != nil { t.Fatalf("Unexpected error calling GetCTLogPubs, expected nil: %v", err) } @@ -61,7 +60,7 @@ func TestGetCTLogPubKeysAlt(t *testing.T) { } t.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", pkFile.Name()) - keys, err := GetCTLogPubs(context.Background()) + keys, err := GetCTLogPubs() if err != nil { t.Errorf("Unexpected error calling GetCTLogPubs, expected nil: %v", err) } diff --git a/pkg/cosign/fulcio.go b/pkg/cosign/fulcio.go new file mode 100644 index 000000000000..67df0f9f7da3 --- /dev/null +++ b/pkg/cosign/fulcio.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 ( + "bytes" + "crypto/x509" + "fmt" + "os" + + "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +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 + } + + trustedRoot, _ := root.NewLiveTrustedRoot(opts) + if trustedRoot == nil { + return getFulcioCertsFromTUF(opts) + } + cas := trustedRoot.FulcioCertificateAuthorities() + if len(cas) < 1 { + return nil, nil, fmt.Errorf("could not find fulcio certificate authorities") + } + rootPool := x509.NewCertPool() + var intermediatePool *x509.CertPool + for _, ca := range cas { + rootPool.AddCert(ca.Root) + for _, i := range ca.Intermediates { + if intermediatePool == nil { + intermediatePool = x509.NewCertPool() + } + intermediatePool.AddCert(i) + } + } + return rootPool, intermediatePool, 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 +} diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 83d6f61f1793..28fa2fa92f26 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -26,7 +26,6 @@ import ( "errors" "fmt" "hash" - "os" "strconv" "strings" @@ -47,8 +46,8 @@ import ( hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" "github.com/sigstore/rekor/pkg/types/intoto" intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" ) // This is the rekor transparency log public key target name @@ -58,7 +57,7 @@ var rekorTargetStr = `rekor.pub` // of the key according to TUF metadata, whether it's active or expired. type TransparencyLogPubKey struct { PubKey crypto.PublicKey - Status tuf.StatusKind + Status tufStatus } // This is a map of TransparencyLog public keys indexed by log ID that's used @@ -119,38 +118,36 @@ func intotoEntry(ctx context.Context, signature, pubKey []byte) (models.Proposed // There are two Env variable that can be used to override this behaviour: // SIGSTORE_REKOR_PUBLIC_KEY - If specified, location of the file that contains // the Rekor Public Key on local filesystem -func GetRekorPubs(ctx context.Context) (*TrustedTransparencyLogPubKeys, error) { +func GetRekorPubs() (*TrustedTransparencyLogPubKeys, error) { publicKeys := NewTrustedTransparencyLogPubKeys() altRekorPub := env.Getenv(env.VariableSigstoreRekorPublicKey) if altRekorPub != "" { - raw, err := os.ReadFile(altRekorPub) - 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 addKeyFromFile(&publicKeys, altRekorPub, "rekor public key") } - 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 + } + + // Try getting keys from trusted_root.json + trustedRoot, _ := root.NewLiveTrustedRoot(opts) + if trustedRoot == nil { + // The TUF repository didn't have a trusted_root.json, try getting the individual target + return addKeyFromTUF(&publicKeys, opts, rekorTargetStr, "rekor public key") } + tlogs := trustedRoot.RekorLogs() + for _, t := range tlogs { + validity := checkValidityPeriod(t.ValidityPeriodStart, t.ValidityPeriodEnd) + if err := publicKeys.AddTransparencyLogPubKey(t.PublicKey, validity); err != nil { + return nil, fmt.Errorf("error adding rekor public key: %w", err) + } + } + if len(publicKeys.Keys) == 0 { + return nil, fmt.Errorf("no rekor public keys found in trusted root") + } return &publicKeys, nil } @@ -163,7 +160,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), active); err != nil { return nil, fmt.Errorf("constructRekorPubKey: %w", err) } return &publicKeys, nil @@ -493,7 +490,7 @@ func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPu if err != nil { return fmt.Errorf("verifying signedEntryTimestamp: %w", err) } - if pubKey.Status != tuf.Active { + if pubKey.Status != active { ui.Infof(ctx, "Successfully verified Rekor entry using an expired verification key") } return nil @@ -505,10 +502,16 @@ 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 +func (t *TrustedTransparencyLogPubKeys) AddTransparencyLogPubKey(pem any, status tufStatus) 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 { diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index 55bd76bb9f6f..c4243ed1a262 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 ( @@ -37,7 +36,7 @@ var ( func TestGetRekorPubKeys(t *testing.T) { t.Setenv("TUF_ROOT", t.TempDir()) - keys, err := GetRekorPubs(context.Background()) + keys, err := GetRekorPubs() if err != nil { t.Fatalf("Unexpected error calling GetRekorPubs, expected nil: %v", err) } @@ -169,7 +168,7 @@ 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, active); err != nil { t.Fatalf("failed to add RSA key to transparency log public keys: %v", err) } diff --git a/pkg/cosign/tsa.go b/pkg/cosign/tsa.go index 9d1c17a33393..4b7a3bcc5cf0 100644 --- a/pkg/cosign/tsa.go +++ b/pkg/cosign/tsa.go @@ -16,14 +16,14 @@ package cosign import ( "bytes" - "context" "crypto/x509" "fmt" "os" "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" ) const ( @@ -38,94 +38,62 @@ 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(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") + // Try getting keys from trusted_root.json + trustedRoot, _ := root.NewLiveTrustedRoot(opts) + if trustedRoot == nil { + return getTSAKeysFromTUF(opts) + } + tsas := trustedRoot.TimestampingAuthorities() + if len(tsas) < 1 { + return nil, fmt.Errorf("could not find timestamp authorities") } - return &TSACertificates{ - LeafCert: leaves[0], - IntermediateCerts: intermediates, - RootCert: roots, + LeafCert: tsas[0].Leaf, + IntermediateCerts: tsas[0].Intermediates, + RootCert: []*x509.Certificate{tsas[0].Root}, }, nil } @@ -152,3 +120,47 @@ 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 +} diff --git a/pkg/cosign/tsa_test.go b/pkg/cosign/tsa_test.go index 9487d1d25fa9..7a2f7b692f05 100644 --- a/pkg/cosign/tsa_test.go +++ b/pkg/cosign/tsa_test.go @@ -15,7 +15,6 @@ package cosign import ( - "context" "errors" "os" "testing" @@ -68,7 +67,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(tempFile.Name()) if err != nil { t.Fatalf("Failed to get TSA certs from env: %v", err) } @@ -86,7 +85,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(tempFile.Name()) if err != nil { t.Fatalf("Failed to get TSA certs from path: %v", err) } @@ -108,7 +107,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(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..1f3b2fd86a5f --- /dev/null +++ b/pkg/cosign/tuf.go @@ -0,0 +1,91 @@ +// 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 ( + "fmt" + "os" + "time" + + "github.com/sigstore/sigstore-go/pkg/tuf" +) + +const ( + tufMirrorEnvVar = "TUF_MIRROR" + tufRootDirEnvVar = "TUF_ROOT" + tufRootJSONEnvVar = "TUF_ROOT_JSON" +) + +type tufStatus int + +const ( + active tufStatus = iota + inactive +) + +func addKeyFromFile(publicKeys *TrustedTransparencyLogPubKeys, name, description string) (*TrustedTransparencyLogPubKeys, error) { + raw, err := os.ReadFile(name) + if err != nil { + return nil, fmt.Errorf("error reading alternate %s file: %w", description, err) + } + if err := publicKeys.AddTransparencyLogPubKey(raw, active); err != nil { + return nil, fmt.Errorf("error adding %s: %w", description, err) + } + return publicKeys, nil +} + +func setTUFOpts() (*tuf.Options, error) { + opts := tuf.DefaultOptions() + if tufMirror := os.Getenv(tufMirrorEnvVar); tufMirror != "" { //nolint:forbidigo + opts.RepositoryBaseURL = tufMirror + } + if tufRootJSON := os.Getenv(tufRootJSONEnvVar); tufRootJSON != "" { //nolint:forbidigo + rootJSONBytes, err := os.ReadFile(tufRootJSON) + if err != nil { + return nil, fmt.Errorf("error reading TUF root.json: %w", err) + } + opts.Root = rootJSONBytes + } + if tufCacheDir := os.Getenv(tufRootDirEnvVar); tufCacheDir != "" { //nolint:forbidigo + opts.CachePath = tufCacheDir + } + return opts, nil +} + +func addKeyFromTUF(publicKeys *TrustedTransparencyLogPubKeys, opts *tuf.Options, name, description string) (*TrustedTransparencyLogPubKeys, error) { + tufClient, err := tuf.New(opts) + if err != nil { + return nil, fmt.Errorf("error creating TUF client: %w", err) + } + pubKeyBytes, err := tufClient.GetTarget(name) + if err != nil { + return nil, fmt.Errorf("error fetching %s: %w", description, err) + } + if err := publicKeys.AddTransparencyLogPubKey(pubKeyBytes, active); err != nil { + return nil, fmt.Errorf("error adding %s: %w", description, err) + } + return publicKeys, nil +} + +func checkValidityPeriod(start, end time.Time) tufStatus { + now := time.Now() + if now.Before(start) { + return inactive + } + if now.After(end) { + return inactive + } + return active +} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3ab5d76026ac..470a8a5e7165 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" ) @@ -1088,7 +1087,7 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { if err != nil { return false, err } - if pubKey.Status != tuf.Active { + if pubKey.Status != active { fmt.Fprintf(os.Stderr, "**Info** Successfully verified Rekor entry using an expired verification key\n") } diff --git a/pkg/cosign/verify_sct.go b/pkg/cosign/verify_sct.go index 1b904c2c4fd8..e5942cade5dc 100644 --- a/pkg/cosign/verify_sct.go +++ b/pkg/cosign/verify_sct.go @@ -29,7 +29,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,7 +109,7 @@ 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 { + if pubKeyMetadata.Status != active { fmt.Fprintf(os.Stderr, "**Info** Successfully verified embedded SCT using an expired verification key\n") } } @@ -134,7 +133,7 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru if err != nil { return fmt.Errorf("error verifying SCT") } - if pubKeyMetadata.Status != tuf.Active { + if pubKeyMetadata.Status != active { fmt.Fprintf(os.Stderr, "**Info** Successfully verified SCT using an expired verification key\n") } return nil diff --git a/pkg/cosign/verify_sct_test.go b/pkg/cosign/verify_sct_test.go index c341379e036a..25f577b8e2f0 100644 --- a/pkg/cosign/verify_sct_test.go +++ b/pkg/cosign/verify_sct_test.go @@ -47,7 +47,7 @@ func TestValidateAndUnpackCertWithSCT(t *testing.T) { rootPool.AddCert(chain[1]) // Grab the CTLog public keys - pubKeys, err := GetCTLogPubs(context.Background()) + pubKeys, err := GetCTLogPubs() if err != nil { t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) } @@ -71,7 +71,7 @@ func TestValidateAndUnpackCertWithSCT(t *testing.T) { t.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) // Grab the CTLog public keys again so we get them from env. - co.CTLogPubKeys, err = GetCTLogPubs(context.Background()) + co.CTLogPubKeys, err = GetCTLogPubs() if err != nil { t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) } @@ -114,7 +114,7 @@ func TestValidateAndUnpackCertWithDetachedSCT(t *testing.T) { } t.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) // Grab the CTLog public keys so we get them from env. - co.CTLogPubKeys, err = GetCTLogPubs(context.Background()) + co.CTLogPubKeys, err = GetCTLogPubs() if err != nil { t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) } @@ -267,7 +267,7 @@ func TestVerifySCTError(t *testing.T) { } writePubKey(t, string(pemKey)) // Grab the keys from TUF - pubKeys, err := GetCTLogPubs(context.Background()) + pubKeys, err := GetCTLogPubs() if err != nil { t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) } @@ -291,7 +291,7 @@ func TestVerifyEmbeddedSCT(t *testing.T) { } // Grab the keys from TUF - pubKeys, err := GetCTLogPubs(context.Background()) + pubKeys, err := GetCTLogPubs() if err != nil { t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) } @@ -305,7 +305,7 @@ func TestVerifyEmbeddedSCT(t *testing.T) { writePubKey(t, testdata.LogPublicKeyPEM) // Above writes the key to disk and sets up an env variable, so grab the // public keys again to get the env path. - pubKeys, err = GetCTLogPubs(context.Background()) + pubKeys, err = GetCTLogPubs() if err != nil { t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) } diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 30586d4a7595..c3dcdcd334a9 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, active) 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, active) 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, active); 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,7 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { Keys: map[string]TransparencyLogPubKey{ logID: { PubKey: rekorPublicKey, - Status: tuf.Active, + Status: active, }, }, } @@ -575,7 +574,7 @@ func TestImageSignatureVerificationWithRekor(t *testing.T) { Keys: map[string]TransparencyLogPubKey{ logID: { PubKey: nonMatchingPublicKey, - Status: tuf.Active, + Status: active, }, }, }