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 (
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue