From 9b17791a21f9eef8ff59e6ea138e1b116dc571b8 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Tue, 29 Oct 2024 18:11:36 -0400 Subject: [PATCH] Add trusted-root create helper command (#3876) * 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 * Linter fixes and docgen Signed-off-by: Zach Steindler * Fix Windows unit test Signed-off-by: Zach Steindler * Output via stdout instead of stderr Signed-off-by: Zach Steindler * Add ctlogs to `cosign trusted-root create` With `--ignore-sct` to support if you are using keys instead of Fulcio. Signed-off-by: Zach Steindler * Replace `--rekor-url` with `--ignore-tlog` Similar to `--ignore-sct` Signed-off-by: Zach Steindler * Just use paths to files on disk Instead of clients querying remote servers Signed-off-by: Zach Steindler * Add the ability to supply multiple verification material Also add ability to specify validity start time for keys Signed-off-by: Zach Steindler * Don't panic if there's unexpected content in PEM file Update tests, also fix documentation for flags that were removed. Co-authored-by: Dmitry S Signed-off-by: Zach Steindler * remove trailing newline Signed-off-by: Zach Steindler * Simplify imports Signed-off-by: Zach Steindler --------- Signed-off-by: Zach Steindler Co-authored-by: Dmitry S --- cmd/cosign/cli/commands.go | 1 + cmd/cosign/cli/options/trustedroot.go | 62 ++++++ cmd/cosign/cli/trustedroot.go | 66 ++++++ cmd/cosign/cli/trustedroot/trustedroot.go | 205 ++++++++++++++++++ .../cli/trustedroot/trustedroot_test.go | 133 ++++++++++++ doc/cosign.md | 1 + doc/cosign_trusted-root.md | 27 +++ doc/cosign_trusted-root_create.md | 37 ++++ 8 files changed, 532 insertions(+) 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 create mode 100644 doc/cosign_trusted-root.md create mode 100644 doc/cosign_trusted-root_create.md 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..298d34d9c8a --- /dev/null +++ b/cmd/cosign/cli/options/trustedroot.go @@ -0,0 +1,62 @@ +// +// 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 { + CertChain []string + CtfeKeyPath []string + CtfeStartTime []string + Out string + RekorKeyPath []string + RekorStartTime []string + TSACertChainPath []string +} + +var _ Interface = (*TrustedRootCreateOptions)(nil) + +func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringArrayVar(&o.CertChain, "certificate-chain", nil, + "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.") + _ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) + + cmd.Flags().StringArrayVar(&o.CtfeKeyPath, "ctfe-key", nil, + "path to a PEM-encoded public key used by certificate authority for "+ + "certificate transparency log.") + + cmd.Flags().StringArrayVar(&o.CtfeStartTime, "ctfe-start-time", nil, + "RFC 3339 string describing validity start time for key use by "+ + "certificate transparency log.") + + cmd.Flags().StringVar(&o.Out, "out", "", "path to output trusted root") + + cmd.Flags().StringArrayVar(&o.RekorKeyPath, "rekor-key", nil, + "path to a PEM-encoded public key used by transparency log like Rekor.") + + cmd.Flags().StringArrayVar(&o.RekorStartTime, "rekor-start-time", nil, + "RFC 3339 string describing validity start time for key use by "+ + "transparency log like Rekor.") + + cmd.Flags().StringArrayVar(&o.TSACertChainPath, "timestamp-certificate-chain", nil, + "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..5ea67fc33e1 --- /dev/null +++ b/cmd/cosign/cli/trustedroot.go @@ -0,0 +1,66 @@ +// +// 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, _ []string) error { + trCreateCmd := &trustedroot.CreateCmd{ + CertChain: o.CertChain, + CtfeKeyPath: o.CtfeKeyPath, + CtfeStartTime: o.CtfeStartTime, + Out: o.Out, + RekorKeyPath: o.RekorKeyPath, + RekorStartTime: o.RekorStartTime, + 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..9b6766897d4 --- /dev/null +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -0,0 +1,205 @@ +// +// 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/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "os" + "time" + + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +type CreateCmd struct { + CertChain []string + CtfeKeyPath []string + CtfeStartTime []string + Out string + RekorKeyPath []string + RekorStartTime []string + TSACertChainPath []string +} + +func (c *CreateCmd) Exec(_ context.Context) error { + var fulcioCertAuthorities []root.CertificateAuthority + ctLogs := make(map[string]*root.TransparencyLog) + var timestampAuthorities []root.CertificateAuthority + rekorTransparencyLogs := make(map[string]*root.TransparencyLog) + + for i := 0; i < len(c.CertChain); i++ { + fulcioAuthority, err := parsePEMFile(c.CertChain[i]) + if err != nil { + return err + } + fulcioCertAuthorities = append(fulcioCertAuthorities, *fulcioAuthority) + } + + for i := 0; i < len(c.CtfeKeyPath); i++ { + ctLogPubKey, id, idBytes, err := getPubKey(c.CtfeKeyPath[i]) + if err != nil { + return err + } + + startTime := time.Unix(0, 0) + + if i < len(c.CtfeStartTime) { + startTime, err = time.Parse(time.RFC3339, c.CtfeStartTime[i]) + if err != nil { + return err + } + } + + ctLogs[id] = &root.TransparencyLog{ + HashFunc: crypto.SHA256, + ID: idBytes, + ValidityPeriodStart: startTime, + PublicKey: *ctLogPubKey, + SignatureHashFunc: crypto.SHA256, + } + } + + for i := 0; i < len(c.RekorKeyPath); i++ { + tlogPubKey, id, idBytes, err := getPubKey(c.RekorKeyPath[i]) + if err != nil { + return err + } + + startTime := time.Unix(0, 0) + + if i < len(c.RekorStartTime) { + startTime, err = time.Parse(time.RFC3339, c.RekorStartTime[i]) + if err != nil { + return err + } + } + + rekorTransparencyLogs[id] = &root.TransparencyLog{ + HashFunc: crypto.SHA256, + ID: idBytes, + ValidityPeriodStart: startTime, + PublicKey: *tlogPubKey, + SignatureHashFunc: crypto.SHA256, + } + } + + for i := 0; i < len(c.TSACertChainPath); i++ { + timestampAuthority, err := parsePEMFile(c.TSACertChainPath[i]) + if err != nil { + return err + } + timestampAuthorities = append(timestampAuthorities, *timestampAuthority) + } + + newTrustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, + fulcioCertAuthorities, ctLogs, 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, 0600) + if err != nil { + return err + } + } else { + fmt.Println(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] + ca.ValidityPeriodStart = certs[len(certs)-1].NotBefore + 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 != nil; 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 +} + +func getPubKey(path string) (*crypto.PublicKey, string, []byte, error) { + pemBytes, err := os.ReadFile(path) + if err != nil { + return nil, "", []byte{}, err + } + + pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pemBytes) + if err != nil { + return nil, "", []byte{}, err + } + + keyID, err := cosign.GetTransparencyLogID(pubKey) + if err != nil { + return nil, "", []byte{}, err + } + + idBytes, err := hex.DecodeString(keyID) + if err != nil { + return nil, "", []byte{}, err + } + + return &pubKey, keyID, idBytes, 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..4db62ba73e4 --- /dev/null +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -0,0 +1,133 @@ +// +// 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 TestCreateCmd(t *testing.T) { + ctx := context.Background() + + // Make some certificate chains + td := t.TempDir() + + fulcioChainPath := filepath.Join(td, "fulcio.pem") + makeChain(t, fulcioChainPath, 2) + + tsaChainPath := filepath.Join(td, "timestamp.pem") + makeChain(t, tsaChainPath, 3) + + outPath := filepath.Join(td, "trustedroot.json") + + trustedrootCreate := CreateCmd{ + CertChain: []string{fulcioChainPath}, + Out: outPath, + TSACertChainPath: []string{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) + + defer fd.Close() + + 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) + + // Ensure we handle unexpected content at the end of the PEM file + _, err = fd.Write([]byte("asdf\n")) + checkErr(t, err) +} + +func checkErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} diff --git a/doc/cosign.md b/doc/cosign.md index d7f90aae469..bb2e39b15d7 100644 --- a/doc/cosign.md +++ b/doc/cosign.md @@ -37,6 +37,7 @@ A tool for Container Signing, Verification and Storage in an OCI registry. * [cosign sign-blob](cosign_sign-blob.md) - Sign the supplied blob, outputting the base64-encoded signature to stdout. * [cosign tree](cosign_tree.md) - Display supply chain security related artifacts for an image such as signatures, SBOMs and attestations * [cosign triangulate](cosign_triangulate.md) - Outputs the located cosign image reference. This is the location where cosign stores the specified artifact type. +* [cosign trusted-root](cosign_trusted-root.md) - Interact with a Sigstore protobuf trusted root * [cosign upload](cosign_upload.md) - Provides utilities for uploading artifacts to a registry * [cosign verify](cosign_verify.md) - Verify a signature on the supplied container image * [cosign verify-attestation](cosign_verify-attestation.md) - Verify an attestation on the supplied container image diff --git a/doc/cosign_trusted-root.md b/doc/cosign_trusted-root.md new file mode 100644 index 00000000000..eb2dc15dfb9 --- /dev/null +++ b/doc/cosign_trusted-root.md @@ -0,0 +1,27 @@ +## cosign trusted-root + +Interact with a Sigstore protobuf trusted root + +### Synopsis + +Tools for interacting with a Sigstore protobuf trusted root + +### Options + +``` + -h, --help help for trusted-root +``` + +### Options inherited from parent commands + +``` + --output-file string log output to a file + -t, --timeout duration timeout for commands (default 3m0s) + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign](cosign.md) - A tool for Container Signing, Verification and Storage in an OCI registry. +* [cosign trusted-root create](cosign_trusted-root_create.md) - Create a Sigstore protobuf trusted root + diff --git a/doc/cosign_trusted-root_create.md b/doc/cosign_trusted-root_create.md new file mode 100644 index 00000000000..486aa8a8a44 --- /dev/null +++ b/doc/cosign_trusted-root_create.md @@ -0,0 +1,37 @@ +## cosign trusted-root create + +Create a Sigstore protobuf trusted root + +### Synopsis + +Create a Sigstore protobuf trusted root by supplying verification material + +``` +cosign trusted-root create [flags] +``` + +### Options + +``` + --certificate-chain stringArray 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. + --ctfe-key stringArray path to a PEM-encoded public key used by certificate authority for certificate transparency log. + --ctfe-start-time stringArray RFC 3339 string describing validity start time for key use by certificate transparency log. + -h, --help help for create + --out string path to output trusted root + --rekor-key stringArray path to a PEM-encoded public key used by transparency log like Rekor. + --rekor-start-time stringArray RFC 3339 string describing validity start time for key use by transparency log like Rekor. + --timestamp-certificate-chain stringArray path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates +``` + +### Options inherited from parent commands + +``` + --output-file string log output to a file + -t, --timeout duration timeout for commands (default 3m0s) + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign trusted-root](cosign_trusted-root.md) - Interact with a Sigstore protobuf trusted root +