Skip to content

Commit

Permalink
Upgrade to TUF v2 client with trusted root
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
cmurphy committed Oct 22, 2024
1 parent 4626251 commit cb58bd3
Show file tree
Hide file tree
Showing 18 changed files with 449 additions and 107 deletions.
20 changes: 18 additions & 2 deletions cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"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"
)

Expand All @@ -32,12 +34,26 @@ 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 {
// We assume that if a trusted_root.json was found, the fulcio chain was included in it.
// fs.Chain will be ignored as root.VerifySignedCertificateTimestamp relies on the trusted root.
// Detached SCTs cannot be verified with this function.
certs, err := cryptoutils.UnmarshalCertificatesFromPEM(fs.Cert)
if err != nil || len(certs) < 1 {
return nil, fmt.Errorf("unmarshalling SCT from PEM: %w", err)
}
if err := verify.VerifySignedCertificateTimestamp(certs[0], 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)
Expand Down
36 changes: 33 additions & 3 deletions cmd/cosign/cli/initialize/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ import (
_ "embed" // To enable the `go:embed` directive.
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/sigstore/cosign/v2/internal/ui"
"github.com/sigstore/cosign/v2/pkg/blob"
"github.com/sigstore/sigstore/pkg/tuf"
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 {
Expand All @@ -36,11 +41,36 @@ func DoInitialize(ctx context.Context, root, mirror string) error {
}
}

if err := tuf.Initialize(ctx, mirror, rootFileBytes); err != nil {
opts := tuf.DefaultOptions()
if root != "" {
opts.Root = rootFileBytes
}
if mirror != "" {
opts.RepositoryBaseURL = mirror
}
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.")
}
// 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.WriteFile(filepath.FromSlash(filepath.Join(opts.CachePath, "remote.json")), remoteBytes, 0o600); err != nil {
return fmt.Errorf("storing remote: %w", 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
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/cosign/cli/options/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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/pkg/cosign"
)

func Sign() *cobra.Command {
Expand Down Expand Up @@ -103,6 +104,9 @@ race conditions or (worse) malicious tampering.
if err != nil {
return err
}

trustedMaterial, _ := cosign.TrustedRoot()

ko := options.KeyOpts{
KeyRef: o.Key,
PassFunc: generate.GetPass,
Expand All @@ -126,6 +130,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 == "" {
Expand Down
16 changes: 11 additions & 5 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ 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/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
Expand Down Expand Up @@ -391,7 +392,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)
Expand Down Expand Up @@ -505,9 +506,14 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin
return nil, err
}
if contains {
pubKeys, err := cosign.GetCTLogPubs(ctx)
if err != nil {
return nil, fmt.Errorf("getting CTLog public keys: %w", err)
var pubKeys any
if trustedMaterial != nil {
pubKeys = trustedMaterial.CTLogs()
} else {
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)
Expand Down Expand Up @@ -567,7 +573,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...")
Expand Down
10 changes: 5 additions & 5 deletions cmd/cosign/cli/sign/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/cosign/cli/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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/pkg/cosign"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -68,6 +69,7 @@ func SignBlob() *cobra.Command {
if err != nil {
return err
}
trustedMaterial, _ := cosign.TrustedRoot()
ko := options.KeyOpts{
KeyRef: o.Key,
PassFunc: generate.GetPass,
Expand All @@ -93,6 +95,7 @@ func SignBlob() *cobra.Command {
TSAServerURL: o.TSAServerURL,
RFC3161TimestampPath: o.RFC3161TimestampPath,
IssueCertificateForExistingKey: o.IssueCertificate,
TrustedMaterial: trustedMaterial,
}

for _, blob := range args {
Expand Down
42 changes: 29 additions & 13 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -128,6 +129,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
return fmt.Errorf("constructing client options: %w", err)
}

trustedMaterial, _ := cosign.TrustedRoot()
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,
Expand All @@ -144,6 +155,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
Expand All @@ -167,11 +179,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) {
Expand All @@ -184,7 +198,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)
Expand Down Expand Up @@ -221,13 +235,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 {
Expand Down
Loading

0 comments on commit cb58bd3

Please sign in to comment.