From 3da261518ba7c3ea57233baf1a22ef8060d587c8 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Tue, 15 Feb 2022 13:14:05 -0600 Subject: [PATCH] Allow generation of other types of SSH CA keys (#14008) * Add generation support for other SSH CA key types This adds two new arguments to config/ca, mirroring the values of PKI secrets engine but tailored towards SSH mounts. Key types are specified as x/crypto/ssh KeyAlgo identifiers (e.g., ssh-rsa or ssh-ed25519) and respect current defaults (ssh-rsa/4096). Key bits defaults to 0, which for ssh-rsa then takes a value of 4096. Signed-off-by: Alexander Scheel * Add documentation on key_type, key_bits for ssh/config/ca Signed-off-by: Alexander Scheel * Add changelog Signed-off-by: Alexander Scheel --- builtin/logical/ssh/path_config_ca.go | 117 ++++++++++++++++++--- builtin/logical/ssh/path_config_ca_test.go | 71 +++++++++++++ changelog/14008.txt | 3 + website/content/api-docs/secret/ssh.mdx | 15 +++ 4 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 changelog/14008.txt diff --git a/builtin/logical/ssh/path_config_ca.go b/builtin/logical/ssh/path_config_ca.go index 3381a64b5..42ae388a6 100644 --- a/builtin/logical/ssh/path_config_ca.go +++ b/builtin/logical/ssh/path_config_ca.go @@ -2,6 +2,10 @@ package ssh import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -45,6 +49,16 @@ func pathConfigCA(b *backend) *framework.Path { Description: `Generate SSH key pair internally rather than use the private_key and public_key fields.`, Default: true, }, + "key_type": { + Type: framework.TypeString, + Description: `Specifies the desired key type when generating; could be a OpenSSH key type identifier (ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, or ssh-ed25519) or an algorithm (rsa, ec, ed25519).`, + Default: "ssh-rsa", + }, + "key_bits": { + Type: framework.TypeInt, + Description: `Specifies the desired key bits when generating variable-length keys (such as when key_type="ssh-rsa") or which NIST P-curve to use when key_type="ec" (256, 384, or 521).`, + Default: 0, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -191,7 +205,10 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request, } if generateSigningKey { - publicKey, privateKey, err = generateSSHKeyPair(b.Backend.GetRandomReader()) + keyType := data.Get("key_type").(string) + keyBits := data.Get("key_bits").(int) + + publicKey, privateKey, err = generateSSHKeyPair(b.Backend.GetRandomReader(), keyType, keyBits) if err != nil { return nil, err } @@ -265,22 +282,98 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request, return nil, nil } -func generateSSHKeyPair(randomSource io.Reader) (string, string, error) { +func generateSSHKeyPair(randomSource io.Reader, keyType string, keyBits int) (string, string, error) { if randomSource == nil { randomSource = rand.Reader } - privateSeed, err := rsa.GenerateKey(randomSource, 4096) - if err != nil { - return "", "", err + + var publicKey crypto.PublicKey + var privateBlock *pem.Block + + switch keyType { + case ssh.KeyAlgoRSA, "rsa": + if keyBits == 0 { + keyBits = 4096 + } + + if keyBits < 2048 { + return "", "", fmt.Errorf("refusing to generate weak %v key: %v bits < 2048 bits", keyType, keyBits) + } + + privateSeed, err := rsa.GenerateKey(randomSource, keyBits) + if err != nil { + return "", "", err + } + + privateBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(privateSeed), + } + + publicKey = privateSeed.Public() + case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521, "ec": + var curve elliptic.Curve + switch keyType { + case ssh.KeyAlgoECDSA256: + curve = elliptic.P256() + case ssh.KeyAlgoECDSA384: + curve = elliptic.P384() + case ssh.KeyAlgoECDSA521: + curve = elliptic.P521() + default: + switch keyBits { + case 0, 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + return "", "", fmt.Errorf("unknown ECDSA key pair algorithm: %v", keyType) + } + } + + privateSeed, err := ecdsa.GenerateKey(curve, randomSource) + if err != nil { + return "", "", err + } + + marshalled, err := x509.MarshalECPrivateKey(privateSeed) + if err != nil { + return "", "", err + } + + privateBlock = &pem.Block{ + Type: "EC PRIVATE KEY", + Headers: nil, + Bytes: marshalled, + } + + publicKey = privateSeed.Public() + case ssh.KeyAlgoED25519, "ed25519": + _, privateSeed, err := ed25519.GenerateKey(randomSource) + if err != nil { + return "", "", err + } + + marshalled, err := x509.MarshalPKCS8PrivateKey(privateSeed) + if err != nil { + return "", "", err + } + + privateBlock = &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Headers: nil, + Bytes: marshalled, + } + + publicKey = privateSeed.Public() + default: + return "", "", fmt.Errorf("unknown ssh key pair algorithm: %v", keyType) } - privateBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Headers: nil, - Bytes: x509.MarshalPKCS1PrivateKey(privateSeed), - } - - public, err := ssh.NewPublicKey(&privateSeed.PublicKey) + public, err := ssh.NewPublicKey(publicKey) if err != nil { return "", "", err } diff --git a/builtin/logical/ssh/path_config_ca_test.go b/builtin/logical/ssh/path_config_ca_test.go index bd2c967b7..d346c5710 100644 --- a/builtin/logical/ssh/path_config_ca_test.go +++ b/builtin/logical/ssh/path_config_ca_test.go @@ -2,6 +2,7 @@ package ssh import ( "context" + "strings" "testing" "github.com/hashicorp/vault/sdk/logical" @@ -167,4 +168,74 @@ func TestSSH_ConfigCAUpdateDelete(t *testing.T) { if err != nil || (resp != nil && resp.IsError()) { t.Fatalf("bad: err: %v, resp:%v", err, resp) } + + // Delete the configured keys + caReq.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(context.Background(), caReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad: err: %v, resp:%v", err, resp) + } +} + +func createDeleteHelper(t *testing.T, b logical.Backend, config *logical.BackendConfig, index int, keyType string, keyBits int) { + // Check that we can create a new key of the specified type + caReq := &logical.Request{ + Path: "config/ca", + Operation: logical.UpdateOperation, + Storage: config.StorageView, + } + caReq.Data = map[string]interface{}{ + "generate_signing_key": true, + "key_type": keyType, + "key_bits": keyBits, + } + resp, err := b.HandleRequest(context.Background(), caReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad case %v: err: %v, resp:%v", index, err, resp) + } + if !strings.Contains(resp.Data["public_key"].(string), caReq.Data["key_type"].(string)) { + t.Fatalf("bad case %v: expected public key of type %v but was %v", index, caReq.Data["key_type"], resp.Data["public_key"]) + } + + // Delete the configured keys + caReq.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(context.Background(), caReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad case %v: err: %v, resp:%v", index, err, resp) + } +} + +func TestSSH_ConfigCAKeyTypes(t *testing.T) { + var err error + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatalf("Cannot create backend: %s", err) + } + + cases := []struct { + keyType string + keyBits int + }{ + {"ssh-rsa", 2048}, + {"ssh-rsa", 4096}, + {"ssh-rsa", 0}, + {"rsa", 2048}, + {"rsa", 4096}, + {"ecdsa-sha2-nistp256", 0}, + {"ecdsa-sha2-nistp384", 0}, + {"ecdsa-sha2-nistp521", 0}, + {"ec", 256}, + {"ec", 384}, + {"ec", 521}, + {"ec", 0}, + {"ssh-ed25519", 0}, + {"ed25519", 0}, + } + + for index, scenario := range cases { + createDeleteHelper(t, b, config, index, scenario.keyType, scenario.keyBits) + } } diff --git a/changelog/14008.txt b/changelog/14008.txt new file mode 100644 index 000000000..624ba6f69 --- /dev/null +++ b/changelog/14008.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/ssh: Add support for generating non-RSA SSH CAs +``` diff --git a/website/content/api-docs/secret/ssh.mdx b/website/content/api-docs/secret/ssh.mdx index 6d7c34c29..5e0f77b6c 100644 --- a/website/content/api-docs/secret/ssh.mdx +++ b/website/content/api-docs/secret/ssh.mdx @@ -649,6 +649,21 @@ overridden._ If `false`, then you must provide `private_key` and `public_key`, but these can be of any valid signing key type. +- `key_type` `(string: ssh-rsa)` - Specifies the desired key type for the + generated SSH CA key when `generate_signing_key` is set to `true`. Valid + values are OpenSSH key type identifiers (`ssh-rsa`, `ecdsa-sha2-nistp256`, + `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, or `ssh-ed25519`) or an + algorithm (`rsa`, `ec`, or `ed25519`). + +- `key_bits` `(int: 0)` - Specifies the desired key bits for the generated SSH + CA key when `generate_signing_key` is set to `true`. This is only used for + variable length keys (such as `ssh-rsa`, where the value of `key_bits` + specifies the size of the RSA key pair to generate; with the default `0` + value resulting in a 4096-bit key) or when the `ec` algorithm is specified + in `key_type` (where the value of `key_bits` identifies which NIST P-curve + to use; `256`, `384`, or `521`, with the default `0` value resulting in a + NIST P-256 key). + ### Sample Payload ```json