From 62013579026b409e6d67e2d32c3c9b969d382548 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 14 Sep 2020 12:30:11 -0400 Subject: [PATCH] Use scope-specific token DEKs (#342) --- internal/auth/auth.go | 230 +++++++++++------- internal/auth/auth_test.go | 6 +- internal/auth/testing.go | 3 +- internal/authtoken/authtoken.go | 28 ++- internal/cmd/commands/scopes/scope.go | 2 +- internal/gen/controller/tokens/tokens.pb.go | 162 ++++++++++++ .../local/controller/tokens/v1/tokens.proto | 10 + internal/servers/controller/cors_test.go | 2 +- internal/servers/controller/handler.go | 2 +- .../authenticate/authenticate_service.go | 2 +- sdk/recovery/recovery.go | 27 +- 11 files changed, 366 insertions(+), 108 deletions(-) create mode 100644 internal/gen/controller/tokens/tokens.pb.go create mode 100644 internal/proto/local/controller/tokens/v1/tokens.proto diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 3cd2a2232..37f0e5e09 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -11,6 +11,7 @@ import ( "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/gen/controller/tokens" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/servers/controller/common" @@ -46,20 +47,16 @@ var verifierKey key // RequestInfo contains request parameters necessary for checking authn/authz type RequestInfo struct { - Path string - Method string - PublicId string - Token string - TokenFormat TokenFormat - - // These are set by the service handlers via options - scopeId string - pin string - - // This is used by the scopes collection - userIdOverride string + Path string + Method string + PublicId string + EncryptedToken string + Token string + TokenFormat TokenFormat // The following are useful for tests + scopeIdOverride string + userIdOverride string DisableAuthzFailures bool DisableAuthEntirely bool } @@ -122,7 +119,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { ret.Scope = new(scopes.ScopeInfo) if v.requestInfo.DisableAuthEntirely { - ret.Scope.Id = v.requestInfo.scopeId + ret.Scope.Id = v.requestInfo.scopeIdOverride if ret.Scope.Id == "" { ret.Scope.Id = opts.withScopeId } @@ -147,6 +144,10 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) { Type: opts.withType, } + if v.requestInfo.EncryptedToken != "" { + v.decryptToken() + } + var authResults *perms.ACLResults var err error authResults, ret.UserId, ret.Scope, err = v.performAuthCheck() @@ -184,6 +185,14 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin // Validate the token and fetch the corresponding user ID switch v.requestInfo.TokenFormat { + case AuthTokenTypeUnknown: + // Nothing; remain as the anonymous user + + case AuthTokenTypeRecoveryKms: + // We validated the encrypted token in decryptToken and handled the + // nonces there, so just set the user + userId = "u_recovery" + case AuthTokenTypeBearer, AuthTokenTypeSplitCookie: if v.requestInfo.Token == "" { // This will end up staying as the anonymous user @@ -204,40 +213,6 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin if at != nil { userId = at.GetIamUserId() } - - case AuthTokenTypeRecoveryKms: - userId = "u_recovery" - if v.kms == nil { - retErr = errors.New("perform auth check: no KMS object available to authz system") - return - } - wrapper := v.kms.GetExternalWrappers().Recovery() - if wrapper == nil { - retErr = errors.New("perform auth check: no recovery KMS is available") - return - } - info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.Token) - if err != nil { - retErr = fmt.Errorf("perform auth check: error validating recovery token: %w", err) - return - } - // If we add the validity period to the creation time (which we've - // verified is before the current time, with a minute of fudging), and - // it's before now, it's expired and might be a replay. - if info.CreationTime.Add(globals.RecoveryTokenValidityPeriod).Before(time.Now()) { - retErr = errors.New("WARNING: perform auth check: recovery token has expired (possible replay attack)") - return - } - repo, err := v.serversRepoFn() - if err != nil { - retErr = fmt.Errorf("perform auth check: error fetching servers repo: %w", err) - return - } - if err := repo.AddRecoveryNonce(v.ctx, info.Nonce); err != nil { - retErr = fmt.Errorf("WARNING: perform auth check: error adding nonce to database (possible replay attack): %w", err) - return - } - v.logger.Warn("NOTE: recovery KMS was used to authorize a call", "url", v.requestInfo.Path, "method", v.requestInfo.Method) } iamRepo, err := v.iamRepoFn() @@ -315,7 +290,7 @@ func (v verifier) performAuthCheck() (aclResults *perms.ACLResults, userId strin // GetTokenFromRequest pulls the token from either the Authorization header or // 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. +// The public ID and _encrypted_ token are returned along with the token format. 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 @@ -361,42 +336,127 @@ func GetTokenFromRequest(logger hclog.Logger, kmsCache *kms.Kms, req *http.Reque } publicId := strings.Join(splitFullToken[0:2], "_") + encryptedToken := splitFullToken[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; continuing as anonymous user", "error", err) - return "", "", AuthTokenTypeUnknown - } - - version := splitFullToken[2][0:len(globals.ServiceTokenV1)] - switch version { - case globals.ServiceTokenV1: - default: - logger.Trace("get token from request: unknown token encryption version; continuing as anonymous user", "version", version) - return "", "", AuthTokenTypeUnknown - } - marshaledToken := base58.Decode(splitFullToken[2][len(globals.ServiceTokenV1):]) - - blobInfo := new(wrapping.EncryptedBlobInfo) - if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil { - logger.Trace("get token from request: error decoding encrypted token; continuing as anonymous user", "error", err) - return "", "", AuthTokenTypeUnknown - } - - tokenBytes, err := tokenWrapper.Decrypt(req.Context(), blobInfo, []byte(publicId)) - if err != nil { - logger.Trace("get token from request: error decrypting encrypted token; continuing as anonymous user", "error", err) - return "", "", AuthTokenTypeUnknown - } - token := string(tokenBytes) - - if receivedTokenType == AuthTokenTypeUnknown || token == "" || publicId == "" { - logger.Trace("get token from request: after parsing, could not find valid token; continuing as anonymous user") - return "", "", AuthTokenTypeUnknown - } - - return publicId, token, receivedTokenType + return publicId, encryptedToken, receivedTokenType +} + +func (v *verifier) decryptToken() { + switch v.requestInfo.TokenFormat { + case AuthTokenTypeUnknown: + // Nothing to decrypt + return + + case AuthTokenTypeBearer, AuthTokenTypeSplitCookie: + if v.kms == nil { + v.logger.Trace("decrypt token: no KMS object available to authz system") + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + tokenRepo, err := v.authTokenRepoFn() + if err != nil { + v.logger.Warn("decrypt bearer token: failed to get authtoken repo", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + at, err := tokenRepo.LookupAuthToken(v.ctx, v.requestInfo.PublicId) + if err != nil { + v.logger.Trace("decrypt bearer token: failed to look up auth token by public ID", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + if at == nil { + v.logger.Trace("decrypt bearer token: nil result from looking up auth token by public ID") + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + tokenWrapper, err := v.kms.GetWrapper(v.ctx, at.GetScopeId(), kms.KeyPurposeTokens) + if err != nil { + v.logger.Warn("decrypt bearer token: unable to get wrapper for tokens; continuing as anonymous user", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + version := v.requestInfo.EncryptedToken[0:len(globals.ServiceTokenV1)] + switch version { + case globals.ServiceTokenV1: + default: + v.logger.Trace("decrypt bearer token: unknown token encryption version; continuing as anonymous user", "version", version) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + marshaledToken := base58.Decode(v.requestInfo.EncryptedToken[len(globals.ServiceTokenV1):]) + + blobInfo := new(wrapping.EncryptedBlobInfo) + if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil { + v.logger.Trace("decrypt bearer token: error decoding encrypted token; continuing as anonymous user", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + s1Bytes, err := tokenWrapper.Decrypt(v.ctx, blobInfo, []byte(v.requestInfo.PublicId)) + if err != nil { + v.logger.Trace("decrypt bearer token: error decrypting encrypted token; continuing as anonymous user", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + var s1Info tokens.S1TokenInfo + if err := proto.Unmarshal(s1Bytes, &s1Info); err != nil { + v.logger.Trace("decrypt bearer token: error unmarshaling token info; continuing as anonymous user", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + if v.requestInfo.TokenFormat == AuthTokenTypeUnknown || s1Info.Token == "" || v.requestInfo.PublicId == "" { + v.logger.Trace("decrypt bearer token: after parsing, could not find valid token; continuing as anonymous user") + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + + v.requestInfo.Token = s1Info.Token + return + + case AuthTokenTypeRecoveryKms: + if v.kms == nil { + v.logger.Trace("decrypt recovery token: no KMS object available to authz system") + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + wrapper := v.kms.GetExternalWrappers().Recovery() + if wrapper == nil { + v.logger.Trace("decrypt recovery token: no recovery KMS is available") + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.EncryptedToken) + if err != nil { + v.logger.Trace("decrypt recovery token: error parsing and validating recovery token", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + // If we add the validity period to the creation time (which we've + // verified is before the current time, with a minute of fudging), and + // it's before now, it's expired and might be a replay. + if info.CreationTime.Add(globals.RecoveryTokenValidityPeriod).Before(time.Now()) { + v.logger.Warn("WARNING: decrypt recovery token: recovery token has expired (possible replay attack)") + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + repo, err := v.serversRepoFn() + if err != nil { + v.logger.Trace("decrypt recovery token: error fetching servers repo", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + if err := repo.AddRecoveryNonce(v.ctx, info.Nonce); err != nil { + v.logger.Warn("WARNING: decrypt recovery token: error adding nonce to database (possible replay attack)", "error", err) + v.requestInfo.TokenFormat = AuthTokenTypeUnknown + return + } + v.logger.Info("NOTE: recovery KMS was used to authorize a call", "url", v.requestInfo.Path, "method", v.requestInfo.Method) + } } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 1fbcd79ed..0fac71d13 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -39,7 +39,7 @@ 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()) + encToken, err := authtoken.EncryptToken(context.Background(), kms, o.GetPublicId(), at.GetPublicId(), at.GetToken()) require.NoError(t, err) tokValue := at.GetPublicId() + "_" + encToken @@ -113,7 +113,7 @@ func TestAuthTokenAuthenticator(t *testing.T) { Path: req.URL.Path, Method: req.Method, } - requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = GetTokenFromRequest(logger, kms, req) + requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(logger, kms, req) assert.Equal(t, tc.tokenFormat, requestInfo.TokenFormat) if tc.userId == "" { @@ -125,6 +125,8 @@ func TestAuthTokenAuthenticator(t *testing.T) { require.True(t, ok) require.NotNil(t, v) + v.decryptToken() + at, err := tokenRepo.ValidateToken(ctx, v.requestInfo.PublicId, v.requestInfo.Token) require.NoError(t, err) assert.Equal(t, tc.userId, at.GetIamUserId()) diff --git a/internal/auth/testing.go b/internal/auth/testing.go index 052995fa1..5004edf0b 100644 --- a/internal/auth/testing.go +++ b/internal/auth/testing.go @@ -6,8 +6,7 @@ import "context" func DisabledAuthTestContext(opt ...Option) context.Context { reqInfo := RequestInfo{DisableAuthEntirely: true} opts := getOpts(opt...) - reqInfo.scopeId = opts.withScopeId - reqInfo.pin = opts.withPin + reqInfo.scopeIdOverride = opts.withScopeId reqInfo.userIdOverride = opts.withUserId return NewVerifierContext(context.Background(), nil, nil, nil, nil, opts.withKms, reqInfo) } diff --git a/internal/authtoken/authtoken.go b/internal/authtoken/authtoken.go index a1be6fd86..0d2589c1b 100644 --- a/internal/authtoken/authtoken.go +++ b/internal/authtoken/authtoken.go @@ -3,13 +3,15 @@ package authtoken import ( "context" "fmt" + mathrand "math/rand" + "time" "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/gen/controller/tokens" "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" @@ -101,17 +103,27 @@ func newAuthToken() (string, error) { } // 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) +// the user. +func EncryptToken(ctx context.Context, kmsCache *kms.Kms, scopeId, publicId, token string) (string, error) { + r := mathrand.New(mathrand.NewSource(time.Now().UnixNano())) + + s1Info := &tokens.S1TokenInfo{ + Token: token, + Confounder: make([]byte, r.Intn(30)), + } + r.Read(s1Info.Confounder) + + marshaledS1Info, err := proto.Marshal(s1Info) + if err != nil { + return "", fmt.Errorf("error marshaling token info: %w", err) + } + + tokenWrapper, err := kmsCache.GetWrapper(ctx, scopeId, kms.KeyPurposeTokens) if err != nil { return "", fmt.Errorf("unable to get wrapper: %w", err) } - blobInfo, err := tokenWrapper.Encrypt(ctx, []byte(token), []byte(publicId)) + blobInfo, err := tokenWrapper.Encrypt(ctx, []byte(marshaledS1Info), []byte(publicId)) if err != nil { return "", fmt.Errorf("error encrypting token: %w", err) } diff --git a/internal/cmd/commands/scopes/scope.go b/internal/cmd/commands/scopes/scope.go index aa532741c..cc17b45cd 100644 --- a/internal/cmd/commands/scopes/scope.go +++ b/internal/cmd/commands/scopes/scope.go @@ -198,7 +198,7 @@ func (c *Command) Run(args []string) int { case "table": if len(listedScopes) == 0 { - c.UI.Output("No scopes found") + c.UI.Output("No child scopes found") return 0 } var output []string diff --git a/internal/gen/controller/tokens/tokens.pb.go b/internal/gen/controller/tokens/tokens.pb.go new file mode 100644 index 000000000..33257d4bc --- /dev/null +++ b/internal/gen/controller/tokens/tokens.pb.go @@ -0,0 +1,162 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.12.4 +// source: controller/tokens/v1/tokens.proto + +package tokens + +import ( + proto "github.com/golang/protobuf/proto" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type S1TokenInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,10,opt,name=token,proto3" json:"token,omitempty"` + Confounder []byte `protobuf:"bytes,20,opt,name=confounder,proto3" json:"confounder,omitempty"` +} + +func (x *S1TokenInfo) Reset() { + *x = S1TokenInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_tokens_v1_tokens_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *S1TokenInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*S1TokenInfo) ProtoMessage() {} + +func (x *S1TokenInfo) ProtoReflect() protoreflect.Message { + mi := &file_controller_tokens_v1_tokens_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use S1TokenInfo.ProtoReflect.Descriptor instead. +func (*S1TokenInfo) Descriptor() ([]byte, []int) { + return file_controller_tokens_v1_tokens_proto_rawDescGZIP(), []int{0} +} + +func (x *S1TokenInfo) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *S1TokenInfo) GetConfounder() []byte { + if x != nil { + return x.Confounder + } + return nil +} + +var File_controller_tokens_v1_tokens_proto protoreflect.FileDescriptor + +var file_controller_tokens_v1_tokens_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x43, 0x0a, 0x0b, 0x53, 0x31, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, + 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x42, 0x45, + 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x3b, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_tokens_v1_tokens_proto_rawDescOnce sync.Once + file_controller_tokens_v1_tokens_proto_rawDescData = file_controller_tokens_v1_tokens_proto_rawDesc +) + +func file_controller_tokens_v1_tokens_proto_rawDescGZIP() []byte { + file_controller_tokens_v1_tokens_proto_rawDescOnce.Do(func() { + file_controller_tokens_v1_tokens_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_tokens_v1_tokens_proto_rawDescData) + }) + return file_controller_tokens_v1_tokens_proto_rawDescData +} + +var file_controller_tokens_v1_tokens_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_tokens_v1_tokens_proto_goTypes = []interface{}{ + (*S1TokenInfo)(nil), // 0: controller.tokens.v1.S1TokenInfo +} +var file_controller_tokens_v1_tokens_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_tokens_v1_tokens_proto_init() } +func file_controller_tokens_v1_tokens_proto_init() { + if File_controller_tokens_v1_tokens_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_tokens_v1_tokens_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*S1TokenInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_tokens_v1_tokens_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_tokens_v1_tokens_proto_goTypes, + DependencyIndexes: file_controller_tokens_v1_tokens_proto_depIdxs, + MessageInfos: file_controller_tokens_v1_tokens_proto_msgTypes, + }.Build() + File_controller_tokens_v1_tokens_proto = out.File + file_controller_tokens_v1_tokens_proto_rawDesc = nil + file_controller_tokens_v1_tokens_proto_goTypes = nil + file_controller_tokens_v1_tokens_proto_depIdxs = nil +} diff --git a/internal/proto/local/controller/tokens/v1/tokens.proto b/internal/proto/local/controller/tokens/v1/tokens.proto new file mode 100644 index 000000000..e02475e3e --- /dev/null +++ b/internal/proto/local/controller/tokens/v1/tokens.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package controller.tokens.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/controller/tokens;tokens"; + +message S1TokenInfo { + string token = 10; + bytes confounder = 20; +} diff --git a/internal/servers/controller/cors_test.go b/internal/servers/controller/cors_test.go index 5fe7861d8..b1c231917 100644 --- a/internal/servers/controller/cors_test.go +++ b/internal/servers/controller/cors_test.go @@ -82,7 +82,7 @@ func TestHandler_CORS(t *testing.T) { require.NoError(t, err) org := iam.TestOrg(t, repo) - encToken, err := authtoken.EncryptToken(context.Background(), tc.c.kms, "fo_o", "bar") + encToken, err := authtoken.EncryptToken(context.Background(), tc.c.kms, org.GetPublicId(), "fo_o", "bar") require.NoError(t, err) token := "fo_o_" + encToken diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index a564d4870..cccc2bc46 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -229,7 +229,7 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp DisableAuthzFailures: disableAuthzFailures, } - requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = auth.GetTokenFromRequest(c.logger, c.kms, r) + requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(c.logger, c.kms, r) 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 c4d35d872..9ab7152ed 100644 --- a/internal/servers/controller/handlers/authenticate/authenticate_service.go +++ b/internal/servers/controller/handlers/authenticate/authenticate_service.go @@ -111,7 +111,7 @@ func (s Service) authenticateWithRepo(ctx context.Context, scopeId, authMethodId return nil, err } - token, err := authtoken.EncryptToken(ctx, s.kms, tok.GetPublicId(), tok.GetToken()) + token, err := authtoken.EncryptToken(ctx, s.kms, scopeId, tok.GetPublicId(), tok.GetToken()) if err != nil { return nil, err } diff --git a/sdk/recovery/recovery.go b/sdk/recovery/recovery.go index e8c82dc0e..4cbc14d4a 100644 --- a/sdk/recovery/recovery.go +++ b/sdk/recovery/recovery.go @@ -15,7 +15,10 @@ import ( "google.golang.org/protobuf/proto" ) -const nonceLength = 32 +const ( + nonceLength = 32 + v1String = "1" +) // FutureLeeway indicates how far in the future we should allow the creation // time of the token to be, in order to account for clock drift @@ -58,18 +61,28 @@ func formatToken(ctx context.Context, wrapper wrapping.Wrapper, info *Info) (str return "", fmt.Errorf("error marshaling encrypted blob: %w", err) } - return fmt.Sprintf("r_%s", base58.Encode(marshaledBlob)), nil + return fmt.Sprintf("r_%s%s", v1String, base58.Encode(marshaledBlob)), nil } -func ParseRecoveryToken(ctx context.Context, wrapper wrapping.Wrapper, token string) (*Info, error) { - token = strings.TrimSpace(token) - if token == "" { +func ParseRecoveryToken(ctx context.Context, wrapper wrapping.Wrapper, versionedToken string) (*Info, error) { + versionedToken = strings.TrimSpace(versionedToken) + if versionedToken == "" { return nil, errors.New("empty token") } - if !strings.HasPrefix(token, "r_") { + if !strings.HasPrefix(versionedToken, "r_") { return nil, errors.New("token has wrong format") } - token = strings.TrimPrefix(token, "r_") + versionedToken = strings.TrimPrefix(versionedToken, "r_") + + ver, token := versionedToken[0:1], versionedToken[1:] + + // Don't do anything yet, but if we ever need to adjust data based on + // version we'd do it here + switch ver { + case v1String: + default: + return nil, fmt.Errorf("unknown recovery token version %s", ver) + } marshaledBlob := base58.Decode(token) if len(marshaledBlob) == 0 {