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 <alex.scheel@hashicorp.com>

* Add documentation on key_type, key_bits for ssh/config/ca

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
Alexander Scheel 2022-02-15 13:14:05 -06:00 committed by GitHub
parent e94455e608
commit 3da261518b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 194 additions and 12 deletions

View File

@ -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
}

View File

@ -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)
}
}

3
changelog/14008.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/ssh: Add support for generating non-RSA SSH CAs
```

View File

@ -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