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:
parent
e94455e608
commit
3da261518b
|
@ -2,6 +2,10 @@ package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"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.`,
|
Description: `Generate SSH key pair internally rather than use the private_key and public_key fields.`,
|
||||||
Default: true,
|
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{
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
@ -191,7 +205,10 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request,
|
||||||
}
|
}
|
||||||
|
|
||||||
if generateSigningKey {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -265,22 +282,98 @@ func (b *backend) pathConfigCAUpdate(ctx context.Context, req *logical.Request,
|
||||||
return nil, nil
|
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 {
|
if randomSource == nil {
|
||||||
randomSource = rand.Reader
|
randomSource = rand.Reader
|
||||||
}
|
}
|
||||||
privateSeed, err := rsa.GenerateKey(randomSource, 4096)
|
|
||||||
if err != nil {
|
var publicKey crypto.PublicKey
|
||||||
return "", "", err
|
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{
|
public, err := ssh.NewPublicKey(publicKey)
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Headers: nil,
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(privateSeed),
|
|
||||||
}
|
|
||||||
|
|
||||||
public, err := ssh.NewPublicKey(&privateSeed.PublicKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
@ -167,4 +168,74 @@ func TestSSH_ConfigCAUpdateDelete(t *testing.T) {
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
if err != nil || (resp != nil && resp.IsError()) {
|
||||||
t.Fatalf("bad: err: %v, resp:%v", err, resp)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
secrets/ssh: Add support for generating non-RSA SSH CAs
|
||||||
|
```
|
|
@ -649,6 +649,21 @@ overridden._
|
||||||
If `false`, then you must provide `private_key` and `public_key`, but these
|
If `false`, then you must provide `private_key` and `public_key`, but these
|
||||||
can be of any valid signing key type.
|
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
|
### Sample Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
Loading…
Reference in New Issue