From 9e42bb2f28f32ffff45e9baf9a2c2f34b79263b6 Mon Sep 17 00:00:00 2001 From: Bewinxed Date: Fri, 17 Jan 2025 21:35:22 +0300 Subject: [PATCH] feat: implement EIP-4361 support with SIWS message handling and verification --- example.env | 11 ++ external_eip4361_siws_example.go | 75 ++++++++ go.mod | 19 +- go.sum | 50 ++++++ internal/api/external_eip4361_siws_test.go | 86 +++++++++ internal/api/helpers.go | 2 + internal/api/provider/eip4361.go | 192 +++++++++++++++++++++ internal/api/token.go | 72 ++++++++ internal/api/web3.go | 8 + internal/conf/configuration.go | 62 +++++++ internal/models/factor.go | 6 + internal/models/web3.go | 4 + internal/reloader/testdata/50_example.env | 13 +- internal/utilities/siws/go.mod.bak | 5 + internal/utilities/siws/go.sum.bak | 35 ++++ internal/utilities/siws/helpers.go | 72 ++++++++ internal/utilities/siws/message.go | 40 +++++ internal/utilities/siws/parser.go | 99 +++++++++++ internal/utilities/siws/siws_test.go | 107 ++++++++++++ internal/utilities/siws/types.go | 28 +++ internal/utilities/siws/verify.go | 58 +++++++ internal/utilities/web3/ethereum/verify.go | 57 ++++++ 22 files changed, 1092 insertions(+), 9 deletions(-) create mode 100644 external_eip4361_siws_example.go create mode 100644 internal/api/external_eip4361_siws_test.go create mode 100644 internal/api/provider/eip4361.go create mode 100644 internal/api/web3.go create mode 100644 internal/models/web3.go create mode 100644 internal/utilities/siws/go.mod.bak create mode 100644 internal/utilities/siws/go.sum.bak create mode 100644 internal/utilities/siws/helpers.go create mode 100644 internal/utilities/siws/message.go create mode 100644 internal/utilities/siws/parser.go create mode 100644 internal/utilities/siws/siws_test.go create mode 100644 internal/utilities/siws/types.go create mode 100644 internal/utilities/siws/verify.go create mode 100644 internal/utilities/web3/ethereum/verify.go diff --git a/example.env b/example.env index e645c96e9c..2708710395 100644 --- a/example.env +++ b/example.env @@ -168,6 +168,17 @@ GOTRUE_EXTERNAL_ZOOM_CLIENT_ID="" GOTRUE_EXTERNAL_ZOOM_SECRET="" GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI="http://localhost:9999/callback" +# EIP-4361 OAuth config +GOTRUE_EXTERNAL_EIP4361_ENABLED="true" +GOTRUE_EXTERNAL_EIP4361_STATEMENT="Sign this message to verify your identity" +GOTRUE_EXTERNAL_EIP4361_VERSION="1" +GOTRUE_EXTERNAL_EIP4361_TIMEOUT="300s" +GOTRUE_EXTERNAL_EIP4361_DOMAIN="localhost:9999" + +# Supported Chains Configuration +GOTRUE_EXTERNAL_EIP4361_SUPPORTED_CHAINS="ethereum:1,ethereum:137,solana:mainnet,solana:devnet" +GOTRUE_EXTERNAL_EIP4361_DEFAULT_CHAIN="ethereum:1" + # Anonymous auth config GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED="false" diff --git a/external_eip4361_siws_example.go b/external_eip4361_siws_example.go new file mode 100644 index 0000000000..9d9325b573 --- /dev/null +++ b/external_eip4361_siws_example.go @@ -0,0 +1,75 @@ +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "time" + + "github.com/btcsuite/btcutil/base58" + siws "github.com/supabase/auth/internal/utilities/siws" +) + +func LogSIWSExample() { + // Configuration + domain := "localhost:9999" + statement := "Sign in with your Solana account" + version := "1" + chain := "solana:mainnet" + + // Generate keys + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + fmt.Println("Error generating keys:", err) + return + } + pubKeyBase58 := base58.Encode(pubKey) + + // Generate nonce + nonce, err := siws.GenerateNonce() + if err != nil { + fmt.Println("Error generating nonce:", err) + return + } + + // Create SIWS message + msg := siws.SIWSMessage{ + Domain: domain, + Address: pubKeyBase58, + Statement: statement, + URI: "https://example.com", + Version: version, + Nonce: nonce, + IssuedAt: time.Now().UTC(), + } + + rawMessage := siws.ConstructMessage(msg) + + // Sign the message + signature := ed25519.Sign(privKey, []byte(rawMessage)) + signatureBase64 := base64.StdEncoding.EncodeToString(signature) + + // Generate JSON payload + payload := map[string]string{ + "grant_type": "eip4361", + "message": rawMessage, + "signature": signatureBase64, + "address": pubKeyBase58, + "chain": chain, + } + + payloadJSON, err := json.Marshal(payload) + if err != nil { + fmt.Println("Error generating payload JSON:", err) + return + } + + // Print JavaScript fetch code + fmt.Println(string(payloadJSON)) +} + +// func main() { +// LogSIWSExample() +// } diff --git a/go.mod b/go.mod index a99b2b688c..232cc1a81b 100644 --- a/go.mod +++ b/go.mod @@ -34,14 +34,17 @@ require ( ) require ( - github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/btcsuite/btcutil v1.0.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/ethereum/go-ethereum v1.14.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-webauthn/x v0.1.12 // indirect github.com/gobuffalo/nulls v0.4.2 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/google/go-tpm v0.9.1 // indirect + github.com/holiman/uint256 v1.3.1 // indirect github.com/jackc/pgx/v4 v4.18.2 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -98,10 +101,10 @@ require ( github.com/beevik/etree v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/crewjam/httperr v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -130,7 +133,7 @@ require ( github.com/luna-duclos/instrumentedsql v1.1.3 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -146,15 +149,15 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb - golang.org/x/net v0.23.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/net v0.24.0 // indirect golang.org/x/sync v0.10.0 golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect + golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 827144acd1..217cd60e38 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/XSAM/otelsql v0.26.0 h1:UhAGVBD34Ctbh2aYcm/JAdL+6T6ybrP+YMWYkHqCdmo= github.com/XSAM/otelsql v0.26.0/go.mod h1:5ciw61eMSh+RtTPN8spvPEPLJpAErZw8mFFPNfYiaxA= github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40 h1:uz4N2yHL4MF8vZX+36n+tcxeUf8D/gL4aJkyouhDw4A= github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40/go.mod h1:dytw+5qs+pdi61fO/S4OmXR7AuEq/HvNCuG03KxQHT4= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= @@ -22,6 +23,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bloom/v3 v3.6.0 h1:dTU0OVLJSoOhz9m68FTXMFfA39nR8U/nTCs1zb26mOI= github.com/bits-and-blooms/bloom/v3 v3.6.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -29,10 +32,22 @@ github.com/bombsimon/logrusr/v3 v3.0.0 h1:tcAoLfuAhKP9npBxWzSdpsvKPQt1XV02nSf2lZ github.com/bombsimon/logrusr/v3 v3.0.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o= @@ -46,6 +61,7 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -55,12 +71,17 @@ github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/didip/tollbooth/v5 v5.1.1 h1:QpKFg56jsbNuQ6FFj++Z1gn2fbBsvAc1ZPLUaDOYW5k= github.com/didip/tollbooth/v5 v5.1.1/go.mod h1:d9rzwOULswrD3YIrAQmP3bfjxab32Df4IaO6+D25l9g= +github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4= +github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -123,6 +144,7 @@ github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQg github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -142,6 +164,9 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0Q github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= @@ -198,18 +223,21 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -255,6 +283,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -269,6 +299,9 @@ github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUll github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/patrickmn/go-cache v0.0.0-20170418232947-7ac151875ffb/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= @@ -404,11 +437,13 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -420,6 +455,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -429,6 +466,7 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20161007143504-f4b625ec9b21/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -443,8 +481,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -453,6 +494,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -474,6 +516,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= @@ -500,6 +543,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -532,6 +577,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -540,11 +587,14 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/api/external_eip4361_siws_test.go b/internal/api/external_eip4361_siws_test.go new file mode 100644 index 0000000000..4fbdded5d2 --- /dev/null +++ b/internal/api/external_eip4361_siws_test.go @@ -0,0 +1,86 @@ +package api + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/supabase/auth/internal/conf" + siws "github.com/supabase/auth/internal/utilities/siws" +) + +const ( + siwsValidUser string = `{"address":"12345abcde","chain":"solana:mainnet"}` + siwsWrongChain string = `{"address":"12345abcde","chain":"ethereum:1"}` + siwsInvalidUser string = `{"address":"","chain":"solana:mainnet"}` +) + +func SIWSTestSignupSetup(ts *ExternalTestSuite) { + ts.Config.External.EIP4361 = conf.EIP4361Configuration{ + Enabled: true, + Domain: "test.example.com", + Statement: "Sign in with your Solana account", + Version: "1", + Timeout: 5 * time.Minute, + SupportedChains: "solana:mainnet", + DefaultChain: "solana:mainnet", + } +} + +func (ts *ExternalTestSuite) TestSignupExternalSIWS() { + SIWSTestSignupSetup(ts) + ts.Config.DisableSignup = false + + // Generate test keys for Solana + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + ts.Require().NoError(err) + pubKeyBase58 := base58.Encode(pubKey) + + nonce, err := siws.GenerateNonce() + ts.Require().NoError(err) + + // Create test message + msg := siws.SIWSMessage{ + Domain: ts.Config.External.EIP4361.Domain, + Address: pubKeyBase58, + Statement: ts.Config.External.EIP4361.Statement, + URI: "https://example.com", + Version: ts.Config.External.EIP4361.Version, + Nonce: nonce, + IssuedAt: time.Now().UTC(), + } + + rawMessage := siws.ConstructMessage(msg) + signature := ed25519.Sign(privKey, []byte(rawMessage)) + signatureBase64 := base64.StdEncoding.EncodeToString(signature) + + formData := url.Values{} + formData.Set("grant_type", "eip4361") + formData.Set("message", rawMessage) + formData.Set("signature", signatureBase64) + formData.Set("address", pubKeyBase58) + formData.Set("chain", "solana:mainnet") + + req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(formData.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + + ts.Require().Equal(http.StatusOK, w.Code) + + var token AccessTokenResponse + ts.Require().NoError(json.NewDecoder(w.Body).Decode(&token)) + + ts.Require().NotEmpty(token.Token) + ts.Require().NotEmpty(token.RefreshToken) + ts.Require().Equal("bearer", token.TokenType) + ts.Require().NotNil(token.User) +} diff --git a/internal/api/helpers.go b/internal/api/helpers.go index 8a9f3267ed..2318013792 100644 --- a/internal/api/helpers.go +++ b/internal/api/helpers.go @@ -74,6 +74,7 @@ type RequestParams interface { SignupParams | SingleSignOnParams | SmsParams | + Web3GrantParams | UserUpdateParams | VerifyFactorParams | VerifyParams | @@ -81,6 +82,7 @@ type RequestParams interface { adminUserDeleteParams | security.GotrueRequest | ChallengeFactorParams | + struct { Email string `json:"email"` Phone string `json:"phone"` diff --git a/internal/api/provider/eip4361.go b/internal/api/provider/eip4361.go new file mode 100644 index 0000000000..7766a6db06 --- /dev/null +++ b/internal/api/provider/eip4361.go @@ -0,0 +1,192 @@ +package provider + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/supabase/auth/internal/conf" + "github.com/supabase/auth/internal/utilities/siws" + "github.com/supabase/auth/internal/utilities/web3/ethereum" + "golang.org/x/oauth2" +) + +const ( + BlockchainEthereum = "ethereum" + BlockchainSolana = "solana" +) + +// EIP4361Provider implements Web3 authentication following EIP-4361 spec +type EIP4361Provider struct { + config conf.EIP4361Configuration + chains map[string]conf.BlockchainConfig + defaultChain string +} + +type SignedMessage struct { + Message string `json:"message"` + Signature string `json:"signature"` + Address string `json:"address"` + Chain string `json:"chain"` +} + +func NewEIP4361Provider(ctx context.Context, config conf.EIP4361Configuration) (*EIP4361Provider, error) { + if !config.Enabled { + return nil, errors.New("EIP4361 provider is not enabled") + } + + // Parse chains + chains, err := config.ParseSupportedChains() + if err != nil { + return nil, err + } + + // Validate default chain + if config.DefaultChain != "" { + if _, ok := chains[config.DefaultChain]; !ok { + return nil, fmt.Errorf("default chain %s not in supported chains", config.DefaultChain) + } + } + + return &EIP4361Provider{ + config: config, + chains: chains, + defaultChain: config.DefaultChain, + }, nil +} + +func (p *EIP4361Provider) AuthCodeURL(state string, args ...oauth2.AuthCodeOption) string { + return "" // Web3 auth doesn't use OAuth flow +} + +func (p *EIP4361Provider) GetOAuthToken(code string) (*oauth2.Token, error) { + return nil, errors.New("GetOAuthToken not implemented for EIP4361") +} + +func (p *EIP4361Provider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + return nil, errors.New("GetUserData not implemented for EIP4361") +} + +// VerifySignedMessage verifies a signed Web3 message based on the blockchain +func (p *EIP4361Provider) VerifySignedMessage(msg *SignedMessage) (*UserProvidedData, error) { + chain, ok := p.chains[msg.Chain] + if !ok { + return nil, fmt.Errorf("unsupported blockchain: %s", msg.Chain) + } + + var err error + switch chain.NetworkName { + case BlockchainEthereum: + err = p.verifyEthereumSignature(msg) + case BlockchainSolana: + err = p.verifySolanaSignature(msg) + default: + return nil, fmt.Errorf("signature verification not implemented for %s", chain.NetworkName) + } + + if err != nil { + return nil, fmt.Errorf("signature verification failed: %w", err) + } + + // Construct the provider_id as chain:address to make it unique + providerId := fmt.Sprintf("%s:%s", msg.Chain, msg.Address) + + return &UserProvidedData{ + Metadata: &Claims{ + CustomClaims: map[string]interface{}{ + "address": msg.Address, + "chain": msg.Chain, + "role": "authenticated", + }, + Subject: providerId, // This becomes the provider_id in the identity + }, + Emails: []Email{}, + }, nil +} + +func (p *EIP4361Provider) verifyEthereumSignature(msg *SignedMessage) error { + return ethereum.VerifySignature(msg.Message, msg.Signature, msg.Address) +} + +func (p *EIP4361Provider) verifySolanaSignature(msg *SignedMessage) error { + parsedMessage, err := siws.ParseSIWSMessage(msg.Message) + if err != nil { + return fmt.Errorf("failed to parse SIWS message: %w", err) + } + + // Decode base64 signature into bytes + sigBytes, err := base64.StdEncoding.DecodeString(msg.Signature) + if err != nil { + return fmt.Errorf("invalid signature encoding: %w", err) + } + + params := siws.SIWSVerificationParams{ + ExpectedDomain: p.config.Domain, + CheckTime: true, + TimeDuration: p.config.Timeout, + } + + if err := siws.VerifySIWS(msg.Message, sigBytes, parsedMessage, params); err != nil { + return fmt.Errorf("SIWS verification failed: %w", err) + } + + return nil +} + +func (p *EIP4361Provider) GenerateSignMessage(address string, chain string, uri string) (string, error) { + if chain == "" { + chain = p.defaultChain + } + + chainCfg, ok := p.chains[chain] + if !ok { + return "", fmt.Errorf("unsupported chain: %s", chain) + } + + // Generate nonce for message uniqueness + nonce, err := siws.GenerateNonce() + if err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + + now := time.Now().UTC() + + switch chainCfg.NetworkName { + case BlockchainSolana: + msg := siws.SIWSMessage{ + Domain: p.config.Domain, + Address: address, + Statement: p.config.Statement, + URI: uri, + Version: p.config.Version, + Nonce: nonce, + IssuedAt: now, + } + return siws.ConstructMessage(msg), nil + + case BlockchainEthereum: + return fmt.Sprintf(`%s wants you to sign in with your %s account: +%s + +URI: %s +Version: %s +Chain ID: %s +Nonce: %d +Issued At: %s +Expiration Time: %s`, + p.config.Domain, + chainCfg.NetworkName, + address, + uri, + p.config.Version, + chainCfg.ChainID, + now.UnixNano(), + now.Format(time.RFC3339), + now.Add(p.config.Timeout).Format(time.RFC3339)), nil + + default: + return "", fmt.Errorf("message generation not implemented for %s", chainCfg.NetworkName) + } +} diff --git a/internal/api/token.go b/internal/api/token.go index cc945f2e13..5dee090334 100644 --- a/internal/api/token.go +++ b/internal/api/token.go @@ -13,6 +13,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/xeipuuv/gojsonschema" + "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/hooks" "github.com/supabase/auth/internal/metering" "github.com/supabase/auth/internal/models" @@ -79,6 +80,7 @@ const InvalidLoginMessage = "Invalid login credentials" func (a *API) Token(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() grantType := r.FormValue("grant_type") + switch grantType { case "password": return a.ResourceOwnerPasswordGrant(ctx, w, r) @@ -88,6 +90,8 @@ func (a *API) Token(w http.ResponseWriter, r *http.Request) error { return a.IdTokenGrant(ctx, w, r) case "pkce": return a.PKCE(ctx, w, r) + case "eip4361": + return a.EIP4361Grant(ctx, w, r) default: return badRequestError(ErrorCodeInvalidCredentials, "unsupported_grant_type") } @@ -307,6 +311,74 @@ func (a *API) PKCE(ctx context.Context, w http.ResponseWriter, r *http.Request) return sendJSON(w, http.StatusOK, token) } +func (a *API) EIP4361Grant(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + db := a.db.WithContext(ctx) + + params := &Web3GrantParams{} + if err := retrieveRequestParams(r, params); err != nil { + return err + } + + web3Provider, err := provider.NewEIP4361Provider(ctx, a.config.External.EIP4361) + if err != nil { + return err + } + + // Convert params to SignedMessage + msg := &provider.SignedMessage{ + Message: params.Message, + Signature: params.Signature, + Address: params.Address, + Chain: params.Chain, + } + + userData, err := web3Provider.VerifySignedMessage(msg) + + if err != nil { + return oauthError("invalid_grant", "Signature verification failed").WithInternalError(err) + } + + var token *AccessTokenResponse + var grantParams models.GrantParams + grantParams.FillGrantParams(r) + + err = db.Transaction(func(tx *storage.Connection) error { + user, terr := a.createAccountFromExternalIdentity(tx, r, userData, "eip4361") + if terr != nil { + return terr + } + + // Log the auth attempt + if terr := models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", map[string]interface{}{ + "provider": "eip4361", + "chain": msg.Chain, + "address": msg.Address, + }); terr != nil { + return terr + } + + token, terr = a.issueRefreshToken(r, tx, user, models.Web3, grantParams) + if terr != nil { + return terr + } + + return nil + }) + + if err != nil { + switch err.(type) { + case *storage.CommitWithError: + return err + case *HTTPError: + return err + default: + return oauthError("server_error", "Internal Server Error").WithInternalError(err) + } + } + + return sendJSON(w, http.StatusOK, token) +} + func (a *API) generateAccessToken(r *http.Request, tx *storage.Connection, user *models.User, sessionId *uuid.UUID, authenticationMethod models.AuthenticationMethod) (string, int64, error) { config := a.config if sessionId == nil { diff --git a/internal/api/web3.go b/internal/api/web3.go new file mode 100644 index 0000000000..958d86693d --- /dev/null +++ b/internal/api/web3.go @@ -0,0 +1,8 @@ +package api + +type Web3GrantParams struct { + Message string `json:"message"` + Signature string `json:"signature"` + Address string `json:"address"` + Chain string `json:"chain"` +} diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index c4d910d991..e19bf92570 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "net/url" "os" "path/filepath" @@ -19,6 +20,7 @@ import ( "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" "github.com/lestrrat-go/jwx/v2/jwk" + siws "github.com/supabase/auth/internal/utilities/siws" "gopkg.in/gomail.v2" ) @@ -339,6 +341,66 @@ type ProviderConfiguration struct { RedirectURL string `json:"redirect_url"` AllowedIdTokenIssuers []string `json:"allowed_id_token_issuers" split_words:"true"` FlowStateExpiryDuration time.Duration `json:"flow_state_expiry_duration" split_words:"true"` + EIP4361 EIP4361Configuration `json:"eip4361" envconfig:"EIP4361"` +} + +type EIP4361Configuration struct { + Enabled bool `json:"enabled" default:"false" split_words:"true"` + Domain string `json:"domain" required:"true" split_words:"true"` + Statement string `json:"statement" split_words:"true"` + Version string `json:"version" default:"1" split_words:"true"` + Timeout time.Duration `json:"timeout" default:"300s" split_words:"true"` + + // Comma-separated list of supported chains (e.g. "ethereum:1,ethereum:137,solana:mainnet") + SupportedChains string `json:"supported_chains" split_words:"true"` + DefaultChain string `json:"default_chain" split_words:"true"` +} + +type BlockchainConfig struct { + ChainID string + NetworkName string +} + +// ParseSupportedChains processes and validates the SupportedChains string. +func (c *EIP4361Configuration) ParseSupportedChains() (map[string]BlockchainConfig, error) { + chainMap := make(map[string]BlockchainConfig) + + // Split comma-separated chains + chains := strings.Split(c.SupportedChains, ",") + for _, chain := range chains { + chain = strings.TrimSpace(chain) + parts := strings.Split(chain, ":") + + // Ensure proper : format + if len(parts) != 2 { + return nil, fmt.Errorf("invalid chain format: %s, expected :", chain) + } + + network := strings.ToLower(parts[0]) + chainID := parts[1] + + // Validate network type + switch network { + case "ethereum": + if _, ok := new(big.Int).SetString(chainID, 10); !ok { + return nil, fmt.Errorf("invalid Ethereum chain ID: %s", chainID) + } + case "solana": + if !siws.IsValidSolanaNetwork(chainID) { + return nil, fmt.Errorf("invalid Solana network: %s", chainID) + } + default: + return nil, fmt.Errorf("unsupported network: %s", network) + } + + // Add to chain map + chainMap[chain] = BlockchainConfig{ + NetworkName: network, + ChainID: chainID, + } + } + + return chainMap, nil } type SMTPConfiguration struct { diff --git a/internal/models/factor.go b/internal/models/factor.go index a88874d734..72309a8557 100644 --- a/internal/models/factor.go +++ b/internal/models/factor.go @@ -54,6 +54,7 @@ const ( EmailChange TokenRefresh Anonymous + Web3 ) func (authMethod AuthenticationMethod) String() string { @@ -86,6 +87,8 @@ func (authMethod AuthenticationMethod) String() string { return "mfa/phone" case MFAWebAuthn: return "mfa/webauthn" + case Web3: + return "web3" } return "" } @@ -121,6 +124,9 @@ func ParseAuthenticationMethod(authMethod string) (AuthenticationMethod, error) return MFAPhone, nil case "mfa/webauthn": return MFAWebAuthn, nil + case "web3": + return Web3, nil + } return 0, fmt.Errorf("unsupported authentication method %q", authMethod) } diff --git a/internal/models/web3.go b/internal/models/web3.go new file mode 100644 index 0000000000..6fc274e16d --- /dev/null +++ b/internal/models/web3.go @@ -0,0 +1,4 @@ +package models + +const Web3Provider = "web3" +const Web3Grant = "web3" diff --git a/internal/reloader/testdata/50_example.env b/internal/reloader/testdata/50_example.env index 1002d8be17..8f4d4949c9 100644 --- a/internal/reloader/testdata/50_example.env +++ b/internal/reloader/testdata/50_example.env @@ -10,7 +10,7 @@ GOTRUE_JWT_ADMIN_ROLES="supabase_admin,service_role" # Database & API connection details GOTRUE_DB_DRIVER="postgres" DB_NAMESPACE="auth" -DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres" +DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5433/postgres" API_EXTERNAL_URL="http://localhost:9999" GOTRUE_API_HOST="localhost" PORT="9999" @@ -168,6 +168,17 @@ GOTRUE_EXTERNAL_ZOOM_CLIENT_ID="" GOTRUE_EXTERNAL_ZOOM_SECRET="" GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI="http://localhost:9999/callback" +# EIP-4361 OAuth config +GOTRUE_EXTERNAL_EIP4361_ENABLED="true" +GOTRUE_EXTERNAL_EIP4361_STATEMENT="Sign this message to verify your identity" +GOTRUE_EXTERNAL_EIP4361_VERSION="1" +GOTRUE_EXTERNAL_EIP4361_TIMEOUT="300s" +GOTRUE_EXTERNAL_EIP4361_DOMAIN="localhost:9999" + +# Supported Chains Configuration +GOTRUE_EXTERNAL_EIP4361_SUPPORTED_CHAINS="ethereum:1,ethereum:137,solana:mainnet,solana:devnet" +GOTRUE_EXTERNAL_EIP4361_DEFAULT_CHAIN="ethereum:1" + # Anonymous auth config GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED="false" diff --git a/internal/utilities/siws/go.mod.bak b/internal/utilities/siws/go.mod.bak new file mode 100644 index 0000000000..125bdd6632 --- /dev/null +++ b/internal/utilities/siws/go.mod.bak @@ -0,0 +1,5 @@ +module github.com/utilities/siwsgo + +go 1.20 + +require github.com/btcsuite/btcutil v1.0.2 diff --git a/internal/utilities/siws/go.sum.bak b/internal/utilities/siws/go.sum.bak new file mode 100644 index 0000000000..eadd72e564 --- /dev/null +++ b/internal/utilities/siws/go.sum.bak @@ -0,0 +1,35 @@ +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/utilities/siws/helpers.go b/internal/utilities/siws/helpers.go new file mode 100644 index 0000000000..7a3b8b2798 --- /dev/null +++ b/internal/utilities/siws/helpers.go @@ -0,0 +1,72 @@ +package siws + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "net/url" + "strings" +) + +// GenerateNonce creates a random 16-byte nonce, returning a hex-encoded string. +func GenerateNonce() (string, error) { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +// ValidateDomain checks if a domain is valid or not. This can be expanded with +// real domain validation logic. Here, we do a simple parse check. +func ValidateDomain(domain string) error { + u, err := url.Parse("https://" + domain) + if err != nil || u.Hostname() == "" { + return errors.New("invalid domain") + } + return nil +} + +// IsBase58PubKey checks if the input is a plausible base58 Solana public key. +// Typically Solana addresses are ~44 characters in base58. This is a naive check. +func IsBase58PubKey(address string) bool { + address = strings.TrimSpace(address) + if len(address) < 32 { + return false + } + // Optionally, you could decode with base58 and check for 32 bytes. + return true +} + +// Add these functions to your existing helpers.go +func IsValidSolanaNetwork(network string) bool { + validNetworks := map[string]bool{ + "mainnet": true, + "devnet": true, + "testnet": true, + } + return validNetworks[strings.ToLower(network)] +} + +// ValidateChainConfig ensures the Solana network configuration is valid +func ValidateChainConfig(chainStr string) error { + if chainStr == "" { + return errors.New("chain configuration cannot be empty") + } + + network := strings.TrimSpace(strings.ToLower(chainStr)) + if !IsValidSolanaNetwork(network) { + return fmt.Errorf("invalid Solana network: %s", network) + } + + return nil +} + +// Add these error types +var ( + ErrInvalidSolanaSignature = errors.New("invalid Solana signature") + ErrInvalidSolanaAddress = errors.New("invalid Solana address format") + ErrExpiredMessage = errors.New("SIWS message has expired") +) diff --git a/internal/utilities/siws/message.go b/internal/utilities/siws/message.go new file mode 100644 index 0000000000..faa50450d5 --- /dev/null +++ b/internal/utilities/siws/message.go @@ -0,0 +1,40 @@ +package siws + +import ( + "fmt" + "strings" + "time" +) + +// ConstructMessage creates the textual message to be signed, following +// an ABNF-like structure for "Sign in with Solana." +func ConstructMessage(msg SIWSMessage) string { + var sb strings.Builder + + // 1) Domain request line + sb.WriteString(fmt.Sprintf("%s wants you to sign in with your Solana account:\n", msg.Domain)) + + // 2) Address + sb.WriteString(fmt.Sprintf("%s\n", msg.Address)) + + // 3) Optional statement + if msg.Statement != "" { + sb.WriteString(fmt.Sprintf("\n%s\n", msg.Statement)) + } + + // 4) Additional metadata (URI, Version, Nonce, IssuedAt) + if msg.URI != "" { + sb.WriteString(fmt.Sprintf("URI: %s\n", msg.URI)) + } + if msg.Version != "" { + sb.WriteString(fmt.Sprintf("Version: %s\n", msg.Version)) + } + if msg.Nonce != "" { + sb.WriteString(fmt.Sprintf("Nonce: %s\n", msg.Nonce)) + } + if !msg.IssuedAt.IsZero() { + sb.WriteString(fmt.Sprintf("Issued At: %s\n", msg.IssuedAt.UTC().Format(time.RFC3339))) + } + + return sb.String() +} diff --git a/internal/utilities/siws/parser.go b/internal/utilities/siws/parser.go new file mode 100644 index 0000000000..7d727986f0 --- /dev/null +++ b/internal/utilities/siws/parser.go @@ -0,0 +1,99 @@ +package siws + +import ( + "errors" + "fmt" + "regexp" + "strings" + "time" +) + +// ParseSIWSMessage parses a raw SIWS message into an SIWSMessage struct, +// performing robust checks to ensure correct formatting. +func ParseSIWSMessage(raw string) (*SIWSMessage, error) { + lines := strings.Split(raw, "\n") + + // Remove empty lines at the end or accidental trailing newlines. + // Some wallets or frameworks may add them. + var cleaned []string + for _, line := range lines { + l := strings.TrimSpace(line) + if l != "" { + cleaned = append(cleaned, l) + } + } + if len(cleaned) < 2 { + return nil, errors.New("message is too short or improperly formatted") + } + + // 1) First line should match " wants you to sign in with your Solana account:" + // Use a regex to capture the domain. + domainRegex := regexp.MustCompile(`^([^ ]+)\s+wants you to sign in with your Solana account:$`) + matches := domainRegex.FindStringSubmatch(cleaned[0]) + if matches == nil || len(matches) < 2 { + return nil, errors.New("first line does not match expected format for domain request") + } + domain := matches[1] + + // 2) Second line is the base58-encoded public key + address := strings.TrimSpace(cleaned[1]) + if address == "" { + return nil, errors.New("missing address line") + } + + // The third line might be blank or might be the statement. We can handle that carefully. + statement := "" + lineIndex := 2 + if lineIndex < len(cleaned) { + // If the line is blank, skip it; otherwise, treat it as statement + if strings.HasPrefix(cleaned[lineIndex], "URI:") || + strings.HasPrefix(cleaned[lineIndex], "Version:") || + strings.HasPrefix(cleaned[lineIndex], "Nonce:") || + strings.HasPrefix(cleaned[lineIndex], "Issued At:") { + // No statement + } else { + // We assume this line is statement + statement = cleaned[lineIndex] + lineIndex++ + } + } + + var uri, version, nonce string + var issuedAt time.Time + + // 3) Parse optional lines in the form "URI: ...", "Version: ...", "Nonce: ...", "Issued At: ..." + for lineIndex < len(cleaned) { + line := cleaned[lineIndex] + switch { + case strings.HasPrefix(line, "URI: "): + uri = strings.TrimSpace(strings.TrimPrefix(line, "URI:")) + case strings.HasPrefix(line, "Version: "): + version = strings.TrimSpace(strings.TrimPrefix(line, "Version:")) + case strings.HasPrefix(line, "Nonce: "): + nonce = strings.TrimSpace(strings.TrimPrefix(line, "Nonce:")) + case strings.HasPrefix(line, "Issued At: "): + tsString := strings.TrimSpace(strings.TrimPrefix(line, "Issued At:")) + var err error + issuedAt, err = time.Parse(time.RFC3339, tsString) + if err != nil { + return nil, fmt.Errorf("failed to parse Issued At time: %w", err) + } + default: + return nil, fmt.Errorf("unrecognized line: %s", line) + } + lineIndex++ + } + + // Construct the final message struct + msg := &SIWSMessage{ + Domain: domain, + Address: address, + Statement: statement, + URI: uri, + Version: version, + Nonce: nonce, + IssuedAt: issuedAt, + } + + return msg, nil +} diff --git a/internal/utilities/siws/siws_test.go b/internal/utilities/siws/siws_test.go new file mode 100644 index 0000000000..35855e74c9 --- /dev/null +++ b/internal/utilities/siws/siws_test.go @@ -0,0 +1,107 @@ +package siws + +import ( + "crypto/ed25519" + "crypto/rand" + "testing" + "time" + + btcbase58 "github.com/btcsuite/btcutil/base58" +) + +func TestSIWSFlow(t *testing.T) { + // 1) Generate Ed25519 key pair + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("failed to generate ed25519 key: %v", err) + } + pubKeyBase58 := btcbase58.Encode(pubKey) + + // 2) Build the SIWS message text (as the user would see) + // - Typically you'd store the nonce in DB now + issuedAt := time.Now().UTC().Truncate(time.Second) + rawMessage := `example.com wants you to sign in with your Solana account: +` + pubKeyBase58 + ` + +This is a test statement + +URI: https://example.com +Version: 1 +Nonce: ABCDEF123456 +Issued At: ` + issuedAt.Format(time.RFC3339) + + // 3) Parse the message (robust approach) + msg, parseErr := ParseSIWSMessage(rawMessage) + if parseErr != nil { + t.Fatalf("failed to parse message: %v", parseErr) + } + + // 4) Sign the raw message + signature := ed25519.Sign(privKey, []byte(rawMessage)) + + // 5) Verify + params := SIWSVerificationParams{ + ExpectedDomain: "example.com", + CheckTime: true, + TimeDuration: 5 * time.Minute, + } + + if err := VerifySIWS(rawMessage, signature, msg, params); err != nil { + t.Fatalf("verification failed: %v", err) + } +} + +func TestBadDomain(t *testing.T) { + pubKey, privKey, _ := ed25519.GenerateKey(rand.Reader) + pubKeyBase58 := btcbase58.Encode(pubKey) + + rawMessage := `wrong-domain.com wants you to sign in with your Solana account: +` + pubKeyBase58 + ` + +URI: https://example.com +Version: 1 +Nonce: SOME_NONCE +Issued At: 2025-01-01T00:00:00Z` + + msg, _ := ParseSIWSMessage(rawMessage) + signature := ed25519.Sign(privKey, []byte(rawMessage)) + + params := SIWSVerificationParams{ + ExpectedDomain: "example.com", + CheckTime: false, + } + + err := VerifySIWS(rawMessage, signature, msg, params) + if err == nil { + t.Error("expected domain mismatch error, got nil") + } +} + +func TestBadSignature(t *testing.T) { + _, privKey1, _ := ed25519.GenerateKey(rand.Reader) + pubKey2, _, _ := ed25519.GenerateKey(rand.Reader) + pubKey2Base58 := btcbase58.Encode(pubKey2) + + rawMessage := `example.com wants you to sign in with your Solana account: +` + pubKey2Base58 + ` + +Statement + +Version: 1 +Nonce: AAA +Issued At: 2025-01-01T00:00:00Z` + + msg, _ := ParseSIWSMessage(rawMessage) + // Sign with privKey1 but the message references a different public key (pubKey2). + signature := ed25519.Sign(privKey1, []byte(rawMessage)) + + params := SIWSVerificationParams{ + ExpectedDomain: "example.com", + CheckTime: false, + } + + err := VerifySIWS(rawMessage, signature, msg, params) + if err == nil { + t.Error("expected signature verification to fail, got success") + } +} diff --git a/internal/utilities/siws/types.go b/internal/utilities/siws/types.go new file mode 100644 index 0000000000..80ad80ce83 --- /dev/null +++ b/internal/utilities/siws/types.go @@ -0,0 +1,28 @@ +package siws + +import ( + "time" +) + +// SIWSMessage is the final structured form of a parsed SIWS message. +type SIWSMessage struct { + Domain string // e.g. "example.com" + Address string // base58-encoded Solana public key + Statement string // optional + URI string // optional + Version string // recommended (e.g. "1") + Nonce string // random nonce + IssuedAt time.Time // "Issued At" timestamp + // ExpirationTime is optional. If set, it should be checked against the current time. + ExpirationTime time.Time +} + +// SIWSVerificationParams holds parameters needed to verify an SIWS message. +type SIWSVerificationParams struct { + // The domain we expect. Must match message.Domain. + ExpectedDomain string + + // Whether or not to enforce time validity (IssuedAt <= now <= IssuedAt + TimeDuration). + CheckTime bool + TimeDuration time.Duration +} diff --git a/internal/utilities/siws/verify.go b/internal/utilities/siws/verify.go new file mode 100644 index 0000000000..a485bf7768 --- /dev/null +++ b/internal/utilities/siws/verify.go @@ -0,0 +1,58 @@ +package siws + +import ( + "crypto/ed25519" + "errors" + "time" + + "github.com/btcsuite/btcutil/base58" +) + +// VerifySIWS fully verifies: +// - The domain in msg matches expected domain +// - The ed25519 signature matches the parsed SIWS message text +// - The base58-encoded public key is valid +// - The message is within the allowed time window (if requested) +func VerifySIWS( + rawMessage string, // the original textual message + signature []byte, // signature returned by the client + msg *SIWSMessage, // the parsed SIWS message (from ParseSIWSMessage) + params SIWSVerificationParams, +) error { + // 1) Domain check + if params.ExpectedDomain == "" { + return errors.New("expected domain is not specified") + } + if msg.Domain != params.ExpectedDomain { + return errors.New("domain mismatch") + } + + // 2) Base58 decode -> ed25519.PublicKey + pubKey := base58.Decode(msg.Address) + if len(pubKey) != ed25519.PublicKeySize { + return errors.New("invalid base58 public key or wrong size (must be 32 bytes)") + } + + // 3) Verify signature + // The message to verify must be exactly the raw text that was originally signed. + if !ed25519.Verify(pubKey, []byte(rawMessage), signature) { + return errors.New("signature verification failed") + } + + // 4) Time check if requested + if params.CheckTime && params.TimeDuration > 0 { + if msg.IssuedAt.IsZero() { + return errors.New("issuedAt not set, but time check requested") + } + now := time.Now().UTC() + expiry := msg.IssuedAt.Add(params.TimeDuration) + if now.Before(msg.IssuedAt) { + return errors.New("message is issued in the future") + } + if now.After(expiry) { + return errors.New("message is expired") + } + } + + return nil +} diff --git a/internal/utilities/web3/ethereum/verify.go b/internal/utilities/web3/ethereum/verify.go new file mode 100644 index 0000000000..9d6cb7a8fa --- /dev/null +++ b/internal/utilities/web3/ethereum/verify.go @@ -0,0 +1,57 @@ +package ethereum + +import ( + "encoding/hex" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func VerifySignature(message string, signature string, address string) error { + // Remove 0x prefix if present + signature = removeHexPrefix(signature) + address = removeHexPrefix(address) + + // Convert signature hex to bytes + sigBytes, err := hex.DecodeString(signature) + if err != nil { + return fmt.Errorf("invalid signature hex: %w", err) + } + + // Adjust V value in signature (Ethereum specific) + if len(sigBytes) != 65 { + return fmt.Errorf("invalid signature length") + } + if sigBytes[64] < 27 { + sigBytes[64] += 27 + } + + // Hash the message according to EIP-191 + prefixedMessage := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message) + hash := crypto.Keccak256Hash([]byte(prefixedMessage)) + + // Recover public key from signature + pubKey, err := crypto.SigToPub(hash.Bytes(), sigBytes) + if err != nil { + return fmt.Errorf("error recovering public key: %w", err) + } + + // Derive Ethereum address from public key + recoveredAddr := crypto.PubkeyToAddress(*pubKey) + checkAddr := common.HexToAddress(address) + + // Compare addresses + if recoveredAddr != checkAddr { + return fmt.Errorf("signature not from expected address") + } + + return nil +} + +func removeHexPrefix(s string) string { + if len(s) > 2 && s[0:2] == "0x" { + return s[2:] + } + return s +}