diff --git a/IMPORT.md b/IMPORT.md new file mode 100644 index 00000000000..56b8be1188a --- /dev/null +++ b/IMPORT.md @@ -0,0 +1,17 @@ +# Import RSA and EC Keypairs +* Currently only supports RSA and ECDSA private keys + +### Import a keypair + +```shell +$ cosign import-key-pair --key opensslrsakey.pem +Enter password for private key: +Enter password for private key again: +Private key written to import-cosign.key +Public key written to import-cosign.pub +``` +### Sign a container with imported keypair + +```shell +$ cosign sign --key import import-cosign.key dlorenc/demo +``` diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index c2acb6d361c..90ebc8ff1bb 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -67,6 +67,7 @@ func New() *cobra.Command { cmd.AddCommand(Download()) cmd.AddCommand(Generate()) cmd.AddCommand(GenerateKeyPair()) + cmd.AddCommand(ImportKeyPair()) cmd.AddCommand(Initialize()) cmd.AddCommand(Load()) cmd.AddCommand(Manifest()) diff --git a/cmd/cosign/cli/generate/generate_key_pair.go b/cmd/cosign/cli/generate/generate_key_pair.go index df895b91d7e..81c21724599 100644 --- a/cmd/cosign/cli/generate/generate_key_pair.go +++ b/cmd/cosign/cli/generate/generate_key_pair.go @@ -141,6 +141,7 @@ func isTerminal() bool { return (stat.Mode() & os.ModeCharDevice) != 0 } +// TODO centralize password prompt logic for code reuse across more use cases -> https://github.com/sigstore/cosign/issues/1078 func getPassFromTerm(confirm bool) ([]byte, error) { fmt.Fprint(os.Stderr, "Enter password for private key: ") pw1, err := term.ReadPassword(0) @@ -164,6 +165,7 @@ func getPassFromTerm(confirm bool) ([]byte, error) { return pw1, nil } +// TODO need to centralize this logic func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { diff --git a/cmd/cosign/cli/import_key_pair.go b/cmd/cosign/cli/import_key_pair.go new file mode 100644 index 00000000000..7c5a017eed4 --- /dev/null +++ b/cmd/cosign/cli/import_key_pair.go @@ -0,0 +1,48 @@ +// +// Copyright 2021 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 ( + "github.com/spf13/cobra" + + "github.com/sigstore/cosign/cmd/cosign/cli/importkeypair" + "github.com/sigstore/cosign/cmd/cosign/cli/options" +) + +func ImportKeyPair() *cobra.Command { + o := &options.ImportKeyPairOptions{} + + cmd := &cobra.Command{ + Use: "import-key-pair", + Short: "Imports a PEM-encoded RSA or EC private key.", + Long: "Imports a PEM-encoded RSA or EC private key for signing.", + Example: ` cosign import-key-pair --key openssl.key + + # import PEM-encoded RSA or EC private key and write to import-cosign.key and import-cosign.pub files + cosign import-key-pair --key + +CAVEATS: + This command interactively prompts for a password. You can use + the COSIGN_PASSWORD environment variable to provide one.`, + + RunE: func(cmd *cobra.Command, args []string) error { + return importkeypair.ImportKeyPairCmd(cmd.Context(), o.Key, args) + }, + } + + o.AddFlags(cmd) + return cmd +} diff --git a/cmd/cosign/cli/importkeypair/import_key_pair.go b/cmd/cosign/cli/importkeypair/import_key_pair.go new file mode 100644 index 00000000000..cc9fc7ba2c6 --- /dev/null +++ b/cmd/cosign/cli/importkeypair/import_key_pair.go @@ -0,0 +1,128 @@ +// +// Copyright 2021 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 importkeypair + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/pkg/errors" + "github.com/sigstore/cosign/pkg/cosign" + "golang.org/x/term" +) + +var ( + // Read is for fuzzing + Read = readPasswordFn +) + +// nolint +func ImportKeyPairCmd(ctx context.Context, keyVal string, args []string) error { + + keys, err := cosign.ImportKeyPair(keyVal, GetPass) + if err != nil { + return err + } + + if fileExists("import-cosign.key") { + var overwrite string + fmt.Fprint(os.Stderr, "File import-cosign.key already exists. Overwrite (y/n)? ") + fmt.Scanf("%s", &overwrite) + switch overwrite { + case "y", "Y": + case "n", "N": + return nil + default: + fmt.Fprintln(os.Stderr, "Invalid input") + return nil + } + } + // TODO: make sure the perms are locked down first. + if err := os.WriteFile("import-cosign.key", keys.PrivateBytes, 0600); err != nil { + return err + } + fmt.Fprintln(os.Stderr, "Private key written to import-cosign.key") + + if err := os.WriteFile("import-cosign.pub", keys.PublicBytes, 0644); err != nil { + return err + } // #nosec G306 + fmt.Fprintln(os.Stderr, "Public key written to import-cosign.pub") + return nil +} + +func GetPass(confirm bool) ([]byte, error) { + read := Read(confirm) + return read() +} + +func readPasswordFn(confirm bool) func() ([]byte, error) { + pw, ok := os.LookupEnv("COSIGN_PASSWORD") + switch { + case ok: + return func() ([]byte, error) { + return []byte(pw), nil + } + case isTerminal(): + return func() ([]byte, error) { + return getPassFromTerm(confirm) + } + // Handle piped in passwords. + default: + return func() ([]byte, error) { + return io.ReadAll(os.Stdin) + } + } +} + +func isTerminal() bool { + stat, _ := os.Stdin.Stat() + return (stat.Mode() & os.ModeCharDevice) != 0 +} + +// TODO centralize password prompt logic for code reuse across more use cases -> https://github.com/sigstore/cosign/issues/1078 +func getPassFromTerm(confirm bool) ([]byte, error) { + fmt.Fprint(os.Stderr, "Enter password for private key: ") + pw1, err := term.ReadPassword(0) + if err != nil { + return nil, err + } + if !confirm { + return pw1, nil + } + fmt.Fprintln(os.Stderr) + fmt.Fprint(os.Stderr, "Enter password for private key again: ") + confirmpw, err := term.ReadPassword(0) + fmt.Fprintln(os.Stderr) + if err != nil { + return nil, err + } + + if string(pw1) != string(confirmpw) { + return nil, errors.New("passwords do not match") + } + return pw1, nil +} + +// TODO need to centralize this logic +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/cmd/cosign/cli/importkeypair/import_key_pair_test.go b/cmd/cosign/cli/importkeypair/import_key_pair_test.go new file mode 100644 index 00000000000..29101df9aa8 --- /dev/null +++ b/cmd/cosign/cli/importkeypair/import_key_pair_test.go @@ -0,0 +1,47 @@ +// +// Copyright 2021 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 importkeypair + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestReadPasswordFn_env(t *testing.T) { + os.Setenv("COSIGN_PASSWORD", "foo") + defer os.Unsetenv("COSIGN_PASSWORD") + b, err := readPasswordFn(true)() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff("foo", string(b)); diff != "" { + t.Fatal(diff) + } +} + +func TestReadPasswordFn_envEmptyVal(t *testing.T) { + os.Setenv("COSIGN_PASSWORD", "") + defer os.Unsetenv("COSIGN_PASSWORD") + b, err := readPasswordFn(true)() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(b) > 0 { + t.Fatalf("expected empty string; got %q", string(b)) + } +} diff --git a/cmd/cosign/cli/options/import_key_pair.go b/cmd/cosign/cli/options/import_key_pair.go new file mode 100644 index 00000000000..b4d3ac3d4bf --- /dev/null +++ b/cmd/cosign/cli/options/import_key_pair.go @@ -0,0 +1,34 @@ +// +// Copyright 2021 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" +) + +// ImportKeyPairOptions is the top level wrapper for the import-key-pair command. +type ImportKeyPairOptions struct { + // Local key file generated by external program such as OpenSSL + Key string +} + +var _ Interface = (*ImportKeyPairOptions)(nil) + +// AddFlags implements Interface +func (o *ImportKeyPairOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Key, "key", "", + "import key pair to use for signing") +} diff --git a/doc/cosign.md b/doc/cosign.md index 9e1453f463a..af588dcec5a 100644 --- a/doc/cosign.md +++ b/doc/cosign.md @@ -22,6 +22,7 @@ * [cosign download](cosign_download.md) - Provides utilities for downloading artifacts and attached artifacts in a registry * [cosign generate](cosign_generate.md) - Generates (unsigned) signature payloads from the supplied container image. * [cosign generate-key-pair](cosign_generate-key-pair.md) - Generates a key-pair. +* [cosign import-key-pair](cosign_import-key-pair.md) - Imports a PEM-encoded RSA or EC private key. * [cosign initialize](cosign_initialize.md) - Initializes SigStore root to retrieve trusted certificate and key targets for verification. * [cosign load](cosign_load.md) - Load a signed image on disk to a remote registry * [cosign manifest](cosign_manifest.md) - Provides utilities for discovering images in and performing operations on Kubernetes manifests diff --git a/doc/cosign_import-key-pair.md b/doc/cosign_import-key-pair.md new file mode 100644 index 00000000000..a1ed70e7396 --- /dev/null +++ b/doc/cosign_import-key-pair.md @@ -0,0 +1,44 @@ +## cosign import-key-pair + +Imports a PEM-encoded RSA or EC private key. + +### Synopsis + +Imports a PEM-encoded RSA or EC private key for signing. + +``` +cosign import-key-pair [flags] +``` + +### Examples + +``` + cosign import-key-pair --key openssl.key + + # import PEM-encoded RSA or EC private key and write to import-cosign.key and import-cosign.pub files + cosign import-key-pair --key + +CAVEATS: + This command interactively prompts for a password. You can use + the COSIGN_PASSWORD environment variable to provide one. +``` + +### Options + +``` + -h, --help help for import-key-pair + --key string import key pair to use for signing +``` + +### Options inherited from parent commands + +``` + --azure-container-registry-config string Path to the file containing Azure container registry configuration information. + --output-file string log output to a file + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign](cosign.md) - + diff --git a/pkg/cosign/keys.go b/pkg/cosign/keys.go index 02edfd37327..a4ad1903b6f 100644 --- a/pkg/cosign/keys.go +++ b/pkg/cosign/keys.go @@ -20,10 +20,13 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" _ "crypto/sha256" // for `crypto.SHA256` "crypto/x509" "encoding/pem" "fmt" + "os" + "path/filepath" "github.com/pkg/errors" "github.com/theupdateframework/go-tuf/encrypted" @@ -34,14 +37,20 @@ import ( ) const ( - PrivakeKeyPemType = "ENCRYPTED COSIGN PRIVATE KEY" - - BundleKey = static.BundleAnnotationKey + PrivateKeyPemType = "ENCRYPTED COSIGN PRIVATE KEY" + RSAPrivateKeyPemType = "RSA PRIVATE KEY" + ECPrivateKeyPemType = "EC PRIVATE KEY" + BundleKey = static.BundleAnnotationKey ) type PassFunc func(bool) ([]byte, error) type Keys struct { + private crypto.PrivateKey + public crypto.PublicKey +} + +type KeysBytes struct { PrivateBytes []byte PublicBytes []byte password []byte @@ -51,45 +60,79 @@ func GeneratePrivateKey() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) } -func GenerateKeyPair(pf PassFunc) (*Keys, error) { - priv, err := GeneratePrivateKey() +func ImportKeyPair(keyPath string, pf PassFunc) (*KeysBytes, error) { + kb, err := os.ReadFile(filepath.Clean(keyPath)) if err != nil { return nil, err } - x509Encoded, err := x509.MarshalPKCS8PrivateKey(priv) + p, _ := pem.Decode(kb) + if p == nil { + return nil, fmt.Errorf("invalid pem block") + } + + var pk crypto.Signer + + switch p.Type { + case RSAPrivateKeyPemType: + pk, err = x509.ParsePKCS1PrivateKey(p.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing error") + } + default: + pk, err = x509.ParseECPrivateKey(p.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing error") + } + } + return marshalKeyPair(Keys{pk, pk.Public()}, pf) +} + +func marshalKeyPair(keypair Keys, pf PassFunc) (*KeysBytes, error) { + x509Encoded, err := x509.MarshalPKCS8PrivateKey(keypair.private) if err != nil { return nil, errors.Wrap(err, "x509 encoding private key") } - // Encrypt the private key and store it. + password, err := pf(true) if err != nil { return nil, err } + encBytes, err := encrypted.Encrypt(x509Encoded, password) if err != nil { return nil, err } + // store in PEM format privBytes := pem.EncodeToMemory(&pem.Block{ Bytes: encBytes, - Type: PrivakeKeyPemType, + Type: PrivateKeyPemType, }) // Now do the public key - pubBytes, err := cryptoutils.MarshalPublicKeyToPEM(&priv.PublicKey) + pubBytes, err := cryptoutils.MarshalPublicKeyToPEM(keypair.public) if err != nil { return nil, err } - return &Keys{ + return &KeysBytes{ PrivateBytes: privBytes, PublicBytes: pubBytes, password: password, }, nil } -func (k *Keys) Password() []byte { +func GenerateKeyPair(pf PassFunc) (*KeysBytes, error) { + priv, err := GeneratePrivateKey() + if err != nil { + return nil, err + } + + return marshalKeyPair(Keys{priv, priv.Public()}, pf) +} + +func (k *KeysBytes) Password() []byte { return k.password } @@ -105,13 +148,13 @@ func PemToECDSAKey(pemBytes []byte) (*ecdsa.PublicKey, error) { return ecdsaPub, nil } -func LoadECDSAPrivateKey(key []byte, pass []byte) (*signature.ECDSASignerVerifier, error) { +func LoadPrivateKey(key []byte, pass []byte) (signature.SignerVerifier, error) { // Decrypt first p, _ := pem.Decode(key) if p == nil { return nil, errors.New("invalid pem block") } - if p.Type != PrivakeKeyPemType { + if p.Type != PrivateKeyPemType { return nil, fmt.Errorf("unsupported pem type: %s", p.Type) } @@ -124,9 +167,12 @@ func LoadECDSAPrivateKey(key []byte, pass []byte) (*signature.ECDSASignerVerifie if err != nil { return nil, errors.Wrap(err, "parsing private key") } - epk, ok := pk.(*ecdsa.PrivateKey) - if !ok { - return nil, errors.New("invalid private key") + switch pk := pk.(type) { + case *rsa.PrivateKey: + return signature.LoadRSAPKCS1v15SignerVerifier(pk, crypto.SHA256) + case *ecdsa.PrivateKey: + return signature.LoadECDSASignerVerifier(pk, crypto.SHA256) + default: + return nil, errors.Wrap(err, "unsupported key type") } - return signature.LoadECDSASignerVerifier(epk, crypto.SHA256) } diff --git a/pkg/cosign/keys_test.go b/pkg/cosign/keys_test.go index e85d5e61730..9c7bc136411 100644 --- a/pkg/cosign/keys_test.go +++ b/pkg/cosign/keys_test.go @@ -17,9 +17,94 @@ package cosign import ( "crypto/rand" + "errors" + "os" + "path/filepath" "testing" + + "github.com/stretchr/testify/require" ) +const validrsa = `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAx5piWVlE62NnZ0UzJ8Z6oKiKOC4dbOZ1HsNhIRtqkM+Oq4G+ +25yq6P+0JU/Qvr9veOGEb3R/J9u8JBo+hv2i5X8OtgvP2V2pi6f1s6vK7L0+6uRb +4YTT/UdMshaVf97MgEqbq41Jf/cuvh+3AV0tZ1BpixZg4aXMKpY6HUP69lbsu27o +SUN1myMv7TSgZiV4CYs3l/gkEfpysBptWlcHRuw5RsB+C0RbjRtbJ/5VxmE/vd3M +lafd5t1WSpMb8yf0a84u5NFaXwZ7CweMfXeOddS0yb19ShSuW3PPRadruBM1mq15 +js9GfagPxDS75Imcs+fA62lWvHxEujTGjYHxawIDAQABAoIBAH+sgLwmHa9zJfEo +klAe5NFe/QpydN/ziXbkAnzqzH9URC3wD+TpkWj4JoK3Sw635NWtasjf+3XDV9S/ +9L7j/g5N91r6sziWcJykEsWaXXKQmm4lI6BdFjwsHyLKz1W7bZOiJXDWLu1rbrqu +DqEQuLoc9WXCKrYrFy0maoXNtfla/1p05kKN0bMigcnnyAQ+xBTwoyco4tkIz5se +IYxorz7qzXrkHQI+knz5BawmNe3ekoSaXUPoLoOR7TRTGsLteL5yukvWAi8S/0rE +gftC+PZCQpoQhSUYq7wXe7RowJ1f+kXb7HsSedOTfTSW1D/pUb/uW+CcRKig42ZI +I9H9TAECgYEA5XGBML6fJyWVqx64sHbUAjQsmQ0RwU6Zo7sqHIEPf6tYVYp7KtzK +KOfi8seOOL5FSy4pjCo11Dzyrh9bn45RNmtjSYTgOnVPSoCfuRNfOcpG+/wCHjYf +EjDvdrCpbg59kVUeaMeBDiyWAlM48HJAn8O7ez2U/iKQCyJmOIwFhSkCgYEA3rSz +Fi1NzqYWxWos4NBmg8iKcQ9SMkmPdgRLAs/WNnZJ8fdgJZwihevkXGytRGJEmav2 +GMKRx1g6ey8fjXTQH9WM8X/kJC5fv8wLHnUCH/K3Mcp9CYwn7PFvSnBr4kQoc/el +bURhcF1+/opEC8vNX/Wk3zAG7Xs1PREXlH2SIHMCgYBV/3kgwBH/JkM25EjtO1yz +hsLAivmAruk/SUO7c1RP0fVF+qW3pxHOyztxLALOmeJ3D1JbSubqKf377Zz17O3b +q9yHDdrNjnKtxhAX2n7ytjJs+EQC9t4mf1kB761RpvTBqFnBhCWHHocLUA4jcW9v +cnmu86IIrwO2aKpPv4vCIQKBgHU9gY3qOazRSOmSlJ+hdmZn+2G7pBTvHsQNTIPl +cCrpqNHl3crO4GnKHkT9vVVjuiOAIKU2QNJFwzu4Og8Y8LvhizpTjoHxm9x3iV72 +UDELcJ+YrqyJCTe2flUcy96o7Pbn50GXnwgtYD6WAW6IUszyn2ITgYIhu4wzZEt6 +s6O7AoGAPTKbRA87L34LMlXyUBJma+etMARIP1zu8bXJ7hSJeMcog8zaLczN7ruT +pGAaLxggvtvuncMuTrG+cdmsR9SafSFKRS92NCxhOUonQ+NP6mLskIGzJZoQ5JvQ +qGzRVIDGbNkrVHM0IsAtHRpC0rYrtZY+9OwiraGcsqUMLwwQdCA= +-----END RSA PRIVATE KEY-----` + +const invalidrsa = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5piWVlE62NnZ0UzJ8Z6 +oKiKOC4dbOZ1HsNhIRtqkM+Oq4G+25yq6P+0JU/Qvr9veOGEb3R/J9u8JBo+hv2i +5X8OtgvP2V2pi6f1s6vK7L0+6uRb4YTT/UdMshaVf97MgEqbq41Jf/cuvh+3AV0t +Z1BpixZg4aXMKpY6HUP69lbsu27oSUN1myMv7TSgZiV4CYs3l/gkEfpysBptWlcH +Ruw5RsB+C0RbjRtbJ/5VxmE/vd3Mlafd5t1WSpMb8yf0a84u5NFaXwZ7CweMfXeO +ddS0yb19ShSuW3PPRadruBM1mq15js9GfagPxDS75Imcs+fA62lWvHxEujTGjYHx +awIDAQAB +-----END PUBLIC KEY-----` + +const invalidkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: BCPG C# v1.6.1.0 + +lQOsBGGTOVkBCACbhVqCN55SElw1rZxI9LQDf91sU5FmrSybGh5r1xGV8rOhpKe+ +eGirYVY3KeI6XUdZoJEIRtXtd6IJWn3msFRgO/MwkUQ4CibORSXPjCwHnseJmh5D +axgZbXpzjP90fW03R+sBqm2AvrUANaWIKIXk8bWWdK5yUhB7TubIxpOZKg/nLlIE +1j6+XdCWIfo56z0mpJWRASzZRGuncfvkHRz73YfA00FpflQykiUDi6+vDV7KTh49 +7nkivRwyx5JcsAT3W1MCXNjCEXsdmdtNah3mN7SMbzSh3RF+IMaonxT4KM5nmEj/ +wGKJ4xUPtKy7kgIPYP+LMOj7j1qCsndYWILzABEBAAH/AwMC5uUvFLMg8b9gVFGU +B1Ak38tCEBPtON9gSIxg9HX80WyMI8/MdfaisEsnFvy4X3UolhTlFJ9v3aqK1Zc8 +JSkEw7cgY0NmFWDr6k8y8LhLN1ATjnKr9J9jzr8G9XvQfgaFbtcuFOF35ylQdeoL +IKKa8GqrXL75rolg+p/OSw52n/7fb17fDXLNyeGQ0g8wjIVTv+7vuvr9Z0kxfIgG +Y9oGIV/SeJvXjoWZWG3GbpTXx+ktmtwCY+tAlxJUt23OwWRfsnC9rS2DAsnJLlG2 +r3Exfl80MUza1sQ/7u1svcHbFuZZOrJ1S9OjRQAWMsfQHFcav34Yrbb3aFweXLjs +iT9BJOMR4W/nyXvKAnMt/6vHKfO6kbxCtDFstH5qZAKbSceWX1Y6UaGimHXCnTYi +tlUMRNWlf6fFLdYBrRCh+MpLs5tSLc6NAYaQXTe3dJrjTRyzkxzYxeE/Y6Mii8KR +gF3Fu5OwkJ39jKdWZf17i/LUofgQHzW4ymuDMWcrqX1kZXPjD6WN8c8NmNCGvlsT +n1V6jPGb8tORIn8+CX+mCyJcxLpbG3ke90DIPnMol7WJ+3xV7J9peJqp0fY4jkmF +I96EUhY1HTZcy4SnhiPwKb8NDpdqwFx1qwytf7eM+65Cf+rj9Nh6ShVOjIfOT9gh +zEp0W0SFTU7p5af9ULnONCJABvRB8Gneosc6iwVclgHhTJcUzILRqNjcrJQu1j1v +oK9Ls+VANww4zEOqx8g+T/P4pHmGTIYTDErzyDmBw8aFD7fDl+kPUtanqC1oTvnJ +qZvoJ3JJ9Z2edW7Ulc1+BhnB8Cfs/jEJQHCngciUjW8yLUcVKdmFKkd9cajhoeQz +bJp6/t9dRUVXo2ulZzvdN93TWV66rTxHQAI4OBZKqbQLYm9iQGJvYi5jb22JARwE +EAECAAYFAmGTOVkACgkQSL3lExF3kQq7swf+Ndhd9iogkofT9ihMuMksoSDituN+ +7hZY5zCj0UzCS8o2vHU+XJCRCpRlOc7gxl+44S60YmwqKtTjT5eqXCXrqa1XTyZz +xYpRfRjwnS6coQbdREQUvIgKapHII+b5gmnNhVX8niO11KyUHc29RWRiLFvMMcYO +urG06WshDewpqrBdq0MYBSSWO7myQLR5xEW6ld6CKkU0153LHgVdlGVIzrLM7sRo +NoHsidPbBIYv+aQxSVHxdKpFEpCHi9vckLSew+8LG5sDA/X3G4l9P3c1KusXP248 +hfOiWo/4tMCN8XJpe0L+99ubcnHjQR7C8htFB4DnIA8KhMBSDdF/Vgp97g== +=8+cN +-----END PGP PRIVATE KEY BLOCK-----` + +const validec = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIGhcmCI5F7BPMH4r3pWCpQdAsveErdU5DjvVQerErJuoAoGCCqGSM49 +AwEHoUQDQgAE+9E3Qe+h25ofmz3Uo2T004Dfy49iX06MMbxf9rsGmLkOPrS0KYDl +1QMfFuSbrtf8wTWNT9HNxrW/Foz39mDhHw== +-----END EC PRIVATE KEY-----` + +const ed25519key = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIALEbo1EFnWFqBK/wC+hhypG/8hXEerwdNetAoFoFVdv +-----END PRIVATE KEY-----` + func pass(s string) PassFunc { return func(_ bool) ([]byte, error) { return []byte(s), nil @@ -34,12 +119,12 @@ func TestLoadECDSAPrivateKey(t *testing.T) { } // Load the private key with the right password - if _, err := LoadECDSAPrivateKey(keys.PrivateBytes, []byte("hello")); err != nil { + if _, err := LoadPrivateKey(keys.PrivateBytes, []byte("hello")); err != nil { t.Errorf("unexpected error decrypting key: %s", err) } // Try it with the wrong one - if _, err := LoadECDSAPrivateKey(keys.PrivateBytes, []byte("wrong")); err == nil { + if _, err := LoadPrivateKey(keys.PrivateBytes, []byte("wrong")); err == nil { t.Error("expected error decrypting key!") } @@ -48,7 +133,54 @@ func TestLoadECDSAPrivateKey(t *testing.T) { if _, err := rand.Read(buf[:]); err != nil { t.Fatal(err) } - if _, err := LoadECDSAPrivateKey(buf[:], []byte("wrong")); err == nil { + if _, err := LoadPrivateKey(buf[:], []byte("wrong")); err == nil { t.Error("expected error decrypting key!") } } + +func TestImportPrivateKey(t *testing.T) { + testCases := []struct { + fileName string + pemData string + expected error + }{ + { + fileName: "validrsa.key", + pemData: validrsa, + expected: nil, + }, + { + fileName: "invalidrsa.key", + pemData: invalidrsa, + expected: errors.New("parsing error"), + }, + { + fileName: "invalidkey.key", + pemData: invalidkey, + expected: errors.New("invalid pem block"), + }, + { + fileName: "validec.key", + pemData: validec, + expected: nil, + }, + { + fileName: "ed25519.key", + pemData: ed25519key, + expected: errors.New("parsing error"), + }, + } + td := t.TempDir() + + for _, tc := range testCases { + t.Run(tc.fileName, func(t *testing.T) { + f := filepath.Join(td, tc.fileName) + err := os.WriteFile(f, []byte(tc.pemData), 0600) + if err != nil { + t.Fatal(err) + } + _, err = ImportKeyPair(f, pass("hello")) + require.Equal(t, tc.expected, err) + }) + } +} diff --git a/pkg/cosign/kubernetes/secret.go b/pkg/cosign/kubernetes/secret.go index 5048795cdad..385846fed8e 100644 --- a/pkg/cosign/kubernetes/secret.go +++ b/pkg/cosign/kubernetes/secret.go @@ -102,7 +102,7 @@ func KeyPairSecret(ctx context.Context, k8sRef string, pf cosign.PassFunc) error // * cosign.key // * cosign.pub // * cosign.password -func secret(keys *cosign.Keys, namespace, name string, data map[string][]byte, immutable bool) *v1.Secret { +func secret(keys *cosign.KeysBytes, namespace, name string, data map[string][]byte, immutable bool) *v1.Secret { if data == nil { data = map[string][]byte{} } diff --git a/pkg/cosign/kubernetes/secret_test.go b/pkg/cosign/kubernetes/secret_test.go index 57b3f4e37a7..9ed86be71c2 100644 --- a/pkg/cosign/kubernetes/secret_test.go +++ b/pkg/cosign/kubernetes/secret_test.go @@ -27,7 +27,7 @@ import ( ) func TestSecret(t *testing.T) { - keys := &cosign.Keys{ + keys := &cosign.KeysBytes{ PrivateBytes: []byte("private"), PublicBytes: []byte("public"), } @@ -52,7 +52,7 @@ func TestSecret(t *testing.T) { } func TestSecretUpdate(t *testing.T) { - keys := &cosign.Keys{ + keys := &cosign.KeysBytes{ PrivateBytes: []byte("private"), PublicBytes: []byte("public"), } diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 6f88036b0dc..2d071686896 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -65,7 +65,7 @@ func VerifierForKeyRef(ctx context.Context, keyRef string, hashAlgorithm crypto. return signature.LoadVerifier(pubKey, hashAlgorithm) } -func loadKey(keyPath string, pf cosign.PassFunc) (*signature.ECDSASignerVerifier, error) { +func loadKey(keyPath string, pf cosign.PassFunc) (signature.SignerVerifier, error) { kb, err := os.ReadFile(filepath.Clean(keyPath)) if err != nil { return nil, err @@ -74,7 +74,7 @@ func loadKey(keyPath string, pf cosign.PassFunc) (*signature.ECDSASignerVerifier if err != nil { return nil, err } - return cosign.LoadECDSAPrivateKey(kb, pass) + return cosign.LoadPrivateKey(kb, pass) } func loadPublicKey(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, error) { @@ -119,7 +119,7 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass } if len(s.Data) > 0 { - return cosign.LoadECDSAPrivateKey(s.Data["cosign.key"], s.Data["cosign.password"]) + return cosign.LoadPrivateKey(s.Data["cosign.key"], s.Data["cosign.password"]) } case strings.HasPrefix(keyRef, gitlab.ReferenceScheme): split := strings.Split(keyRef, "://") @@ -140,7 +140,7 @@ func SignerVerifierFromKeyRef(ctx context.Context, keyRef string, pf cosign.Pass return nil, err } - return cosign.LoadECDSAPrivateKey([]byte(pk), []byte(pass)) + return cosign.LoadPrivateKey([]byte(pk), []byte(pass)) } sv, err := kms.Get(ctx, keyRef, crypto.SHA256) diff --git a/test/e2e_test.go b/test/e2e_test.go index 0958832b8b9..7a403c35700 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -172,6 +172,35 @@ func TestSignVerifyClean(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) } +func TestImportSignVerifyClean(t *testing.T) { + + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "cosign-e2e") + + _, _, _ = mkimage(t, imgName) + + _, privKeyPath, pubKeyPath := importKeyPair(t, td) + + ctx := context.Background() + + // Now sign the image + ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", "", false, false, ""), t) + + // Now verify and download should work! + must(verify(pubKeyPath, imgName, true, nil, ""), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + + // Now clean signature from the given image + must(cli.CleanCmd(ctx, options.RegistryOptions{}, imgName), t) + + // It doesn't work + mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) +} + func TestAttestVerify(t *testing.T) { repo, stop := reg(t) defer stop() @@ -318,7 +347,7 @@ func TestGenerateKeyPairEnvVar(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := cosign.LoadECDSAPrivateKey(keys.PrivateBytes, []byte("foo")); err != nil { + if _, err := cosign.LoadPrivateKey(keys.PrivateBytes, []byte("foo")); err != nil { t.Fatal(err) } } @@ -463,7 +492,7 @@ func TestGenerate(t *testing.T) { equals(ss.Optional["foo"], "bar", t) } -func keypair(t *testing.T, td string) (*cosign.Keys, string, string) { +func keypair(t *testing.T, td string) (*cosign.KeysBytes, string, string) { wd, err := os.Getwd() if err != nil { t.Fatal(err) @@ -491,6 +520,70 @@ func keypair(t *testing.T, td string) (*cosign.Keys, string, string) { return keys, privKeyPath, pubKeyPath } +func importKeyPair(t *testing.T, td string) (*cosign.KeysBytes, string, string) { + + const validrsa1 = `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAx5piWVlE62NnZ0UzJ8Z6oKiKOC4dbOZ1HsNhIRtqkM+Oq4G+ +25yq6P+0JU/Qvr9veOGEb3R/J9u8JBo+hv2i5X8OtgvP2V2pi6f1s6vK7L0+6uRb +4YTT/UdMshaVf97MgEqbq41Jf/cuvh+3AV0tZ1BpixZg4aXMKpY6HUP69lbsu27o +SUN1myMv7TSgZiV4CYs3l/gkEfpysBptWlcHRuw5RsB+C0RbjRtbJ/5VxmE/vd3M +lafd5t1WSpMb8yf0a84u5NFaXwZ7CweMfXeOddS0yb19ShSuW3PPRadruBM1mq15 +js9GfagPxDS75Imcs+fA62lWvHxEujTGjYHxawIDAQABAoIBAH+sgLwmHa9zJfEo +klAe5NFe/QpydN/ziXbkAnzqzH9URC3wD+TpkWj4JoK3Sw635NWtasjf+3XDV9S/ +9L7j/g5N91r6sziWcJykEsWaXXKQmm4lI6BdFjwsHyLKz1W7bZOiJXDWLu1rbrqu +DqEQuLoc9WXCKrYrFy0maoXNtfla/1p05kKN0bMigcnnyAQ+xBTwoyco4tkIz5se +IYxorz7qzXrkHQI+knz5BawmNe3ekoSaXUPoLoOR7TRTGsLteL5yukvWAi8S/0rE +gftC+PZCQpoQhSUYq7wXe7RowJ1f+kXb7HsSedOTfTSW1D/pUb/uW+CcRKig42ZI +I9H9TAECgYEA5XGBML6fJyWVqx64sHbUAjQsmQ0RwU6Zo7sqHIEPf6tYVYp7KtzK +KOfi8seOOL5FSy4pjCo11Dzyrh9bn45RNmtjSYTgOnVPSoCfuRNfOcpG+/wCHjYf +EjDvdrCpbg59kVUeaMeBDiyWAlM48HJAn8O7ez2U/iKQCyJmOIwFhSkCgYEA3rSz +Fi1NzqYWxWos4NBmg8iKcQ9SMkmPdgRLAs/WNnZJ8fdgJZwihevkXGytRGJEmav2 +GMKRx1g6ey8fjXTQH9WM8X/kJC5fv8wLHnUCH/K3Mcp9CYwn7PFvSnBr4kQoc/el +bURhcF1+/opEC8vNX/Wk3zAG7Xs1PREXlH2SIHMCgYBV/3kgwBH/JkM25EjtO1yz +hsLAivmAruk/SUO7c1RP0fVF+qW3pxHOyztxLALOmeJ3D1JbSubqKf377Zz17O3b +q9yHDdrNjnKtxhAX2n7ytjJs+EQC9t4mf1kB761RpvTBqFnBhCWHHocLUA4jcW9v +cnmu86IIrwO2aKpPv4vCIQKBgHU9gY3qOazRSOmSlJ+hdmZn+2G7pBTvHsQNTIPl +cCrpqNHl3crO4GnKHkT9vVVjuiOAIKU2QNJFwzu4Og8Y8LvhizpTjoHxm9x3iV72 +UDELcJ+YrqyJCTe2flUcy96o7Pbn50GXnwgtYD6WAW6IUszyn2ITgYIhu4wzZEt6 +s6O7AoGAPTKbRA87L34LMlXyUBJma+etMARIP1zu8bXJ7hSJeMcog8zaLczN7ruT +pGAaLxggvtvuncMuTrG+cdmsR9SafSFKRS92NCxhOUonQ+NP6mLskIGzJZoQ5JvQ +qGzRVIDGbNkrVHM0IsAtHRpC0rYrtZY+9OwiraGcsqUMLwwQdCA= +-----END RSA PRIVATE KEY-----` + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(td); err != nil { + t.Fatal(err) + } + defer func() { + os.Chdir(wd) + }() + + err = os.WriteFile("validrsa1.key", []byte(validrsa1), 0600) + if err != nil { + t.Fatal(err) + } + + keys, err := cosign.ImportKeyPair("validrsa1.key", passFunc) + if err != nil { + t.Fatal(err) + } + + privKeyPath := filepath.Join(td, "import-cosign.key") + if err := os.WriteFile(privKeyPath, keys.PrivateBytes, 0600); err != nil { + t.Fatal(err) + } + + pubKeyPath := filepath.Join(td, "import-cosign.pub") + if err := os.WriteFile(pubKeyPath, keys.PublicBytes, 0600); err != nil { + t.Fatal(err) + } + return keys, privKeyPath, pubKeyPath + +} + func TestUploadDownload(t *testing.T) { repo, stop := reg(t) defer stop()