diff --git a/cmd/cosign/cli/initialize.go b/cmd/cosign/cli/initialize.go index a701ebb49fa..a59b3fcc891 100644 --- a/cmd/cosign/cli/initialize.go +++ b/cmd/cosign/cli/initialize.go @@ -41,19 +41,22 @@ Any updated TUF repository will be written to $HOME/.sigstore/root/. Trusted keys and certificate used in cosign verification (e.g. verifying Fulcio issued certificates with Fulcio root CA) are pulled form the trusted metadata.`, - Example: `cosign initialize -mirror -out + Example: `cosign initialize --mirror --out # initialize root with distributed root keys, default mirror, and default out path. cosign initialize # initialize with an out-of-band root key file, using the default mirror. -cosign initialize -root +cosign initialize --root # initialize with an out-of-band root key file and custom repository mirror. -cosign initialize -mirror -root `, +cosign initialize --mirror --root + +# initialize with an out-of-band root key file and custom repository mirror while verifying root checksum. +cosign initialize --mirror --root --root-checksum `, PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, _ []string) error { - return initialize.DoInitialize(cmd.Context(), o.Root, o.Mirror) + return initialize.DoInitializeWithRootChecksum(cmd.Context(), o.Root, o.Mirror, o.RootChecksum) }, } diff --git a/cmd/cosign/cli/initialize/init.go b/cmd/cosign/cli/initialize/init.go index 158bc0a5f0d..eca80ea15ea 100644 --- a/cmd/cosign/cli/initialize/init.go +++ b/cmd/cosign/cli/initialize/init.go @@ -20,17 +20,38 @@ import ( _ "embed" // To enable the `go:embed` directive. "encoding/json" "fmt" + "os" + "strings" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/sigstore/pkg/tuf" ) func DoInitialize(ctx context.Context, root, mirror string) error { + return doInitialize(ctx, root, mirror, "", true) +} + +func DoInitializeWithRootChecksum(ctx context.Context, root, mirror, rootChecksum string) error { + return doInitialize(ctx, root, mirror, rootChecksum, false) +} + +func doInitialize(ctx context.Context, root, mirror, rootChecksum string, forceSkipChecksumValidation bool) error { // Get the initial trusted root contents. var rootFileBytes []byte var err error if root != "" { - rootFileBytes, err = blob.LoadFileOrURL(root) + if !forceSkipChecksumValidation { + if rootChecksum == "" && (strings.HasPrefix(root, "http://") || strings.HasPrefix(root, "https://")) { + fmt.Fprintln(os.Stderr, options.RootWithoutChecksumDeprecation) + } + } + verifyChecksum := !forceSkipChecksumValidation && (rootChecksum != "") + if verifyChecksum { + rootFileBytes, err = blob.LoadFileOrURLWithChecksum(root, rootChecksum) + } else { + rootFileBytes, err = blob.LoadFileOrURL(root) + } if err != nil { return err } diff --git a/cmd/cosign/cli/options/deprecate.go b/cmd/cosign/cli/options/deprecate.go index 76084afa179..e5fca7cc13a 100644 --- a/cmd/cosign/cli/options/deprecate.go +++ b/cmd/cosign/cli/options/deprecate.go @@ -19,3 +19,8 @@ const SBOMAttachmentDeprecation = "WARNING: SBOM attachments are deprecated " + "and support will be removed in a Cosign release soon after 2024-02-22 " + "(see https://github.com/sigstore/cosign/issues/2755). " + "Instead, please use SBOM attestations." + +const RootWithoutChecksumDeprecation = "WARNING: Fetching initial root from URL " + + "without providing its checksum is deprecated and will be disallowed in " + + "a Cosing release soon after TODO. Please provide the initial root checksum " + + "via the --root-checksum argument." diff --git a/cmd/cosign/cli/options/initialize.go b/cmd/cosign/cli/options/initialize.go index ab91955ee7c..b8cfc243c62 100644 --- a/cmd/cosign/cli/options/initialize.go +++ b/cmd/cosign/cli/options/initialize.go @@ -22,8 +22,9 @@ import ( // InitializeOptions is the top level wrapper for the initialize command. type InitializeOptions struct { - Mirror string - Root string + Mirror string + Root string + RootChecksum string } var _ Interface = (*InitializeOptions)(nil) @@ -36,4 +37,7 @@ func (o *InitializeOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.Root, "root", "", "path to trusted initial root. defaults to embedded root") _ = cmd.Flags().SetAnnotation("root", cobra.BashCompSubdirsInDir, []string{}) + + cmd.Flags().StringVar(&o.RootChecksum, "root-checksum", "", + "checksum of the initial root, required if root is downloaded via http(s). expects sha512 by default, can be changed to sha256 by providing sha256:") } diff --git a/doc/cosign_initialize.md b/doc/cosign_initialize.md index 84f4fa30272..a11b861bace 100644 --- a/doc/cosign_initialize.md +++ b/doc/cosign_initialize.md @@ -25,24 +25,28 @@ cosign initialize [flags] ### Examples ``` -cosign initialize -mirror -out +cosign initialize --mirror --out # initialize root with distributed root keys, default mirror, and default out path. cosign initialize # initialize with an out-of-band root key file, using the default mirror. -cosign initialize -root +cosign initialize --root # initialize with an out-of-band root key file and custom repository mirror. -cosign initialize -mirror -root +cosign initialize --mirror --root + +# initialize with an out-of-band root key file and custom repository mirror while verifying root checksum. +cosign initialize --mirror --root --root-checksum ``` ### Options ``` - -h, --help help for initialize - --mirror string GCS bucket to a SigStore TUF repository, or HTTP(S) base URL, or file:/// for local filestore remote (air-gap) (default "https://tuf-repo-cdn.sigstore.dev") - --root string path to trusted initial root. defaults to embedded root + -h, --help help for initialize + --mirror string GCS bucket to a SigStore TUF repository, or HTTP(S) base URL, or file:/// for local filestore remote (air-gap) (default "https://tuf-repo-cdn.sigstore.dev") + --root string path to trusted initial root. defaults to embedded root + --root-checksum string checksum of the initial root, required if root is downloaded via http(s). expects sha512 by default, can be changed to sha256 by providing sha256: ``` ### Options inherited from parent commands diff --git a/pkg/blob/load.go b/pkg/blob/load.go index 543af56fac1..8b410edc9a2 100644 --- a/pkg/blob/load.go +++ b/pkg/blob/load.go @@ -15,6 +15,9 @@ package blob import ( + "crypto/sha256" + "crypto/sha512" + "encoding/hex" "fmt" "io" "net/http" @@ -72,3 +75,35 @@ func LoadFileOrURL(fileRef string) ([]byte, error) { } return raw, nil } + +func LoadFileOrURLWithChecksum(fileRef string, checksum string) ([]byte, error) { + checksumParts := strings.Split(checksum, ":") + if len(checksumParts) >= 3 { + return nil, fmt.Errorf("wrong checksum input format, must have at most 1 colon: %s", checksum) + } + + checksumAlgo := sha512.New() + checksumValue := checksumParts[len(checksumParts)-1] + if len(checksumParts) == 2 { + switch checksumParts[0] { + case "sha512": // the default set above + case "sha256": + checksumAlgo = sha256.New() + default: + return nil, fmt.Errorf("unsupported checksum algorithm: %s", checksumParts[0]) + } + } + + fileContent, err := LoadFileOrURL(fileRef) + if err != nil { + return nil, err + } + + checksumAlgo.Write(fileContent) + computedChecksum := hex.EncodeToString(checksumAlgo.Sum(nil)) + if computedChecksum != checksumValue { + return nil, fmt.Errorf("incorrect checksum for file %s: expected %s but got %s", fileRef, checksumValue, computedChecksum) + } + + return fileContent, nil +} diff --git a/pkg/blob/load_test.go b/pkg/blob/load_test.go index 2e09ff5d061..cbb01f75167 100644 --- a/pkg/blob/load_test.go +++ b/pkg/blob/load_test.go @@ -22,6 +22,7 @@ import ( "os" "path" "runtime" + "strings" "testing" ) @@ -97,3 +98,52 @@ func TestLoadURL(t *testing.T) { t.Error("LoadFileOrURL(): expected error for invalid scheme") } } + +func TestLoadURLWithChecksum(t *testing.T) { + data := []byte("test") + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.Write(data) + })) + defer server.Close() + + // default behavior with sha512 + actual, err := LoadFileOrURLWithChecksum( + server.URL, + "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + ) + if err != nil { + t.Errorf("Reading from HTTP failed: %v", err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(HTTP) = '%s'; want '%s'", actual, data) + } + + // override checksum algo to sha256 + actual, err = LoadFileOrURLWithChecksum( + server.URL, + "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + ) + if err != nil { + t.Errorf("Reading from HTTP failed: %v", err) + } else if !bytes.Equal(actual, data) { + t.Errorf("LoadFileOrURL(HTTP) = '%s'; want '%s'", actual, data) + } + + // ensure it fails with the wrong checksum + _, err = LoadFileOrURLWithChecksum( + server.URL, + "certainly not a correct checksum value", + ) + if err == nil || !strings.Contains(err.Error(), "incorrect checksum") { + t.Errorf("Expected an 'incorrect checksum' error, got: %v", err) + } + + // ensure it fails with incorrect algorithm + _, err = LoadFileOrURLWithChecksum( + server.URL, + "sha321123:foobar", + ) + if err == nil || !strings.Contains(err.Error(), "unsupported checksum") { + t.Errorf("Expected an 'unsupported checksum' error, got: %v", err) + } +}