Skip to content

Commit cc8043a

Browse files
committed
cmd/clef: implement EIP-4361 SIWE (Sign-In With Ethereum) message validator
1 parent d0216db commit cc8043a

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

cmd/clef/siwe-validator/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SIWE Validator for Clef
2+
3+
This directory implements a minimal Sign-In with Ethereum (SIWE) message validator for Clef,
4+
designed to verify incoming EIP-4361 formatted messages before approving signing requests.
5+
6+
The validator checks critical fields including:
7+
8+
- **Domain**: Ensures the requested domain matches an expected domain (e.g., `localhost:3000`).
9+
- **Ethereum Address**: Verifies that a valid `0x` prefixed address is provided.
10+
- **URI**: Confirms the target URI matches the expected resource.
11+
- **Version**: Verifies that the SIWE version is `1`.
12+
- **ChainID**: Ensures the chain ID matches the intended network (e.g., `1` for Ethereum Mainnet).
13+
- **Nonce**: Checks that a unique nonce is included to prevent replay attacks.
14+
- **Issued At**: Ensures the issued timestamp follows the ISO 8601/RFC3339 format.
15+
16+
Unlike previous implementations relying on external libraries such as `spruceid/siwe-go`,
17+
this version introduces a **lightweight internal parser** that directly processes SIWE messages,
18+
eliminating external dependencies and improving maintainability.
19+
20+
---
21+
22+
## How It Works
23+
24+
Upon receiving a signing request, Clef will invoke the `siwe-validator` binary,
25+
passing the SIWE message via standard input (stdin).
26+
27+
The validator parses the message line-by-line and verifies mandatory fields according to EIP-4361 specifications.
28+
29+
If validation passes, Clef proceeds with the signing flow. Otherwise, signing is rejected.
30+
31+
### Manually Testing the Validator
32+
33+
You can manually simulate a Clef signing request by piping a SIWE message into `siwe-validator`.
34+
For example:
35+
36+
```bash
37+
echo "localhost:3000 wants you to sign in with your Ethereum account:
38+
0x32e0556aeC41a34C3002a264f4694193EBCf44F7
39+
40+
URI: https://localhost:3000
41+
Version: 1
42+
ChainID: 1
43+
Nonce: 32891756
44+
Issued At: 2025-04-26T12:00:00Z" | ./siwe-validator
45+
```
46+
47+
If the message is valid, `siwe-validator` will exit silently with code `0`.
48+
If the message is invalid, an error message will be printed to `stderr`.
49+
50+
---
51+
52+
## Test Data
53+
54+
The `testdata/genmsg_test.go` file provides a minimal static SIWE message generator for manual testing purposes.
55+
56+
It outputs a standardized EIP-4361 formatted message, allowing developers to easily validate the `siwe-validator` behavior.
57+
58+
This file is intended for manual verification only and is not part of the production codebase or automated tests.
59+
60+
To manually generate and test a SIWE message:
61+
62+
```bash
63+
cd cmd/clef/siwevalidator
64+
go run testdata/genmsg_test.go | ./siwe-validator
65+
```
66+
67+
---
68+
69+
## Notes
70+
71+
- The validator currently supports basic field validation only.
72+
- Future improvements may include supporting optional fields like `Resources`, `Expiration Time`, and `Request ID`.
73+
- This implementation follows the EIP-4361.
74+
75+
---

cmd/clef/siwe-validator/main.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"strings"
8+
)
9+
10+
func main() {
11+
raw, err := io.ReadAll(os.Stdin)
12+
if err != nil {
13+
fmt.Fprintln(os.Stderr, "failed to read stdin:", err)
14+
os.Exit(1)
15+
}
16+
17+
lines := strings.Split(string(raw), "\n")
18+
19+
if len(lines) < 8 {
20+
fmt.Fprintln(os.Stderr, "invalid message: not enough lines")
21+
os.Exit(1)
22+
}
23+
24+
domainLine := lines[0]
25+
addressLine := lines[1]
26+
uriLine := lines[3]
27+
versionLine := lines[4]
28+
chainIDLine := lines[5]
29+
nonceLine := lines[6]
30+
issuedAtLine := lines[7]
31+
32+
if !strings.Contains(domainLine, "localhost:3000") {
33+
fmt.Fprintf(os.Stderr, "domain mismatch: %s\n", domainLine)
34+
os.Exit(1)
35+
}
36+
37+
if !strings.HasPrefix(addressLine, "0x") {
38+
fmt.Fprintf(os.Stderr, "invalid address: %s\n", addressLine)
39+
os.Exit(1)
40+
}
41+
42+
if !strings.HasPrefix(uriLine, "URI: https://localhost:3000") {
43+
fmt.Fprintf(os.Stderr, "uri mismatch: %s\n", uriLine)
44+
os.Exit(1)
45+
}
46+
47+
if !strings.Contains(versionLine, "Version: 1") {
48+
fmt.Fprintf(os.Stderr, "version mismatch: %s\n", versionLine)
49+
os.Exit(1)
50+
}
51+
52+
if !strings.Contains(chainIDLine, "ChainID: 1") {
53+
fmt.Fprintf(os.Stderr, "chainID mismatch: %s\n", chainIDLine)
54+
os.Exit(1)
55+
}
56+
57+
if !strings.Contains(nonceLine, "Nonce:") {
58+
fmt.Fprintf(os.Stderr, "nonce missing: %s\n", nonceLine)
59+
os.Exit(1)
60+
}
61+
62+
if !strings.Contains(issuedAtLine, "Issued At:") {
63+
fmt.Fprintf(os.Stderr, "issued at missing: %s\n", issuedAtLine)
64+
os.Exit(1)
65+
}
66+
67+
// 全部檢查通過
68+
os.Exit(0)
69+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
func main() {
8+
fmt.Print(`localhost:3000 wants you to sign in with your Ethereum account:
9+
0x32e0556aeC41a34C3002a264f4694193EBCf44F7
10+
11+
URI: https://localhost:3000
12+
Version: 1
13+
ChainID: 1
14+
Nonce: 32891756
15+
Issued At: 2025-04-26T12:00:00Z
16+
`)
17+
}

0 commit comments

Comments
 (0)