Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trusted-root create helper command #3876

Merged
merged 11 commits into from
Oct 29, 2024
1 change: 1 addition & 0 deletions cmd/cosign/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
62 changes: 62 additions & 0 deletions cmd/cosign/cli/options/trustedroot.go
Original file line number Diff line number Diff line change
@@ -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")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing a way to provide keys for the CT log service, is that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of - I was trying to mirror the flags of commands like cosign verify-blob ... as much as possible, and I didn't see a flag for specifying the public key of the CT log. If there is one, and I missed it, I'm happy to add it!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is overridden via an environment variable

VariableSigstoreCTLogPublicKeyFile Variable = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE"
but by default it is fetched from the TUF targets. I don't think there is a flag to override it.

The CTFE key is used for signing, so unfortunately I don't think the flags for verify-blob are a good analog for what needs to be included in the trusted root.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little tricky because of the diversity of private deployments of Sigstore. I added a --ignore-sct so that if the private deployment is using long-lived keys instead of a private Fulcio we don't try to add CT Log keys to their trusted root; otherwise we call cosign.GetCTLogPubs() which will make use of the environment variable or TUF target.

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")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not uber-critical, but with the options here there's no way to provide a uri for either the CA or the TSA. Other tools like trtool allow the user to manually provide that information.

Copy link
Member Author

@steiza steiza Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm leaning towards keeping this tool more minimal and mirroring the flags of existing cosign verify* ... commands, but people are of course welcome to edit the output to add additional context (including setting URIs or more sane valid timestamps!), or use more complete tooling like trtool.

66 changes: 66 additions & 0 deletions cmd/cosign/cli/trustedroot.go
Original file line number Diff line number Diff line change
@@ -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
}
205 changes: 205 additions & 0 deletions cmd/cosign/cli/trustedroot/trustedroot.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading