193 lines
5.4 KiB
Go
193 lines
5.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
type keySpecs struct {
|
|
Type string
|
|
Bits int
|
|
}
|
|
|
|
func pathIssue(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "issue/" + framework.GenericNameWithAtRegex("role"),
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixSSH,
|
|
OperationVerb: "issue",
|
|
OperationSuffix: "certificate",
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
Callback: b.pathIssue,
|
|
},
|
|
},
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"role": {
|
|
Type: framework.TypeString,
|
|
Description: `The desired role with configuration for this request.`,
|
|
},
|
|
"key_type": {
|
|
Type: framework.TypeString,
|
|
Description: "Specifies the desired key type; must be `rsa`, `ed25519` or `ec`",
|
|
Default: "rsa",
|
|
},
|
|
"key_bits": {
|
|
Type: framework.TypeInt,
|
|
Description: "Specifies the number of bits to use for the generated keys.",
|
|
Default: 0,
|
|
},
|
|
"ttl": {
|
|
Type: framework.TypeDurationSecond,
|
|
Description: `The requested Time To Live for the SSH certificate;
|
|
sets the expiration date. If not specified
|
|
the role default, backend default, or system
|
|
default TTL is used, in that order. Cannot
|
|
be later than the role max TTL.`,
|
|
},
|
|
"valid_principals": {
|
|
Type: framework.TypeString,
|
|
Description: `Valid principals, either usernames or hostnames, that the certificate should be signed for.`,
|
|
},
|
|
"cert_type": {
|
|
Type: framework.TypeString,
|
|
Description: `Type of certificate to be created; either "user" or "host".`,
|
|
Default: "user",
|
|
},
|
|
"key_id": {
|
|
Type: framework.TypeString,
|
|
Description: `Key id that the created certificate should have. If not specified, the display name of the token will be used.`,
|
|
},
|
|
"critical_options": {
|
|
Type: framework.TypeMap,
|
|
Description: `Critical options that the certificate should be signed for.`,
|
|
},
|
|
"extensions": {
|
|
Type: framework.TypeMap,
|
|
Description: `Extensions that the certificate should be signed for.`,
|
|
},
|
|
},
|
|
HelpSynopsis: pathIssueHelpSyn,
|
|
HelpDescription: pathIssueHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathIssue(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
// Get the role
|
|
roleName := data.Get("role").(string)
|
|
role, err := b.getRole(ctx, req.Storage, roleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if role == nil {
|
|
return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", roleName)), nil
|
|
}
|
|
|
|
if role.KeyType != "ca" {
|
|
return logical.ErrorResponse("role key type '%s' not allowed to issue key pairs", role.KeyType), nil
|
|
}
|
|
|
|
// Validate and extract key specifications
|
|
keySpecs, err := extractKeySpecs(role, data)
|
|
if err != nil {
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
}
|
|
|
|
// Issue certificate
|
|
return b.pathIssueCertificate(ctx, req, data, role, keySpecs)
|
|
}
|
|
|
|
func (b *backend) pathIssueCertificate(ctx context.Context, req *logical.Request, data *framework.FieldData, role *sshRole, keySpecs *keySpecs) (*logical.Response, error) {
|
|
publicKey, privateKey, err := generateSSHKeyPair(rand.Reader, keySpecs.Type, keySpecs.Bits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sign key
|
|
userPublicKey, err := parsePublicSSHKey(publicKey)
|
|
if err != nil {
|
|
return logical.ErrorResponse(fmt.Sprintf("failed to parse public_key as SSH key: %s", err)), nil
|
|
}
|
|
|
|
response, err := b.pathSignIssueCertificateHelper(ctx, req, data, role, userPublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if response.IsError() {
|
|
return response, nil
|
|
}
|
|
|
|
// Additional to sign response
|
|
response.Data["private_key"] = privateKey
|
|
response.Data["private_key_type"] = keySpecs.Type
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func extractKeySpecs(role *sshRole, data *framework.FieldData) (*keySpecs, error) {
|
|
keyType := data.Get("key_type").(string)
|
|
keyBits := data.Get("key_bits").(int)
|
|
keySpecs := keySpecs{
|
|
Type: keyType,
|
|
Bits: keyBits,
|
|
}
|
|
|
|
keyTypeToMapKey := createKeyTypeToMapKey(keyType, keyBits)
|
|
|
|
if len(role.AllowedUserKeyTypesLengths) != 0 {
|
|
var keyAllowed bool
|
|
var bitsAllowed bool
|
|
|
|
keyTypeAliasesLoop:
|
|
for _, keyTypeAlias := range keyTypeToMapKey[keyType] {
|
|
allowedValues, allowed := role.AllowedUserKeyTypesLengths[keyTypeAlias]
|
|
if !allowed {
|
|
continue
|
|
}
|
|
keyAllowed = true
|
|
|
|
for _, value := range allowedValues {
|
|
if value == keyBits {
|
|
bitsAllowed = true
|
|
break keyTypeAliasesLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
if !keyAllowed {
|
|
return nil, errors.New("provided key_type value not in allowed_user_key_types")
|
|
}
|
|
|
|
if !bitsAllowed {
|
|
return nil, errors.New("provided key_bits value not in list of role's allowed_user_key_types")
|
|
}
|
|
}
|
|
|
|
return &keySpecs, nil
|
|
}
|
|
|
|
const pathIssueHelpSyn = `
|
|
Request a certificate using a certain role with the provided details.
|
|
`
|
|
|
|
const pathIssueHelpDesc = `
|
|
This path allows requesting a certificate to be issued according to the
|
|
policy of the given role. The certificate will only be issued if the
|
|
requested details are allowed by the role policy.
|
|
|
|
This path returns a certificate and a private key. If you want a workflow
|
|
that does not expose a private key, generate a CSR locally and use the
|
|
sign path instead.
|
|
`
|