siwe

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 24, 2026 License: Apache-2.0, MIT Imports: 17 Imported by: 0

README

Sign in with Ethereum (Go)

Pure-Go implementation of EIP-4361: Sign in with Ethereum.

This package is maintained by the Ethereum Identity Foundation alongside the canonical TypeScript, Python, and Rust libraries. All implementations share the same test vectors, ensuring byte-for-byte parity across languages.

Install

go get github.com/signinwithethereum/siwe-go

Usage

Parsing a message
m, err := siwe.ParseMessage(messageStr)
if err != nil {
    // parse error (structured *siwe.Error)
}

Any non-fatal issues are surfaced via m.Warnings — for example, parsing an address that is not EIP-55 checksummed emits a warning but still succeeds.

Constructing a message
m, err := siwe.InitMessage(
    "example.com",                             // domain
    "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", // address
    "https://example.com",                     // uri
    siwe.GenerateNonce(),                      // nonce
    map[string]interface{}{
        "statement":      "Example statement for SIWE",
        "chainId":        1,
        "expirationTime": time.Now().Add(24 * time.Hour),
        "requestId":      "req-1234",
        "resources":      []string{"https://example.com/resources/1"},
    },
)

Serialize for signing with m.String(). EIP-191 pre-hash is available via m.EIP191Hash() if you want to sign directly.

Verifying

VerifyEIP191 returns the recovered public key for a valid EOA signature:

pub, err := m.VerifyEIP191(signatureHex)

VerifyWith performs full verification including time, domain, nonce, URI, chain-id, and request-id bindings, plus optional contract-wallet fallback. It fails closed: Domain, Nonce, URI, and ChainID are all required and must match the message exactly.

res, err := m.VerifyWith(ctx, signature, siwe.VerifyParams{
    Domain:  &domain,
    Nonce:   &nonce,
    URI:     &uri,
    ChainID: &chainID,
}, siwe.VerifyOptions{})
Smart-contract wallet signatures (EIP-1271 / EIP-6492)

Wire any go-ethereum–compatible caller (*ethclient.Client implements the EthCaller interface) into VerifyOptions.ContractVerifier:

import "github.com/ethereum/go-ethereum/ethclient"

cl, _ := ethclient.Dial("https://mainnet.infura.io/v3/...")
verifier := siwe.NewEthCallerVerifier(cl)

res, err := m.VerifyWith(ctx, signature, siwe.VerifyParams{
    Domain:  &domain,
    Nonce:   &nonce,
    URI:     &uri,
    ChainID: &chainID,
}, siwe.VerifyOptions{
    ContractVerifier: verifier,
})

EOA recovery is attempted first; if it fails, the verifier is consulted via isValidSignature(bytes32,bytes) per EIP-1271. Signatures carrying the EIP-6492 magic suffix are handed to the universal off-chain validator bytecode via eth_call, which covers counterfactual (undeployed) wallets as well as already-deployed ones.

Note: this is verification only. EIP-6492 allows a verifier to optionally submit the factory transaction after a successful check to finalize on-chain deployment ("side-effectful" verification). This library does not do that — if you need the wallet actually deployed, submit the factory call yourself.

Time constraints
ok, err := m.ValidNow()        // current time
ok, err  = m.ValidAt(when)     // specific point
Signing from Go
hash := m.EIP191Hash()
sig, err := crypto.Sign(hash.Bytes(), privateKey)
if err != nil { /* ... */ }
sig[64] += 27 // normalize recovery byte for Ethereum wallets
hexSig := hexutil.Encode(sig)

Error handling

All errors returned by this package are *siwe.Error with a machine-readable Type field corresponding to the SiweErrorType codes used by the canonical TypeScript library. Match with errors.As:

var e *siwe.Error
if errors.As(err, &e) {
    switch e.Type {
    case siwe.ErrExpiredMessage:
    case siwe.ErrNonceMismatch:
    // ...
    }
}

See also

License

Dual-licensed under MIT and Apache-2.0.

Documentation

Overview

Package siwe implements Sign in with Ethereum (EIP-4361).

This is a Go port of the canonical TypeScript library (@signinwithethereum/siwe). It supports message parsing, construction, EIP-191 signature verification, and (opt-in) EIP-1271 contract-wallet verification through an external signer interface.

Index

Constants

View Source
const Version = "1.0.0"

Version is the semantic version of this package.

Variables

This section is empty.

Functions

func GenerateNonce

func GenerateNonce() string

GenerateNonce returns a cryptographically random 17-character alphanumeric nonce suitable for the EIP-4361 `nonce` field.

func IsEIP6492Signature

func IsEIP6492Signature(sigHex string) bool

IsEIP6492Signature reports whether the hex-encoded signature carries the EIP-6492 magic suffix (0x6492...6492).

Types

type ContractSignatureVerifier

type ContractSignatureVerifier interface {
	// VerifyContractSignature reports whether `sig` is a valid signature of
	// `hash` (the EIP-191 prehash) for `address` on `chainID`. Implementations
	// should return (false, err) for transport errors distinct from a simple
	// "not valid" result.
	VerifyContractSignature(ctx context.Context, address common.Address, hash common.Hash, sig []byte, chainID int) (bool, error)
}

ContractSignatureVerifier is the pluggable backend for smart-contract wallet signature verification (EIP-1271 and EIP-6492). The Go library does not ship a built-in RPC client because choice of client (ethclient, web3go, gnosis-safe-sdk, etc.) is environment-specific; implementations typically wrap go-ethereum's ethclient.

type Error

type Error struct {
	Type     ErrorType
	Expected string
	Received string
}

Error is a structured SIWE error. It carries a type code plus optional expected/received context so callers can match against specific failures without parsing free-form messages.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is supports errors.Is matching by type code.

type ErrorType

type ErrorType string

ErrorType is a structured error type for SIWE validation and verification.

const (
	ErrExpiredMessage        ErrorType = "Expired message."
	ErrInvalidDomain         ErrorType = "Invalid domain."
	ErrSchemeMismatch        ErrorType = "Scheme does not match provided scheme for verification."
	ErrDomainMismatch        ErrorType = "Domain does not match provided domain for verification."
	ErrNonceMismatch         ErrorType = "Nonce does not match provided nonce for verification."
	ErrURIMismatch           ErrorType = "URI does not match provided URI for verification."
	ErrChainIDMismatch       ErrorType = "Chain ID does not match provided chain ID for verification."
	ErrRequestIDMismatch     ErrorType = "Request ID does not match provided request ID for verification."
	ErrInvalidAddress        ErrorType = "Invalid address."
	ErrInvalidURI            ErrorType = "URI does not conform to RFC 3986."
	ErrInvalidNonce          ErrorType = "Nonce size smaller then 8 characters or is not alphanumeric."
	ErrNotYetValidMessage    ErrorType = "Message is not valid yet."
	ErrInvalidSignature      ErrorType = "Signature does not match address of the message."
	ErrInvalidSignatureChain ErrorType = "Contract wallet verification provider chain does not match message chain ID."
	ErrInvalidTimeFormat     ErrorType = "Invalid time format."
	ErrInvalidMessageVersion ErrorType = "Invalid message version."
	ErrUnableToParseMessage  ErrorType = "Unable to parse the message."
	ErrMissingDomain         ErrorType = "Domain is required for verification."
	ErrMissingNonce          ErrorType = "Nonce is required for verification."
	ErrMissingURI            ErrorType = "URI is required for verification."
	ErrMissingChainID        ErrorType = "Chain ID is required for verification."
	ErrInvalidParams         ErrorType = "Invalid parameters passed to verify."
	ErrMalformedMessage      ErrorType = "Message could not be prepared for signing."
	ErrInvalidStatement      ErrorType = "Statement contains invalid characters."
	ErrInvalidRequestID      ErrorType = "Request ID contains invalid characters."
)

Error type codes. These match the identifiers used by the canonical TypeScript implementation (@signinwithethereum/siwe).

type EthCaller

type EthCaller interface {
	CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
	ChainID(ctx context.Context) (*big.Int, error)
}

EthCaller is the minimal interface needed to perform read-only EVM calls. It is satisfied by *ethclient.Client and by any custom backend the user wishes to wire up (e.g. rpc.Client with eth_call).

type EthCallerVerifier

type EthCallerVerifier struct {
	Caller EthCaller
}

EthCallerVerifier implements ContractSignatureVerifier against any EthCaller. It performs a strict chain-id check (to avoid accepting EIP-1271 signatures from the wrong network) followed by an EIP-1271 isValidSignature call. When the signature carries the EIP-6492 magic suffix, the universal off-chain validator bytecode is executed via eth_call so that counterfactual (not yet deployed) smart accounts verify correctly.

func NewEthCallerVerifier

func NewEthCallerVerifier(caller EthCaller) *EthCallerVerifier

NewEthCallerVerifier constructs a verifier against an ethclient-style caller.

func (*EthCallerVerifier) VerifyContractSignature

func (v *EthCallerVerifier) VerifyContractSignature(ctx context.Context, address common.Address, hash common.Hash, sig []byte, chainID int) (bool, error)

VerifyContractSignature performs an EIP-1271 isValidSignature call.

type Message

type Message struct {
	// Scheme is the optional RFC 3986 URI scheme preceding the authority
	// (added by https://eips.ethereum.org/EIPS/eip-4361 revision 2023). When
	// nil the preamble is rendered as "<domain> wants you to sign in..."; when
	// set it is rendered as "<scheme>://<domain> wants you to sign in...".
	Scheme *string

	// Domain is the RFC 3986 authority requesting the signing.
	Domain string

	// Address is the Ethereum address performing the signing.
	Address common.Address

	// AddressRaw carries the 40-character (no 0x prefix) hex exactly as it
	// appeared in the parsed source when the input was not EIP-55 checksummed
	// (all-lower or all-upper). When non-nil, String() emits this verbatim so
	// the EIP-191 pre-hash matches what the signer actually signed. Leave nil
	// when building a message programmatically; the canonical EIP-55 form is
	// used.
	AddressRaw *string

	// Statement is the optional human-readable assertion. A nil value means
	// "no statement"; an empty non-nil value means "empty statement present"
	// (spec allows this). Line breaks are forbidden.
	Statement *string

	// URI is an RFC 3986 URI.
	URI string

	// Version must be "1".
	Version string

	// ChainID is the EIP-155 chain the session is bound to.
	ChainID int

	// Nonce is a randomized token, at least 8 alphanumeric characters.
	Nonce string

	// IssuedAt is an ISO 8601 / RFC 3339 datetime string.
	IssuedAt string

	// ExpirationTime, if set, is the point after which the message becomes invalid.
	ExpirationTime *string

	// NotBefore, if set, is the point before which the message is not yet valid.
	NotBefore *string

	// RequestID is an optional system-specific identifier. Empty string is
	// allowed per the ABNF (`request-id = *pchar`); nil means absent.
	RequestID *string

	// Resources is an optional list of RFC 3986 URI references. A nil slice
	// means the "Resources:" section is absent; a non-nil empty slice means
	// the section is present with no items.
	Resources []string

	// Warnings collects non-fatal validation messages surfaced during parsing
	// or construction (e.g. an address that is not EIP-55 checksummed).
	Warnings []string
	// contains filtered or unexported fields
}

Message represents a parsed or constructed EIP-4361 "Sign in with Ethereum" message. Fields follow the spec terminology; the zero value of a pointer field means the corresponding field is absent from the message.

func InitMessage

func InitMessage(domain, address, uri, nonce string, options map[string]interface{}) (*Message, error)

InitMessage is retained for backward compatibility; prefer NewMessage.

func NewMessage

func NewMessage(domain, address, uri, nonce string, options map[string]interface{}) (*Message, error)

NewMessage constructs a Message from explicit fields, applying the same validation as ParseMessage. Optional fields are passed via the options map with the same keys as the TS/Python APIs (`statement`, `chainId`, `issuedAt`, `expirationTime`, `notBefore`, `requestId`, `resources`, `scheme`). Values may be typed (int, string, *string, []string, time.Time) or passed through from JSON decoding (float64 for numbers).

func ParseMessage

func ParseMessage(s string) (*Message, error)

ParseMessage parses an EIP-4361 formatted message into a Message.

The parser is line-based and follows the ABNF grammar in `test-vectors/../ts/packages/siwe-parser/lib/siwe-abnf.txt`. It preserves the original address hex case when constructing an AddressRaw, so that roundtripping (parse -> String) produces byte-identical output for signatures to remain valid.

func (*Message) ClearResources

func (m *Message) ClearResources()

ClearResources removes the Resources section entirely (same as nil).

func (*Message) EIP191Hash

func (m *Message) EIP191Hash() common.Hash

EIP191Hash returns the EIP-191 personal-message hash of the prepared message.

func (*Message) GetAddress

func (m *Message) GetAddress() common.Address

func (*Message) GetChainID

func (m *Message) GetChainID() int

func (*Message) GetDomain

func (m *Message) GetDomain() string

func (*Message) GetExpirationTime

func (m *Message) GetExpirationTime() *string

func (*Message) GetIssuedAt

func (m *Message) GetIssuedAt() string

func (*Message) GetNonce

func (m *Message) GetNonce() string

func (*Message) GetNotBefore

func (m *Message) GetNotBefore() *string

func (*Message) GetRequestID

func (m *Message) GetRequestID() *string

func (*Message) GetResources

func (m *Message) GetResources() []string

GetResources returns the resources (nil if the section is absent).

func (*Message) GetScheme

func (m *Message) GetScheme() *string

func (*Message) GetStatement

func (m *Message) GetStatement() *string

func (*Message) GetURI

func (m *Message) GetURI() string

GetURI returns the URI field. For backward compatibility this returns a net/url.URL parse; callers that need the exact string should read URI.

func (*Message) GetVersion

func (m *Message) GetVersion() string

func (*Message) PrepareMessage

func (m *Message) PrepareMessage() string

PrepareMessage is an alias for String kept for parity with the canonical TypeScript/Python/Rust APIs.

func (*Message) SetResources

func (m *Message) SetResources(r []string)

SetResources assigns Resources explicitly and marks the field as present. This is the only way to produce an all-empty `Resources:\n` section from programmatic construction (since a nil slice is treated as absent).

func (*Message) String

func (m *Message) String() string

String serializes the message in EIP-4361 form, ready for EIP-191 signing.

func (*Message) ValidAt

func (m *Message) ValidAt(when time.Time) (bool, error)

ValidAt validates the time constraints at a specific point in time.

func (*Message) ValidNow

func (m *Message) ValidNow() (bool, error)

ValidNow validates the time constraints of the message at current time.

func (*Message) Validate

func (m *Message) Validate() error

Validate checks all field-level invariants on the Message. It is invoked automatically by ParseMessage/InitMessage/NewMessage; call it directly if you mutate fields after construction and want to re-verify.

func (*Message) VerifyEIP191

func (m *Message) VerifyEIP191(signature string) (*ecdsa.PublicKey, error)

VerifyEIP191 verifies a 65-byte EOA signature (hex encoded, with or without 0x prefix) against the message. Returns the recovered ECDSA public key on success.

func (*Message) VerifyWith

func (m *Message) VerifyWith(ctx context.Context, signature string, params VerifyParams, opts VerifyOptions) (*VerifyResult, error)

VerifyWith runs full verification (binding checks + signature) using the supplied params and options. The signature is a hex string (with or without 0x prefix).

The high-level API fails closed: Domain, Nonce, URI, and ChainID bindings are mandatory and must match the message exactly. If a relying party does not yet know what to expect for a field, it cannot safely call this method. For signature-only checks without binding enforcement, use VerifyEIP191 (EOA) or a ContractSignatureVerifier directly.

type VerifyOptions

type VerifyOptions struct {
	// ContractVerifier, if non-nil, is consulted for EIP-1271 / EIP-6492
	// contract-wallet signatures when the ECDSA recovery path does not match.
	ContractVerifier ContractSignatureVerifier
}

VerifyOptions configures verification.

type VerifyParams

type VerifyParams struct {
	// Scheme binding. If non-nil, the message's scheme must match exactly.
	Scheme *string
	// Domain binding. Required.
	Domain *string
	// Nonce binding. Required.
	Nonce *string
	// URI binding. Required.
	URI *string
	// ChainID binding. Required.
	ChainID *int
	// RequestID binding. Optional.
	RequestID *string
	// Timestamp for time-window checks. Nil uses the current time.
	Time *time.Time
}

VerifyParams carries the binding checks performed during VerifyWith.

Per ERC-4361 § Verifying a signed Message, the message MUST be checked against expected values after parsing. This library treats Domain, Nonce, URI, and ChainID as required: callers must supply what they expect and the message's fields must match exactly. Scheme and RequestID remain optional.

type VerifyResult

type VerifyResult struct {
	ECDSAPublicKey   *ecdsa.PublicKey
	ContractVerified bool
}

VerifyResult is returned by Verify. ECDSAPublicKey is populated on a successful EOA recovery; ContractVerified is true when EIP-1271/6492 verification succeeded.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL