diff --git a/cmd/cosign/cli/bundle.go b/cmd/cosign/cli/bundle.go index 25e1a245..a72d06e1 100644 --- a/cmd/cosign/cli/bundle.go +++ b/cmd/cosign/cli/bundle.go @@ -31,6 +31,7 @@ func Bundle() *cobra.Command { } cmd.AddCommand(bundleCreate()) + cmd.AddCommand(bundleUpgrade()) return cmd } @@ -68,3 +69,28 @@ func bundleCreate() *cobra.Command { o.AddFlags(cmd) return cmd } + +func bundleUpgrade() *cobra.Command { + o := &options.BundleUpgradeOptions{} + + cmd := &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade a Sigstore protobuf bundle", + Long: "Upgrade a Sigstore Protobuf bundle to the latest version. This command only supports standardized bundles.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + bundleUpgradeCmd := &bundle.UpgradeCmd{ + Out: o.Out, + RekorURL: o.RekorURL, + } + + ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) + defer cancel() + + return bundleUpgradeCmd.Exec(ctx, args[0]) + }, + } + + o.AddFlags(cmd) + return cmd +} diff --git a/cmd/cosign/cli/bundle/upgrade.go b/cmd/cosign/cli/bundle/upgrade.go new file mode 100644 index 00000000..1fcf8003 --- /dev/null +++ b/cmd/cosign/cli/bundle/upgrade.go @@ -0,0 +1,156 @@ +// +// Copyright 2026 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 bundle + +import ( + "context" + "crypto/x509" + "fmt" + "os" + + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + rekor "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/tle" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/sigstore/cosign/v3/internal/ui" +) + +type UpgradeCmd struct { + Out string + RekorURL string +} + +func (c *UpgradeCmd) Exec(ctx context.Context, bundlePath string) error { + data, err := os.ReadFile(bundlePath) + if err != nil { + return fmt.Errorf("reading input file: %w", err) + } + + rekorClient, err := rekor.GetRekorClient(c.RekorURL) + if err != nil { + return fmt.Errorf("creating rekor client: %w", err) + } + + upgradedBundle, err := upgradeBundle(ctx, data, rekorClient) + if err != nil { + return fmt.Errorf("upgrading bundle: %w", err) + } + + if c.Out != "" { + err = os.WriteFile(c.Out, upgradedBundle, 0600) + if err != nil { + return fmt.Errorf("writing upgraded bundle: %w", err) + } + ui.Infof(ctx, "Successfully upgraded bundle written to %s", c.Out) + } else { + fmt.Println(string(upgradedBundle)) + } + + return nil +} + +func upgradeBundle(ctx context.Context, data []byte, rekorClient *client.Rekor) ([]byte, error) { + var bundle protobundle.Bundle + if err := protojson.Unmarshal(data, &bundle); err != nil { + return nil, fmt.Errorf("unmarshaling bundle: %w", err) + } + + if bundle.VerificationMaterial == nil { + return nil, fmt.Errorf("bundle is missing verification material") + } + if bundle.VerificationMaterial.Content == nil { + return nil, fmt.Errorf("bundle verification material is missing content (public key or certificate)") + } + + switch bundle.MediaType { + case "application/vnd.dev.sigstore.bundle.v0.3+json", "application/vnd.dev.sigstore.bundle+json;version=0.3": + ui.Infof(ctx, "Bundle is already at v0.3, no upgrade needed.") + return data, nil + case "application/vnd.dev.sigstore.bundle+json;version=0.1": + ui.Infof(ctx, "Upgrading from v0.1 to v0.3...") + case "application/vnd.dev.sigstore.bundle+json;version=0.2": + ui.Infof(ctx, "Upgrading from v0.2 to v0.3...") + default: + return nil, fmt.Errorf("unsupported bundle version: %s", bundle.MediaType) + } + + if chainContent, ok := bundle.VerificationMaterial.Content.(*protobundle.VerificationMaterial_X509CertificateChain); ok { + certChain := chainContent.X509CertificateChain.Certificates + if len(certChain) > 0 { + ui.Infof(ctx, "Truncating certificate chain to only the leaf certificate...") + for i, cert := range certChain { + parsedCert, err := x509.ParseCertificate(cert.RawBytes) + if err != nil { + ui.Infof(ctx, " Certificate %d: ", i, err) + continue + } + + var identity string + if len(parsedCert.EmailAddresses) > 0 { + identity = parsedCert.EmailAddresses[0] + } else if len(parsedCert.URIs) > 0 { + identity = parsedCert.URIs[0].String() + } else if s := parsedCert.Subject.String(); s != "" { + identity = s + } else { + identity = "" + } + ui.Infof(ctx, " Certificate %d Identity: %s", i, identity) + } + bundle.VerificationMaterial.Content = &protobundle.VerificationMaterial_Certificate{ + Certificate: certChain[0], + } + } + } + + for i, entry := range bundle.VerificationMaterial.TlogEntries { + if entry.InclusionPromise != nil && entry.InclusionProof == nil { + ui.Infof(ctx, "Fetching missing inclusion proof from Rekor for log index %d...", entry.LogIndex) + + params := entries.NewGetLogEntryByIndexParamsWithContext(ctx) + params.SetLogIndex(entry.LogIndex) + + resp, err := rekorClient.Entries.GetLogEntryByIndex(params) + if err != nil { + return nil, fmt.Errorf("fetching log entry by index: %w", err) + } + + if len(resp.Payload) != 1 { + return nil, fmt.Errorf("expected exactly 1 entry from Rekor for index %d, got %d", entry.LogIndex, len(resp.Payload)) + } + + for _, e := range resp.Payload { + protoEntry, err := tle.GenerateTransparencyLogEntry(e) + if err != nil { + return nil, fmt.Errorf("generating proto entry: %w", err) + } + bundle.VerificationMaterial.TlogEntries[i] = protoEntry + } + } + } + + bundle.MediaType = "application/vnd.dev.sigstore.bundle.v0.3+json" + + out, err := protojson.Marshal(&bundle) + if err != nil { + return nil, fmt.Errorf("marshaling bundle: %w", err) + } + + return out, nil +} diff --git a/cmd/cosign/cli/bundle/upgrade_test.go b/cmd/cosign/cli/bundle/upgrade_test.go new file mode 100644 index 00000000..594715d4 --- /dev/null +++ b/cmd/cosign/cli/bundle/upgrade_test.go @@ -0,0 +1,447 @@ +// +// Copyright 2026 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 bundle + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "io" + "math/big" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/generated/models" +) + +type mockEntriesClient struct { + entries.ClientService + getLogEntryByIndexFunc func(params *entries.GetLogEntryByIndexParams, opts ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) +} + +func (m *mockEntriesClient) GetLogEntryByIndex(params *entries.GetLogEntryByIndexParams, opts ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) { + if m.getLogEntryByIndexFunc != nil { + return m.getLogEntryByIndexFunc(params, opts...) + } + return nil, nil +} + +func TestUpgradeBundle(t *testing.T) { + checkV03 := func(expectedCertB64 string) func(t *testing.T, output []byte) { + return func(t *testing.T, output []byte) { + var m map[string]interface{} + err := json.Unmarshal(output, &m) + if err != nil { + t.Fatalf("unmarshaling output: %v", err) + } + + if m["mediaType"] != "application/vnd.dev.sigstore.bundle.v0.3+json" { + t.Fatalf("expected mediaType to be 'application/vnd.dev.sigstore.bundle.v0.3+json', got %v", m["mediaType"]) + } + + vm, ok := m["verificationMaterial"].(map[string]interface{}) + if !ok { + t.Fatal("missing verificationMaterial") + } + + cert, ok := vm["certificate"].(map[string]interface{}) + if !ok { + t.Fatal("missing certificate field in verificationMaterial") + } + + if cert["rawBytes"] != expectedCertB64 { + t.Fatalf("expected leaf certificate rawBytes to be %v, got %v", expectedCertB64, cert["rawBytes"]) + } + } + } + + genCert := func(cn string, emails []string, uris []string) string { + cert, _, err := selfSignedCertificate(cn, emails, uris) + if err != nil { + t.Fatal(err) + } + return base64.StdEncoding.EncodeToString(cert.Raw) + } + + certEmailB64 := genCert("cert", []string{"foo@bar.com"}, nil) + certURIB64 := genCert("cert", nil, []string{"https://example.com/workflow"}) + certSubjectB64 := genCert("my-identity", nil, nil) + certNoneB64 := genCert("", nil, nil) + + tests := []struct { + name string + input string + expectError bool + checkOutput func(t *testing.T, output []byte) + rekorClient *client.Rekor + }{ + { + name: "Already v0.3", + input: `{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{}}}`, + checkOutput: func(t *testing.T, output []byte) { + expected := `{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{}}}` + if string(output) != expected { + t.Fatal("expected output to match input") + } + }, + }, + { + name: "Upgrade v0.1 to v0.3", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "bGVhZg=="}, + {"rawBytes": "aW50ZXJtZWRpYXRl"} + ] + } + } + }`, + checkOutput: checkV03("bGVhZg=="), + }, + { + name: "Upgrade v0.2 to v0.3", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "bGVhZg=="}, + {"rawBytes": "aW50ZXJtZWRpYXRl"} + ] + } + } + }`, + checkOutput: checkV03("bGVhZg=="), + }, + { + name: "Upgrade with Email SAN", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "` + certEmailB64 + `"} + ] + } + } + }`, + checkOutput: checkV03(certEmailB64), + }, + { + name: "Upgrade with URI SAN", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "` + certURIB64 + `"} + ] + } + } + }`, + checkOutput: checkV03(certURIB64), + }, + { + name: "Upgrade with Subject", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "` + certSubjectB64 + `"} + ] + } + } + }`, + checkOutput: checkV03(certSubjectB64), + }, + { + name: "Upgrade with no identity", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "` + certNoneB64 + `"} + ] + } + } + }`, + checkOutput: checkV03(certNoneB64), + }, + { + name: "Missing VerificationMaterial", + input: `{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1"}`, + expectError: true, + }, + { + name: "Missing Content in VerificationMaterial", + input: `{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", "verificationMaterial": {}}`, + expectError: true, + }, + { + name: "Unsupported version", + input: `{"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.4", "verificationMaterial": {"publicKey": {}}}`, + expectError: true, + }, + { + name: "Upgrade with missing inclusion proof", + input: `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "bGVhZg=="} + ] + }, + "tlogEntries": [ + { + "logIndex": 42, + "inclusionPromise": { + "signedEntryTimestamp": "c2ln" + } + } + ] + } + }`, + rekorClient: &client.Rekor{ + Entries: &mockEntriesClient{ + getLogEntryByIndexFunc: func(_ *entries.GetLogEntryByIndexParams, _ ...entries.ClientOption) (*entries.GetLogEntryByIndexOK, error) { + return &entries.GetLogEntryByIndexOK{ + Payload: models.LogEntry{ + "uuid": createMockLogEntryAnon(), + }, + }, nil + }, + }, + }, + checkOutput: func(t *testing.T, output []byte) { + var m map[string]interface{} + if err := json.Unmarshal(output, &m); err != nil { + t.Fatalf("unmarshaling output: %v", err) + } + vm := m["verificationMaterial"].(map[string]interface{}) + tlogEntries := vm["tlogEntries"].([]interface{}) + entry := tlogEntries[0].(map[string]interface{}) + proof := entry["inclusionProof"].(map[string]interface{}) + if proof["logIndex"].(string) != "42" { + t.Fatalf("expected logIndex 42, got %v", proof["logIndex"]) + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + out, err := upgradeBundle(context.Background(), []byte(tc.input), tc.rekorClient) + if tc.expectError { + if err == nil { + t.Fatal("expected error, got nil") + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tc.checkOutput != nil { + tc.checkOutput(t, out) + } + } + }) + } +} + +func TestUpgradeCmd(t *testing.T) { + ctx := context.Background() + + t.Run("File Output", func(t *testing.T) { + td := t.TempDir() + + inputPath := filepath.Join(td, "input.json") + v01Bundle := `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "bGVhZg=="} + ] + } + } + }` + err := os.WriteFile(inputPath, []byte(v01Bundle), 0600) + if err != nil { + t.Fatal(err) + } + + outputPath := filepath.Join(td, "output.json") + + cmd := UpgradeCmd{ + Out: outputPath, + } + + err = cmd.Exec(ctx, inputPath) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile(outputPath) + if err != nil { + t.Fatal(err) + } + + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + t.Fatal(err) + } + + if m["mediaType"] != "application/vnd.dev.sigstore.bundle.v0.3+json" { + t.Fatalf("expected mediaType to be 'application/vnd.dev.sigstore.bundle.v0.3+json', got %v", m["mediaType"]) + } + }) + + t.Run("Stdout Fallback", func(t *testing.T) { + td := t.TempDir() + + inputPath := filepath.Join(td, "input.json") + v01Bundle := `{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + {"rawBytes": "bGVhZg=="} + ] + } + } + }` + err := os.WriteFile(inputPath, []byte(v01Bundle), 0600) + if err != nil { + t.Fatal(err) + } + + cmd := UpgradeCmd{ + Out: "", + } + + reader, writer, err := os.Pipe() + if err != nil { + t.Fatal("failed to create a pipe for testing os.Stdout") + } + stdout := os.Stdout + os.Stdout = writer + + err = cmd.Exec(ctx, inputPath) + + os.Stdout = stdout + writer.Close() + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var buffer bytes.Buffer + _, err = io.Copy(&buffer, reader) + if err != nil { + t.Fatal(err) + } + + output := buffer.Bytes() + var m map[string]interface{} + if err := json.Unmarshal(output, &m); err != nil { + t.Fatalf("unmarshaling stdout output: %v", err) + } + + if m["mediaType"] != "application/vnd.dev.sigstore.bundle.v0.3+json" { + t.Fatalf("expected mediaType to be 'application/vnd.dev.sigstore.bundle.v0.3+json', got %v", m["mediaType"]) + } + }) +} + +func createMockLogEntryAnon() models.LogEntryAnon { + body := "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyMzBkODM1OGRjOGU4ODkwYjRjNThkZWViNjI5MTJlZTJmMjAzNTdhZTkyYTVjYzg2MWI5OGU2OGZlMzFhY2I1In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUURHcXFXL1Q4YzZqZTltSVFPaUNhZXdldWZpUmJXMC9YZVBJUmMxWVdXUVZBSWdNWjVlbmNvWFdNdjdUK3AxU0k1YUUzdzZOb3Zyb3RYdGVoMlV3V040SUJvPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVSnJha05EUVZScFowRjNTVUpCWjBsQ1FWUkJTMEpuWjNGb2EycFBVRkZSUkVGcVFYVk5VbFYzUlhkWlJGWlJVVXRGZDNoNllWZGtlbVJIT1hrS1dsTTFhMXBZV1hoR1ZFRlVRbWRPVmtKQlRWUkVTRTV3V2pOT01HSXpTbXhNV0U0eFdXcEJaVVozTUhsT2FrRXdUVlJWZUU1RVRUUk9SR2hoUm5jd2VRcE9ha0V3VFZSVmVFNVVUVFZPUkdoaFRVRkJkMWRVUVZSQ1oyTnhhR3RxVDFCUlNVSkNaMmR4YUd0cVQxQlJUVUpDZDA1RFFVRlVMM0JKUVdaWldXNUxDazlHYkUxS00zYzBMMnBxUmxoNFRHNHhTRlZOWkVzMGJteDJiU3RLV0c1WmNUbFlPRlZpTkVWRFJFcFFjRUp1VW1sd1pGQkRUWGxYWTBsRGRIRkVPV1lLVlVGdFpIVnZiMUZLWmpReWJ6TlZkMk42UVU5Q1owNVdTRkU0UWtGbU9FVkNRVTFEUWpSQmQwVjNXVVJXVWpCc1FrRjNkME5uV1VsTGQxbENRbEZWU0FwQmQwMTNTSGRaUkZaU01HcENRbWQzUm05QlZXbHFPVEJ5VFZSeVVGaHlkM2hHYm5kTk9FOUdla3MxUW5Ock1IZEdVVmxFVmxJd1VrRlJTQzlDUVhOM0NrTlpSVWhqTTFacFlXMVdhbVJFUVZWQ1oyOXlRbWRGUlVGWlR5OU5RVVZDUWtGYWNHTXpUakZhV0VsM1EyZFpTVXR2V2tsNmFqQkZRWGRKUkZOQlFYY0tVbEZKWjFSSk4ya3pUWEYwVVRBMlp6QnlNbTlEWXpWb1QwZFBWRVZLYlVKSlZUSTVhRXhqVGxKNFJXeHBTbTlEU1ZGRWNuWk9TSE56U21vd1JqWlVNd293UmpaTU4wRkhiWEpDVlhGQllWSllSV0owWVhOaE5IcFJkWHA0YUdjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifX19fQ==" + integratedTime := int64(1234567890) + logIndex := int64(42) + treeSize := int64(100) + rootHash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + logID := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + checkpoint := "checkpoint" + return models.LogEntryAnon{ + Body: body, + IntegratedTime: &integratedTime, + LogIndex: &logIndex, + LogID: &logID, + Verification: &models.LogEntryAnonVerification{ + SignedEntryTimestamp: []byte("sig"), + InclusionProof: &models.InclusionProof{ + LogIndex: &logIndex, + TreeSize: &treeSize, + RootHash: &rootHash, + Hashes: []string{}, + Checkpoint: &checkpoint, + }, + }, + } +} + +func selfSignedCertificate(commonName string, emails []string, uris []string) (*x509.Certificate, *ecdsa.PrivateKey, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + var parsedURIs []*url.URL + for _, u := range uris { + parsed, _ := url.Parse(u) + parsedURIs = append(parsedURIs, parsed) + } + + var subject pkix.Name + if commonName != "" { + subject.CommonName = commonName + subject.Organization = []string{"dev"} + } + + ct := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: subject, + EmailAddresses: emails, + URIs: parsedURIs, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + } + certBytes, err := x509.CreateCertificate(rand.Reader, ct, ct, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, nil, err + } + return cert, priv, nil +} diff --git a/cmd/cosign/cli/options/bundle.go b/cmd/cosign/cli/options/bundle.go index 8001c491..fad9c1d0 100644 --- a/cmd/cosign/cli/options/bundle.go +++ b/cmd/cosign/cli/options/bundle.go @@ -91,3 +91,18 @@ func (o *BundleCreateOptions) AddFlags(cmd *cobra.Command) { cmd.MarkFlagsMutuallyExclusive("bundle", "certificate") cmd.MarkFlagsMutuallyExclusive("bundle", "signature") } + +type BundleUpgradeOptions struct { + Out string + RekorURL string +} + +var _ Interface = (*BundleUpgradeOptions)(nil) + +func (o *BundleUpgradeOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Out, "out", "", "path to the output upgraded bundle file") + _ = cmd.MarkFlagFilename("out", bundleExts...) + + cmd.Flags().StringVar(&o.RekorURL, "rekor-url", "https://rekor.sigstore.dev", "URL of the transparency log") + _ = cmd.RegisterFlagCompletionFunc("rekor-url", cobra.NoFileCompletions) +} diff --git a/doc/cosign_bundle.md b/doc/cosign_bundle.md index 27c77009..50e712d9 100644 --- a/doc/cosign_bundle.md +++ b/doc/cosign_bundle.md @@ -24,4 +24,5 @@ Tools for interacting with a Sigstore protobuf bundle * [cosign](cosign.md) - A tool for Container Signing, Verification and Storage in an OCI registry. * [cosign bundle create](cosign_bundle_create.md) - Create a Sigstore protobuf bundle +* [cosign bundle upgrade](cosign_bundle_upgrade.md) - Upgrade a Sigstore protobuf bundle diff --git a/doc/cosign_bundle_upgrade.md b/doc/cosign_bundle_upgrade.md new file mode 100644 index 00000000..2df5ee48 --- /dev/null +++ b/doc/cosign_bundle_upgrade.md @@ -0,0 +1,32 @@ +## cosign bundle upgrade + +Upgrade a Sigstore protobuf bundle + +### Synopsis + +Upgrade a Sigstore Protobuf bundle to the latest version. This command only supports standardized bundles. + +``` +cosign bundle upgrade [flags] +``` + +### Options + +``` + -h, --help help for upgrade + --out string path to the output upgraded bundle file + --rekor-url string URL of the transparency log (default "https://rekor.sigstore.dev") +``` + +### Options inherited from parent commands + +``` + --output-file string log output to a file + -t, --timeout duration timeout for commands (default 3m0s) + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign bundle](cosign_bundle.md) - Interact with a Sigstore protobuf bundle +