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" + } + } +}