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:
Alexander Scheel 2022-02-18 16:48:16 -06:00 committed by GitHub
parent c8cca2cab5
commit d72fb08884
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 148 additions and 40 deletions

View File

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

View File

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

View File

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