-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmessage.go
More file actions
281 lines (244 loc) · 9.09 KB
/
Copy pathmessage.go
File metadata and controls
281 lines (244 loc) · 9.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package siwe
import (
"fmt"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
)
// 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.
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
// resourcesSet distinguishes "absent" (nil) from "empty list" (non-nil len
// 0). We cannot rely on nil vs non-nil alone because a programmatically
// constructed Message{Resources: []string{}} is meant to render the empty
// section, while Message{} (no Resources at all) should omit it.
resourcesSet bool
// Warnings collects non-fatal validation messages surfaced during parsing
// or construction (e.g. an address that is not EIP-55 checksummed).
Warnings []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 (m *Message) SetResources(r []string) {
if r == nil {
r = []string{}
}
m.Resources = r
m.resourcesSet = true
}
// ClearResources removes the Resources section entirely (same as nil).
func (m *Message) ClearResources() {
m.Resources = nil
m.resourcesSet = false
}
// hasResources reports whether the Resources section should be rendered.
func (m *Message) hasResources() bool {
return m.resourcesSet || m.Resources != nil
}
// String serializes the message in EIP-4361 form, ready for EIP-191 signing.
func (m *Message) String() string {
var sb strings.Builder
// Preamble
if m.Scheme != nil && *m.Scheme != "" {
sb.WriteString(*m.Scheme)
sb.WriteString("://")
}
sb.WriteString(m.Domain)
sb.WriteString(" wants you to sign in with your Ethereum account:\n")
// Address line
if m.AddressRaw != nil {
sb.WriteString("0x")
sb.WriteString(*m.AddressRaw)
} else {
sb.WriteString(eip55(m.Address))
}
sb.WriteByte('\n')
// Statement block
sb.WriteByte('\n')
if m.Statement != nil {
sb.WriteString(*m.Statement)
sb.WriteByte('\n')
}
// Required fields
sb.WriteByte('\n')
fmt.Fprintf(&sb, "URI: %s\n", m.URI)
fmt.Fprintf(&sb, "Version: %s\n", m.Version)
fmt.Fprintf(&sb, "Chain ID: %d\n", m.ChainID)
fmt.Fprintf(&sb, "Nonce: %s\n", m.Nonce)
fmt.Fprintf(&sb, "Issued At: %s", m.IssuedAt)
// Optional fields (ordered per ABNF)
if m.ExpirationTime != nil {
fmt.Fprintf(&sb, "\nExpiration Time: %s", *m.ExpirationTime)
}
if m.NotBefore != nil {
fmt.Fprintf(&sb, "\nNot Before: %s", *m.NotBefore)
}
if m.RequestID != nil {
fmt.Fprintf(&sb, "\nRequest ID: %s", *m.RequestID)
}
if m.hasResources() {
sb.WriteString("\nResources:")
for _, r := range m.Resources {
sb.WriteString("\n- ")
sb.WriteString(r)
}
}
return sb.String()
}
// PrepareMessage is an alias for String kept for parity with the canonical
// TypeScript/Python/Rust APIs.
func (m *Message) PrepareMessage() string { return m.String() }
// 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 (m *Message) Validate() error {
if m.Domain == "" {
return newErrorWith(ErrInvalidDomain, "valid RFC 3986 authority", m.Domain)
}
if !validateAuthority(m.Domain) {
return newErrorWith(ErrInvalidDomain, "valid RFC 3986 authority", m.Domain)
}
if m.Scheme != nil {
if !isValidScheme(*m.Scheme) {
return newErrorWith(ErrInvalidURI, "valid RFC 3986 scheme", *m.Scheme)
}
}
if m.URI == "" {
return newErrorWith(ErrInvalidURI, "valid RFC 3986 URI", m.URI)
}
if !validateURI(m.URI) {
return newErrorWith(ErrInvalidURI, "valid RFC 3986 URI", m.URI)
}
if m.Version != "1" {
return newErrorWith(ErrInvalidMessageVersion, "1", m.Version)
}
if m.ChainID < 0 {
return newErrorWith(ErrUnableToParseMessage, "non-negative chainId", fmt.Sprintf("%d", m.ChainID))
}
if err := validateNonce(m.Nonce); err != nil {
return err
}
if m.IssuedAt == "" {
return newErrorWith(ErrUnableToParseMessage, "valid ISO 8601 issuedAt", "")
}
if !isValidISO8601(m.IssuedAt) {
return newErrorWith(ErrInvalidTimeFormat, "valid ISO 8601", m.IssuedAt)
}
if m.ExpirationTime != nil && !isValidISO8601(*m.ExpirationTime) {
return newErrorWith(ErrInvalidTimeFormat, "valid ISO 8601", *m.ExpirationTime)
}
if m.NotBefore != nil && !isValidISO8601(*m.NotBefore) {
return newErrorWith(ErrInvalidTimeFormat, "valid ISO 8601", *m.NotBefore)
}
if m.Statement != nil && !isValidStatement(*m.Statement) {
return newErrorWith(ErrInvalidStatement, "printable ASCII without LF", *m.Statement)
}
if m.RequestID != nil && !isValidRequestID(*m.RequestID) {
return newErrorWith(ErrInvalidRequestID, "*pchar per RFC 3986", *m.RequestID)
}
for _, r := range m.Resources {
if !validateURI(r) {
return newErrorWith(ErrInvalidURI, "valid RFC 3986 URI", r)
}
}
return nil
}
// ValidNow validates the time constraints of the message at current time.
func (m *Message) ValidNow() (bool, error) { return m.ValidAt(time.Now().UTC()) }
// ValidAt validates the time constraints at a specific point in time.
func (m *Message) ValidAt(when time.Time) (bool, error) {
if m.ExpirationTime != nil {
exp, err := parseISO8601(*m.ExpirationTime)
if err != nil {
return false, newErrorWith(ErrInvalidTimeFormat, "valid ISO 8601", *m.ExpirationTime)
}
if !when.Before(exp) {
return false, newError(ErrExpiredMessage)
}
}
if m.NotBefore != nil {
nbf, err := parseISO8601(*m.NotBefore)
if err != nil {
return false, newErrorWith(ErrInvalidTimeFormat, "valid ISO 8601", *m.NotBefore)
}
if when.Before(nbf) {
return false, newError(ErrNotYetValidMessage)
}
}
return true, nil
}
// Getters are kept for backward compatibility with earlier releases of the
// package. New code should access fields directly.
func (m *Message) GetDomain() string { return m.Domain }
func (m *Message) GetAddress() common.Address { return m.Address }
func (m *Message) GetVersion() string { return m.Version }
func (m *Message) GetChainID() int { return m.ChainID }
func (m *Message) GetNonce() string { return m.Nonce }
func (m *Message) GetIssuedAt() string { return m.IssuedAt }
func (m *Message) GetStatement() *string { return copyStringPtr(m.Statement) }
func (m *Message) GetExpirationTime() *string { return copyStringPtr(m.ExpirationTime) }
func (m *Message) GetNotBefore() *string { return copyStringPtr(m.NotBefore) }
func (m *Message) GetRequestID() *string { return copyStringPtr(m.RequestID) }
func (m *Message) GetScheme() *string { return copyStringPtr(m.Scheme) }
// 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 (m *Message) GetURI() string { return m.URI }
// GetResources returns the resources (nil if the section is absent).
func (m *Message) GetResources() []string {
if m.Resources == nil {
return nil
}
out := make([]string, len(m.Resources))
copy(out, m.Resources)
return out
}
func copyStringPtr(s *string) *string {
if s == nil {
return nil
}
v := *s
return &v
}