Skip to content

Commit 93f7134

Browse files
committed
Fixes sigstore#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.
1 parent 8defb0e commit 93f7134

File tree

7 files changed

+458
-6
lines changed

7 files changed

+458
-6
lines changed

cmd/cosign/cli/commands.go

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func New() *cobra.Command {
120120
cmd.AddCommand(VerifyBlob())
121121
cmd.AddCommand(VerifyBlobAttestation())
122122
cmd.AddCommand(Triangulate())
123+
cmd.AddCommand(TrustedRoot())
123124
cmd.AddCommand(Env())
124125
cmd.AddCommand(version.WithFont("starwars"))
125126

cmd/cosign/cli/options/trustedroot.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Copyright 2024 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package options
17+
18+
import (
19+
"github.com/spf13/cobra"
20+
)
21+
22+
type TrustedRootCreateOptions struct {
23+
CAIntermediates string
24+
CARoots string
25+
CertChain string
26+
Out string
27+
RekorURL string
28+
TSACertChainPath string
29+
}
30+
31+
var _ Interface = (*TrustedRootCreateOptions)(nil)
32+
33+
func (o *TrustedRootCreateOptions) AddFlags(cmd *cobra.Command) {
34+
cmd.Flags().StringVar(&o.CAIntermediates, "ca-intermediates", "",
35+
"path to a file of intermediate CA certificates in PEM format which will be needed "+
36+
"when building the certificate chains for the signing certificate. "+
37+
"The flag is optional and must be used together with --ca-roots, conflicts with "+
38+
"--certificate-chain.")
39+
_ = cmd.Flags().SetAnnotation("ca-intermediates", cobra.BashCompFilenameExt, []string{"cert"})
40+
41+
cmd.Flags().StringVar(&o.CARoots, "ca-roots", "",
42+
"path to a bundle file of CA certificates in PEM format which will be needed "+
43+
"when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.")
44+
_ = cmd.Flags().SetAnnotation("ca-roots", cobra.BashCompFilenameExt, []string{"cert"})
45+
46+
cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "",
47+
"path to a list of CA certificates in PEM format which will be needed "+
48+
"when building the certificate chain for the signing certificate. "+
49+
"Must start with the parent intermediate CA certificate of the "+
50+
"signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates.")
51+
_ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"})
52+
53+
cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain")
54+
cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain")
55+
56+
cmd.Flags().StringVar(&o.Out, "out", "",
57+
"path to output trusted root")
58+
59+
cmd.Flags().StringVar(&o.RekorURL, "rekor-url", "",
60+
"address of rekor STL server")
61+
62+
cmd.Flags().StringVar(&o.TSACertChainPath, "timestamp-certificate-chain", "",
63+
"path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. "+
64+
"Optionally may contain intermediate CA certificates")
65+
}

cmd/cosign/cli/trustedroot.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Copyright 2024 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cli
17+
18+
import (
19+
"context"
20+
21+
"github.com/spf13/cobra"
22+
23+
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
24+
"github.com/sigstore/cosign/v2/cmd/cosign/cli/trustedroot"
25+
)
26+
27+
func TrustedRoot() *cobra.Command {
28+
cmd := &cobra.Command{
29+
Use: "trusted-root",
30+
Short: "Interact with a Sigstore protobuf trusted root",
31+
Long: "Tools for interacting with a Sigstore protobuf trusted root",
32+
}
33+
34+
cmd.AddCommand(trustedRootCreate())
35+
36+
return cmd
37+
}
38+
39+
func trustedRootCreate() *cobra.Command {
40+
o := &options.TrustedRootCreateOptions{}
41+
42+
cmd := &cobra.Command{
43+
Use: "create",
44+
Short: "Create a Sigstore protobuf trusted root",
45+
Long: "Create a Sigstore protobuf trusted root by supplying verification material",
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
trCreateCmd := &trustedroot.TrustedRootCreateCmd{
48+
CAIntermediates: o.CAIntermediates,
49+
CARoots: o.CARoots,
50+
CertChain: o.CertChain,
51+
Out: o.Out,
52+
RekorURL: o.RekorURL,
53+
TSACertChainPath: o.TSACertChainPath,
54+
}
55+
56+
ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout)
57+
defer cancel()
58+
59+
return trCreateCmd.Exec(ctx)
60+
},
61+
}
62+
63+
o.AddFlags(cmd)
64+
return cmd
65+
}
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//
2+
// Copyright 2024 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package trustedroot
17+
18+
import (
19+
"context"
20+
"crypto"
21+
"crypto/sha256"
22+
"crypto/x509"
23+
"encoding/base64"
24+
"encoding/pem"
25+
"errors"
26+
"fmt"
27+
"os"
28+
29+
"github.com/sigstore/sigstore-go/pkg/root"
30+
31+
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
32+
"github.com/sigstore/cosign/v2/internal/ui"
33+
)
34+
35+
type TrustedRootCreateCmd struct {
36+
CAIntermediates string
37+
CARoots string
38+
CertChain string
39+
Out string
40+
RekorURL string
41+
TSACertChainPath string
42+
}
43+
44+
func (c *TrustedRootCreateCmd) Exec(ctx context.Context) error {
45+
var fulcioCertAuthorities []root.CertificateAuthority
46+
var timestampAuthorities []root.CertificateAuthority
47+
rekorTransparencyLogs := make(map[string]*root.TransparencyLog)
48+
49+
if c.CertChain != "" {
50+
fulcioAuthority, err := parsePEMFile(c.CertChain)
51+
if err != nil {
52+
return err
53+
}
54+
fulcioCertAuthorities = append(fulcioCertAuthorities, *fulcioAuthority)
55+
56+
} else if c.CARoots != "" {
57+
roots, err := parseCerts(c.CARoots)
58+
if err != nil {
59+
return err
60+
}
61+
62+
var intermediates []*x509.Certificate
63+
if c.CAIntermediates != "" {
64+
intermediates, err = parseCerts(c.CAIntermediates)
65+
if err != nil {
66+
return err
67+
}
68+
}
69+
70+
// Here we're trying to "flatten" the x509.CertPool cosign was using
71+
// into a trusted root with a clear mapping between roots and
72+
// intermediates. Make a guess that if there are intermediates, there
73+
// is one per root.
74+
75+
for i, rootCert := range roots {
76+
var fulcioAuthority root.CertificateAuthority
77+
fulcioAuthority.Root = rootCert
78+
if i < len(intermediates) {
79+
fulcioAuthority.Intermediates = []*x509.Certificate{intermediates[i]}
80+
}
81+
fulcioCertAuthorities = append(fulcioCertAuthorities, fulcioAuthority)
82+
}
83+
}
84+
85+
if c.RekorURL != "" {
86+
rekorClient, err := rekor.NewClient(c.RekorURL)
87+
if err != nil {
88+
return fmt.Errorf("creating Rekor client: %w", err)
89+
}
90+
91+
rekorPubKey, err := rekorClient.Pubkey.GetPublicKey(nil)
92+
if err != nil {
93+
return err
94+
}
95+
96+
block, _ := pem.Decode([]byte(rekorPubKey.Payload))
97+
if block == nil {
98+
return errors.New("failed to decode public key of server")
99+
}
100+
101+
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
102+
if err != nil {
103+
return err
104+
}
105+
106+
keyHash := sha256.Sum256(block.Bytes)
107+
keyID := base64.StdEncoding.EncodeToString(keyHash[:])
108+
109+
rekorTransparencyLog := root.TransparencyLog{
110+
BaseURL: c.RekorURL,
111+
HashFunc: crypto.Hash(crypto.SHA256),
112+
ID: keyHash[:],
113+
PublicKey: pub,
114+
SignatureHashFunc: crypto.Hash(crypto.SHA256),
115+
}
116+
117+
rekorTransparencyLogs[keyID] = &rekorTransparencyLog
118+
}
119+
120+
if c.TSACertChainPath != "" {
121+
timestampAuthority, err := parsePEMFile(c.TSACertChainPath)
122+
if err != nil {
123+
return err
124+
}
125+
timestampAuthorities = append(timestampAuthorities, *timestampAuthority)
126+
}
127+
128+
newTrustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01,
129+
fulcioCertAuthorities, nil, timestampAuthorities, rekorTransparencyLogs,
130+
)
131+
if err != nil {
132+
return err
133+
}
134+
135+
var trBytes []byte
136+
137+
trBytes, err = newTrustedRoot.MarshalJSON()
138+
if err != nil {
139+
return err
140+
}
141+
142+
if c.Out != "" {
143+
err = os.WriteFile(c.Out, trBytes, 0640)
144+
if err != nil {
145+
return err
146+
}
147+
} else {
148+
ui.Infof(ctx, string(trBytes))
149+
}
150+
151+
return nil
152+
}
153+
154+
func parsePEMFile(path string) (*root.CertificateAuthority, error) {
155+
certs, err := parseCerts(path)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
var ca root.CertificateAuthority
161+
ca.Root = certs[len(certs)-1]
162+
if len(certs) > 1 {
163+
ca.Intermediates = certs[:len(certs)-1]
164+
}
165+
166+
return &ca, nil
167+
}
168+
169+
func parseCerts(path string) ([]*x509.Certificate, error) {
170+
var certs []*x509.Certificate
171+
172+
contents, err := os.ReadFile(path)
173+
if err != nil {
174+
return nil, err
175+
}
176+
177+
for block, contents := pem.Decode(contents); ; block, contents = pem.Decode(contents) {
178+
cert, err := x509.ParseCertificate(block.Bytes)
179+
if err != nil {
180+
return nil, err
181+
}
182+
certs = append(certs, cert)
183+
184+
if len(contents) == 0 {
185+
break
186+
}
187+
}
188+
189+
if len(certs) == 0 {
190+
return nil, fmt.Errorf("No certificates in file %s", path)
191+
}
192+
193+
return certs, nil
194+
}

0 commit comments

Comments
 (0)