signer/clef: implement EIP-4361 Sign-In With Ethereum (SIWE) support

When Clef receives a text/plain signing request, it now detects and
validates EIP-4361 (SIWE) messages:

- Parses the full ABNF structure (domain, address, statement, URI,
  version, chain ID, nonce, issued-at, and all optional fields) with
  strict field ordering and RFC 3339/3986 format validation
- Warns the user when a message looks like SIWE but fails to parse
- For HTTP connections, compares the HTTP Origin header against the
  domain claimed in the message; a mismatch is a CRIT-level warning
  that causes hard rejection in non-advanced mode (--advanced flag
  suppresses rejection to a warning instead)
- Renders parsed SIWE fields as structured labelled entries in the
  signing prompt rather than a raw text blob
- Adds EIP-55 address checksum validation per spec requirement

Test vectors are sourced from the reference SIWE implementation.
This commit is contained in:
murraystewart96 2026-05-16 15:43:35 +01:00
parent 8a0223e8da
commit 0b5d47ad55
6 changed files with 1022 additions and 5 deletions

View file

@ -180,15 +180,39 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
if err != nil { if err != nil {
return nil, useEthereumV, err 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", Name: "message",
Typ: accounts.MimetypeTextPlain, Typ: accounts.MimetypeTextPlain,
Value: msg, 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.Address = addr
req.Meta = MetadataFromContext(ctx) req.Meta = MetadataFromContext(ctx)

View file

@ -246,6 +246,40 @@ func TestSignData(t *testing.T) {
} else if have := signature; !bytes.Equal(have, want) { } else if have := signature; !bytes.Equal(have, want) {
t.Fatalf("want %x, have %x", want, have) 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) { func TestDomainChainId(t *testing.T) {

410
signer/core/siwe.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

270
signer/core/siwe_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -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_"
}

View file

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