diff --git a/api/gen/proto/go/teleport/join/v1/joinservice.pb.go b/api/gen/proto/go/teleport/join/v1/joinservice.pb.go index 93c0b0bf6d0f4..3a9ca8717d7c6 100644 --- a/api/gen/proto/go/teleport/join/v1/joinservice.pb.go +++ b/api/gen/proto/go/teleport/join/v1/joinservice.pb.go @@ -450,6 +450,440 @@ func (x *TokenInit) GetClientParams() *ClientParams { return nil } +// BoundKeypairInit is sent from the client in response to the ServerInit +// message for the bound keypair join method. +// The server is expected to respond with a BoundKeypairChallenge. +// +// The bound keypair method join flow is: +// 1. client->server: ClientInit +// 2. server->client: ServerInit +// 3. client->server: BoundKeypairInit +// 4. server->client: BoundKeypairChallenge +// 5. client->server: BoundKeypairChallengeSolution +// (optional additional steps if keypair rotation is required) +// server->client: BoundKeypairRotationRequest +// client->server: BoundKeypairRotationResponse +// server->client: BoundKeypairChallenge +// client->server: BoundKeypairChallengeSolution +// 6. server->client: Result containing BoundKeypairResult +type BoundKeypairInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ClientParams holds parameters for the specific type of client trying to join. + ClientParams *ClientParams `protobuf:"bytes,1,opt,name=client_params,json=clientParams,proto3" json:"client_params,omitempty"` + // If set, attempts to bind a new keypair using an initial join secret. + // Any value set here will be ignored if a keypair is already bound. + InitialJoinSecret string `protobuf:"bytes,2,opt,name=initial_join_secret,json=initialJoinSecret,proto3" json:"initial_join_secret,omitempty"` + // A document signed by Auth containing join state parameters from the + // previous join attempt. Not required on initial join; required on all + // subsequent joins. + PreviousJoinState []byte `protobuf:"bytes,3,opt,name=previous_join_state,json=previousJoinState,proto3" json:"previous_join_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoundKeypairInit) Reset() { + *x = BoundKeypairInit{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoundKeypairInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoundKeypairInit) ProtoMessage() {} + +func (x *BoundKeypairInit) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoundKeypairInit.ProtoReflect.Descriptor instead. +func (*BoundKeypairInit) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{6} +} + +func (x *BoundKeypairInit) GetClientParams() *ClientParams { + if x != nil { + return x.ClientParams + } + return nil +} + +func (x *BoundKeypairInit) GetInitialJoinSecret() string { + if x != nil { + return x.InitialJoinSecret + } + return "" +} + +func (x *BoundKeypairInit) GetPreviousJoinState() []byte { + if x != nil { + return x.PreviousJoinState + } + return nil +} + +// BoundKeypairChallenge is a challenge issued by the server that joining +// clients are expected to complete. +// The client is expected to respond with a BoundKeypairChallengeSolution. +type BoundKeypairChallenge struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The desired public key corresponding to the private key that should be used + // to sign this challenge, in SSH authorized keys format. + PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + // A challenge to sign with the requested public key. During keypair rotation, + // a second challenge will be provided to verify the new keypair before certs + // are returned. + Challenge string `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoundKeypairChallenge) Reset() { + *x = BoundKeypairChallenge{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoundKeypairChallenge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoundKeypairChallenge) ProtoMessage() {} + +func (x *BoundKeypairChallenge) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoundKeypairChallenge.ProtoReflect.Descriptor instead. +func (*BoundKeypairChallenge) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{7} +} + +func (x *BoundKeypairChallenge) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *BoundKeypairChallenge) GetChallenge() string { + if x != nil { + return x.Challenge + } + return "" +} + +// BoundKeypairChallengeSolution is sent from the client in response to the +// BoundKeypairChallenge. +// The server is expected to respond with either a Result or a +// BoundKeypairRotationRequest. +type BoundKeypairChallengeSolution struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A solution to a challenge from the server. This generated by signing the + // challenge as a JWT using the keypair associated with the requested public + // key. + Solution []byte `protobuf:"bytes,1,opt,name=solution,proto3" json:"solution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoundKeypairChallengeSolution) Reset() { + *x = BoundKeypairChallengeSolution{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoundKeypairChallengeSolution) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoundKeypairChallengeSolution) ProtoMessage() {} + +func (x *BoundKeypairChallengeSolution) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoundKeypairChallengeSolution.ProtoReflect.Descriptor instead. +func (*BoundKeypairChallengeSolution) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{8} +} + +func (x *BoundKeypairChallengeSolution) GetSolution() []byte { + if x != nil { + return x.Solution + } + return nil +} + +// BoundKeypairRotationRequest is sent by the server in response to a +// BoundKeypairChallenge when a keypair rotation is required. It acts like an +// additional challenge, the client is expected to respond with a +// BoundKeypairRotationResponse. +type BoundKeypairRotationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The signature algorithm suite in use by the cluster. + SignatureAlgorithmSuite string `protobuf:"bytes,1,opt,name=signature_algorithm_suite,json=signatureAlgorithmSuite,proto3" json:"signature_algorithm_suite,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoundKeypairRotationRequest) Reset() { + *x = BoundKeypairRotationRequest{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoundKeypairRotationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoundKeypairRotationRequest) ProtoMessage() {} + +func (x *BoundKeypairRotationRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoundKeypairRotationRequest.ProtoReflect.Descriptor instead. +func (*BoundKeypairRotationRequest) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{9} +} + +func (x *BoundKeypairRotationRequest) GetSignatureAlgorithmSuite() string { + if x != nil { + return x.SignatureAlgorithmSuite + } + return "" +} + +// BoundKeypairRotationResponse is sent by the client in response to a +// BoundKeypairRotationRequest from the server. +// The server is expected to respond with an additional BoundKeypairChallenge +// for the new key. +type BoundKeypairRotationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The public key to be registered with auth. Clients should expect a + // subsequent challenge against this public key to be sent. This is encoded in + // SSH authorized keys format. + PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoundKeypairRotationResponse) Reset() { + *x = BoundKeypairRotationResponse{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoundKeypairRotationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoundKeypairRotationResponse) ProtoMessage() {} + +func (x *BoundKeypairRotationResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoundKeypairRotationResponse.ProtoReflect.Descriptor instead. +func (*BoundKeypairRotationResponse) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{10} +} + +func (x *BoundKeypairRotationResponse) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +// BoundKeypairResult holds additional result parameters relevant to the bound +// keypair join method. +type BoundKeypairResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A signed join state document to be provided on the next join attempt. + JoinState []byte `protobuf:"bytes,2,opt,name=join_state,json=joinState,proto3" json:"join_state,omitempty"` + // The public key registered with Auth at the end of the joining ceremony. + // After a successful keypair rotation, this should reflect the newly + // registered public key. This is encoded in SSH authorized keys format. + PublicKey []byte `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoundKeypairResult) Reset() { + *x = BoundKeypairResult{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoundKeypairResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoundKeypairResult) ProtoMessage() {} + +func (x *BoundKeypairResult) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoundKeypairResult.ProtoReflect.Descriptor instead. +func (*BoundKeypairResult) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{11} +} + +func (x *BoundKeypairResult) GetJoinState() []byte { + if x != nil { + return x.JoinState + } + return nil +} + +func (x *BoundKeypairResult) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +// ChallengeSolution holds a solution to a challenge issued by the server. +type ChallengeSolution struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ChallengeSolution_BoundKeypairChallengeSolution + // *ChallengeSolution_BoundKeypairRotationResponse + Payload isChallengeSolution_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChallengeSolution) Reset() { + *x = ChallengeSolution{} + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChallengeSolution) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChallengeSolution) ProtoMessage() {} + +func (x *ChallengeSolution) ProtoReflect() protoreflect.Message { + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChallengeSolution.ProtoReflect.Descriptor instead. +func (*ChallengeSolution) Descriptor() ([]byte, []int) { + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{12} +} + +func (x *ChallengeSolution) GetPayload() isChallengeSolution_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ChallengeSolution) GetBoundKeypairChallengeSolution() *BoundKeypairChallengeSolution { + if x != nil { + if x, ok := x.Payload.(*ChallengeSolution_BoundKeypairChallengeSolution); ok { + return x.BoundKeypairChallengeSolution + } + } + return nil +} + +func (x *ChallengeSolution) GetBoundKeypairRotationResponse() *BoundKeypairRotationResponse { + if x != nil { + if x, ok := x.Payload.(*ChallengeSolution_BoundKeypairRotationResponse); ok { + return x.BoundKeypairRotationResponse + } + } + return nil +} + +type isChallengeSolution_Payload interface { + isChallengeSolution_Payload() +} + +type ChallengeSolution_BoundKeypairChallengeSolution struct { + BoundKeypairChallengeSolution *BoundKeypairChallengeSolution `protobuf:"bytes,1,opt,name=bound_keypair_challenge_solution,json=boundKeypairChallengeSolution,proto3,oneof"` +} + +type ChallengeSolution_BoundKeypairRotationResponse struct { + BoundKeypairRotationResponse *BoundKeypairRotationResponse `protobuf:"bytes,2,opt,name=bound_keypair_rotation_response,json=boundKeypairRotationResponse,proto3,oneof"` +} + +func (*ChallengeSolution_BoundKeypairChallengeSolution) isChallengeSolution_Payload() {} + +func (*ChallengeSolution_BoundKeypairRotationResponse) isChallengeSolution_Payload() {} + // JoinRequest is the message type sent from the joining client to the server. type JoinRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -457,6 +891,8 @@ type JoinRequest struct { // // *JoinRequest_ClientInit // *JoinRequest_TokenInit + // *JoinRequest_BoundKeypairInit + // *JoinRequest_Solution Payload isJoinRequest_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -464,7 +900,7 @@ type JoinRequest struct { func (x *JoinRequest) Reset() { *x = JoinRequest{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -476,7 +912,7 @@ func (x *JoinRequest) String() string { func (*JoinRequest) ProtoMessage() {} func (x *JoinRequest) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[6] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -489,7 +925,7 @@ func (x *JoinRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use JoinRequest.ProtoReflect.Descriptor instead. func (*JoinRequest) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{6} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{13} } func (x *JoinRequest) GetPayload() isJoinRequest_Payload { @@ -517,6 +953,24 @@ func (x *JoinRequest) GetTokenInit() *TokenInit { return nil } +func (x *JoinRequest) GetBoundKeypairInit() *BoundKeypairInit { + if x != nil { + if x, ok := x.Payload.(*JoinRequest_BoundKeypairInit); ok { + return x.BoundKeypairInit + } + } + return nil +} + +func (x *JoinRequest) GetSolution() *ChallengeSolution { + if x != nil { + if x, ok := x.Payload.(*JoinRequest_Solution); ok { + return x.Solution + } + } + return nil +} + type isJoinRequest_Payload interface { isJoinRequest_Payload() } @@ -529,10 +983,22 @@ type JoinRequest_TokenInit struct { TokenInit *TokenInit `protobuf:"bytes,2,opt,name=token_init,json=tokenInit,proto3,oneof"` } +type JoinRequest_BoundKeypairInit struct { + BoundKeypairInit *BoundKeypairInit `protobuf:"bytes,3,opt,name=bound_keypair_init,json=boundKeypairInit,proto3,oneof"` +} + +type JoinRequest_Solution struct { + Solution *ChallengeSolution `protobuf:"bytes,4,opt,name=solution,proto3,oneof"` +} + func (*JoinRequest_ClientInit) isJoinRequest_Payload() {} func (*JoinRequest_TokenInit) isJoinRequest_Payload() {} +func (*JoinRequest_BoundKeypairInit) isJoinRequest_Payload() {} + +func (*JoinRequest_Solution) isJoinRequest_Payload() {} + // ServerInit is the first message sent from the server in response to the // ClientInit message. type ServerInit struct { @@ -548,7 +1014,7 @@ type ServerInit struct { func (x *ServerInit) Reset() { *x = ServerInit{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -560,7 +1026,7 @@ func (x *ServerInit) String() string { func (*ServerInit) ProtoMessage() {} func (x *ServerInit) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[7] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -573,7 +1039,7 @@ func (x *ServerInit) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerInit.ProtoReflect.Descriptor instead. func (*ServerInit) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{7} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{14} } func (x *ServerInit) GetJoinMethod() string { @@ -592,14 +1058,19 @@ func (x *ServerInit) GetSignatureAlgorithmSuite() string { // Challenge is a challenge message sent from the server that the client must solve. type Challenge struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *Challenge_BoundKeypairChallenge + // *Challenge_BoundKeypairRotationRequest + Payload isChallenge_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Challenge) Reset() { *x = Challenge{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -611,7 +1082,7 @@ func (x *Challenge) String() string { func (*Challenge) ProtoMessage() {} func (x *Challenge) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[8] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -624,9 +1095,50 @@ func (x *Challenge) ProtoReflect() protoreflect.Message { // Deprecated: Use Challenge.ProtoReflect.Descriptor instead. func (*Challenge) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{8} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{15} +} + +func (x *Challenge) GetPayload() isChallenge_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *Challenge) GetBoundKeypairChallenge() *BoundKeypairChallenge { + if x != nil { + if x, ok := x.Payload.(*Challenge_BoundKeypairChallenge); ok { + return x.BoundKeypairChallenge + } + } + return nil +} + +func (x *Challenge) GetBoundKeypairRotationRequest() *BoundKeypairRotationRequest { + if x != nil { + if x, ok := x.Payload.(*Challenge_BoundKeypairRotationRequest); ok { + return x.BoundKeypairRotationRequest + } + } + return nil +} + +type isChallenge_Payload interface { + isChallenge_Payload() +} + +type Challenge_BoundKeypairChallenge struct { + BoundKeypairChallenge *BoundKeypairChallenge `protobuf:"bytes,1,opt,name=bound_keypair_challenge,json=boundKeypairChallenge,proto3,oneof"` } +type Challenge_BoundKeypairRotationRequest struct { + BoundKeypairRotationRequest *BoundKeypairRotationRequest `protobuf:"bytes,2,opt,name=bound_keypair_rotation_request,json=boundKeypairRotationRequest,proto3,oneof"` +} + +func (*Challenge_BoundKeypairChallenge) isChallenge_Payload() {} + +func (*Challenge_BoundKeypairRotationRequest) isChallenge_Payload() {} + // Result is the final message sent from the cluster back to the client, it // contains the result of the joining process including the assigned host ID // and issued certificates. @@ -643,7 +1155,7 @@ type Result struct { func (x *Result) Reset() { *x = Result{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -655,7 +1167,7 @@ func (x *Result) String() string { func (*Result) ProtoMessage() {} func (x *Result) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[9] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -668,7 +1180,7 @@ func (x *Result) ProtoReflect() protoreflect.Message { // Deprecated: Use Result.ProtoReflect.Descriptor instead. func (*Result) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{9} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{16} } func (x *Result) GetPayload() isResult_Payload { @@ -731,7 +1243,7 @@ type Certificates struct { func (x *Certificates) Reset() { *x = Certificates{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -743,7 +1255,7 @@ func (x *Certificates) String() string { func (*Certificates) ProtoMessage() {} func (x *Certificates) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[10] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -756,7 +1268,7 @@ func (x *Certificates) ProtoReflect() protoreflect.Message { // Deprecated: Use Certificates.ProtoReflect.Descriptor instead. func (*Certificates) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{10} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{17} } func (x *Certificates) GetTlsCert() []byte { @@ -800,7 +1312,7 @@ type HostResult struct { func (x *HostResult) Reset() { *x = HostResult{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -812,7 +1324,7 @@ func (x *HostResult) String() string { func (*HostResult) ProtoMessage() {} func (x *HostResult) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[11] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -825,7 +1337,7 @@ func (x *HostResult) ProtoReflect() protoreflect.Message { // Deprecated: Use HostResult.ProtoReflect.Descriptor instead. func (*HostResult) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{11} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{18} } func (x *HostResult) GetCertificates() *Certificates { @@ -846,14 +1358,16 @@ func (x *HostResult) GetHostId() string { type BotResult struct { state protoimpl.MessageState `protogen:"open.v1"` // Certificates holds issued certificates and cluster CAs. - Certificates *Certificates `protobuf:"bytes,1,opt,name=certificates,proto3" json:"certificates,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Certificates *Certificates `protobuf:"bytes,1,opt,name=certificates,proto3" json:"certificates,omitempty"` + // BoundKeypairResult holds extra result parameters relevant to the bound keypair join method. + BoundKeypairResult *BoundKeypairResult `protobuf:"bytes,2,opt,name=bound_keypair_result,json=boundKeypairResult,proto3,oneof" json:"bound_keypair_result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BotResult) Reset() { *x = BotResult{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -865,7 +1379,7 @@ func (x *BotResult) String() string { func (*BotResult) ProtoMessage() {} func (x *BotResult) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[12] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -878,7 +1392,7 @@ func (x *BotResult) ProtoReflect() protoreflect.Message { // Deprecated: Use BotResult.ProtoReflect.Descriptor instead. func (*BotResult) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{12} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{19} } func (x *BotResult) GetCertificates() *Certificates { @@ -888,6 +1402,13 @@ func (x *BotResult) GetCertificates() *Certificates { return nil } +func (x *BotResult) GetBoundKeypairResult() *BoundKeypairResult { + if x != nil { + return x.BoundKeypairResult + } + return nil +} + // JoinResponse is the message type sent from the server to the joining client. type JoinResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -903,7 +1424,7 @@ type JoinResponse struct { func (x *JoinResponse) Reset() { *x = JoinResponse{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -915,7 +1436,7 @@ func (x *JoinResponse) String() string { func (*JoinResponse) ProtoMessage() {} func (x *JoinResponse) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[13] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -928,7 +1449,7 @@ func (x *JoinResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use JoinResponse.ProtoReflect.Descriptor instead. func (*JoinResponse) Descriptor() ([]byte, []int) { - return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{13} + return file_teleport_join_v1_joinservice_proto_rawDescGZIP(), []int{20} } func (x *JoinResponse) GetPayload() isJoinResponse_Payload { @@ -1011,7 +1532,7 @@ type ClientInit_ProxySuppliedParams struct { func (x *ClientInit_ProxySuppliedParams) Reset() { *x = ClientInit_ProxySuppliedParams{} - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1023,7 +1544,7 @@ func (x *ClientInit_ProxySuppliedParams) String() string { func (*ClientInit_ProxySuppliedParams) ProtoMessage() {} func (x *ClientInit_ProxySuppliedParams) ProtoReflect() protoreflect.Message { - mi := &file_teleport_join_v1_joinservice_proto_msgTypes[14] + mi := &file_teleport_join_v1_joinservice_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1098,19 +1619,48 @@ const file_teleport_join_v1_joinservice_proto_rawDesc = "" + "bot_params\x18\x02 \x01(\v2\x1b.teleport.join.v1.BotParamsH\x00R\tbotParamsB\t\n" + "\apayload\"P\n" + "\tTokenInit\x12C\n" + - "\rclient_params\x18\x01 \x01(\v2\x1e.teleport.join.v1.ClientParamsR\fclientParams\"\x97\x01\n" + + "\rclient_params\x18\x01 \x01(\v2\x1e.teleport.join.v1.ClientParamsR\fclientParams\"\xb7\x01\n" + + "\x10BoundKeypairInit\x12C\n" + + "\rclient_params\x18\x01 \x01(\v2\x1e.teleport.join.v1.ClientParamsR\fclientParams\x12.\n" + + "\x13initial_join_secret\x18\x02 \x01(\tR\x11initialJoinSecret\x12.\n" + + "\x13previous_join_state\x18\x03 \x01(\fR\x11previousJoinState\"T\n" + + "\x15BoundKeypairChallenge\x12\x1d\n" + + "\n" + + "public_key\x18\x01 \x01(\fR\tpublicKey\x12\x1c\n" + + "\tchallenge\x18\x02 \x01(\tR\tchallenge\";\n" + + "\x1dBoundKeypairChallengeSolution\x12\x1a\n" + + "\bsolution\x18\x01 \x01(\fR\bsolution\"Y\n" + + "\x1bBoundKeypairRotationRequest\x12:\n" + + "\x19signature_algorithm_suite\x18\x01 \x01(\tR\x17signatureAlgorithmSuite\"=\n" + + "\x1cBoundKeypairRotationResponse\x12\x1d\n" + + "\n" + + "public_key\x18\x01 \x01(\fR\tpublicKey\"R\n" + + "\x12BoundKeypairResult\x12\x1d\n" + + "\n" + + "join_state\x18\x02 \x01(\fR\tjoinState\x12\x1d\n" + + "\n" + + "public_key\x18\x03 \x01(\fR\tpublicKey\"\x93\x02\n" + + "\x11ChallengeSolution\x12z\n" + + " bound_keypair_challenge_solution\x18\x01 \x01(\v2/.teleport.join.v1.BoundKeypairChallengeSolutionH\x00R\x1dboundKeypairChallengeSolution\x12w\n" + + "\x1fbound_keypair_rotation_response\x18\x02 \x01(\v2..teleport.join.v1.BoundKeypairRotationResponseH\x00R\x1cboundKeypairRotationResponseB\t\n" + + "\apayload\"\xae\x02\n" + "\vJoinRequest\x12?\n" + "\vclient_init\x18\x01 \x01(\v2\x1c.teleport.join.v1.ClientInitH\x00R\n" + "clientInit\x12<\n" + "\n" + - "token_init\x18\x02 \x01(\v2\x1b.teleport.join.v1.TokenInitH\x00R\ttokenInitB\t\n" + + "token_init\x18\x02 \x01(\v2\x1b.teleport.join.v1.TokenInitH\x00R\ttokenInit\x12R\n" + + "\x12bound_keypair_init\x18\x03 \x01(\v2\".teleport.join.v1.BoundKeypairInitH\x00R\x10boundKeypairInit\x12A\n" + + "\bsolution\x18\x04 \x01(\v2#.teleport.join.v1.ChallengeSolutionH\x00R\bsolutionB\t\n" + "\apayload\"i\n" + "\n" + "ServerInit\x12\x1f\n" + "\vjoin_method\x18\x01 \x01(\tR\n" + "joinMethod\x12:\n" + - "\x19signature_algorithm_suite\x18\x02 \x01(\tR\x17signatureAlgorithmSuite\"\v\n" + - "\tChallenge\"\x92\x01\n" + + "\x19signature_algorithm_suite\x18\x02 \x01(\tR\x17signatureAlgorithmSuite\"\xef\x01\n" + + "\tChallenge\x12a\n" + + "\x17bound_keypair_challenge\x18\x01 \x01(\v2'.teleport.join.v1.BoundKeypairChallengeH\x00R\x15boundKeypairChallenge\x12t\n" + + "\x1ebound_keypair_rotation_request\x18\x02 \x01(\v2-.teleport.join.v1.BoundKeypairRotationRequestH\x00R\x1bboundKeypairRotationRequestB\t\n" + + "\apayload\"\x92\x01\n" + "\x06Result\x12?\n" + "\vhost_result\x18\x01 \x01(\v2\x1c.teleport.join.v1.HostResultH\x00R\n" + "hostResult\x12<\n" + @@ -1126,9 +1676,11 @@ const file_teleport_join_v1_joinservice_proto_rawDesc = "" + "\n" + "HostResult\x12B\n" + "\fcertificates\x18\x01 \x01(\v2\x1e.teleport.join.v1.CertificatesR\fcertificates\x12\x17\n" + - "\ahost_id\x18\x02 \x01(\tR\x06hostId\"O\n" + + "\ahost_id\x18\x02 \x01(\tR\x06hostId\"\xc5\x01\n" + "\tBotResult\x12B\n" + - "\fcertificates\x18\x01 \x01(\v2\x1e.teleport.join.v1.CertificatesR\fcertificates\"\xbe\x01\n" + + "\fcertificates\x18\x01 \x01(\v2\x1e.teleport.join.v1.CertificatesR\fcertificates\x12[\n" + + "\x14bound_keypair_result\x18\x02 \x01(\v2$.teleport.join.v1.BoundKeypairResultH\x00R\x12boundKeypairResult\x88\x01\x01B\x17\n" + + "\x15_bound_keypair_result\"\xbe\x01\n" + "\fJoinResponse\x122\n" + "\x04init\x18\x01 \x01(\v2\x1c.teleport.join.v1.ServerInitH\x00R\x04init\x12;\n" + "\tchallenge\x18\x02 \x01(\v2\x1b.teleport.join.v1.ChallengeH\x00R\tchallenge\x122\n" + @@ -1149,7 +1701,7 @@ func file_teleport_join_v1_joinservice_proto_rawDescGZIP() []byte { return file_teleport_join_v1_joinservice_proto_rawDescData } -var file_teleport_join_v1_joinservice_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_teleport_join_v1_joinservice_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_teleport_join_v1_joinservice_proto_goTypes = []any{ (*ClientInit)(nil), // 0: teleport.join.v1.ClientInit (*PublicKeys)(nil), // 1: teleport.join.v1.PublicKeys @@ -1157,41 +1709,56 @@ var file_teleport_join_v1_joinservice_proto_goTypes = []any{ (*BotParams)(nil), // 3: teleport.join.v1.BotParams (*ClientParams)(nil), // 4: teleport.join.v1.ClientParams (*TokenInit)(nil), // 5: teleport.join.v1.TokenInit - (*JoinRequest)(nil), // 6: teleport.join.v1.JoinRequest - (*ServerInit)(nil), // 7: teleport.join.v1.ServerInit - (*Challenge)(nil), // 8: teleport.join.v1.Challenge - (*Result)(nil), // 9: teleport.join.v1.Result - (*Certificates)(nil), // 10: teleport.join.v1.Certificates - (*HostResult)(nil), // 11: teleport.join.v1.HostResult - (*BotResult)(nil), // 12: teleport.join.v1.BotResult - (*JoinResponse)(nil), // 13: teleport.join.v1.JoinResponse - (*ClientInit_ProxySuppliedParams)(nil), // 14: teleport.join.v1.ClientInit.ProxySuppliedParams - (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp + (*BoundKeypairInit)(nil), // 6: teleport.join.v1.BoundKeypairInit + (*BoundKeypairChallenge)(nil), // 7: teleport.join.v1.BoundKeypairChallenge + (*BoundKeypairChallengeSolution)(nil), // 8: teleport.join.v1.BoundKeypairChallengeSolution + (*BoundKeypairRotationRequest)(nil), // 9: teleport.join.v1.BoundKeypairRotationRequest + (*BoundKeypairRotationResponse)(nil), // 10: teleport.join.v1.BoundKeypairRotationResponse + (*BoundKeypairResult)(nil), // 11: teleport.join.v1.BoundKeypairResult + (*ChallengeSolution)(nil), // 12: teleport.join.v1.ChallengeSolution + (*JoinRequest)(nil), // 13: teleport.join.v1.JoinRequest + (*ServerInit)(nil), // 14: teleport.join.v1.ServerInit + (*Challenge)(nil), // 15: teleport.join.v1.Challenge + (*Result)(nil), // 16: teleport.join.v1.Result + (*Certificates)(nil), // 17: teleport.join.v1.Certificates + (*HostResult)(nil), // 18: teleport.join.v1.HostResult + (*BotResult)(nil), // 19: teleport.join.v1.BotResult + (*JoinResponse)(nil), // 20: teleport.join.v1.JoinResponse + (*ClientInit_ProxySuppliedParams)(nil), // 21: teleport.join.v1.ClientInit.ProxySuppliedParams + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp } var file_teleport_join_v1_joinservice_proto_depIdxs = []int32{ - 14, // 0: teleport.join.v1.ClientInit.proxy_supplied_parameters:type_name -> teleport.join.v1.ClientInit.ProxySuppliedParams + 21, // 0: teleport.join.v1.ClientInit.proxy_supplied_parameters:type_name -> teleport.join.v1.ClientInit.ProxySuppliedParams 1, // 1: teleport.join.v1.HostParams.public_keys:type_name -> teleport.join.v1.PublicKeys 1, // 2: teleport.join.v1.BotParams.public_keys:type_name -> teleport.join.v1.PublicKeys - 15, // 3: teleport.join.v1.BotParams.expires:type_name -> google.protobuf.Timestamp + 22, // 3: teleport.join.v1.BotParams.expires:type_name -> google.protobuf.Timestamp 2, // 4: teleport.join.v1.ClientParams.host_params:type_name -> teleport.join.v1.HostParams 3, // 5: teleport.join.v1.ClientParams.bot_params:type_name -> teleport.join.v1.BotParams 4, // 6: teleport.join.v1.TokenInit.client_params:type_name -> teleport.join.v1.ClientParams - 0, // 7: teleport.join.v1.JoinRequest.client_init:type_name -> teleport.join.v1.ClientInit - 5, // 8: teleport.join.v1.JoinRequest.token_init:type_name -> teleport.join.v1.TokenInit - 11, // 9: teleport.join.v1.Result.host_result:type_name -> teleport.join.v1.HostResult - 12, // 10: teleport.join.v1.Result.bot_result:type_name -> teleport.join.v1.BotResult - 10, // 11: teleport.join.v1.HostResult.certificates:type_name -> teleport.join.v1.Certificates - 10, // 12: teleport.join.v1.BotResult.certificates:type_name -> teleport.join.v1.Certificates - 7, // 13: teleport.join.v1.JoinResponse.init:type_name -> teleport.join.v1.ServerInit - 8, // 14: teleport.join.v1.JoinResponse.challenge:type_name -> teleport.join.v1.Challenge - 9, // 15: teleport.join.v1.JoinResponse.result:type_name -> teleport.join.v1.Result - 6, // 16: teleport.join.v1.JoinService.Join:input_type -> teleport.join.v1.JoinRequest - 13, // 17: teleport.join.v1.JoinService.Join:output_type -> teleport.join.v1.JoinResponse - 17, // [17:18] is the sub-list for method output_type - 16, // [16:17] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 4, // 7: teleport.join.v1.BoundKeypairInit.client_params:type_name -> teleport.join.v1.ClientParams + 8, // 8: teleport.join.v1.ChallengeSolution.bound_keypair_challenge_solution:type_name -> teleport.join.v1.BoundKeypairChallengeSolution + 10, // 9: teleport.join.v1.ChallengeSolution.bound_keypair_rotation_response:type_name -> teleport.join.v1.BoundKeypairRotationResponse + 0, // 10: teleport.join.v1.JoinRequest.client_init:type_name -> teleport.join.v1.ClientInit + 5, // 11: teleport.join.v1.JoinRequest.token_init:type_name -> teleport.join.v1.TokenInit + 6, // 12: teleport.join.v1.JoinRequest.bound_keypair_init:type_name -> teleport.join.v1.BoundKeypairInit + 12, // 13: teleport.join.v1.JoinRequest.solution:type_name -> teleport.join.v1.ChallengeSolution + 7, // 14: teleport.join.v1.Challenge.bound_keypair_challenge:type_name -> teleport.join.v1.BoundKeypairChallenge + 9, // 15: teleport.join.v1.Challenge.bound_keypair_rotation_request:type_name -> teleport.join.v1.BoundKeypairRotationRequest + 18, // 16: teleport.join.v1.Result.host_result:type_name -> teleport.join.v1.HostResult + 19, // 17: teleport.join.v1.Result.bot_result:type_name -> teleport.join.v1.BotResult + 17, // 18: teleport.join.v1.HostResult.certificates:type_name -> teleport.join.v1.Certificates + 17, // 19: teleport.join.v1.BotResult.certificates:type_name -> teleport.join.v1.Certificates + 11, // 20: teleport.join.v1.BotResult.bound_keypair_result:type_name -> teleport.join.v1.BoundKeypairResult + 14, // 21: teleport.join.v1.JoinResponse.init:type_name -> teleport.join.v1.ServerInit + 15, // 22: teleport.join.v1.JoinResponse.challenge:type_name -> teleport.join.v1.Challenge + 16, // 23: teleport.join.v1.JoinResponse.result:type_name -> teleport.join.v1.Result + 13, // 24: teleport.join.v1.JoinService.Join:input_type -> teleport.join.v1.JoinRequest + 20, // 25: teleport.join.v1.JoinService.Join:output_type -> teleport.join.v1.JoinResponse + 25, // [25:26] is the sub-list for method output_type + 24, // [24:25] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_teleport_join_v1_joinservice_proto_init() } @@ -1205,15 +1772,26 @@ func file_teleport_join_v1_joinservice_proto_init() { (*ClientParams_HostParams)(nil), (*ClientParams_BotParams)(nil), } - file_teleport_join_v1_joinservice_proto_msgTypes[6].OneofWrappers = []any{ + file_teleport_join_v1_joinservice_proto_msgTypes[12].OneofWrappers = []any{ + (*ChallengeSolution_BoundKeypairChallengeSolution)(nil), + (*ChallengeSolution_BoundKeypairRotationResponse)(nil), + } + file_teleport_join_v1_joinservice_proto_msgTypes[13].OneofWrappers = []any{ (*JoinRequest_ClientInit)(nil), (*JoinRequest_TokenInit)(nil), + (*JoinRequest_BoundKeypairInit)(nil), + (*JoinRequest_Solution)(nil), } - file_teleport_join_v1_joinservice_proto_msgTypes[9].OneofWrappers = []any{ + file_teleport_join_v1_joinservice_proto_msgTypes[15].OneofWrappers = []any{ + (*Challenge_BoundKeypairChallenge)(nil), + (*Challenge_BoundKeypairRotationRequest)(nil), + } + file_teleport_join_v1_joinservice_proto_msgTypes[16].OneofWrappers = []any{ (*Result_HostResult)(nil), (*Result_BotResult)(nil), } - file_teleport_join_v1_joinservice_proto_msgTypes[13].OneofWrappers = []any{ + file_teleport_join_v1_joinservice_proto_msgTypes[19].OneofWrappers = []any{} + file_teleport_join_v1_joinservice_proto_msgTypes[20].OneofWrappers = []any{ (*JoinResponse_Init)(nil), (*JoinResponse_Challenge)(nil), (*JoinResponse_Result)(nil), @@ -1224,7 +1802,7 @@ func file_teleport_join_v1_joinservice_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_join_v1_joinservice_proto_rawDesc), len(file_teleport_join_v1_joinservice_proto_rawDesc)), NumEnums: 0, - NumMessages: 15, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/teleport/join/v1/joinservice.proto b/api/proto/teleport/join/v1/joinservice.proto index 5250c0315d403..cf9a6aa7cec28 100644 --- a/api/proto/teleport/join/v1/joinservice.proto +++ b/api/proto/teleport/join/v1/joinservice.proto @@ -107,11 +107,104 @@ message TokenInit { ClientParams client_params = 1; } +// BoundKeypairInit is sent from the client in response to the ServerInit +// message for the bound keypair join method. +// The server is expected to respond with a BoundKeypairChallenge. +// +// The bound keypair method join flow is: +// 1. client->server: ClientInit +// 2. server->client: ServerInit +// 3. client->server: BoundKeypairInit +// 4. server->client: BoundKeypairChallenge +// 5. client->server: BoundKeypairChallengeSolution +// (optional additional steps if keypair rotation is required) +// server->client: BoundKeypairRotationRequest +// client->server: BoundKeypairRotationResponse +// server->client: BoundKeypairChallenge +// client->server: BoundKeypairChallengeSolution +// 6. server->client: Result containing BoundKeypairResult +message BoundKeypairInit { + // ClientParams holds parameters for the specific type of client trying to join. + ClientParams client_params = 1; + // If set, attempts to bind a new keypair using an initial join secret. + // Any value set here will be ignored if a keypair is already bound. + string initial_join_secret = 2; + // A document signed by Auth containing join state parameters from the + // previous join attempt. Not required on initial join; required on all + // subsequent joins. + bytes previous_join_state = 3; +} + +// BoundKeypairChallenge is a challenge issued by the server that joining +// clients are expected to complete. +// The client is expected to respond with a BoundKeypairChallengeSolution. +message BoundKeypairChallenge { + // The desired public key corresponding to the private key that should be used + // to sign this challenge, in SSH authorized keys format. + bytes public_key = 1; + // A challenge to sign with the requested public key. During keypair rotation, + // a second challenge will be provided to verify the new keypair before certs + // are returned. + string challenge = 2; +} + +// BoundKeypairChallengeSolution is sent from the client in response to the +// BoundKeypairChallenge. +// The server is expected to respond with either a Result or a +// BoundKeypairRotationRequest. +message BoundKeypairChallengeSolution { + // A solution to a challenge from the server. This generated by signing the + // challenge as a JWT using the keypair associated with the requested public + // key. + bytes solution = 1; +} + +// BoundKeypairRotationRequest is sent by the server in response to a +// BoundKeypairChallenge when a keypair rotation is required. It acts like an +// additional challenge, the client is expected to respond with a +// BoundKeypairRotationResponse. +message BoundKeypairRotationRequest { + // The signature algorithm suite in use by the cluster. + string signature_algorithm_suite = 1; +} + +// BoundKeypairRotationResponse is sent by the client in response to a +// BoundKeypairRotationRequest from the server. +// The server is expected to respond with an additional BoundKeypairChallenge +// for the new key. +message BoundKeypairRotationResponse { + // The public key to be registered with auth. Clients should expect a + // subsequent challenge against this public key to be sent. This is encoded in + // SSH authorized keys format. + bytes public_key = 1; +} + +// BoundKeypairResult holds additional result parameters relevant to the bound +// keypair join method. +message BoundKeypairResult { + // A signed join state document to be provided on the next join attempt. + bytes join_state = 2; + // The public key registered with Auth at the end of the joining ceremony. + // After a successful keypair rotation, this should reflect the newly + // registered public key. This is encoded in SSH authorized keys format. + bytes public_key = 3; +} + +// ChallengeSolution holds a solution to a challenge issued by the server. +message ChallengeSolution { + oneof payload { + BoundKeypairChallengeSolution bound_keypair_challenge_solution = 1; + BoundKeypairRotationResponse bound_keypair_rotation_response = 2; + } +} + // JoinRequest is the message type sent from the joining client to the server. message JoinRequest { oneof payload { ClientInit client_init = 1; TokenInit token_init = 2; + BoundKeypairInit bound_keypair_init = 3; + ChallengeSolution solution = 4; } } @@ -126,7 +219,12 @@ message ServerInit { } // Challenge is a challenge message sent from the server that the client must solve. -message Challenge {} +message Challenge { + oneof payload { + BoundKeypairChallenge bound_keypair_challenge = 1; + BoundKeypairRotationRequest bound_keypair_rotation_request = 2; + } +} // Result is the final message sent from the cluster back to the client, it // contains the result of the joining process including the assigned host ID @@ -164,6 +262,8 @@ message HostResult { message BotResult { // Certificates holds issued certificates and cluster CAs. Certificates certificates = 1; + // BoundKeypairResult holds extra result parameters relevant to the bound keypair join method. + optional BoundKeypairResult bound_keypair_result = 2; } // JoinResponse is the message type sent from the server to the joining client. diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 8b701efcdeb1e..f48dd1e710ad2 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -118,6 +118,7 @@ import ( "github.com/gravitational/teleport/lib/integrations/awsra/createsession" "github.com/gravitational/teleport/lib/inventory" iterstream "github.com/gravitational/teleport/lib/itertools/stream" + joinboundkeypair "github.com/gravitational/teleport/lib/join/boundkeypair" kubetoken "github.com/gravitational/teleport/lib/kube/token" "github.com/gravitational/teleport/lib/limiter" "github.com/gravitational/teleport/lib/loginrule" @@ -780,7 +781,7 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (as *Server, err error) { } if as.createBoundKeypairValidator == nil { - as.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) { + as.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) { return boundkeypair.NewChallengeValidator(subject, clusterName, publicKey) } } @@ -1267,7 +1268,7 @@ type Server struct { // createBoundKeypairValidator is a helper to create new bound keypair // challenge validators. Used to override the implementation used in tests. - createBoundKeypairValidator createBoundKeypairValidator + createBoundKeypairValidator joinboundkeypair.CreateBoundKeypairValidator // loadAllCAs tells tsh to load the host CAs for all clusters when trying to ssh into a node. loadAllCAs bool @@ -1492,6 +1493,12 @@ func (a *Server) SetLockWatcher(lockWatcher *services.LockWatcher) { a.lockWatcher = lockWatcher } +// CheckLockInForce returns an AccessDenied error if there is a lock in force +// matching at least one of the targets. +func (a *Server) CheckLockInForce(mode constants.LockingMode, targets []types.LockTarget) error { + return a.checkLockInForce(mode, targets) +} + func (a *Server) checkLockInForce(mode constants.LockingMode, targets []types.LockTarget) error { a.lock.RLock() defer a.lock.RUnlock() diff --git a/lib/auth/bound_keypair_tokens.go b/lib/auth/bound_keypair_tokens.go new file mode 100644 index 0000000000000..82af4823345e5 --- /dev/null +++ b/lib/auth/bound_keypair_tokens.go @@ -0,0 +1,148 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package auth + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils" +) + +// validateBoundKeypairTokenSpec performs some basic validation checks on a +// bound_keypair-type join token. +func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair) error { + if spec.Recovery == nil { + return trace.BadParameter("spec.bound_keypair.recovery: field is required") + } + + return nil +} + +// populateRegistrationSecret populates the +// `status.BoundKeypair.RegistrationSecret` field of a bound keypair token. It +// should be called as part of any token creation or update to ensure the +// registration secret is made available if needed. +func populateRegistrationSecret(v2 *types.ProvisionTokenV2) error { + if v2.GetJoinMethod() != types.JoinMethodBoundKeypair { + return trace.BadParameter("must be called with a bound keypair token") + } + + if v2.Spec.BoundKeypair == nil { + v2.Spec.BoundKeypair = &types.ProvisionTokenSpecV2BoundKeypair{} + } + + if v2.Status == nil { + v2.Status = &types.ProvisionTokenStatusV2{} + } + if v2.Status.BoundKeypair == nil { + v2.Status.BoundKeypair = &types.ProvisionTokenStatusV2BoundKeypair{} + } + if v2.Spec.BoundKeypair.Onboarding == nil { + v2.Spec.BoundKeypair.Onboarding = &types.ProvisionTokenSpecV2BoundKeypair_OnboardingSpec{} + } + + spec := v2.Spec.BoundKeypair + status := v2.Status.BoundKeypair + + if status.BoundPublicKey != "" || spec.Onboarding.InitialPublicKey != "" { + // A key has already been bound or preregistered, nothing to do. + return nil + } + + if status.RegistrationSecret != "" { + // A secret has already been generated, nothing to do. + return nil + } + + if spec.Onboarding.RegistrationSecret != "" { + // An explicit registration secret was provided, so copy it to status. + status.RegistrationSecret = spec.Onboarding.RegistrationSecret + return nil + } + + // Otherwise, we have no key and no secret, so generate one now. + s, err := utils.CryptoRandomHex(defaults.TokenLenBytes) + if err != nil { + return trace.Wrap(err) + } + + status.RegistrationSecret = s + return nil +} + +func (a *Server) CreateBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error { + if token.GetJoinMethod() != types.JoinMethodBoundKeypair { + return trace.BadParameter("must be called with a bound keypair token") + } + + tokenV2, ok := token.(*types.ProvisionTokenV2) + if !ok { + return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle) + } + + spec := tokenV2.Spec.BoundKeypair + if spec == nil { + return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair") + } + + if err := validateBoundKeypairTokenSpec(spec); err != nil { + return trace.Wrap(err) + } + + // Not as much to do here - ideally we'd like to prevent users from + // tampering with the status field, but we don't have a good mechanism to + // stop that that wouldn't also break backup and restore. For now, it's + // simpler and easier to just tell users not to edit those fields. + + if err := populateRegistrationSecret(tokenV2); err != nil { + return trace.Wrap(err) + } + + return trace.Wrap(a.CreateToken(ctx, tokenV2)) +} + +func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error { + if token.GetJoinMethod() != types.JoinMethodBoundKeypair { + return trace.BadParameter("must be called with a bound keypair token") + } + + tokenV2, ok := token.(*types.ProvisionTokenV2) + if !ok { + return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle) + } + + spec := tokenV2.Spec.BoundKeypair + if spec == nil { + return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair") + } + + if err := validateBoundKeypairTokenSpec(spec); err != nil { + return trace.Wrap(err) + } + + if err := populateRegistrationSecret(tokenV2); err != nil { + return trace.Wrap(err) + } + + // Implementation note: checkAndSetDefaults() impl for this token type is + // called at insertion time as part of `tokenToItem()` + return trace.Wrap(a.UpsertToken(ctx, token)) +} diff --git a/lib/auth/export_test.go b/lib/auth/export_test.go index d962ec391aa96..f11dd8278f066 100644 --- a/lib/auth/export_test.go +++ b/lib/auth/export_test.go @@ -18,7 +18,6 @@ package auth import ( "context" - "crypto" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -46,6 +45,7 @@ import ( "github.com/gravitational/teleport/lib/circleci" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/inventory" + "github.com/gravitational/teleport/lib/join/boundkeypair" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tpm" "github.com/gravitational/teleport/lib/utils" @@ -249,14 +249,8 @@ func (a *Server) SetGHAIDTokenJWKSValidator(validator ghaIDTokenJWKSValidator) { a.ghaIDTokenJWKSValidator = validator } -type BoundKeypairValidator = boundKeypairValidator - -type CreateBoundKeypairValidator func(subject string, clusterName string, publicKey crypto.PublicKey) (BoundKeypairValidator, error) - -func (a *Server) SetCreateBoundKeypairValidator(validator CreateBoundKeypairValidator) { - a.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) { - return validator(subject, clusterName, publicKey) - } +func (a *Server) SetCreateBoundKeypairValidator(validator boundkeypair.CreateBoundKeypairValidator) { + a.createBoundKeypairValidator = validator } func (a *Server) AuthenticateUserLogin(ctx context.Context, req authclient.AuthenticateUserRequest) (services.UserState, *services.SplitAccessChecker, error) { diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index de42805c8484c..4828ccd3bb6d6 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -5850,6 +5850,7 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { joinv1.RegisterJoinServiceServer(server, join.NewServer(&join.ServerConfig{ Authorizer: cfg.Authorizer, AuthService: cfg.AuthServer, + Clock: cfg.AuthServer.clock, })) integrationServiceServer, err := integrationv1.NewService(&integrationv1.ServiceConfig{ diff --git a/lib/auth/join_bound_keypair_adapter.go b/lib/auth/join_bound_keypair_adapter.go new file mode 100644 index 0000000000000..dce5da8b59082 --- /dev/null +++ b/lib/auth/join_bound_keypair_adapter.go @@ -0,0 +1,38 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package auth + +import ( + "context" + + "github.com/gravitational/teleport/api/client" + "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/lib/join" +) + +// RegisterUsingBoundKeypairMethod handles bound keypair joining for the legacy +// join service and accepts the legacy protobuf message types. It calls into +// the common logic implemented in lib/join. +// +// TODO(nklaassen): DELETE IN 20 when removing the legacy join service. +func (a *Server) RegisterUsingBoundKeypairMethod( + ctx context.Context, + req *proto.RegisterUsingBoundKeypairInitialRequest, + challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc, +) (*client.BoundKeypairRegistrationResponse, error) { + return join.AdaptRegisterUsingBoundKeypairMethod(ctx, a, a.createBoundKeypairValidator, req, challengeResponse) +} diff --git a/lib/auth/join_bound_keypair_test.go b/lib/auth/join_bound_keypair_test.go index 0c2270ea5c4c5..551ef59d8e367 100644 --- a/lib/auth/join_bound_keypair_test.go +++ b/lib/auth/join_bound_keypair_test.go @@ -37,11 +37,11 @@ import ( headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/authtest" "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/boundkeypair" "github.com/gravitational/teleport/lib/cryptosuites" + joinboundkeypair "github.com/gravitational/teleport/lib/join/boundkeypair" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/tlsca" ) @@ -113,7 +113,7 @@ func TestServer_RegisterUsingBoundKeypairMethod(t *testing.T) { srv := newTestTLSServer(t, withClock(clock)) authServer := srv.Auth() - authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) { + authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) { return &mockBoundKeypairValidator{ subject: subject, clusterName: clusterName, @@ -960,7 +960,7 @@ func TestServer_RegisterUsingBoundKeypairMethod_GenerationCounter(t *testing.T) srv := newTestTLSServer(t, withClock(clock)) authServer := srv.Auth() - authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) { + authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) { return &mockBoundKeypairValidator{ subject: subject, clusterName: clusterName, @@ -1167,7 +1167,7 @@ func TestServer_RegisterUsingBoundKeypairMethod_JoinStateFailure(t *testing.T) { srv := newTestTLSServer(t, withClock(clock)) authServer := srv.Auth() - authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) { + authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) { return &mockBoundKeypairValidator{ subject: subject, clusterName: clusterName, @@ -1323,7 +1323,7 @@ func TestServer_RegisterUsingBoundKeypairMethod_JoinStateFailureDuringRenewal(t srv := newTestTLSServer(t, withClock(clock)) authServer := srv.Auth() - authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (auth.BoundKeypairValidator, error) { + authServer.SetCreateBoundKeypairValidator(func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) { return &mockBoundKeypairValidator{ subject: subject, clusterName: clusterName, diff --git a/lib/join/adapters.go b/lib/join/adapters.go new file mode 100644 index 0000000000000..4c4bcf1c9cfc4 --- /dev/null +++ b/lib/join/adapters.go @@ -0,0 +1,118 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package join + +import ( + "encoding/pem" + + "github.com/gravitational/trace" + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/join/internal/messages" +) + +func clientInitFromRegisterUsingTokenRequest(req *types.RegisterUsingTokenRequest, joinMethod string) *messages.ClientInit { + return &messages.ClientInit{ + JoinMethod: &joinMethod, + TokenName: req.Token, + SystemRole: req.Role.String(), + } +} + +func clientParamsFromRegisterUsingTokenRequest(req *types.RegisterUsingTokenRequest) (*messages.ClientParams, error) { + rawTLSPub, _ := pem.Decode(req.PublicTLSKey) + if rawTLSPub == nil { + return nil, trace.BadParameter("failed to decode PublicTLSKey from PEM") + } + sshPub, _, _, _, err := ssh.ParseAuthorizedKey(req.PublicSSHKey) + if err != nil { + return nil, trace.Wrap(err, "parsing PublicSSHKey from authorized_keys format") + } + publicKeys := messages.PublicKeys{ + PublicTLSKey: rawTLSPub.Bytes, + PublicSSHKey: sshPub.Marshal(), + } + var clientParams messages.ClientParams + if req.Role == types.RoleBot { + clientParams.BotParams = &messages.BotParams{ + PublicKeys: publicKeys, + Expires: req.Expires, + } + } else { + clientParams.HostParams = &messages.HostParams{ + PublicKeys: publicKeys, + HostName: req.NodeName, + AdditionalPrincipals: req.AdditionalPrincipals, + DNSNames: req.DNSNames, + } + } + return &clientParams, nil +} + +func protoCertsFromCertificates(certs messages.Certificates) (*proto.Certs, error) { + sshCert, err := sshPubWireFormatToAuthorizedKey(certs.SSHCert) + if err != nil { + return nil, trace.Wrap(err) + } + sshCAKeys, err := sshPubWireFormatsToAuthorizedKeys(certs.SSHCAKeys) + if err != nil { + return nil, trace.Wrap(err) + } + return &proto.Certs{ + TLS: pemEncodeTLSCert(certs.TLSCert), + TLSCACerts: pemEncodeTLSCerts(certs.TLSCACerts), + SSH: sshCert, + SSHCACerts: sshCAKeys, // SSHCACerts is a misnomer, they're just public keys. + }, nil +} + +func pemEncodeTLSCerts(rawCerts [][]byte) [][]byte { + out := make([][]byte, len(rawCerts)) + for i, rawCert := range rawCerts { + out[i] = pemEncodeTLSCert(rawCert) + } + return out +} + +func pemEncodeTLSCert(rawCert []byte) []byte { + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: rawCert, + }) +} + +func sshPubWireFormatsToAuthorizedKeys(wireFormats [][]byte) ([][]byte, error) { + out := make([][]byte, len(wireFormats)) + for i, wireFormat := range wireFormats { + var err error + out[i], err = sshPubWireFormatToAuthorizedKey(wireFormat) + if err != nil { + return nil, trace.Wrap(err) + } + } + return out, nil +} + +func sshPubWireFormatToAuthorizedKey(wireFormat []byte) ([]byte, error) { + pub, err := ssh.ParsePublicKey(wireFormat) + if err != nil { + return nil, trace.Wrap(err) + } + return ssh.MarshalAuthorizedKey(pub), nil +} diff --git a/lib/auth/join_bound_keypair.go b/lib/join/boundkeypair/boundkeypair.go similarity index 66% rename from lib/auth/join_bound_keypair.go rename to lib/join/boundkeypair/boundkeypair.go index 9a168bacd3f04..dd9c8527429a3 100644 --- a/lib/auth/join_bound_keypair.go +++ b/lib/join/boundkeypair/boundkeypair.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package auth +package boundkeypair import ( "context" @@ -29,155 +29,37 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" - "github.com/gravitational/teleport/api/client" - "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/lib/auth/keystore" "github.com/gravitational/teleport/lib/boundkeypair" - "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" + "github.com/gravitational/teleport/lib/join/internal/authz" + "github.com/gravitational/teleport/lib/join/internal/diagnostic" + "github.com/gravitational/teleport/lib/join/internal/messages" "github.com/gravitational/teleport/lib/jwt" + "github.com/gravitational/teleport/lib/services/readonly" libsshutils "github.com/gravitational/teleport/lib/sshutils" - "github.com/gravitational/teleport/lib/utils" ) -type boundKeypairValidator interface { +type BoundKeypairValidator interface { IssueChallenge() (*boundkeypair.ChallengeDocument, error) ValidateChallengeResponse(issued *boundkeypair.ChallengeDocument, compactResponse string) error } -type createBoundKeypairValidator func(subject string, clusterName string, publicKey crypto.PublicKey) (boundKeypairValidator, error) - -// validateBoundKeypairTokenSpec performs some basic validation checks on a -// bound_keypair-type join token. -func validateBoundKeypairTokenSpec(spec *types.ProvisionTokenSpecV2BoundKeypair) error { - if spec.Recovery == nil { - return trace.BadParameter("spec.bound_keypair.recovery: field is required") - } - - return nil -} - -// populateRegistrationSecret populates the -// `status.BoundKeypair.RegistrationSecret` field of a bound keypair token. It -// should be called as part of any token creation or update to ensure the -// registration secret is made available if needed. -func populateRegistrationSecret(v2 *types.ProvisionTokenV2) error { - if v2.GetJoinMethod() != types.JoinMethodBoundKeypair { - return trace.BadParameter("must be called with a bound keypair token") - } - - if v2.Spec.BoundKeypair == nil { - v2.Spec.BoundKeypair = &types.ProvisionTokenSpecV2BoundKeypair{} - } - if v2.Spec.BoundKeypair.Onboarding == nil { - v2.Spec.BoundKeypair.Onboarding = &types.ProvisionTokenSpecV2BoundKeypair_OnboardingSpec{} - } - - if v2.Status == nil { - v2.Status = &types.ProvisionTokenStatusV2{} - } - if v2.Status.BoundKeypair == nil { - v2.Status.BoundKeypair = &types.ProvisionTokenStatusV2BoundKeypair{} - } - - spec := v2.Spec.BoundKeypair - status := v2.Status.BoundKeypair - - if status.BoundPublicKey != "" || spec.Onboarding.InitialPublicKey != "" { - // A key has already been bound or preregistered, nothing to do. - return nil - } - - if status.RegistrationSecret != "" { - // A secret has already been generated, nothing to do. - return nil - } - - if spec.Onboarding.RegistrationSecret != "" { - // An explicit registration secret was provided, so copy it to status. - status.RegistrationSecret = spec.Onboarding.RegistrationSecret - return nil - } - - // Otherwise, we have no key and no secret, so generate one now. - s, err := utils.CryptoRandomHex(defaults.TokenLenBytes) - if err != nil { - return trace.Wrap(err) - } - - status.RegistrationSecret = s - return nil -} - -func (a *Server) CreateBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error { - if token.GetJoinMethod() != types.JoinMethodBoundKeypair { - return trace.BadParameter("must be called with a bound keypair token") - } - - tokenV2, ok := token.(*types.ProvisionTokenV2) - if !ok { - return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle) - } - - spec := tokenV2.Spec.BoundKeypair - if spec == nil { - return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair") - } - - if err := validateBoundKeypairTokenSpec(spec); err != nil { - return trace.Wrap(err) - } - - // Not as much to do here - ideally we'd like to prevent users from - // tampering with the status field, but we don't have a good mechanism to - // stop that that wouldn't also break backup and restore. For now, it's - // simpler and easier to just tell users not to edit those fields. - - if err := populateRegistrationSecret(tokenV2); err != nil { - return trace.Wrap(err) - } - - return trace.Wrap(a.CreateToken(ctx, tokenV2)) -} - -func (a *Server) UpsertBoundKeypairToken(ctx context.Context, token types.ProvisionToken) error { - if token.GetJoinMethod() != types.JoinMethodBoundKeypair { - return trace.BadParameter("must be called with a bound keypair token") - } - - tokenV2, ok := token.(*types.ProvisionTokenV2) - if !ok { - return trace.BadParameter("%v join method requires ProvisionTokenV2", types.JoinMethodOracle) - } - - spec := tokenV2.Spec.BoundKeypair - if spec == nil { - return trace.BadParameter("bound_keypair token requires non-nil spec.bound_keypair") - } - - if err := validateBoundKeypairTokenSpec(spec); err != nil { - return trace.Wrap(err) - } - - if err := populateRegistrationSecret(tokenV2); err != nil { - return trace.Wrap(err) - } - - // Implementation note: checkAndSetDefaults() impl for this token type is - // called at insertion time as part of `tokenToItem()` - return trace.Wrap(a.UpsertToken(ctx, token)) -} +type CreateBoundKeypairValidator func(subject string, clusterName string, publicKey crypto.PublicKey) (BoundKeypairValidator, error) // issueBoundKeypairChallenge creates a new challenge for the given marshaled // public key in ssh authorized_keys format, requests a solution from the // client using the given `challengeResponse` function, and validates the // response. -func (a *Server) issueBoundKeypairChallenge( +func issueBoundKeypairChallenge( ctx context.Context, + params *JoinParams, marshaledKey string, - challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc, ) error { key, err := libsshutils.CryptoPublicKey([]byte(marshaledKey)) if err != nil { @@ -193,14 +75,14 @@ func (a *Server) issueBoundKeypairChallenge( return trace.Wrap(err, "determining the key ID") } - clusterName, err := a.GetClusterName(ctx) + clusterName, err := params.AuthService.GetClusterName(ctx) if err != nil { return trace.Wrap(err) } - a.logger.DebugContext(ctx, "issuing bound keypair challenge", "key_id", keyID) + params.Logger.DebugContext(ctx, "issuing bound keypair challenge", "key_id", keyID) - validator, err := a.createBoundKeypairValidator(keyID, clusterName.GetClusterName(), key) + validator, err := params.CreateBoundKeypairValidator(keyID, clusterName.GetClusterName(), key) if err != nil { return trace.Wrap(err) } @@ -215,32 +97,23 @@ func (a *Server) issueBoundKeypairChallenge( return trace.Wrap(err) } - response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{ - Response: &proto.RegisterUsingBoundKeypairMethodResponse_Challenge{ - Challenge: &proto.RegisterUsingBoundKeypairChallenge{ - PublicKey: marshaledKey, - Challenge: string(marshalledChallenge), - }, - }, + solution, err := params.IssueChallenge(&messages.BoundKeypairChallenge{ + PublicKey: []byte(marshaledKey), + Challenge: string(marshalledChallenge), }) if err != nil { return trace.Wrap(err, "requesting a signed challenge") } - solutionResponse, ok := response.Payload.(*proto.RegisterUsingBoundKeypairMethodRequest_ChallengeResponse) - if !ok { - return trace.BadParameter("client provided unexpected challenge response type %T", response.Payload) - } - if err := validator.ValidateChallengeResponse( challenge, - string(solutionResponse.ChallengeResponse.Solution), + string(solution.Solution), ); err != nil { // TODO: Consider access denied instead? return trace.Wrap(err, "validating challenge response") } - a.logger.InfoContext(ctx, "bound keypair challenge response verified successfully", "key_id", keyID) + params.Logger.InfoContext(ctx, "bound keypair challenge response verified successfully", "key_id", keyID) return nil } @@ -300,37 +173,29 @@ func ensurePublicKeysNotEqual(a, b string) error { // requestBoundKeypairRotation requests that clients generate a new keypair and // send the public key, then issues a signing challenge to ensure ownership of // the new key. -func (a *Server) requestBoundKeypairRotation( +func requestBoundKeypairRotation( ctx context.Context, - challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc, + params *JoinParams, ) (string, error) { - cap, err := a.GetAuthPreference(ctx) + cap, err := params.AuthService.GetAuthPreference(ctx) if err != nil { return "", trace.Wrap(err) } - a.logger.InfoContext(ctx, "requesting bound keypair rotation", "suite", cap.GetSignatureAlgorithmSuite()) + params.Logger.InfoContext(ctx, "requesting bound keypair rotation", "suite", cap.GetSignatureAlgorithmSuite()) // Request a new marshaled public key from the client. - response, err := challengeResponse(&proto.RegisterUsingBoundKeypairMethodResponse{ - Response: &proto.RegisterUsingBoundKeypairMethodResponse_Rotation{ - Rotation: &proto.RegisterUsingBoundKeypairRotationRequest{ - SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(), - }, - }, + algoSuite := types.SignatureAlgorithmSuiteToString(cap.GetSignatureAlgorithmSuite()) + rotationResponse, err := params.IssueRotationRequest(&messages.BoundKeypairRotationRequest{ + SignatureAlgorithmSuite: algoSuite, }) if err != nil { return "", trace.Wrap(err, "requesting a new public key") } - pubKeyResponse, ok := response.Payload.(*proto.RegisterUsingBoundKeypairMethodRequest_RotationResponse) - if !ok { - return "", trace.BadParameter("client provided unexpected keypair request response type %T", response.Payload) - } - // Issue a challenge against this new key to ensure ownership. - pubKey := pubKeyResponse.RotationResponse.PublicKey - if err := a.issueBoundKeypairChallenge(ctx, pubKey, challengeResponse); err != nil { + pubKey := string(rotationResponse.PublicKey) + if err := issueBoundKeypairChallenge(ctx, params, pubKey); err != nil { return "", trace.Wrap(err, "solving challenge for new public key") } @@ -448,9 +313,9 @@ func formatTimePointer(t *time.Time) string { // emitBoundKeypairRecoveryEvent emits an audit event indicating a bound keypair // token was used to recover a bot. -func (a *Server) emitBoundKeypairRecoveryEvent( +func emitBoundKeypairRecoveryEvent( ctx context.Context, - req *proto.RegisterUsingBoundKeypairInitialRequest, + params *JoinParams, token *types.ProvisionTokenV2, boundPublicKey string, recoveryCount uint32, @@ -468,14 +333,14 @@ func (a *Server) emitBoundKeypairRecoveryEvent( } } - if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.BoundKeypairRecovery{ + if err := params.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), &apievents.BoundKeypairRecovery{ Metadata: apievents.Metadata{ Type: events.BoundKeypairRecovery, Code: events.BoundKeypairRecoveryCode, }, Status: status, ConnectionMetadata: apievents.ConnectionMetadata{ - RemoteAddr: req.JoinRequest.RemoteAddr, + RemoteAddr: params.Diag.Get().RemoteAddr, }, TokenName: token.GetName(), BotName: token.GetBotName(), @@ -483,15 +348,15 @@ func (a *Server) emitBoundKeypairRecoveryEvent( RecoveryCount: recoveryCount, RecoveryMode: token.Spec.BoundKeypair.Recovery.Mode, }); err != nil { - a.logger.WarnContext(ctx, "Failed to emit failed bound keypair recovery event", "error", err) + params.Logger.WarnContext(ctx, "Failed to emit failed bound keypair recovery event", "error", err) } } // emitBoundKeypairRotationEvent emits an audit event indicating a bound keypair // rotation occurred. -func (a *Server) emitBoundKeypairRotationEvent( +func emitBoundKeypairRotationEvent( ctx context.Context, - req *proto.RegisterUsingBoundKeypairInitialRequest, + params *JoinParams, token *types.ProvisionTokenV2, prevPublicKey, newPublicKey string, err error, @@ -508,33 +373,33 @@ func (a *Server) emitBoundKeypairRotationEvent( } } - if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.BoundKeypairRotation{ + if err := params.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), &apievents.BoundKeypairRotation{ Metadata: apievents.Metadata{ Type: events.BoundKeypairRotation, Code: events.BoundKeypairRotationCode, }, Status: status, ConnectionMetadata: apievents.ConnectionMetadata{ - RemoteAddr: req.JoinRequest.RemoteAddr, + RemoteAddr: params.Diag.Get().RemoteAddr, }, TokenName: token.GetName(), BotName: token.GetBotName(), PreviousPublicKey: prevPublicKey, NewPublicKey: newPublicKey, }); err != nil { - a.logger.WarnContext(ctx, "Failed to emit failed bound keypair rotation event", "error", err) + params.Logger.WarnContext(ctx, "Failed to emit failed bound keypair rotation event", "error", err) } } -func (a *Server) tryLockBotInvalidJoinState( +func tryLockBotInvalidJoinState( ctx context.Context, + params *JoinParams, ptv2 *types.ProvisionTokenV2, - req *proto.RegisterUsingBoundKeypairInitialRequest, validationError error, ) { - log := a.logger.With("join_token", ptv2.GetName(), "validation_error", validationError) + log := params.Logger.With("join_token", ptv2.GetName(), "validation_error", validationError) - if auditErr := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.BoundKeypairJoinStateVerificationFailed{ + if auditErr := params.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), &apievents.BoundKeypairJoinStateVerificationFailed{ Metadata: apievents.Metadata{ Type: events.BoundKeypairJoinStateVerificationFailed, Code: events.BoundKeypairJoinStateVerificationFailedCode, @@ -544,7 +409,7 @@ func (a *Server) tryLockBotInvalidJoinState( Error: validationError.Error(), }, ConnectionMetadata: apievents.ConnectionMetadata{ - RemoteAddr: req.JoinRequest.RemoteAddr, + RemoteAddr: params.Diag.Get().RemoteAddr, }, TokenName: ptv2.GetName(), BotName: ptv2.GetBotName(), @@ -563,13 +428,13 @@ func (a *Server) tryLockBotInvalidJoinState( "stolen keypair.", ptv2.GetName(), ptv2.GetBotName(), ), - CreatedAt: a.clock.Now(), + CreatedAt: params.Clock.Now(), }) if err != nil { - a.logger.ErrorContext(ctx, "Unable to create lock for bound keypair token") + params.Logger.ErrorContext(ctx, "Unable to create lock for bound keypair token") return } - if err := a.UpsertLock(ctx, lock); err != nil { + if err := params.AuthService.UpsertLock(ctx, lock); err != nil { log.ErrorContext(ctx, "Unable to create lock for bound keypair token after join state verification failed") } } @@ -581,16 +446,15 @@ func (a *Server) tryLockBotInvalidJoinState( // compromised. If verification is not required, this is a no-op. Join state // should be verified whenever a client rejoins, but only after they have proven // ownership of their private key. -func (a *Server) verifyBoundKeypairJoinState( +func verifyBoundKeypairJoinState( ctx context.Context, - log *slog.Logger, - req *proto.RegisterUsingBoundKeypairInitialRequest, + params *JoinParams, ptv2 *types.ProvisionTokenV2, ca types.CertAuthority, -) error { +) (previousBotInstnceID string, err error) { recoveryMode, err := boundkeypair.ParseRecoveryMode(ptv2.Spec.BoundKeypair.Recovery.Mode) if err != nil { - return trace.Wrap(err, "parsing recovery mode") + return "", trace.Wrap(err, "parsing recovery mode") } // Join state is required after the initial join (first recovery), so long @@ -602,45 +466,41 @@ func (a *Server) verifyBoundKeypairJoinState( // no client intervention. joinStateRequired := ptv2.Status.BoundKeypair.RecoveryCount > 0 && recoveryMode != boundkeypair.RecoveryModeInsecure if !joinStateRequired { - log.DebugContext( + params.Logger.DebugContext( ctx, "skipping join state verification, not required due to token state", "recovery_count", ptv2.Status.BoundKeypair.RecoveryCount, "recovery_mode", ptv2.Spec.BoundKeypair.Recovery.Mode, ) - return nil + return "", nil } // If join state is required but missing, raise an error. - hasIncomingJoinState := len(req.PreviousJoinState) > 0 + hasIncomingJoinState := len(params.BoundKeypairInit.PreviousJoinState) > 0 if !hasIncomingJoinState { - return trace.AccessDenied("previous join state is required but was not provided") + return "", trace.AccessDenied("previous join state is required but was not provided") } - log.DebugContext(ctx, "join state verification required, verifying") + params.Logger.DebugContext(ctx, "join state verification required, verifying") joinState, err := boundkeypair.VerifyJoinState( ca, - string(req.PreviousJoinState), + string(params.BoundKeypairInit.PreviousJoinState), &boundkeypair.JoinStateParams{ - Clock: a.clock, + Clock: params.Clock, ClusterName: ca.GetClusterName(), // equivalent to clusterName but saves a method param Token: ptv2, }, ) if err != nil { - log.ErrorContext(ctx, "bound keypair join state verification failed", "error", err) - a.tryLockBotInvalidJoinState(ctx, ptv2, req, err) + params.Logger.ErrorContext(ctx, "bound keypair join state verification failed", "error", err) + tryLockBotInvalidJoinState(ctx, params, ptv2, err) - return trace.AccessDenied("join state verification failed") + return "", trace.AccessDenied("join state verification failed") } - // Now that we've verified it, make sure the previous bot instance ID is - // passed along to generateCerts. This will only be used if a new bot - // instance is generated. - req.JoinRequest.PreviousBotInstanceID = joinState.BotInstanceID - - log.DebugContext(ctx, "join state verified successfully", "join_state", joinState) - return nil + params.Logger.DebugContext(ctx, "join state verified successfully", "join_state", joinState) + // Now that we've verified it, return the previous bot instance ID. + return joinState.BotInstanceID, nil } // verifyLocksForBoundKeypairToken checks if any token-level locks are in place @@ -648,62 +508,123 @@ func (a *Server) verifyBoundKeypairJoinState( // been authenticated (exact criteria varies depending on token state) but // before the request has mutated anything on the server - including creation of // additional locks: we don't want to allow continuous lock creation. -func (a *Server) verifyLocksForBoundKeypairToken(ctx context.Context, token *types.ProvisionTokenV2) error { - readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx) +func verifyLocksForBoundKeypairToken(ctx context.Context, params *JoinParams, token *types.ProvisionTokenV2) error { + readOnlyAuthPref, err := params.AuthService.GetReadOnlyAuthPreference(ctx) if err != nil { return trace.Wrap(err) } - return trace.Wrap(a.checkLockInForce( + return trace.Wrap(params.AuthService.CheckLockInForce( readOnlyAuthPref.GetLockingMode(), []types.LockTarget{{JoinToken: token.GetName()}}, )) } -// RegisterUsingBoundKeypairMethod handles joining requests for the bound -// keypair join method. If successful, returns a certificate bundle and client -// joining parameters for use in subsequent join attempts. -func (a *Server) RegisterUsingBoundKeypairMethod( - ctx context.Context, - req *proto.RegisterUsingBoundKeypairInitialRequest, - challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc, -) (_ *client.BoundKeypairRegistrationResponse, err error) { - var provisionToken types.ProvisionToken - var joinFailureMetadata any - defer func() { - // Emit a log message and audit event on join failure. - if err != nil { - a.handleJoinFailure( - ctx, err, provisionToken, joinFailureMetadata, req.JoinRequest, - ) +// JoinParams holds all parameters necessary to handle a bound keypair join attempt. +type JoinParams struct { + // AuthService is the auth service. + AuthService AuthService + // AuthCtx is authentication context for the request, relevant for re-join attempts. + AuthCtx *authz.Context + // Diag is the join attempt diagnostic. + Diag *diagnostic.Diagnostic + // ProvisionToken is the provision token used for the join attempt. + ProvisionToken types.ProvisionToken + // ClientInit is the ClientInit message sent by the joining client. + ClientInit *messages.ClientInit + // BoundKeypairInit is the BoundKeypairInit message sent by the joining client. + BoundKeypairInit *messages.BoundKeypairInit + // IssueChallenge sends a challenge to the joining client and returns the + // response. + IssueChallenge ChallengeResponseFunc + // IssueRotationRequest sends a rotation request to the joining client and + // returns the response. + IssueRotationRequest RotationFunc + // CreateBoundKeypairValidator is a function that creates a bound keypair + // validator, used to override the validator in tests. + CreateBoundKeypairValidator CreateBoundKeypairValidator + // GenerateBotCerts is a function that generates bot certificates. + GenerateBotCerts func(ctx context.Context, previousBotInstanceID string, claims any) (*messages.Certificates, string, error) + // Clock is the clock. + Clock clockwork.Clock + // Logger is a logger. + Logger *slog.Logger +} + +// ChallengeResponseFunc is function that sends a bound keypair challenge and +// returns the response. +type ChallengeResponseFunc func(*messages.BoundKeypairChallenge) (*messages.BoundKeypairChallengeSolution, error) + +// RotationFunc is function that sends a bound keypair rotation request and +// returns the response. +type RotationFunc func(*messages.BoundKeypairRotationRequest) (*messages.BoundKeypairRotationResponse, error) + +func (p *JoinParams) checkAndSetDefaults() error { + switch { + case p.AuthService == nil: + return trace.BadParameter("AuthService is required") + case p.AuthCtx == nil: + return trace.BadParameter("AuthCtx is required") + case p.Diag == nil: + return trace.BadParameter("Diag is required") + case p.ProvisionToken == nil: + return trace.BadParameter("ProvisionToken is required") + case p.ClientInit == nil: + return trace.BadParameter("ClientInit is required") + case p.BoundKeypairInit == nil: + return trace.BadParameter("BoundKeypairInit is required") + case p.IssueChallenge == nil: + return trace.BadParameter("IssueChallenge is required") + case p.IssueRotationRequest == nil: + return trace.BadParameter("IssueRotationRequest is required") + case p.Logger == nil: + return trace.BadParameter("Logger is required") + } + if p.CreateBoundKeypairValidator == nil { + p.CreateBoundKeypairValidator = func(subject string, clusterName string, publicKey crypto.PublicKey) (BoundKeypairValidator, error) { + return boundkeypair.NewChallengeValidator(subject, clusterName, publicKey) } - }() + } + if p.Clock == nil { + p.Clock = clockwork.NewRealClock() + } + return nil +} + +// AuthService is the subset of the Auth service interface required to implement bound keypair joining. +type AuthService interface { + EmitAuditEvent(ctx context.Context, e apievents.AuditEvent) error + GetClusterName(context.Context) (types.ClusterName, error) + GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error) + GetKeyStore() *keystore.Manager + PatchToken(context.Context, string, func(types.ProvisionToken) (types.ProvisionToken, error)) (types.ProvisionToken, error) + GetAuthPreference(context.Context) (types.AuthPreference, error) + GetReadOnlyAuthPreference(context.Context) (readonly.AuthPreference, error) + UpsertLock(context.Context, types.Lock) error + CheckLockInForce(constants.LockingMode, []types.LockTarget) error +} - // First, check the specified token exists, and is a bound keypair-type join - // token. - if err := req.JoinRequest.CheckAndSetDefaults(); err != nil { +// HandleBoundKeypairJoin handles joining requests for the bound keypair join +// method. +func HandleBoundKeypairJoin( + ctx context.Context, + params *JoinParams, +) (*messages.BotResult, error) { + if err := params.checkAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } - // Only bot joining is supported at the moment - unique ID verification is // required and this is currently only implemented for bots. - if req.JoinRequest.Role != types.RoleBot { + if types.SystemRole(params.ClientInit.SystemRole) != types.RoleBot { return nil, trace.BadParameter("bound keypair joining is only supported for bots") } - provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req.JoinRequest) - if err != nil { - return nil, trace.Wrap(err) - } - ptv2, ok := provisionToken.(*types.ProvisionTokenV2) + ptv2, ok := params.ProvisionToken.(*types.ProvisionTokenV2) if !ok { - return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", provisionToken) - } - if ptv2.Spec.JoinMethod != types.JoinMethodBoundKeypair { - return nil, trace.BadParameter("specified join token is not for `%s` method", types.JoinMethodBoundKeypair) + return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", params.ProvisionToken) } - log := a.logger.With("token", ptv2.GetName()) + log := params.Logger.With("token", ptv2.GetName()) if ptv2.Status == nil { ptv2.Status = &types.ProvisionTokenStatusV2{} @@ -712,7 +633,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( ptv2.Status.BoundKeypair = &types.ProvisionTokenStatusV2BoundKeypair{} } - clusterName, err := a.GetClusterName(ctx) + clusterName, err := params.AuthService.GetClusterName(ctx) if err != nil { return nil, trace.Wrap(err) } @@ -721,7 +642,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( status := ptv2.Status.BoundKeypair hasBoundPublicKey := status.BoundPublicKey != "" hasBoundBotInstance := status.BoundBotInstanceID != "" - hasIncomingBotInstance := req.JoinRequest.BotInstanceID != "" + hasIncomingBotInstance := params.AuthCtx.BotInstanceID != "" hasJoinsRemaining := status.RecoveryCount < spec.Recovery.Limit recoveryMode, err := boundkeypair.ParseRecoveryMode(spec.Recovery.Mode) @@ -745,7 +666,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( var mutators []boundKeypairStatusMutator // Get the join state JWT signer CA - ca, err := a.GetCertAuthority(ctx, types.CertAuthID{ + ca, err := params.AuthService.GetCertAuthority(ctx, types.CertAuthID{ Type: types.BoundKeypairCA, DomainName: clusterName.GetClusterName(), }, /* loadKeys */ true) @@ -753,6 +674,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( return nil, trace.Wrap(err) } + var verifiedPreviousBotInstanceID string switch { case !hasBoundPublicKey && !hasIncomingBotInstance: // Normal initial join attempt. No bound key, and no incoming bot @@ -764,13 +686,13 @@ func (a *Server) RegisterUsingBoundKeypairMethod( if spec.Onboarding.InitialPublicKey != "" { // An initial public key was configured, so we can immediately ask // the client to complete a challenge. - if err := a.issueBoundKeypairChallenge( + if err := issueBoundKeypairChallenge( ctx, + params, spec.Onboarding.InitialPublicKey, - challengeResponse, ); err != nil { log.WarnContext(ctx, "denying initial join attempt, client failed to complete challenge", "error", err) - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, spec.Onboarding.InitialPublicKey, 0, err) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, spec.Onboarding.InitialPublicKey, 0, err) return nil, trace.AccessDenied("failed to complete challenge") } @@ -786,14 +708,14 @@ func (a *Server) RegisterUsingBoundKeypairMethod( const errMsg = "a valid registration secret is required" // A registration secret is expected. - if req.InitialJoinSecret == "" { + if params.BoundKeypairInit.InitialJoinSecret == "" { log.WarnContext(ctx, "denying join attempt, client failed to provide required registration secret") - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, "", 0, trace.AccessDenied("no registration secret was provided")) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, "", 0, trace.AccessDenied("no registration secret was provided")) return nil, trace.AccessDenied(errMsg) } if spec.Onboarding.MustRegisterBefore != nil { - if a.clock.Now().After(*spec.Onboarding.MustRegisterBefore) { + if params.Clock.Now().After(*spec.Onboarding.MustRegisterBefore) { log.WarnContext( ctx, "denying join attempt due to expired registration secret", @@ -805,18 +727,18 @@ func (a *Server) RegisterUsingBoundKeypairMethod( } // Verify the secret. - if subtle.ConstantTimeCompare([]byte(status.RegistrationSecret), []byte(req.InitialJoinSecret)) != 1 { + if subtle.ConstantTimeCompare([]byte(status.RegistrationSecret), []byte(params.BoundKeypairInit.InitialJoinSecret)) != 1 { log.WarnContext(ctx, "denying join attempt, client provided incorrect registration secret") - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, "", 0, trace.AccessDenied("registration secret comparison failed")) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, "", 0, trace.AccessDenied("registration secret comparison failed")) return nil, trace.AccessDenied(errMsg) } // Ask the client for a new public key. - newPubKey, err := a.requestBoundKeypairRotation(ctx, challengeResponse) + newPubKey, err := requestBoundKeypairRotation(ctx, params) if err != nil { // Audit note: `requestBoundKeypairRotation()` will also emit an // audit event. - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, "", 0, err) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, "", 0, err) return nil, trace.Wrap(err, "requesting public key") } @@ -847,7 +769,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( // request. We don't want to leak the lock status to random // unauthenticated clients, and by this point, we haven't mutated any // server-side state. - if err := a.verifyLocksForBoundKeypairToken(ctx, ptv2); err != nil { + if err := verifyLocksForBoundKeypairToken(ctx, params, ptv2); err != nil { return nil, trace.Wrap(err) } @@ -855,7 +777,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( recoveryCount += 1 expectNewBotInstance = true - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, boundPublicKey, recoveryCount, nil) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, boundPublicKey, recoveryCount, nil) case !hasBoundPublicKey && hasIncomingBotInstance: // Not allowed, at least at the moment. This would imply e.g. trying to // change auth methods. @@ -867,10 +789,10 @@ func (a *Server) RegisterUsingBoundKeypairMethod( return nil, trace.BadParameter("bad backend state, please recreate the join token") case hasBoundPublicKey && hasBoundBotInstance && hasIncomingBotInstance: // Standard rejoin case, does not consume a rejoin. - if err := a.issueBoundKeypairChallenge( + if err := issueBoundKeypairChallenge( ctx, + params, status.BoundPublicKey, - challengeResponse, ); err != nil { return nil, trace.Wrap(err) } @@ -878,7 +800,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( // Verify locks here now that we've verified private key ownership but // before we check join state. Otherwise, we could allow a lock creation // loop. - if err := a.verifyLocksForBoundKeypairToken(ctx, ptv2); err != nil { + if err := verifyLocksForBoundKeypairToken(ctx, params, ptv2); err != nil { return nil, trace.Wrap(err) } @@ -887,7 +809,8 @@ func (a *Server) RegisterUsingBoundKeypairMethod( // make sure an otherwise unauthorized client can't trigger a lockout. // This also needs to be done before rotation to prevent an attacker // from rotating the key. - if err := a.verifyBoundKeypairJoinState(ctx, log, req, ptv2, ca); err != nil { + verifiedPreviousBotInstanceID, err = verifyBoundKeypairJoinState(ctx, params, ptv2, ca) + if err != nil { return nil, trace.AccessDenied("join state verification failed") } @@ -899,19 +822,19 @@ func (a *Server) RegisterUsingBoundKeypairMethod( // any event that might have cycled bot instance IDs should have also // modified the join state causing a failure above. In any case, we'll // keep this as a sanity check. - if status.BoundBotInstanceID != req.JoinRequest.BotInstanceID { + if status.BoundBotInstanceID != params.AuthCtx.BotInstanceID { return nil, trace.AccessDenied("bot instance mismatch") } // Nothing else to do, no key change, no additional audit event; regular // bot join event will be emitted later. case hasBoundPublicKey && hasBoundBotInstance && !hasIncomingBotInstance: - if err := a.issueBoundKeypairChallenge( + if err := issueBoundKeypairChallenge( ctx, + params, status.BoundPublicKey, - challengeResponse, ); err != nil { - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, boundPublicKey, recoveryCount, err) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, boundPublicKey, recoveryCount, err) return nil, trace.Wrap(err) } @@ -925,13 +848,14 @@ func (a *Server) RegisterUsingBoundKeypairMethod( // Verify locks here now that we've verified private key ownership but // before we check join state. Otherwise, we could allow a lock creation // loop. - if err := a.verifyLocksForBoundKeypairToken(ctx, ptv2); err != nil { + if err := verifyLocksForBoundKeypairToken(ctx, params, ptv2); err != nil { return nil, trace.Wrap(err) } // As in the standard case above, once we've verified the client has the // matching private key, validate the join state. - if err := a.verifyBoundKeypairJoinState(ctx, log, req, ptv2, ca); err != nil { + verifiedPreviousBotInstanceID, err = verifyBoundKeypairJoinState(ctx, params, ptv2, ca) + if err != nil { return nil, trace.AccessDenied("join state verification failed") } @@ -942,7 +866,7 @@ func (a *Server) RegisterUsingBoundKeypairMethod( recoveryCount += 1 expectNewBotInstance = true - a.emitBoundKeypairRecoveryEvent(ctx, req, ptv2, boundPublicKey, recoveryCount, nil) + emitBoundKeypairRecoveryEvent(ctx, params, ptv2, boundPublicKey, recoveryCount, nil) default: log.ErrorContext( ctx, "unexpected state", @@ -956,22 +880,22 @@ func (a *Server) RegisterUsingBoundKeypairMethod( } // If we've crossed a keypair rotation threshold, request one now. - now := a.clock.Now() + now := params.Clock.Now() if shouldRequestBoundKeypairRotation(spec.RotateAfter, status.LastRotatedAt, now) { log.DebugContext( ctx, "requesting keypair rotation", "rotate_after", formatTimePointer(spec.RotateAfter), "last_rotated_at", formatTimePointer(status.LastRotatedAt), ) - newPubKey, err := a.requestBoundKeypairRotation(ctx, challengeResponse) + newPubKey, err := requestBoundKeypairRotation(ctx, params) if err != nil { - a.emitBoundKeypairRotationEvent(ctx, req, ptv2, boundPublicKey, "", err) + emitBoundKeypairRotationEvent(ctx, params, ptv2, boundPublicKey, "", err) return nil, trace.Wrap(err) } // Don't let clients provide the same key again. if err := ensurePublicKeysNotEqual(boundPublicKey, newPubKey); err != nil { - a.emitBoundKeypairRotationEvent(ctx, req, ptv2, boundPublicKey, newPubKey, err) + emitBoundKeypairRotationEvent(ctx, params, ptv2, boundPublicKey, newPubKey, err) return nil, trace.Wrap(err) } @@ -980,20 +904,15 @@ func (a *Server) RegisterUsingBoundKeypairMethod( mutateStatusLastRotatedAt(&now, status.LastRotatedAt), ) - a.emitBoundKeypairRotationEvent(ctx, req, ptv2, boundPublicKey, newPubKey, nil) + emitBoundKeypairRotationEvent(ctx, params, ptv2, boundPublicKey, newPubKey, nil) boundPublicKey = newPubKey } - params := makeBotCertsParams( - req.JoinRequest, - &boundkeypair.Claims{ - PublicKey: boundPublicKey, - RecoveryCount: recoveryCount, - RecoveryMode: recoveryMode, - }, - nil, // TODO: workload id claims - ) - certs, botInstanceID, err := a.GenerateBotCertsForJoin(ctx, provisionToken, params) + certs, botInstanceID, err := params.GenerateBotCerts(ctx, verifiedPreviousBotInstanceID, &boundkeypair.Claims{ + PublicKey: boundPublicKey, + RecoveryCount: recoveryCount, + RecoveryMode: recoveryMode, + }) if err != nil { return nil, trace.Wrap(err) } @@ -1010,10 +929,10 @@ func (a *Server) RegisterUsingBoundKeypairMethod( finalToken := ptv2 if len(mutators) > 0 { - patched, err := a.PatchToken(ctx, ptv2.GetName(), func(token types.ProvisionToken) (types.ProvisionToken, error) { - ptv2, ok := provisionToken.(*types.ProvisionTokenV2) + patched, err := params.AuthService.PatchToken(ctx, ptv2.GetName(), func(token types.ProvisionToken) (types.ProvisionToken, error) { + ptv2, ok := params.ProvisionToken.(*types.ProvisionTokenV2) if !ok { - return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", provisionToken) + return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", params.ProvisionToken) } // Apply all mutators. Individual mutators may make additional @@ -1035,17 +954,17 @@ func (a *Server) RegisterUsingBoundKeypairMethod( // This should be impossible, but if it did fail, we can't generate // a join state without an accurate token. The certs we just // generated will be useless, so just return an error. - return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", provisionToken) + return nil, trace.BadParameter("expected *types.ProvisionTokenV2, got %T", params.ProvisionToken) } } - signer, err := a.GetKeyStore().GetJWTSigner(ctx, ca) + signer, err := params.AuthService.GetKeyStore().GetJWTSigner(ctx, ca) if err != nil { return nil, trace.Wrap(err, "issuing join state document") } newJoinState, err := boundkeypair.IssueJoinState(signer, &boundkeypair.JoinStateParams{ - Clock: a.clock, + Clock: params.Clock, ClusterName: clusterName.GetClusterName(), Token: finalToken, }) @@ -1053,9 +972,11 @@ func (a *Server) RegisterUsingBoundKeypairMethod( return nil, trace.Wrap(err, "issuing join state document") } - return &client.BoundKeypairRegistrationResponse{ - Certs: certs, - BoundPublicKey: boundPublicKey, - JoinState: []byte(newJoinState), + return &messages.BotResult{ + Certificates: *certs, + BoundKeypairResult: &messages.BoundKeypairResult{ + JoinState: []byte(newJoinState), + PublicKey: []byte(boundPublicKey), + }, }, nil } diff --git a/lib/join/internal/messages/messages.go b/lib/join/internal/messages/messages.go index 1f6071711cac2..be8c144c32ff3 100644 --- a/lib/join/internal/messages/messages.go +++ b/lib/join/internal/messages/messages.go @@ -192,6 +192,98 @@ func (k *PublicKeys) check() error { return nil } +// BoundKeypairInit is sent from the client in response to the ServerInit +// message for the bound keypair join method. +// The server is expected to respond with a BoundKeypairChallenge. +// +// The bound keypair method join flow is: +// 1. client->server: ClientInit +// 2. server->client: ServerInit +// 3. client->server: BoundKeypairInit +// 4. server->client: BoundKeypairChallenge +// 5. client->server: BoundKeypairChallengeSolution +// (optional additional steps if keypair rotation is required) +// server->client: BoundKeypairRotationRequest +// client->server: BoundKeypairRotationResponse +// server->client: BoundKeypairChallenge +// client->server: BoundKeypairChallengeSolution +// 6. server->client: Result containing BoundKeypairResult +type BoundKeypairInit struct { + embedRequest + + // ClientParams holds parameters for the specific type of client trying to join. + ClientParams ClientParams + // If set, attempts to bind a new keypair using an initial join secret. Any + // value set here will be ignored if a keypair is already bound. + InitialJoinSecret string + // A document signed by Auth containing join state parameters from the + // previous join attempt. Not required on initial join; required on all + // subsequent joins. + PreviousJoinState []byte +} + +// BoundKeypairChallenge is a challenge issued by the server that joining +// clients are expected to complete. +// The client is expected to respond with a BoundKeypairChallengeSolution. +type BoundKeypairChallenge struct { + embedResponse + + // The desired public key corresponding to the private key that should be + // used to sign this challenge, in SSH authorized keys format. + PublicKey []byte + // A challenge to sign with the requested public key. During keypair + // rotation, a second challenge will be provided to verify the new keypair + // before certs are returned. + Challenge string +} + +// BoundKeypairChallengeSolution is sent from the client in response to the +// BoundKeypairChallenge. +// The server is expected to respond with either a Result or a +// BoundKeypairRotationRequest. +type BoundKeypairChallengeSolution struct { + embedRequest + + // A solution to a challenge from the server. This generated by signing the + // challenge as a JWT using the keypair associated with the requested public + // key. + Solution []byte +} + +// BoundKeypairRotationRequest is sent by the server in response to a +// BoundKeypairChallenge when a keypair rotation is required. It acts like an +// additional challenge, the client is expected to respond with a +// BoundKeypairRotationResponse. +type BoundKeypairRotationRequest struct { + embedResponse + + // The signature algorithm suite in use by the cluster. + SignatureAlgorithmSuite string +} + +// BoundKeypairRotationResponse is sent by the client in response to a +// BoundKeypairRotationRequest from the server. The server is expected to +// respond with an additional BoundKeypairChallenge for the new key. +type BoundKeypairRotationResponse struct { + embedRequest + + // The public key to be registered with auth. Clients should expect a + // subsequent challenge against this public key to be sent. This is encoded + // in SSH authorized keys format. + PublicKey []byte +} + +// BoundKeypairResult holds additional result parameters relevant to the bound +// keypair join method. +type BoundKeypairResult struct { + // A signed join state document to be provided on the next join attempt. + JoinState []byte + // The public key registered with Auth at the end of the joining ceremony. + // After a successful keypair rotation, this should reflect the newly + // registered public key. This is encoded in SSH authorized keys format. + PublicKey []byte +} + // Response is implemented by all join response messages. type Response interface { isResponse() @@ -231,6 +323,8 @@ type BotResult struct { // Certificates holds issued certificates and cluster CAs. Certificates Certificates + // BoundKeypairResult holds extra result parameters relevant to the bound keypair join method. + BoundKeypairResult *BoundKeypairResult } // Certificates holds issued certificates and cluster CAs. diff --git a/lib/join/joinclient/join.go b/lib/join/joinclient/join.go index d20b35777576b..51e63e1fc2e48 100644 --- a/lib/join/joinclient/join.go +++ b/lib/join/joinclient/join.go @@ -168,43 +168,54 @@ func joinWithClient(ctx context.Context, params JoinParams, client *joinv1.Clien clientParams := makeClientParams(params, publicKeys) // Delegate out to the handler for the specific join method. - if err := joinWithMethod(stream, clientParams, serverInit.JoinMethod); err != nil { + resultMsg, err := joinWithMethod(ctx, stream, params, clientParams, serverInit.JoinMethod) + if err != nil { return nil, trace.Wrap(err) } - // Receive the final result message. - if params.ID.Role == types.RoleBot { - botResult, err := messages.RecvResponse[*messages.BotResult](stream) + // Convert the result message into a JoinResult. + switch typedResult := resultMsg.(type) { + case *messages.HostResult: + return makeJoinResult(signer, typedResult.Certificates) + case *messages.BotResult: + joinResult, err := makeJoinResult(signer, typedResult.Certificates) if err != nil { return nil, trace.Wrap(err) } - return makeJoinResult(signer, botResult.Certificates) - } - hostResult, err := messages.RecvResponse[*messages.HostResult](stream) - if err != nil { - return nil, trace.Wrap(err) + if typedResult.BoundKeypairResult != nil { + joinResult.BoundKeypair = &authjoin.BoundKeypairRegisterResult{ + BoundPublicKey: string(typedResult.BoundKeypairResult.PublicKey), + JoinState: typedResult.BoundKeypairResult.JoinState, + } + } + return joinResult, nil + default: + return nil, trace.BadParameter("unhandled result message type %T", resultMsg) } - return makeJoinResult(signer, hostResult.Certificates) } func joinWithMethod( + ctx context.Context, stream messages.ClientStream, + joinParams JoinParams, clientParams messages.ClientParams, method string, -) error { +) (messages.Response, error) { switch types.JoinMethod(method) { case types.JoinMethodToken: - return trace.Wrap(tokenJoin(stream, clientParams)) + return tokenJoin(stream, clientParams) + case types.JoinMethodBoundKeypair: + return boundKeypairJoin(ctx, stream, joinParams, clientParams) default: // TODO(nklaassen): implement remaining join methods. - return trace.NotImplemented("server selected join method %v which is not supported by this client", method) + return nil, trace.NotImplemented("server selected join method %v which is not supported by this client", method) } } func tokenJoin( stream messages.ClientStream, clientParams messages.ClientParams, -) error { +) (messages.Response, error) { // The token join method is relatively simple, the flow is // // client->server ClientInit @@ -213,12 +224,16 @@ func tokenJoin( // client<-server Result // // At this point the ServerInit messages has already been received, all - // that's left is to send the TokenInit message, the caller will handle - // receiving the final result. + // that's left is to send the TokenInit message and receive the final result. tokenInitMsg := &messages.TokenInit{ ClientParams: clientParams, } - return trace.Wrap(stream.Send(tokenInitMsg)) + if err := stream.Send(tokenInitMsg); err != nil { + return nil, trace.Wrap(err) + } + // Receive and return the final result. + result, err := stream.Recv() + return result, trace.Wrap(err) } func makeClientParams(params JoinParams, publicKeys *messages.PublicKeys) messages.ClientParams { diff --git a/lib/join/joinclient/join_boundkeypair.go b/lib/join/joinclient/join_boundkeypair.go new file mode 100644 index 0000000000000..222e304bf0551 --- /dev/null +++ b/lib/join/joinclient/join_boundkeypair.go @@ -0,0 +1,162 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package joinclient + +import ( + "context" + "crypto" + "log/slog" + "strings" + + "github.com/go-jose/go-jose/v3" + "github.com/gravitational/trace" + "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport/api/types" + authjoin "github.com/gravitational/teleport/lib/auth/join" + "github.com/gravitational/teleport/lib/cryptosuites" + "github.com/gravitational/teleport/lib/join/internal/messages" + "github.com/gravitational/teleport/lib/jwt" +) + +type ( + BoundKeypairParams = authjoin.BoundKeypairParams + GetSignerFunc = authjoin.GetSignerFunc + KeygenFunc = authjoin.KeygenFunc + BoundKeypairResult = authjoin.BoundKeypairRegisterResult +) + +func boundKeypairJoin( + ctx context.Context, + stream messages.ClientStream, + joinParams JoinParams, + clientParams messages.ClientParams, +) (messages.Response, error) { + // The bound keypair join method is relatively complex compared to other + // join methods, the flow is: + // + // client->server ClientInit + // client<-server ServerInit + // client->server BoundKeypairInit + // client<-server BoundKeypairChallenge + // client->server BoundKeypairChallengeSolution + // (optional additional steps if keypair rotation is required) + // client<-server: BoundKeypairRotationRequest + // client->server: BoundKeypairRotationResponse + // client<-server: BoundKeypairChallenge + // client->server: BoundKeypairChallengeSolution + // client<-server: Result containing BoundKeypairResult + // + // At this point the ServerInit message has already been received, this + // function needs to send the BoundKeyPairInit and then handle any + // challenges and rotation requests. + boundKeypairInit := &messages.BoundKeypairInit{ + ClientParams: clientParams, + InitialJoinSecret: joinParams.BoundKeypairParams.InitialJoinSecret, + PreviousJoinState: joinParams.BoundKeypairParams.PreviousJoinState, + } + if err := stream.Send(boundKeypairInit); err != nil { + return nil, trace.Wrap(err) + } + + bkParams := joinParams.BoundKeypairParams + for { + msg, err := stream.Recv() + if err != nil { + return nil, trace.Wrap(err) + } + switch kind := msg.(type) { + case *messages.BotResult: + // Return the final result. + return kind, nil + case *messages.BoundKeypairChallenge: + signer, err := bkParams.GetSigner(string(kind.PublicKey)) + if err != nil { + return nil, trace.Wrap(err, "could not lookup signer for public key %+v", string(kind.PublicKey)) + } + + alg, err := jwt.AlgorithmForPublicKey(signer.Public()) + if err != nil { + return nil, trace.Wrap(err, "determining signing algorithm for public key") + } + + opts := (&jose.SignerOptions{}).WithType("JWT") + key := jose.SigningKey{ + Algorithm: alg, + Key: signer, + } + + joseSigner, err := jose.NewSigner(key, opts) + if err != nil { + return nil, trace.Wrap(err, "creating signer") + } + + jws, err := joseSigner.Sign([]byte(kind.Challenge)) + if err != nil { + return nil, trace.Wrap(err, "signing challenge") + } + + serialized, err := jws.CompactSerialize() + if err != nil { + return nil, trace.Wrap(err, "serializing signed challenge") + } + if err := stream.Send(&messages.BoundKeypairChallengeSolution{ + Solution: []byte(serialized), + }); err != nil { + return nil, trace.Wrap(err) + } + case *messages.BoundKeypairRotationRequest: + if bkParams.RequestNewKeypair == nil { + return nil, trace.BadParameter("RequestNewKeypair is required") + } + + slog.InfoContext(ctx, "Server has requested keypair rotation", "suite", kind.SignatureAlgorithmSuite) + + suite, err := types.SignatureAlgorithmSuiteFromString(kind.SignatureAlgorithmSuite) + if err != nil { + return nil, trace.Wrap(err) + } + newSigner, err := bkParams.RequestNewKeypair(ctx, cryptosuites.StaticAlgorithmSuite(suite)) + if err != nil { + return nil, trace.Wrap(err, "requesting new keypair") + } + + newPubkey, err := sshPubKeyFromSigner(newSigner) + if err != nil { + return nil, trace.Wrap(err) + } + if err := stream.Send(&messages.BoundKeypairRotationResponse{ + PublicKey: []byte(newPubkey), + }); err != nil { + return nil, trace.Wrap(err) + } + default: + return nil, trace.Errorf("server sent unexpected message type %T", msg) + } + } +} + +// sshPubKeyFromSigner returns the public key of the given signer in ssh +// authorized_keys format. +func sshPubKeyFromSigner(signer crypto.Signer) (string, error) { + sshKey, err := ssh.NewPublicKey(signer.Public()) + if err != nil { + return "", trace.Wrap(err, "creating SSH public key from signer") + } + + return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshKey))), nil +} diff --git a/lib/join/joinv1/messages.go b/lib/join/joinv1/messages.go index 685aa902b2381..e8467262b21e7 100644 --- a/lib/join/joinv1/messages.go +++ b/lib/join/joinv1/messages.go @@ -32,6 +32,10 @@ func requestToMessage(req *joinv1.JoinRequest) (messages.Request, error) { return clientInitToMessage(msg.ClientInit), nil case *joinv1.JoinRequest_TokenInit: return tokenInitToMessage(msg.TokenInit) + case *joinv1.JoinRequest_BoundKeypairInit: + return boundKeypairInitToMessage(msg.BoundKeypairInit) + case *joinv1.JoinRequest_Solution: + return challengeSolutionToMessage(msg.Solution) default: return nil, trace.BadParameter("unrecognized join request message type %T", msg) } @@ -57,6 +61,26 @@ func requestFromMessage(msg messages.Request) (*joinv1.JoinRequest, error) { TokenInit: tokenInit, }, }, nil + case *messages.BoundKeypairInit: + boundKeypairInit, err := boundKeypairInitFromMessage(typedMsg) + if err != nil { + return nil, trace.Wrap(err) + } + return &joinv1.JoinRequest{ + Payload: &joinv1.JoinRequest_BoundKeypairInit{ + BoundKeypairInit: boundKeypairInit, + }, + }, nil + case *messages.BoundKeypairChallengeSolution, *messages.BoundKeypairRotationResponse: + solution, err := challengeSolutionFromMessage(typedMsg) + if err != nil { + return nil, trace.Wrap(err) + } + return &joinv1.JoinRequest{ + Payload: &joinv1.JoinRequest_Solution{ + Solution: solution, + }, + }, nil default: return nil, trace.BadParameter("unrecognized join request message type %T", msg) } @@ -197,15 +221,47 @@ func publicKeysFromMessage(msg messages.PublicKeys) *joinv1.PublicKeys { } } +func challengeSolutionToMessage(req *joinv1.ChallengeSolution) (messages.Request, error) { + switch payload := req.GetPayload().(type) { + case *joinv1.ChallengeSolution_BoundKeypairChallengeSolution: + return boundKeypairChallengeSolutionToMessage(payload.BoundKeypairChallengeSolution), nil + case *joinv1.ChallengeSolution_BoundKeypairRotationResponse: + return boundKeypairRotationResponseToMessage(payload.BoundKeypairRotationResponse), nil + default: + return nil, trace.BadParameter("unrecognized challenge solution message type %T", payload) + } +} + +func challengeSolutionFromMessage(msg messages.Request) (*joinv1.ChallengeSolution, error) { + switch typedMsg := msg.(type) { + case *messages.BoundKeypairChallengeSolution: + return &joinv1.ChallengeSolution{ + Payload: &joinv1.ChallengeSolution_BoundKeypairChallengeSolution{ + BoundKeypairChallengeSolution: boundKeypairChallengeSolutionFromMessage(typedMsg), + }, + }, nil + case *messages.BoundKeypairRotationResponse: + return &joinv1.ChallengeSolution{ + Payload: &joinv1.ChallengeSolution_BoundKeypairRotationResponse{ + BoundKeypairRotationResponse: boundKeypairRotationResponseFromMessage(typedMsg), + }, + }, nil + default: + return nil, trace.BadParameter("unrecognized challenge solution message type %T", msg) + } +} + // responseToMessage converts a gRPC JoinResponse into a protocol-agnostic [messages.Response]. func responseToMessage(resp *joinv1.JoinResponse) (messages.Response, error) { switch typedResp := resp.Payload.(type) { case *joinv1.JoinResponse_Init: return serverInitToMessage(typedResp.Init) + case *joinv1.JoinResponse_Challenge: + return challengeToMessage(typedResp.Challenge) case *joinv1.JoinResponse_Result: return resultToMessage(typedResp.Result) default: - return nil, trace.BadParameter("unrecognized join responsed message type %T", typedResp) + return nil, trace.BadParameter("unrecognized join response message type %T", typedResp) } } @@ -219,6 +275,16 @@ func responseFromMessage(msg messages.Response) (*joinv1.JoinResponse, error) { Init: serverInitFromMessage(typedMsg), }, }, nil + case *messages.BoundKeypairChallenge, *messages.BoundKeypairRotationRequest: + challenge, err := challengeFromMessage(msg) + if err != nil { + return nil, trace.Wrap(err) + } + return &joinv1.JoinResponse{ + Payload: &joinv1.JoinResponse_Challenge{ + Challenge: challenge, + }, + }, nil case *messages.HostResult: return &joinv1.JoinResponse{ Payload: &joinv1.JoinResponse_Result{ @@ -262,6 +328,36 @@ func serverInitFromMessage(msg *messages.ServerInit) *joinv1.ServerInit { } } +func challengeToMessage(resp *joinv1.Challenge) (messages.Response, error) { + switch payload := resp.Payload.(type) { + case *joinv1.Challenge_BoundKeypairChallenge: + return boundKeypairChallengeToMessage(payload.BoundKeypairChallenge), nil + case *joinv1.Challenge_BoundKeypairRotationRequest: + return boundKeypairRotationRequestToMessage(payload.BoundKeypairRotationRequest), nil + default: + return nil, trace.BadParameter("unrecognized challenge payload type %T", payload) + } +} + +func challengeFromMessage(resp messages.Response) (*joinv1.Challenge, error) { + switch msg := resp.(type) { + case *messages.BoundKeypairChallenge: + return &joinv1.Challenge{ + Payload: &joinv1.Challenge_BoundKeypairChallenge{ + BoundKeypairChallenge: boundKeypairChallengeFromMessage(msg), + }, + }, nil + case *messages.BoundKeypairRotationRequest: + return &joinv1.Challenge{ + Payload: &joinv1.Challenge_BoundKeypairRotationRequest{ + BoundKeypairRotationRequest: boundKeypairRotationRequestFromMessage(msg), + }, + }, nil + default: + return nil, trace.BadParameter("unrecognized challenge message type %T", msg) + } +} + func resultToMessage(resp *joinv1.Result) (messages.Response, error) { switch resp.Payload.(type) { case *joinv1.Result_HostResult: @@ -289,13 +385,15 @@ func hostResultFromMessage(msg *messages.HostResult) *joinv1.HostResult { func botResultToMessage(resp *joinv1.BotResult) *messages.BotResult { return &messages.BotResult{ - Certificates: certificatesToMessage(resp.Certificates), + Certificates: certificatesToMessage(resp.Certificates), + BoundKeypairResult: boundKeypairResultToMessage(resp.BoundKeypairResult), } } func botResultFromMessage(msg *messages.BotResult) *joinv1.BotResult { return &joinv1.BotResult{ - Certificates: certificatesFromMessage(&msg.Certificates), + Certificates: certificatesFromMessage(&msg.Certificates), + BoundKeypairResult: boundKeypairResultFromMessage(msg.BoundKeypairResult), } } diff --git a/lib/join/joinv1/messages_boundkeypair.go b/lib/join/joinv1/messages_boundkeypair.go new file mode 100644 index 0000000000000..32739453305d3 --- /dev/null +++ b/lib/join/joinv1/messages_boundkeypair.go @@ -0,0 +1,118 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package joinv1 + +import ( + "github.com/gravitational/trace" + + joinv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/join/v1" + "github.com/gravitational/teleport/lib/join/internal/messages" +) + +func boundKeypairInitToMessage(req *joinv1.BoundKeypairInit) (*messages.BoundKeypairInit, error) { + clientParams, err := clientParamsToMessage(req.ClientParams) + if err != nil { + return nil, trace.Wrap(err) + } + return &messages.BoundKeypairInit{ + ClientParams: clientParams, + InitialJoinSecret: req.InitialJoinSecret, + PreviousJoinState: req.PreviousJoinState, + }, nil +} + +func boundKeypairInitFromMessage(msg *messages.BoundKeypairInit) (*joinv1.BoundKeypairInit, error) { + clientParams, err := clientParamsFromMessage(msg.ClientParams) + if err != nil { + return nil, trace.Wrap(err) + } + return &joinv1.BoundKeypairInit{ + ClientParams: clientParams, + InitialJoinSecret: msg.InitialJoinSecret, + PreviousJoinState: msg.PreviousJoinState, + }, nil +} + +func boundKeypairChallengeToMessage(resp *joinv1.BoundKeypairChallenge) *messages.BoundKeypairChallenge { + return &messages.BoundKeypairChallenge{ + PublicKey: resp.PublicKey, + Challenge: resp.Challenge, + } +} + +func boundKeypairChallengeFromMessage(msg *messages.BoundKeypairChallenge) *joinv1.BoundKeypairChallenge { + return &joinv1.BoundKeypairChallenge{ + PublicKey: msg.PublicKey, + Challenge: msg.Challenge, + } +} + +func boundKeypairChallengeSolutionToMessage(req *joinv1.BoundKeypairChallengeSolution) *messages.BoundKeypairChallengeSolution { + return &messages.BoundKeypairChallengeSolution{ + Solution: req.Solution, + } +} + +func boundKeypairRotationRequestToMessage(resp *joinv1.BoundKeypairRotationRequest) *messages.BoundKeypairRotationRequest { + return &messages.BoundKeypairRotationRequest{ + SignatureAlgorithmSuite: resp.SignatureAlgorithmSuite, + } +} + +func boundKeypairRotationRequestFromMessage(resp *messages.BoundKeypairRotationRequest) *joinv1.BoundKeypairRotationRequest { + return &joinv1.BoundKeypairRotationRequest{ + SignatureAlgorithmSuite: resp.SignatureAlgorithmSuite, + } +} + +func boundKeypairChallengeSolutionFromMessage(msg *messages.BoundKeypairChallengeSolution) *joinv1.BoundKeypairChallengeSolution { + return &joinv1.BoundKeypairChallengeSolution{ + Solution: msg.Solution, + } +} + +func boundKeypairRotationResponseToMessage(req *joinv1.BoundKeypairRotationResponse) *messages.BoundKeypairRotationResponse { + return &messages.BoundKeypairRotationResponse{ + PublicKey: req.PublicKey, + } +} + +func boundKeypairRotationResponseFromMessage(msg *messages.BoundKeypairRotationResponse) *joinv1.BoundKeypairRotationResponse { + return &joinv1.BoundKeypairRotationResponse{ + PublicKey: msg.PublicKey, + } +} + +func boundKeypairResultToMessage(req *joinv1.BoundKeypairResult) *messages.BoundKeypairResult { + if req == nil { + return nil + } + return &messages.BoundKeypairResult{ + JoinState: req.JoinState, + PublicKey: req.PublicKey, + } +} + +func boundKeypairResultFromMessage(msg *messages.BoundKeypairResult) *joinv1.BoundKeypairResult { + if msg == nil { + return nil + } + return &joinv1.BoundKeypairResult{ + JoinState: msg.JoinState, + PublicKey: msg.PublicKey, + } +} diff --git a/lib/join/server.go b/lib/join/server.go index 6e1ba91da23c1..94c8810b2345e 100644 --- a/lib/join/server.go +++ b/lib/join/server.go @@ -36,14 +36,17 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/auth/keystore" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/events" joinauthz "github.com/gravitational/teleport/lib/join/internal/authz" "github.com/gravitational/teleport/lib/join/internal/diagnostic" "github.com/gravitational/teleport/lib/join/internal/messages" + "github.com/gravitational/teleport/lib/services/readonly" "github.com/gravitational/teleport/lib/utils/hostid" logutils "github.com/gravitational/teleport/lib/utils/log" ) @@ -58,6 +61,14 @@ type AuthService interface { GenerateBotCertsForJoin(ctx context.Context, provisionToken types.ProvisionToken, req *BotCertsParams) (*proto.Certs, string, error) EmitAuditEvent(ctx context.Context, e apievents.AuditEvent) error GetAuthPreference(ctx context.Context) (types.AuthPreference, error) + GetReadOnlyAuthPreference(context.Context) (readonly.AuthPreference, error) + GetClusterName(context.Context) (types.ClusterName, error) + GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error) + GetKeyStore() *keystore.Manager + PatchToken(context.Context, string, func(types.ProvisionToken) (types.ProvisionToken, error)) (types.ProvisionToken, error) + UpsertLock(context.Context, types.Lock) error + CheckLockInForce(constants.LockingMode, []types.LockTarget) error + GetClock() clockwork.Clock } // ServerConfig holds configuration parameters for [Server]. @@ -99,7 +110,8 @@ func (s *Server) Join(stream messages.ServerStream) (err error) { diag := stream.Diagnostic() defer func() { if err != nil { - s.handleJoinFailure(ctx, diag, err) + diag.Set(func(i *diagnostic.Info) { i.Error = err }) + handleJoinFailure(ctx, s.cfg.AuthService, diag) } }() @@ -187,6 +199,8 @@ func (s *Server) handleJoinMethod( switch joinMethod { case types.JoinMethodToken: return s.handleTokenJoin(stream, authCtx, clientInit, provisionToken) + case types.JoinMethodBoundKeypair: + return s.handleBoundKeypairJoin(stream, authCtx, clientInit, provisionToken) default: // TODO(nklaassen): implement checks for all join methods. return nil, trace.NotImplemented("join method %s is not yet implemented by the new join service", joinMethod) @@ -312,13 +326,15 @@ func (s *Server) makeResult( authCtx *joinauthz.Context, clientInit *messages.ClientInit, clientParams *messages.ClientParams, + rawClaims any, provisionToken types.ProvisionToken, ) (messages.Response, error) { switch types.SystemRole(clientInit.SystemRole) { case types.RoleInstance: return s.makeHostResult(ctx, diag, authCtx, clientParams.HostParams, provisionToken) case types.RoleBot: - return s.makeBotResult(ctx, diag, authCtx, clientParams.BotParams, provisionToken) + result, _, err := s.makeBotResult(ctx, diag, authCtx, clientParams.BotParams, rawClaims, provisionToken) + return result, trace.Wrap(err) default: return nil, trace.NotImplemented("new join service only supports Instance and Bot system roles, client requested %s", clientInit.SystemRole) } @@ -408,23 +424,24 @@ func (s *Server) makeBotResult( diag *diagnostic.Diagnostic, authCtx *joinauthz.Context, botParams *messages.BotParams, + rawClaims any, provisionToken types.ProvisionToken, -) (*messages.BotResult, error) { - certsParams, err := makeBotCertsParams(diag, authCtx, botParams) +) (*messages.BotResult, string, error) { + certsParams, err := makeBotCertsParams(diag, authCtx, botParams, rawClaims) if err != nil { - return nil, trace.Wrap(err) + return nil, "", trace.Wrap(err) } - certs, _, err := s.cfg.AuthService.GenerateBotCertsForJoin(ctx, provisionToken, certsParams) + certs, botInstanceID, err := s.cfg.AuthService.GenerateBotCertsForJoin(ctx, provisionToken, certsParams) if err != nil { - return nil, trace.Wrap(err) + return nil, "", trace.Wrap(err) } certificates, err := convertCerts(certs) if err != nil { - return nil, trace.Wrap(err) + return nil, "", trace.Wrap(err) } return &messages.BotResult{ Certificates: *certificates, - }, nil + }, botInstanceID, nil } // makeBotCertsParams returns [BotCertsParams] populated by the @@ -433,6 +450,7 @@ func makeBotCertsParams( diag *diagnostic.Diagnostic, authCtx *joinauthz.Context, botParams *messages.BotParams, + rawClaims any, ) (*BotCertsParams, error) { // GenerateBotCertsForJoin requires the TLS key to be PEM-encoded. tlsPub, err := x509.ParsePKIXPublicKey(botParams.PublicKeys.PublicTLSKey) @@ -458,6 +476,7 @@ func makeBotCertsParams( BotGeneration: int32(authCtx.BotGeneration), Expires: botParams.Expires, RemoteAddr: diag.Get().RemoteAddr, + RawJoinClaims: rawClaims, }, nil } @@ -531,10 +550,9 @@ func setDiagnosticClientParams(diag *diagnostic.Diagnostic, clientParams *messag } } -func (s *Server) handleJoinFailure(ctx context.Context, diag *diagnostic.Diagnostic, err error) { - diag.Set(func(i *diagnostic.Info) { i.Error = err }) +func handleJoinFailure(ctx context.Context, emitter apievents.Emitter, diag *diagnostic.Diagnostic) { log.LogAttrs(ctx, slog.LevelWarn, "Failure to join cluster occurred", diag.SlogAttrs()...) - if err := s.cfg.AuthService.EmitAuditEvent(context.WithoutCancel(ctx), makeAuditEvent(diag)); err != nil { + if err := emitter.EmitAuditEvent(context.WithoutCancel(ctx), makeAuditEvent(diag)); err != nil { log.WarnContext(ctx, "Failed to emit failed join event", "error", err) } } diff --git a/lib/join/server_boundkeypair.go b/lib/join/server_boundkeypair.go new file mode 100644 index 0000000000000..aa9dfaa434ba1 --- /dev/null +++ b/lib/join/server_boundkeypair.go @@ -0,0 +1,284 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package join + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/client" + "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/join/boundkeypair" + "github.com/gravitational/teleport/lib/join/internal/authz" + "github.com/gravitational/teleport/lib/join/internal/diagnostic" + "github.com/gravitational/teleport/lib/join/internal/messages" +) + +// handleBoundKeypairJoin handles join attempts for the bound keypair join +// method. +// +// The bound keypair join method involves the following messages: +// +// client->server ClientInit +// client<-server ServerInit +// client->server BoundKeypairInit +// client<-server BoundKeypairChallenge +// client->server BoundKeypairChallengeSolution +// +// (optional additional steps if keypair rotation is required) +// client<-server: BoundKeypairRotationRequest +// client->server: BoundKeypairRotationResponse +// client<-server: BoundKeypairChallenge +// client->server: BoundKeypairChallengeSolution +// +// client<-server: Result containing BoundKeypairResult +// +// At this point the ServerInit message has already been sent, what's left is +// to receive the BoundKeypairInit message, handle the challenge-response (and +// rotation if necessary), and send the final result if everything checks out. +func (s *Server) handleBoundKeypairJoin( + stream messages.ServerStream, + authCtx *authz.Context, + clientInit *messages.ClientInit, + provisionToken types.ProvisionToken, +) (*messages.BotResult, error) { + ctx := stream.Context() + diag := stream.Diagnostic() + // Only bot joining is supported at the moment - unique ID verification is + // required and this is currently only implemented for bots. + if clientInit.SystemRole != types.RoleBot.String() { + return nil, trace.BadParameter("bound keypair joining is only supported for bots") + } + boundKeypairInit, err := messages.RecvRequest[*messages.BoundKeypairInit](stream) + if err != nil { + return nil, trace.Wrap(err) + } + issueChallenge := func(challenge *messages.BoundKeypairChallenge) (*messages.BoundKeypairChallengeSolution, error) { + if err := stream.Send(challenge); err != nil { + return nil, trace.Wrap(err) + } + solution, err := messages.RecvRequest[*messages.BoundKeypairChallengeSolution](stream) + return solution, trace.Wrap(err) + } + issueRotationRequest := func(rotationReq *messages.BoundKeypairRotationRequest) (*messages.BoundKeypairRotationResponse, error) { + if err := stream.Send(rotationReq); err != nil { + return nil, trace.Wrap(err) + } + rotationResp, err := messages.RecvRequest[*messages.BoundKeypairRotationResponse](stream) + return rotationResp, trace.Wrap(err) + } + generateBotCerts := func(ctx context.Context, previousBotInstanceID string, claims any) (*messages.Certificates, string, error) { + botCertsParams, err := makeBotCertsParams(diag, authCtx, boundKeypairInit.ClientParams.BotParams, claims) + if err != nil { + return nil, "", trace.Wrap(err) + } + botCertsParams.PreviousBotInstanceID = previousBotInstanceID + protoCerts, botInstanceID, err := s.cfg.AuthService.GenerateBotCertsForJoin(ctx, provisionToken, botCertsParams) + if err != nil { + return nil, "", trace.Wrap(err) + } + botCerts, err := convertCerts(protoCerts) + if err != nil { + return nil, "", trace.Wrap(err) + } + return botCerts, botInstanceID, nil + } + return boundkeypair.HandleBoundKeypairJoin(ctx, &boundkeypair.JoinParams{ + AuthService: s.cfg.AuthService, + AuthCtx: authCtx, + Diag: diag, + ProvisionToken: provisionToken, + ClientInit: clientInit, + BoundKeypairInit: boundKeypairInit, + IssueChallenge: issueChallenge, + IssueRotationRequest: issueRotationRequest, + GenerateBotCerts: generateBotCerts, + Clock: s.clock, + Logger: log, + }) +} + +// AdaptRegisterUsingBoundKeypairMethod handles requests from the legacy join +// gRPC service and adapts the request types to the protocol-agnostic types +// defined in [messages] before calling [boundkeypair.HandleBoundKeypairJoin] +// which contains the actual logic for bound keypair joining. +// +// TODO(nklaassen): DELETE IN 20 when removing the legacy join service. +func AdaptRegisterUsingBoundKeypairMethod( + ctx context.Context, + a AuthService, + createBoundKeypairValidator boundkeypair.CreateBoundKeypairValidator, + req *proto.RegisterUsingBoundKeypairInitialRequest, + challengeResponse client.RegisterUsingBoundKeypairChallengeResponseFunc, +) (_ *client.BoundKeypairRegistrationResponse, err error) { + diag := diagnostic.New() + diag.Set(func(i *diagnostic.Info) { + i.RemoteAddr = req.JoinRequest.RemoteAddr + i.Role = req.JoinRequest.Role.String() + i.RequestedJoinMethod = string(types.JoinMethodBoundKeypair) + i.BotInstanceID = req.JoinRequest.BotInstanceID + i.BotGeneration = uint64(req.JoinRequest.BotGeneration) + }) + defer func() { + if err != nil { + diag.Set(func(i *diagnostic.Info) { i.Error = err }) + handleJoinFailure(ctx, a, diag) + } + }() + + // Construct an [authz.Context] to pass to HandleBoundKeypairJoin. + authCtx := &authz.Context{ + // These are verified at the gRPC layer by the legacy join service. + BotInstanceID: req.JoinRequest.BotInstanceID, + BotGeneration: uint64(req.JoinRequest.BotGeneration), + } + + // Only bot joining is supported at the moment - unique ID verification is + // required and this is currently only implemented for bots. + if req.JoinRequest.Role != types.RoleBot { + return nil, trace.BadParameter("bound keypair joining is only supported for bots") + } + + provisionToken, err := a.ValidateToken(ctx, req.JoinRequest.Token) + if err != nil { + return nil, trace.Wrap(err) + } + // Set any diagnostic info we can get from the token. + diag.Set(func(i *diagnostic.Info) { + i.SafeTokenName = provisionToken.GetSafeName() + i.TokenJoinMethod = string(provisionToken.GetJoinMethod()) + i.TokenExpires = provisionToken.Expiry() + i.BotName = provisionToken.GetBotName() + }) + if provisionToken.GetJoinMethod() != types.JoinMethodBoundKeypair { + return nil, trace.BadParameter("specified join token is not for `%s` method", types.JoinMethodBoundKeypair) + } + + // Assert that the provision token allows the requested system role. + if err := ProvisionTokenAllowsRole(provisionToken, req.JoinRequest.Role); err != nil { + return nil, trace.Wrap(err) + } + + clientInit := clientInitFromRegisterUsingTokenRequest(req.JoinRequest, string(types.JoinMethodBoundKeypair)) + clientParams, err := clientParamsFromRegisterUsingTokenRequest(req.JoinRequest) + if err != nil { + return nil, trace.Wrap(err) + } + + boundKeypairInit := &messages.BoundKeypairInit{ + ClientParams: *clientParams, + InitialJoinSecret: req.InitialJoinSecret, + PreviousJoinState: req.PreviousJoinState, + } + + generateBotCerts := func(ctx context.Context, previousBotInstanceID string, claims any) (*messages.Certificates, string, error) { + botCertsParams, err := makeBotCertsParams(diag, authCtx, clientParams.BotParams, claims) + if err != nil { + return nil, "", trace.Wrap(err) + } + botCertsParams.PreviousBotInstanceID = previousBotInstanceID + protoCerts, botInstanceID, err := a.GenerateBotCertsForJoin(ctx, provisionToken, botCertsParams) + if err != nil { + return nil, "", trace.Wrap(err) + } + botCerts, err := convertCerts(protoCerts) + if err != nil { + return nil, "", trace.Wrap(err) + } + return botCerts, botInstanceID, nil + } + + botResult, err := boundkeypair.HandleBoundKeypairJoin(ctx, &boundkeypair.JoinParams{ + AuthService: a, + AuthCtx: authCtx, + Diag: diag, + ProvisionToken: provisionToken, + ClientInit: clientInit, + BoundKeypairInit: boundKeypairInit, + IssueChallenge: adaptBoundKeypairChallenge(challengeResponse), + IssueRotationRequest: adaptBoundKeypairRotationRequest(challengeResponse), + CreateBoundKeypairValidator: createBoundKeypairValidator, + GenerateBotCerts: generateBotCerts, + Clock: a.GetClock(), + Logger: log, + }) + if err != nil { + return nil, trace.Wrap(err) + } + certs, err := protoCertsFromCertificates(botResult.Certificates) + if err != nil { + return nil, trace.Wrap(err) + } + return &client.BoundKeypairRegistrationResponse{ + Certs: certs, + BoundPublicKey: string(botResult.BoundKeypairResult.PublicKey), + JoinState: botResult.BoundKeypairResult.JoinState, + }, nil +} + +func adaptBoundKeypairChallenge(challengeResponseFunc client.RegisterUsingBoundKeypairChallengeResponseFunc) boundkeypair.ChallengeResponseFunc { + return func(challengeMsg *messages.BoundKeypairChallenge) (*messages.BoundKeypairChallengeSolution, error) { + challenge := &proto.RegisterUsingBoundKeypairMethodResponse{ + Response: &proto.RegisterUsingBoundKeypairMethodResponse_Challenge{ + Challenge: &proto.RegisterUsingBoundKeypairChallenge{ + PublicKey: string(challengeMsg.PublicKey), + Challenge: challengeMsg.Challenge, + }, + }, + } + resp, err := challengeResponseFunc(challenge) + if err != nil { + return nil, trace.Wrap(err) + } + challengeResp := resp.GetChallengeResponse() + if challengeResp == nil { + return nil, trace.BadParameter("client did not send challenge response, got %T", resp.Payload) + } + return &messages.BoundKeypairChallengeSolution{ + Solution: challengeResp.Solution, + }, nil + } +} + +func adaptBoundKeypairRotationRequest(challengeResponseFunc client.RegisterUsingBoundKeypairChallengeResponseFunc) boundkeypair.RotationFunc { + return func(rotationMsg *messages.BoundKeypairRotationRequest) (*messages.BoundKeypairRotationResponse, error) { + suite, err := types.SignatureAlgorithmSuiteFromString(rotationMsg.SignatureAlgorithmSuite) + if err != nil { + return nil, trace.Wrap(err) + } + challenge := &proto.RegisterUsingBoundKeypairMethodResponse{ + Response: &proto.RegisterUsingBoundKeypairMethodResponse_Rotation{ + Rotation: &proto.RegisterUsingBoundKeypairRotationRequest{ + SignatureAlgorithmSuite: suite, + }, + }, + } + resp, err := challengeResponseFunc(challenge) + if err != nil { + return nil, trace.Wrap(err) + } + rotationResp := resp.GetRotationResponse() + if rotationResp == nil { + return nil, trace.BadParameter("client did not send rotation response, got %T", resp.Payload) + } + return &messages.BoundKeypairRotationResponse{ + PublicKey: []byte(rotationResp.PublicKey), + }, nil + } +} diff --git a/lib/join/server_token.go b/lib/join/server_token.go index e58e711b6515e..54688bfba0779 100644 --- a/lib/join/server_token.go +++ b/lib/join/server_token.go @@ -47,6 +47,7 @@ func (s *Server) handleTokenJoin( authCtx, clientInit, &tokenInit.ClientParams, + nil, /*rawClaims*/ provisionToken, ) return result, trace.Wrap(err)