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
65 changes: 65 additions & 0 deletions cmd/cosign/cli/options/trustedroot.go
Original file line number Diff line number Diff line change
@@ -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", "",
Copy link
Contributor

Choose a reason for hiding this comment

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

How do cert pools work here? This seems incompatible with the trusted root, as you have to specify a chain.

I see below that you're flattening, assuming 1 intermediate per root and assuming these two lists are ordered. I think that's not always going to be accurate - i would assume a client would throw all their certs into a list without thinking about chain building.

I would recommend we drop these until we support pools in the trust root, and require that clients construct the chains themselves.

Copy link
Member Author

Choose a reason for hiding this comment

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

Initially we were trying to mirror other cosign flags, but we ended up moving away from that, so how about we specify a single root and a single intermediate file (that can have one or more intermediates)?

Again, this is in the spirit of "enough to get you started" and not a do-it-all utility.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd lean towards just removing these flags entirely and only have --certificate-chain as an option. if you specify a root and its intermediates separately, you still have to know their order - might as well just use the chain flag at that point.

Copy link
Member Author

Choose a reason for hiding this comment

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

Only supporting chains sounds good to me! I updated this pull request to reflect that.

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

Choose a reason for hiding this comment

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

Should all of these options be plural, so that we can provide multiple chains, multiple logs, and multiple TSAs?

Copy link
Member Author

Choose a reason for hiding this comment

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

At this point, you can probably anticipate my response : D but I think we should stick to the bare minimum required to get folks off the ground, and let them iterate on top of the basic trusted root we give them.

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel less strongly on this one, but I think pluralizing is a small enough change that if we did that now, we'd cover future use cases (e.g. trusting both an internal deployment and public deployment)

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 did end up being a pretty minor change! You can now use the flags multiple times to specify additional verification materials.

"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")

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().StringVar(&o.Out, "out", "",
"path to output trusted root")

cmd.Flags().StringVar(&o.RekorURL, "rekor-url", "",
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the rationale for using the rekor URL here instead of an on-disk artifact? How do we know we can trust the key being served at this URL?

Copy link
Member Author

Choose a reason for hiding this comment

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

Another great question, with a similar answer - as much as possible I'm trying to mirror the flags and behavior of the cosign verify* ... commands.

The idea being when we go to deprecate these flags from cosign verify* ... commands, you will generate the trusted root you need by calling cosign trusted-root create ... with the deprecated flags you were passing to cosign verify* ....

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the flags for verify* are an ideal analog for what needs to be included in the trusted root. The --rekor-url flag used for the verify command is used in an online API call to verify the transparency log inclusion, but the trusted keys for the transparency log are fetched separately using the TUF configuration. We couldn't deprecate --rekor-url, or if we do it would be in favor of offline verification.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough! I replaced --rekor-url with --ignore-tlog, with a similar implementation to --ignore-sct.

"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")
}
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.

65 changes: 65 additions & 0 deletions cmd/cosign/cli/trustedroot.go
Original file line number Diff line number Diff line change
@@ -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, _ []string) error {
trCreateCmd := &trustedroot.CreateCmd{
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
}
193 changes: 193 additions & 0 deletions cmd/cosign/cli/trustedroot/trustedroot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//
// 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 CreateCmd struct {
CAIntermediates string
CARoots string
CertChain string
Out string
RekorURL string
TSACertChainPath string
}

func (c *CreateCmd) 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.SHA256,
ID: keyHash[:],
PublicKey: pub,
SignatureHashFunc: 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, 0600)
if err != nil {
return err
}
} else {
ui.Infof(ctx, string(trBytes))
steiza marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
Loading
Loading