From 25523716c8eecc5d72131d2cec386a4c98d1a111 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 11 Sep 2024 09:00:48 -0400 Subject: [PATCH 01/11] 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= From ea1c77f3294cb508bf9d5f7459568471a4390319 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 11 Sep 2024 09:45:25 -0400 Subject: [PATCH 02/11] Linter fixes and docgen Signed-off-by: Zach Steindler --- cmd/cosign/cli/trustedroot.go | 4 +-- cmd/cosign/cli/trustedroot/trustedroot.go | 13 ++++--- .../cli/trustedroot/trustedroot_test.go | 4 +-- doc/cosign.md | 1 + doc/cosign_trusted-root.md | 27 ++++++++++++++ doc/cosign_trusted-root_create.md | 36 +++++++++++++++++++ 6 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 doc/cosign_trusted-root.md create mode 100644 doc/cosign_trusted-root_create.md diff --git a/cmd/cosign/cli/trustedroot.go b/cmd/cosign/cli/trustedroot.go index 24d271e3e33..fb10c7ad555 100644 --- a/cmd/cosign/cli/trustedroot.go +++ b/cmd/cosign/cli/trustedroot.go @@ -43,8 +43,8 @@ func trustedRootCreate() *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{ + RunE: func(cmd *cobra.Command, _ []string) error { + trCreateCmd := &trustedroot.CreateCmd{ CAIntermediates: o.CAIntermediates, CARoots: o.CARoots, CertChain: o.CertChain, diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index 125ed4f06c8..5cc6ad99818 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -32,7 +32,7 @@ import ( "github.com/sigstore/cosign/v2/internal/ui" ) -type TrustedRootCreateCmd struct { +type CreateCmd struct { CAIntermediates string CARoots string CertChain string @@ -41,7 +41,7 @@ type TrustedRootCreateCmd struct { TSACertChainPath string } -func (c *TrustedRootCreateCmd) Exec(ctx context.Context) error { +func (c *CreateCmd) Exec(ctx context.Context) error { var fulcioCertAuthorities []root.CertificateAuthority var timestampAuthorities []root.CertificateAuthority rekorTransparencyLogs := make(map[string]*root.TransparencyLog) @@ -52,7 +52,6 @@ func (c *TrustedRootCreateCmd) Exec(ctx context.Context) error { return err } fulcioCertAuthorities = append(fulcioCertAuthorities, *fulcioAuthority) - } else if c.CARoots != "" { roots, err := parseCerts(c.CARoots) if err != nil { @@ -108,10 +107,10 @@ func (c *TrustedRootCreateCmd) Exec(ctx context.Context) error { rekorTransparencyLog := root.TransparencyLog{ BaseURL: c.RekorURL, - HashFunc: crypto.Hash(crypto.SHA256), + HashFunc: crypto.SHA256, ID: keyHash[:], PublicKey: pub, - SignatureHashFunc: crypto.Hash(crypto.SHA256), + SignatureHashFunc: crypto.SHA256, } rekorTransparencyLogs[keyID] = &rekorTransparencyLog @@ -140,7 +139,7 @@ func (c *TrustedRootCreateCmd) Exec(ctx context.Context) error { } if c.Out != "" { - err = os.WriteFile(c.Out, trBytes, 0640) + err = os.WriteFile(c.Out, trBytes, 0600) if err != nil { return err } @@ -187,7 +186,7 @@ func parseCerts(path string) ([]*x509.Certificate, error) { } if len(certs) == 0 { - return nil, fmt.Errorf("No certificates in file %s", path) + 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 index 582b6cf7f29..3b2f4878af6 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -29,7 +29,7 @@ import ( "github.com/sigstore/sigstore-go/pkg/root" ) -func TestTrustedRootCreate(t *testing.T) { +func TestCreateCmd(t *testing.T) { ctx := context.Background() // Make some certificate chains @@ -43,7 +43,7 @@ func TestTrustedRootCreate(t *testing.T) { outPath := filepath.Join(td, "trustedroot.json") - trustedrootCreate := TrustedRootCreateCmd{ + trustedrootCreate := CreateCmd{ CertChain: fulcioChainPath, Out: outPath, TSACertChainPath: tsaChainPath, 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..8c7e94bba37 --- /dev/null +++ b/doc/cosign_trusted-root_create.md @@ -0,0 +1,36 @@ +## 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 + +``` + --ca-intermediates string 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. + --ca-roots string 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. + --certificate-chain string 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. + -h, --help help for create + --out string path to output trusted root + --rekor-url string address of rekor STL server + --timestamp-certificate-chain string 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 + From a53ec25587fb342dcd94b8581fd10a92cd4493d6 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 11 Sep 2024 10:41:05 -0400 Subject: [PATCH 03/11] Fix Windows unit test Signed-off-by: Zach Steindler --- cmd/cosign/cli/trustedroot/trustedroot_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/cosign/cli/trustedroot/trustedroot_test.go b/cmd/cosign/cli/trustedroot/trustedroot_test.go index 3b2f4878af6..b5e56f9225e 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -79,6 +79,8 @@ 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, From e0041bb7463a2a9daf2eab4520cee9ad49d7f290 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 12 Sep 2024 11:42:30 -0400 Subject: [PATCH 04/11] Output via stdout instead of stderr Signed-off-by: Zach Steindler --- cmd/cosign/cli/trustedroot/trustedroot.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index 5cc6ad99818..e254f506ea6 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -29,7 +29,6 @@ import ( "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v2/internal/ui" ) type CreateCmd struct { @@ -41,7 +40,7 @@ type CreateCmd struct { TSACertChainPath string } -func (c *CreateCmd) Exec(ctx context.Context) error { +func (c *CreateCmd) Exec(_ context.Context) error { var fulcioCertAuthorities []root.CertificateAuthority var timestampAuthorities []root.CertificateAuthority rekorTransparencyLogs := make(map[string]*root.TransparencyLog) @@ -144,7 +143,7 @@ func (c *CreateCmd) Exec(ctx context.Context) error { return err } } else { - ui.Infof(ctx, string(trBytes)) + fmt.Println(string(trBytes)) } return nil From 06284e27f45bdda614dc20ad0c3db095477b2ecc Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 12 Sep 2024 14:45:11 -0400 Subject: [PATCH 05/11] 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 --- cmd/cosign/cli/options/trustedroot.go | 5 ++++ cmd/cosign/cli/trustedroot.go | 1 + cmd/cosign/cli/trustedroot/trustedroot.go | 29 +++++++++++++++++-- .../cli/trustedroot/trustedroot_test.go | 1 + doc/cosign_trusted-root_create.md | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/cmd/cosign/cli/options/trustedroot.go b/cmd/cosign/cli/options/trustedroot.go index 89f3810a409..70661a3f22b 100644 --- a/cmd/cosign/cli/options/trustedroot.go +++ b/cmd/cosign/cli/options/trustedroot.go @@ -23,6 +23,7 @@ type TrustedRootCreateOptions struct { CAIntermediates string CARoots string CertChain string + IgnoreSCT bool Out string RekorURL string TSACertChainPath string @@ -53,6 +54,10 @@ func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) { cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain") cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain") + cmd.Flags().BoolVar(&o.IgnoreSCT, "ignore-sct", false, + "when set, do not include key for verifying certificate transparency "+ + "log. Set this if you signed with a key instead of using Fulcio.") + cmd.Flags().StringVar(&o.Out, "out", "", "path to output trusted root") diff --git a/cmd/cosign/cli/trustedroot.go b/cmd/cosign/cli/trustedroot.go index fb10c7ad555..c0e9e905258 100644 --- a/cmd/cosign/cli/trustedroot.go +++ b/cmd/cosign/cli/trustedroot.go @@ -48,6 +48,7 @@ func trustedRootCreate() *cobra.Command { CAIntermediates: o.CAIntermediates, CARoots: o.CARoots, CertChain: o.CertChain, + IgnoreSCT: o.IgnoreSCT, Out: o.Out, RekorURL: o.RekorURL, TSACertChainPath: o.TSACertChainPath, diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index e254f506ea6..f9df2d2f349 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/pem" "errors" "fmt" @@ -29,19 +30,22 @@ import ( "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v2/pkg/cosign" ) type CreateCmd struct { CAIntermediates string CARoots string CertChain string + IgnoreSCT bool Out string RekorURL string TSACertChainPath string } -func (c *CreateCmd) Exec(_ context.Context) error { +func (c *CreateCmd) Exec(ctx context.Context) error { var fulcioCertAuthorities []root.CertificateAuthority + ctLogs := make(map[string]*root.TransparencyLog) var timestampAuthorities []root.CertificateAuthority rekorTransparencyLogs := make(map[string]*root.TransparencyLog) @@ -80,6 +84,26 @@ func (c *CreateCmd) Exec(_ context.Context) error { } } + if !c.IgnoreSCT { + ctLogPubKeys, err := cosign.GetCTLogPubs(ctx) + if err != nil { + return err + } + + for id, key := range ctLogPubKeys.Keys { + idBytes, err := hex.DecodeString(id) + if err != nil { + return err + } + ctLogs[id] = &root.TransparencyLog{ + ID: idBytes, + HashFunc: crypto.SHA256, + PublicKey: key.PubKey, + SignatureHashFunc: crypto.SHA256, + } + } + } + if c.RekorURL != "" { rekorClient, err := rekor.NewClient(c.RekorURL) if err != nil { @@ -124,7 +148,8 @@ func (c *CreateCmd) Exec(_ context.Context) error { } newTrustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, - fulcioCertAuthorities, nil, timestampAuthorities, rekorTransparencyLogs, + fulcioCertAuthorities, ctLogs, timestampAuthorities, + rekorTransparencyLogs, ) if err != nil { return err diff --git a/cmd/cosign/cli/trustedroot/trustedroot_test.go b/cmd/cosign/cli/trustedroot/trustedroot_test.go index b5e56f9225e..236d81a6afd 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -45,6 +45,7 @@ func TestCreateCmd(t *testing.T) { trustedrootCreate := CreateCmd{ CertChain: fulcioChainPath, + IgnoreSCT: true, Out: outPath, TSACertChainPath: tsaChainPath, } diff --git a/doc/cosign_trusted-root_create.md b/doc/cosign_trusted-root_create.md index 8c7e94bba37..c6aca66c159 100644 --- a/doc/cosign_trusted-root_create.md +++ b/doc/cosign_trusted-root_create.md @@ -17,6 +17,7 @@ cosign trusted-root create [flags] --ca-roots string 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. --certificate-chain string 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. -h, --help help for create + --ignore-sct when set, do not include key for verifying certificate transparency log. Set this if you signed with a key instead of using Fulcio. --out string path to output trusted root --rekor-url string address of rekor STL server --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates From b3262d7f395794c44bc6472d09ec4ea40574bf9b Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 12 Sep 2024 15:38:56 -0400 Subject: [PATCH 06/11] Replace `--rekor-url` with `--ignore-tlog` Similar to `--ignore-sct` Signed-off-by: Zach Steindler --- cmd/cosign/cli/options/trustedroot.go | 9 +++-- cmd/cosign/cli/trustedroot.go | 2 +- cmd/cosign/cli/trustedroot/trustedroot.go | 49 +++++++---------------- doc/cosign_trusted-root_create.md | 2 +- 4 files changed, 22 insertions(+), 40 deletions(-) diff --git a/cmd/cosign/cli/options/trustedroot.go b/cmd/cosign/cli/options/trustedroot.go index 70661a3f22b..a05805ae884 100644 --- a/cmd/cosign/cli/options/trustedroot.go +++ b/cmd/cosign/cli/options/trustedroot.go @@ -24,8 +24,8 @@ type TrustedRootCreateOptions struct { CARoots string CertChain string IgnoreSCT bool + IgnoreTlog bool Out string - RekorURL string TSACertChainPath string } @@ -58,12 +58,13 @@ func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) { "when set, do not include key for verifying certificate transparency "+ "log. Set this if you signed with a key instead of using Fulcio.") + cmd.Flags().BoolVar(&o.IgnoreTlog, "ignore-tlog", false, + "when set, do not include key for verifying transparency. Set this if "+ + "you did not sign with Rekor.") + 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 index c0e9e905258..374451f8a22 100644 --- a/cmd/cosign/cli/trustedroot.go +++ b/cmd/cosign/cli/trustedroot.go @@ -49,8 +49,8 @@ func trustedRootCreate() *cobra.Command { CARoots: o.CARoots, CertChain: o.CertChain, IgnoreSCT: o.IgnoreSCT, + IgnoreTlog: o.IgnoreTlog, Out: o.Out, - RekorURL: o.RekorURL, TSACertChainPath: o.TSACertChainPath, } diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index f9df2d2f349..a0b56557a7d 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -18,18 +18,14 @@ package trustedroot import ( "context" "crypto" - "crypto/sha256" "crypto/x509" - "encoding/base64" "encoding/hex" "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/pkg/cosign" ) @@ -38,8 +34,8 @@ type CreateCmd struct { CARoots string CertChain string IgnoreSCT bool + IgnoreTlog bool Out string - RekorURL string TSACertChainPath string } @@ -96,47 +92,32 @@ func (c *CreateCmd) Exec(ctx context.Context) error { return err } ctLogs[id] = &root.TransparencyLog{ - ID: idBytes, HashFunc: crypto.SHA256, + ID: idBytes, PublicKey: key.PubKey, SignatureHashFunc: crypto.SHA256, } } } - 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 !c.IgnoreTlog { + tlogPubKeys, err := cosign.GetRekorPubs(ctx) if err != nil { return err } - keyHash := sha256.Sum256(block.Bytes) - keyID := base64.StdEncoding.EncodeToString(keyHash[:]) - - rekorTransparencyLog := root.TransparencyLog{ - BaseURL: c.RekorURL, - HashFunc: crypto.SHA256, - ID: keyHash[:], - PublicKey: pub, - SignatureHashFunc: crypto.SHA256, + for id, key := range tlogPubKeys.Keys { + idBytes, err := hex.DecodeString(id) + if err != nil { + return err + } + rekorTransparencyLogs[id] = &root.TransparencyLog{ + HashFunc: crypto.SHA256, + ID: idBytes, + PublicKey: key.PubKey, + SignatureHashFunc: crypto.SHA256, + } } - - rekorTransparencyLogs[keyID] = &rekorTransparencyLog } if c.TSACertChainPath != "" { diff --git a/doc/cosign_trusted-root_create.md b/doc/cosign_trusted-root_create.md index c6aca66c159..bc052813961 100644 --- a/doc/cosign_trusted-root_create.md +++ b/doc/cosign_trusted-root_create.md @@ -18,8 +18,8 @@ cosign trusted-root create [flags] --certificate-chain string 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. -h, --help help for create --ignore-sct when set, do not include key for verifying certificate transparency log. Set this if you signed with a key instead of using Fulcio. + --ignore-tlog when set, do not include key for verifying transparency. Set this if you did not sign with Rekor. --out string path to output trusted root - --rekor-url string address of rekor STL server --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates ``` From cab9148d18c64c16cf58b8c10ee3bb5f2c8a8173 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 26 Sep 2024 16:14:46 -0400 Subject: [PATCH 07/11] Just use paths to files on disk Instead of clients querying remote servers Signed-off-by: Zach Steindler --- cmd/cosign/cli/options/trustedroot.go | 15 ++-- cmd/cosign/cli/trustedroot.go | 4 +- cmd/cosign/cli/trustedroot/trustedroot.go | 71 +++++++++++-------- .../cli/trustedroot/trustedroot_test.go | 1 - doc/cosign_trusted-root_create.md | 4 +- 5 files changed, 53 insertions(+), 42 deletions(-) diff --git a/cmd/cosign/cli/options/trustedroot.go b/cmd/cosign/cli/options/trustedroot.go index a05805ae884..f400c22870d 100644 --- a/cmd/cosign/cli/options/trustedroot.go +++ b/cmd/cosign/cli/options/trustedroot.go @@ -23,8 +23,8 @@ type TrustedRootCreateOptions struct { CAIntermediates string CARoots string CertChain string - IgnoreSCT bool - IgnoreTlog bool + CtfeKeyPath string + RekorKeyPath string Out string TSACertChainPath string } @@ -54,13 +54,12 @@ func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) { cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain") cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain") - cmd.Flags().BoolVar(&o.IgnoreSCT, "ignore-sct", false, - "when set, do not include key for verifying certificate transparency "+ - "log. Set this if you signed with a key instead of using Fulcio.") + cmd.Flags().StringVar(&o.CtfeKeyPath, "ctfe-key", "", + "path to a PEM-encoded public key used by certificate authority for "+ + "certificate transparency log.") - cmd.Flags().BoolVar(&o.IgnoreTlog, "ignore-tlog", false, - "when set, do not include key for verifying transparency. Set this if "+ - "you did not sign with Rekor.") + cmd.Flags().StringVar(&o.RekorKeyPath, "rekor-key", "", + "path to a PEM-encoded public key used by transparency log like Rekor.") cmd.Flags().StringVar(&o.Out, "out", "", "path to output trusted root") diff --git a/cmd/cosign/cli/trustedroot.go b/cmd/cosign/cli/trustedroot.go index 374451f8a22..3b3a2b326f1 100644 --- a/cmd/cosign/cli/trustedroot.go +++ b/cmd/cosign/cli/trustedroot.go @@ -48,9 +48,9 @@ func trustedRootCreate() *cobra.Command { CAIntermediates: o.CAIntermediates, CARoots: o.CARoots, CertChain: o.CertChain, - IgnoreSCT: o.IgnoreSCT, - IgnoreTlog: o.IgnoreTlog, + CtfeKeyPath: o.CtfeKeyPath, Out: o.Out, + RekorKeyPath: o.RekorKeyPath, TSACertChainPath: o.TSACertChainPath, } diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index a0b56557a7d..78ef216a9e4 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -25,6 +25,7 @@ import ( "os" "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/cosign/v2/pkg/cosign" ) @@ -33,13 +34,13 @@ type CreateCmd struct { CAIntermediates string CARoots string CertChain string - IgnoreSCT bool - IgnoreTlog bool + CtfeKeyPath string Out string + RekorKeyPath string TSACertChainPath string } -func (c *CreateCmd) Exec(ctx context.Context) error { +func (c *CreateCmd) Exec(_ context.Context) error { var fulcioCertAuthorities []root.CertificateAuthority ctLogs := make(map[string]*root.TransparencyLog) var timestampAuthorities []root.CertificateAuthority @@ -80,43 +81,31 @@ func (c *CreateCmd) Exec(ctx context.Context) error { } } - if !c.IgnoreSCT { - ctLogPubKeys, err := cosign.GetCTLogPubs(ctx) + if c.CtfeKeyPath != "" { + ctLogPubKey, id, idBytes, err := getPubKey(c.CtfeKeyPath) if err != nil { return err } - for id, key := range ctLogPubKeys.Keys { - idBytes, err := hex.DecodeString(id) - if err != nil { - return err - } - ctLogs[id] = &root.TransparencyLog{ - HashFunc: crypto.SHA256, - ID: idBytes, - PublicKey: key.PubKey, - SignatureHashFunc: crypto.SHA256, - } + ctLogs[id] = &root.TransparencyLog{ + HashFunc: crypto.SHA256, + ID: idBytes, + PublicKey: *ctLogPubKey, + SignatureHashFunc: crypto.SHA256, } } - if !c.IgnoreTlog { - tlogPubKeys, err := cosign.GetRekorPubs(ctx) + if c.RekorKeyPath != "" { + tlogPubKey, id, idBytes, err := getPubKey(c.RekorKeyPath) if err != nil { return err } - for id, key := range tlogPubKeys.Keys { - idBytes, err := hex.DecodeString(id) - if err != nil { - return err - } - rekorTransparencyLogs[id] = &root.TransparencyLog{ - HashFunc: crypto.SHA256, - ID: idBytes, - PublicKey: key.PubKey, - SignatureHashFunc: crypto.SHA256, - } + rekorTransparencyLogs[id] = &root.TransparencyLog{ + HashFunc: crypto.SHA256, + ID: idBytes, + PublicKey: *tlogPubKey, + SignatureHashFunc: crypto.SHA256, } } @@ -196,3 +185,27 @@ func parseCerts(path string) ([]*x509.Certificate, error) { 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 index 236d81a6afd..b5e56f9225e 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -45,7 +45,6 @@ func TestCreateCmd(t *testing.T) { trustedrootCreate := CreateCmd{ CertChain: fulcioChainPath, - IgnoreSCT: true, Out: outPath, TSACertChainPath: tsaChainPath, } diff --git a/doc/cosign_trusted-root_create.md b/doc/cosign_trusted-root_create.md index bc052813961..5e7ea26d34b 100644 --- a/doc/cosign_trusted-root_create.md +++ b/doc/cosign_trusted-root_create.md @@ -16,10 +16,10 @@ cosign trusted-root create [flags] --ca-intermediates string 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. --ca-roots string 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. --certificate-chain string 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. + --ctfe-key string path to a PEM-encoded public key used by certificate authority for certificate transparency log. -h, --help help for create - --ignore-sct when set, do not include key for verifying certificate transparency log. Set this if you signed with a key instead of using Fulcio. - --ignore-tlog when set, do not include key for verifying transparency. Set this if you did not sign with Rekor. --out string path to output trusted root + --rekor-key string path to a PEM-encoded public key used by transparency log like Rekor. --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates ``` From f70583628f5dda2da8a33a53898f6de794efdd37 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Tue, 8 Oct 2024 10:22:55 -0400 Subject: [PATCH 08/11] Add the ability to supply multiple verification material Also add ability to specify validity start time for keys Signed-off-by: Zach Steindler --- cmd/cosign/cli/options/trustedroot.go | 46 +++++----- cmd/cosign/cli/trustedroot.go | 4 +- cmd/cosign/cli/trustedroot/trustedroot.go | 83 +++++++++---------- .../cli/trustedroot/trustedroot_test.go | 4 +- doc/cosign_trusted-root_create.md | 16 ++-- 5 files changed, 70 insertions(+), 83 deletions(-) diff --git a/cmd/cosign/cli/options/trustedroot.go b/cmd/cosign/cli/options/trustedroot.go index f400c22870d..66497690ae7 100644 --- a/cmd/cosign/cli/options/trustedroot.go +++ b/cmd/cosign/cli/options/trustedroot.go @@ -20,51 +20,43 @@ import ( ) type TrustedRootCreateOptions struct { - CAIntermediates string - CARoots string - CertChain string - CtfeKeyPath string - RekorKeyPath string + CertChain []string + CtfeKeyPath []string + CtfeStartTime []string Out string - TSACertChainPath string + RekorKeyPath []string + RekorStartTime []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", "", + 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. 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.CtfeKeyPath, "ctfe-key", "", + 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().StringVar(&o.RekorKeyPath, "rekor-key", "", + 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().StringVar(&o.Out, "out", "", - "path to output trusted root") + 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().StringVar(&o.TSACertChainPath, "timestamp-certificate-chain", "", + 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 index 3b3a2b326f1..5ea67fc33e1 100644 --- a/cmd/cosign/cli/trustedroot.go +++ b/cmd/cosign/cli/trustedroot.go @@ -45,12 +45,12 @@ func trustedRootCreate() *cobra.Command { Long: "Create a Sigstore protobuf trusted root by supplying verification material", RunE: func(cmd *cobra.Command, _ []string) error { trCreateCmd := &trustedroot.CreateCmd{ - CAIntermediates: o.CAIntermediates, - CARoots: o.CARoots, CertChain: o.CertChain, CtfeKeyPath: o.CtfeKeyPath, + CtfeStartTime: o.CtfeStartTime, Out: o.Out, RekorKeyPath: o.RekorKeyPath, + RekorStartTime: o.RekorStartTime, TSACertChainPath: o.TSACertChainPath, } diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index 78ef216a9e4..ee2faef41ac 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -23,6 +23,7 @@ import ( "encoding/pem" "fmt" "os" + "time" "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -31,13 +32,13 @@ import ( ) type CreateCmd struct { - CAIntermediates string - CARoots string - CertChain string - CtfeKeyPath string + CertChain []string + CtfeKeyPath []string + CtfeStartTime []string Out string - RekorKeyPath string - TSACertChainPath string + RekorKeyPath []string + RekorStartTime []string + TSACertChainPath []string } func (c *CreateCmd) Exec(_ context.Context) error { @@ -46,71 +47,64 @@ func (c *CreateCmd) Exec(_ context.Context) error { var timestampAuthorities []root.CertificateAuthority rekorTransparencyLogs := make(map[string]*root.TransparencyLog) - if c.CertChain != "" { - fulcioAuthority, err := parsePEMFile(c.CertChain) + for i := 0; i < len(c.CertChain); i++ { + fulcioAuthority, err := parsePEMFile(c.CertChain[i]) if err != nil { return err } fulcioCertAuthorities = append(fulcioCertAuthorities, *fulcioAuthority) - } else if c.CARoots != "" { - roots, err := parseCerts(c.CARoots) + } + + for i := 0; i < len(c.CtfeKeyPath); i++ { + ctLogPubKey, id, idBytes, err := getPubKey(c.CtfeKeyPath[i]) if err != nil { return err } - var intermediates []*x509.Certificate - if c.CAIntermediates != "" { - intermediates, err = parseCerts(c.CAIntermediates) + startTime := time.Unix(0, 0) + + if i < len(c.CtfeStartTime) { + startTime, err = time.Parse(time.RFC3339, c.CtfeStartTime[i]) 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) + ctLogs[id] = &root.TransparencyLog{ + HashFunc: crypto.SHA256, + ID: idBytes, + ValidityPeriodStart: startTime, + PublicKey: *ctLogPubKey, + SignatureHashFunc: crypto.SHA256, } } - if c.CtfeKeyPath != "" { - ctLogPubKey, id, idBytes, err := getPubKey(c.CtfeKeyPath) + for i := 0; i < len(c.RekorKeyPath); i++ { + tlogPubKey, id, idBytes, err := getPubKey(c.RekorKeyPath[i]) if err != nil { return err } - ctLogs[id] = &root.TransparencyLog{ - HashFunc: crypto.SHA256, - ID: idBytes, - PublicKey: *ctLogPubKey, - SignatureHashFunc: crypto.SHA256, - } - } + startTime := time.Unix(0, 0) - if c.RekorKeyPath != "" { - tlogPubKey, id, idBytes, err := getPubKey(c.RekorKeyPath) - if err != nil { - return err + 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, - PublicKey: *tlogPubKey, - SignatureHashFunc: crypto.SHA256, + HashFunc: crypto.SHA256, + ID: idBytes, + ValidityPeriodStart: startTime, + PublicKey: *tlogPubKey, + SignatureHashFunc: crypto.SHA256, } } - if c.TSACertChainPath != "" { - timestampAuthority, err := parsePEMFile(c.TSACertChainPath) + for i := 0; i < len(c.TSACertChainPath); i++ { + timestampAuthority, err := parsePEMFile(c.TSACertChainPath[i]) if err != nil { return err } @@ -152,6 +146,7 @@ func parsePEMFile(path string) (*root.CertificateAuthority, error) { 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] } diff --git a/cmd/cosign/cli/trustedroot/trustedroot_test.go b/cmd/cosign/cli/trustedroot/trustedroot_test.go index b5e56f9225e..746edff639c 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -44,9 +44,9 @@ func TestCreateCmd(t *testing.T) { outPath := filepath.Join(td, "trustedroot.json") trustedrootCreate := CreateCmd{ - CertChain: fulcioChainPath, + CertChain: []string{fulcioChainPath}, Out: outPath, - TSACertChainPath: tsaChainPath, + TSACertChainPath: []string{tsaChainPath}, } err := trustedrootCreate.Exec(ctx) diff --git a/doc/cosign_trusted-root_create.md b/doc/cosign_trusted-root_create.md index 5e7ea26d34b..0d62614d187 100644 --- a/doc/cosign_trusted-root_create.md +++ b/doc/cosign_trusted-root_create.md @@ -13,14 +13,14 @@ cosign trusted-root create [flags] ### Options ``` - --ca-intermediates string 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. - --ca-roots string 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. - --certificate-chain string 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. - --ctfe-key string path to a PEM-encoded public key used by certificate authority for certificate transparency log. - -h, --help help for create - --out string path to output trusted root - --rekor-key string path to a PEM-encoded public key used by transparency log like Rekor. - --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates + --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. Conflicts with --ca-roots and --ca-intermediates. + --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 From b8d58d727fc944ab7b86a04c971e679e8c07da80 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 17 Oct 2024 16:00:03 -0400 Subject: [PATCH 09/11] 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 --- cmd/cosign/cli/options/trustedroot.go | 2 +- cmd/cosign/cli/trustedroot/trustedroot.go | 2 +- cmd/cosign/cli/trustedroot/trustedroot_test.go | 9 +++++++-- doc/cosign_trusted-root_create.md | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/cmd/cosign/cli/options/trustedroot.go b/cmd/cosign/cli/options/trustedroot.go index 66497690ae7..298d34d9c8a 100644 --- a/cmd/cosign/cli/options/trustedroot.go +++ b/cmd/cosign/cli/options/trustedroot.go @@ -36,7 +36,7 @@ func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) { "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.") + "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, diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index ee2faef41ac..305ba51facf 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -162,7 +162,7 @@ func parseCerts(path string) ([]*x509.Certificate, error) { return nil, err } - for block, contents := pem.Decode(contents); ; block, contents = pem.Decode(contents) { + 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 diff --git a/cmd/cosign/cli/trustedroot/trustedroot_test.go b/cmd/cosign/cli/trustedroot/trustedroot_test.go index 746edff639c..9e0f1b32cc0 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -35,10 +35,10 @@ func TestCreateCmd(t *testing.T) { // Make some certificate chains td := t.TempDir() - fulcioChainPath := filepath.Join(td, "fulcio.crt") + fulcioChainPath := filepath.Join(td, "fulcio.pem") makeChain(t, fulcioChainPath, 2) - tsaChainPath := filepath.Join(td, "timestamp.crt") + tsaChainPath := filepath.Join(td, "timestamp.pem") makeChain(t, tsaChainPath, 3) outPath := filepath.Join(td, "trustedroot.json") @@ -73,6 +73,7 @@ func TestCreateCmd(t *testing.T) { if len(timestampAuthorities[0].Intermediates) != 2 { t.Fatal("unexpected number of timestamp intermediate certificates") } + } func makeChain(t *testing.T, path string, size int) { @@ -120,6 +121,10 @@ func makeChain(t *testing.T, path string, size int) { } 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) { diff --git a/doc/cosign_trusted-root_create.md b/doc/cosign_trusted-root_create.md index 0d62614d187..486aa8a8a44 100644 --- a/doc/cosign_trusted-root_create.md +++ b/doc/cosign_trusted-root_create.md @@ -13,7 +13,7 @@ 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. Conflicts with --ca-roots and --ca-intermediates. + --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 From 1e7a436e2171e9880a7bec467e8a4483d7528f48 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 17 Oct 2024 16:08:01 -0400 Subject: [PATCH 10/11] remove trailing newline Signed-off-by: Zach Steindler --- cmd/cosign/cli/trustedroot/trustedroot_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/cosign/cli/trustedroot/trustedroot_test.go b/cmd/cosign/cli/trustedroot/trustedroot_test.go index 9e0f1b32cc0..4db62ba73e4 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot_test.go +++ b/cmd/cosign/cli/trustedroot/trustedroot_test.go @@ -73,7 +73,6 @@ func TestCreateCmd(t *testing.T) { if len(timestampAuthorities[0].Intermediates) != 2 { t.Fatal("unexpected number of timestamp intermediate certificates") } - } func makeChain(t *testing.T, path string, size int) { From 1bd2b080cf29224b1e5bb8f8996908bff5e021fc Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Fri, 18 Oct 2024 11:20:52 -0400 Subject: [PATCH 11/11] Simplify imports Signed-off-by: Zach Steindler --- cmd/cosign/cli/trustedroot/trustedroot.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/cosign/cli/trustedroot/trustedroot.go b/cmd/cosign/cli/trustedroot/trustedroot.go index 305ba51facf..9b6766897d4 100644 --- a/cmd/cosign/cli/trustedroot/trustedroot.go +++ b/cmd/cosign/cli/trustedroot/trustedroot.go @@ -25,10 +25,9 @@ import ( "os" "time" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" - - "github.com/sigstore/cosign/v2/pkg/cosign" ) type CreateCmd struct {