From 74544f6324b1516f67ba72c9e239700572173002 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 25 Aug 2020 00:20:50 -0400 Subject: [PATCH] Encrypt tokens on the way out and decrypt on the way in (#302) * Encrypt tokens on the way out and decrypt on the way in This isn't a security feature related to tokens specifically; it's so that we can discard cryptographically bad tokens before we ever hit the DB. This way we can't pass through a DDoS to the database due to fetching obviously invalid token values. I'm using base58 to encode the resulting value as there aren't great base62 options, and allowing base64 / and + values can hamper usability by creating copying/highlighting breaks. --- globals/globals.go | 4 ++ go.mod | 1 + go.sum | 17 +++++ internal/auth/auth.go | 63 ++++++++++++++++-- internal/auth/auth_test.go | 10 ++- internal/auth/password/account_test.go | 1 - internal/authtoken/authtoken.go | 28 ++++++++ internal/cmd/base/base.go | 64 +++++++++++-------- internal/cmd/base/option.go | 10 +++ .../cmd/commands/authenticate/password.go | 2 +- internal/kms/const.go | 5 ++ internal/kms/kms.go | 2 +- internal/servers/controller/handler.go | 8 ++- .../authenticate/authenticate_service.go | 25 ++++++-- .../authenticate/authenticate_service_test.go | 5 +- 15 files changed, 196 insertions(+), 49 deletions(-) diff --git a/globals/globals.go b/globals/globals.go index e233e5a80..42dce3430 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -9,6 +9,10 @@ import "time" // package as "freeing us from circular dependency hell". These values should // only ever be set at startup, but simply available to reference from anywhere. +const ( + TokenEncryptionVersion = "1" +) + type ContextMaxRequestSizeType int type ContextOriginalRequestPathType int diff --git a/go.mod b/go.mod index 69e1fbf07..b5a41252d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/armon/go-metrics v0.3.4 + github.com/btcsuite/btcutil v1.0.2 github.com/bufbuild/buf v0.20.5 github.com/fatih/color v1.9.0 github.com/fatih/structs v1.1.0 diff --git a/go.sum b/go.sum index e9c9af9ae..75c22d65e 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -150,6 +151,16 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bufbuild/buf v0.20.5 h1:msLCy5vBoCdFXD+PxtMNA43KMkVJml7UCBCSaFWmEkU= github.com/bufbuild/buf v0.20.5/go.mod h1:Kgpp44ayt6oQ0J0kXEzIkYRfPggDxOriiEHNswd2vEo= github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= @@ -212,6 +223,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -656,6 +668,7 @@ github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZ github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4= github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= @@ -675,6 +688,7 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -698,6 +712,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -1073,6 +1088,7 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1093,6 +1109,7 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 572e12b6d..8914096e3 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/btcsuite/btcutil/base58" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" "github.com/hashicorp/boundary/internal/kms" @@ -19,7 +20,9 @@ import ( "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/recovery" "github.com/hashicorp/go-hclog" + wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/kr/pretty" + "google.golang.org/protobuf/proto" ) const ( @@ -32,9 +35,19 @@ type TokenFormat int const ( AuthTokenTypeUnknown TokenFormat = iota + + // Came in via the Authentication: Bearer header AuthTokenTypeBearer + + // Came in via split cookies AuthTokenTypeSplitCookie - AuthTokenRecoveryKms + + // It's of recovery type + AuthTokenTypeRecoveryKms + + // It's _known_ to be invalid, that is, a token was provided that is simply + // not valid and may be part of a DDoS or other attack + AuthTokenTypeInvalid ) type key int @@ -127,6 +140,12 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { return } + // table stakes + if v.requestInfo.TokenFormat == AuthTokenTypeInvalid { + v.logger.Trace("got invalid token type in auth function, which should not have occurred") + return + } + v.ctx = ctx v.requestInfo.scopeIdOverride = opts.withScopeId if err := v.parseAuthParams(); err != nil { @@ -405,7 +424,7 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin userId = at.GetIamUserId() } - case AuthTokenRecoveryKms: + case AuthTokenTypeRecoveryKms: userId = "u_recovery" if v.kms == nil { retErr = errors.New("perform auth check: no KMS object available to authz system") @@ -468,7 +487,7 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin } // At this point we don't need to look up grants since it's automatically allowed - if v.requestInfo.TokenFormat == AuthTokenRecoveryKms { + if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms { aclResults = &perms.ACLResults{Allowed: true} retErr = nil return @@ -504,7 +523,7 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin // split cookies and parses it. If it cannot be parsed successfully, the issue // is logged and we return blank, so logic will continue as the anonymous user. // The public ID and token are returned along with the token format. -func GetTokenFromRequest(logger hclog.Logger, req *http.Request) (string, string, TokenFormat) { +func GetTokenFromRequest(logger hclog.Logger, kmsCache *kms.Kms, req *http.Request) (string, string, TokenFormat) { // First, get the token, either from the authorization header or from split // cookies var receivedTokenType TokenFormat @@ -539,18 +558,48 @@ func GetTokenFromRequest(logger hclog.Logger, req *http.Request) (string, string } if strings.HasPrefix(fullToken, "r_") { - return "", fullToken, AuthTokenRecoveryKms + return "", fullToken, AuthTokenTypeRecoveryKms } splitFullToken := strings.Split(fullToken, "_") if len(splitFullToken) != 3 { logger.Trace("get token from request: unexpected number of segments in token", "expected", 3, "found", len(splitFullToken)) - return "", "", AuthTokenTypeUnknown + return "", "", AuthTokenTypeInvalid } - token := splitFullToken[2] publicId := strings.Join(splitFullToken[0:2], "_") + // This is hardcoded to global because at this point we don't know the + // scope. But that's okay; this is not really a security feature so much as + // an anti-DDoSing-the-backing-database feature. + tokenWrapper, err := kmsCache.GetWrapper(req.Context(), scope.Global.String(), kms.KeyPurposeTokens) + if err != nil { + logger.Warn("get token from request: unable to get wrapper for tokens", "error", err) + return "", "", AuthTokenTypeInvalid + } + + version := string(splitFullToken[2][0]) + switch version { + case globals.TokenEncryptionVersion: + default: + logger.Trace("unknown token encryption version", "version", version) + return "", "", AuthTokenTypeInvalid + } + marshaledToken := base58.Decode(splitFullToken[2][1:]) + + blobInfo := new(wrapping.EncryptedBlobInfo) + if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil { + logger.Trace("error decoding encrypted token", "error", err) + return "", "", AuthTokenTypeInvalid + } + + tokenBytes, err := tokenWrapper.Decrypt(req.Context(), blobInfo, []byte(publicId)) + if err != nil { + logger.Trace("error decrypting encrypted token", "error", err) + return "", "", AuthTokenTypeInvalid + } + token := string(tokenBytes) + if receivedTokenType == AuthTokenTypeUnknown || token == "" || publicId == "" { logger.Trace("get token from request: after parsing, could not find valid token") return "", "", AuthTokenTypeUnknown diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 73cbb0d14..aa5c1ccff 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/go-hclog" "github.com/kr/pretty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -271,6 +272,7 @@ func TestHandler_AuthDecoration(t *testing.T) { func TestAuthTokenAuthenticator(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) + logger := hclog.New(nil) wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) tokenRepo, err := authtoken.NewRepository(rw, rw, kms) @@ -288,8 +290,10 @@ func TestAuthTokenAuthenticator(t *testing.T) { o, _ := iam.TestScopes(t, iamRepo) at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId()) + encToken, err := authtoken.EncryptToken(context.Background(), kms, at.GetPublicId(), at.GetToken()) + require.NoError(t, err) - tokValue := at.GetPublicId() + "_" + at.GetToken() + tokValue := at.GetPublicId() + "_" + encToken jsCookieVal, httpCookieVal := tokValue[:len(tokValue)/2], tokValue[len(tokValue)/2:] cases := []struct { @@ -360,13 +364,13 @@ func TestAuthTokenAuthenticator(t *testing.T) { Path: req.URL.Path, Method: req.Method, } - requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = GetTokenFromRequest(nil, req) + requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = GetTokenFromRequest(logger, kms, req) assert.Equal(t, tc.tokenFormat, requestInfo.TokenFormat) if tc.userId == "" { return } - ctx := NewVerifierContext(context.Background(), nil, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) v, ok := ctx.Value(verifierKey).(*verifier) require.True(t, ok) diff --git a/internal/auth/password/account_test.go b/internal/auth/password/account_test.go index efa90e623..5862c75ad 100644 --- a/internal/auth/password/account_test.go +++ b/internal/auth/password/account_test.go @@ -19,7 +19,6 @@ func TestAccount_New(t *testing.T) { type args struct { authMethodId string - loginName string opts []Option } diff --git a/internal/authtoken/authtoken.go b/internal/authtoken/authtoken.go index 076124991..2e3a1b6a3 100644 --- a/internal/authtoken/authtoken.go +++ b/internal/authtoken/authtoken.go @@ -4,8 +4,12 @@ import ( "context" "fmt" + "github.com/btcsuite/btcutil/base58" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/authtoken/store" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/types/scope" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" "github.com/hashicorp/vault/sdk/helper/base62" @@ -95,3 +99,27 @@ func newAuthToken() (string, error) { } return fmt.Sprintf("%s%s", TokenValueVersionPrefix, token), nil } + +// EncryptToken is a shared function for encrypting a token value for return to +// the user. We always use the global scope because on authenticate we don't +// have scope info at this point and the idea is to remove a DB lookup if the +// token is made up/invalid so as to prevent DDoS against a third party service +// by just randomly guessing tokens. +func EncryptToken(ctx context.Context, kmsCache *kms.Kms, publicId, token string) (string, error) { + tokenWrapper, err := kmsCache.GetWrapper(ctx, scope.Global.String(), kms.KeyPurposeTokens) + if err != nil { + return "", fmt.Errorf("unable to get wrapper: %w", err) + } + + blobInfo, err := tokenWrapper.Encrypt(ctx, []byte(token), []byte(publicId)) + if err != nil { + return "", fmt.Errorf("error encrypting token: %w", err) + } + + marshaledBlob, err := proto.Marshal(blobInfo) + if err != nil { + return "", fmt.Errorf("error marshaling encrypted token: %w", err) + } + + return globals.TokenEncryptionVersion + base58.Encode(marshaledBlob), nil +} diff --git a/internal/cmd/base/base.go b/internal/cmd/base/base.go index a6b2dec66..e5ebbaea2 100644 --- a/internal/cmd/base/base.go +++ b/internal/cmd/base/base.go @@ -32,6 +32,7 @@ const ( // NotSetValue is a flag value for a not-set value NotSetValue = "(not set)" + envToken = "BOUNDARY_TOKEN" envTokenName = "BOUNDARY_TOKEN_NAME" envRecoveryConfig = "BOUNDARY_RECOVERY_CONFIG" ) @@ -62,6 +63,7 @@ type Command struct { flagTLSInsecure bool flagFormat string + FlagToken string FlagTokenName string FlagRecoveryConfig string flagOutputCurlString bool @@ -200,34 +202,35 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) { } c.client.SetToken(token) + case c.FlagToken != "": + c.client.SetToken(c.FlagToken) + case c.client.Token() == "": - if c.client.Token() == "" { - if c.FlagTokenName != "" { - tokenName = c.FlagTokenName - } - if tokenName != "none" { - token, err := keyring.Get("HashiCorp Boundary Auth Token", tokenName) - if err != nil { - if err == keyring.ErrNotFound { - c.UI.Info("No saved credential found, continuing without") - } else { - c.UI.Error(fmt.Sprintf("Error reading auth token from system credential store: %s", err)) - } - token = "" + if c.FlagTokenName != "" { + tokenName = c.FlagTokenName + } + if tokenName != "none" { + token, err := keyring.Get("HashiCorp Boundary Auth Token", tokenName) + if err != nil { + if err == keyring.ErrNotFound { + c.UI.Info("No saved credential found, continuing without") + } else { + c.UI.Error(fmt.Sprintf("Error reading auth token from system credential store: %s", err)) } - if token != "" { - tokenBytes, err := base64.RawStdEncoding.DecodeString(token) - if err != nil { - c.UI.Error(fmt.Sprintf("Error unmarshaling stored token from system credential store: %s", err)) + token = "" + } + if token != "" { + tokenBytes, err := base64.RawStdEncoding.DecodeString(token) + if err != nil { + c.UI.Error(fmt.Sprintf("Error unmarshaling stored token from system credential store: %s", err)) + } else { + var authToken authtokens.AuthToken + if err := json.Unmarshal(tokenBytes, &authToken); err != nil { + c.UI.Error(fmt.Sprintf("Error unmarshaling stored token information after reading from system credential store: %s", err)) } else { - var authToken authtokens.AuthToken - if err := json.Unmarshal(tokenBytes, &authToken); err != nil { - c.UI.Error(fmt.Sprintf("Error unmarshaling stored token information after reading from system credential store: %s", err)) - } else { - c.client.SetToken(authToken.Token) - if !opts.withNoTokenScope { - c.client.SetScopeId(authToken.Scope.Id) - } + c.client.SetToken(authToken.Token) + if !opts.withNoTokenScope { + c.client.SetScopeId(authToken.Scope.Id) } } } @@ -259,6 +262,10 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) { } } + if opts.withNoTokenValue { + c.client.SetToken("") + } + return c.client, nil } @@ -375,6 +382,13 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets { Usage: `If specified, the given value will be used as the name when storing the token in the system credential store. This can allow switching user identities for different commands. Set to "none" to disable storing the token.`, }) + f.StringVar(&StringVar{ + Name: "token", + Target: &c.FlagToken, + EnvVar: envToken, + Usage: `If specified, the given value will be used as the token for the call. Overrides the "token-name" parameter.`, + }) + f.StringVar(&StringVar{ Name: "recovery-config", Target: &c.FlagRecoveryConfig, diff --git a/internal/cmd/base/option.go b/internal/cmd/base/option.go index c0b33b868..93603a05f 100644 --- a/internal/cmd/base/option.go +++ b/internal/cmd/base/option.go @@ -15,6 +15,7 @@ type Option func(*Options) // Options - how Options are represented. type Options struct { withNoTokenScope bool + withNoTokenValue bool } func getDefaultOptions() Options { @@ -29,3 +30,12 @@ func WithNoTokenScope() Option { o.withNoTokenScope = true } } + +// WithNoTokenValue tells the client not to set a token for the client from a +// saved token's value, as this can cause confusing behavior at authentication +// time. +func WithNoTokenValue() Option { + return func(o *Options) { + o.withNoTokenValue = true + } +} diff --git a/internal/cmd/commands/authenticate/password.go b/internal/cmd/commands/authenticate/password.go index 6b4166441..92f111db9 100644 --- a/internal/cmd/commands/authenticate/password.go +++ b/internal/cmd/commands/authenticate/password.go @@ -112,7 +112,7 @@ func (c *PasswordCommand) Run(args []string) int { c.flagPassword = strings.TrimSpace(value) } - client, err := c.Client(base.WithNoTokenScope()) + client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue()) if err != nil { c.UI.Error(fmt.Sprintf("Error creating API client: %s", err.Error())) return 2 diff --git a/internal/kms/const.go b/internal/kms/const.go index 00153d097..b85e0dc4c 100644 --- a/internal/kms/const.go +++ b/internal/kms/const.go @@ -18,6 +18,9 @@ const ( // KeyPurposeRecovery is used for recovery access KeyPurposeRecovery + + // KeyPurposeTokens is used for token encryption + KeyPurposeTokens ) // String returns the key purpose cast as a string, just so it can be called as @@ -30,6 +33,8 @@ func (k KeyPurpose) String() string { return "oplog" case KeyPurposeRecovery: return "recovery" + case KeyPurposeTokens: + return "tokens" default: return "unknown" } diff --git a/internal/kms/kms.go b/internal/kms/kms.go index c7e16bcc5..1639650a4 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -149,7 +149,7 @@ func generateKeyId(scopeId string, purpose KeyPurpose, version uint32) string { // decryption. func (k *Kms) GetWrapper(ctx context.Context, scopeId string, purpose KeyPurpose, opt ...Option) (wrapping.Wrapper, error) { switch purpose { - case KeyPurposeOplog, KeyPurposeDatabase: + case KeyPurposeOplog, KeyPurposeDatabase, KeyPurposeTokens: case KeyPurposeUnknown: return nil, errors.New("key purpose not specified") default: diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 029088b5a..caf1b9ddd 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -106,7 +106,7 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er if err := services.RegisterAccountServiceHandlerServer(ctx, mux, accts); err != nil { return nil, fmt.Errorf("failed to register account service handler: %w", err) } - auths, err := authenticate.NewService(c.PasswordAuthRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) + auths, err := authenticate.NewService(c.kms, c.PasswordAuthRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) if err != nil { return nil, fmt.Errorf("failed to create authentication handler service: %w", err) } @@ -205,7 +205,11 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp DisableAuthzFailures: disableAuthzFailures, } - requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = auth.GetTokenFromRequest(c.logger, r) + requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = auth.GetTokenFromRequest(c.logger, c.kms, r) + if requestInfo.TokenFormat == auth.AuthTokenTypeInvalid { + w.WriteHeader(http.StatusForbidden) + return + } ctx = auth.NewVerifierContext(ctx, c.logger, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms, requestInfo) // Set the context back on the request diff --git a/internal/servers/controller/handlers/authenticate/authenticate_service.go b/internal/servers/controller/handlers/authenticate/authenticate_service.go index cdacb5893..95a564115 100644 --- a/internal/servers/controller/handlers/authenticate/authenticate_service.go +++ b/internal/servers/controller/handlers/authenticate/authenticate_service.go @@ -2,7 +2,7 @@ package authenticate import ( "context" - "fmt" + "errors" "regexp" "strings" @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "google.golang.org/grpc/codes" @@ -29,24 +30,28 @@ var ( // Service handles request as described by the pbs.OrgServiceServer interface. type Service struct { + kms *kms.Kms pwRepo common.PasswordAuthRepoFactory iamRepo common.IamRepoFactory authTokenRepo common.AuthTokenRepoFactory } // NewService returns an org service which handles org related requests to boundary. -func NewService(pwRepo common.PasswordAuthRepoFactory, iamRepo common.IamRepoFactory, atRepo common.AuthTokenRepoFactory) (Service, error) { +func NewService(kms *kms.Kms, pwRepo common.PasswordAuthRepoFactory, iamRepo common.IamRepoFactory, atRepo common.AuthTokenRepoFactory) (Service, error) { + if kms == nil { + return Service{}, errors.New("nil kms provided") + } if iamRepo == nil { - return Service{}, fmt.Errorf("nil iam repository provided") + return Service{}, errors.New("nil iam repository provided") } if atRepo == nil { - return Service{}, fmt.Errorf("nil auth token repository provided") + return Service{}, errors.New("nil auth token repository provided") } if pwRepo == nil { - return Service{}, fmt.Errorf("nil password repository provided") + return Service{}, errors.New("nil password repository provided") } - return Service{pwRepo: pwRepo, iamRepo: iamRepo, authTokenRepo: atRepo}, nil + return Service{kms: kms, pwRepo: pwRepo, iamRepo: iamRepo, authTokenRepo: atRepo}, nil } var _ pbs.AuthenticationServiceServer = Service{} @@ -103,7 +108,13 @@ func (s Service) authenticateWithRepo(ctx context.Context, scopeId, authMethodId if err != nil { return nil, err } - tok.Token = tok.GetPublicId() + "_" + tok.GetToken() + + token, err := authtoken.EncryptToken(ctx, s.kms, tok.GetPublicId(), tok.GetToken()) + if err != nil { + return nil, err + } + + tok.Token = tok.GetPublicId() + "_" + token prot := toProto(tok) scp, err := iamRepo.LookupScope(ctx, u.GetScopeId()) diff --git a/internal/servers/controller/handlers/authenticate/authenticate_service_test.go b/internal/servers/controller/handlers/authenticate/authenticate_service_test.go index 532930695..2b352f599 100644 --- a/internal/servers/controller/handlers/authenticate/authenticate_service_test.go +++ b/internal/servers/controller/handlers/authenticate/authenticate_service_test.go @@ -44,6 +44,7 @@ func TestAuthenticate(t *testing.T) { require.NoError(t, err) acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) require.NoError(t, err) + require.NotNil(t, acct) cases := []struct { name string @@ -147,7 +148,7 @@ func TestAuthenticate(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := NewService(passwordRepoFn, iamRepoFn, authTokenRepoFn) + s, err := NewService(kms, passwordRepoFn, iamRepoFn, authTokenRepoFn) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(auth.WithScopeId(o.GetPublicId())), tc.request) @@ -195,7 +196,7 @@ func TestAuthenticate_AuthAccountConnectedToIamUser(t *testing.T) { iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId(), iam.WithAutoVivify(true)) require.NoError(err) - s, err := NewService(passwordRepoFn, iamRepoFn, authTokenRepoFn) + s, err := NewService(kms, passwordRepoFn, iamRepoFn, authTokenRepoFn) require.NoError(err) resp, err := s.Authenticate(auth.DisabledAuthTestContext(auth.WithScopeId(o.GetPublicId())), &pbs.AuthenticateRequest{ AuthMethodId: am.GetPublicId(),