Skip to content

Commit

Permalink
refactor: migrate SIWS utilities to web3/solana and remove deprecated…
Browse files Browse the repository at this point in the history
… code
  • Loading branch information
Bewinxed committed Jan 18, 2025
1 parent 9e42bb2 commit fd8b16d
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 92 deletions.
9 changes: 4 additions & 5 deletions external_eip4361_siws_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"time"

"github.com/btcsuite/btcutil/base58"
siws "github.com/supabase/auth/internal/utilities/siws"
siws "github.com/supabase/auth/internal/utilities/web3/solana"
)

func LogSIWSExample() {
Expand Down Expand Up @@ -69,7 +69,6 @@ func LogSIWSExample() {
// Print JavaScript fetch code
fmt.Println(string(payloadJSON))
}

// func main() {
// LogSIWSExample()
// }
func main() {
LogSIWSExample()
}
10 changes: 4 additions & 6 deletions internal/api/provider/eip4361.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
"time"

"github.com/supabase/auth/internal/conf"
"github.com/supabase/auth/internal/utilities/siws"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/utilities/web3/ethereum"
siws "github.com/supabase/auth/internal/utilities/web3/solana"
"golang.org/x/oauth2"
)

Expand Down Expand Up @@ -128,7 +129,7 @@ func (p *EIP4361Provider) verifySolanaSignature(msg *SignedMessage) error {
TimeDuration: p.config.Timeout,
}

if err := siws.VerifySIWS(msg.Message, sigBytes, parsedMessage, params); err != nil {
if err := crypto.VerifySIWS(msg.Message, sigBytes, parsedMessage, params); err != nil {
return fmt.Errorf("SIWS verification failed: %w", err)
}

Expand All @@ -146,10 +147,7 @@ func (p *EIP4361Provider) GenerateSignMessage(address string, chain string, uri
}

// Generate nonce for message uniqueness
nonce, err := siws.GenerateNonce()
if err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
nonce := crypto.SecureToken()

now := time.Now().UTC()

Expand Down
2 changes: 1 addition & 1 deletion internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +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"
siws "github.com/supabase/auth/internal/utilities/web3/solana"
"gopkg.in/gomail.v2"
)

Expand Down
56 changes: 56 additions & 0 deletions internal/crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import (
"strconv"
"strings"

"crypto/ed25519"
"errors"
"time"

"golang.org/x/crypto/hkdf"

"github.com/btcsuite/btcutil/base58"
siws "github.com/supabase/auth/internal/utilities/web3/solana"
)

// SecureToken creates a new random token
Expand Down Expand Up @@ -157,3 +164,52 @@ func NewEncryptedString(id string, data []byte, keyID string, keyBase64URL strin

return &es, nil
}

// 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 *siws.SIWSMessage, // the parsed SIWS message (from ParseSIWSMessage)
params siws.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 !siws.IsBase58PubKey(msg.Address) {
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
}
2 changes: 1 addition & 1 deletion internal/reloader/testdata/50_example.env
Original file line number Diff line number Diff line change
Expand Up @@ -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:5433/postgres"
DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres"
API_EXTERNAL_URL="http://localhost:9999"
GOTRUE_API_HOST="localhost"
PORT="9999"
Expand Down
58 changes: 0 additions & 58 deletions internal/utilities/siws/verify.go

This file was deleted.

File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package siws

import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"net/url"
"strings"

"github.com/btcsuite/btcutil/base58"
)

// GenerateNonce creates a random 16-byte nonce, returning a hex-encoded string.
Expand All @@ -24,36 +27,38 @@ func GenerateNonce() (string, error) {
func ValidateDomain(domain string) error {
u, err := url.Parse("https://" + domain)
if err != nil || u.Hostname() == "" {
return errors.New("invalid domain")
return errors.New("siws: 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 {

// Basic length check before trying to decode
if len(address) == 0 {
return false
}
// Optionally, you could decode with base58 and check for 32 bytes.
return true

decoded := base58.Decode(address)
return len(decoded) == ed25519.PublicKeySize // ed25519.PublicKeySize is 32
}

// Add these functions to your existing helpers.go
func IsValidSolanaNetwork(network string) bool {
validNetworks := map[string]bool{
"mainnet": true,
"devnet": true,
"testnet": true,
switch network {
case "mainnet", "devnet", "testnet":
return true
default:
return false
}
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")
return errors.New("siws: chain configuration cannot be empty")
}

network := strings.TrimSpace(strings.ToLower(chainStr))
Expand All @@ -66,7 +71,7 @@ func ValidateChainConfig(chainStr string) error {

// 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")
ErrInvalidSolanaSignature = errors.New("siws: invalid Solana signature")
ErrInvalidSolanaAddress = errors.New("siws: invalid Solana address format")
ErrExpiredMessage = errors.New("siws: SIWS message has expired")
)
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import (
"time"
)

var (
// domainRegex matches the first line of a SIWS message containing the domain
domainRegex = regexp.MustCompile(`^([^ ]+)\s+wants you to sign in with your Solana account:$`)
)

// Common errors
var (
ErrMessageTooShort = errors.New("siws: message is too short or improperly formatted")
ErrInvalidDomainFormat = errors.New("siws: first line does not match expected format for domain request")
ErrMissingAddress = errors.New("siws: missing address line")
)

// ParseSIWSMessage parses a raw SIWS message into an SIWSMessage struct,
// performing robust checks to ensure correct formatting.
func ParseSIWSMessage(raw string) (*SIWSMessage, error) {
Expand All @@ -22,28 +34,28 @@ func ParseSIWSMessage(raw string) (*SIWSMessage, error) {
cleaned = append(cleaned, l)
}
}

if len(cleaned) < 2 {
return nil, errors.New("message is too short or improperly formatted")
return nil, ErrMessageTooShort
}

// 1) First line should match "<domain> 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")
return nil, ErrInvalidDomainFormat
}
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")
return nil, ErrMissingAddress
}

// 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:") ||
Expand Down Expand Up @@ -76,10 +88,10 @@ func ParseSIWSMessage(raw string) (*SIWSMessage, error) {
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)
return nil, fmt.Errorf("siws: failed to parse Issued At time: %w", err)
}
default:
return nil, fmt.Errorf("unrecognized line: %s", line)
return nil, fmt.Errorf("siws: unrecognized line: %s", line)
}
lineIndex++
}
Expand Down
File renamed without changes.
File renamed without changes.

0 comments on commit fd8b16d

Please sign in to comment.