From 25523716c8eecc5d72131d2cec386a4c98d1a111 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 11 Sep 2024 09:00:48 -0400 Subject: [PATCH] Fixes #3700: add trusted-root create helper command To help cosign users move from providing disparate verification material to a single file that contains the needed verification material. This makes it easier for users to rotate key material and specify what time period different keys were valid. Signed-off-by: Zach Steindler --- cmd/cosign/cli/commands.go | 1 + cmd/cosign/cli/options/trustedroot.go | 65 ++++++ cmd/cosign/cli/trustedroot.go | 65 ++++++ cmd/cosign/cli/trustedroot/trustedroot.go | 194 ++++++++++++++++++ .../cli/trustedroot/trustedroot_test.go | 127 ++++++++++++ go.mod | 4 +- go.sum | 8 +- 7 files changed, 458 insertions(+), 6 deletions(-) create mode 100644 cmd/cosign/cli/options/trustedroot.go create mode 100644 cmd/cosign/cli/trustedroot.go create mode 100644 cmd/cosign/cli/trustedroot/trustedroot.go create mode 100644 cmd/cosign/cli/trustedroot/trustedroot_test.go diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index 6c67e890c40..25d710e0b09 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -120,6 +120,7 @@ func New() *cobra.Command { cmd.AddCommand(VerifyBlob()) cmd.AddCommand(VerifyBlobAttestation()) cmd.AddCommand(Triangulate()) + cmd.AddCommand(TrustedRoot()) cmd.AddCommand(Env()) cmd.AddCommand(version.WithFont("starwars")) diff --git a/cmd/cosign/cli/options/trustedroot.go b/cmd/cosign/cli/options/trustedroot.go new file mode 100644 index 00000000000..89f3810a409 --- /dev/null +++ b/cmd/cosign/cli/options/trustedroot.go @@ -0,0 +1,65 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/spf13/cobra" +) + +type TrustedRootCreateOptions struct { + CAIntermediates string + CARoots string + CertChain string + Out string + RekorURL string + TSACertChainPath string +} + +var _ Interface = (*TrustedRootCreateOptions)(nil) + +func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.CAIntermediates, "ca-intermediates", "", + "path to a file of intermediate CA certificates in PEM format which will be needed "+ + "when building the certificate chains for the signing certificate. "+ + "The flag is optional and must be used together with --ca-roots, conflicts with "+ + "--certificate-chain.") + _ = cmd.Flags().SetAnnotation("ca-intermediates", cobra.BashCompFilenameExt, []string{"cert"}) + + cmd.Flags().StringVar(&o.CARoots, "ca-roots", "", + "path to a bundle file of CA certificates in PEM format which will be needed "+ + "when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.") + _ = cmd.Flags().SetAnnotation("ca-roots", cobra.BashCompFilenameExt, []string{"cert"}) + + cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "", + "path to a list of CA certificates in PEM format which will be needed "+ + "when building the certificate chain for the signing certificate. "+ + "Must start with the parent intermediate CA certificate of the "+ + "signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates.") + _ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) + + cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain") + cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain") + + cmd.Flags().StringVar(&o.Out, "out", "", + "path to output trusted root") + + cmd.Flags().StringVar(&o.RekorURL, "rekor-url", "", + "address of rekor STL server") + + cmd.Flags().StringVar(&o.TSACertChainPath, "timestamp-certificate-chain", "", + "path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. "+ + "Optionally may contain intermediate CA certificates") +} diff --git a/cmd/cosign/cli/trustedroot.go b/cmd/cosign/cli/trustedroot.go new file mode 100644 index 00000000000..24d271e3e33 --- /dev/null +++ b/cmd/cosign/cli/trustedroot.go @@ -0,0 +1,65 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/trustedroot" +) + +func TrustedRoot() *cobra.Command { + cmd := &cobra.Command{ + Use: "trusted-root", + Short: "Interact with a Sigstore protobuf trusted root", + Long: "Tools for interacting with a Sigstore protobuf trusted root", + } + + cmd.AddCommand(trustedRootCreate()) + + return cmd +} + +func trustedRootCreate() *cobra.Command { + o := &options.TrustedRootCreateOptions{} + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a Sigstore protobuf trusted root", + Long: "Create a Sigstore protobuf trusted root by supplying verification material", + RunE: func(cmd *cobra.Command, args []string) error { + trCreateCmd := &trustedroot.TrustedRootCreateCmd{ + CAIntermediates: o.CAIntermediates, + CARoots: o.CARoots, + CertChain: o.CertChain, + Out: o.Out, + RekorURL: o.RekorURL, + TSACertChainPath: o.TSACertChainPath, + } + + ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) + defer cancel() + + return trCreateCmd.Exec(ctx) + }, + } + + o.AddFlags(cmd) + return cmd +} diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go new file mode 100644 index 00000000000..125ed4f06c8 --- /dev/null +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -0,0 +1,194 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trustedroot + +import ( + "context" + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/sigstore/sigstore-go/pkg/root" + + "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v2/internal/ui" +) + +type TrustedRootCreateCmd struct { + CAIntermediates string + CARoots string + CertChain string + Out string + RekorURL string + TSACertChainPath string +} + +func (c *TrustedRootCreateCmd) Exec(ctx context.Context) error { + var fulcioCertAuthorities []root.CertificateAuthority + var timestampAuthorities []root.CertificateAuthority + rekorTransparencyLogs := make(map[string]*root.TransparencyLog) + + if c.CertChain != "" { + fulcioAuthority, err := parsePEMFile(c.CertChain) + if err != nil { + return err + } + fulcioCertAuthorities = append(fulcioCertAuthorities, *fulcioAuthority) + + } else if c.CARoots != "" { + roots, err := parseCerts(c.CARoots) + if err != nil { + return err + } + + var intermediates []*x509.Certificate + if c.CAIntermediates != "" { + intermediates, err = parseCerts(c.CAIntermediates) + if err != nil { + return err + } + } + + // Here we're trying to "flatten" the x509.CertPool cosign was using + // into a trusted root with a clear mapping between roots and + // intermediates. Make a guess that if there are intermediates, there + // is one per root. + + for i, rootCert := range roots { + var fulcioAuthority root.CertificateAuthority + fulcioAuthority.Root = rootCert + if i < len(intermediates) { + fulcioAuthority.Intermediates = []*x509.Certificate{intermediates[i]} + } + fulcioCertAuthorities = append(fulcioCertAuthorities, fulcioAuthority) + } + } + + if c.RekorURL != "" { + rekorClient, err := rekor.NewClient(c.RekorURL) + if err != nil { + return fmt.Errorf("creating Rekor client: %w", err) + } + + rekorPubKey, err := rekorClient.Pubkey.GetPublicKey(nil) + if err != nil { + return err + } + + block, _ := pem.Decode([]byte(rekorPubKey.Payload)) + if block == nil { + return errors.New("failed to decode public key of server") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return err + } + + keyHash := sha256.Sum256(block.Bytes) + keyID := base64.StdEncoding.EncodeToString(keyHash[:]) + + rekorTransparencyLog := root.TransparencyLog{ + BaseURL: c.RekorURL, + HashFunc: crypto.Hash(crypto.SHA256), + ID: keyHash[:], + PublicKey: pub, + SignatureHashFunc: crypto.Hash(crypto.SHA256), + } + + rekorTransparencyLogs[keyID] = &rekorTransparencyLog + } + + if c.TSACertChainPath != "" { + timestampAuthority, err := parsePEMFile(c.TSACertChainPath) + if err != nil { + return err + } + timestampAuthorities = append(timestampAuthorities, *timestampAuthority) + } + + newTrustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, + fulcioCertAuthorities, nil, timestampAuthorities, rekorTransparencyLogs, + ) + if err != nil { + return err + } + + var trBytes []byte + + trBytes, err = newTrustedRoot.MarshalJSON() + if err != nil { + return err + } + + if c.Out != "" { + err = os.WriteFile(c.Out, trBytes, 0640) + if err != nil { + return err + } + } else { + ui.Infof(ctx, string(trBytes)) + } + + return nil +} + +func parsePEMFile(path string) (*root.CertificateAuthority, error) { + certs, err := parseCerts(path) + if err != nil { + return nil, err + } + + var ca root.CertificateAuthority + ca.Root = certs[len(certs)-1] + if len(certs) > 1 { + ca.Intermediates = certs[:len(certs)-1] + } + + return &ca, nil +} + +func parseCerts(path string) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + for block, contents := pem.Decode(contents); ; block, contents = pem.Decode(contents) { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, cert) + + if len(contents) == 0 { + break + } + } + + if len(certs) == 0 { + return nil, fmt.Errorf("No certificates in file %s", path) + } + + return certs, nil +} diff --git a/cmd/cosign/cli/trustedroot/trustedroot_test.go b/cmd/cosign/cli/trustedroot/trustedroot_test.go new file mode 100644 index 00000000000..582b6cf7f29 --- /dev/null +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -0,0 +1,127 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trustedroot + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/sigstore/sigstore-go/pkg/root" +) + +func TestTrustedRootCreate(t *testing.T) { + ctx := context.Background() + + // Make some certificate chains + td := t.TempDir() + + fulcioChainPath := filepath.Join(td, "fulcio.crt") + makeChain(t, fulcioChainPath, 2) + + tsaChainPath := filepath.Join(td, "timestamp.crt") + makeChain(t, tsaChainPath, 3) + + outPath := filepath.Join(td, "trustedroot.json") + + trustedrootCreate := TrustedRootCreateCmd{ + CertChain: fulcioChainPath, + Out: outPath, + TSACertChainPath: tsaChainPath, + } + + err := trustedrootCreate.Exec(ctx) + checkErr(t, err) + + tr, err := root.NewTrustedRootFromPath(outPath) + checkErr(t, err) + + fulcioCAs := tr.FulcioCertificateAuthorities() + + if len(fulcioCAs) != 1 { + t.Fatal("unexpected number of fulcio certificate authorities") + } + + if len(fulcioCAs[0].Intermediates) != 1 { + t.Fatal("unexpected number of fulcio intermediate certificates") + } + + timestampAuthorities := tr.TimestampingAuthorities() + if len(timestampAuthorities) != 1 { + t.Fatal("unexpected number of timestamp authorities") + } + + if len(timestampAuthorities[0].Intermediates) != 2 { + t.Fatal("unexpected number of timestamp intermediate certificates") + } +} + +func makeChain(t *testing.T, path string, size int) { + fd, err := os.Create(path) + checkErr(t, err) + + chainCert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + BasicConstraintsValid: true, + IsCA: true, + } + chainKey, err := rsa.GenerateKey(rand.Reader, 512) //nolint:gosec + checkErr(t, err) + rootDer, err := x509.CreateCertificate(rand.Reader, chainCert, chainCert, &chainKey.PublicKey, chainKey) + checkErr(t, err) + + for i := 1; i < size; i++ { + intermediateCert := &x509.Certificate{ + SerialNumber: big.NewInt(1 + int64(i)), + BasicConstraintsValid: true, + IsCA: true, + } + intermediateKey, err := rsa.GenerateKey(rand.Reader, 512) //nolint:gosec + checkErr(t, err) + intermediateDer, err := x509.CreateCertificate(rand.Reader, intermediateCert, chainCert, &intermediateKey.PublicKey, chainKey) + checkErr(t, err) + + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: intermediateDer, + } + err = pem.Encode(fd, block) + checkErr(t, err) + + chainCert = intermediateCert + chainKey = intermediateKey + } + + // Write out root last + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: rootDer, + } + err = pem.Encode(fd, block) + checkErr(t, err) +} + +func checkErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 620a6b322ba..78d05ea6182 100644 --- a/go.mod +++ b/go.mod @@ -34,8 +34,8 @@ require ( github.com/sigstore/fulcio v1.6.3 github.com/sigstore/protobuf-specs v0.3.2 github.com/sigstore/rekor v1.3.6 - github.com/sigstore/sigstore v1.8.8 - github.com/sigstore/sigstore-go v0.6.0 + github.com/sigstore/sigstore v1.8.9 + github.com/sigstore/sigstore-go v0.6.1 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8 diff --git a/go.sum b/go.sum index 0a56c4033ec..4f5c52d4168 100644 --- a/go.sum +++ b/go.sum @@ -614,10 +614,10 @@ github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWw github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA= github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= -github.com/sigstore/sigstore v1.8.8 h1:B6ZQPBKK7Z7tO3bjLNnlCMG+H66tO4E/+qAphX8T/hg= -github.com/sigstore/sigstore v1.8.8/go.mod h1:GW0GgJSCTBJY3fUOuGDHeFWcD++c4G8Y9K015pwcpDI= -github.com/sigstore/sigstore-go v0.6.0 h1:X72BkR8kXFcdhF/V5GA2fpFvCz+VyZ6fI0YgTBn5feI= -github.com/sigstore/sigstore-go v0.6.0/go.mod h1:+RyopI/FJDE6z5WVs2sQ2nkc+zsxxByDmbp8a4HoxbA= +github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= +github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= +github.com/sigstore/sigstore-go v0.6.1 h1:tGkkv1oDIER+QYU5MrjqlttQOVDWfSkmYwMqkJhB/cg= +github.com/sigstore/sigstore-go v0.6.1/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 h1:2zHmUvaYCwV6LVeTo+OAkTm8ykOGzA9uFlAjwDPAUWM= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8/go.mod h1:OEhheBplZinUsm7W9BupafztVZV3ldkAxEHbpAeC0Pk= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 h1:RKk4Z+qMaLORUdT7zntwMqKiYAej1VQlCswg0S7xNSY=