Skip to content

Commit

Permalink
Add trusted-root create helper command (#3876)
Browse files Browse the repository at this point in the history
* 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 <steiza@github.com>

* Linter fixes and docgen

Signed-off-by: Zach Steindler <steiza@github.com>

* Fix Windows unit test

Signed-off-by: Zach Steindler <steiza@github.com>

* Output via stdout instead of stderr

Signed-off-by: Zach Steindler <steiza@github.com>

* 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 <steiza@github.com>

* Replace `--rekor-url` with `--ignore-tlog`

Similar to `--ignore-sct`

Signed-off-by: Zach Steindler <steiza@github.com>

* Just use paths to files on disk

Instead of clients querying remote servers

Signed-off-by: Zach Steindler <steiza@github.com>

* Add the ability to supply multiple verification material

Also add ability to specify validity start time for keys

Signed-off-by: Zach Steindler <steiza@github.com>

* 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 <dsavints@gmail.com>
Signed-off-by: Zach Steindler <steiza@github.com>

* remove trailing newline

Signed-off-by: Zach Steindler <steiza@github.com>

* Simplify imports

Signed-off-by: Zach Steindler <steiza@github.com>

---------

Signed-off-by: Zach Steindler <steiza@github.com>
Co-authored-by: Dmitry S <dsavints@gmail.com>
  • Loading branch information
steiza and dmitris authored Oct 29, 2024
1 parent 3818a1d commit 9b17791
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 0 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
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")

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

0 comments on commit 9b17791

Please sign in to comment.