-
Notifications
You must be signed in to change notification settings - Fork 28
feat(clearnode): ERC-1271 smart contract wallet authentication #534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "math/big" | ||
|
|
||
| "github.com/ethereum/go-ethereum" | ||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/ethereum/go-ethereum/crypto" | ||
| ) | ||
|
|
||
| // ERC-1271 magic value returned by isValidSignature when the signature is valid. | ||
| // bytes4(keccak256("isValidSignature(bytes32,bytes)")) | ||
| var erc1271MagicValue = [4]byte{0x16, 0x26, 0xba, 0x7e} | ||
|
|
||
| // isValidSignature(bytes32,bytes) selector | ||
| var isValidSignatureSelector = crypto.Keccak256([]byte("isValidSignature(bytes32,bytes)"))[:4] | ||
|
|
||
| // Ethereum interface for ERC-1271 verification (matches existing Ethereum interface in the codebase). | ||
| type ERC1271Verifier interface { | ||
| CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) | ||
| CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) | ||
| } | ||
|
|
||
| // IsContract checks if the given address is a smart contract (has code deployed). | ||
| func IsContract(ctx context.Context, client ERC1271Verifier, addr common.Address) (bool, error) { | ||
| code, err := client.CodeAt(ctx, addr, nil) | ||
| if err != nil { | ||
| return false, fmt.Errorf("failed to check contract code: %w", err) | ||
| } | ||
| return len(code) > 0, nil | ||
| } | ||
|
|
||
| // VerifyERC1271Signature calls isValidSignature(bytes32,bytes) on a smart contract | ||
| // wallet and returns true if the contract considers the signature valid. | ||
| func VerifyERC1271Signature(ctx context.Context, client ERC1271Verifier, contractAddr common.Address, hash []byte, signature []byte) (bool, error) { | ||
| // ABI-encode the call: isValidSignature(bytes32 hash, bytes signature) | ||
| // Selector (4 bytes) + hash (32 bytes padded) + offset to bytes (32 bytes) + length (32 bytes) + signature data (padded to 32) | ||
| callData := make([]byte, 0, 4+32+32+32+len(signature)+32) | ||
|
|
||
| // Function selector | ||
| callData = append(callData, isValidSignatureSelector...) | ||
|
|
||
| // bytes32 hash (already 32 bytes) | ||
| if len(hash) != 32 { | ||
| return false, fmt.Errorf("hash must be 32 bytes, got %d", len(hash)) | ||
| } | ||
| callData = append(callData, hash...) | ||
|
|
||
| // Offset to bytes parameter (always 64 = 0x40 for two fixed params) | ||
| offset := make([]byte, 32) | ||
| offset[31] = 64 | ||
| callData = append(callData, offset...) | ||
|
|
||
| // Length of signature bytes | ||
| sigLen := make([]byte, 32) | ||
| bigLen := big.NewInt(int64(len(signature))) | ||
| bigLen.FillBytes(sigLen) | ||
| callData = append(callData, sigLen...) | ||
|
|
||
| // Signature data (padded to 32-byte boundary) | ||
| callData = append(callData, signature...) | ||
| if pad := len(signature) % 32; pad != 0 { | ||
| callData = append(callData, make([]byte, 32-pad)...) | ||
| } | ||
|
|
||
| result, err := client.CallContract(ctx, ethereum.CallMsg{ | ||
| To: &contractAddr, | ||
| Data: callData, | ||
| }, nil) | ||
| if err != nil { | ||
| return false, fmt.Errorf("isValidSignature call failed: %w", err) | ||
| } | ||
|
|
||
| if len(result) < 32 { | ||
| return false, fmt.Errorf("isValidSignature returned %d bytes, expected 32", len(result)) | ||
| } | ||
|
|
||
| // The magic value is in the first 4 bytes of the 32-byte return value | ||
| return result[0] == erc1271MagicValue[0] && | ||
| result[1] == erc1271MagicValue[1] && | ||
| result[2] == erc1271MagicValue[2] && | ||
| result[3] == erc1271MagicValue[3], nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,10 @@ package main | |
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/google/uuid" | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/shopspring/decimal" | ||
|
|
@@ -183,7 +185,9 @@ func (r *RPCRouter) handleAuthJWTVerify(ctx context.Context, authParams AuthVeri | |
| }, nil | ||
| } | ||
|
|
||
| // handleAuthJWTVerify verifies the challenge signature and returns the policy, response data and error. | ||
| // handleAuthSigVerify verifies the challenge signature and returns the policy, response data and error. | ||
| // It first attempts ECDSA recovery. If that fails or the recovered address doesn't match, | ||
| // it falls back to ERC-1271 smart contract wallet verification. | ||
| func (r *RPCRouter) handleAuthSigVerify(ctx context.Context, sig Signature, authParams AuthVerifyParams) (*Policy, any, error) { | ||
| logger := LoggerFromContext(ctx) | ||
|
|
||
|
|
@@ -192,18 +196,70 @@ func (r *RPCRouter) handleAuthSigVerify(ctx context.Context, sig Signature, auth | |
| logger.Error("failed to get challenge", "error", err) | ||
| return nil, nil, RPCErrorf("invalid challenge") | ||
| } | ||
| recoveredAddress, err := RecoverAddressFromEip712Signature( | ||
|
|
||
| // Copy sig so ECDSA recovery's V-byte mutation (27/28 → 0/1) doesn't | ||
| // corrupt the original for the ERC-1271 fallback path. | ||
| ecdsaSig := make(Signature, len(sig)) | ||
| copy(ecdsaSig, sig) | ||
|
|
||
| recoveredAddress, ecdsaErr := RecoverAddressFromEip712Signature( | ||
| challenge.Address, | ||
| challenge.Token.String(), | ||
| challenge.SessionKey, | ||
| challenge.Application, | ||
| challenge.Allowances, | ||
| challenge.Scope, | ||
| challenge.SessionKeyExpiresAt, | ||
| sig) | ||
| if err != nil { | ||
| logger.Error("failed to recover address from signature", "error", err) | ||
| return nil, nil, RPCErrorf("invalid signature") | ||
| ecdsaSig) | ||
|
|
||
| // If ECDSA recovery failed or recovered address doesn't match, try ERC-1271 | ||
| ecdsaMatch := ecdsaErr == nil && strings.EqualFold(recoveredAddress, challenge.Address) | ||
| if !ecdsaMatch { | ||
| erc1271Verified := false | ||
| expectedAddr := common.HexToAddress(challenge.Address) | ||
|
|
||
| typedDataHash, hashErr := ComputeAuthTypedDataHash( | ||
| challenge.Address, | ||
| challenge.Token.String(), | ||
| challenge.SessionKey, | ||
| challenge.Application, | ||
| challenge.Allowances, | ||
| challenge.Scope, | ||
| challenge.SessionKeyExpiresAt, | ||
| ) | ||
|
|
||
| if hashErr == nil { | ||
| // Try ERC-1271 verification on all configured chains | ||
| for chainID, client := range r.EthClients { | ||
| isContract, cErr := IsContract(ctx, client, expectedAddr) | ||
| if cErr != nil || !isContract { | ||
| continue | ||
| } | ||
|
Comment on lines
+235
to
+237
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When if cErr != nil {
logger.Debug("ERC-1271: IsContract check failed", "chainID", chainID, "address", expectedAddr, "error", cErr)
continue
}
if !isContract {
continue
} |
||
|
|
||
| verified, vErr := VerifyERC1271Signature(ctx, client, expectedAddr, typedDataHash, sig) | ||
| if vErr == nil && verified { | ||
| logger.Info("ERC-1271 signature verified for smart wallet", | ||
| "address", challenge.Address, "chainID", chainID) | ||
| erc1271Verified = true | ||
| break | ||
| } | ||
| } | ||
| } else { | ||
| logger.Warn("failed to compute typed data hash for ERC-1271 fallback", "error", hashErr) | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if !erc1271Verified { | ||
| if ecdsaErr != nil { | ||
| logger.Error("failed to recover address from signature", "error", ecdsaErr) | ||
| } else { | ||
| logger.Debug("signature address mismatch and ERC-1271 verification failed", | ||
| "expected", challenge.Address, "recovered", recoveredAddress) | ||
| } | ||
| return nil, nil, RPCErrorf("invalid signature") | ||
| } | ||
|
|
||
| // ERC-1271 verified — use the challenge address as the recovered address | ||
| recoveredAddress = challenge.Address | ||
| } | ||
|
|
||
| if err := r.AuthManager.ValidateChallenge(authParams.Challenge, recoveredAddress); err != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manual ABI encoding for the
isValidSignaturecall is complex and can be error-prone. For better robustness and maintainability, I recommend using thego-ethereum/accounts/abipackage. This is the standard library for this purpose in Go and would make the code simpler and less likely to contain subtle bugs related to encoding.