Allow OpenSSH-style key type identifiers (#14143)
* Allow OpenSSH-style key type identifiers To bring better parity with the changes of #14008, wherein we allowed OpenSSH-style key identifiers during generation. When specifying a list of allowed keys, validate against both OpenSSH-style key identifiers and the usual simplified names as well ("rsa" or "ecdsa"). Notably, the PKI secrets engine prefers "ec" over "ecdsa", so we permit both as well. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix missing quote in docs
This commit is contained in:
parent
c8cca2cab5
commit
d72fb08884
|
@ -119,6 +119,9 @@ SjOQL/GkH1nkRcDS9++aAAAAAmNhAQID
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
`
|
`
|
||||||
|
|
||||||
|
publicKeyECDSA256 = `ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJsfOouYIjJNI23QJqaDsFTGukm21fRAMeGvKZDB59i5jnX1EubMH1AEjjzz4fgySUlyWKo+TS31rxU8kX3DDM4= demo@example.com`
|
||||||
|
publicKeyECDSA521 = `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAEg73ORD4J3FV2CrL01gLSKREO2EHrZPlJCOeDL5OKD3M1GCHv3q8O452RW49Aw+8zFFFU5u6d1Ys3Qsj05zdaQwQDt/D3ceWLGVkWiKyLPQStfn0GGOZh3YFKEw5XmeW9jh6xudEHlKs4Pfv2FrroaUKZvM2SlxR/feOK0tCQyq3MN/g== demo@example.com`
|
||||||
|
|
||||||
// testPublicKeyInstall is the public key that is installed in the
|
// testPublicKeyInstall is the public key that is installed in the
|
||||||
// admin account's authorized_keys
|
// admin account's authorized_keys
|
||||||
testPublicKeyInstall = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9i+hFxZHGo6KblVme4zrAcJstR6I0PTJozW286X4WyvPnkMYDQ5mnhEYC7UWCvjoTWbPEXPX7NjhRtwQTGD67bV+lrxgfyzK1JZbUXK4PwgKJvQD+XyyWYMzDgGSQY61KUSqCxymSm/9NZkPU3ElaQ9xQuTzPpztM4ROfb8f2Yv6/ZESZsTo0MTAkp8Pcy+WkioI/uJ1H7zqs0EA4OMY4aDJRu0UtP4rTVeYNEAuRXdX+eH4aW3KMvhzpFTjMbaJHJXlEeUm2SaX5TNQyTOvghCeQILfYIL/Ca2ij8iwCmulwdV6eQGfd4VDu40PvSnmfoaE38o6HaPnX0kUcnKiT"
|
testPublicKeyInstall = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9i+hFxZHGo6KblVme4zrAcJstR6I0PTJozW286X4WyvPnkMYDQ5mnhEYC7UWCvjoTWbPEXPX7NjhRtwQTGD67bV+lrxgfyzK1JZbUXK4PwgKJvQD+XyyWYMzDgGSQY61KUSqCxymSm/9NZkPU3ElaQ9xQuTzPpztM4ROfb8f2Yv6/ZESZsTo0MTAkp8Pcy+WkioI/uJ1H7zqs0EA4OMY4aDJRu0UtP4rTVeYNEAuRXdX+eH4aW3KMvhzpFTjMbaJHJXlEeUm2SaX5TNQyTOvghCeQILfYIL/Ca2ij8iwCmulwdV6eQGfd4VDu40PvSnmfoaE38o6HaPnX0kUcnKiT"
|
||||||
|
@ -1307,6 +1310,60 @@ func TestBackend_AllowedUserKeyLengths(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Fail with ECDSA key
|
||||||
|
{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "sign/multikey",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"public_key": publicKeyECDSA256,
|
||||||
|
},
|
||||||
|
ErrorOk: true,
|
||||||
|
Check: func(resp *logical.Response) error {
|
||||||
|
if resp.Data["error"] != "public_key failed to meet the key requirements: key of type ecdsa is not allowed" {
|
||||||
|
return errors.New("an ECDSA key was allowed under RSA-only policy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createRoleStep("ectypes", map[string]interface{}{
|
||||||
|
"key_type": "ca",
|
||||||
|
"allow_user_certificates": true,
|
||||||
|
"allowed_user_key_lengths": map[string]interface{}{
|
||||||
|
"ec": []int{256},
|
||||||
|
"ecdsa-sha2-nistp521": 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Pass with ECDSA P-256
|
||||||
|
{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "sign/ectypes",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"public_key": publicKeyECDSA256,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Pass with ECDSA P-521
|
||||||
|
{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "sign/ectypes",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"public_key": publicKeyECDSA521,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Fail with RSA key
|
||||||
|
{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "sign/ectypes",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"public_key": publicKey3072,
|
||||||
|
},
|
||||||
|
ErrorOk: true,
|
||||||
|
Check: func(resp *logical.Response) error {
|
||||||
|
if resp.Data["error"] != "public_key failed to meet the key requirements: key of type rsa is not allowed" {
|
||||||
|
return errors.New("an RSA key was allowed under ECDSA-only policy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -448,63 +448,100 @@ func (b *backend) calculateTTL(data *framework.FieldData, role *sshRole) (time.D
|
||||||
|
|
||||||
func (b *backend) validateSignedKeyRequirements(publickey ssh.PublicKey, role *sshRole) error {
|
func (b *backend) validateSignedKeyRequirements(publickey ssh.PublicKey, role *sshRole) error {
|
||||||
if len(role.AllowedUserKeyTypesLengths) != 0 {
|
if len(role.AllowedUserKeyTypesLengths) != 0 {
|
||||||
var kstr string
|
var keyType string
|
||||||
var kbits int
|
var keyBits int
|
||||||
|
|
||||||
switch k := publickey.(type) {
|
switch k := publickey.(type) {
|
||||||
case ssh.CryptoPublicKey:
|
case ssh.CryptoPublicKey:
|
||||||
ff := k.CryptoPublicKey()
|
ff := k.CryptoPublicKey()
|
||||||
switch k := ff.(type) {
|
switch k := ff.(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
kstr = "rsa"
|
keyType = "rsa"
|
||||||
kbits = k.N.BitLen()
|
keyBits = k.N.BitLen()
|
||||||
case *dsa.PublicKey:
|
case *dsa.PublicKey:
|
||||||
kstr = "dsa"
|
keyType = "dsa"
|
||||||
kbits = k.Parameters.P.BitLen()
|
keyBits = k.Parameters.P.BitLen()
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
kstr = "ecdsa"
|
keyType = "ecdsa"
|
||||||
kbits = k.Curve.Params().BitSize
|
keyBits = k.Curve.Params().BitSize
|
||||||
case ed25519.PublicKey:
|
case ed25519.PublicKey:
|
||||||
kstr = "ed25519"
|
keyType = "ed25519"
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("public key type of %s is not allowed", kstr)
|
return fmt.Errorf("public key type of %s is not allowed", keyType)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("pubkey not suitable for crypto (expected ssh.CryptoPublicKey but found %T)", k)
|
return fmt.Errorf("pubkey not suitable for crypto (expected ssh.CryptoPublicKey but found %T)", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowed_values, ok := role.AllowedUserKeyTypesLengths[kstr]; ok {
|
keyTypeToMapKey := map[string][]string{
|
||||||
var pass bool
|
"rsa": {"rsa", ssh.KeyAlgoRSA},
|
||||||
for _, value := range allowed_values {
|
"dsa": {"dsa", ssh.KeyAlgoDSA},
|
||||||
switch kstr {
|
"ecdsa": {"ecdsa", "ec"},
|
||||||
case "rsa":
|
"ed25519": {"ed25519", ssh.KeyAlgoED25519},
|
||||||
if kbits == value {
|
}
|
||||||
pass = true
|
|
||||||
break
|
if keyType == "ecdsa" {
|
||||||
}
|
ecCurveBitsToAlgoName := map[int]string{
|
||||||
case "dsa":
|
256: ssh.KeyAlgoECDSA256,
|
||||||
if kbits == value {
|
384: ssh.KeyAlgoECDSA384,
|
||||||
pass = true
|
521: ssh.KeyAlgoECDSA521,
|
||||||
break
|
|
||||||
}
|
|
||||||
case "ecdsa":
|
|
||||||
if kbits == value {
|
|
||||||
pass = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "ed25519":
|
|
||||||
// ed25519 public keys are always 256 bits in length,
|
|
||||||
// so there is no need to inspect their value
|
|
||||||
pass = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pass {
|
if algo, ok := ecCurveBitsToAlgoName[keyBits]; ok {
|
||||||
return fmt.Errorf("key is of an invalid size: %v", kbits)
|
keyTypeToMapKey[keyType] = append(keyTypeToMapKey[keyType], algo)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return fmt.Errorf("key type of %s is not allowed", kstr)
|
// If the algorithm is not found, it could be that we have a curve
|
||||||
|
// that we haven't added a constant for yet. But they could allow it
|
||||||
|
// (assuming x/crypto/ssh can parse it) via setting a ec: <keyBits>
|
||||||
|
// mapping rather than using a named SSH key type, so erring out here
|
||||||
|
// isn't advisable.
|
||||||
|
}
|
||||||
|
|
||||||
|
var present bool
|
||||||
|
var pass bool
|
||||||
|
for _, kstr := range keyTypeToMapKey[keyType] {
|
||||||
|
allowed_values, ok := role.AllowedUserKeyTypesLengths[kstr]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
present = true
|
||||||
|
|
||||||
|
for _, value := range allowed_values {
|
||||||
|
if keyType == "rsa" || keyType == "dsa" {
|
||||||
|
// Regardless of map naming, we always need to validate the
|
||||||
|
// bit length of RSA and DSA keys. Use the keyType flag to
|
||||||
|
if keyBits == value {
|
||||||
|
pass = true
|
||||||
|
}
|
||||||
|
} else if kstr == "ec" || kstr == "ecdsa" {
|
||||||
|
// If the map string is "ecdsa", we have to validate the keyBits
|
||||||
|
// are a match for an allowed value, meaning that our curve
|
||||||
|
// is allowed. This isn't necessary when a named curve (e.g.
|
||||||
|
// ssh.KeyAlgoECDSA256) is allowed (and hence kstr is that),
|
||||||
|
// because keyBits is already specified in the kstr. Thus,
|
||||||
|
// we have conditioned around kstr and not keyType (like with
|
||||||
|
// rsa or dsa).
|
||||||
|
if keyBits == value {
|
||||||
|
pass = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We get here in two cases: we have a algo-named EC key
|
||||||
|
// matching a format specifier in the key map (e.g., a P-256
|
||||||
|
// key with a KeyAlgoECDSA256 entry in the map) or we have a
|
||||||
|
// ed25519 key (which is always allowed).
|
||||||
|
pass = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !present {
|
||||||
|
return fmt.Errorf("key of type %s is not allowed", keyType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pass {
|
||||||
|
return fmt.Errorf("key is of an invalid size: %v", keyBits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -213,7 +213,21 @@ This endpoint creates or updates a named role.
|
||||||
- `allowed_user_key_lengths` `(map<string|(int|[]int|string)>: "")` – Specifies a
|
- `allowed_user_key_lengths` `(map<string|(int|[]int|string)>: "")` – Specifies a
|
||||||
map of ssh key types and their expected sizes which are allowed to be signed by
|
map of ssh key types and their expected sizes which are allowed to be signed by
|
||||||
the CA type. To specify multiple sizes, either use a comma-separated list or an
|
the CA type. To specify multiple sizes, either use a comma-separated list or an
|
||||||
array of allowed key widths.
|
array of allowed key widths. We support both OpenSSH-style key identifiers and
|
||||||
|
short names (`rsa`, `ecdsa`, `dsa`, or `ed25519`) as keys. For example, a valid
|
||||||
|
policy to allow common RSA and ECDSA key lengths might be:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"rsa": [2048, 3072, 4096],
|
||||||
|
"ec": 256,
|
||||||
|
"ecdsa-sha2-nistp521": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when an algorithm identifier uniquely specifies a key length (such as
|
||||||
|
with `ecdsa-sha2-nistp256` or `ed25519`), the value of the length is ignored (and
|
||||||
|
can be zero).
|
||||||
|
|
||||||
- `algorithm_signer` `(string: "default")` - Algorithm to sign keys with. Valid
|
- `algorithm_signer` `(string: "default")` - Algorithm to sign keys with. Valid
|
||||||
values are `ssh-rsa`, `rsa-sha2-256`, `rsa-sha2-512`, or `default`. This
|
values are `ssh-rsa`, `rsa-sha2-256`, `rsa-sha2-512`, or `default`. This
|
||||||
|
|
Loading…
Reference in New Issue