pull latest from dendrite-fork subtree

This commit is contained in:
Tak Wai Wong 2022-06-09 13:29:33 -07:00 committed by Tak Wai Wong
parent 88bbaed247
commit 34342934cc
No known key found for this signature in database
GPG key ID: 222E4AF2AA1F467D
12 changed files with 171 additions and 195 deletions

View file

@ -16,20 +16,14 @@ package auth
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"regexp"
"strings" "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/jsonerror"
"github.com/matrix-org/dendrite/clientapi/userutil" "github.com/matrix-org/dendrite/clientapi/userutil"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/tidwall/gjson" "github.com/spruceid/siwe-go"
) )
type LoginPublicKeyEthereum struct { type LoginPublicKeyEthereum struct {
@ -39,28 +33,11 @@ type LoginPublicKeyEthereum struct {
Session string `json:"session"` Session string `json:"session"`
Message string `json:"message"` Message string `json:"message"`
Signature string `json:"signature"` Signature string `json:"signature"`
HashFields publicKeyEthereumHashFields `json:"hashFields"`
HashFieldsRaw string // Raw base64 encoded string of MessageFields for hash verification
userAPI userapi.ClientUserAPI userAPI userapi.ClientUserAPI
config *config.ClientAPI 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( func CreatePublicKeyEthereumHandler(
reqBytes []byte, reqBytes []byte,
userAPI userapi.ClientUserAPI, userAPI userapi.ClientUserAPI,
@ -71,15 +48,8 @@ func CreatePublicKeyEthereumHandler(
return nil, jsonerror.BadJSON("auth") return nil, jsonerror.BadJSON("auth")
} }
hashFields := gjson.GetBytes(reqBytes, "hashFields")
if !hashFields.Exists() {
return nil, jsonerror.BadJSON("auth.hashFields")
}
pk.config = config pk.config = config
pk.userAPI = userAPI pk.userAPI = userAPI
// Save raw bytes for hash verification later.
pk.HashFieldsRaw = hashFields.Raw
// Case-insensitive // Case-insensitive
pk.Address = strings.ToLower(pk.Address) 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) { func (pk LoginPublicKeyEthereum) ValidateLoginResponse() (bool, *jsonerror.MatrixError) {
// Check signature to verify message was not tempered // Parse the message to extract all the fields.
isVerified := verifySignature(pk.Address, []byte(pk.Message), pk.Signature) message, err := siwe.ParseMessage(pk.Message)
if !isVerified {
return false, jsonerror.InvalidSignature("")
}
// Extract the required message fields for validation
requiredFields, err := extractRequiredMessageFields(pk.Message)
if err != nil { 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. // Check signature to verify message was not tempered
if !verifyHash(pk.HashFieldsRaw, requiredFields.Hash) { _, err = message.Verify(pk.Signature, (*string)(&pk.config.Matrix.ServerName), nil, nil)
return false, jsonerror.Forbidden("error verifying message hash") if err != nil {
} return false, jsonerror.InvalidSignature(err.Error())
// 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")
} }
// Error if the chainId is not supported by the server. // 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") return false, jsonerror.Forbidden("chainId")
} }
@ -169,75 +118,7 @@ func (pk LoginPublicKeyEthereum) CreateLogin() *Login {
return &login return &login
} }
// The required fields in the signed message are: func contains(list []int, element int) bool {
// 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<address>0x\w+)\n`)
var regexpHash = regexp.MustCompile(`\nHash: (?P<hash>.*)\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 {
for _, i := range list { for _, i := range list {
if i == element { if i == element {
return true return true

View file

@ -62,7 +62,6 @@ func TestLoginFromJSONReader(t *testing.T) {
}, },
} }
userInteractive := UserInteractive{ userInteractive := UserInteractive{
Completed: []string{},
Flows: []userInteractiveFlow{}, Flows: []userInteractiveFlow{},
Types: make(map[string]Type), Types: make(map[string]Type),
Sessions: make(map[string][]string), Sessions: make(map[string][]string),
@ -148,7 +147,6 @@ func TestBadLoginFromJSONReader(t *testing.T) {
}, },
} }
userInteractive := UserInteractive{ userInteractive := UserInteractive{
Completed: []string{},
Flows: []userInteractiveFlow{}, Flows: []userInteractiveFlow{},
Types: make(map[string]Type), Types: make(map[string]Type),
Sessions: make(map[string][]string), Sessions: make(map[string][]string),

View file

@ -21,6 +21,7 @@ import (
"sync" "sync"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "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/setup/config"
"github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/util" "github.com/matrix-org/util"
@ -177,17 +178,27 @@ type Challenge struct {
// Challenge returns an HTTP 401 with the supported flows for authenticating // Challenge returns an HTTP 401 with the supported flows for authenticating
func (u *UserInteractive) challenge(sessionID string) *util.JSONResponse { func (u *UserInteractive) challenge(sessionID string) *util.JSONResponse {
u.RLock() u.RLock()
paramsCopy := mapsutil.MapCopy(u.Params)
completed := u.Sessions[sessionID] completed := u.Sessions[sessionID]
flows := u.Flows flows := u.Flows
u.RUnlock() 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{ return &util.JSONResponse{
Code: 401, Code: 401,
JSON: Challenge{ JSON: Challenge{
Completed: completed, Completed: completed,
Flows: flows, Flows: flows,
Session: sessionID, 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. // 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. // `bodyBytes` is the HTTP request body which must contain an `auth` key.
// Returns the login that was verified for additional checks if required. // 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 // 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" // "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 // TODO: Check if there's more stages to go and return an error
return login, nil return login, nil
} }
func GetAuthParams(params interface{}) interface{} {
v, ok := params.(config.AuthParams)
if ok {
p := v.GetParams()
return p
}
return nil
}

View file

@ -17,6 +17,11 @@ var (
serverName = gomatrixserverlib.ServerName("example.com") serverName = gomatrixserverlib.ServerName("example.com")
// space separated localpart+password -> account // space separated localpart+password -> account
lookup = make(map[string]*api.Account) lookup = make(map[string]*api.Account)
device = &api.Device{
AccessToken: "flibble",
DisplayName: "My Device",
ID: "device_id_goes_here",
}
) )
type fakeAccountDatabase struct { type fakeAccountDatabase struct {
@ -55,7 +60,7 @@ func setup() *UserInteractive {
func TestUserInteractiveChallenge(t *testing.T) { func TestUserInteractiveChallenge(t *testing.T) {
uia := setup() uia := setup()
// no auth key results in a challenge // no auth key results in a challenge
_, errRes := uia.Verify(ctx, []byte(`{}`)) _, errRes := uia.Verify(ctx, []byte(`{}`), device)
if errRes == nil { if errRes == nil {
t.Fatalf("Verify succeeded with {} but expected failure") t.Fatalf("Verify succeeded with {} but expected failure")
} }
@ -95,7 +100,7 @@ func TestUserInteractivePasswordLogin(t *testing.T) {
}`), }`),
} }
for _, tc := range testCases { for _, tc := range testCases {
_, errRes := uia.Verify(ctx, tc) _, errRes := uia.Verify(ctx, tc, device)
if errRes != nil { if errRes != nil {
t.Errorf("Verify failed but expected success for request: %s - got %+v", string(tc), errRes) 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 { for _, tc := range testCases {
_, errRes := uia.Verify(ctx, tc.body) _, errRes := uia.Verify(ctx, tc.body, device)
if errRes == nil { if errRes == nil {
t.Errorf("Verify succeeded but expected failure for request: %s", string(tc.body)) t.Errorf("Verify succeeded but expected failure for request: %s", string(tc.body))
continue continue

View file

@ -28,7 +28,7 @@ func Deactivate(
} }
} }
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes) login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, deviceAPI)
if errRes != nil { if errRes != nil {
return *errRes return *errRes
} }

View file

@ -198,7 +198,7 @@ func DeleteDeviceById(
sessionID = s sessionID = s
} }
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes) login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
if errRes != nil { if errRes != nil {
switch data := errRes.JSON.(type) { switch data := errRes.JSON.(type) {
case auth.Challenge: case auth.Challenge:

View file

@ -68,6 +68,13 @@ func handlePublicKeyRegistration(
return false, "", nil 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() isCompleted, jerr := authHandler.ValidateLoginResponse()
if jerr != nil { if jerr != nil {
return false, "", &util.JSONResponse{ return false, "", &util.JSONResponse{

View file

@ -170,6 +170,16 @@ client_api:
# of whether registration is otherwise disabled. # of whether registration is otherwise disabled.
registration_shared_secret: "" 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 # 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 # then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used
# for coordinated spam attacks. # for coordinated spam attacks.

View file

@ -27,3 +27,18 @@ func MapsUnion(a map[string]interface{}, b map[string]interface{}) map[string]in
return c 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
}

View file

@ -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.Params[authtypes.LoginTypeRecaptcha] = map[string]string{"public_key": config.ClientAPI.RecaptchaPublicKey}
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeRecaptcha}}) authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeRecaptcha}})
} else { } else if !config.ClientAPI.PasswordAuthenticationDisabled {
config.Derived.Registration.Flows = append(config.Derived.Registration.Flows, config.Derived.Registration.Flows = append(config.Derived.Registration.Flows,
authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeDummy}}) authtypes.Flow{Stages: []authtypes.LoginType{authtypes.LoginTypeDummy}})
} }

View file

@ -3,8 +3,6 @@ package config
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
) )
type ClientAPI struct { type ClientAPI struct {
@ -162,44 +160,3 @@ func (r *RateLimiting) Defaults() {
r.Threshold = 5 r.Threshold = 5
r.CooloffMS = 500 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
}

View file

@ -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)
}