| // 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. |
| ` |