Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow generation of other types of SSH CA keys #14008

Merged
merged 3 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 104 additions & 11 deletions builtin/logical/ssh/path_config_ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package ssh

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

privateBlock := &pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(privateSeed),
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)
}

public, err := ssh.NewPublicKey(&privateSeed.PublicKey)
public, err := ssh.NewPublicKey(publicKey)
if err != nil {
return "", "", err
}
Expand Down
71 changes: 71 additions & 0 deletions builtin/logical/ssh/path_config_ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ssh

import (
"context"
"strings"
"testing"

"github.com/hashicorp/vault/sdk/logical"
Expand Down Expand Up @@ -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)
}
}
3 changes: 3 additions & 0 deletions changelog/14008.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/ssh: Add support for generating non-RSA SSH CAs
```
15 changes: 15 additions & 0 deletions website/content/api-docs/secret/ssh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down