open-vault/vault/wrapping.go

475 lines
14 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2017-01-04 21:44:03 +00:00
package vault
import (
"context"
2017-01-04 21:44:03 +00:00
"crypto/ecdsa"
"crypto/elliptic"
"encoding/json"
"errors"
2017-01-04 21:44:03 +00:00
"fmt"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/logical"
"gopkg.in/square/go-jose.v2"
squarejwt "gopkg.in/square/go-jose.v2/jwt"
2017-01-04 21:44:03 +00:00
)
const (
// The location of the key used to generate response-wrapping JWTs
coreWrappingJWTKeyPath = "core/wrapping/jwtkey"
)
func (c *Core) ensureWrappingKey(ctx context.Context) error {
entry, err := c.barrier.Get(ctx, coreWrappingJWTKeyPath)
2017-01-04 21:44:03 +00:00
if err != nil {
return err
}
var keyParams certutil.ClusterKeyParams
2017-01-04 21:44:03 +00:00
if entry == nil {
key, err := ecdsa.GenerateKey(elliptic.P521(), c.secureRandomReader)
2017-01-04 21:44:03 +00:00
if err != nil {
return fmt.Errorf("failed to generate wrapping key: %w", err)
2017-01-04 21:44:03 +00:00
}
keyParams.D = key.D
keyParams.X = key.X
keyParams.Y = key.Y
keyParams.Type = corePrivateKeyTypeP521
val, err := jsonutil.EncodeJSON(keyParams)
if err != nil {
return fmt.Errorf("failed to encode wrapping key: %w", err)
2017-01-04 21:44:03 +00:00
}
entry = &logical.StorageEntry{
2017-01-04 21:44:03 +00:00
Key: coreWrappingJWTKeyPath,
Value: val,
}
if err = c.barrier.Put(ctx, entry); err != nil {
return fmt.Errorf("failed to store wrapping key: %w", err)
2017-01-04 21:44:03 +00:00
}
}
// Redundant if we just created it, but in this case serves as a check anyways
if err = jsonutil.DecodeJSON(entry.Value, &keyParams); err != nil {
return fmt.Errorf("failed to decode wrapping key parameters: %w", err)
2017-01-04 21:44:03 +00:00
}
c.wrappingJWTKey = &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: elliptic.P521(),
X: keyParams.X,
Y: keyParams.Y,
},
D: keyParams.D,
}
c.logger.Info("loaded wrapping token key")
2017-01-04 21:44:03 +00:00
return nil
}
// wrapInCubbyhole is invoked when a caller asks for response wrapping.
// On success, return (nil, nil) and mutates resp. On failure, returns
// either a response describing the failure or an error.
func (c *Core) wrapInCubbyhole(ctx context.Context, req *logical.Request, resp *logical.Response, auth *logical.Auth) (*logical.Response, error) {
2018-09-18 03:03:00 +00:00
if c.perfStandby {
return forwardWrapRequest(ctx, c, req, resp, auth)
}
2017-01-04 21:44:03 +00:00
// Before wrapping, obey special rules for listing: if no entries are
// found, 404. This prevents unwrapping only to find empty data.
if req.Operation == logical.ListOperation {
if resp == nil || (len(resp.Data) == 0 && len(resp.Warnings) == 0) {
2017-01-04 21:44:03 +00:00
return nil, logical.ErrUnsupportedPath
}
2017-01-04 21:44:03 +00:00
keysRaw, ok := resp.Data["keys"]
if !ok || keysRaw == nil {
if len(resp.Data) > 0 || len(resp.Warnings) > 0 {
// We could be returning extra metadata on a list, or returning
// warnings with no data, so handle these cases
goto DONELISTHANDLING
}
2017-01-04 21:44:03 +00:00
return nil, logical.ErrUnsupportedPath
}
2017-01-04 21:44:03 +00:00
keys, ok := keysRaw.([]string)
if !ok {
return nil, logical.ErrUnsupportedPath
}
if len(keys) == 0 {
return nil, logical.ErrUnsupportedPath
}
}
DONELISTHANDLING:
2017-01-04 21:44:03 +00:00
var err error
sealWrap := resp.WrapInfo.SealWrap
2017-01-04 21:44:03 +00:00
2018-09-18 03:03:00 +00:00
var ns *namespace.Namespace
// If we are creating a JWT wrapping token we always want them to live in
// the root namespace. These are only used for replication and plugin setup.
switch resp.WrapInfo.Format {
case "jwt":
ns = namespace.RootNamespace
ctx = namespace.ContextWithNamespace(ctx, ns)
default:
ns, err = namespace.FromContext(ctx)
if err != nil {
return nil, err
}
}
2017-01-04 21:44:03 +00:00
// If we are wrapping, the first part (performed in this functions) happens
// before auditing so that resp.WrapInfo.Token can contain the HMAC'd
// wrapping token ID in the audit logs, so that it can be determined from
// the audit logs whether the token was ever actually used.
creationTime := time.Now()
te := logical.TokenEntry{
2017-01-04 21:44:03 +00:00
Path: req.Path,
Policies: []string{"response-wrapping"},
CreationTime: creationTime.Unix(),
TTL: resp.WrapInfo.TTL,
NumUses: 1,
ExplicitMaxTTL: resp.WrapInfo.TTL,
2018-09-18 03:03:00 +00:00
NamespaceID: ns.ID,
2017-01-04 21:44:03 +00:00
}
feature: secrets/auth plugin multiplexing (#14946) * enable registering backend muxed plugins in plugin catalog * set the sysview on the pluginconfig to allow enabling secrets/auth plugins * store backend instances in map * store single implementations in the instances map cleanup instance map and ensure we don't deadlock * fix system backend unit tests move GetMultiplexIDFromContext to pluginutil package fix pluginutil test fix dbplugin ut * return error(s) if we can't get the plugin client update comments * refactor/move GetMultiplexIDFromContext test * add changelog * remove unnecessary field on pluginClient * add unit tests to PluginCatalog for secrets/auth plugins * fix comment * return pluginClient from TestRunTestPlugin * add multiplexed backend test * honor metadatamode value in newbackend pluginconfig * check that connection exists on cleanup * add automtls to secrets/auth plugins * don't remove apiclientmeta parsing * use formatting directive for fmt.Errorf * fix ut: remove tls provider func * remove tlsproviderfunc from backend plugin tests * use env var to prevent test plugin from running as a unit test * WIP: remove lazy loading * move non lazy loaded backend to new package * use version wrapper for backend plugin factory * remove backendVersionWrapper type * implement getBackendPluginType for plugin catalog * handle backend plugin v4 registration * add plugin automtls env guard * modify plugin factory to determine the backend to use * remove old pluginsets from v5 and log pid in plugin catalog * add reload mechanism via context * readd v3 and v4 to pluginset * call cleanup from reload if non-muxed * move v5 backend code to new package * use context reload for for ErrPluginShutdown case * add wrapper on v5 backend * fix run config UTs * fix unit tests - use v4/v5 mapping for plugin versions - fix test build err - add reload method on fakePluginClient - add multiplexed cases for integration tests * remove comment and update AutoMTLS field in test * remove comment * remove errwrap and unused context * only support metadatamode false for v5 backend plugins * update plugin catalog errors * use const for env variables * rename locks and remove unused * remove unneeded nil check * improvements based on staticcheck recommendations * use const for single implementation string * use const for context key * use info default log level * move pid to pluginClient struct * remove v3 and v4 from multiplexed plugin set * return from reload when non-multiplexed * update automtls env string * combine getBackend and getBrokeredClient * update comments for plugin reload, Backend return val and log * revert Backend return type * allow non-muxed plugins to serve v5 * move v5 code to existing sdk plugin package * do next export sdk fields now that we have removed extra plugin pkg * set TLSProvider in ServeMultiplex for backwards compat * use bool to flag multiplexing support on grpc backend server * revert userpass main.go * refactor plugin sdk - update comments - make use of multiplexing boolean and single implementation ID const * update comment and use multierr * attempt v4 if dispense fails on getPluginTypeForUnknown * update comments on sdk plugin backend
2022-08-30 02:42:26 +00:00
if err := c.CreateToken(ctx, &te); err != nil {
c.logger.Error("failed to create wrapping token", "error", err)
2017-01-04 21:44:03 +00:00
return nil, ErrInternalError
}
// Count the successful token creation
ttl_label := metricsutil.TTLBucket(resp.WrapInfo.TTL)
mountPointWithoutNs := ns.TrimmedPath(req.MountPoint)
c.metricSink.IncrCounterWithLabels(
[]string{"token", "creation"},
1,
[]metrics.Label{
metricsutil.NamespaceLabel(ns),
// The type of the secret engine is not all that useful;
// we could use "token" but let's be more descriptive,
// even if it's not a real auth method.
{"auth_method", "response_wrapping"},
{"mount_point", mountPointWithoutNs},
{"creation_ttl", ttl_label},
// *Should* be service, but let's use whatever create() did..
{"token_type", te.Type.String()},
},
)
resp.WrapInfo.Token = te.ExternalID
2017-11-13 20:31:32 +00:00
resp.WrapInfo.Accessor = te.Accessor
2017-01-04 21:44:03 +00:00
resp.WrapInfo.CreationTime = creationTime
// If this is not a rewrap, store the request path as creation_path
if req.Path != "sys/wrapping/rewrap" {
resp.WrapInfo.CreationPath = req.Path
}
2017-01-04 21:44:03 +00:00
if auth != nil && auth.EntityID != "" {
resp.WrapInfo.WrappedEntityID = auth.EntityID
}
2017-01-04 21:44:03 +00:00
// This will only be non-nil if this response contains a token, so in that
// case put the accessor in the wrap info.
if resp.Auth != nil {
resp.WrapInfo.WrappedAccessor = resp.Auth.Accessor
}
// Store the accessor of the approle secret in WrappedAccessor
if secretIdAccessor, ok := resp.Data["secret_id_accessor"]; ok && resp.Auth == nil && req.MountType == "approle" {
resp.WrapInfo.WrappedAccessor = secretIdAccessor.(string)
}
2017-01-04 21:44:03 +00:00
switch resp.WrapInfo.Format {
case "jwt":
// Create the JWT
claims := squarejwt.Claims{
// Map the JWT ID to the token ID for ease of use
ID: te.ID,
// Set the issue time to the creation time
IssuedAt: squarejwt.NewNumericDate(creationTime),
// Set the expiration to the TTL
Expiry: squarejwt.NewNumericDate(creationTime.Add(resp.WrapInfo.TTL)),
// Set a reasonable not-before time; since unwrapping happens on this
// node we shouldn't have to worry much about drift
NotBefore: squarejwt.NewNumericDate(time.Now().Add(-5 * time.Second)),
}
type privateClaims struct {
Accessor string `json:"accessor"`
Type string `json:"type"`
Addr string `json:"addr"`
}
priClaims := &privateClaims{
Type: "wrapping",
Addr: c.redirectAddr,
}
2017-01-04 21:44:03 +00:00
if resp.Auth != nil {
priClaims.Accessor = resp.Auth.Accessor
2017-01-04 21:44:03 +00:00
}
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES512, Key: c.wrappingJWTKey},
(&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to create JWT builder", "error", err)
return nil, ErrInternalError
}
ser, err := squarejwt.Signed(sig).Claims(claims).Claims(priClaims).CompactSerialize()
2017-01-04 21:44:03 +00:00
if err != nil {
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to serialize JWT", "error", err)
2017-01-04 21:44:03 +00:00
return nil, ErrInternalError
}
resp.WrapInfo.Token = ser
2017-10-23 16:50:34 +00:00
if c.redirectAddr == "" {
resp.AddWarning("No redirect address set in Vault so none could be encoded in the token. You may need to supply Vault's API address when unwrapping the token.")
}
2017-01-04 21:44:03 +00:00
}
cubbyReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "cubbyhole/response",
ClientToken: te.ID,
}
if sealWrap {
cubbyReq.WrapInfo = &logical.RequestWrapInfo{
SealWrap: true,
}
}
2018-09-18 03:03:00 +00:00
cubbyReq.SetTokenEntry(&te)
2017-01-04 21:44:03 +00:00
// During a rewrap, store the original response, don't wrap it again.
if req.Path == "sys/wrapping/rewrap" {
cubbyReq.Data = map[string]interface{}{
"response": resp.Data["response"],
}
} else {
httpResponse := logical.LogicalResponseToHTTPResponse(resp)
// Add the unique identifier of the original request to the response
httpResponse.RequestID = req.ID
// Because of the way that JSON encodes (likely just in Go) we actually get
// mixed-up values for ints if we simply put this object in the response
// and encode the whole thing; so instead we marshal it first, then store
// the string response. This actually ends up making it easier on the
// client side, too, as it becomes a straight read-string-pass-to-unmarshal
// operation.
marshaledResponse, err := json.Marshal(httpResponse)
if err != nil {
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to marshal wrapped response", "error", err)
2017-01-04 21:44:03 +00:00
return nil, ErrInternalError
}
cubbyReq.Data = map[string]interface{}{
"response": string(marshaledResponse),
}
}
cubbyResp, err := c.router.Route(ctx, cubbyReq)
2017-01-04 21:44:03 +00:00
if err != nil {
// Revoke since it's not yet being tracked for expiration
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to store wrapped response information", "error", err)
2017-01-04 21:44:03 +00:00
return nil, ErrInternalError
}
if cubbyResp != nil && cubbyResp.IsError() {
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to store wrapped response information", "error", cubbyResp.Data["error"])
2017-01-04 21:44:03 +00:00
return cubbyResp, nil
}
// Store info for lookup
cubbyReq.WrapInfo = nil
2017-01-04 21:44:03 +00:00
cubbyReq.Path = "cubbyhole/wrapinfo"
cubbyReq.Data = map[string]interface{}{
"creation_ttl": resp.WrapInfo.TTL,
"creation_time": creationTime,
}
// Store creation_path if not a rewrap
if req.Path != "sys/wrapping/rewrap" {
cubbyReq.Data["creation_path"] = req.Path
} else {
cubbyReq.Data["creation_path"] = resp.WrapInfo.CreationPath
}
cubbyResp, err = c.router.Route(ctx, cubbyReq)
2017-01-04 21:44:03 +00:00
if err != nil {
// Revoke since it's not yet being tracked for expiration
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to store wrapping information", "error", err)
2017-01-04 21:44:03 +00:00
return nil, ErrInternalError
}
if cubbyResp != nil && cubbyResp.IsError() {
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to store wrapping information", "error", cubbyResp.Data["error"])
2017-01-04 21:44:03 +00:00
return cubbyResp, nil
}
wAuth := &logical.Auth{
2017-01-04 21:44:03 +00:00
ClientToken: te.ID,
Policies: []string{"response-wrapping"},
LeaseOptions: logical.LeaseOptions{
TTL: te.TTL,
Renewable: false,
},
}
// Register the wrapped token with the expiration manager
if err := c.expiration.RegisterAuth(ctx, &te, wAuth, c.DetermineRoleFromLoginRequest(req.MountPoint, req.Data, ctx)); err != nil {
2017-01-04 21:44:03 +00:00
// Revoke since it's not yet being tracked for expiration
c.tokenStore.revokeOrphan(ctx, te.ID)
c.logger.Error("failed to register cubbyhole wrapping token lease", "request_path", req.Path, "error", err)
2017-01-04 21:44:03 +00:00
return nil, ErrInternalError
}
return nil, nil
}
// validateWrappingToken checks whether a token is a wrapping token. The passed
// in logical request will be updated if the wrapping token was provided within
// a JWT token.
func (c *Core) validateWrappingToken(ctx context.Context, req *logical.Request) (valid bool, err error) {
if req == nil {
return false, fmt.Errorf("invalid request")
}
if c.Sealed() {
return false, consts.ErrSealed
}
if c.standby && !c.perfStandby {
return false, consts.ErrStandby
}
defer func() {
// Perform audit logging before returning if there's an issue with checking
// the wrapping token
if err != nil || !valid {
// We log the Auth object like so here since the wrapping token can
// come from the header, which gets set as the ClientToken
auth := &logical.Auth{
ClientToken: req.ClientToken,
Accessor: req.ClientTokenAccessor,
}
logInput := &logical.LogInput{
Auth: auth,
Request: req,
}
if err != nil {
logInput.OuterErr = errors.New("error validating wrapping token")
}
if !valid {
logInput.OuterErr = consts.ErrInvalidWrappingToken
}
if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil {
c.logger.Error("failed to audit request", "path", req.Path, "error", err)
}
}
}()
2017-01-04 21:44:03 +00:00
var token string
var thirdParty bool
// Check if the wrapping token is coming from the request body, and if not
// assume that req.ClientToken is the wrapping token
2017-01-04 21:44:03 +00:00
if req.Data != nil && req.Data["token"] != nil {
thirdParty = true
2017-01-04 21:44:03 +00:00
if tokenStr, ok := req.Data["token"].(string); !ok {
return false, fmt.Errorf("could not decode token in request body")
} else if tokenStr == "" {
return false, fmt.Errorf("empty token in request body")
} else {
token = tokenStr
}
} else {
token = req.ClientToken
}
// Check for it being a JWT. If it is, and it is valid, we extract the
// internal client token from it and use that during lookup. The second
// check is a quick check to verify that we don't consider a namespaced
// token to be a JWT -- namespaced tokens have two dots too, but Vault
// token types (for now at least) begin with a letter representing a type
// and then a dot.
if IsJWT(token) {
// Implement the jose library way
parsedJWT, err := squarejwt.ParseSigned(token)
if err != nil {
return false, fmt.Errorf("wrapping token could not be parsed: %w", err)
}
var claims squarejwt.Claims
allClaims := make(map[string]interface{})
if err = parsedJWT.Claims(&c.wrappingJWTKey.PublicKey, &claims, &allClaims); err != nil {
return false, fmt.Errorf("wrapping token signature could not be validated: %w", err)
}
typeClaimRaw, ok := allClaims["type"]
if !ok {
return false, errors.New("could not validate type claim")
}
typeClaim, ok := typeClaimRaw.(string)
if !ok {
return false, errors.New("could not parse type claim")
}
if typeClaim != "wrapping" {
return false, errors.New("unexpected type claim")
}
if !thirdParty {
req.ClientToken = claims.ID
} else {
req.Data["token"] = claims.ID
2017-01-04 21:44:03 +00:00
}
token = claims.ID
2017-01-04 21:44:03 +00:00
}
if token == "" {
return false, fmt.Errorf("token is empty")
}
2018-09-18 03:03:00 +00:00
te, err := c.tokenStore.Lookup(ctx, token)
2017-01-04 21:44:03 +00:00
if err != nil {
return false, err
}
if te == nil {
return false, nil
}
if !IsWrappingToken(te) {
2017-01-04 21:44:03 +00:00
return false, nil
}
2018-09-18 03:03:00 +00:00
if !thirdParty {
req.ClientTokenAccessor = te.Accessor
req.ClientTokenRemainingUses = te.NumUses
req.SetTokenEntry(te)
}
2017-01-04 21:44:03 +00:00
return true, nil
}
func IsWrappingToken(te *logical.TokenEntry) bool {
if len(te.Policies) != 1 {
return false
}
if te.Policies[0] != responseWrappingPolicyName && te.Policies[0] != controlGroupPolicyName {
return false
}
return true
}