diff --git a/clientapi/auth/login_publickey_ethereum.go b/clientapi/auth/login_publickey_ethereum.go
index 2a8cd78cb..938a9f816 100644
--- a/clientapi/auth/login_publickey_ethereum.go
+++ b/clientapi/auth/login_publickey_ethereum.go
@@ -16,51 +16,28 @@ package auth
import (
"context"
- "encoding/base64"
"encoding/json"
- "errors"
- "regexp"
"strings"
- "github.com/ethereum/go-ethereum/accounts"
- "github.com/ethereum/go-ethereum/common/hexutil"
- "github.com/ethereum/go-ethereum/crypto"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/userutil"
"github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api"
- "github.com/tidwall/gjson"
+ "github.com/spruceid/siwe-go"
)
type LoginPublicKeyEthereum struct {
// https://github.com/tak-hntlabs/matrix-spec-proposals/blob/main/proposals/3782-matrix-publickey-login-spec.md#client-sends-login-request-with-authentication-data
- Type string `json:"type"`
- Address string `json:"address"`
- Session string `json:"session"`
- Message string `json:"message"`
- Signature string `json:"signature"`
- HashFields publicKeyEthereumHashFields `json:"hashFields"`
- HashFieldsRaw string // Raw base64 encoded string of MessageFields for hash verification
+ Type string `json:"type"`
+ Address string `json:"address"`
+ Session string `json:"session"`
+ Message string `json:"message"`
+ Signature string `json:"signature"`
userAPI userapi.ClientUserAPI
config *config.ClientAPI
}
-type publicKeyEthereumHashFields struct {
- // Todo: See https://...
- Domain string `json:"domain"` // home server domain
- Address string `json:"address"` // Ethereum address. 0x...
- Nonce string `json:"nonce"` // session ID
- Version string `json:"version"` // version of the Matrix public key spec that the client is complying with
- ChainId string `json:"chainId"` // blockchain network ID.
-}
-
-type publicKeyEthereumRequiredFields struct {
- From string // Sender
- To string // Recipient
- Hash string // Hash of JSON representation of the message fields
-}
-
func CreatePublicKeyEthereumHandler(
reqBytes []byte,
userAPI userapi.ClientUserAPI,
@@ -71,15 +48,8 @@ func CreatePublicKeyEthereumHandler(
return nil, jsonerror.BadJSON("auth")
}
- hashFields := gjson.GetBytes(reqBytes, "hashFields")
- if !hashFields.Exists() {
- return nil, jsonerror.BadJSON("auth.hashFields")
- }
-
pk.config = config
pk.userAPI = userAPI
- // Save raw bytes for hash verification later.
- pk.HashFieldsRaw = hashFields.Raw
// Case-insensitive
pk.Address = strings.ToLower(pk.Address)
@@ -116,41 +86,20 @@ func (pk LoginPublicKeyEthereum) AccountExists(ctx context.Context) (string, *js
}
func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) {
- // Check signature to verify message was not tempered
- isVerified := verifySignature(pk.Address, []byte(pk.Message), pk.Signature)
- if !isVerified {
- return false, jsonerror.InvalidSignature("")
- }
-
- // Extract the required message fields for validation
- requiredFields, err := extractRequiredMessageFields(pk.Message)
+ // Parse the message to extract all the fields.
+ message, err := siwe.ParseMessage(pk.Message)
if err != nil {
- return false, jsonerror.MissingParam("message does not contain domain, address, or hash")
+ return false, jsonerror.InvalidParam("auth.message")
}
- // Verify that the hash is valid for the message fields.
- if !verifyHash(pk.HashFieldsRaw, requiredFields.Hash) {
- return false, jsonerror.Forbidden("error verifying message hash")
- }
-
- // Unmarshal the hashFields for further validation
- var authData publicKeyEthereumHashFields
- if err := json.Unmarshal([]byte(pk.HashFieldsRaw), &authData); err != nil {
- return false, jsonerror.BadJSON("auth.hashFields")
- }
-
- // Error if the message is not from the expected public address
- if pk.Address != requiredFields.From || requiredFields.From != pk.HashFields.Address {
- return false, jsonerror.Forbidden("address")
- }
-
- // Error if the message is not for the home server
- if requiredFields.To != pk.HashFields.Domain {
- return false, jsonerror.Forbidden("domain")
+ // Check signature to verify message was not tempered
+ _, err = message.Verify(pk.Signature, (*string)(&pk.config.Matrix.ServerName), nil, nil)
+ if err != nil {
+ return false, jsonerror.InvalidSignature(err.Error())
}
// Error if the chainId is not supported by the server.
- if !contains(pk.config.PublicKeyAuthentication.Ethereum.ChainIDs, authData.ChainId) {
+ if !contains(pk.config.PublicKeyAuthentication.Ethereum.ChainIDs, message.GetChainID()) {
return false, jsonerror.Forbidden("chainId")
}
@@ -169,75 +118,7 @@ func (pk LoginPublicKeyEthereum) CreateLogin() *Login {
return &login
}
-// The required fields in the signed message are:
-// 1. Domain -- home server. First non-whitespace characters in the first line.
-// 2. Address -- public address of the user. Starts with 0x... in the second line on its own.
-// 3. Hash -- Base64-encoded hash string of the metadata that represents the message.
-// The rest of the fields are informational, and will be used in the future.
-var regexpAuthority = regexp.MustCompile(`^\S+`)
-var regexpAddress = regexp.MustCompile(`\n(?P
0x\w+)\n`)
-var regexpHash = regexp.MustCompile(`\nHash: (?P.*)\n`)
-
-func extractRequiredMessageFields(message string) (*publicKeyEthereumRequiredFields, error) {
- var requiredFields publicKeyEthereumRequiredFields
- /*
- service.org wants you to sign in with your account:
- 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
-
- I accept the ServiceOrg Terms of Service: https://service.org/tos
-
- Hash: yfSIwarByPfKFxeYSCWN3XoIgNgeEFJffbwFA+JxYbA=
- */
-
- requiredFields.To = regexpAuthority.FindString(message)
-
- from := regexpAddress.FindStringSubmatch(message)
- if len(from) == 2 {
- requiredFields.From = from[1]
- }
-
- hash := regexpHash.FindStringSubmatch(message)
- if len(hash) == 2 {
- requiredFields.Hash = hash[1]
- }
-
- if len(requiredFields.To) == 0 || len(requiredFields.From) == 0 || len(requiredFields.Hash) == 0 {
- return nil, errors.New("required message fields are missing")
- }
-
- // Make these fields case-insensitive
- requiredFields.From = strings.ToLower(requiredFields.From)
- requiredFields.To = strings.ToLower(requiredFields.To)
-
- return &requiredFields, nil
-}
-
-func verifySignature(from string, message []byte, signature string) bool {
- decodedSig := hexutil.MustDecode(signature)
-
- message = accounts.TextHash(message)
- // Issue: https://stackoverflow.com/questions/49085737/geth-ecrecover-invalid-signature-recovery-id
- // Fix: https://gist.github.com/dcb9/385631846097e1f59e3cba3b1d42f3ed#file-eth_sign_verify-go
- decodedSig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
-
- recovered, err := crypto.SigToPub(message, decodedSig)
- if err != nil {
- return false
- }
-
- recoveredAddr := crypto.PubkeyToAddress(*recovered)
-
- addressStr := strings.ToLower(recoveredAddr.Hex())
- return from == addressStr
-}
-
-func verifyHash(rawStr string, expectedHash string) bool {
- hash := crypto.Keccak256([]byte(rawStr))
- hashStr := base64.StdEncoding.EncodeToString(hash)
- return expectedHash == hashStr
-}
-
-func contains(list []string, element string) bool {
+func contains(list []int, element int) bool {
for _, i := range list {
if i == element {
return true
diff --git a/clientapi/auth/login_test.go b/clientapi/auth/login_test.go
index 655455515..04e51323d 100644
--- a/clientapi/auth/login_test.go
+++ b/clientapi/auth/login_test.go
@@ -62,11 +62,10 @@ func TestLoginFromJSONReader(t *testing.T) {
},
}
userInteractive := UserInteractive{
- Completed: []string{},
- Flows: []userInteractiveFlow{},
- Types: make(map[string]Type),
- Sessions: make(map[string][]string),
- Params: make(map[string]interface{}),
+ Flows: []userInteractiveFlow{},
+ Types: make(map[string]Type),
+ Sessions: make(map[string][]string),
+ Params: make(map[string]interface{}),
}
for _, tst := range tsts {
@@ -148,11 +147,10 @@ func TestBadLoginFromJSONReader(t *testing.T) {
},
}
userInteractive := UserInteractive{
- Completed: []string{},
- Flows: []userInteractiveFlow{},
- Types: make(map[string]Type),
- Sessions: make(map[string][]string),
- Params: make(map[string]interface{}),
+ Flows: []userInteractiveFlow{},
+ Types: make(map[string]Type),
+ Sessions: make(map[string][]string),
+ Params: make(map[string]interface{}),
}
for _, tst := range tsts {
diff --git a/clientapi/auth/user_interactive.go b/clientapi/auth/user_interactive.go
index 2a5084bb9..b7e1724f1 100644
--- a/clientapi/auth/user_interactive.go
+++ b/clientapi/auth/user_interactive.go
@@ -21,6 +21,7 @@ import (
"sync"
"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/userapi/api"
"github.com/matrix-org/util"
@@ -177,17 +178,27 @@ type Challenge struct {
// Challenge returns an HTTP 401 with the supported flows for authenticating
func (u *UserInteractive) challenge(sessionID string) *util.JSONResponse {
u.RLock()
+ paramsCopy := mapsutil.MapCopy(u.Params)
completed := u.Sessions[sessionID]
flows := u.Flows
u.RUnlock()
+ for key, element := range paramsCopy {
+ p := GetAuthParams(element)
+ if p != nil {
+ // If an auth flow has params,
+ // send it as part of the challenge.
+ paramsCopy[key] = p
+ }
+ }
+
return &util.JSONResponse{
Code: 401,
JSON: Challenge{
Completed: completed,
Flows: flows,
Session: sessionID,
- Params: u.Params,
+ Params: paramsCopy,
},
}
}
@@ -233,7 +244,7 @@ func (u *UserInteractive) ResponseWithChallenge(sessionID string, response inter
// Verify returns an error/challenge response to send to the client, or nil if the user is authenticated.
// `bodyBytes` is the HTTP request body which must contain an `auth` key.
// Returns the login that was verified for additional checks if required.
-func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte) (*Login, *util.JSONResponse) {
+func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *api.Device) (*Login, *util.JSONResponse) {
// TODO: rate limit
// "A client should first make a request with no auth parameter. The homeserver returns an HTTP 401 response, with a JSON body"
@@ -284,3 +295,12 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte) (*Login,
// TODO: Check if there's more stages to go and return an error
return login, nil
}
+
+func GetAuthParams(params interface{}) interface{} {
+ v, ok := params.(config.AuthParams)
+ if ok {
+ p := v.GetParams()
+ return p
+ }
+ return nil
+}
diff --git a/clientapi/auth/user_interactive_test.go b/clientapi/auth/user_interactive_test.go
index bc1239910..3dbb9dabc 100644
--- a/clientapi/auth/user_interactive_test.go
+++ b/clientapi/auth/user_interactive_test.go
@@ -17,6 +17,11 @@ var (
serverName = gomatrixserverlib.ServerName("example.com")
// space separated localpart+password -> account
lookup = make(map[string]*api.Account)
+ device = &api.Device{
+ AccessToken: "flibble",
+ DisplayName: "My Device",
+ ID: "device_id_goes_here",
+ }
)
type fakeAccountDatabase struct {
@@ -55,7 +60,7 @@ func setup() *UserInteractive {
func TestUserInteractiveChallenge(t *testing.T) {
uia := setup()
// no auth key results in a challenge
- _, errRes := uia.Verify(ctx, []byte(`{}`))
+ _, errRes := uia.Verify(ctx, []byte(`{}`), device)
if errRes == nil {
t.Fatalf("Verify succeeded with {} but expected failure")
}
@@ -95,7 +100,7 @@ func TestUserInteractivePasswordLogin(t *testing.T) {
}`),
}
for _, tc := range testCases {
- _, errRes := uia.Verify(ctx, tc)
+ _, errRes := uia.Verify(ctx, tc, device)
if errRes != nil {
t.Errorf("Verify failed but expected success for request: %s - got %+v", string(tc), errRes)
}
@@ -176,7 +181,7 @@ func TestUserInteractivePasswordBadLogin(t *testing.T) {
},
}
for _, tc := range testCases {
- _, errRes := uia.Verify(ctx, tc.body)
+ _, errRes := uia.Verify(ctx, tc.body, device)
if errRes == nil {
t.Errorf("Verify succeeded but expected failure for request: %s", string(tc.body))
continue
diff --git a/clientapi/routing/deactivate.go b/clientapi/routing/deactivate.go
index 9f80dff61..f213db7f3 100644
--- a/clientapi/routing/deactivate.go
+++ b/clientapi/routing/deactivate.go
@@ -28,7 +28,7 @@ func Deactivate(
}
}
- login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes)
+ login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, deviceAPI)
if errRes != nil {
return *errRes
}
diff --git a/clientapi/routing/device.go b/clientapi/routing/device.go
index 84e11bc7a..e3a02661c 100644
--- a/clientapi/routing/device.go
+++ b/clientapi/routing/device.go
@@ -198,7 +198,7 @@ func DeleteDeviceById(
sessionID = s
}
- login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes)
+ login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
if errRes != nil {
switch data := errRes.JSON.(type) {
case auth.Challenge:
diff --git a/clientapi/routing/register_publickey.go b/clientapi/routing/register_publickey.go
index 46807f41e..c6cd5e30a 100644
--- a/clientapi/routing/register_publickey.go
+++ b/clientapi/routing/register_publickey.go
@@ -68,6 +68,13 @@ func handlePublicKeyRegistration(
return false, "", nil
}
+ if _, ok := sessions.sessions[authHandler.GetSession()]; !ok {
+ return false, "", &util.JSONResponse{
+ Code: http.StatusUnauthorized,
+ JSON: jsonerror.Unknown("the session ID is missing or unknown."),
+ }
+ }
+
isCompleted, jerr := authHandler.ValidateLoginResponse()
if jerr != nil {
return false, "", &util.JSONResponse{
diff --git a/dendrite-sample.monolith.yaml b/dendrite-sample.monolith.yaml
index eadb74a2a..b030b62eb 100644
--- a/dendrite-sample.monolith.yaml
+++ b/dendrite-sample.monolith.yaml
@@ -170,6 +170,16 @@ client_api:
# of whether registration is otherwise disabled.
registration_shared_secret: ""
+ # Disable password authentication.
+ password_authentication_disabled: false
+
+ # public key authentication
+ public_key_authentication:
+ ethereum:
+ enabled: false
+ version: 1
+ chain_ids: []
+
# Whether to require reCAPTCHA for registration. If you have enabled registration
# then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used
# for coordinated spam attacks.
diff --git a/internal/mapsutil/maps.go b/internal/mapsutil/maps.go
index 038ef53a8..b7eaba0dd 100644
--- a/internal/mapsutil/maps.go
+++ b/internal/mapsutil/maps.go
@@ -27,3 +27,18 @@ func MapsUnion(a map[string]interface{}, b map[string]interface{}) map[string]in
return c
}
+
+// Make a copy of the map
+func MapCopy(m map[string]interface{}) map[string]interface{} {
+ cp := make(map[string]interface{})
+ for k, v := range m {
+ vm, ok := v.(map[string]interface{})
+ if ok {
+ cp[k] = MapCopy(vm)
+ } else {
+ cp[k] = v
+ }
+ }
+
+ return cp
+}
diff --git a/setup/config/config.go b/setup/config/config.go
index 34edee1cc..150053fd2 100644
--- a/setup/config/config.go
+++ b/setup/config/config.go
@@ -302,7 +302,7 @@ func (config *Dendrite) Derive() error {
config.Derived.Registration.Params[authtypes.LoginTypeRecaptcha] = map[string]string{"public_key": config.ClientAPI.RecaptchaPublicKey}
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeRecaptcha}})
- } else {
+ } else if !config.ClientAPI.PasswordAuthenticationDisabled {
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeDummy}})
}
diff --git a/setup/config/config_clientapi.go b/setup/config/config_clientapi.go
index e8ca28196..c9e4418c8 100644
--- a/setup/config/config_clientapi.go
+++ b/setup/config/config_clientapi.go
@@ -3,8 +3,6 @@ package config
import (
"fmt"
"time"
-
- "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
)
type ClientAPI struct {
@@ -162,44 +160,3 @@ func (r *RateLimiting) Defaults() {
r.Threshold = 5
r.CooloffMS = 500
}
-
-type ethereumAuthParams struct {
- Version uint32 `json:"version"`
- ChainIDs []string `json:"chain_ids"`
-}
-
-type ethereumAuthConfig struct {
- Enabled bool `yaml:"enabled"`
- Version uint32 `yaml:"version"`
- ChainIDs []string `yaml:"chain_ids"`
-}
-
-type publicKeyAuthentication struct {
- Ethereum ethereumAuthConfig `yaml:"ethereum"`
-}
-
-func (pk *publicKeyAuthentication) Enabled() bool {
- return pk.Ethereum.Enabled
-}
-
-func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
- var flows []authtypes.Flow
- if pk.Ethereum.Enabled {
- flows = append(flows, authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypePublicKeyEthereum}})
- }
-
- return flows
-}
-
-func (pk *publicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
- params := make(map[string]interface{})
- if pk.Ethereum.Enabled {
- p := ethereumAuthParams{
- Version: pk.Ethereum.Version,
- ChainIDs: pk.Ethereum.ChainIDs,
- }
- params[authtypes.LoginTypePublicKeyEthereum] = p
- }
-
- return params
-}
diff --git a/setup/config/config_publickey.go b/setup/config/config_publickey.go
new file mode 100644
index 000000000..9820a5969
--- /dev/null
+++ b/setup/config/config_publickey.go
@@ -0,0 +1,83 @@
+package config
+
+import (
+ "math/rand"
+ "time"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+)
+
+var nonceLength = 32
+
+type AuthParams interface {
+ GetParams() interface{}
+ GetNonce() string
+}
+
+type EthereumAuthParams struct {
+ Version uint `json:"version"`
+ ChainIDs []int `json:"chain_ids"`
+ Nonce string `json:"nonce"`
+}
+
+func (p EthereumAuthParams) GetParams() interface{} {
+ copyP := p
+ copyP.ChainIDs = make([]int, len(p.ChainIDs))
+ copy(copyP.ChainIDs, p.ChainIDs)
+ copyP.Nonce = newNonce(nonceLength)
+ return copyP
+}
+
+func (p EthereumAuthParams) GetNonce() string {
+ return p.Nonce
+}
+
+type ethereumAuthConfig struct {
+ Enabled bool `yaml:"enabled"`
+ Version uint `yaml:"version"`
+ ChainIDs []int `yaml:"chain_ids"`
+}
+
+type publicKeyAuthentication struct {
+ Ethereum ethereumAuthConfig `yaml:"ethereum"`
+}
+
+func (pk *publicKeyAuthentication) Enabled() bool {
+ return pk.Ethereum.Enabled
+}
+
+func (pk *publicKeyAuthentication) GetPublicKeyRegistrationFlows() []authtypes.Flow {
+ var flows []authtypes.Flow
+ if pk.Ethereum.Enabled {
+ flows = append(flows, authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypePublicKeyEthereum}})
+ }
+
+ return flows
+}
+
+func (pk *publicKeyAuthentication) GetPublicKeyRegistrationParams() map[string]interface{} {
+ params := make(map[string]interface{})
+ if pk.Ethereum.Enabled {
+ p := EthereumAuthParams{
+ Version: pk.Ethereum.Version,
+ ChainIDs: pk.Ethereum.ChainIDs,
+ Nonce: "",
+ }
+ params[authtypes.LoginTypePublicKeyEthereum] = p
+ }
+
+ return params
+}
+
+const lettersAndNumbers = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+func newNonce(n int) string {
+ nonce := make([]byte, n)
+ rand.Seed(time.Now().UnixNano())
+
+ for i := range nonce {
+ nonce[i] = lettersAndNumbers[rand.Int63()%int64(len(lettersAndNumbers))]
+ }
+
+ return string(nonce)
+}