From 4c59c581a5bddadcfa7c959a9375dd22dd0d3a6d Mon Sep 17 00:00:00 2001 From: SequeI Date: Wed, 11 Feb 2026 10:02:08 +0000 Subject: [PATCH] fix: use sigstore-go TUF client for verify to match initialize gitsign initialize writes the TUF cache in sigstore-go format, but verify was reading using the old sigstore/sigstore TUF client which expects a different cache layout. This caused verify to fall back to its expired embedded root. Switch all TUF reads to sigstore-go so initialize and verify use the same cache. Signed-off-by: SequeI --- e2e/sign_test.go | 7 +- go.mod | 4 +- internal/commands/version/version.go | 2 +- internal/e2e/offline_test.go | 11 +- internal/e2e/sigstoreroot_test.go | 69 ++++++++++++ internal/fulcio/fulcioroots/fulcioroots.go | 46 +++----- internal/gitsign/gitsign.go | 13 ++- internal/sigstoreroot/root.go | 117 +++++++++++++++++++++ internal/sigstoreroot/root_test.go | 77 ++++++++++++++ pkg/git/verifier.go | 16 ++- pkg/rekor/rekor.go | 12 ++- pkg/version/version.go | 2 +- pkg/version/version_test.go | 2 +- 13 files changed, 329 insertions(+), 49 deletions(-) create mode 100644 internal/e2e/sigstoreroot_test.go create mode 100644 internal/sigstoreroot/root.go create mode 100644 internal/sigstoreroot/root_test.go diff --git a/e2e/sign_test.go b/e2e/sign_test.go index 3432c194..ec47b3ea 100644 --- a/e2e/sign_test.go +++ b/e2e/sign_test.go @@ -30,6 +30,7 @@ import ( "github.com/go-git/go-git/v5/storage/memory" "github.com/sigstore/cosign/v3/pkg/providers" "github.com/sigstore/gitsign/internal/git/gittest" + "github.com/sigstore/gitsign/internal/sigstoreroot" "github.com/sigstore/gitsign/pkg/fulcio" gsgit "github.com/sigstore/gitsign/pkg/git" "github.com/sigstore/gitsign/pkg/gitsign" @@ -104,7 +105,11 @@ func TestSign(t *testing.T) { sig := []byte(commit.PGPSignature) // Verify the commit - verifier, err := gsgit.NewDefaultVerifier(ctx) + trustedRoot, err := sigstoreroot.FetchTrustedRoot() + if err != nil { + t.Fatalf("error fetching trusted root: %v", err) + } + verifier, err := gsgit.NewVerifierFromTrustedRoot(trustedRoot) if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 78747f84..a8424521 100644 --- a/go.mod +++ b/go.mod @@ -16,12 +16,14 @@ require ( github.com/jonboulle/clockwork v0.5.0 github.com/mattn/go-tty v0.0.7 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/secure-systems-lab/go-securesystemslib v0.10.0 github.com/sigstore/cosign/v3 v3.0.4 github.com/sigstore/fulcio v1.8.5 github.com/sigstore/protobuf-specs v0.5.0 github.com/sigstore/rekor v1.5.0 github.com/sigstore/sigstore v1.10.4 + github.com/sigstore/sigstore-go v1.1.4 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 golang.org/x/crypto v0.47.0 @@ -206,7 +208,6 @@ require ( github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect - github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect @@ -215,7 +216,6 @@ require ( github.com/sergi/go-diff v1.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect - github.com/sigstore/sigstore-go v1.1.4 // indirect github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.3 // indirect github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.3 // indirect github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.3 // indirect diff --git a/internal/commands/version/version.go b/internal/commands/version/version.go index 88847d20..fe8a1f6a 100644 --- a/internal/commands/version/version.go +++ b/internal/commands/version/version.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package version +package version // nolint:revive import ( "encoding/json" diff --git a/internal/e2e/offline_test.go b/internal/e2e/offline_test.go index 97bb51df..5ea8c037 100644 --- a/internal/e2e/offline_test.go +++ b/internal/e2e/offline_test.go @@ -24,17 +24,20 @@ import ( "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" "github.com/sigstore/gitsign/internal/git/gittest" + "github.com/sigstore/gitsign/internal/sigstoreroot" "github.com/sigstore/gitsign/pkg/git" "github.com/sigstore/gitsign/pkg/rekor" - "github.com/sigstore/sigstore/pkg/tuf" ) func TestVerifyOffline(t *testing.T) { ctx := context.Background() - // Initialize to prod root. - tuf.Initialize(ctx, tuf.DefaultRemoteRoot, nil) - root, intermediate, err := fulcioroots.New(x509.NewCertPool(), fulcioroots.FromTUF(ctx)) + trustedRoot, err := sigstoreroot.FetchTrustedRoot() + if err != nil { + t.Fatalf("error fetching trusted root: %v", err) + } + + root, intermediate, err := fulcioroots.New(x509.NewCertPool(), fulcioroots.FromTrustedRoot(trustedRoot)) if err != nil { t.Fatalf("error getting certificate root: %v", err) } diff --git a/internal/e2e/sigstoreroot_test.go b/internal/e2e/sigstoreroot_test.go new file mode 100644 index 00000000..662edd93 --- /dev/null +++ b/internal/e2e/sigstoreroot_test.go @@ -0,0 +1,69 @@ +// Copyright 2026 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. + +//go:build e2e +// +build e2e + +package e2e + +import ( + "testing" + + "github.com/sigstore/gitsign/internal/sigstoreroot" +) + +func TestFetchTrustedRoot(t *testing.T) { + trustedRoot, err := sigstoreroot.FetchTrustedRoot() + if err != nil { + t.Fatalf("FetchTrustedRoot() error = %v", err) + } + if trustedRoot == nil { + t.Fatal("FetchTrustedRoot() returned nil") + } + + ctPubs, err := sigstoreroot.GetCTLogPubs(trustedRoot) + if err != nil { + t.Fatalf("GetCTLogPubs() error = %v", err) + } + if len(ctPubs.Keys) == 0 { + t.Fatal("GetCTLogPubs() returned no keys") + } + + rekorPubs, err := sigstoreroot.GetRekorPubs(trustedRoot) + if err != nil { + t.Fatalf("GetRekorPubs() error = %v", err) + } + if len(rekorPubs.Keys) == 0 { + t.Fatal("GetRekorPubs() returned no keys") + } + + certs, err := sigstoreroot.FulcioCertificates(trustedRoot) + if err != nil { + t.Fatalf("FulcioCertificates() error = %v", err) + } + if len(certs) == 0 { + t.Fatal("FulcioCertificates() returned no certificates") + } + + hasCA := false + for _, cert := range certs { + if cert.IsCA { + hasCA = true + break + } + } + if !hasCA { + t.Fatal("FulcioCertificates() did not return any CA certificates") + } +} diff --git a/internal/fulcio/fulcioroots/fulcioroots.go b/internal/fulcio/fulcioroots/fulcioroots.go index 1060ebcd..1a5f205c 100644 --- a/internal/fulcio/fulcioroots/fulcioroots.go +++ b/internal/fulcio/fulcioroots/fulcioroots.go @@ -19,13 +19,13 @@ import ( "bytes" "context" "crypto/x509" - "errors" "fmt" "os" "github.com/sigstore/gitsign/internal/config" + "github.com/sigstore/gitsign/internal/sigstoreroot" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/tuf" ) type CertificateSource func() ([]*x509.Certificate, error) @@ -67,41 +67,27 @@ func NewFromConfig(ctx context.Context, cfg *config.Config) (*x509.CertPool, *x5 return New(x509.NewCertPool(), src...) } -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" -) - -// FromTUF loads certs from the TUF client. -func FromTUF(ctx context.Context) CertificateSource { +// FromTUF loads Fulcio certificates from the sigstore-go TUF cache. +func FromTUF(_ context.Context) CertificateSource { return func() ([]*x509.Certificate, error) { - tufClient, err := tuf.NewFromEnv(ctx) + trustedRoot, err := sigstoreroot.FetchTrustedRoot() if err != nil { return nil, fmt.Errorf("initializing tuf: %w", err) } - // Retrieve from the embedded or cached TUF root. If expired, a network - // call is made to update the root. - targets, err := tufClient.GetTargetsByMeta(tuf.Fulcio, []string{fulcioTargetStr, fulcioV1TargetStr, fulcioV1IntermediateTargetStr}) + certs, err := sigstoreroot.FulcioCertificates(trustedRoot) if err != nil { - return nil, fmt.Errorf("error getting targets: %w", err) - } - if len(targets) == 0 { - return nil, errors.New("none of the Fulcio roots have been found") + return nil, fmt.Errorf("initializing tuf: %w", err) } + return certs, nil + } +} - certs := []*x509.Certificate{} - for _, t := range targets { - c, err := cryptoutils.UnmarshalCertificatesFromPEM(t.Target) - if err != nil { - return nil, fmt.Errorf("error unmarshalling certificates: %w", err) - } - certs = append(certs, c...) +// FromTrustedRoot loads Fulcio certificates from a pre-fetched TrustedRoot. +func FromTrustedRoot(trustedRoot *root.TrustedRoot) CertificateSource { + return func() ([]*x509.Certificate, error) { + certs, err := sigstoreroot.FulcioCertificates(trustedRoot) + if err != nil { + return nil, fmt.Errorf("getting fulcio certificates from trusted root: %w", err) } return certs, nil } diff --git a/internal/gitsign/gitsign.go b/internal/gitsign/gitsign.go index c3b0c254..3d0ff4f0 100644 --- a/internal/gitsign/gitsign.go +++ b/internal/gitsign/gitsign.go @@ -26,6 +26,7 @@ import ( "github.com/sigstore/gitsign/internal/config" "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" rekorinternal "github.com/sigstore/gitsign/internal/rekor" + "github.com/sigstore/gitsign/internal/sigstoreroot" "github.com/sigstore/gitsign/pkg/git" "github.com/sigstore/gitsign/pkg/rekor" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -86,10 +87,18 @@ func NewVerifierWithCosignOpts(ctx context.Context, cfg *config.Config, opts *co // and warn if missing. var certverifier cert.Verifier if opts != nil { - ctpub, err := cosign.GetCTLogPubs(ctx) + trustedRoot, err := sigstoreroot.FetchTrustedRoot() + if err != nil { + return nil, fmt.Errorf("error fetching trusted root: %w", err) + } + ctpub, err := sigstoreroot.GetCTLogPubs(trustedRoot) if err != nil { return nil, fmt.Errorf("error getting CT log public key: %w", err) } + rekorPubs, err := sigstoreroot.GetRekorPubs(trustedRoot) + if err != nil { + return nil, fmt.Errorf("error getting Rekor public key: %w", err) + } identities, err := opts.Identities() if err != nil { return nil, fmt.Errorf("error parsing identities: %w", err) @@ -99,7 +108,7 @@ func NewVerifierWithCosignOpts(ctx context.Context, cfg *config.Config, opts *co RootCerts: root, IntermediateCerts: intermediate, CTLogPubKeys: ctpub, - RekorPubKeys: rekor.PublicKeys(), + RekorPubKeys: rekorPubs, CertGithubWorkflowTrigger: opts.CertGithubWorkflowTrigger, CertGithubWorkflowSha: opts.CertGithubWorkflowSha, CertGithubWorkflowName: opts.CertGithubWorkflowName, diff --git a/internal/sigstoreroot/root.go b/internal/sigstoreroot/root.go new file mode 100644 index 00000000..de355e32 --- /dev/null +++ b/internal/sigstoreroot/root.go @@ -0,0 +1,117 @@ +// Copyright 2026 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 sigstoreroot loads the Sigstore trusted root via the sigstore-go TUF client. +package sigstoreroot + +import ( + "crypto/x509" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + sigstoretuf "github.com/sigstore/sigstore/pkg/tuf" +) + +// TUFOptions returns sigstore-go TUF options, reading the mirror URL from remote.json if available. +func TUFOptions() *tuf.Options { + opts := tuf.DefaultOptions() + if mirror, err := readRemoteHint(opts.CachePath); err == nil && mirror != "" { + opts.RepositoryBaseURL = mirror + } + return opts +} + +// FetchTrustedRoot loads the Sigstore trusted root from the TUF cache. +func FetchTrustedRoot() (*root.TrustedRoot, error) { + return root.FetchTrustedRootWithOptions(TUFOptions()) +} + +// GetCTLogPubs returns CT log public keys from the trusted root. +func GetCTLogPubs(trustedRoot *root.TrustedRoot) (*cosign.TrustedTransparencyLogPubKeys, error) { + return transparencyLogPubKeys(trustedRoot.CTLogs()) +} + +// GetRekorPubs returns Rekor transparency log public keys from the trusted root. +func GetRekorPubs(trustedRoot *root.TrustedRoot) (*cosign.TrustedTransparencyLogPubKeys, error) { + return transparencyLogPubKeys(trustedRoot.RekorLogs()) +} + +// FulcioCertificates extracts Fulcio root and intermediate certificates from the trusted root. +func FulcioCertificates(trustedRoot *root.TrustedRoot) ([]*x509.Certificate, error) { + cas := trustedRoot.FulcioCertificateAuthorities() + if len(cas) == 0 { + return nil, fmt.Errorf("no Fulcio certificate authorities found in trusted root") + } + + var certs []*x509.Certificate + for _, ca := range cas { + fca, ok := ca.(*root.FulcioCertificateAuthority) + if !ok { + continue + } + if fca.Root != nil { + certs = append(certs, fca.Root) + } + certs = append(certs, fca.Intermediates...) + } + if len(certs) == 0 { + return nil, fmt.Errorf("no Fulcio certificates found in trusted root") + } + return certs, nil +} + +func transparencyLogPubKeys(logs map[string]*root.TransparencyLog) (*cosign.TrustedTransparencyLogPubKeys, error) { + pubKeys := cosign.NewTrustedTransparencyLogPubKeys() + + for logID, log := range logs { + if log.PublicKey == nil { + continue + } + + status := sigstoretuf.Active + if !log.ValidityPeriodEnd.IsZero() && time.Now().After(log.ValidityPeriodEnd) { + status = sigstoretuf.Expired + } + + pubKeys.Keys[logID] = cosign.TransparencyLogPubKey{ + PubKey: log.PublicKey, + Status: status, + } + } + + if len(pubKeys.Keys) == 0 { + return nil, fmt.Errorf("no transparency log public keys found") + } + return &pubKeys, nil +} + +func readRemoteHint(cachePath string) (string, error) { + data, err := os.ReadFile(filepath.Join(cachePath, "remote.json")) + if err != nil { + return "", err + } + var remote struct { + Mirror string `json:"mirror"` + } + if err := json.Unmarshal(data, &remote); err != nil { + return "", err + } + return remote.Mirror, nil +} diff --git a/internal/sigstoreroot/root_test.go b/internal/sigstoreroot/root_test.go new file mode 100644 index 00000000..71401f07 --- /dev/null +++ b/internal/sigstoreroot/root_test.go @@ -0,0 +1,77 @@ +// Copyright 2026 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 sigstoreroot + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestTUFOptions(t *testing.T) { + opts := TUFOptions() + if opts == nil { + t.Fatal("TUFOptions() returned nil") + } + if opts.CachePath == "" { + t.Fatal("TUFOptions() CachePath is empty") + } +} + +func TestReadRemoteHint(t *testing.T) { + tmpDir := t.TempDir() + + remoteHint := struct { + Mirror string `json:"mirror"` + }{ + Mirror: "https://custom.mirror.example.com", + } + data, err := json.Marshal(remoteHint) + if err != nil { + t.Fatalf("failed to marshal remote hint: %v", err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "remote.json"), data, 0644); err != nil { + t.Fatalf("failed to write remote.json: %v", err) + } + + mirror, err := readRemoteHint(tmpDir) + if err != nil { + t.Fatalf("readRemoteHint() error = %v", err) + } + if mirror != "https://custom.mirror.example.com" { + t.Errorf("readRemoteHint() = %q, want %q", mirror, "https://custom.mirror.example.com") + } +} + +func TestReadRemoteHintMissingFile(t *testing.T) { + tmpDir := t.TempDir() + _, err := readRemoteHint(tmpDir) + if err == nil { + t.Fatal("readRemoteHint() expected error for missing file, got nil") + } +} + +func TestReadRemoteHintInvalidJSON(t *testing.T) { + tmpDir := t.TempDir() + if err := os.WriteFile(filepath.Join(tmpDir, "remote.json"), []byte("not json"), 0644); err != nil { + t.Fatalf("failed to write remote.json: %v", err) + } + + _, err := readRemoteHint(tmpDir) + if err == nil { + t.Fatal("readRemoteHint() expected error for invalid JSON, got nil") + } +} diff --git a/pkg/git/verifier.go b/pkg/git/verifier.go index 1eab8885..dc154186 100644 --- a/pkg/git/verifier.go +++ b/pkg/git/verifier.go @@ -24,7 +24,7 @@ import ( cms "github.com/sigstore/gitsign/internal/fork/ietf-cms" "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" - "github.com/sigstore/sigstore/pkg/tuf" + "github.com/sigstore/sigstore-go/pkg/root" ) // Verifier verifies git commit signature data. @@ -142,15 +142,21 @@ func (v *CertVerifier) Verify(_ context.Context, data, sig []byte, detached bool return cert, nil } -// NewDefaultVerifier returns a new CertVerifier with the default Fulcio roots loaded from the local TUF client. +// NewDefaultVerifier returns a new CertVerifier with the default Fulcio roots loaded from TUF. // See https://docs.sigstore.dev/system_config/custom_components/ for how to customize this behavior. func NewDefaultVerifier(ctx context.Context) (*CertVerifier, error) { - if err := tuf.Initialize(ctx, tuf.DefaultRemoteRoot, nil); err != nil { - return nil, err - } root, intermediate, err := fulcioroots.New(x509.NewCertPool(), fulcioroots.FromTUF(ctx)) if err != nil { return nil, err } return NewCertVerifier(WithRootPool(root), WithIntermediatePool(intermediate)) } + +// NewVerifierFromTrustedRoot returns a new CertVerifier using Fulcio certificates from a pre-fetched TrustedRoot. +func NewVerifierFromTrustedRoot(trustedRoot *root.TrustedRoot) (*CertVerifier, error) { + rootPool, intermediate, err := fulcioroots.New(x509.NewCertPool(), fulcioroots.FromTrustedRoot(trustedRoot)) + if err != nil { + return nil, err + } + return NewCertVerifier(WithRootPool(rootPool), WithIntermediatePool(intermediate)) +} diff --git a/pkg/rekor/rekor.go b/pkg/rekor/rekor.go index a491dc91..c11c83dd 100644 --- a/pkg/rekor/rekor.go +++ b/pkg/rekor/rekor.go @@ -33,6 +33,7 @@ import ( "github.com/sigstore/cosign/v3/pkg/cosign" cms "github.com/sigstore/gitsign/internal/fork/ietf-cms" rekoroid "github.com/sigstore/gitsign/internal/rekor/oid" + "github.com/sigstore/gitsign/internal/sigstoreroot" rekor "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/client/index" @@ -68,9 +69,8 @@ func New(url string, opts ...rekor.Option) (*Client, error) { } func NewWithOptions(ctx context.Context, url string, opts ...Option) (*Client, error) { - // Defaults o := &options{ - rekorPublicKeys: cosign.GetRekorPubs, + rekorPublicKeys: getRekorPubsFromTrustedRoot, } for _, f := range opts { f(o) @@ -260,6 +260,14 @@ func (c *Client) PublicKeys() *cosign.TrustedTransparencyLogPubKeys { return c.publicKeys } +func getRekorPubsFromTrustedRoot(_ context.Context) (*cosign.TrustedTransparencyLogPubKeys, error) { + trustedRoot, err := sigstoreroot.FetchTrustedRoot() + if err != nil { + return nil, fmt.Errorf("error fetching trusted root: %w", err) + } + return sigstoreroot.GetRekorPubs(trustedRoot) +} + // VerifyInclusion verifies a signature's inclusion in Rekor using offline verification. // NOTE: This does **not** verify the correctness of the signature against the content. // Prefer using [git.Verify] instead for complete verification. diff --git a/pkg/version/version.go b/pkg/version/version.go index ef710703..23ea80bc 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package version +package version // nolint:revive import ( "os" diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go index 71d4c9aa..088abf1d 100644 --- a/pkg/version/version_test.go +++ b/pkg/version/version_test.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package version +package version // nolint:revive import ( "os"