Add login_publickey tests, and register_publickey tests. Fixed a bug in login, and a bug in register

This commit is contained in:
Tak Wai Wong 2022-07-14 14:23:13 -07:00
parent 94799f1fd1
commit c905b1c09d
12 changed files with 1162 additions and 21 deletions

View file

@ -30,10 +30,10 @@ import (
type LoginPublicKeyHandler interface {
AccountExists(ctx context.Context) (string, *jsonerror.MatrixError)
IsValidUserIdForRegistration(userId string) bool
CreateLogin() *Login
GetSession() string
GetType() string
IsValidUserId(userId string) bool
ValidateLoginResponse() (bool, *jsonerror.MatrixError)
}

View file

@ -73,6 +73,10 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
return "", jsonerror.Forbidden("the address is incorrect, or the account does not exist.")
}
if !pk.IsValidUserId(localPart) {
return "", jsonerror.InvalidUsername("the username is not valid.")
}
res := userapi.QueryAccountAvailabilityResponse{}
if err := pk.userAPI.QueryAccountAvailability(ctx, &userapi.QueryAccountAvailabilityRequest{
Localpart: localPart,
@ -80,7 +84,7 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
return "", jsonerror.Unknown("failed to check availability: " + err.Error())
}
if res.Available {
if localPart == "" || res.Available {
return "", jsonerror.Forbidden("the address is incorrect, account does not exist")
}
@ -89,7 +93,7 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
var validChainAgnosticIdRegex = regexp.MustCompile("^eip155=3a[0-9]+=3a0x[0-9a-fA-F]+$")
func (pk LoginPublicKeyEthereum) IsValidUserIdForRegistration(userId string) bool {
func (pk LoginPublicKeyEthereum) IsValidUserId(userId string) bool {
// Verify that the user ID is a valid one according to spec.
// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md
@ -100,9 +104,9 @@ func (pk LoginPublicKeyEthereum) IsValidUserIdForRegistration(userId string) boo
isValid := validChainAgnosticIdRegex.MatchString(userId)
// In addition, double check that the user ID for registration
// In addition, double check that the user ID
// matches the authentication data in the request.
return isValid && userId == pk.UserId
return isValid && strings.ToLower(userId) == pk.UserId
}
func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) {

View file

@ -0,0 +1,472 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/internal/mapsutil"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/test"
uapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/stretchr/testify/assert"
)
type loginContext struct {
config *config.ClientAPI
userInteractive *UserInteractive
}
func createLoginContext(t *testing.T) *loginContext {
chainIds := []int{4}
cfg := &config.ClientAPI{
Matrix: &config.Global{
ServerName: test.TestServerName,
},
Derived: &config.Derived{},
PasswordAuthenticationDisabled: true,
PublicKeyAuthentication: config.PublicKeyAuthentication{
Ethereum: config.EthereumAuthConfig{
Enabled: true,
Version: 1,
ChainIDs: chainIds,
},
},
}
pkFlows := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationFlows()
cfg.Derived.Registration.Flows = append(cfg.Derived.Registration.Flows, pkFlows...)
pkParams := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationParams()
cfg.Derived.Registration.Params = mapsutil.MapsUnion(cfg.Derived.Registration.Params, pkParams)
var userAPI fakePublicKeyUserApi
var loginApi uapi.UserLoginAPI
userInteractive := NewUserInteractive(
loginApi,
&userAPI,
cfg)
return &loginContext{
config: cfg,
userInteractive: userInteractive,
}
}
type fakePublicKeyUserApi struct {
UserInternalAPIForLogin
uapi.UserLoginAPI
uapi.ClientUserAPI
DeletedTokens []string
}
func (ua *fakePublicKeyUserApi) QueryAccountAvailability(ctx context.Context, req *uapi.QueryAccountAvailabilityRequest, res *uapi.QueryAccountAvailabilityResponse) error {
if req.Localpart == "does_not_exist" {
res.Available = true
return nil
}
res.Available = false
return nil
}
func (ua *fakePublicKeyUserApi) QueryAccountByPassword(ctx context.Context, req *uapi.QueryAccountByPasswordRequest, res *uapi.QueryAccountByPasswordResponse) error {
if req.PlaintextPassword == "invalidpassword" {
res.Account = nil
return nil
}
res.Exists = true
res.Account = &uapi.Account{}
return nil
}
func (ua *fakePublicKeyUserApi) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
return nil
}
func (ua *fakePublicKeyUserApi) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error {
return nil
}
func (*fakePublicKeyUserApi) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
if req.Token == "invalidtoken" {
return nil
}
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
return nil
}
func publicKeyTestSession(
ctx *context.Context,
cfg *config.ClientAPI,
userInteractive *UserInteractive,
userAPI *fakePublicKeyUserApi,
) string {
emptyAuth := struct {
Body string
}{
Body: `{
"type": "m.login.publickey"
}`,
}
_, cleanup, err := LoginFromJSONReader(
*ctx,
strings.NewReader(emptyAuth.Body),
userAPI,
userAPI,
userAPI,
userInteractive,
cfg)
if cleanup != nil {
cleanup(*ctx, nil)
}
json := err.JSON.(Challenge)
return json.Session
}
func TestLoginPublicKeyEthereum(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
wallet, _ := test.CreateTestAccount()
message, _ := test.CreateEip4361TestMessage(wallet.PublicAddress)
signature, _ := test.SignMessage(message.String(), wallet.PrivateKey)
sessionId := publicKeyTestSession(
&ctx,
loginContext.config,
loginContext.userInteractive,
&userAPI,
)
// Escape \t and \n. Work around for marshalling and unmarshalling message.
msgStr := test.FromEip4361MessageToString(message)
body := fmt.Sprintf(`{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "%v",
"user_id": "%v",
"message": "%v",
"signature": "%v"
}
}`,
sessionId,
wallet.Eip155UserId,
msgStr,
signature,
)
test := struct {
Body string
}{
Body: body,
}
// Test
login, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Nilf(err, "err actual: %v, expected: nil", err)
assert.NotNil(login, "login: actual: nil, expected: not nil")
assert.Truef(
login.Identifier.Type == "m.id.decentralizedid",
"login.Identifier.Type actual: %v, expected: %v", login.Identifier.Type, "m.id.decentralizedid")
walletAddress := strings.ToLower(wallet.Eip155UserId)
assert.Truef(
login.Identifier.User == walletAddress,
"login.Identifier.User actual: %v, expected: %v", login.Identifier.User, walletAddress)
}
func TestLoginPublicKeyEthereumMissingSignature(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
wallet, _ := test.CreateTestAccount()
message, _ := test.CreateEip4361TestMessage(wallet.PublicAddress)
sessionId := publicKeyTestSession(
&ctx,
loginContext.config,
loginContext.userInteractive,
&userAPI,
)
// Escape \t and \n. Work around for marshalling and unmarshalling message.
msgStr := test.FromEip4361MessageToString(message)
body := fmt.Sprintf(`{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "%v",
"user_id": "%v",
"message": "%v"
}
}`,
sessionId,
wallet.Eip155UserId,
msgStr,
)
test := struct {
Body string
}{
Body: body,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Truef(
err.Code == http.StatusUnauthorized,
"err.Code actual: %v, expected: %v", err.Code, http.StatusUnauthorized)
json := err.JSON.(*jsonerror.MatrixError)
expectedErr := jsonerror.InvalidSignature("")
assert.Truef(
json.ErrCode == expectedErr.ErrCode,
"err.JSON.ErrCode actual: %v, expected: %v", json.ErrCode, expectedErr.ErrCode)
}
func TestLoginPublicKeyEthereumEmptyMessage(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
wallet, _ := test.CreateTestAccount()
sessionId := publicKeyTestSession(
&ctx,
loginContext.config,
loginContext.userInteractive,
&userAPI,
)
body := fmt.Sprintf(`{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "%v",
"user_id": "%v"
}
}`, sessionId, wallet.Eip155UserId)
test := struct {
Body string
}{
Body: body,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Truef(
err.Code == http.StatusUnauthorized,
"err.Code actual: %v, expected: %v", err.Code, http.StatusUnauthorized)
json := err.JSON.(*jsonerror.MatrixError)
expectedErr := jsonerror.InvalidParam("")
assert.Truef(
json.ErrCode == expectedErr.ErrCode,
"err.JSON.ErrCode actual: %v, expected: %v", json.ErrCode, expectedErr.ErrCode)
}
func TestLoginPublicKeyEthereumWrongUserId(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
wallet, _ := test.CreateTestAccount()
sessionId := publicKeyTestSession(
&ctx,
loginContext.config,
loginContext.userInteractive,
&userAPI,
)
body := fmt.Sprintf(`{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "%v",
"user_id": "%v"
}
}`,
sessionId,
wallet.PublicAddress)
test := struct {
Body string
}{
Body: body,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Truef(
err.Code == http.StatusForbidden,
"err.Code actual: %v, expected: %v", err.Code, http.StatusForbidden)
}
func TestLoginPublicKeyEthereumMissingUserId(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
sessionId := publicKeyTestSession(
&ctx,
loginContext.config,
loginContext.userInteractive,
&userAPI,
)
body := fmt.Sprintf(`{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "%v"
}
}`, sessionId)
test := struct {
Body string
}{
Body: body,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Truef(
err.Code == http.StatusForbidden,
"err.Code actual: %v, expected: %v", err.Code, http.StatusForbidden)
}
func TestLoginPublicKeyEthereumAccountNotAvailable(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
sessionId := publicKeyTestSession(
&ctx,
loginContext.config,
loginContext.userInteractive,
&userAPI,
)
body := fmt.Sprintf(`{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "%v",
"user_id": "does_not_exist"
}
}`, sessionId)
test := struct {
Body string
}{
Body: body,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Truef(
err.Code == http.StatusForbidden,
"err.Code actual: %v, expected: %v", err.Code, http.StatusForbidden)
}

View file

@ -0,0 +1,161 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"net/http"
"strings"
"testing"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/setup/config"
"github.com/stretchr/testify/assert"
)
func TestLoginPublicKeyNewSession(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
test := struct {
Body string
}{
Body: `{ "type": "m.login.publickey" }`,
}
// Test
login, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.NotNilf(
err,
"err actual: not nil returned %+v, expected: nil", login)
assert.Truef(
err.Code == http.StatusUnauthorized,
"err.Code actual: %v, expected: %v", err.Code, http.StatusUnauthorized)
challenge := err.JSON.(Challenge)
assert.NotEmptyf(challenge.Session, "challenge.Session")
assert.NotEmptyf(challenge.Completed, "challenge.Completed")
assert.Truef(
authtypes.LoginTypePublicKeyEthereum == challenge.Flows[0].Stages[0],
"challenge.Flows[0].Stages[0] actual: %v, expected: %v", challenge.Flows[0].Stages[0], authtypes.LoginTypePublicKeyEthereum)
params := challenge.Params[authtypes.LoginTypePublicKeyEthereum]
assert.NotEmptyf(
params,
"challenge.Params[\"%v\"] actual %v, expected %v",
authtypes.LoginTypePublicKeyEthereum,
params,
"[object]")
ethParams := params.(config.EthereumAuthParams)
assert.NotEmptyf(ethParams.ChainIDs, "ChainIDs actual: empty, expected not empty")
assert.NotEmptyf(ethParams.Nonce, "Nonce actual: \"\", expected: not empty")
assert.NotEmptyf(ethParams.Version, "Version actual: \"\", expected: not empty")
}
func TestLoginPublicKeyInvalidSessionId(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
test := struct {
Body string
}{
Body: `{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.ethereum",
"session": "invalid_session_id"
}
}`,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.Truef(
err.Code == http.StatusUnauthorized,
"err.Code actual %v, expected %v", err.Code, http.StatusUnauthorized)
}
func TestLoginPublicKeyInvalidAuthType(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
ctx := context.Background()
loginContext := createLoginContext(t)
test := struct {
Body string
}{
Body: `{
"type": "m.login.publickey",
"auth": {
"type": "m.login.publickey.someAlgo"
}
}`,
}
// Test
_, cleanup, err := LoginFromJSONReader(
ctx,
strings.NewReader(test.Body),
&userAPI,
&userAPI,
&userAPI,
loginContext.userInteractive,
loginContext.config)
if cleanup != nil {
cleanup(ctx, nil)
}
// Asserts
assert := assert.New(t)
assert.NotNil(err, "Expected an err response.actual: nil")
assert.Truef(
err.Code == http.StatusUnauthorized,
"err.Code actual %v, expected %v", err.Code, http.StatusUnauthorized)
_, ok := err.JSON.(Challenge)
assert.False(
ok,
"should not return a Challenge response")
}

View file

@ -173,13 +173,13 @@ type Challenge struct {
func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
paramsCopy := mapsutil.MapCopy(u.Params)
for key, element := range paramsCopy {
p := getAuthParams(element)
p := GetAuthParams(element)
if p != nil {
// If an auth flow has params,
// send it as part of the challenge.
paramsCopy[key] = p
// If an auth flow generated a nonce, track it as well.
// If an auth flow generated a nonce, add it to the session.
nonce := getAuthParamNonce(p)
if nonce != "" {
u.Sessions[sessionID] = append(u.Sessions[sessionID], nonce)
@ -280,7 +280,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
return login, nil
}
func getAuthParams(params interface{}) interface{} {
func GetAuthParams(params interface{}) interface{} {
v, ok := params.(config.AuthParams)
if ok {
p := v.GetParams()

View file

@ -45,6 +45,7 @@ import (
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/userutil"
"github.com/matrix-org/dendrite/internal/mapsutil"
userapi "github.com/matrix-org/dendrite/userapi/api"
)
@ -247,7 +248,7 @@ type authDict struct {
}
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
type userInteractiveResponse struct {
type UserInteractiveResponse struct {
Flows []authtypes.Flow `json:"flows"`
Completed []authtypes.LoginType `json:"completed"`
Params map[string]interface{} `json:"params"`
@ -260,9 +261,18 @@ func newUserInteractiveResponse(
sessionID string,
fs []authtypes.Flow,
params map[string]interface{},
) userInteractiveResponse {
return userInteractiveResponse{
fs, sessions.getCompletedStages(sessionID), params, sessionID,
) UserInteractiveResponse {
paramsCopy := mapsutil.MapCopy(params)
for key, element := range paramsCopy {
p := auth.GetAuthParams(element)
if p != nil {
// If an auth flow has params, make a new copy
// and send it as part of the response.
paramsCopy[key] = p
}
}
return UserInteractiveResponse{
fs, sessions.getCompletedStages(sessionID), paramsCopy, sessionID,
}
}

View file

@ -69,7 +69,7 @@ func handlePublicKeyRegistration(
}
}
isValidUserId := authHandler.IsValidUserIdForRegistration(r.Username)
isValidUserId := authHandler.IsValidUserId(r.Username)
if !isValidUserId {
return false, "", &util.JSONResponse{
Code: http.StatusUnauthorized,

View file

@ -0,0 +1,386 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routing
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/matrix-org/dendrite/clientapi/auth"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/internal/mapsutil"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/test"
"github.com/matrix-org/dendrite/userapi/api"
uapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/util"
"github.com/stretchr/testify/assert"
)
const testCaip10UserId = "eip155=3a1=3a0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb"
type registerContext struct {
config *config.ClientAPI
userInteractive *auth.UserInteractive
}
func createRegisterContext(t *testing.T) *registerContext {
chainIds := []int{4}
cfg := &config.ClientAPI{
Matrix: &config.Global{
ServerName: test.TestServerName,
},
Derived: &config.Derived{},
PasswordAuthenticationDisabled: true,
PublicKeyAuthentication: config.PublicKeyAuthentication{
Ethereum: config.EthereumAuthConfig{
Enabled: true,
Version: 1,
ChainIDs: chainIds,
},
},
}
pkFlows := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationFlows()
cfg.Derived.Registration.Flows = append(cfg.Derived.Registration.Flows, pkFlows...)
pkParams := cfg.PublicKeyAuthentication.GetPublicKeyRegistrationParams()
cfg.Derived.Registration.Params = mapsutil.MapsUnion(cfg.Derived.Registration.Params, pkParams)
var userAPI fakePublicKeyUserApi
var loginApi uapi.UserLoginAPI
userInteractive := auth.NewUserInteractive(
loginApi,
&userAPI,
cfg)
return &registerContext{
config: cfg,
userInteractive: userInteractive,
}
}
type fakeHttpRequest struct {
request *http.Request
body []byte
registerRequest registerRequest
}
func createFakeHttpRequest(body string) *fakeHttpRequest {
var r registerRequest
req, _ := http.NewRequest(http.MethodPost, "", strings.NewReader(body))
reqBody := []byte(body)
json.Unmarshal([]byte(body), &r)
return &fakeHttpRequest{
request: req,
body: reqBody,
registerRequest: r,
}
}
type fakePublicKeyUserApi struct {
auth.UserInternalAPIForLogin
uapi.UserLoginAPI
uapi.ClientUserAPI
DeletedTokens []string
}
func (ua *fakePublicKeyUserApi) QueryAccountAvailability(ctx context.Context, req *uapi.QueryAccountAvailabilityRequest, res *uapi.QueryAccountAvailabilityResponse) error {
if req.Localpart == "does_not_exist" {
res.Available = true
return nil
}
res.Available = false
return nil
}
func (ua *fakePublicKeyUserApi) QueryAccountByPassword(ctx context.Context, req *uapi.QueryAccountByPasswordRequest, res *uapi.QueryAccountByPasswordResponse) error {
if req.PlaintextPassword == "invalidpassword" {
res.Account = nil
return nil
}
res.Exists = true
res.Account = &uapi.Account{}
return nil
}
func (ua *fakePublicKeyUserApi) PerformDeviceCreation(
ctx context.Context,
req *uapi.PerformDeviceCreationRequest,
res *uapi.PerformDeviceCreationResponse) error {
res.DeviceCreated = true
res.Device = &api.Device{
ID: "device_id",
UserID: req.Localpart,
AccessToken: req.AccessToken,
}
return nil
}
func (ua *fakePublicKeyUserApi) PerformAccountCreation(
ctx context.Context,
req *uapi.PerformAccountCreationRequest,
res *uapi.PerformAccountCreationResponse) error {
res.AccountCreated = true
res.Account = &api.Account{
AppServiceID: req.AppServiceID,
Localpart: req.Localpart,
ServerName: test.TestServerName,
UserID: fmt.Sprintf("@%s:%s", req.Localpart, test.TestServerName),
AccountType: req.AccountType,
}
return nil
}
func (ua *fakePublicKeyUserApi) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
return nil
}
func (ua *fakePublicKeyUserApi) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error {
return nil
}
func (*fakePublicKeyUserApi) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
if req.Token == "invalidtoken" {
return nil
}
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
return nil
}
func newRegistrationSession(
t *testing.T,
userId string,
cfg *config.ClientAPI,
userInteractive *auth.UserInteractive,
userAPI *fakePublicKeyUserApi,
) string {
body := fmt.Sprintf(`{
"auth": {
"type": "m.login.publickey",
"username": "%v"
}
}`,
userId)
test := struct {
Body string
}{
Body: body,
}
fakeReq := createFakeHttpRequest(test.Body)
sessionID := util.RandomString(sessionIDLength)
registerContext := createRegisterContext(t)
// Test
response := handleRegistrationFlow(
fakeReq.request,
fakeReq.body,
fakeReq.registerRequest,
sessionID,
registerContext.config,
userAPI,
"",
nil,
)
json := response.JSON.(UserInteractiveResponse)
return json.Session
}
func TestRegisterEthereum(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
wallet, _ := test.CreateTestAccount()
message, _ := test.CreateEip4361TestMessage(wallet.PublicAddress)
signature, _ := test.SignMessage(message.String(), wallet.PrivateKey)
registerContext := createRegisterContext(t)
sessionId := newRegistrationSession(
t,
wallet.Eip155UserId,
registerContext.config,
registerContext.userInteractive,
&userAPI,
)
// Escape \t and \n. Work around for marshalling and unmarshalling message.
msgStr := test.FromEip4361MessageToString(message)
body := fmt.Sprintf(`{
"username": "%v",
"auth": {
"type": "m.login.publickey",
"session": "%v",
"public_key_response": {
"type": "m.login.publickey.ethereum",
"session": "%v",
"user_id": "%v",
"message": "%v",
"signature": "%v"
}
}
}`,
wallet.Eip155UserId,
sessionId,
sessionId,
wallet.Eip155UserId,
msgStr,
signature,
)
test := struct {
Body string
}{
Body: body,
}
fakeReq := createFakeHttpRequest(test.Body)
// Test
response := handleRegistrationFlow(
fakeReq.request,
fakeReq.body,
fakeReq.registerRequest,
sessionId,
registerContext.config,
&userAPI,
"",
nil,
)
// Asserts
assert := assert.New(t)
assert.NotNil(response, "response actual: nil, expected: not nil")
registerRes := response.JSON.(registerResponse)
assert.Truef(
registerRes.UserID == wallet.Eip155UserId,
"registerRes.UserID actual: %v, expected: %v", registerRes.UserID, wallet.Eip155UserId)
assert.NotEmptyf(
registerRes.AccessToken,
"registerRes.AccessToken actual: empty, expected: not empty")
}
func TestNewRegistrationSession(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
body := fmt.Sprintf(`{
"auth": {
"type": "m.login.publickey",
"username": "%v"
}
}`,
testCaip10UserId)
test := struct {
Body string
}{
Body: body,
}
fakeReq := createFakeHttpRequest(test.Body)
sessionID := util.RandomString(sessionIDLength)
registerContext := createRegisterContext(t)
// Test
response := handleRegistrationFlow(
fakeReq.request,
fakeReq.body,
fakeReq.registerRequest,
sessionID,
registerContext.config,
&userAPI,
"",
nil,
)
// Asserts
assert := assert.New(t)
assert.NotNilf(response, "response not nil")
assert.Truef(
response.Code == http.StatusUnauthorized,
"response.Code actual %v, expected %v", response.Code, http.StatusUnauthorized)
json := response.JSON.(UserInteractiveResponse)
assert.NotEmptyf(json.Session, "response.Session")
assert.NotEmptyf(json.Completed, "response.Completed")
assert.Truef(
json.Completed[0] == authtypes.LoginStagePublicKeyNewRegistration,
"response.Completed[0] actual %v, expected %v", json.Completed[0], authtypes.LoginStagePublicKeyNewRegistration)
assert.Truef(
authtypes.LoginTypePublicKeyEthereum == json.Flows[0].Stages[0],
"response.Flows[0].Stages[0] actual: %v, expected: %v", json.Flows[0].Stages[0], authtypes.LoginTypePublicKeyEthereum)
params := json.Params[authtypes.LoginTypePublicKeyEthereum]
assert.NotEmptyf(
params,
"response.Params[\"%v\"] actual %v, expected %v",
authtypes.LoginTypePublicKeyEthereum,
params,
"[object]")
ethParams := params.(config.EthereumAuthParams)
assert.NotEmptyf(ethParams.ChainIDs, "ChainIDs actual: empty, expected not empty")
assert.NotEmptyf(ethParams.Nonce, "Nonce actual: \"\", expected: not empty")
assert.NotEmptyf(ethParams.Version, "Version actual: \"\", expected: not empty")
}
func TestRegistrationUnimplementedAlgo(t *testing.T) {
// Setup
var userAPI fakePublicKeyUserApi
body := fmt.Sprintf(`{
"auth": {
"type": "m.login.publickey.someAlgo",
"username": "%v"
}
}`,
testCaip10UserId)
test := struct {
Body string
}{
Body: body,
}
fakeReq := createFakeHttpRequest(test.Body)
sessionID := util.RandomString(sessionIDLength)
registerContext := createRegisterContext(t)
// Test
response := handleRegistrationFlow(
fakeReq.request,
fakeReq.body,
fakeReq.registerRequest,
sessionID,
registerContext.config,
&userAPI,
"",
nil,
)
// Asserts
assert := assert.New(t)
assert.NotNilf(response, "response not nil")
assert.Truef(
response.Code == http.StatusNotImplemented,
"response.Code actual %v, expected %v", response.Code, http.StatusNotImplemented)
}

2
go.mod
View file

@ -59,7 +59,7 @@ require (
nhooyr.io/websocket v1.8.7
)
require github.com/ethereum/go-ethereum v1.10.15 // indirect
require github.com/ethereum/go-ethereum v1.10.15
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect

View file

@ -54,7 +54,7 @@ type ClientAPI struct {
PasswordAuthenticationDisabled bool `yaml:"password_authentication_disabled"`
// Public key authentication
PublicKeyAuthentication publicKeyAuthentication `yaml:"public_key_authentication"`
PublicKeyAuthentication PublicKeyAuthentication `yaml:"public_key_authentication"`
}
func (c *ClientAPI) Defaults(generate bool) {

View file

@ -32,21 +32,21 @@ func (p EthereumAuthParams) GetNonce() string {
return p.Nonce
}
type ethereumAuthConfig struct {
type EthereumAuthConfig struct {
Enabled bool `yaml:"enabled"`
Version uint `yaml:"version"`
ChainIDs []int `yaml:"chain_ids"`
}
type publicKeyAuthentication struct {
Ethereum ethereumAuthConfig `yaml:"ethereum"`
type PublicKeyAuthentication struct {
Ethereum EthereumAuthConfig `yaml:"ethereum"`
}
func (pk *publicKeyAuthentication) Enabled() bool {
func (pk *PublicKeyAuthentication) Enabled() bool {
return pk.Ethereum.Enabled
}
func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
func (pk *PublicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
var flows []authtypes.Flow
if pk.Ethereum.Enabled {
flows = append(flows, authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypePublicKeyEthereum}})
@ -55,7 +55,7 @@ func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.F
return flows
}
func (pk *publicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
func (pk *PublicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
params := make(map[string]interface{})
if pk.Ethereum.Enabled {
p := EthereumAuthParams{

108
test/publickey_utils.go Normal file
View file

@ -0,0 +1,108 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package test
import (
"crypto/ecdsa"
"errors"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/spruceid/siwe-go"
)
const EthereumTestNetworkId = 4 // Rinkeby test network ID
const TestServerName = "localhost"
type EthereumTestWallet struct {
Eip155UserId string
PublicAddress string
PrivateKey *ecdsa.PrivateKey
}
// https://goethereumbook.org/wallet-generate/
func CreateTestAccount() (*EthereumTestWallet, error) {
// Create a new public / private key pair.
privateKey, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
// Get the public key
publicKey := privateKey.Public()
// Transform public key to the Ethereum address
publicKeyEcdsa, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("error casting public key to ECDSA")
}
address := crypto.PubkeyToAddress(*publicKeyEcdsa).Hex()
eip155UserId := fmt.Sprintf("eip155=3a%d=3a%s", EthereumTestNetworkId, address)
return &EthereumTestWallet{
PublicAddress: address,
PrivateKey: privateKey,
Eip155UserId: eip155UserId,
},
nil
}
func CreateEip4361TestMessage(
publicAddress string,
) (*siwe.Message, error) {
options := make(map[string]interface{})
options["chainId"] = 4 // Rinkeby test network
options["statement"] = "This is a test statement"
message, err := siwe.InitMessage(
TestServerName,
publicAddress,
"https://localhost/login",
siwe.GenerateNonce(),
options,
)
if err != nil {
return nil, err
}
return message, nil
}
func FromEip4361MessageToString(message *siwe.Message) string {
// Escape the formatting characters to
// prevent unmarshal exceptions.
str := strings.ReplaceAll(message.String(), "\n", "\\n")
str = strings.ReplaceAll(str, "\t", "\\t")
return str
}
// https://goethereumbook.org/signature-generate/
func SignMessage(message string, privateKey *ecdsa.PrivateKey) (string, error) {
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
data := []byte(msg)
hash := crypto.Keccak256Hash(data)
signature, err := crypto.Sign(hash.Bytes(), privateKey)
if err != nil {
return "", err
}
// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
return hexutil.Encode(signature), nil
}