Skip to content

Commit

Permalink
Fixes sigstore#3700: add trusted-root create helper command
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
steiza committed Sep 11, 2024
1 parent 8defb0e commit 93f7134
Show file tree
Hide file tree
Showing 7 changed files with 458 additions and 6 deletions.
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", "",
"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")
}
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, 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
}
194 changes: 194 additions & 0 deletions cmd/cosign/cli/trustedroot/trustedroot.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 93f7134

Please sign in to comment.