diff --git a/cmd/conformance/main.go b/cmd/conformance/main.go index 4d43702aa5e..d4916df6df6 100644 --- a/cmd/conformance/main.go +++ b/cmd/conformance/main.go @@ -124,11 +124,16 @@ func main() { args = append(args, os.Args[len(os.Args)-1]) dir := filepath.Dir(os.Args[0]) + initCmd := exec.Command(filepath.Join(dir, "cosign"), "initialize") // #nosec G204 + err := initCmd.Run() + if err != nil { + log.Fatal(err) + } cmd := exec.Command(filepath.Join(dir, "cosign"), args...) // #nosec G204 var out strings.Builder cmd.Stdout = &out cmd.Stderr = &out - err := cmd.Run() + err = cmd.Run() fmt.Println(out.String()) diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 8646bb298bd..548949c6660 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -17,12 +17,15 @@ package fulcioverifier import ( "context" + "crypto/x509" "fmt" "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" ) @@ -32,12 +35,31 @@ func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerV return nil, err } - // Grab the PublicKeys for the CTFE, either from tuf or env. + if ko.TrustedMaterial != nil && len(fs.SCT) == 0 { + // Detached SCTs cannot be verified with this function. + chain, err := cryptoutils.UnmarshalCertificatesFromPEM(fs.Chain) + if err != nil { + return nil, fmt.Errorf("unmarshalling cert chain from PEM for SCT verification: %w", err) + } + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(fs.Cert) + if err != nil || len(certs) < 1 { + return nil, fmt.Errorf("unmarshalling cert from PEM for SCT verification: %w", err) + } + chain = append(chain, certs...) + chains := make([][]*x509.Certificate, 1) + chains[0] = chain + if err := verify.VerifySignedCertificateTimestamp(chains, 1, ko.TrustedMaterial); err != nil { + return nil, fmt.Errorf("verifying SCT using trusted root: %w", err) + } + ui.Infof(ctx, "Successfully verified SCT...") + return fs, nil + } + + // There was no trusted_root.json or we need to verify a detached SCT, so grab the PublicKeys for the CTFE, either from tuf or env. pubKeys, err := cosign.GetCTLogPubs(ctx) if err != nil { return nil, fmt.Errorf("getting CTFE public keys: %w", err) } - // verify the sct if err := cosign.VerifySCT(ctx, fs.Cert, fs.Chain, fs.SCT, pubKeys); err != nil { return nil, fmt.Errorf("verifying SCT: %w", err) diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go new file mode 100644 index 00000000000..c0b11006ec1 --- /dev/null +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier_test.go @@ -0,0 +1,419 @@ +// Copyright 2025 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 fulcioverifier + +import ( + "context" + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "math/big" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/tls" + "github.com/google/certificate-transparency-go/trillian/ctfe" + ctx509 "github.com/google/certificate-transparency-go/x509" + ctx509util "github.com/google/certificate-transparency-go/x509util" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/initialize" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/fulcio/pkg/ctl" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/stretchr/testify/assert" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +func TestNewSigner(t *testing.T) { + td := t.TempDir() + t.Setenv("TUF_ROOT", td) + tufRepo := t.TempDir() + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + skid, err := cryptoutils.SKID(&privateKey.PublicKey) + if err != nil { + t.Fatal(err) + } + caCert := createBaseCert(t, privateKey, skid, big.NewInt(1)) + logID, err := ctfe.GetCTLogID(&privateKey.PublicKey) + if err != nil { + t.Fatal(err) + } + sct := ct.SignedCertificateTimestamp{ + SCTVersion: ct.V1, + Timestamp: 12345, + LogID: ct.LogID{KeyID: logID}, + } + preCert := createBaseCert(t, privateKey, skid, big.NewInt(1)) + if err != nil { + t.Fatal(err) + } + pubBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + t.Fatal(err) + } + pubPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubBytes, + }) + err = newTUF(tufRepo, map[string][]byte{"ctfe.pub": pubPEM}) + if err != nil { + t.Fatal(err) + } + tufServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.Dir(tufRepo)).ServeHTTP(w, r) + })) + err = initialize.DoInitialize(context.Background(), filepath.Join(tufRepo, "1.root.json"), tufServer.URL) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + embeddedSCT bool + trustedMaterial root.TrustedMaterial + }{ + { + name: "detached SCT", + embeddedSCT: false, + trustedMaterial: nil, + }, + { + name: "embedded SCT with legacy TUF metadata", + embeddedSCT: true, + trustedMaterial: nil, + }, + { + name: "embedded SCT with trusted root", + embeddedSCT: true, + trustedMaterial: &fakeTrustedMaterial{ + transparencyLog: map[string]*root.TransparencyLog{ + hex.EncodeToString(logID[:]): { + PublicKey: &privateKey.PublicKey, + }, + }, + cas: []root.CertificateAuthority{ + &root.FulcioCertificateAuthority{ + Root: caCert, + }, + }, + }, + }, + { + name: "detached SCT with trusted root uses legacy TUF client", + embeddedSCT: false, + trustedMaterial: &fakeTrustedMaterial{ + transparencyLog: map[string]*root.TransparencyLog{ + hex.EncodeToString(logID[:]): { + PublicKey: &privateKey.PublicKey, + }, + }, + cas: []root.CertificateAuthority{ + &root.FulcioCertificateAuthority{ + Root: caCert, + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + leafCert := preCert + sctHeader := "" + if test.embeddedSCT { + leafCert = embedSCT(t, privateKey, skid, preCert, sct) + } else { + sctHeader = detachedSCT(t, privateKey, preCert, sct) + } + pemChain, _ := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, caCert}) + testServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { + if sctHeader != "" { + w.Header().Set("SCT", sctHeader) + } + w.WriteHeader(http.StatusCreated) + _, _ = w.Write(pemChain) + })) + defer testServer.Close() + + ctx := context.Background() + ko := options.KeyOpts{ + OIDCDisableProviders: true, + // random test token + IDToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + FulcioURL: testServer.URL, + FulcioAuthFlow: "token", + } + privKey, err := cosign.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + + fs, err := NewSigner(ctx, ko, sv) + if test.embeddedSCT { + assert.Empty(t, fs.SCT) + } else { + assert.NotEmpty(t, fs.SCT) + } + assert.NoError(t, err) + }) + } +} + +func getSCT(t *testing.T, privateKey *rsa.PrivateKey, preCert *x509.Certificate, sctInput ct.SignedCertificateTimestamp, embedded bool) ct.SignedCertificateTimestamp { + logEntry := ct.LogEntry{ + Leaf: ct.MerkleTreeLeaf{ + Version: ct.V1, + LeafType: ct.TimestampedEntryLeafType, + TimestampedEntry: &ct.TimestampedEntry{ + Timestamp: sctInput.Timestamp, + }, + }, + } + if embedded { + logEntry.Leaf.TimestampedEntry.EntryType = ct.PrecertLogEntryType + logEntry.Leaf.TimestampedEntry.PrecertEntry = &ct.PreCert{ + IssuerKeyHash: sha256.Sum256(preCert.RawSubjectPublicKeyInfo), + TBSCertificate: preCert.RawTBSCertificate, + } + } else { + logEntry.Leaf.TimestampedEntry.EntryType = ct.X509LogEntryType + logEntry.Leaf.TimestampedEntry.X509Entry = &ct.ASN1Cert{Data: preCert.Raw} + } + data, err := ct.SerializeSCTSignatureInput(sctInput, logEntry) + if err != nil { + t.Fatal(err) + } + h := sha256.Sum256(data) + signature, err := privateKey.Sign(rand.Reader, h[:], crypto.SHA256) + if err != nil { + t.Fatal(err) + } + sct := ct.SignedCertificateTimestamp{ + SCTVersion: sctInput.SCTVersion, + LogID: sctInput.LogID, + Timestamp: sctInput.Timestamp, + Signature: ct.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.RSA, + }, + Signature: signature, + }, + } + return sct +} + +func detachedSCT(t *testing.T, privateKey *rsa.PrivateKey, preCert *x509.Certificate, sctInput ct.SignedCertificateTimestamp) string { + sct := getSCT(t, privateKey, preCert, sctInput, false) + addChainResp, err := ctl.ToAddChainResponse(&sct) + if err != nil { + t.Fatal(err) + } + sctBytes, err := json.Marshal(addChainResp) + if err != nil { + t.Fatal(err) + } + + return base64.StdEncoding.EncodeToString(sctBytes) +} + +func embedSCT(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, preCert *x509.Certificate, sctInput ct.SignedCertificateTimestamp) *x509.Certificate { + sct := getSCT(t, privateKey, preCert, sctInput, true) + sctList, err := ctx509util.MarshalSCTsIntoSCTList([]*ct.SignedCertificateTimestamp{&sct}) + if err != nil { + t.Fatal(err) + } + sctBytes, err := tls.Marshal(*sctList) + if err != nil { + t.Fatal(err) + } + asnSCT, err := asn1.Marshal(sctBytes) + if err != nil { + t.Fatal(err) + } + cert := &x509.Certificate{ + SerialNumber: preCert.SerialNumber, + SubjectKeyId: skid, + ExtraExtensions: []pkix.Extension{ + { + Id: asn1.ObjectIdentifier(ctx509.OIDExtensionCTSCT), + Value: asnSCT, + }, + }, + } + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey) + if err != nil { + t.Fatal(err) + } + parsedCert, err := x509.ParseCertificate(certDERBytes) + if err != nil { + t.Fatal(err) + } + return parsedCert +} + +func newKey() (*metadata.Key, signature.Signer, error) { + pub, private, err := ed25519.GenerateKey(nil) + if err != nil { + return nil, nil, err + } + public, err := metadata.KeyFromPublicKey(pub) + if err != nil { + return nil, nil, err + } + signer, err := signature.LoadSigner(private, crypto.Hash(0)) + if err != nil { + return nil, nil, err + } + return public, signer, nil +} + +func newTUF(td string, targetList map[string][]byte) error { + expiration := time.Now().AddDate(0, 0, 1).UTC() + targets := metadata.Targets(expiration) + targetsDir := filepath.Join(td, "targets") + err := os.Mkdir(targetsDir, 0700) + if err != nil { + return err + } + for name, content := range targetList { + targetPath := filepath.Join(targetsDir, name) + err := os.WriteFile(targetPath, content, 0600) + if err != nil { + return err + } + targetFileInfo, err := metadata.TargetFile().FromFile(targetPath, "sha256") + if err != nil { + return err + } + targets.Signed.Targets[name] = targetFileInfo + } + snapshot := metadata.Snapshot(expiration) + timestamp := metadata.Timestamp(expiration) + root := metadata.Root(expiration) + root.Signed.ConsistentSnapshot = false + public, signer, err := newKey() + if err != nil { + return err + } + for _, name := range []string{"targets", "snapshot", "timestamp", "root"} { + err := root.Signed.AddKey(public, name) + if err != nil { + return err + } + switch name { + case "targets": + _, err = targets.Sign(signer) + case "snapshot": + _, err = snapshot.Sign(signer) + case "timestamp": + _, err = timestamp.Sign(signer) + case "root": + _, err = root.Sign(signer) + } + if err != nil { + return err + } + } + err = targets.ToFile(filepath.Join(td, "targets.json"), false) + if err != nil { + return err + } + err = snapshot.ToFile(filepath.Join(td, "snapshot.json"), false) + if err != nil { + return err + } + err = timestamp.ToFile(filepath.Join(td, "timestamp.json"), false) + if err != nil { + return err + } + err = root.ToFile(filepath.Join(td, "1.root.json"), false) + if err != nil { + return err + } + err = root.VerifyDelegate("root", root) + if err != nil { + return err + } + err = root.VerifyDelegate("targets", targets) + if err != nil { + return err + } + err = root.VerifyDelegate("snapshot", snapshot) + if err != nil { + return err + } + err = root.VerifyDelegate("timestamp", timestamp) + return err +} + +func createBaseCert(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, serialNumber *big.Int) *x509.Certificate { + cert := &x509.Certificate{ + SerialNumber: serialNumber, + SubjectKeyId: skid, + } + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey) + if err != nil { + t.Fatal(err) + } + parsedCert, err := x509.ParseCertificate(certDERBytes) + if err != nil { + t.Fatal(err) + } + return parsedCert +} + +type fakeTrustedMaterial struct { + transparencyLog map[string]*root.TransparencyLog + cas []root.CertificateAuthority +} + +func (t *fakeTrustedMaterial) CTLogs() map[string]*root.TransparencyLog { + return t.transparencyLog +} + +func (t *fakeTrustedMaterial) FulcioCertificateAuthorities() []root.CertificateAuthority { + return t.cas +} + +func (t *fakeTrustedMaterial) TimestampingAuthorities() []root.TimestampingAuthority { + panic("not implemented") +} +func (t *fakeTrustedMaterial) RekorLogs() map[string]*root.TransparencyLog { panic("not implemented") } +func (t *fakeTrustedMaterial) PublicKeyVerifier(string) (root.TimeConstrainedVerifier, error) { + panic("not implemented") +} diff --git a/cmd/cosign/cli/initialize/init.go b/cmd/cosign/cli/initialize/init.go index eca80ea15ea..cf4fe1b03fe 100644 --- a/cmd/cosign/cli/initialize/init.go +++ b/cmd/cosign/cli/initialize/init.go @@ -21,11 +21,16 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" - "github.com/sigstore/sigstore/pkg/tuf" + "github.com/sigstore/cosign/v2/pkg/cosign/env" + tufroot "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + tufv1 "github.com/sigstore/sigstore/pkg/tuf" ) func DoInitialize(ctx context.Context, root, mirror string) error { @@ -57,11 +62,44 @@ func doInitialize(ctx context.Context, root, mirror, rootChecksum string, forceS } } - if err := tuf.Initialize(ctx, mirror, rootFileBytes); err != nil { + opts := tuf.DefaultOptions() + if root != "" { + opts.Root = rootFileBytes + } + if mirror != "" { + opts.RepositoryBaseURL = mirror + } + if tufCacheDir := env.Getenv(env.VariableTUFRootDir); tufCacheDir != "" { //nolint:forbidigo + opts.CachePath = tufCacheDir + } + + // Leave a hint for where the current remote is. Adopted from sigstore/sigstore TUF client. + remote := map[string]string{"remote": opts.RepositoryBaseURL} + remoteBytes, err := json.Marshal(remote) + if err != nil { + return err + } + if err := os.MkdirAll(opts.CachePath, 0o700); err != nil { + return fmt.Errorf("creating cache directory: %w", err) + } + if err := os.WriteFile(filepath.FromSlash(filepath.Join(opts.CachePath, "remote.json")), remoteBytes, 0o600); err != nil { + return fmt.Errorf("storing remote: %w", err) + } + + trustedRoot, err := tufroot.NewLiveTrustedRoot(opts) + if err != nil { + ui.Warnf(ctx, "Could not find trusted_root.json in TUF mirror, falling back to individual targets. It is recommended to update your TUF metadata repository to include trusted_root.json.") + } + if trustedRoot != nil { + return nil + } + + // The mirror did not have a trusted_root.json, so initialize the legacy TUF targets. + if err := tufv1.Initialize(ctx, mirror, rootFileBytes); err != nil { return err } - status, err := tuf.GetRootStatus(ctx) + status, err := tufv1.GetRootStatus(ctx) if err != nil { return err } diff --git a/cmd/cosign/cli/initialize/init_test.go b/cmd/cosign/cli/initialize/init_test.go new file mode 100644 index 00000000000..cfb79598a6b --- /dev/null +++ b/cmd/cosign/cli/initialize/init_test.go @@ -0,0 +1,224 @@ +// Copyright 2025 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 initialize + +import ( + "context" + "crypto" + "crypto/ed25519" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/sigstore/sigstore/pkg/signature" + "github.com/stretchr/testify/assert" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +func newKey() (*metadata.Key, signature.Signer, error) { + pub, private, err := ed25519.GenerateKey(nil) + if err != nil { + return nil, nil, err + } + public, err := metadata.KeyFromPublicKey(pub) + if err != nil { + return nil, nil, err + } + signer, err := signature.LoadSigner(private, crypto.Hash(0)) + if err != nil { + return nil, nil, err + } + return public, signer, nil +} + +func newTUF(td string, targetList map[string][]byte) error { + expiration := time.Now().AddDate(0, 0, 1).UTC() + targets := metadata.Targets(expiration) + targetsDir := filepath.Join(td, "targets") + err := os.Mkdir(targetsDir, 0700) + if err != nil { + return err + } + for name, content := range targetList { + targetPath := filepath.Join(targetsDir, name) + err := os.WriteFile(targetPath, content, 0600) + if err != nil { + return err + } + targetFileInfo, err := metadata.TargetFile().FromFile(targetPath, "sha256") + if err != nil { + return err + } + targets.Signed.Targets[name] = targetFileInfo + } + snapshot := metadata.Snapshot(expiration) + timestamp := metadata.Timestamp(expiration) + root := metadata.Root(expiration) + root.Signed.ConsistentSnapshot = false + public, signer, err := newKey() + if err != nil { + return err + } + for _, name := range []string{"targets", "snapshot", "timestamp", "root"} { + err := root.Signed.AddKey(public, name) + if err != nil { + return err + } + switch name { + case "targets": + _, err = targets.Sign(signer) + case "snapshot": + _, err = snapshot.Sign(signer) + case "timestamp": + _, err = timestamp.Sign(signer) + case "root": + _, err = root.Sign(signer) + } + if err != nil { + return err + } + } + err = targets.ToFile(filepath.Join(td, "targets.json"), false) + if err != nil { + return err + } + err = snapshot.ToFile(filepath.Join(td, "snapshot.json"), false) + if err != nil { + return err + } + err = timestamp.ToFile(filepath.Join(td, "timestamp.json"), false) + if err != nil { + return err + } + err = root.ToFile(filepath.Join(td, "1.root.json"), false) + if err != nil { + return err + } + err = root.VerifyDelegate("root", root) + if err != nil { + return err + } + err = root.VerifyDelegate("targets", targets) + if err != nil { + return err + } + err = root.VerifyDelegate("snapshot", snapshot) + if err != nil { + return err + } + err = root.VerifyDelegate("timestamp", timestamp) + return err +} + +func captureOutput(f func() error) (string, string, error) { + stdout := os.Stdout + stderr := os.Stderr + rout, wout, _ := os.Pipe() + os.Stdout = wout + rerr, werr, _ := os.Pipe() + os.Stderr = werr + err := f() + os.Stdout = stdout + os.Stderr = stderr + wout.Close() + werr.Close() + out, _ := io.ReadAll(rout) + errMsg, _ := io.ReadAll(rerr) + return string(out), string(errMsg), err +} + +func TestDoInitialize(t *testing.T) { + tests := []struct { + name string + targets map[string][]byte + root string + wantStdOut string + wantStdErr string + wantErr bool + wantFiles []string + expectV2 bool + }{ + { + name: "tuf v2 with trusted root", + targets: map[string][]byte{"trusted_root.json": []byte(`{"mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`)}, + root: "1.root.json", + wantStdOut: "", + wantStdErr: "", + wantErr: false, + wantFiles: []string{filepath.Join("targets", "trusted_root.json")}, + expectV2: true, + }, + { + name: "tuf v1", + targets: map[string][]byte{"ctfe.pub": []byte(`-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----`)}, + root: "1.root.json", + wantStdOut: "ctfe.pub", + wantStdErr: "WARNING: Could not find trusted_root.json in TUF mirror, falling back to individual targets. It is recommended to update your TUF metadata repository to include trusted_root.json.", + wantErr: false, + wantFiles: []string{filepath.Join("targets", "ctfe.pub")}, + expectV2: false, + }, + { + name: "invalid root - should not try to use embedded", + wantErr: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tufRepo := t.TempDir() + err := newTUF(tufRepo, test.targets) + if err != nil { + t.Fatal(err) + } + tufServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.Dir(tufRepo)).ServeHTTP(w, r) + })) + rootJSONPath := filepath.Join(tufRepo, test.root) + tufCache := t.TempDir() + t.Setenv("TUF_ROOT", tufCache) + gotStdOut, gotStdErr, gotErr := captureOutput(func() error { + return DoInitialize(context.Background(), rootJSONPath, tufServer.URL) + }) + if test.wantErr { + assert.Error(t, gotErr) + return + } + assert.NoError(t, gotErr) + if test.wantStdOut == "" { + assert.Empty(t, gotStdOut) + } else { + assert.Contains(t, gotStdOut, test.wantStdOut) + } + if test.wantStdErr == "" { + assert.Empty(t, gotStdErr) + } else { + assert.Contains(t, gotStdErr, test.wantStdErr) + } + var mirrorDir string + if test.expectV2 { + mirrorDir = tufServer.URL + mirrorDir = strings.ReplaceAll(mirrorDir, "/", "-") + } + for _, f := range test.wantFiles { + assert.FileExists(t, filepath.Join(tufCache, mirrorDir, f)) + } + assert.FileExists(t, filepath.Join(tufCache, "remote.json")) + }) + } +} diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go index 634911fdb59..2cae8d5cdbe 100644 --- a/cmd/cosign/cli/options/key.go +++ b/cmd/cosign/cli/options/key.go @@ -15,7 +15,10 @@ package options -import "github.com/sigstore/cosign/v2/pkg/cosign" +import ( + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/root" +) type KeyOpts struct { Sk bool @@ -53,4 +56,7 @@ type KeyOpts struct { // Modeled after InsecureSkipVerify in tls.Config, this disables // verifying the SCT. InsecureSkipFulcioVerify bool + + // TrustedMaterial contains trusted metadata for all Sigstore services. It is exclusive with RekorPubKeys, RootCerts, IntermediateCerts, CTLogPubKeys, and the TSA* cert fields. + TrustedMaterial root.TrustedMaterial } diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index a4ae71210f6..aa18b7f0073 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -16,6 +16,7 @@ package cli import ( + "context" "fmt" "os" @@ -24,6 +25,8 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v2/internal/ui" + "github.com/sigstore/cosign/v2/pkg/cosign" ) func Sign() *cobra.Command { @@ -103,6 +106,12 @@ race conditions or (worse) malicious tampering. if err != nil { return err } + + trustedMaterial, err := cosign.TrustedRoot() + if err != nil { + ui.Warnf(context.Background(), "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, @@ -126,6 +135,7 @@ race conditions or (worse) malicious tampering. TSAServerName: o.TSAServerName, TSAServerURL: o.TSAServerURL, IssueCertificateForExistingKey: o.IssueCertificate, + TrustedMaterial: trustedMaterial, } if err := sign.SignCmd(ro, ko, *o, args); err != nil { if o.Attachment == "" { diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1289e7b1bb4..cd25b2d4560 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -54,6 +54,8 @@ import ( ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/oci/walk" sigs "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" @@ -391,7 +393,7 @@ func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier }, nil } -func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc) (*SignerVerifier, error) { +func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, trustedMaterial root.TrustedMaterial) (*SignerVerifier, error) { k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc) if err != nil { return nil, fmt.Errorf("reading key: %w", err) @@ -499,24 +501,33 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { return nil, fmt.Errorf("unable to validate certificate chain: %w", err) } + certSigner.Chain = certChainBytes // Verify SCT if present in the leaf certificate. contains, err := cosign.ContainsSCT(leafCert.Raw) if err != nil { return nil, err } - if contains { - pubKeys, err := cosign.GetCTLogPubs(ctx) - if err != nil { - return nil, fmt.Errorf("getting CTLog public keys: %w", err) - } - var chain []*x509.Certificate - chain = append(chain, leafCert) - chain = append(chain, certChain...) - if err := cosign.VerifyEmbeddedSCT(context.Background(), chain, pubKeys); err != nil { - return nil, err + if !contains { + return certSigner, nil + } + var chain []*x509.Certificate + chain = append(chain, leafCert) + chain = append(chain, certChain...) + if trustedMaterial != nil { + chains := make([][]*x509.Certificate, 1) + chains[0] = chain + if err := verify.VerifySignedCertificateTimestamp(chains, 1, trustedMaterial); err != nil { + return nil, fmt.Errorf("validating embedded SCT with trusted root: %w", err) } + return certSigner, nil + } + pubKeys, err := cosign.GetCTLogPubs(ctx) + if err != nil { + return nil, fmt.Errorf("getting CTLog public keys: %w", err) + } + if err := cosign.VerifyEmbeddedSCT(context.Background(), chain, pubKeys); err != nil { + return nil, err } - certSigner.Chain = certChainBytes return certSigner, nil } @@ -567,7 +578,7 @@ func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath strin case ko.Sk: sv, err = signerFromSecurityKey(ctx, ko.Slot) case ko.KeyRef != "": - sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc) + sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, ko.TrustedMaterial) default: genKey = true ui.Infof(ctx, "Generating ephemeral keys...") diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index 2735741d09e..b34009c2499 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -134,7 +134,7 @@ func Test_signerFromKeyRefSuccess(t *testing.T) { ctx := context.Background() keyFile, certFile, chainFile, privKey, cert, chain := generateCertificateFiles(t, tmpDir, pass("foo")) - signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo")) + signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo"), nil) if err != nil { t.Fatalf("unexpected error generating signer: %v", err) } @@ -173,17 +173,17 @@ func Test_signerFromKeyRefFailure(t *testing.T) { _, certFile2, chainFile2, _, _, _ := generateCertificateFiles(t, tmpDir2, pass("bar")) // Public keys don't match - _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo")) + _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo"), nil) if err == nil || err.Error() != "public key in certificate does not match the provided public key" { t.Fatalf("expected mismatched keys error, got %v", err) } // Certificate chain cannot be verified - _, err = signerFromKeyRef(ctx, certFile, chainFile2, keyFile, pass("foo")) + _, err = signerFromKeyRef(ctx, certFile, chainFile2, keyFile, pass("foo"), nil) if err == nil || !strings.Contains(err.Error(), "unable to validate certificate chain") { t.Fatalf("expected chain verification error, got %v", err) } // Certificate chain specified without certificate - _, err = signerFromKeyRef(ctx, "", chainFile2, keyFile, pass("foo")) + _, err = signerFromKeyRef(ctx, "", chainFile2, keyFile, pass("foo"), nil) if err == nil || !strings.Contains(err.Error(), "no leaf certificate found or provided while specifying chain") { t.Fatalf("expected no leaf error, got %v", err) } @@ -203,7 +203,7 @@ func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { t.Fatalf("failed to write chain file: %v", err) } - _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo")) + _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo"), nil) if err == nil || err.Error() != "no certificates in certificate chain" { t.Fatalf("expected empty chain error, got %v", err) } diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index e1a2cbf8b0e..8bb8225a1de 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -16,12 +16,15 @@ package cli import ( + "context" "fmt" "os" "github.com/sigstore/cosign/v2/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v2/internal/ui" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -68,6 +71,12 @@ func SignBlob() *cobra.Command { if err != nil { return err } + + trustedMaterial, err := cosign.TrustedRoot() + if err != nil { + ui.Warnf(context.Background(), "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + ko := options.KeyOpts{ KeyRef: o.Key, PassFunc: generate.GetPass, @@ -93,6 +102,7 @@ func SignBlob() *cobra.Command { TSAServerURL: o.TSAServerURL, RFC3161TimestampPath: o.RFC3161TimestampPath, IssueCertificateForExistingKey: o.IssueCertificate, + TrustedMaterial: trustedMaterial, } for _, blob := range args { diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 17fd63e8330..395420796e1 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -37,6 +37,7 @@ import ( "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci" @@ -128,6 +129,20 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("constructing client options: %w", err) } + trustedMaterial, err := cosign.TrustedRoot() + if err != nil { + ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + + if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) > 0 || + env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) != "" || + env.Getenv(env.VariableSigstoreRootFile) != "" || + env.Getenv(env.VariableSigstoreRekorPublicKey) != "" || + env.Getenv(env.VariableSigstoreTSACertificateFile) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ Annotations: c.Annotations.Annotations, RegistryClientOpts: ociremoteOpts, @@ -144,6 +159,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { IgnoreTlog: c.IgnoreTlog, MaxWorkers: c.MaxWorkers, ExperimentalOCI11: c.ExperimentalOCI11, + TrustedMaterial: trustedMaterial, } if c.CheckClaims { co.ClaimVerifier = cosign.SimpleClaimVerifier @@ -167,11 +183,13 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } co.RekorClient = rekorClient } - // 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) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) + if co.TrustedMaterial == nil { + // 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) + if err != nil { + return fmt.Errorf("getting Rekor public keys: %w", err) + } } } if keylessVerification(c.KeyRef, c.Sk) { @@ -184,7 +202,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { certRef := c.CertRef // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) @@ -221,13 +239,15 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { switch { case c.CertChain == "" && co.RootCerts == nil: // If no certChain and no CARoots are passed, the Fulcio root certificate will be used - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) + if co.TrustedMaterial == nil { + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) + } } pubKey, err = cosign.ValidateAndUnpackCert(cert, co) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 93c27690455..45c7607617e 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -31,6 +31,7 @@ import ( "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/cue" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/cosign/rego" @@ -107,6 +108,21 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("constructing client options: %w", err) } + trustedMaterial, err := cosign.TrustedRoot() + if err != nil { + ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + + if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) > 0 || + env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) != "" || + env.Getenv(env.VariableSigstoreRootFile) != "" || + env.Getenv(env.VariableSigstoreRekorPublicKey) != "" || + env.Getenv(env.VariableSigstoreTSACertificateFile) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, or environment variables point to the key material, + // so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ RegistryClientOpts: ociremoteOpts, CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, @@ -119,12 +135,13 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, MaxWorkers: c.MaxWorkers, + TrustedMaterial: trustedMaterial, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) @@ -149,11 +166,13 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } co.RekorClient = rekorClient } - // 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) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) + if co.TrustedMaterial == nil { + // 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) + if err != nil { + return fmt.Errorf("getting Rekor public keys: %w", err) + } } } @@ -193,13 +212,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } if c.CertChain == "" { // If no certChain is passed, the Fulcio root certificate will be used - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) + if co.TrustedMaterial == nil { + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) + } } co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 37e2d0282bf..193cce13fd5 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -34,6 +34,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci/static" @@ -127,6 +128,20 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return err } + trustedMaterial, err := cosign.TrustedRoot() + if err != nil { + ui.Warnf(context.Background(), "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + + if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) > 0 || + env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) != "" || + env.Getenv(env.VariableSigstoreRootFile) != "" || + env.Getenv(env.VariableSigstoreRekorPublicKey) != "" || + env.Getenv(env.VariableSigstoreTSACertificateFile) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, @@ -137,6 +152,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + TrustedMaterial: trustedMaterial, } if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") @@ -159,11 +175,13 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } co.RekorClient = rekorClient } - // 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) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) + if co.TrustedMaterial == nil { + // 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) + if err != nil { + return fmt.Errorf("getting Rekor public keys: %w", err) + } } } @@ -293,7 +311,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) { + if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) 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 3c420930dbb..d298aaee544 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -34,9 +34,11 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" internal "github.com/sigstore/cosign/v2/internal/pkg/cosign" payloadsize "github.com/sigstore/cosign/v2/internal/pkg/cosign/payload/size" + "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/cosign/env" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci/static" @@ -113,6 +115,20 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } + trustedMaterial, err := cosign.TrustedRoot() + if err != nil { + ui.Warnf(context.Background(), "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + + if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) > 0 || + env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) != "" || + env.Getenv(env.VariableSigstoreRootFile) != "" || + env.Getenv(env.VariableSigstoreRekorPublicKey) != "" || + env.Getenv(env.VariableSigstoreTSACertificateFile) != "" { + // trusted_root.json was found, but a cert chain was explicitly provided, so don't overrule the user's intentions. + trustedMaterial = nil + } + co := &cosign.CheckOpts{ Identities: identities, CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, @@ -123,6 +139,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st IgnoreSCT: c.IgnoreSCT, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + TrustedMaterial: trustedMaterial, } var h v1.Hash if c.CheckClaims { @@ -177,11 +194,13 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } co.RekorClient = rekorClient } - // 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) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) + if co.TrustedMaterial == nil { + // 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) + if err != nil { + return fmt.Errorf("getting Rekor public keys: %w", err) + } } } if keylessVerification(c.KeyRef, c.Sk) { @@ -191,7 +210,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) { + if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index 6b1b127052c..6b959b9ce2f 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -820,6 +820,7 @@ func makeLocalNewBundle(t *testing.T, sig []byte, digest [32]byte) string { } func TestVerifyBlobCmdWithBundle(t *testing.T) { + t.Setenv("TUF_ROOT", t.TempDir()) keyless := newKeylessStack(t) defer os.RemoveAll(keyless.td) @@ -1333,6 +1334,7 @@ func TestVerifyBlobCmdWithBundle(t *testing.T) { } func TestVerifyBlobCmdInvalidRootCA(t *testing.T) { + t.Setenv("TUF_ROOT", t.TempDir()) keyless := newKeylessStack(t) defer os.RemoveAll(keyless.td) diff --git a/go.mod b/go.mod index a7ad9d83742..bcafbf8c3ba 100644 --- a/go.mod +++ b/go.mod @@ -161,6 +161,7 @@ require ( github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect @@ -173,6 +174,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/tink/go v1.7.0 // indirect + github.com/google/trillian v1.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect @@ -185,11 +187,17 @@ require ( github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.5 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/hashicorp/vault/api v1.15.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/in-toto/attestation v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/jellydator/ttlcache/v3 v3.3.0 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect diff --git a/go.sum b/go.sum index 01623a6bdc6..916076e7bab 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= @@ -346,6 +348,8 @@ github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -431,7 +435,6 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25L github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= @@ -559,8 +562,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ= github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/pkg/cosign/env/env.go b/pkg/cosign/env/env.go index 016687acb8e..00500843dbf 100644 --- a/pkg/cosign/env/env.go +++ b/pkg/cosign/env/env.go @@ -60,6 +60,11 @@ const ( VariableSigstoreIDToken Variable = "SIGSTORE_ID_TOKEN" //nolint:gosec VariableSigstoreTSACertificateFile Variable = "SIGSTORE_TSA_CERTIFICATE_FILE" + // TUF environment variables + VariableTUFRootDir Variable = "TUF_ROOT" + VariableTUFMirror Variable = "TUF_MIRROR" + VariableTUFRootJSON Variable = "TUF_ROOT_JSON" + // Other external environment variables VariableGitHubHost Variable = "GITHUB_HOST" VariableGitHubToken Variable = "GITHUB_TOKEN" //nolint:gosec @@ -145,6 +150,24 @@ var ( Sensitive: false, External: true, }, + VariableTUFMirror: { + Description: "URL of the TUF mirror. Use with TUF_ROOT_JSON to refresh TUF metadata during signing and verification commands. Setting this will cause cosign to attempt to use trusted_root.json if available and will ignore custom TUF metadata.", + Expects: "URL of the TUF mirror", + Sensitive: false, + External: true, + }, + VariableTUFRootDir: { + Description: "path to the TUF cache directory", + Expects: "path to the TUF cache directory", + Sensitive: false, + External: true, + }, + VariableTUFRootJSON: { + Description: "path to the TUF root.json file used to initialize and update a local TUF repository. Use with TUF_MIRROR to refresh TUF metadata during signing and verification commands. Setting this will cause cosign to attempt to use trusted_root.json if available and will ignore custom TUF metadata.", + Expects: "path to root.json", + Sensitive: false, + External: true, + }, VariableGitHubHost: { Description: "is URL of the GitHub Enterprise instance", Expects: "string with the URL of GitHub Enterprise instance", diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 83d6f61f179..af716938f01 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -47,6 +47,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-go/pkg/tlog" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/tuf" ) @@ -219,7 +221,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(ctx, e, rekorPubsFromAPI, nil) } return nil, err } @@ -443,14 +445,15 @@ 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(ctx context.Context, e *models.LogEntryAnon, rekorPubKeys *TrustedTransparencyLogPubKeys, trustedMaterial root.TrustedMaterial) error { if e.Verification == nil || e.Verification.InclusionProof == nil { return errors.New("inclusion proof not provided") } - if rekorPubKeys == nil || rekorPubKeys.Keys == nil { + if trustedMaterial == nil && (rekorPubKeys == nil || rekorPubKeys.Keys == nil) { return errors.New("no trusted rekor public keys provided") } + // Make sure all the rekorPubKeys are ecsda.PublicKeys for k, v := range rekorPubKeys.Keys { if _, ok := v.PubKey.(*ecdsa.PublicKey); !ok { @@ -478,6 +481,23 @@ func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPu } // Verify rekor's signature over the SET. + if trustedMaterial != nil { + logID, err := hex.DecodeString(*e.LogID) + if err != nil { + return fmt.Errorf("decoding log ID: %w", err) + } + entry, err := tlog.NewEntry(entryBytes, *e.IntegratedTime, *e.LogIndex, logID, e.Verification.SignedEntryTimestamp, e.Verification.InclusionProof) + if err != nil { + return fmt.Errorf("converting tlog entry: %w", err) + } + if err := tlog.VerifySET(entry, trustedMaterial.RekorLogs()); err != nil { + return fmt.Errorf("verifying SET offline: %w", err) + } + return nil + } + + // No trusted root available, so verify the SET with legacy TUF metadata: + payload := bundle.RekorPayload{ Body: e.Body, IntegratedTime: *e.IntegratedTime, diff --git a/pkg/cosign/tlog_test.go b/pkg/cosign/tlog_test.go index 55bd76bb9f6..c3829c32326 100644 --- a/pkg/cosign/tlog_test.go +++ b/pkg/cosign/tlog_test.go @@ -173,7 +173,7 @@ func TestVerifyTLogEntryOfflineFailsWithInvalidPublicKey(t *testing.T) { 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(context.Background(), &models.LogEntryAnon{Verification: &models.LogEntryAnonVerification{InclusionProof: &models.InclusionProof{}}}, &rekorPubKeys, nil) if err == nil { t.Fatal("Wanted error got none") } diff --git a/pkg/cosign/tuf.go b/pkg/cosign/tuf.go new file mode 100644 index 00000000000..6d4e5ef0470 --- /dev/null +++ b/pkg/cosign/tuf.go @@ -0,0 +1,106 @@ +// Copyright 2025 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 ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/sigstore/cosign/v2/pkg/cosign/env" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" +) + +func TrustedRoot() (root.TrustedMaterial, error) { + opts, err := setTUFOpts() + if err != nil { + return nil, fmt.Errorf("error setting TUF options: %w", err) + } + return root.NewLiveTrustedRoot(opts) +} + +// setTUFOpts sets the TUF cache directory, the mirror URL, and the root.json in the TUF options. +// The cache directory is provided by the user as an environment variable TUF_ROOT, or the default $HOME/.sigstore/root is used. +// The mirror URL is provided by the user as an environment variable TUF_MIRROR. If not overridden by the user, the value set during `cosign initialize` in remote.json in the cache directory is used. +// If the mirror happens to be the sigstore.dev production TUF CDN, the options are returned since it is safe to use all the default settings. +// If the mirror is a custom mirror, we try to find a cached root.json. We must not use the default embedded root.json. +// If the TUF options cannot be found through these steps, the caller should not try to use this TUF client to fetch the trusted root and should instead fall back to the legacy TUF client to fetch individual trusted keys. +func setTUFOpts() (*tuf.Options, error) { + opts := tuf.DefaultOptions() + if tufCacheDir := env.Getenv(env.VariableTUFRootDir); tufCacheDir != "" { //nolint:forbidigo + opts.CachePath = tufCacheDir + } + err := setTUFMirror(opts) + if err != nil { + return nil, fmt.Errorf("error setting TUF mirror: %w", err) + } + if opts.RepositoryBaseURL == tuf.DefaultMirror { + // Using the default mirror, so just use the embedded root.json. + return opts, nil + } + err = setTUFRootJSON(opts) + if err != nil { + return nil, fmt.Errorf("error setting root: %w", err) + } + return opts, nil +} + +func setTUFMirror(opts *tuf.Options) error { + if tufMirror := env.Getenv(env.VariableTUFMirror); 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 errors.Is(err, os.ErrNotExist) { + return nil // `cosign initialize` wasn't run, so use the default + } + 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 := env.Getenv(env.VariableTUFRootJSON); 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 + } + return fmt.Errorf("could not find cached root.json") +} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index d49ed2ddb52..b791f915bb6 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -65,6 +65,9 @@ import ( intoto_v001 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" intoto_v002 "github.com/sigstore/rekor/pkg/types/intoto/v0.0.2" rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tlog" + "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" @@ -94,6 +97,9 @@ type CheckOpts struct { // ClaimVerifier, if provided, verifies claims present in the oci.Signature. ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error + // TrustedMaterial contains trusted metadata for all Sigstore services. It is exclusive with RekorPubKeys, RootCerts, IntermediateCerts, CTLogPubKeys, and the TSA* cert fields. + TrustedMaterial root.TrustedMaterial + // RekorClient, if set, is used to make online tlog calls use to verify signatures and public keys. RekorClient *client.Rekor // RekorPubKeys, if set, is used to validate signatures on log entries from @@ -248,11 +254,27 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt } // Now verify the cert, then the signature. - chains, err := TrustedCert(cert, co.RootCerts, intermediateCerts) + shouldVerifyEmbeddedSCT := !co.IgnoreSCT + contains, err := ContainsSCT(cert.Raw) if err != nil { return nil, err } + shouldVerifyEmbeddedSCT = shouldVerifyEmbeddedSCT && contains + // If trusted root is available and the SCT is embedded, use the verifiers from sigstore-go (preferred). + var chains [][]*x509.Certificate + if co.TrustedMaterial != nil && shouldVerifyEmbeddedSCT { + if chains, err = verify.VerifyLeafCertificate(cert.NotBefore, cert, co.TrustedMaterial); err != nil { + return nil, err + } + } else { + // If the trusted root is not available, OR if the SCT is detached, use the verifiers from cosign (legacy). + // The certificate chains will be needed for the legacy SCT verifiers, which is why we can't use sigstore-go. + chains, err = TrustedCert(cert, co.RootCerts, intermediateCerts) + if err != nil { + return nil, err + } + } err = CheckCertificatePolicy(cert, co) if err != nil { @@ -263,15 +285,20 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt if co.IgnoreSCT { return verifier, nil } - contains, err := ContainsSCT(cert.Raw) - if err != nil { - return nil, err - } if !contains && len(co.SCT) == 0 { return nil, &VerificationFailure{ fmt.Errorf("certificate does not include required embedded SCT and no detached SCT was set"), } } + + // If trusted root is available and the SCT is embedded, use the verifiers from sigstore-go (preferred). + if co.TrustedMaterial != nil && contains { + if err := verify.VerifySignedCertificateTimestamp(chains, 1, co.TrustedMaterial); err != nil { + return nil, err + } + return verifier, nil + } + // handle if chains has more than one chain - grab first and print message if len(chains) > 1 { fmt.Fprintf(os.Stderr, "**Info** Multiple valid certificate chains found. Selecting the first to verify the SCT.\n") @@ -280,22 +307,22 @@ func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpt if err := VerifyEmbeddedSCT(context.Background(), chains[0], co.CTLogPubKeys); err != nil { return nil, err } - } else { - chain := chains[0] - if len(chain) < 2 { - return nil, errors.New("certificate chain must contain at least a certificate and its issuer") - } - certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) - if err != nil { - return nil, err - } - chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) - if err != nil { - return nil, err - } - if err := VerifySCT(context.Background(), certPEM, chainPEM, co.SCT, co.CTLogPubKeys); err != nil { - return nil, err - } + return verifier, nil + } + chain := chains[0] + if len(chain) < 2 { + return nil, errors.New("certificate chain must contain at least a certificate and its issuer") + } + certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) + if err != nil { + return nil, err + } + chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) + if err != nil { + return nil, err + } + if err := VerifySCT(context.Background(), certPEM, chainPEM, co.SCT, co.CTLogPubKeys); err != nil { + return nil, err } return verifier, nil @@ -433,7 +460,7 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif return ValidateAndUnpackCert(cert, co) } -func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, +func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, trustedMaterial root.TrustedMaterial, sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() if err != nil { @@ -457,7 +484,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(ctx, &entry, rekorPubKeys, trustedMaterial); err != nil { entryVerificationErrs = append(entryVerificationErrs, err.Error()) continue } @@ -704,7 +731,7 @@ func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, return false, err } - e, err := tlogValidateEntry(ctx, co.RekorClient, co.RekorPubKeys, sig, pemBytes) + e, err := tlogValidateEntry(ctx, co.RekorClient, co.RekorPubKeys, co.TrustedMaterial, sig, pemBytes) if err != nil { return false, err } @@ -1060,9 +1087,22 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { return false, nil } - if co.RekorPubKeys == nil || co.RekorPubKeys.Keys == nil { + if co.TrustedMaterial == nil && (co.RekorPubKeys == nil || co.RekorPubKeys.Keys == nil) { return false, errors.New("no trusted rekor public keys provided") } + + if co.TrustedMaterial != nil { + payload := bundle.Payload + body, _ := base64.StdEncoding.DecodeString(payload.Body.(string)) + entry, err := tlog.NewEntry(body, payload.IntegratedTime, payload.LogIndex, []byte(payload.LogID), bundle.SignedEntryTimestamp, nil) + if err != nil { + return false, fmt.Errorf("converting tlog entry: %w", err) + } + if err := tlog.VerifySET(entry, co.TrustedMaterial.RekorLogs()); err != nil { + return false, fmt.Errorf("verifying bundle with trusted root: %w", err) + } + return true, nil + } // Make sure all the rekorPubKeys are ecsda.PublicKeys for k, v := range co.RekorPubKeys.Keys { if _, ok := v.PubKey.(*ecdsa.PublicKey); !ok { @@ -1116,6 +1156,17 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { return true, nil } +type signedEntityForTimestamp struct { + verify.BaseSignedEntity + timestamp *cbundle.RFC3161Timestamp +} + +func (e *signedEntityForTimestamp) Timestamps() ([][]byte, error) { + timestamps := make([][]byte, 1) + timestamps[0] = e.timestamp.SignedRFC3161Timestamp + return timestamps, nil +} + // VerifyRFC3161Timestamp verifies that the timestamp in sig is correctly signed, and if so, // returns the timestamp value. // It returns (nil, nil) if there is no timestamp, or (nil, err) if there is an invalid timestamp or if @@ -1153,6 +1204,17 @@ func VerifyRFC3161Timestamp(sig oci.Signature, co *CheckOpts) (*timestamp.Timest tsBytes = rawSig } + if co.TrustedMaterial != nil { + entity := &signedEntityForTimestamp{ + timestamp: ts, + } + verifiedTimestamps, err := verify.VerifyTimestampAuthority(entity, co.TrustedMaterial) + if err != nil { + return nil, fmt.Errorf("unable to verify signed timestamps with trusted root: %w", err) + } + return ×tamp.Timestamp{Time: verifiedTimestamps[0].Time}, nil + } + return tsaverification.VerifyTimestampResponse(ts.SignedRFC3161Timestamp, bytes.NewReader(tsBytes), tsaverification.VerifyOpts{ TSACertificate: co.TSACertificate, diff --git a/pkg/cosign/verify_sct.go b/pkg/cosign/verify_sct.go index 1b904c2c4fd..444c488149c 100644 --- a/pkg/cosign/verify_sct.go +++ b/pkg/cosign/verify_sct.go @@ -19,7 +19,6 @@ import ( "crypto/x509" "encoding/hex" "encoding/json" - "errors" "fmt" "os" @@ -50,7 +49,7 @@ func getCTPublicKey(sct *ct.SignedCertificateTimestamp, keyID := hex.EncodeToString(sct.LogID.KeyID[:]) pubKeyMetadata, ok := pubKeys.Keys[keyID] if !ok { - return nil, errors.New("ctfe public key not found for payload. Check your TUF root (see cosign initialize) or set a custom key with env var SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") + return nil, fmt.Errorf("ctfe public key not found for payload. Check your TUF root (see cosign initialize) or set a custom key with env var SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") } return &pubKeyMetadata, nil } @@ -73,7 +72,7 @@ func getCTPublicKey(sct *ct.SignedCertificateTimestamp, // an alternate, the file can be PEM, or DER format. func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *TrustedTransparencyLogPubKeys) error { if pubKeys == nil || len(pubKeys.Keys) == 0 { - return errors.New("none of the CTFE keys have been found") + return fmt.Errorf("none of the CTFE keys have been found") } // parse certificate and chain @@ -86,7 +85,7 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru return err } if len(certChain) == 0 { - return errors.New("no certificate chain found") + return fmt.Errorf("no certificate chain found") } // fetch embedded SCT if present @@ -96,7 +95,7 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru } // SCT must be either embedded or in header if len(embeddedSCTs) == 0 && len(rawSCT) == 0 { - return errors.New("no SCT found") + return fmt.Errorf("no SCT found") } // check SCT embedded in certificate @@ -143,7 +142,7 @@ func VerifySCT(_ context.Context, certPEM, chainPEM, rawSCT []byte, pubKeys *Tru // VerifyEmbeddedSCT verifies an embedded SCT in a certificate. func VerifyEmbeddedSCT(ctx context.Context, chain []*x509.Certificate, pubKeys *TrustedTransparencyLogPubKeys) error { if len(chain) < 2 { - return errors.New("certificate chain must contain at least a certificate and its issuer") + return fmt.Errorf("certificate chain must contain at least a certificate and its issuer") } certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) if err != nil { diff --git a/test/e2e_test.go b/test/e2e_test.go index 48a6d80ae58..5551f24fa41 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -62,6 +62,7 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/publickey" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/trustedroot" cliverify "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" "github.com/sigstore/cosign/v2/internal/pkg/cosign/fulcio/fulcioroots" "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" @@ -556,6 +557,38 @@ func downloadTSACerts(downloadDirectory string, tsaServer string) (string, strin return leafPath, intermediatePath, rootPath, nil } +func prepareTrustedRoot(t *testing.T, tsaURL string) string { + downloadDirectory := t.TempDir() + caPath := filepath.Join(downloadDirectory, "fulcio.crt.pem") + caFP, err := os.Create(caPath) + must(err, t) + defer caFP.Close() + must(downloadFile(fulcioURL+"/api/v1/rootCert", caFP), t) + rekorPath := filepath.Join(downloadDirectory, "rekor.pub") + rekorFP, err := os.Create(rekorPath) + must(err, t) + defer rekorFP.Close() + must(downloadFile(rekorURL+"/api/v1/log/publicKey", rekorFP), t) + ctfePath := filepath.Join(downloadDirectory, "ctfe.pub") + home, err := os.UserHomeDir() + must(err, t) + must(copyFile(filepath.Join(home, "fulcio", "config", "ctfe", "pubkey.pem"), ctfePath), t) + tsaPath := filepath.Join(downloadDirectory, "tsa.crt.pem") + tsaFP, err := os.Create(tsaPath) + must(err, t) + must(downloadFile(tsaURL+"/api/v1/timestamp/certchain", tsaFP), t) + out := filepath.Join(downloadDirectory, "trusted_root.json") + cmd := &trustedroot.CreateCmd{ + CertChain: []string{caPath}, + CtfeKeyPath: []string{ctfePath}, + Out: out, + RekorKeyPath: []string{rekorPath}, + TSACertChainPath: []string{tsaPath}, + } + must(cmd.Exec(context.TODO()), t) + return out +} + func TestSignVerifyWithTUFMirror(t *testing.T) { home, err := os.UserHomeDir() // fulcio repo was downloaded to $HOME in e2e_test.sh must(err, t) @@ -573,6 +606,7 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { mirror := tufServer.URL tsaLeaf, tsaInter, tsaRoot, err := downloadTSACerts(t.TempDir(), tsaServer.URL) must(err, t) + trustedRoot := prepareTrustedRoot(t, tsaServer.URL) tests := []struct { name string targets []targetInfo @@ -688,6 +722,15 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { }, }, }, + { + name: "trusted root", + targets: []targetInfo{ + { + name: "trusted_root.json", + source: trustedRoot, + }, + }, + }, } tuf, err := newTUF(tufMirror, tests[0].targets) must(err, t)