diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go
index d8b6ef0674..d4264bc156 100644
--- a/signer/core/signed_data.go
+++ b/signer/core/signed_data.go
@@ -180,15 +180,39 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
if err != nil {
return nil, useEthereumV, err
}
- sighash, msg := accounts.TextAndHash(textData)
- messages := []*apitypes.NameValueType{
- {
+
+ // EIP-4361 SIWE validation
+ messages, callInfo := validateSIWEMessage(string(textData), MetadataFromContext(ctx))
+
+ // In rejectMode (i.e. not --advanced), a CRIT callInfo entry means the
+ // request must be rejected outright rather than shown to the user.
+ // WARN entries (e.g. missing Origin header) still surface to the user.
+ if api.rejectMode {
+ for _, info := range callInfo {
+ if info.Typ == apitypes.CRIT {
+ return nil, useEthereumV, errors.New(info.Message)
+ }
+ }
+ }
+
+ // if SIWE parsing didn't give us structured messages fall back to raw message
+ if messages == nil {
+ _, msg := accounts.TextAndHash(textData)
+ messages = []*apitypes.NameValueType{{
Name: "message",
Typ: accounts.MimetypeTextPlain,
Value: msg,
- },
+ }}
+ }
+
+ sighash, msg := accounts.TextAndHash(textData)
+ req = &SignDataRequest{
+ ContentType: mediaType,
+ Rawdata: []byte(msg),
+ Messages: messages,
+ Callinfo: callInfo,
+ Hash: sighash,
}
- req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
}
req.Address = addr
req.Meta = MetadataFromContext(ctx)
diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go
index 8455aaf9c5..76f2571b57 100644
--- a/signer/core/signed_data_test.go
+++ b/signer/core/signed_data_test.go
@@ -246,6 +246,40 @@ func TestSignData(t *testing.T) {
} else if have := signature; !bytes.Equal(have, want) {
t.Fatalf("want %x, have %x", want, have)
}
+
+ // EIP-4361 SIWE message — valid, no HTTP context so no domain check runs.
+ siweMsg := "example.com wants you to sign in with your Ethereum account:\n" +
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n" +
+ "\n" +
+ "\n" +
+ "URI: https://example.com/login\n" +
+ "Version: 1\n" +
+ "Chain ID: 1\n" +
+ "Nonce: 32891757\n" +
+ "Issued At: 2021-09-30T16:25:24Z"
+ control.approveCh <- "Y"
+ control.inputCh <- "a_long_password"
+ signature, err = api.SignData(t.Context(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte(siweMsg)))
+ if err != nil {
+ t.Fatalf("SIWE sign: unexpected error: %v", err)
+ }
+ if len(signature) != 65 {
+ t.Errorf("SIWE sign: expected 65-byte signature, got %d", len(signature))
+ }
+
+ // Malformed SIWE (contains the trigger phrase but fails parsing) — treated as
+ // raw text/plain with a WARN callInfo entry;
+ badSiweMsg := "example.com wants you to sign in with your Ethereum account:\nbad message"
+ control.approveCh <- "Y"
+ control.inputCh <- "a_long_password"
+ signature, err = api.SignData(t.Context(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte(badSiweMsg)))
+ if err != nil {
+ t.Fatalf("malformed SIWE sign: unexpected error: %v", err)
+ }
+ if len(signature) != 65 {
+ t.Errorf("malformed SIWE sign: expected 65-byte signature, got %d", len(signature))
+ }
+
}
func TestDomainChainId(t *testing.T) {
diff --git a/signer/core/siwe.go b/signer/core/siwe.go
new file mode 100644
index 0000000000..5eae7faf6f
--- /dev/null
+++ b/signer/core/siwe.go
@@ -0,0 +1,410 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package core
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/signer/core/apitypes"
+)
+
+// SIWEMessage represents a parsed EIP-4361 Sign-In with Ethereum message.
+type SIWEMessage struct {
+ Scheme string // optional
+ Domain string // required
+ Address string // required
+ Statement string // optional
+ URI string // required
+ Version string // required
+ ChainID uint64 // required
+ Nonce string // required
+ IssuedAt time.Time // required
+ ExpirationTime *time.Time // optional
+ NotBefore *time.Time // optional
+ RequestID string // optional
+ Resources []string // optional
+}
+
+// SIWEWantedPrefix is the phrase that identifies a SIWE message. Wallet
+// implementers SHOULD warn users if this appears in any EIP-191 signing
+// request that does not fully conform to EIP-4361.
+const (
+ SIWEWantedPrefix = "wants you to sign in with your Ethereum account"
+
+ siweHeaderSuffix = " wants you to sign in with your Ethereum account:"
+)
+
+var (
+ siweSchemeRegexp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9+\-.]*$`)
+ siweNonceRegexp = regexp.MustCompile(`^[a-zA-Z0-9]{8,}$`)
+)
+
+// validateSIWEMessage inspects a text/plain signing request for EIP-4361 content.
+// If the text contains the SIWE phrase it attempts a full parse and domain check,
+// returning structured display messages and any validation warnings or errors.
+// Returns nil, nil when the text is not SIWE-related.
+func validateSIWEMessage(text string, meta Metadata) ([]*apitypes.NameValueType, []apitypes.ValidationInfo) {
+ if !strings.Contains(text, SIWEWantedPrefix) {
+ return nil, nil
+ }
+ siweMsg, err := parseSIWEMessage(text)
+ if err != nil {
+ return nil, []apitypes.ValidationInfo{{
+ Typ: apitypes.WARN,
+ Message: fmt.Sprintf("message appears to be Sign-In With Ethereum but is not valid: %v", err),
+ }}
+ }
+ var callInfo []apitypes.ValidationInfo
+ switch meta.Scheme {
+ case "ipc":
+ // no browser involved, no origin to verify
+ case "http":
+ if meta.Origin == "" {
+ callInfo = append(callInfo, apitypes.ValidationInfo{
+ Typ: apitypes.WARN,
+ Message: "could not verify domain: request has no Origin header",
+ })
+ } else {
+ origin, err := url.Parse(meta.Origin)
+ if err == nil && origin.Host != siweMsg.Domain {
+ callInfo = append(callInfo, apitypes.ValidationInfo{
+ Typ: apitypes.CRIT,
+ Message: fmt.Sprintf("domain mismatch: message claims %q but request origin is %q",
+ siweMsg.Domain, origin.Host),
+ })
+ }
+ }
+ }
+ return siweToAPIMessages(siweMsg), callInfo
+}
+
+// parseSIWEMessage parses a Sign-In with Ethereum message as defined by EIP-4361.
+// It validates structure, field order, and the format of each field value.
+func parseSIWEMessage(msg string) (*SIWEMessage, error) {
+ lines := strings.Split(msg, "\n")
+
+ // Minimum: header, address, blank, blank, URI, Version, Chain ID, Nonce, Issued At
+ if len(lines) < 9 {
+ return nil, errors.New("message too short to be a valid SIWE message")
+ }
+
+ cursor := 0
+ scheme, domain, err := parseSIWEHeader(lines[cursor])
+ if err != nil {
+ return nil, err
+ }
+ cursor++
+
+ if err := validateSIWEAddress(lines[cursor]); err != nil {
+ return nil, err
+ }
+ cursor++
+
+ if lines[cursor] != "" {
+ return nil, errors.New("expected empty line after address")
+ }
+ cursor++
+
+ statement, err := parseSIWEStatement(lines, &cursor)
+ if err != nil {
+ return nil, err
+ }
+
+ uri, version, chainID, nonce, issuedAt, err := parseSIWERequiredFields(lines, &cursor)
+ if err != nil {
+ return nil, err
+ }
+
+ siwe := &SIWEMessage{
+ Scheme: scheme,
+ Domain: domain,
+ Address: lines[1],
+ Statement: statement,
+ URI: uri,
+ Version: version,
+ ChainID: chainID,
+ Nonce: nonce,
+ IssuedAt: issuedAt,
+ }
+
+ if err := parseSIWEOptionalFields(lines, &cursor, siwe); err != nil {
+ return nil, err
+ }
+
+ if cursor < len(lines) {
+ return nil, fmt.Errorf("unexpected content after SIWE fields: %q", lines[cursor])
+ }
+
+ return siwe, nil
+}
+
+// parseSIWEHeader extracts the scheme (optional) and domain from line 0.
+func parseSIWEHeader(line string) (scheme, domain string, err error) {
+ if !strings.HasSuffix(line, siweHeaderSuffix) {
+ return "", "", errors.New("first line must end with \" wants you to sign in with your Ethereum account:\"")
+ }
+ prefix := strings.TrimSuffix(line, siweHeaderSuffix)
+
+ if i := strings.Index(prefix, "://"); i != -1 {
+ scheme = prefix[:i]
+ domain = prefix[i+3:]
+ if !siweSchemeRegexp.MatchString(scheme) {
+ return "", "", fmt.Errorf("invalid URI scheme %q", scheme)
+ }
+ } else {
+ domain = prefix
+ }
+
+ if err := validateSIWEDomain(domain); err != nil {
+ return "", "", err
+ }
+ return scheme, domain, nil
+}
+
+// validateSIWEAddress checks that s is a valid hex Ethereum address with EIP-55 checksum.
+func validateSIWEAddress(s string) error {
+ if !common.IsHexAddress(s) {
+ return errors.New("invalid Ethereum address")
+ }
+ if common.HexToAddress(s).Hex() != s {
+ return errors.New("address does not conform to EIP-55 checksum encoding")
+ }
+ return nil
+}
+
+// parseSIWEStatement reads the optional statement and the blank line that follows it.
+// cursor is left pointing at the first key-value field line.
+func parseSIWEStatement(lines []string, cursor *int) (string, error) {
+ if lines[*cursor] == "" {
+ *cursor++
+ return "", nil
+ }
+ statement := lines[*cursor]
+ *cursor++
+ if *cursor >= len(lines) || lines[*cursor] != "" {
+ return "", errors.New("expected empty line after statement")
+ }
+ *cursor++
+ return statement, nil
+}
+
+// parseSIWERequiredFields reads URI through Issued At in strict order.
+func parseSIWERequiredFields(lines []string, cursor *int) (uri, version string, chainID uint64, nonce string, issuedAt time.Time, err error) {
+ uri, err = parseSIWEField(lines, cursor, "URI: ")
+ if err != nil {
+ return
+ }
+ if err = validateSIWEURI(uri); err != nil {
+ return
+ }
+
+ version, err = parseSIWEField(lines, cursor, "Version: ")
+ if err != nil {
+ return
+ }
+ if version != "1" {
+ err = fmt.Errorf("unsupported SIWE version %q, must be \"1\"", version)
+ return
+ }
+
+ var chainIDStr string
+ chainIDStr, err = parseSIWEField(lines, cursor, "Chain ID: ")
+ if err != nil {
+ return
+ }
+ chainID, err = strconv.ParseUint(chainIDStr, 10, 64)
+ if err != nil {
+ err = fmt.Errorf("invalid Chain ID %q: must be a positive integer", chainIDStr)
+ return
+ }
+
+ nonce, err = parseSIWEField(lines, cursor, "Nonce: ")
+ if err != nil {
+ return
+ }
+ if !siweNonceRegexp.MatchString(nonce) {
+ err = errors.New("nonce must be at least 8 alphanumeric characters")
+ return
+ }
+
+ var issuedAtStr string
+ issuedAtStr, err = parseSIWEField(lines, cursor, "Issued At: ")
+ if err != nil {
+ return
+ }
+ issuedAt, err = parseSIWEDateTime(issuedAtStr)
+ if err != nil {
+ err = fmt.Errorf("invalid Issued At: %w", err)
+ }
+ return
+}
+
+// parseSIWEOptionalFields reads Expiration Time, Not Before, Request ID, and
+// Resources in strict order. Any unrecognised line is left for the caller to
+// detect as unexpected content.
+func parseSIWEOptionalFields(lines []string, cursor *int, siwe *SIWEMessage) error {
+ if err := parseSIWEOptionalTime(lines, cursor, "Expiration Time: ", &siwe.ExpirationTime); err != nil {
+ return err
+ }
+ if err := parseSIWEOptionalTime(lines, cursor, "Not Before: ", &siwe.NotBefore); err != nil {
+ return err
+ }
+ if *cursor < len(lines) && strings.HasPrefix(lines[*cursor], "Request ID: ") {
+ siwe.RequestID = strings.TrimPrefix(lines[*cursor], "Request ID: ")
+ (*cursor)++
+ }
+ return parseSIWEResources(lines, cursor, siwe)
+}
+
+// parseSIWEOptionalTime parses an optional datetime field if its prefix is present.
+func parseSIWEOptionalTime(lines []string, cursor *int, prefix string, dst **time.Time) error {
+ if *cursor >= len(lines) || !strings.HasPrefix(lines[*cursor], prefix) {
+ return nil
+ }
+ val := strings.TrimPrefix(lines[*cursor], prefix)
+ t, err := parseSIWEDateTime(val)
+ if err != nil {
+ return err
+ }
+ *dst = &t
+ (*cursor)++
+ return nil
+}
+
+// parseSIWEResources reads the Resources section if present.
+func parseSIWEResources(lines []string, cursor *int, siwe *SIWEMessage) error {
+ if *cursor >= len(lines) || lines[*cursor] != "Resources:" {
+ return nil
+ }
+ (*cursor)++
+ for *cursor < len(lines) {
+ if !strings.HasPrefix(lines[*cursor], "- ") {
+ return fmt.Errorf("invalid resource line %q: must start with \"- \"", lines[*cursor])
+ }
+ resource := strings.TrimPrefix(lines[*cursor], "- ")
+ if err := validateSIWEURI(resource); err != nil {
+ return err
+ }
+ siwe.Resources = append(siwe.Resources, resource)
+ (*cursor)++
+ }
+ return nil
+}
+
+// parseSIWEField reads the line at *cursor, strips the expected prefix, advances
+// the cursor, and returns the value. Returns an error if the line is missing or
+// does not start with prefix.
+func parseSIWEField(lines []string, cursor *int, prefix string) (string, error) {
+ if *cursor >= len(lines) {
+ return "", fmt.Errorf("missing required field %q", strings.TrimRight(prefix, " "))
+ }
+ if !strings.HasPrefix(lines[*cursor], prefix) {
+ return "", fmt.Errorf("expected field %q, got %q", strings.TrimRight(prefix, " "), lines[*cursor])
+ }
+ val := strings.TrimPrefix(lines[*cursor], prefix)
+ (*cursor)++
+ return val, nil
+}
+
+// parseSIWEDateTime parses an RFC 3339 datetime string, with or without
+// sub-second precision.
+func parseSIWEDateTime(s string) (time.Time, error) {
+ if t, err := time.Parse(time.RFC3339, s); err == nil {
+ return t, nil
+ }
+ if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
+ return t, nil
+ }
+ return time.Time{}, fmt.Errorf("not a valid RFC 3339 datetime: %q", s)
+}
+
+// validateSIWEURI checks that s is a valid RFC 3986 absolute URI.
+// url.Parse is too lenient (accepts raw spaces); we check for whitespace
+// explicitly since RFC 3986 requires spaces to be percent-encoded.
+func validateSIWEURI(s string) error {
+ if strings.ContainsAny(s, " \t") {
+ return fmt.Errorf("URI %q contains invalid whitespace", s)
+ }
+ u, err := url.Parse(s)
+ if err != nil || !u.IsAbs() {
+ return fmt.Errorf("URI %q is not a valid RFC 3986 absolute URI", s)
+ }
+ return nil
+}
+
+// validateSIWEDomain checks that s is a valid RFC 3986 authority (host[:port]).
+// Per EIP-4361: domain = authority = [ userinfo "@" ] host [ ":" port ]
+func validateSIWEDomain(s string) error {
+ if s == "" {
+ return errors.New("domain is empty")
+ }
+ u, err := url.Parse("http://" + s)
+ if err != nil {
+ return fmt.Errorf("domain %q is not a valid RFC 3986 authority", s)
+ }
+ // Go splits userinfo into u.User and u.Host, so reconstruct the full
+ // authority to verify it round-trips without modification.
+ authority := u.Host
+ if u.User != nil {
+ authority = u.User.String() + "@" + u.Host
+ }
+ if authority != s {
+ return fmt.Errorf("domain %q is not a valid RFC 3986 authority", s)
+ }
+ return nil
+}
+
+func siweToAPIMessages(m *SIWEMessage) []*apitypes.NameValueType {
+ nvts := []*apitypes.NameValueType{
+ {Name: "Domain", Typ: "domain", Value: m.Domain},
+ {Name: "Address", Typ: "address", Value: m.Address},
+ }
+ if m.Statement != "" {
+ nvts = append(nvts, &apitypes.NameValueType{Name: "Statement", Typ: "string", Value: m.Statement})
+ }
+ nvts = append(nvts,
+ &apitypes.NameValueType{Name: "URI", Typ: "uri", Value: m.URI},
+ &apitypes.NameValueType{Name: "Version", Typ: "uint", Value: m.Version},
+ &apitypes.NameValueType{Name: "Chain ID", Typ: "uint", Value: fmt.Sprintf("%d", m.ChainID)},
+ &apitypes.NameValueType{Name: "Nonce", Typ: "string", Value: m.Nonce},
+ &apitypes.NameValueType{Name: "Issued At", Typ: "datetime", Value: m.IssuedAt.String()},
+ )
+ if m.ExpirationTime != nil {
+ nvts = append(nvts, &apitypes.NameValueType{Name: "Expiration Time", Typ: "datetime", Value: m.ExpirationTime.String()})
+ }
+ if m.NotBefore != nil {
+ nvts = append(nvts, &apitypes.NameValueType{Name: "Not Before", Typ: "datetime", Value: m.NotBefore.String()})
+ }
+ if m.RequestID != "" {
+ nvts = append(nvts, &apitypes.NameValueType{Name: "Request ID", Typ: "string", Value: m.RequestID})
+ }
+ if len(m.Resources) > 0 {
+ res := make([]*apitypes.NameValueType, len(m.Resources))
+ for i, r := range m.Resources {
+ res[i] = &apitypes.NameValueType{Name: fmt.Sprintf("%d", i+1), Typ: "uri", Value: r}
+ }
+ nvts = append(nvts, &apitypes.NameValueType{Name: "Resources", Typ: "list", Value: res})
+ }
+ return nvts
+}
diff --git a/signer/core/siwe_test.go b/signer/core/siwe_test.go
new file mode 100644
index 0000000000..4d1dbd20f3
--- /dev/null
+++ b/signer/core/siwe_test.go
@@ -0,0 +1,270 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package core
+
+import (
+ "encoding/json"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/signer/core/apitypes"
+)
+
+// siwePositiveCase mirrors the structure of siwe_parsing_positive.json.
+type siwePositiveCase struct {
+ Message string `json:"message"`
+ Fields siweExpectedFields `json:"fields"`
+}
+
+// siweExpectedFields holds the expected parsed values from the test fixture.
+// Time fields are kept as strings because the fixture preserves the raw wire
+// format; we parse them via parseSIWEDateTime for comparison.
+type siweExpectedFields struct {
+ Scheme *string `json:"scheme"`
+ Domain string `json:"domain"`
+ Address string `json:"address"`
+ Statement string `json:"statement"`
+ URI string `json:"uri"`
+ Version string `json:"version"`
+ ChainID uint64 `json:"chainId"`
+ Nonce string `json:"nonce"`
+ IssuedAt string `json:"issuedAt"`
+ ExpirationTime *string `json:"expirationTime"`
+ NotBefore *string `json:"notBefore"`
+ RequestID *string `json:"requestId"`
+ Resources []string `json:"resources"`
+}
+
+// minimalSIWE is a valid EIP-4361 message used across domain-check tests.
+const minimalSIWE = "example.com wants you to sign in with your Ethereum account:\n" +
+ "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n" +
+ "\n" +
+ "\n" +
+ "URI: https://example.com/login\n" +
+ "Version: 1\n" +
+ "Chain ID: 1\n" +
+ "Nonce: 32891757\n" +
+ "Issued At: 2021-09-30T16:25:24Z"
+
+func TestParseSIWEMessage_Positive(t *testing.T) {
+ data, err := os.ReadFile("testdata/siwe/parsing_positive.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var cases map[string]siwePositiveCase
+ if err := json.Unmarshal(data, &cases); err != nil {
+ t.Fatal(err)
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ got, err := parseSIWEMessage(tc.Message)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ checkSIWEFields(t, got, tc.Fields)
+ })
+ }
+}
+
+func TestParseSIWEMessage_Negative(t *testing.T) {
+ data, err := os.ReadFile("testdata/siwe/parsing_negative.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var cases map[string]string
+ if err := json.Unmarshal(data, &cases); err != nil {
+ t.Fatal(err)
+ }
+
+ for name, message := range cases {
+ t.Run(name, func(t *testing.T) {
+ _, err := parseSIWEMessage(message)
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ })
+ }
+}
+
+func TestValidateSIWEMessage(t *testing.T) {
+ tests := []struct {
+ name string
+ text string
+ meta Metadata
+ wantMessages bool
+ wantCRIT bool
+ wantWARN bool
+ }{
+ {
+ name: "non-SIWE text returns nil",
+ text: "hello world",
+ meta: Metadata{Scheme: "http"},
+ wantMessages: false,
+ },
+ {
+ name: "valid SIWE over IPC skips domain check",
+ text: minimalSIWE,
+ meta: Metadata{Scheme: "ipc"},
+ wantMessages: true,
+ },
+ {
+ name: "valid SIWE over HTTP with matching origin",
+ text: minimalSIWE,
+ meta: Metadata{Scheme: "http", Origin: "https://example.com"},
+ wantMessages: true,
+ },
+ {
+ name: "valid SIWE over HTTP with mismatched origin",
+ text: minimalSIWE,
+ meta: Metadata{Scheme: "http", Origin: "https://evil.com"},
+ wantMessages: true,
+ wantCRIT: true,
+ },
+ {
+ name: "valid SIWE over HTTP with no origin header",
+ text: minimalSIWE,
+ meta: Metadata{Scheme: "http", Origin: ""},
+ wantMessages: true,
+ wantWARN: true,
+ },
+ {
+ name: "malformed SIWE returns nil messages and a WARN",
+ text: "example.com wants you to sign in with your Ethereum account:\nnot-an-address",
+ meta: Metadata{Scheme: "http"},
+ wantMessages: false,
+ wantWARN: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ messages, callInfo := validateSIWEMessage(tt.text, tt.meta)
+ if tt.wantMessages && messages == nil {
+ t.Error("expected structured messages, got nil")
+ }
+ if !tt.wantMessages && messages != nil {
+ t.Errorf("expected nil messages, got %d entries", len(messages))
+ }
+ checkSIWECallInfo(t, callInfo, tt.wantCRIT, tt.wantWARN)
+ })
+ }
+}
+
+func checkSIWECallInfo(t *testing.T, callInfo []apitypes.ValidationInfo, wantCRIT, wantWARN bool) {
+ t.Helper()
+ var hasCRIT, hasWARN bool
+ for _, info := range callInfo {
+ hasCRIT = hasCRIT || info.Typ == apitypes.CRIT
+ hasWARN = hasWARN || info.Typ == apitypes.WARN
+ }
+ if wantCRIT != hasCRIT {
+ t.Errorf("CRIT callInfo: want %v, got entries %v", wantCRIT, callInfo)
+ }
+ if wantWARN != hasWARN {
+ t.Errorf("WARN callInfo: want %v, got entries %v", wantWARN, callInfo)
+ }
+}
+
+func checkSIWEFields(t *testing.T, got *SIWEMessage, want siweExpectedFields) {
+ t.Helper()
+
+ wantScheme := ""
+ if want.Scheme != nil {
+ wantScheme = *want.Scheme
+ }
+ if got.Scheme != wantScheme {
+ t.Errorf("Scheme: got %q, want %q", got.Scheme, wantScheme)
+ }
+ if got.Domain != want.Domain {
+ t.Errorf("Domain: got %q, want %q", got.Domain, want.Domain)
+ }
+ if got.Address != want.Address {
+ t.Errorf("Address: got %q, want %q", got.Address, want.Address)
+ }
+ if got.Statement != want.Statement {
+ t.Errorf("Statement: got %q, want %q", got.Statement, want.Statement)
+ }
+ if got.URI != want.URI {
+ t.Errorf("URI: got %q, want %q", got.URI, want.URI)
+ }
+ if got.Version != want.Version {
+ t.Errorf("Version: got %q, want %q", got.Version, want.Version)
+ }
+ if got.ChainID != want.ChainID {
+ t.Errorf("ChainID: got %d, want %d", got.ChainID, want.ChainID)
+ }
+ if got.Nonce != want.Nonce {
+ t.Errorf("Nonce: got %q, want %q", got.Nonce, want.Nonce)
+ }
+
+ wantIssuedAt, err := parseSIWEDateTime(want.IssuedAt)
+ if err != nil {
+ t.Fatalf("test data has invalid IssuedAt %q: %v", want.IssuedAt, err)
+ }
+ if !got.IssuedAt.Equal(wantIssuedAt) {
+ t.Errorf("IssuedAt: got %v, want %v", got.IssuedAt, wantIssuedAt)
+ }
+
+ checkSIWEOptionalTime(t, "ExpirationTime", got.ExpirationTime, want.ExpirationTime)
+ checkSIWEOptionalTime(t, "NotBefore", got.NotBefore, want.NotBefore)
+
+ wantRequestID := ""
+ if want.RequestID != nil {
+ wantRequestID = *want.RequestID
+ }
+ if got.RequestID != wantRequestID {
+ t.Errorf("RequestID: got %q, want %q", got.RequestID, wantRequestID)
+ }
+
+ checkSIWEResources(t, got.Resources, want.Resources)
+}
+
+func checkSIWEResources(t *testing.T, got, want []string) {
+ t.Helper()
+ if len(got) != len(want) {
+ t.Errorf("Resources: got %d items, want %d", len(got), len(want))
+ return
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Errorf("Resources[%d]: got %q, want %q", i, got[i], want[i])
+ }
+ }
+}
+
+func checkSIWEOptionalTime(t *testing.T, field string, got *time.Time, wantStr *string) {
+ t.Helper()
+ if wantStr == nil {
+ if got != nil {
+ t.Errorf("%s: got %v, want nil", field, *got)
+ }
+ return
+ }
+ if got == nil {
+ t.Errorf("%s: got nil, want %q", field, *wantStr)
+ return
+ }
+ want, err := parseSIWEDateTime(*wantStr)
+ if err != nil {
+ t.Fatalf("test data has invalid %s %q: %v", field, *wantStr, err)
+ }
+ if !got.Equal(want) {
+ t.Errorf("%s: got %v, want %v", field, *got, want)
+ }
+}
diff --git a/signer/core/testdata/siwe/parsing_negative.json b/signer/core/testdata/siwe/parsing_negative.json
new file mode 100644
index 0000000000..8fd22cfc3a
--- /dev/null
+++ b/signer/core/testdata/siwe/parsing_negative.json
@@ -0,0 +1,31 @@
+{
+ "missing domain": " wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "missing address": "service.org wants you to sign in with your Ethereum account:\n\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "missing uri": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\n\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "missing version": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\n\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "missing chainId": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\n\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "missing nonce": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\n\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "missing issuedAt": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\n\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order uri": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nVersion: 1\nURI: https://service.org/login\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order version": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nChain ID: 1\nVersion: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order chainId": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nNonce: 12341234Chain ID: 1\n\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order nonce": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nIssued At: 2022-03-17T12:45:13.610Z\nNonce: 12341234\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order issuedAt": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nExpiration Time: 2023-03-17T12:45:13.610Z\nIssued At: 2022-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order expirationTime": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "out of order notBefore": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nRequest ID: some_id\nNot Before: 2022-03-17T12:45:13.610Z\nResources:\n- https://service.org/login",
+ "out of order requestId": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nResources:\n- https://service.org/login\nRequest ID: some_id",
+ "out of order resources": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nResources:\n- https://service.org/login\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id",
+ "domain not RFC4501 authority": "#notrfc4501 wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "address not EIP-55": "service.org wants you to sign in with your Ethereum account:\n0xe5a12547fe4e872d192e3ececb76f2ce1aea4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "statement has line break": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: \nhttps://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "uri is non-RFC 3986": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: :not_a_rfc3986_valid_uri_\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "version not 1": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 3\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "not a valid chainId": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: ?\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "nonce with less then 8 chars": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 1234567\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "non-ISO 8601 issuedAt": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: Wed Oct 05 2011 16:48:00 GMT+0200 (CEST)\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "non-ISO 8601 expirationTime": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: Wed Oct 05 2011 16:48:00 GMT+0200 (CEST)\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "non-ISO 8601 notBefore": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: Wed Oct 05 2011 16:48:00 GMT+0200 (CEST)\nRequest ID: some_id\nResources:\n- https://service.org/login",
+ "resources not separated by line break": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login - https://service.org/login/2",
+ "first resource not-RFC 3986": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- :not_a_rfc3986_valid_uri_\n- https://service.org/login",
+ "second resource is not-RFC3986": "service.org wants you to sign in with your Ethereum account:\n0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 12341234\nIssued At: 2022-03-17T12:45:13.610Z\nExpiration Time: 2023-03-17T12:45:13.610Z\nNot Before: 2022-03-17T12:45:13.610Z\nRequest ID: some_id\nResources:\n- https://service.org/login\n- :not_a_rfc3986_valid_uri_"
+}
diff --git a/signer/core/testdata/siwe/parsing_positive.json b/signer/core/testdata/siwe/parsing_positive.json
new file mode 100644
index 0000000000..95ca7540c5
--- /dev/null
+++ b/signer/core/testdata/siwe/parsing_positive.json
@@ -0,0 +1,248 @@
+{
+ "couple of optional fields": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z\nResources:\n- ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu\n- https://example.com/my-web2-claim.json",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z",
+ "resources": [
+ "ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu",
+ "https://example.com/my-web2-claim.json"
+ ]
+ }
+ },
+ "no optional field": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "timestamp without microseconds": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24Z"
+ }
+ },
+ "timezone not utc": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24-02:00",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24-02:00"
+ }
+ },
+ "domain is RFC 3986 authority with IP": {
+ "message": "127.0.0.1 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "127.0.0.1",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "domain is RFC 3986 authority with userinfo": {
+ "message": "test@127.0.0.1 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "test@127.0.0.1",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "domain is RFC 3986 authority with port": {
+ "message": "127.0.0.1:8080 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "127.0.0.1:8080",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "domain is localhost authority with port": {
+ "message": "localhost:8080 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "localhost:8080",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "domain is RFC 3986 authority with userinfo and port": {
+ "message": "test@127.0.0.1:8080 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ServiceOrg Terms of Service: https://service.org/tos\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "test@127.0.0.1:8080",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "no statement": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "domain ipv6": {
+ "message": "[::cafe] wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "[::cafe]",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "uri ipv6": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://[::cafe]\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://[::cafe]",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "uri ipv4": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://127.0.0.1\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://127.0.0.1",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "uri with port": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://127.0.0.1:4361\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://127.0.0.1:4361",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "uri ipv4 query params and fragment": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://127.0.0.1/?query=one#begin\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://127.0.0.1/?query=one#begin",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "chainId not 1": {
+ "message": "service.org wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\n\nURI: https://service.org/login\nVersion: 1\nChain ID: 4\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z",
+ "fields": {
+ "domain": "service.org",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "uri": "https://service.org/login",
+ "version": "1",
+ "chainId": 4,
+ "nonce": "32891757",
+ "issuedAt": "2021-09-30T16:25:24.000Z"
+ }
+ },
+ "recovery byte starting at 0": {
+ "message": "www.tally.xyz wants you to sign in with your Ethereum account:\n0xc95EB884FE852e241D409234bfC7045CB9E31BD7\n\nSign in with Ethereum to Tally\n\nURI: https://tally.xyz\nVersion: 1\nChain ID: 1\nNonce: 15050747\nIssued At: 2022-06-30T14:08:51.382Z",
+ "fields": {
+ "domain": "www.tally.xyz",
+ "address": "0xc95EB884FE852e241D409234bfC7045CB9E31BD7",
+ "statement": "Sign in with Ethereum to Tally",
+ "uri": "https://tally.xyz",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "15050747",
+ "issuedAt": "2022-06-30T14:08:51.382Z"
+ }
+ },
+ "domain contains optional scheme": {
+ "message": "https://example.com wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ExampleOrg Terms of Service: https://example.com/tos\n\nURI: https://example.com/login\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z",
+ "fields": {
+ "scheme": "https",
+ "domain": "example.com",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ExampleOrg Terms of Service: https://example.com/tos",
+ "uri": "https://example.com/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891756",
+ "issuedAt": "2021-09-30T16:25:24Z"
+ }
+ },
+ "scheme is not parsed from elsehwere in message": {
+ "message": "localhost:3030 wants you to sign in with your Ethereum account:\n0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\n\nI accept the ExampleOrg Terms of Service: http://localhost:3030/tos\n\nURI: http://localhost:3030/login\nVersion: 1\nChain ID: 1\nNonce: 32891756\nIssued At: 2021-09-30T16:25:24Z",
+ "fields": {
+ "scheme": null,
+ "domain": "localhost:3030",
+ "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "statement": "I accept the ExampleOrg Terms of Service: http://localhost:3030/tos",
+ "uri": "http://localhost:3030/login",
+ "version": "1",
+ "chainId": 1,
+ "nonce": "32891756",
+ "issuedAt": "2021-09-30T16:25:24Z"
+ }
+ }
+}