mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-14 18:47:01 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
193 lines
5.4 KiB
Go
193 lines
5.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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.
|
|
`
|