2017-01-04 21:44:03 +00:00
package vault
import (
2018-01-08 18:31:38 +00:00
"context"
2017-01-04 21:44:03 +00:00
"crypto/ecdsa"
"crypto/elliptic"
"encoding/json"
2019-03-20 18:54:03 +00:00
"errors"
2017-01-04 21:44:03 +00:00
"fmt"
"strings"
"time"
2020-06-02 18:40:54 +00:00
"github.com/armon/go-metrics"
2020-10-13 23:38:21 +00:00
"github.com/hashicorp/vault/helper/metricsutil"
2019-04-13 07:44:06 +00:00
"github.com/hashicorp/vault/helper/namespace"
2019-04-12 21:54:35 +00:00
"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"
2019-10-17 17:33:00 +00:00
"gopkg.in/square/go-jose.v2"
2019-03-20 18:54:03 +00:00
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"
)
2018-01-19 06:44:44 +00:00
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
}
2019-02-15 02:14:56 +00:00
var keyParams certutil . ClusterKeyParams
2017-01-04 21:44:03 +00:00
if entry == nil {
2019-10-17 17:33:00 +00:00
key , err := ecdsa . GenerateKey ( elliptic . P521 ( ) , c . secureRandomReader )
2017-01-04 21:44:03 +00:00
if err != nil {
2021-05-11 17:12:54 +00:00
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 {
2021-05-11 17:12:54 +00:00
return fmt . Errorf ( "failed to encode wrapping key: %w" , err )
2017-01-04 21:44:03 +00:00
}
2019-01-31 14:25:18 +00:00
entry = & logical . StorageEntry {
2017-01-04 21:44:03 +00:00
Key : coreWrappingJWTKeyPath ,
Value : val ,
}
2018-01-19 06:44:44 +00:00
if err = c . barrier . Put ( ctx , entry ) ; err != nil {
2021-05-11 17:12:54 +00:00
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 {
2021-05-11 17:12:54 +00:00
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 ,
}
2018-04-03 00:46:59 +00:00
c . logger . Info ( "loaded wrapping token key" )
2017-01-04 21:44:03 +00:00
return nil
}
2021-02-24 11:58:10 +00:00
// 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.
2018-01-08 18:31:38 +00:00
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 {
2018-04-04 02:35:45 +00:00
if resp == nil || ( len ( resp . Data ) == 0 && len ( resp . Warnings ) == 0 ) {
2017-01-04 21:44:03 +00:00
return nil , logical . ErrUnsupportedPath
}
2018-04-04 02:35:45 +00:00
2017-01-04 21:44:03 +00:00
keysRaw , ok := resp . Data [ "keys" ]
if ! ok || keysRaw == nil {
2018-04-04 02:35:45 +00:00
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
}
2018-04-04 02:35:45 +00:00
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
}
}
2018-04-04 02:35:45 +00:00
DONELISTHANDLING :
2017-01-04 21:44:03 +00:00
var err error
2017-11-09 17:47:42 +00:00
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 ( )
2018-06-08 21:24:27 +00:00
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
}
2018-01-19 06:44:44 +00:00
if err := c . tokenStore . create ( ctx , & te ) ; err != nil {
2018-04-03 00:46:59 +00:00
c . logger . Error ( "failed to create wrapping token" , "error" , err )
2017-01-04 21:44:03 +00:00
return nil , ErrInternalError
}
2020-06-02 18:40:54 +00:00
// Count the successful token creation
ttl_label := metricsutil . TTLBucket ( resp . WrapInfo . TTL )
2020-07-14 19:28:11 +00:00
mountPointWithoutNs := ns . TrimmedPath ( req . MountPoint )
2020-06-02 18:40:54 +00:00
c . metricSink . IncrCounterWithLabels (
[ ] string { "token" , "creation" } ,
1 ,
[ ] metrics . Label {
2020-10-13 23:38:21 +00:00
metricsutil . NamespaceLabel ( ns ) ,
2020-06-02 18:40:54 +00:00
// 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" } ,
2020-07-14 19:28:11 +00:00
{ "mount_point" , mountPointWithoutNs } ,
2020-06-02 18:40:54 +00:00
{ "creation_ttl" , ttl_label } ,
// *Should* be service, but let's use whatever create() did..
{ "token_type" , te . Type . String ( ) } ,
} ,
)
2017-01-04 21:44:03 +00:00
resp . WrapInfo . Token = te . ID
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
2017-08-02 22:28:58 +00:00
// 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
2017-10-11 17:21:20 +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
}
switch resp . WrapInfo . Format {
case "jwt" :
// Create the JWT
2019-03-20 18:54:03 +00:00
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 {
2019-03-20 18:54:03 +00:00
priClaims . Accessor = resp . Auth . Accessor
2017-01-04 21:44:03 +00:00
}
2019-03-20 18:54: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 {
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
c . logger . Error ( "failed to serialize JWT" , "error" , err )
2017-01-04 21:44:03 +00:00
return nil , ErrInternalError
}
2019-03-20 18:54:03 +00:00
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 ,
}
2017-11-09 17:47:42 +00:00
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 {
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
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 ) ,
}
}
2018-01-08 18:31:38 +00:00
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
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
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 ( ) {
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
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
2017-11-09 17:47:42 +00:00
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 ,
}
2017-08-02 22:28:58 +00:00
// 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
}
2018-01-08 18:31:38 +00:00
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
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
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 ( ) {
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
c . logger . Error ( "failed to store wrapping information" , "error" , cubbyResp . Data [ "error" ] )
2017-01-04 21:44:03 +00:00
return cubbyResp , nil
}
2017-10-11 17:21:20 +00:00
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
2018-09-18 03:03:00 +00:00
if err := c . expiration . RegisterAuth ( ctx , & te , wAuth ) ; err != nil {
2017-01-04 21:44:03 +00:00
// Revoke since it's not yet being tracked for expiration
2018-05-10 19:50:02 +00:00
c . tokenStore . revokeOrphan ( ctx , te . ID )
2018-04-03 00:46:59 +00:00
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
}
2019-07-05 21:15:14 +00:00
// 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 ) {
2019-07-06 02:31:03 +00:00
if req == nil {
return false , fmt . Errorf ( "invalid request" )
}
if c . Sealed ( ) {
return false , consts . ErrSealed
}
c . stateLock . RLock ( )
defer c . stateLock . RUnlock ( )
if c . standby && ! c . perfStandby {
return false , consts . ErrStandby
}
2019-07-05 21:15:14 +00:00
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
2017-01-05 04:50:24 +00:00
var thirdParty bool
2019-07-05 21:15:14 +00:00
// 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 {
2017-01-05 04:50:24 +00:00
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
2019-04-04 21:13:09 +00:00
// 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 strings . Count ( token , "." ) == 2 && token [ 1 ] != '.' {
2019-03-20 18:54:03 +00:00
// Implement the jose library way
parsedJWT , err := squarejwt . ParseSigned ( token )
if err != nil {
2021-05-11 17:12:54 +00:00
return false , fmt . Errorf ( "wrapping token could not be parsed: %w" , err )
2019-03-20 18:54:03 +00:00
}
var claims squarejwt . Claims
2021-04-08 16:43:39 +00:00
allClaims := make ( map [ string ] interface { } )
2019-03-20 18:54:03 +00:00
if err = parsedJWT . Claims ( & c . wrappingJWTKey . PublicKey , & claims , & allClaims ) ; err != nil {
2021-05-11 17:12:54 +00:00
return false , fmt . Errorf ( "wrapping token signature could not be validated: %w" , err )
2019-03-20 18:54:03 +00:00
}
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
}
2019-07-05 21:15:14 +00:00
2019-03-20 18:54: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 len ( te . Policies ) != 1 {
return false , nil
}
2017-11-13 20:31:32 +00:00
if te . Policies [ 0 ] != responseWrappingPolicyName && te . Policies [ 0 ] != controlGroupPolicyName {
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
}