1
0
mirror of synced 2026-10-24 12:59:22 +03:00

Upgrade to TUF v2 client with trusted root

Use sigstore-go's TUF client to fetch the trusted_root.json from the TUF
mirror, if available. Where possible, use sigstore-go's verifiers which
natively accept the trusted root as its trusted material. Where there is
no trusted root available in TUF or sigstore-go doesn't support a use
case, fall back to the sigstore/sigstore TUF v1 client and the existing
verifiers in cosign.

Signed-off-by: Colleen Murphy <colleenmurphy@google.com>
This commit is contained in:
Colleen Murphy
2024-10-18 13:46:53 -07:00
committed by Colleen Murphy
parent 95bec1ad76
commit 32a2d62a99
24 changed files with 1360 additions and 129 deletions

View File

@@ -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())

View File

@@ -16,11 +16,14 @@
package cli
import (
"context"
"fmt"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/attest"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"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/spf13/cobra"
)
@@ -69,6 +72,7 @@ func Attest() *cobra.Command {
if err != nil {
return err
}
ko := options.KeyOpts{
KeyRef: o.Key,
PassFunc: generate.GetPass,
@@ -92,6 +96,13 @@ func Attest() *cobra.Command {
TSAServerURL: o.TSAServerURL,
NewBundleFormat: o.NewBundleFormat,
}
if o.Key == "" { // Get the trusted root if using fulcio for signing
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.TrustedMaterial = trustedMaterial
}
attestCommand := attest.AttestCommand{
KeyOpts: ko,
RegistryOptions: o.Registry,

View File

@@ -15,9 +15,13 @@
package cli
import (
"context"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/attest"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"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/spf13/cobra"
)
@@ -54,6 +58,7 @@ func AttestBlob() *cobra.Command {
if err != nil {
return err
}
ko := options.KeyOpts{
KeyRef: o.Key,
PassFunc: generate.GetPass,
@@ -79,6 +84,13 @@ func AttestBlob() *cobra.Command {
BundlePath: o.BundlePath,
NewBundleFormat: o.NewBundleFormat,
}
if o.Key == "" { // Get the trusted root if using fulcio for signing
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.TrustedMaterial = trustedMaterial
}
v := attest.AttestBlobCommand{
KeyOpts: ko,
CertPath: o.Cert,

View File

@@ -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(certs, chain...)
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)

View File

@@ -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")
}

View File

@@ -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{"mirror": 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 fetch trusted_root.json from the TUF mirror (encountered error: %v), falling back to individual targets. It is recommended to update your TUF metadata repository to include trusted_root.json.", err)
}
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
}

View File

@@ -0,0 +1,226 @@
// 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 fetch trusted_root.json from the TUF mirror (encountered error: getting info for target \"trusted_root.json\": target trusted_root.json not found), 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.CutPrefix(mirrorDir, "http://")
mirrorDir = strings.ReplaceAll(mirrorDir, "/", "-")
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"))
})
}
}

View File

@@ -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
}

View File

@@ -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"
)
@@ -102,6 +105,7 @@ race conditions or (worse) malicious tampering.
if err != nil {
return err
}
ko := options.KeyOpts{
KeyRef: o.Key,
PassFunc: generate.GetPass,
@@ -126,6 +130,13 @@ race conditions or (worse) malicious tampering.
TSAServerURL: o.TSAServerURL,
IssueCertificateForExistingKey: o.IssueCertificate,
}
if o.Key == "" || o.IssueCertificate {
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.TrustedMaterial = trustedMaterial
}
if err := sign.SignCmd(ro, ko, *o, args); err != nil {
if o.Attachment == "" {
return fmt.Errorf("signing %v: %w", args, err)

View File

@@ -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,7 @@ func SignBlob() *cobra.Command {
if err != nil {
return err
}
ko := options.KeyOpts{
KeyRef: o.Key,
PassFunc: generate.GetPass,
@@ -94,6 +98,13 @@ func SignBlob() *cobra.Command {
RFC3161TimestampPath: o.RFC3161TimestampPath,
IssueCertificateForExistingKey: o.IssueCertificate,
}
if o.Key == "" || o.IssueCertificate {
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.TrustedMaterial = trustedMaterial
}
for _, blob := range args {
// TODO: remove when the output flag has been deprecated

View File

@@ -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"
@@ -144,14 +145,24 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
if err != nil {
return fmt.Errorf("loading trusted root: %w", err)
}
} else 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) == "" {
// don't overrule the user's intentions if they provided their own keys
co.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 c.CheckClaims {
co.ClaimVerifier = cosign.SimpleClaimVerifier
}
// If we are using signed timestamps, we need to load the TSA certificates
if co.UseSignedTimestamps {
// If we are using signed timestamps and there is no trusted root, we need to load the TSA certificates
if co.UseSignedTimestamps && co.TrustedMaterial == nil {
tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets)
if err != nil {
return fmt.Errorf("unable to load TSA certificates: %w", err)
@@ -169,14 +180,16 @@ 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) {
if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) {
if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil {
return err
}
@@ -186,7 +199,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)
@@ -223,13 +236,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 {

View File

@@ -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"
@@ -98,6 +99,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,
@@ -112,23 +128,39 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
MaxWorkers: c.MaxWorkers,
UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps,
NewBundleFormat: c.NewBundleFormat,
TrustedMaterial: trustedMaterial,
}
if c.CheckClaims {
co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
}
if c.TrustedRootPath != "" {
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
if err != nil {
return fmt.Errorf("loading trusted root: %w", err)
}
} else 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) == "" {
co.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 c.NewBundleFormat {
if err = checkSigstoreBundleUnsupportedOptions(c); err != nil {
return err
}
co.TrustedMaterial, err = loadTrustedRoot(ctx, c.TrustedRootPath)
if err != nil {
return err
if co.TrustedMaterial == nil {
return fmt.Errorf("trusted root is required when using new bundle format")
}
}
// Ignore Signed Certificate Timestamp if the flag is set or a key is provided
if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !c.NewBundleFormat {
if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !c.NewBundleFormat {
co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx)
if err != nil {
return fmt.Errorf("getting ctlog public keys: %w", err)
@@ -136,7 +168,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
}
// If we are using signed timestamps, we need to load the TSA certificates
if co.UseSignedTimestamps && !c.NewBundleFormat {
if co.UseSignedTimestamps && co.TrustedMaterial == nil && !c.NewBundleFormat {
tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets)
if err != nil {
return fmt.Errorf("unable to load TSA certificates: %w", err)
@@ -154,15 +186,17 @@ 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)
}
}
}
if keylessVerification(c.KeyRef, c.Sk) {
if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) {
if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil {
return err
}
@@ -202,13 +236,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 {

View File

@@ -37,6 +37,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"
@@ -140,16 +141,29 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
}
}
if c.TrustedRootPath != "" {
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
if err != nil {
return fmt.Errorf("loading trusted root: %w", err)
}
} else 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) == "" {
co.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 co.NewBundleFormat {
if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SigRef, c.SCTRef) > 0 {
return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root")
}
if co.TrustedMaterial == nil {
co.TrustedMaterial, err = loadTrustedRoot(ctx, c.TrustedRootPath)
if err != nil {
return err
}
return fmt.Errorf("trusted root is required when using new bundle format")
}
bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath)
@@ -191,7 +205,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
} else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps {
return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path")
}
if co.UseSignedTimestamps {
if co.UseSignedTimestamps && co.TrustedMaterial == nil {
tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets)
if err != nil {
return fmt.Errorf("unable to load TSA certificates: %w", err)
@@ -209,15 +223,17 @@ 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)
}
}
}
if keylessVerification(c.KeyRef, c.Sk) {
if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) {
if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil {
return err
}
@@ -315,7 +331,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)
@@ -393,12 +409,3 @@ func payloadDigest(blobRef string) (string, []byte, error) {
}
return hexAlg, digestBytes, nil
}
func loadTrustedRoot(_ context.Context, trustedRootPath string) (*root.TrustedRoot, error) {
if trustedRootPath != "" {
return root.NewTrustedRootFromPath(trustedRootPath)
}
// Assume we're using public good instance; fetch via TUF
// TODO: allow custom TUF settings
return root.FetchTrustedRoot()
}

View File

@@ -38,12 +38,14 @@ 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"
"github.com/sigstore/cosign/v2/pkg/policy"
sigs "github.com/sigstore/cosign/v2/pkg/signature"
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/root"
sgverify "github.com/sigstore/sigstore-go/pkg/verify"
"github.com/sigstore/sigstore/pkg/cryptoutils"
)
@@ -181,16 +183,29 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st
co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
}
if c.TrustedRootPath != "" {
co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath)
if err != nil {
return fmt.Errorf("loading trusted root: %w", err)
}
} else 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) == "" {
co.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 co.NewBundleFormat {
if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SCTRef) > 0 {
return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root")
}
if co.TrustedMaterial == nil {
co.TrustedMaterial, err = loadTrustedRoot(ctx, c.TrustedRootPath)
if err != nil {
return err
}
return fmt.Errorf("trusted root is required when using new bundle format")
}
bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath)
@@ -215,7 +230,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st
} else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps {
return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path")
}
if co.UseSignedTimestamps {
if co.UseSignedTimestamps && co.TrustedMaterial == nil {
tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets)
if err != nil {
return fmt.Errorf("unable to load TSA certificates: %w", err)
@@ -233,21 +248,23 @@ 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) {
if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) {
if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil {
return err
}
}
// 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)

View File

@@ -819,6 +819,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)
@@ -1332,6 +1333,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)

8
go.mod
View File

@@ -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.9.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@@ -172,6 +173,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/trillian v1.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
@@ -184,10 +186,16 @@ 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.16.0 // indirect
github.com/in-toto/attestation v1.1.1 // 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.2 // 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

4
go.sum
View File

@@ -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.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/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/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E=
@@ -335,6 +337,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
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=

23
pkg/cosign/env/env.go vendored
View File

@@ -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",

View File

@@ -44,6 +44,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"
"github.com/transparency-dev/merkle/proof"
@@ -218,7 +220,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
}
@@ -442,20 +444,14 @@ 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 {
return fmt.Errorf("rekor Public key for LogID %s is not type ecdsa.PublicKey", k)
}
}
hashes := [][]byte{}
for _, h := range e.Verification.InclusionProof.Hashes {
@@ -477,6 +473,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,
@@ -484,6 +497,13 @@ func VerifyTLogEntryOffline(ctx context.Context, e *models.LogEntryAnon, rekorPu
LogID: *e.LogID,
}
// Make sure all the rekorPubKeys are ecsda.PublicKeys
for k, v := range rekorPubKeys.Keys {
if _, ok := v.PubKey.(*ecdsa.PublicKey); !ok {
return fmt.Errorf("rekor Public key for LogID %s is not type ecdsa.PublicKey", k)
}
}
pubKey, ok := rekorPubKeys.Keys[payload.LogID]
if !ok {
return errors.New("rekor log public key not found for payload. Check your TUF root (see cosign initialize) or set a custom key with env var SIGSTORE_REKOR_PUBLIC_KEY")

View File

@@ -15,19 +15,27 @@
package cosign
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"strings"
"testing"
"time"
"github.com/go-openapi/swag"
ttestdata "github.com/google/certificate-transparency-go/trillian/testdata"
"github.com/sigstore/rekor/pkg/generated/models"
rtypes "github.com/sigstore/rekor/pkg/types"
hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/tuf"
)
@@ -173,7 +181,57 @@ 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)
// generate a valid log entry with valid inclusion proof
sigSigner, err := signature.LoadRSAPKCS1v15Signer(rsaPrivKey.(*rsa.PrivateKey), crypto.SHA256)
if err != nil {
t.Fatalf("Unable to load RSA signer")
}
ctx := context.Background()
blob := []byte("foo")
blobSignature, err := sigSigner.SignMessage(bytes.NewReader(blob))
if err != nil {
t.Fatal(err)
}
logID := calculateLogID(t, signer.Public())
payloadHash := sha256.Sum256(blob)
artifactProperties := rtypes.ArtifactProperties{
ArtifactHash: hex.EncodeToString(payloadHash[:]),
SignatureBytes: blobSignature,
PublicKeyBytes: [][]byte{rsaPEM},
PKIFormat: "x509",
}
entryProps, err := hashedrekord_v001.V001Entry{}.CreateFromArtifactProperties(ctx, artifactProperties)
if err != nil {
t.Fatal(err)
}
rekorEntry, err := rtypes.UnmarshalEntry(entryProps)
if err != nil {
t.Fatal(err)
}
canonicalEntry, err := rekorEntry.Canonicalize(ctx)
if err != nil {
t.Fatal(err)
}
lea := &models.LogEntryAnon{
Body: base64.StdEncoding.EncodeToString(canonicalEntry),
LogIndex: swag.Int64(0),
LogID: swag.String(logID),
IntegratedTime: swag.Int64(time.Now().Unix()),
}
entryUUID, err := ComputeLeafHash(lea)
if err != nil {
t.Fatal(err)
}
lea.Verification = &models.LogEntryAnonVerification{
InclusionProof: &models.InclusionProof{
LogIndex: swag.Int64(0),
TreeSize: swag.Int64(1),
RootHash: swag.String(hex.EncodeToString(entryUUID)),
Hashes: []string{},
},
}
err = VerifyTLogEntryOffline(ctx, lea, &rekorPubKeys, nil)
if err == nil {
t.Fatal("Wanted error got none")
}

111
pkg/cosign/tuf.go Normal file
View File

@@ -0,0 +1,111 @@
// 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)
}
tr, err := root.NewLiveTrustedRoot(opts)
if err != nil {
return nil, fmt.Errorf("error getting live trusted root: %w", err)
}
return tr, nil
}
// 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 nil
}
return fmt.Errorf("could not find cached root.json")
}

View File

@@ -29,6 +29,7 @@ import (
"errors"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"regexp"
@@ -38,7 +39,6 @@ import (
"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
"github.com/digitorus/timestamp"
"github.com/go-openapi/runtime"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
@@ -66,6 +66,7 @@ import (
sgbundle "github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
"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"
@@ -96,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
@@ -170,10 +174,6 @@ type CheckOpts struct {
// NewBundleFormat enables the new bundle format (Cosign Bundle Spec) and the new verifier.
NewBundleFormat bool
// TrustedMaterial is the trusted material to use for verification.
// Currently, this is only applicable when NewBundleFormat is true.
TrustedMaterial root.TrustedMaterial
}
type verifyTrustedMaterial struct {
@@ -342,11 +342,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 {
@@ -357,15 +373,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")
@@ -374,22 +395,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
@@ -527,7 +548,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 {
@@ -551,7 +572,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
}
@@ -594,8 +615,8 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co
}
// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil {
return nil, false, errors.New("one of verifier, root certs, or trusted root is required")
}
// This is a carefully optimized sequence for fetching the signatures of the
@@ -639,8 +660,8 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co
// If there were no valid signatures, we return an error.
func VerifyLocalImageSignatures(ctx context.Context, path string, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) {
// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil {
return nil, false, errors.New("one of verifier, root certs, or trusted root is required")
}
se, err := layout.SignedImageIndex(path)
@@ -805,7 +826,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
}
@@ -1016,8 +1037,8 @@ func VerifyImageAttestations(ctx context.Context, signedImgRef name.Reference, c
// If there were no valid signatures, we return an error.
func VerifyLocalImageAttestations(ctx context.Context, path string, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) {
// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil {
return nil, false, errors.New("one of verifier, root certs, or trusted root is required")
}
se, err := layout.SignedImageIndex(path)
@@ -1164,9 +1185,26 @@ 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
logID, err := hex.DecodeString(payload.LogID)
if err != nil {
return false, fmt.Errorf("decoding log ID: %w", err)
}
body, _ := base64.StdEncoding.DecodeString(payload.Body.(string))
entry, err := tlog.NewEntry(body, payload.IntegratedTime, payload.LogIndex, 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 {
@@ -1220,6 +1258,40 @@ func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) {
return true, nil
}
type signedEntityForTimestamp struct {
verify.BaseSignedEntity
timestamp *cbundle.RFC3161Timestamp
sigContent *sigContent
}
type sigContent struct {
rawSig []byte
}
func (e *signedEntityForTimestamp) Timestamps() ([][]byte, error) {
timestamps := make([][]byte, 1)
timestamps[0] = e.timestamp.SignedRFC3161Timestamp
return timestamps, nil
}
func (e *signedEntityForTimestamp) SignatureContent() (verify.SignatureContent, error) {
return e.sigContent, nil
}
func (s *sigContent) Signature() []byte {
return s.rawSig
}
func (s *sigContent) EnvelopeContent() verify.EnvelopeContent {
log.Fatal("programmer error: EnvelopeContent was called but not implemented")
return nil
}
func (s *sigContent) MessageSignatureContent() verify.MessageSignatureContent {
log.Fatal("programmer error: MessageSignatureContent was called but not implemented")
return 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
@@ -1231,7 +1303,7 @@ func VerifyRFC3161Timestamp(sig oci.Signature, co *CheckOpts) (*timestamp.Timest
return nil, err
case ts == nil:
return nil, nil
case co.TSARootCertificates == nil:
case co.TSARootCertificates == nil && co.TrustedMaterial == nil:
return nil, errors.New("no TSA root certificate(s) provided to verify timestamp")
}
@@ -1257,6 +1329,18 @@ func VerifyRFC3161Timestamp(sig oci.Signature, co *CheckOpts) (*timestamp.Timest
tsBytes = rawSig
}
if co.TrustedMaterial != nil {
entity := &signedEntityForTimestamp{
timestamp: ts,
sigContent: &sigContent{rawSig: tsBytes},
}
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 &timestamp.Timestamp{Time: verifiedTimestamps[0].Time}, nil
}
return tsaverification.VerifyTimestampResponse(ts.SignedRFC3161Timestamp, bytes.NewReader(tsBytes),
tsaverification.VerifyOpts{
TSACertificate: co.TSACertificate,
@@ -1464,8 +1548,8 @@ func correctAnnotations(wanted, have map[string]interface{}) bool {
// If there were no valid signatures, we return an error, using OCI 1.1+ behavior.
func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) {
// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil {
return nil, false, errors.New("one of verifier, root certs, or trusted root is required")
}
// This is a carefully optimized sequence for fetching the signatures of the

View File

@@ -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 {

View File

@@ -60,6 +60,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"
@@ -555,6 +556,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)
@@ -572,6 +605,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
@@ -687,6 +721,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)
@@ -704,6 +747,7 @@ func TestSignVerifyWithTUFMirror(t *testing.T) {
t.Fatal(err)
}
// Sign an image
repo, stop := reg(t)
defer stop()
imgName := path.Join(repo, "cosign-e2e-tuf")
@@ -717,6 +761,10 @@ func TestSignVerifyWithTUFMirror(t *testing.T) {
SkipConfirmation: true,
TSAServerURL: tsaServer.URL + "/api/v1/timestamp",
}
trustedMaterial, err := cosign.TrustedRoot()
if err == nil {
ko.TrustedMaterial = trustedMaterial
}
so := options.SignOptions{
Upload: true,
TlogUpload: true,
@@ -728,6 +776,8 @@ func TestSignVerifyWithTUFMirror(t *testing.T) {
return
}
must(gotErr, t)
// Verify an image
issuer := os.Getenv("OIDC_URL")
verifyCmd := cliverify.VerifyCommand{
CertVerifyOptions: options.CertVerifyOptions{
@@ -744,6 +794,42 @@ func TestSignVerifyWithTUFMirror(t *testing.T) {
} else {
must(gotErr, t)
}
// Sign a blob
blob := "someblob"
blobDir := t.TempDir()
bp := filepath.Join(blobDir, blob)
if err := os.WriteFile(bp, []byte(blob), 0644); err != nil {
t.Fatal(err)
}
tsPath := filepath.Join(blobDir, "ts.txt")
bundlePath := filepath.Join(blobDir, "bundle.sig")
// TODO(cmurphy): make this work with ko.NewBundleFormat = true
ko.BundlePath = bundlePath
ko.RFC3161TimestampPath = tsPath
_, gotErr = sign.SignBlobCmd(ro, ko, bp, true, "", "", true)
if test.wantSignErr {
mustErr(gotErr, t)
} else {
must(gotErr, t)
}
// Verify a blob
verifyBlobCmd := cliverify.VerifyBlobCmd{
KeyOpts: ko,
CertVerifyOptions: options.CertVerifyOptions{
CertOidcIssuer: issuer,
CertIdentity: certID,
},
Offline: true,
UseSignedTimestamps: true,
}
gotErr = verifyBlobCmd.Exec(ctx, bp)
if test.wantVerifyErr {
mustErr(gotErr, t)
} else {
must(gotErr, t)
}
})
}
}