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 +