Skip to content

Commit 488ef8c

Browse files
Add signing-config create command (#4280)
This introduces a new command `cosign signing-config create` to generate a SigningConfig message. This will first be used by our testing infrastructure to create a SigningConfig, and then by users adopting the bundle format in Cosign v2 and eventually as a requirement for v3 when signing with private deployments. Signed-off-by: Hayden B <[email protected]>
1 parent 722207e commit 488ef8c

File tree

12 files changed

+2091
-17
lines changed

12 files changed

+2091
-17
lines changed

.golangci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ linters:
6868
- forbidigo
6969
- gosec
7070
path: _test\.go
71+
- linters:
72+
- staticcheck
73+
path: pkg/cosign/tlog.go
74+
# NewEntry used for Rekor v1, will update to NewTlogEntry for Rekor v2 support
75+
text: SA1019
76+
- linters:
77+
- staticcheck
78+
path: pkg/cosign/verify.go
79+
# NewEntry used for Rekor v1, will update to NewTlogEntry for Rekor v2 support
80+
text: SA1019
7181
paths:
7282
- third_party$
7383
- builtin$

cmd/cosign/cli/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func New() *cobra.Command {
121121
cmd.AddCommand(VerifyBlobAttestation())
122122
cmd.AddCommand(Triangulate())
123123
cmd.AddCommand(TrustedRoot())
124+
cmd.AddCommand(SigningConfig())
124125
cmd.AddCommand(Env())
125126
cmd.AddCommand(version.WithFont("starwars"))
126127

cmd/cosign/cli/initialize/init_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func TestDoInitialize(t *testing.T) {
169169
targets: map[string][]byte{"ctfe.pub": []byte(`-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----`)},
170170
root: "1.root.json",
171171
wantStdOut: "ctfe.pub",
172-
wantStdErr: "WARNING: Could not fetch trusted_root.json from the TUF mirror (encountered error: getting info for target \"trusted_root.json\": target trusted_root.json not found), falling back to individual targets. It is recommended to update your TUF metadata repository to include trusted_root.json.",
172+
wantStdErr: "WARNING: Could not fetch trusted_root.json from the TUF mirror (encountered error: failed to get target from TUF client getting info for target \"trusted_root.json\": target trusted_root.json not found), falling back to individual targets. It is recommended to update your TUF metadata repository to include trusted_root.json.",
173173
wantErr: false,
174174
wantFiles: []string{filepath.Join("targets", "ctfe.pub")},
175175
expectV2: false,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2025 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package options
16+
17+
import (
18+
"github.com/spf13/cobra"
19+
)
20+
21+
type SigningConfigCreateOptions struct {
22+
Fulcio []string
23+
Rekor []string
24+
OIDCProvider []string
25+
TSA []string
26+
TSAConfig string
27+
RekorConfig string
28+
Out string
29+
}
30+
31+
var _ Interface = (*SigningConfigCreateOptions)(nil)
32+
33+
func (o *SigningConfigCreateOptions) AddFlags(cmd *cobra.Command) {
34+
cmd.Flags().StringArrayVar(&o.Fulcio, "fulcio", nil,
35+
"fulcio service specification, as a comma-separated key-value list.\nRequired keys: url, api-version (integer), start-time, operator. Optional keys: end-time.")
36+
cmd.Flags().StringArrayVar(&o.Rekor, "rekor", nil,
37+
"rekor service specification, as a comma-separated key-value list.\nRequired keys: url, api-version (integer), start-time, operator. Optional keys: end-time.")
38+
cmd.Flags().StringArrayVar(&o.OIDCProvider, "oidc-provider", nil,
39+
"oidc provider specification, as a comma-separated key-value list.\nRequired keys: url, api-version (integer), start-time, operator. Optional keys: end-time.")
40+
cmd.Flags().StringArrayVar(&o.TSA, "tsa", nil,
41+
"timestamping authority specification, as a comma-separated key-value list.\nRequired keys: url, api-version (integer), start-time, operator. Optional keys: end-time.")
42+
43+
cmd.Flags().StringVar(&o.TSAConfig, "tsa-config", "",
44+
"timestamping authority configuration. Required if --tsa is provided. One of: ANY, ALL, EXACT:<count>")
45+
cmd.Flags().StringVar(&o.RekorConfig, "rekor-config", "",
46+
"rekor configuration. Required if --rekor is provided. One of: ANY, ALL, EXACT:<count>")
47+
48+
cmd.Flags().StringVar(&o.Out, "out", "", "path to output signing config")
49+
}

cmd/cosign/cli/signingconfig.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cli
16+
17+
import (
18+
"context"
19+
20+
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
21+
"github.com/sigstore/cosign/v2/cmd/cosign/cli/signingconfig"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func SigningConfig() *cobra.Command {
26+
cmd := &cobra.Command{
27+
Use: "signing-config",
28+
Short: "Interact with a Sigstore protobuf signing config",
29+
Long: "Tool for interacting with a Sigstore protobuf signing config",
30+
}
31+
32+
cmd.AddCommand(signingConfigCreate())
33+
34+
return cmd
35+
}
36+
37+
func signingConfigCreate() *cobra.Command {
38+
o := &options.SigningConfigCreateOptions{}
39+
40+
cmd := &cobra.Command{
41+
Use: "create",
42+
Short: "Create a Sigstore protobuf signing config",
43+
Long: `Create a Sigstore protobuf signing config by supplying verification material for Fulcio, Rekor, OIDC, and TSA services.
44+
Each service is specified via a repeatable flag (--fulcio, --rekor, --oidc-provider, --tsa) that takes a comma-separated list of key-value pairs.`,
45+
Example: `cosign signing-config create \
46+
--fulcio="url=https://fulcio.sigstore.dev,api-version=1,start-time=2024-01-01T00:00:00Z,end-time=2025-01-01T00:00:00Z,operator=sigstore.dev" \
47+
--rekor="url=https://rekor.sigstore.dev,api-version=1,start-time=2024-01-01T00:00:00Z,operator=sigstore.dev" \
48+
--rekor-config="ANY" \
49+
--oidc-provider="url=https://oauth2.sigstore.dev/auth,api-version=1,start-time=2024-01-01T00:00:00Z,operator=sigstore.dev" \
50+
--tsa="url=https://timestamp.sigstore.dev/api/v1/timestamp,api-version=1,start-time=2024-01-01T00:00:00Z,operator=sigstore.dev" \
51+
--tsa-config="EXACT:1" \
52+
--out signing-config.json`,
53+
RunE: func(cmd *cobra.Command, _ []string) error {
54+
scCreateCmd := &signingconfig.CreateCmd{
55+
FulcioSpecs: o.Fulcio,
56+
RekorSpecs: o.Rekor,
57+
OIDCProviderSpecs: o.OIDCProvider,
58+
TSASpecs: o.TSA,
59+
TSAConfig: o.TSAConfig,
60+
RekorConfig: o.RekorConfig,
61+
Out: o.Out,
62+
}
63+
64+
ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout)
65+
defer cancel()
66+
67+
return scCreateCmd.Exec(ctx)
68+
},
69+
}
70+
71+
o.AddFlags(cmd)
72+
return cmd
73+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright 2025 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package signingconfig
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
"strconv"
22+
"strings"
23+
"time"
24+
25+
prototrustroot "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1"
26+
"github.com/sigstore/sigstore-go/pkg/root"
27+
)
28+
29+
type CreateCmd struct {
30+
FulcioSpecs []string
31+
RekorSpecs []string
32+
OIDCProviderSpecs []string
33+
TSASpecs []string
34+
TSAConfig string
35+
RekorConfig string
36+
Out string
37+
}
38+
39+
func (c *CreateCmd) Exec(_ context.Context) error {
40+
if len(c.RekorSpecs) > 0 && c.RekorConfig == "" {
41+
return fmt.Errorf("--rekor-config must be provided when --rekor is specified")
42+
}
43+
if len(c.TSASpecs) > 0 && c.TSAConfig == "" {
44+
return fmt.Errorf("--tsa-config must be provided when --tsa is specified")
45+
}
46+
47+
fulcioServices := make([]root.Service, 0, len(c.FulcioSpecs))
48+
rekorServices := make([]root.Service, 0, len(c.RekorSpecs))
49+
oidcProviders := make([]root.Service, 0, len(c.OIDCProviderSpecs))
50+
tsaServices := make([]root.Service, 0, len(c.TSASpecs))
51+
52+
rekorConfig, err := parseServiceConfig(c.RekorConfig)
53+
if err != nil {
54+
return fmt.Errorf("parsing rekor-config: %w", err)
55+
}
56+
57+
tsaConfig, err := parseServiceConfig(c.TSAConfig)
58+
if err != nil {
59+
return fmt.Errorf("parsing tsa-config: %w", err)
60+
}
61+
62+
for _, spec := range c.FulcioSpecs {
63+
svc, err := parseService(spec)
64+
if err != nil {
65+
return fmt.Errorf("parsing fulcio spec: %w", err)
66+
}
67+
fulcioServices = append(fulcioServices, svc)
68+
}
69+
70+
for _, spec := range c.RekorSpecs {
71+
svc, err := parseService(spec)
72+
if err != nil {
73+
return fmt.Errorf("parsing rekor spec: %w", err)
74+
}
75+
rekorServices = append(rekorServices, svc)
76+
}
77+
78+
for _, spec := range c.OIDCProviderSpecs {
79+
svc, err := parseService(spec)
80+
if err != nil {
81+
return fmt.Errorf("parsing oidc-provider spec: %w", err)
82+
}
83+
oidcProviders = append(oidcProviders, svc)
84+
}
85+
86+
for _, spec := range c.TSASpecs {
87+
svc, err := parseService(spec)
88+
if err != nil {
89+
return fmt.Errorf("parsing tsa spec: %w", err)
90+
}
91+
tsaServices = append(tsaServices, svc)
92+
}
93+
94+
signingConfig, err := root.NewSigningConfig(
95+
root.SigningConfigMediaType02,
96+
fulcioServices,
97+
oidcProviders,
98+
rekorServices,
99+
rekorConfig,
100+
tsaServices,
101+
tsaConfig,
102+
)
103+
if err != nil {
104+
return fmt.Errorf("creating signing config: %w", err)
105+
}
106+
107+
scBytes, err := signingConfig.MarshalJSON()
108+
if err != nil {
109+
return err
110+
}
111+
112+
if c.Out != "" {
113+
err = os.WriteFile(c.Out, scBytes, 0600)
114+
if err != nil {
115+
return err
116+
}
117+
} else {
118+
fmt.Println(string(scBytes))
119+
}
120+
121+
return nil
122+
}
123+
124+
func parseService(spec string) (root.Service, error) {
125+
kvs, err := parseKVs(spec)
126+
if err != nil {
127+
return root.Service{}, err
128+
}
129+
130+
// Validate required keys
131+
requiredKeys := []string{"url", "api-version", "start-time", "operator"}
132+
for _, key := range requiredKeys {
133+
if val, ok := kvs[key]; !ok || val == "" {
134+
return root.Service{}, fmt.Errorf("missing required key '%s' in service spec", key)
135+
}
136+
}
137+
138+
apiVersion, err := strconv.ParseUint(kvs["api-version"], 10, 32)
139+
if err != nil {
140+
return root.Service{}, fmt.Errorf("parsing api-version: %w", err)
141+
}
142+
143+
startTime, err := time.Parse(time.RFC3339, kvs["start-time"])
144+
if err != nil {
145+
return root.Service{}, fmt.Errorf("parsing start-time: %w", err)
146+
}
147+
148+
svc := root.Service{
149+
URL: kvs["url"],
150+
MajorAPIVersion: uint32(apiVersion),
151+
Operator: kvs["operator"],
152+
ValidityPeriodStart: startTime,
153+
}
154+
155+
if et, ok := kvs["end-time"]; ok && et != "" {
156+
endTime, err := time.Parse(time.RFC3339, et)
157+
if err != nil {
158+
return root.Service{}, fmt.Errorf("parsing end-time: %w", err)
159+
}
160+
svc.ValidityPeriodEnd = endTime
161+
}
162+
return svc, nil
163+
}
164+
165+
func parseKVs(spec string) (map[string]string, error) {
166+
kvs := make(map[string]string)
167+
pairs := strings.Split(spec, ",")
168+
for _, pair := range pairs {
169+
parts := strings.SplitN(pair, "=", 2)
170+
if len(parts) != 2 {
171+
return nil, fmt.Errorf("invalid key-value pair: %s", pair)
172+
}
173+
kvs[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
174+
}
175+
return kvs, nil
176+
}
177+
178+
func parseServiceConfig(config string) (root.ServiceConfiguration, error) {
179+
if config == "" {
180+
return root.ServiceConfiguration{}, nil
181+
}
182+
parts := strings.SplitN(config, ":", 2)
183+
mode := strings.ToUpper(parts[0])
184+
var selector prototrustroot.ServiceSelector
185+
switch mode {
186+
case "ANY":
187+
selector = prototrustroot.ServiceSelector_ANY
188+
if len(parts) > 1 {
189+
return root.ServiceConfiguration{}, fmt.Errorf("mode %s does not accept a count", mode)
190+
}
191+
return root.ServiceConfiguration{Selector: selector}, nil
192+
case "ALL":
193+
selector = prototrustroot.ServiceSelector_ALL
194+
if len(parts) > 1 {
195+
return root.ServiceConfiguration{}, fmt.Errorf("mode %s does not accept a count", mode)
196+
}
197+
return root.ServiceConfiguration{Selector: selector}, nil
198+
case "EXACT":
199+
selector = prototrustroot.ServiceSelector_EXACT
200+
if len(parts) != 2 {
201+
return root.ServiceConfiguration{}, fmt.Errorf("mode EXACT requires a count, e.g. EXACT:2")
202+
}
203+
count, err := strconv.ParseUint(parts[1], 10, 32)
204+
if err != nil {
205+
return root.ServiceConfiguration{}, fmt.Errorf("invalid count for EXACT mode: %w", err)
206+
}
207+
return root.ServiceConfiguration{Selector: selector, Count: uint32(count)}, nil
208+
default:
209+
return root.ServiceConfiguration{}, fmt.Errorf("invalid service config mode: %s", mode)
210+
}
211+
}

0 commit comments

Comments
 (0)