From 1a5ef0c497f610485f548259fef7176ba9c65ac2 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Thu, 1 Feb 2024 13:41:12 +0100 Subject: [PATCH 01/28] Add support for Intent sessions built on secp256r1 --- intents/intent.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/intents/intent.go b/intents/intent.go index f3740bd..b907bf2 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -1,8 +1,12 @@ package intents import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" "encoding/json" "fmt" + "math/big" "strings" "github.com/0xsequence/go-sequence/intents/packets" @@ -107,7 +111,20 @@ func (intent *Intent) IsValid() bool { return packet.IsValid() } +func (intent *Intent) isValidSECP256R1Session(session string, signature string) bool { + return strings.HasPrefix(session, "r1:") && strings.HasPrefix(signature, "r1:") +} + func (intent *Intent) isValidSignature(session string, signature string) bool { + if intent.isValidSECP256R1Session(session, signature) { + return intent.isValidSignatureSECP256R1(session, signature) + } else { + return intent.isValidSignatureSPECP256K1(session, signature) + } +} + +// isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256k1 session +func (intent *Intent) isValidSignatureSPECP256K1(session string, signature string) bool { // Get hash of the packet hash, err := intent.Hash() if err != nil { @@ -135,6 +152,42 @@ func (intent *Intent) isValidSignature(session string, signature string) bool { return strings.ToLower(addr.Hex()) == strings.ToLower(session) } +// isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256r1 session +func (intent *Intent) isValidSignatureSECP256R1(session string, signature string) bool { + // session + sessionHex, _ := strings.CutPrefix(session, "r1:") + sessionBuff := common.FromHex(sessionHex) + + // public key + // TODO: check if can use ecdh instead of unmarshal + // NOTE: no way to convert ecdh pub key into elliptic pub key? + x, y := elliptic.Unmarshal(elliptic.P256(), sessionBuff) + if x == nil || y == nil { + return false + } + + pub := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + // message hash + messageHash, _ := intent.Hash() + messageHash2 := sha256.Sum256(messageHash) + + // signature + signatureHex, _ := strings.CutPrefix(signature, "r1:") + signatureBytes := common.FromHex(signatureHex) + if len(signatureBytes) != 64 { + return false + } + + r := new(big.Int).SetBytes(signatureBytes[:32]) + s := new(big.Int).SetBytes(signatureBytes[32:64]) + return ecdsa.Verify(&pub, messageHash2[:], r, s) +} + func (intent *Intent) PacketCode() string { var packetCode struct { Code string `json:"code"` From bc0a243d4f4634bcfb2acbc2f2d10defd8839f8a Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Thu, 1 Feb 2024 14:29:51 +0100 Subject: [PATCH 02/28] Add support for Intent sessions built on secp256r1 - remove prefix --- intents/intent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intents/intent.go b/intents/intent.go index b907bf2..f758eb1 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -112,7 +112,7 @@ func (intent *Intent) IsValid() bool { } func (intent *Intent) isValidSECP256R1Session(session string, signature string) bool { - return strings.HasPrefix(session, "r1:") && strings.HasPrefix(signature, "r1:") + return len(session) == 65 && len(signature) == 64 } func (intent *Intent) isValidSignature(session string, signature string) bool { From 528b8d07e7386cb2d2965ab480a797dc3fb77701 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Thu, 1 Feb 2024 14:42:54 +0100 Subject: [PATCH 03/28] Add support for Intent sessions built on secp256r1 - remove prefix 2 --- intents/intent.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index f758eb1..d4e1c50 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -155,8 +155,7 @@ func (intent *Intent) isValidSignatureSPECP256K1(session string, signature strin // isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256r1 session func (intent *Intent) isValidSignatureSECP256R1(session string, signature string) bool { // session - sessionHex, _ := strings.CutPrefix(session, "r1:") - sessionBuff := common.FromHex(sessionHex) + sessionBuff := common.FromHex(session) // public key // TODO: check if can use ecdh instead of unmarshal @@ -177,8 +176,7 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string messageHash2 := sha256.Sum256(messageHash) // signature - signatureHex, _ := strings.CutPrefix(signature, "r1:") - signatureBytes := common.FromHex(signatureHex) + signatureBytes := common.FromHex(signature) if len(signatureBytes) != 64 { return false } From 8d97defea15c8f2ecacf7f900a7b8a2089aca0ba Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 09:26:49 +0100 Subject: [PATCH 04/28] Add support for Intent sessions built on secp256r1 - add prefix again --- intents/intent.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index d4e1c50..cc18c4b 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -112,7 +112,7 @@ func (intent *Intent) IsValid() bool { } func (intent *Intent) isValidSECP256R1Session(session string, signature string) bool { - return len(session) == 65 && len(signature) == 64 + return strings.HasPrefix(session, "r1:") && strings.HasPrefix(signature, "r1:") } func (intent *Intent) isValidSignature(session string, signature string) bool { @@ -155,7 +155,7 @@ func (intent *Intent) isValidSignatureSPECP256K1(session string, signature strin // isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256r1 session func (intent *Intent) isValidSignatureSECP256R1(session string, signature string) bool { // session - sessionBuff := common.FromHex(session) + sessionBuff := common.FromHex(session[3:]) // public key // TODO: check if can use ecdh instead of unmarshal @@ -176,7 +176,7 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string messageHash2 := sha256.Sum256(messageHash) // signature - signatureBytes := common.FromHex(signature) + signatureBytes := common.FromHex(signature[3:]) if len(signatureBytes) != 64 { return false } From 17f4af5741472befef68d8312a73be4c04ea6bba Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 10:33:26 +0100 Subject: [PATCH 05/28] Add more verbose errors in intent validation --- intents/intent.go | 24 ++++++++++++++++++------ intents/packets/packet.go | 24 +++++++++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index cc18c4b..ab348c2 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -16,6 +16,12 @@ import ( "github.com/0xsequence/ethkit/go-ethereum/crypto" ) +var ( + ErrInvalidPacket = fmt.Errorf("invalid packet") + ErrNoSignatures = fmt.Errorf("no signatures") + ErrInvalidSignature = fmt.Errorf("invalid signature") +) + type Intent struct { Version string `json:"version"` Packet json.RawMessage `json:"packet"` @@ -82,33 +88,39 @@ func (intent *Intent) Signers() []string { return signers } -func (intent *Intent) IsValid() bool { +func (intent *Intent) IsValid() (bool, error) { // Check if the packet is valid var packet packets.BasePacket err := json.Unmarshal(intent.Packet, &packet) if err != nil { - return false + return false, fmt.Errorf("intent: %w", ErrInvalidPacket) } // OpenSession packets do not require signatures if packet.Code == packets.OpenSessionPacketCode { - return packet.IsValid() + if ok, err := packet.IsValid(); !ok { + return false, fmt.Errorf("intent: %w", err) + } + return true, nil } // Check if there are any signatures if len(intent.signatures) == 0 { - return false + return false, fmt.Errorf("intent: %w", ErrNoSignatures) } // Check if all signatures are valid for _, signature := range intent.signatures { if !intent.isValidSignature(signature.Session, signature.Signature) { - return false + return false, fmt.Errorf("intent: %w", ErrInvalidSignature) } } // Check if the packet is valid - return packet.IsValid() + if ok, err := packet.IsValid(); !ok { + return false, fmt.Errorf("intent: %w", err) + } + return true, nil } func (intent *Intent) isValidSECP256R1Session(session string, signature string) bool { diff --git a/intents/packets/packet.go b/intents/packets/packet.go index 4ccaa40..186dfe9 100644 --- a/intents/packets/packet.go +++ b/intents/packets/packet.go @@ -1,6 +1,17 @@ package packets -import "time" +import ( + "fmt" + "time" +) + +const allowedTimeDrift = 5 + +var ( + ErrInvalidPacketCode = fmt.Errorf("invalid packet code") + ErrorPackedIssuedInFuture = fmt.Errorf("packet issued time is in the future") + ErrorPacketExpired = fmt.Errorf("packet expired") +) type BasePacket struct { Code string `json:"code"` @@ -8,9 +19,16 @@ type BasePacket struct { Expires uint64 `json:"expires"` } -func (p *BasePacket) IsValid() bool { +func (p *BasePacket) IsValid() (bool, error) { now := uint64(time.Now().Unix()) - return p.Code != "" && p.Issued <= now && p.Expires > now + if p.Code == "" { + return false, ErrInvalidPacketCode + } else if p.Issued <= now+allowedTimeDrift { + return false, ErrorPackedIssuedInFuture + } else if p.Expires > now-allowedTimeDrift { + return false, ErrorPacketExpired + } + return true, nil } type BasePacketForWallet struct { From 4c5bae30e770f652098190afd6a73703dc6dde1b Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 10:40:29 +0100 Subject: [PATCH 06/28] debug - 1 --- intents/intent.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/intents/intent.go b/intents/intent.go index ab348c2..7f6c0e3 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -129,8 +129,10 @@ func (intent *Intent) isValidSECP256R1Session(session string, signature string) func (intent *Intent) isValidSignature(session string, signature string) bool { if intent.isValidSECP256R1Session(session, signature) { + fmt.Println("isValidSignatureSECP256R1") return intent.isValidSignatureSECP256R1(session, signature) } else { + fmt.Println("isValidSignatureSECP256R1") return intent.isValidSignatureSPECP256K1(session, signature) } } @@ -166,6 +168,9 @@ func (intent *Intent) isValidSignatureSPECP256K1(session string, signature strin // isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256r1 session func (intent *Intent) isValidSignatureSECP256R1(session string, signature string) bool { + fmt.Println("session: ", session) + fmt.Println("signature: ", signature) + // session sessionBuff := common.FromHex(session[3:]) From 58e620900cca9fd3eb3f53740a525f70122d3d74 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 10:41:01 +0100 Subject: [PATCH 07/28] debug - 2 --- intents/intent.go | 1 + 1 file changed, 1 insertion(+) diff --git a/intents/intent.go b/intents/intent.go index 7f6c0e3..7640976 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -190,6 +190,7 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string // message hash messageHash, _ := intent.Hash() + fmt.Println("messageHash: ", messageHash) messageHash2 := sha256.Sum256(messageHash) // signature From 98e94730322fc7ff31bc4af26f97efff7d912529 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 10:44:13 +0100 Subject: [PATCH 08/28] debug - 3 --- intents/intent.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intents/intent.go b/intents/intent.go index 7640976..10bde04 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -179,6 +179,7 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string // NOTE: no way to convert ecdh pub key into elliptic pub key? x, y := elliptic.Unmarshal(elliptic.P256(), sessionBuff) if x == nil || y == nil { + fmt.Println("not valid x,y") return false } @@ -196,6 +197,7 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string // signature signatureBytes := common.FromHex(signature[3:]) if len(signatureBytes) != 64 { + fmt.Println("not valid signature bytes") return false } From 851ff9fadeb1d9fed7710ba7317c4abd030cfaf4 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 10:50:11 +0100 Subject: [PATCH 09/28] debug - 4 --- intents/packets/packet.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intents/packets/packet.go b/intents/packets/packet.go index 186dfe9..c3045cf 100644 --- a/intents/packets/packet.go +++ b/intents/packets/packet.go @@ -24,8 +24,10 @@ func (p *BasePacket) IsValid() (bool, error) { if p.Code == "" { return false, ErrInvalidPacketCode } else if p.Issued <= now+allowedTimeDrift { + fmt.Println("issued err", p.Issued, now+allowedTimeDrift) return false, ErrorPackedIssuedInFuture } else if p.Expires > now-allowedTimeDrift { + fmt.Println("expired err", p.Issued, now+allowedTimeDrift) return false, ErrorPacketExpired } return true, nil From 65a8e217d4d3bc7c03994e8c9f45ec377f410669 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 10:52:29 +0100 Subject: [PATCH 10/28] debug - 5 --- intents/packets/packet.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/intents/packets/packet.go b/intents/packets/packet.go index c3045cf..84d6b64 100644 --- a/intents/packets/packet.go +++ b/intents/packets/packet.go @@ -23,10 +23,10 @@ func (p *BasePacket) IsValid() (bool, error) { now := uint64(time.Now().Unix()) if p.Code == "" { return false, ErrInvalidPacketCode - } else if p.Issued <= now+allowedTimeDrift { + } else if p.Issued > now+allowedTimeDrift { fmt.Println("issued err", p.Issued, now+allowedTimeDrift) return false, ErrorPackedIssuedInFuture - } else if p.Expires > now-allowedTimeDrift { + } else if p.Expires < now-allowedTimeDrift { fmt.Println("expired err", p.Issued, now+allowedTimeDrift) return false, ErrorPacketExpired } From 67b0b56cd5f5029e3a5f6d204040cf14634aed46 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 11:17:37 +0100 Subject: [PATCH 11/28] remove debugs --- intents/intent.go | 8 -------- intents/packets/packet.go | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index 10bde04..ab348c2 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -129,10 +129,8 @@ func (intent *Intent) isValidSECP256R1Session(session string, signature string) func (intent *Intent) isValidSignature(session string, signature string) bool { if intent.isValidSECP256R1Session(session, signature) { - fmt.Println("isValidSignatureSECP256R1") return intent.isValidSignatureSECP256R1(session, signature) } else { - fmt.Println("isValidSignatureSECP256R1") return intent.isValidSignatureSPECP256K1(session, signature) } } @@ -168,9 +166,6 @@ func (intent *Intent) isValidSignatureSPECP256K1(session string, signature strin // isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256r1 session func (intent *Intent) isValidSignatureSECP256R1(session string, signature string) bool { - fmt.Println("session: ", session) - fmt.Println("signature: ", signature) - // session sessionBuff := common.FromHex(session[3:]) @@ -179,7 +174,6 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string // NOTE: no way to convert ecdh pub key into elliptic pub key? x, y := elliptic.Unmarshal(elliptic.P256(), sessionBuff) if x == nil || y == nil { - fmt.Println("not valid x,y") return false } @@ -191,13 +185,11 @@ func (intent *Intent) isValidSignatureSECP256R1(session string, signature string // message hash messageHash, _ := intent.Hash() - fmt.Println("messageHash: ", messageHash) messageHash2 := sha256.Sum256(messageHash) // signature signatureBytes := common.FromHex(signature[3:]) if len(signatureBytes) != 64 { - fmt.Println("not valid signature bytes") return false } diff --git a/intents/packets/packet.go b/intents/packets/packet.go index 84d6b64..ecf25af 100644 --- a/intents/packets/packet.go +++ b/intents/packets/packet.go @@ -5,6 +5,8 @@ import ( "time" ) +// allowedTimeDrift is the amount of time in seconds that a packet can be +// issued in the future or expired in the past and still be considered valid. const allowedTimeDrift = 5 var ( @@ -24,10 +26,8 @@ func (p *BasePacket) IsValid() (bool, error) { if p.Code == "" { return false, ErrInvalidPacketCode } else if p.Issued > now+allowedTimeDrift { - fmt.Println("issued err", p.Issued, now+allowedTimeDrift) return false, ErrorPackedIssuedInFuture } else if p.Expires < now-allowedTimeDrift { - fmt.Println("expired err", p.Issued, now+allowedTimeDrift) return false, ErrorPacketExpired } return true, nil From c5d3b8f792523d91e2e1f78fb78697fdb88ffc88 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 11:53:51 +0100 Subject: [PATCH 12/28] Make distinct split between SessionId and SessionVerifier(the value used for signature verification) --- intents/intent.go | 22 +- intents/intent_send_transaction_test.go | 13 +- intents/intent_sign_message_test.go | 13 +- intents/intent_test.go | 224 ++++++++++++++++++++- intents/packets/close_session.go | 2 +- intents/packets/finish_validate_session.go | 2 +- intents/packets/open_session.go | 4 +- 7 files changed, 263 insertions(+), 17 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index ab348c2..513f194 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -39,7 +39,7 @@ type JSONIntent struct { } type Signature struct { - Session string `json:"session"` + SessionId string `json:"sessionId"` Signature string `json:"signature"` } @@ -76,19 +76,24 @@ func (intent *Intent) Hash() ([]byte, error) { return crypto.Keccak256(packetBytes), nil } -func (intent *Intent) Signers() []string { +func (intent *Intent) Signers(getSessionVerifier func(sessionId string) (string, error)) []string { var signers []string for _, signature := range intent.signatures { - if intent.isValidSignature(signature.Session, signature.Signature) { - signers = append(signers, signature.Session) + sessionVerifier, err := getSessionVerifier(signature.SessionId) + if err != nil { + return nil + } + + if intent.isValidSignature(sessionVerifier, signature.Signature) { + signers = append(signers, sessionVerifier) } } return signers } -func (intent *Intent) IsValid() (bool, error) { +func (intent *Intent) IsValid(getSessionVerifier func(sessionId string) (string, error)) (bool, error) { // Check if the packet is valid var packet packets.BasePacket err := json.Unmarshal(intent.Packet, &packet) @@ -111,7 +116,12 @@ func (intent *Intent) IsValid() (bool, error) { // Check if all signatures are valid for _, signature := range intent.signatures { - if !intent.isValidSignature(signature.Session, signature.Signature) { + sessionVerifier, err := getSessionVerifier(signature.SessionId) + if err != nil { + return false, fmt.Errorf("intent: %w", err) + } + + if !intent.isValidSignature(sessionVerifier, signature.Signature) { return false, fmt.Errorf("intent: %w", ErrInvalidSignature) } } diff --git a/intents/intent_send_transaction_test.go b/intents/intent_send_transaction_test.go index 27168d5..6e2f0b8 100644 --- a/intents/intent_send_transaction_test.go +++ b/intents/intent_send_transaction_test.go @@ -2,6 +2,7 @@ package intents import ( "encoding/json" + "fmt" "math/big" "testing" @@ -84,7 +85,7 @@ func TestRecoverTransactionIntent(t *testing.T) { }, "signatures": [ { - "session": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", + "sessionId": "afaf60c0-67ba-4c9b-89ae-b115c78026a4", "signature": "0xdd137166e6e73fcaa710e822aa3eef3d501ef1b7969d59e8583cb602a32233e0628d4e28ea5a562a1ccf6bd85bfccfcd1004673a28763640cca33002fbedbb3a1b" } ] @@ -101,7 +102,15 @@ func TestRecoverTransactionIntent(t *testing.T) { assert.Nil(t, err) assert.Equal(t, common.Bytes2Hex(hash), "2feb22d5631075041c5aaafce98da8950d706a9eca8d9ea2b28ea95142d8e890") - signers := intent.Signers() + getSessionVerifier := func(sessionId string) (string, error) { + if sessionId == "afaf60c0-67ba-4c9b-89ae-b115c78026a4" { + return "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", nil + } else { + return "", fmt.Errorf("invalid session id") + } + } + + signers := intent.Signers(getSessionVerifier) assert.Equal(t, 1, len(signers)) assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) diff --git a/intents/intent_sign_message_test.go b/intents/intent_sign_message_test.go index a837b67..abf4b1f 100644 --- a/intents/intent_sign_message_test.go +++ b/intents/intent_sign_message_test.go @@ -2,6 +2,7 @@ package intents import ( "encoding/json" + "fmt" "math/big" "testing" @@ -23,7 +24,7 @@ func TestRecoverMessageIntent(t *testing.T) { "message": "0xdeadbeef" }, "signatures": [{ - "session": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", + "sessionId": "afaf60c0-67ba-4c9b-89ae-b115c78026a4", "signature": "0x827b2a2afbf4a8a79e761fdb26e567b519a56a06e897dce5517b3ccfb408b55f20aaba276c1dade28112f51fe7262fbd0508da0019c0f8582c41b2be451ddede1b" }] }` @@ -39,7 +40,15 @@ func TestRecoverMessageIntent(t *testing.T) { assert.Nil(t, err) assert.Equal(t, common.Bytes2Hex(hash), "5b15538a25716e951630dde1cf38ae056d764976145d1134576461203a621ddb") - signers := intent.Signers() + getSessionVerifier := func(sessionId string) (string, error) { + if sessionId == "afaf60c0-67ba-4c9b-89ae-b115c78026a4" { + return "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", nil + } else { + return "", fmt.Errorf("invalid session id") + } + } + + signers := intent.Signers(getSessionVerifier) assert.Equal(t, 1, len(signers)) assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) diff --git a/intents/intent_test.go b/intents/intent_test.go index e54e97f..2310109 100644 --- a/intents/intent_test.go +++ b/intents/intent_test.go @@ -1,14 +1,23 @@ package intents import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" "encoding/json" + "fmt" + "math/big" "testing" "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/davecgh/go-spew/spew" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) func TestParseAndRecoverIntent(t *testing.T) { + fmt.Println(uuid.New().String()) + data := `{ "version": "1.0.0", "packet": { @@ -26,7 +35,7 @@ func TestParseAndRecoverIntent(t *testing.T) { }] }, "signatures": [{ - "session": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", + "sessionId": "afaf60c0-67ba-4c9b-89ae-b115c78026a4", "signature": "0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c" }] }` @@ -42,7 +51,15 @@ func TestParseAndRecoverIntent(t *testing.T) { assert.NotNil(t, hash) assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - signers := intent.Signers() + getSessionVerifier := func(sessionId string) (string, error) { + if sessionId == "afaf60c0-67ba-4c9b-89ae-b115c78026a4" { + return "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", nil + } else { + return "", fmt.Errorf("invalid session id") + } + } + + signers := intent.Signers(getSessionVerifier) assert.Equal(t, 1, len(signers)) assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) assert.Equal(t, intent.PacketCode(), "sendTransactions") @@ -62,7 +79,7 @@ func TestParseAndRecoverIntent(t *testing.T) { assert.NotEqual(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") assert.Equal(t, intent.PacketCode(), "sendTransactions2") - signers = intent.Signers() + signers = intent.Signers(getSessionVerifier) assert.Equal(t, 0, len(signers)) // Parsing the JSON without tabs, spaces, newlines, etc. should still work @@ -77,3 +94,204 @@ func TestParseAndRecoverIntent(t *testing.T) { assert.NotNil(t, hash2) assert.Equal(t, common.Bytes2Hex(hash2), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") } + +func TestBase64url(t *testing.T) { + var keyMap = map[string]uint8{ + "0": 4, + "1": 43, + "2": 148, + "3": 238, + "4": 57, + "5": 214, + "6": 157, + "7": 138, + "8": 175, + "9": 239, + "10": 172, + "11": 236, + "12": 18, + "13": 198, + "14": 251, + "15": 186, + "16": 224, + "17": 178, + "18": 102, + "19": 131, + "20": 137, + "21": 22, + "22": 40, + "23": 59, + "24": 251, + "25": 55, + "26": 142, + "27": 51, + "28": 153, + "29": 45, + "30": 136, + "31": 255, + "32": 123, + "33": 66, + "34": 109, + "35": 89, + "36": 17, + "37": 22, + "38": 231, + "39": 240, + "40": 189, + "41": 65, + "42": 40, + "43": 132, + "44": 53, + "45": 67, + "46": 104, + "47": 191, + "48": 26, + "49": 103, + "50": 133, + "51": 83, + "52": 188, + "53": 159, + "54": 192, + "55": 23, + "56": 222, + "57": 157, + "58": 239, + "59": 41, + "60": 138, + "61": 94, + "62": 183, + "63": 101, + "64": 189, + } + + var rawKey []byte + for i := 0; i < len(keyMap); i++ { + rawKey = append(rawKey, keyMap[fmt.Sprintf("%d", i)]) + } + + x, y := elliptic.Unmarshal(elliptic.P256(), rawKey) + assert.NotNil(t, x) + + pub := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + spew.Dump(pub) + + sigMap := map[string]uint8{ + "0": 154, + "1": 100, + "2": 255, + "3": 225, + "4": 83, + "5": 191, + "6": 91, + "7": 32, + "8": 90, + "9": 78, + "10": 73, + "11": 248, + "12": 86, + "13": 165, + "14": 61, + "15": 93, + "16": 203, + "17": 253, + "18": 24, + "19": 12, + "20": 1, + "21": 243, + "22": 58, + "23": 222, + "24": 231, + "25": 65, + "26": 133, + "27": 250, + "28": 203, + "29": 39, + "30": 6, + "31": 175, + "32": 100, + "33": 171, + "34": 230, + "35": 122, + "36": 84, + "37": 247, + "38": 37, + "39": 201, + "40": 31, + "41": 12, + "42": 212, + "43": 235, + "44": 42, + "45": 162, + "46": 86, + "47": 140, + "48": 185, + "49": 82, + "50": 99, + "51": 137, + "52": 0, + "53": 161, + "54": 237, + "55": 143, + "56": 225, + "57": 238, + "58": 22, + "59": 179, + "60": 95, + "61": 246, + "62": 154, + "63": 93, + } + + var sigBytes []byte + for i := 0; i < len(sigMap); i++ { + sigBytes = append(sigBytes, sigMap[fmt.Sprintf("%d", i)]) + } + + hash := sha256.Sum256([]byte("test")) + + r := new(big.Int).SetBytes(sigBytes[:32]) + s := new(big.Int).SetBytes(sigBytes[32:64]) + verified := ecdsa.Verify(&pub, hash[:], r, s) + assert.True(t, verified) +} + +func TestECDSAP256SessionSig(t *testing.T) { + //sessionId := "r1:0x04d0ca179fec17ca13e0a05dfee4dd6e56c4f14975001ab093f6306f96f915c04d437047e1ca0244080fb2741d0518fe78c9e893c2a5b7d0549816198414c39335" + //message := "0x7b2270726f6a6563744964223a31312c226964546f6b656e223a2265794a68624763694f694a53557a49314e694973496d74705a434936496a67315a5455314d5441334e445932596a646c4d6a6b344d7a59784f546c6a4e54686a4e7a55344d575931596a6b794d324a6c4e4451694c434a30655841694f694a4b5631516966512e65794a7063334d694f694a6f64485277637a6f764c32466a59323931626e527a4c6d6476623264735a53356a623230694c434a68656e41694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a68645751694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a7a645749694f6949784d5451324f4455344d5467784e4441784f44597a4e446b304d4451694c434a6c6257467062434936496d3168636d6c75627a4d355147647459576c734c6d4e7662534973496d567459576c7358335a6c636d6c6d6157566b496a7030636e566c4c434a75596d59694f6a45334d4459334f44677a4f546773496d3568625755694f694a4e59584a6a6157346752384f7a636e70357859527a61326b694c434a7761574e3064584a6c496a6f696148523063484d364c79397361444d755a3239765a32786c64584e6c636d4e76626e526c626e5175593239744c32457651554e6e4f47396a536b464c623031435956424e5a3064494e3246355a6e644e54485236596931786431706f526b524b527a645656485a70556c705854566f7850584d354e69316a496977695a326c325a573566626d46745a534936496b3168636d4e7062694973496d5a6862576c73655639755957316c496a6f6952384f7a636e70357859527a61326b694c434a7362324e68624755694f694a7762434973496d6c68644349364d5463774e6a63344f4459354f4377695a586877496a6f784e7a41324e7a6b794d6a6b344c434a7164476b694f6949314e4759315a5745334d6a646a4f546b314f544a684e446b784e6d497a59574d31593245344f4755334e7a637a596a677a4e546b35496e302e4d346c73304f434d6a30586d4152354c7358385852707074644f55724e4a34346845364d476e4e664770334d444e495642396e7043524567386d64415033674f427171784e4d4e3464566d50734d334e5a54385779575a546871616e736254484a6d586e556d355245767357675f456d7954525a436d486545414c4e6877636e57477a386d375636664a7a7050366543554474534942722d376832756f364f637a6b704c534b382d74324569796b533773706c54475952486f42427265364d787762685044377074574a767957466c6f35594a617a74574c347454376c795957504264504470334456555743687679622d707944654e2d5066756530716a333165326b7448756c6b6a576b7a35325a5a73723758314876734164767642436838614c5f3553354f2d314a3649475f455f4e497168373254596f4141683968374e7639474c706e746a6d5a566764687769755f6f485067222c2273657373696f6e41646472657373223a2272313a307830346430636131373966656331376361313365306130356466656534646436653536633466313439373530303161623039336636333036663936663931356330346434333730343765316361303234343038306662323734316430353138666537386339653839336332613562376430353439383136313938343134633339333335222c22667269656e646c794e616d65223a22f09f95b72072697475616c2076696c6c616765222c22696e74656e744a534f4e223a227b5c2276657273696f6e5c223a5c22302e302e302d646576315c222c5c227061636b65745c223a7b5c226973737565645c223a313730363738383639392c5c22657870697265735c223a313730363738383939392c5c22636f64655c223a5c226f70656e53657373696f6e5c222c5c2273657373696f6e5c223a5c2272313a3078303464306361313739666563313763613133653061303564666565346464366535366334663134393735303031616230393366363330366639366639313563303464343337303437653163613032343430383066623237343164303531386665373863396538393363326135623764303534393831363139383431346333393333355c222c5c2270726f6f665c223a7b5c226964546f6b656e5c223a5c2265794a68624763694f694a53557a49314e694973496d74705a434936496a67315a5455314d5441334e445932596a646c4d6a6b344d7a59784f546c6a4e54686a4e7a55344d575931596a6b794d324a6c4e4451694c434a30655841694f694a4b5631516966512e65794a7063334d694f694a6f64485277637a6f764c32466a59323931626e527a4c6d6476623264735a53356a623230694c434a68656e41694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a68645751694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a7a645749694f6949784d5451324f4455344d5467784e4441784f44597a4e446b304d4451694c434a6c6257467062434936496d3168636d6c75627a4d355147647459576c734c6d4e7662534973496d567459576c7358335a6c636d6c6d6157566b496a7030636e566c4c434a75596d59694f6a45334d4459334f44677a4f546773496d3568625755694f694a4e59584a6a6157346752384f7a636e70357859527a61326b694c434a7761574e3064584a6c496a6f696148523063484d364c79397361444d755a3239765a32786c64584e6c636d4e76626e526c626e5175593239744c32457651554e6e4f47396a536b464c623031435956424e5a3064494e3246355a6e644e54485236596931786431706f526b524b527a645656485a70556c705854566f7850584d354e69316a496977695a326c325a573566626d46745a534936496b3168636d4e7062694973496d5a6862576c73655639755957316c496a6f6952384f7a636e70357859527a61326b694c434a7362324e68624755694f694a7762434973496d6c68644349364d5463774e6a63344f4459354f4377695a586877496a6f784e7a41324e7a6b794d6a6b344c434a7164476b694f6949314e4759315a5745334d6a646a4f546b314f544a684e446b784e6d497a59574d31593245344f4755334e7a637a596a677a4e546b35496e302e4d346c73304f434d6a30586d4152354c7358385852707074644f55724e4a34346845364d476e4e664770334d444e495642396e7043524567386d64415033674f427171784e4d4e3464566d50734d334e5a54385779575a546871616e736254484a6d586e556d355245767357675f456d7954525a436d486545414c4e6877636e57477a386d375636664a7a7050366543554474534942722d376832756f364f637a6b704c534b382d74324569796b533773706c54475952486f42427265364d787762685044377074574a767957466c6f35594a617a74574c347454376c795957504264504470334456555743687679622d707944654e2d5066756530716a333165326b7448756c6b6a576b7a35325a5a73723758314876734164767642436838614c5f3553354f2d314a3649475f455f4e497168373254596f4141683968374e7639474c706e746a6d5a566764687769755f6f4850675c227d7d2c5c227369676e6174757265735c223a5b5d7d227d" + //signature := "r1:0x5ca339d779449af10bc0dd74eb7e1ff6f9b584a52681bbb6dc27bb8923ec3ef629cdb9b17d4b6ed44fd125c4bb9376f869a2478513326613b03eb80a2bbfaeae" + + sessionId := "r1:0x040714f2ed82b5748ba30e3d81df81d481371b20c43cdbec81a89cbdb74e149e73ee083a1306328236c7de6d26b6f8d4494951d7423946422a04700ed182092a45" + message := "0x7a7e5a0913e63cac5886afcafedba93b17baae3eb4066534ffdd5e3da3e8c714" + signature := "r1:0x4038376385b045c19754bb69fa6cde925674778e6a1a78b8fa3135ec96b695aef5b8126c78dc17a2cc0be522a4e6154bf5152c908d763fb1c28e47cf419a3ea5" + + // get public key from sessionId + sessionIdBuff := common.FromHex(sessionId[3:]) + + x, y := elliptic.Unmarshal(elliptic.P256(), sessionIdBuff) + assert.NotNil(t, x) + + pub := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + spew.Dump(pub) + + // get message hash + messageBytes := common.FromHex(message) + messageHash := sha256.Sum256(messageBytes) + + // get signature + signatureBytes := common.FromHex(signature[3:]) + + r := new(big.Int).SetBytes(signatureBytes[:32]) + s := new(big.Int).SetBytes(signatureBytes[32:64]) + verified := ecdsa.Verify(&pub, messageHash[:], r, s) + assert.True(t, verified) +} diff --git a/intents/packets/close_session.go b/intents/packets/close_session.go index 4bfaa52..4aef3a7 100644 --- a/intents/packets/close_session.go +++ b/intents/packets/close_session.go @@ -7,7 +7,7 @@ import ( type CloseSessionPacket struct { BasePacketForWallet - Session string `json:"session"` + SessionId string `json:"sessionId"` } const CloseSessionPacketCode = "closeSession" diff --git a/intents/packets/finish_validate_session.go b/intents/packets/finish_validate_session.go index e108aae..6fec3cc 100644 --- a/intents/packets/finish_validate_session.go +++ b/intents/packets/finish_validate_session.go @@ -7,7 +7,7 @@ import ( type FinishValidateSessionPacket struct { Code string `json:"code"` - Session string `json:"session"` + SessionId string `json:"sessionId"` Salt string `json:"salt"` Challenge string `json:"challenge"` } diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go index ae1417a..8257f42 100644 --- a/intents/packets/open_session.go +++ b/intents/packets/open_session.go @@ -7,8 +7,8 @@ import ( type OpenSessionPacket struct { BasePacket - Session string `json:"session"` - Proof OpenSessionPacketProof `json:"proof"` + SessionVerifier string `json:"sessionVerifier"` + Proof OpenSessionPacketProof `json:"proof"` } type OpenSessionPacketProof struct { From 19595f2e7d9dfc775b8cb422c338774727814d09 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 11:56:07 +0100 Subject: [PATCH 13/28] Make distinct split between SessionId and SessionVerifier(the value used for signature verification) - 2 --- intents/packets/validate_session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intents/packets/validate_session.go b/intents/packets/validate_session.go index 944355c..cd5e560 100644 --- a/intents/packets/validate_session.go +++ b/intents/packets/validate_session.go @@ -42,7 +42,7 @@ const GetSessionPacketCode = "getSession" type GetSessionPacket struct { BasePacketForWallet - Session string `json:"session"` + SessionId string `json:"sessionId"` } func (p *GetSessionPacket) Unmarshal(packet json.RawMessage) error { From 1cbfcb421ae49edf1e9346b3aaaf2b8c5dc377c1 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 14:05:19 +0100 Subject: [PATCH 14/28] Intent signers extraction impr --- intents/intent.go | 2 +- intents/intent_test.go | 165 ----------------------------------------- 2 files changed, 1 insertion(+), 166 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index 513f194..c02c436 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -82,7 +82,7 @@ func (intent *Intent) Signers(getSessionVerifier func(sessionId string) (string, for _, signature := range intent.signatures { sessionVerifier, err := getSessionVerifier(signature.SessionId) if err != nil { - return nil + continue } if intent.isValidSignature(sessionVerifier, signature.Signature) { diff --git a/intents/intent_test.go b/intents/intent_test.go index 2310109..81a7885 100644 --- a/intents/intent_test.go +++ b/intents/intent_test.go @@ -95,171 +95,6 @@ func TestParseAndRecoverIntent(t *testing.T) { assert.Equal(t, common.Bytes2Hex(hash2), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") } -func TestBase64url(t *testing.T) { - var keyMap = map[string]uint8{ - "0": 4, - "1": 43, - "2": 148, - "3": 238, - "4": 57, - "5": 214, - "6": 157, - "7": 138, - "8": 175, - "9": 239, - "10": 172, - "11": 236, - "12": 18, - "13": 198, - "14": 251, - "15": 186, - "16": 224, - "17": 178, - "18": 102, - "19": 131, - "20": 137, - "21": 22, - "22": 40, - "23": 59, - "24": 251, - "25": 55, - "26": 142, - "27": 51, - "28": 153, - "29": 45, - "30": 136, - "31": 255, - "32": 123, - "33": 66, - "34": 109, - "35": 89, - "36": 17, - "37": 22, - "38": 231, - "39": 240, - "40": 189, - "41": 65, - "42": 40, - "43": 132, - "44": 53, - "45": 67, - "46": 104, - "47": 191, - "48": 26, - "49": 103, - "50": 133, - "51": 83, - "52": 188, - "53": 159, - "54": 192, - "55": 23, - "56": 222, - "57": 157, - "58": 239, - "59": 41, - "60": 138, - "61": 94, - "62": 183, - "63": 101, - "64": 189, - } - - var rawKey []byte - for i := 0; i < len(keyMap); i++ { - rawKey = append(rawKey, keyMap[fmt.Sprintf("%d", i)]) - } - - x, y := elliptic.Unmarshal(elliptic.P256(), rawKey) - assert.NotNil(t, x) - - pub := ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: x, - Y: y, - } - - spew.Dump(pub) - - sigMap := map[string]uint8{ - "0": 154, - "1": 100, - "2": 255, - "3": 225, - "4": 83, - "5": 191, - "6": 91, - "7": 32, - "8": 90, - "9": 78, - "10": 73, - "11": 248, - "12": 86, - "13": 165, - "14": 61, - "15": 93, - "16": 203, - "17": 253, - "18": 24, - "19": 12, - "20": 1, - "21": 243, - "22": 58, - "23": 222, - "24": 231, - "25": 65, - "26": 133, - "27": 250, - "28": 203, - "29": 39, - "30": 6, - "31": 175, - "32": 100, - "33": 171, - "34": 230, - "35": 122, - "36": 84, - "37": 247, - "38": 37, - "39": 201, - "40": 31, - "41": 12, - "42": 212, - "43": 235, - "44": 42, - "45": 162, - "46": 86, - "47": 140, - "48": 185, - "49": 82, - "50": 99, - "51": 137, - "52": 0, - "53": 161, - "54": 237, - "55": 143, - "56": 225, - "57": 238, - "58": 22, - "59": 179, - "60": 95, - "61": 246, - "62": 154, - "63": 93, - } - - var sigBytes []byte - for i := 0; i < len(sigMap); i++ { - sigBytes = append(sigBytes, sigMap[fmt.Sprintf("%d", i)]) - } - - hash := sha256.Sum256([]byte("test")) - - r := new(big.Int).SetBytes(sigBytes[:32]) - s := new(big.Int).SetBytes(sigBytes[32:64]) - verified := ecdsa.Verify(&pub, hash[:], r, s) - assert.True(t, verified) -} - func TestECDSAP256SessionSig(t *testing.T) { //sessionId := "r1:0x04d0ca179fec17ca13e0a05dfee4dd6e56c4f14975001ab093f6306f96f915c04d437047e1ca0244080fb2741d0518fe78c9e893c2a5b7d0549816198414c39335" //message := "0x7b2270726f6a6563744964223a31312c226964546f6b656e223a2265794a68624763694f694a53557a49314e694973496d74705a434936496a67315a5455314d5441334e445932596a646c4d6a6b344d7a59784f546c6a4e54686a4e7a55344d575931596a6b794d324a6c4e4451694c434a30655841694f694a4b5631516966512e65794a7063334d694f694a6f64485277637a6f764c32466a59323931626e527a4c6d6476623264735a53356a623230694c434a68656e41694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a68645751694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a7a645749694f6949784d5451324f4455344d5467784e4441784f44597a4e446b304d4451694c434a6c6257467062434936496d3168636d6c75627a4d355147647459576c734c6d4e7662534973496d567459576c7358335a6c636d6c6d6157566b496a7030636e566c4c434a75596d59694f6a45334d4459334f44677a4f546773496d3568625755694f694a4e59584a6a6157346752384f7a636e70357859527a61326b694c434a7761574e3064584a6c496a6f696148523063484d364c79397361444d755a3239765a32786c64584e6c636d4e76626e526c626e5175593239744c32457651554e6e4f47396a536b464c623031435956424e5a3064494e3246355a6e644e54485236596931786431706f526b524b527a645656485a70556c705854566f7850584d354e69316a496977695a326c325a573566626d46745a534936496b3168636d4e7062694973496d5a6862576c73655639755957316c496a6f6952384f7a636e70357859527a61326b694c434a7362324e68624755694f694a7762434973496d6c68644349364d5463774e6a63344f4459354f4377695a586877496a6f784e7a41324e7a6b794d6a6b344c434a7164476b694f6949314e4759315a5745334d6a646a4f546b314f544a684e446b784e6d497a59574d31593245344f4755334e7a637a596a677a4e546b35496e302e4d346c73304f434d6a30586d4152354c7358385852707074644f55724e4a34346845364d476e4e664770334d444e495642396e7043524567386d64415033674f427171784e4d4e3464566d50734d334e5a54385779575a546871616e736254484a6d586e556d355245767357675f456d7954525a436d486545414c4e6877636e57477a386d375636664a7a7050366543554474534942722d376832756f364f637a6b704c534b382d74324569796b533773706c54475952486f42427265364d787762685044377074574a767957466c6f35594a617a74574c347454376c795957504264504470334456555743687679622d707944654e2d5066756530716a333165326b7448756c6b6a576b7a35325a5a73723758314876734164767642436838614c5f3553354f2d314a3649475f455f4e497168373254596f4141683968374e7639474c706e746a6d5a566764687769755f6f485067222c2273657373696f6e41646472657373223a2272313a307830346430636131373966656331376361313365306130356466656534646436653536633466313439373530303161623039336636333036663936663931356330346434333730343765316361303234343038306662323734316430353138666537386339653839336332613562376430353439383136313938343134633339333335222c22667269656e646c794e616d65223a22f09f95b72072697475616c2076696c6c616765222c22696e74656e744a534f4e223a227b5c2276657273696f6e5c223a5c22302e302e302d646576315c222c5c227061636b65745c223a7b5c226973737565645c223a313730363738383639392c5c22657870697265735c223a313730363738383939392c5c22636f64655c223a5c226f70656e53657373696f6e5c222c5c2273657373696f6e5c223a5c2272313a3078303464306361313739666563313763613133653061303564666565346464366535366334663134393735303031616230393366363330366639366639313563303464343337303437653163613032343430383066623237343164303531386665373863396538393363326135623764303534393831363139383431346333393333355c222c5c2270726f6f665c223a7b5c226964546f6b656e5c223a5c2265794a68624763694f694a53557a49314e694973496d74705a434936496a67315a5455314d5441334e445932596a646c4d6a6b344d7a59784f546c6a4e54686a4e7a55344d575931596a6b794d324a6c4e4451694c434a30655841694f694a4b5631516966512e65794a7063334d694f694a6f64485277637a6f764c32466a59323931626e527a4c6d6476623264735a53356a623230694c434a68656e41694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a68645751694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a7a645749694f6949784d5451324f4455344d5467784e4441784f44597a4e446b304d4451694c434a6c6257467062434936496d3168636d6c75627a4d355147647459576c734c6d4e7662534973496d567459576c7358335a6c636d6c6d6157566b496a7030636e566c4c434a75596d59694f6a45334d4459334f44677a4f546773496d3568625755694f694a4e59584a6a6157346752384f7a636e70357859527a61326b694c434a7761574e3064584a6c496a6f696148523063484d364c79397361444d755a3239765a32786c64584e6c636d4e76626e526c626e5175593239744c32457651554e6e4f47396a536b464c623031435956424e5a3064494e3246355a6e644e54485236596931786431706f526b524b527a645656485a70556c705854566f7850584d354e69316a496977695a326c325a573566626d46745a534936496b3168636d4e7062694973496d5a6862576c73655639755957316c496a6f6952384f7a636e70357859527a61326b694c434a7362324e68624755694f694a7762434973496d6c68644349364d5463774e6a63344f4459354f4377695a586877496a6f784e7a41324e7a6b794d6a6b344c434a7164476b694f6949314e4759315a5745334d6a646a4f546b314f544a684e446b784e6d497a59574d31593245344f4755334e7a637a596a677a4e546b35496e302e4d346c73304f434d6a30586d4152354c7358385852707074644f55724e4a34346845364d476e4e664770334d444e495642396e7043524567386d64415033674f427171784e4d4e3464566d50734d334e5a54385779575a546871616e736254484a6d586e556d355245767357675f456d7954525a436d486545414c4e6877636e57477a386d375636664a7a7050366543554474534942722d376832756f364f637a6b704c534b382d74324569796b533773706c54475952486f42427265364d787762685044377074574a767957466c6f35594a617a74574c347454376c795957504264504470334456555743687679622d707944654e2d5066756530716a333165326b7448756c6b6a576b7a35325a5a73723758314876734164767642436838614c5f3553354f2d314a3649475f455f4e497168373254596f4141683968374e7639474c706e746a6d5a566764687769755f6f4850675c227d7d2c5c227369676e6174757265735c223a5b5d7d227d" From ab26dc56188d8bb45b855a0970b07faa14a41bcc Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 2 Feb 2024 14:39:26 +0100 Subject: [PATCH 15/28] More refactor session to sessionId --- intents/packets/validate_session.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/intents/packets/validate_session.go b/intents/packets/validate_session.go index cd5e560..d6287d4 100644 --- a/intents/packets/validate_session.go +++ b/intents/packets/validate_session.go @@ -7,7 +7,7 @@ import ( type ValidateSessionPacket struct { BasePacketForWallet - Session string `json:"session"` + SessionId string `json:"sessionId"` DeviceMetadata string `json:"deviceMetadata"` RedirectURL *string `json:"redirectURL"` } @@ -64,7 +64,7 @@ type GetSessionPacketResponse struct { Code string `json:"code"` Data struct { - Session string `json:"session"` + SessionId string `json:"sessionId"` Wallet string `json:"wallet"` Validated bool `json:"validated"` } From 6cdb3d8aee40eb7badd5446181a4109ff3d1d57e Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Mon, 5 Feb 2024 14:54:58 +0100 Subject: [PATCH 16/28] Add type for IntentVerifierGetter --- intents/intent.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index c02c436..6ca60dc 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -22,6 +22,8 @@ var ( ErrInvalidSignature = fmt.Errorf("invalid signature") ) +type IntentVerifierGetter func(sessionId string) (string, error) + type Intent struct { Version string `json:"version"` Packet json.RawMessage `json:"packet"` @@ -76,11 +78,11 @@ func (intent *Intent) Hash() ([]byte, error) { return crypto.Keccak256(packetBytes), nil } -func (intent *Intent) Signers(getSessionVerifier func(sessionId string) (string, error)) []string { +func (intent *Intent) Signers(verifierGetter IntentVerifierGetter) []string { var signers []string for _, signature := range intent.signatures { - sessionVerifier, err := getSessionVerifier(signature.SessionId) + sessionVerifier, err := verifierGetter(signature.SessionId) if err != nil { continue } @@ -93,7 +95,7 @@ func (intent *Intent) Signers(getSessionVerifier func(sessionId string) (string, return signers } -func (intent *Intent) IsValid(getSessionVerifier func(sessionId string) (string, error)) (bool, error) { +func (intent *Intent) IsValid(verifierGetter IntentVerifierGetter) (bool, error) { // Check if the packet is valid var packet packets.BasePacket err := json.Unmarshal(intent.Packet, &packet) @@ -116,7 +118,7 @@ func (intent *Intent) IsValid(getSessionVerifier func(sessionId string) (string, // Check if all signatures are valid for _, signature := range intent.signatures { - sessionVerifier, err := getSessionVerifier(signature.SessionId) + sessionVerifier, err := verifierGetter(signature.SessionId) if err != nil { return false, fmt.Errorf("intent: %w", err) } From de3bd256dd01ed9e21b6d0d52019600d7caacba6 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Mon, 5 Feb 2024 15:13:05 +0100 Subject: [PATCH 17/28] Add packet resp OpenSessionPacketResponse --- intents/packets/open_session.go | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go index 8257f42..1245da1 100644 --- a/intents/packets/open_session.go +++ b/intents/packets/open_session.go @@ -38,3 +38,55 @@ func (p *OpenSessionPacket) HasEmail() bool { func (p *OpenSessionPacket) HasIDToken() bool { return p.Proof.IDToken != "" } + +const OpenSessionPacketResponseCode = "openSession" + +type OpenSessionPacketResponse struct { + Code string `json:"code"` + Data OpenSessionPacketResponseData `json:"data"` +} + +type OpenSessionPacketResponseData struct { + SessionId string `json:"sessionId"` + Wallet string `json:"wallet"` +} + +func (p *OpenSessionPacketResponse) UnmarshalFromMap(m map[string]any) error { + if code, ok := m["code"].(string); ok && code == OpenSessionPacketResponseCode { + p.Code = code + } else { + return fmt.Errorf("packet code is not '%s', got '%s'", OpenSessionPacketResponseCode, m["code"]) + } + + data, ok := m["data"].(map[string]any) + if !ok { + return fmt.Errorf("packet data is not a map") + } + + if sessionId, ok := data["sessionId"].(string); ok { + p.Data.SessionId = sessionId + } else { + return fmt.Errorf("packet data is missing sessionId") + } + + if wallet, ok := data["wallet"].(string); ok { + p.Data.Wallet = wallet + } else { + return fmt.Errorf("packet data is missing wallet") + } + + return nil +} + +func (p *OpenSessionPacketResponse) Unmarshal(packet json.RawMessage) error { + err := json.Unmarshal(packet, &p) + if err != nil { + return err + } + + if p.Code != OpenSessionPacketResponseCode { + return fmt.Errorf("packet code is not '%s', got '%s'", OpenSessionPacketResponseCode, p.Code) + } + + return nil +} From 0108982a9f02f94a058e8e6d30d38dc1b13aef3e Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Mon, 5 Feb 2024 15:15:54 +0100 Subject: [PATCH 18/28] Add packet resp OpenSessionPacketResponse 2 --- intents/packets/open_session.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go index 1245da1..58db165 100644 --- a/intents/packets/open_session.go +++ b/intents/packets/open_session.go @@ -51,7 +51,12 @@ type OpenSessionPacketResponseData struct { Wallet string `json:"wallet"` } -func (p *OpenSessionPacketResponse) UnmarshalFromMap(m map[string]any) error { +func (p *OpenSessionPacketResponse) UnmarshalFromAny(a any) error { + m, ok := a.(map[string]any) + if !ok { + return fmt.Errorf("packet is not a map") + } + if code, ok := m["code"].(string); ok && code == OpenSessionPacketResponseCode { p.Code = code } else { From ee49074512b340d35c98d1cc696fc744fddf8a3b Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Mon, 5 Feb 2024 15:20:34 +0100 Subject: [PATCH 19/28] Add packet resp OpenSessionPacketResponse 3 --- intents/packets/open_session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go index 58db165..b2bbfc7 100644 --- a/intents/packets/open_session.go +++ b/intents/packets/open_session.go @@ -39,7 +39,7 @@ func (p *OpenSessionPacket) HasIDToken() bool { return p.Proof.IDToken != "" } -const OpenSessionPacketResponseCode = "openSession" +const OpenSessionPacketResponseCode = "sessionOpened" type OpenSessionPacketResponse struct { Code string `json:"code"` From 3f9055cf37fa1ae1ed0ccc5d34700d0f20514f2c Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 13:53:00 +0100 Subject: [PATCH 20/28] Return to having sessionId = pubKey/sessAdddr --- intents/intent.go | 72 ++++++++++++-------- intents/intent_send_transaction_test.go | 11 +-- intents/intent_sign_message_test.go | 13 +--- intents/intent_test.go | 89 +++++++++++++++++++++---- 4 files changed, 121 insertions(+), 64 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index 6ca60dc..8d22fe6 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -22,7 +22,12 @@ var ( ErrInvalidSignature = fmt.Errorf("invalid signature") ) -type IntentVerifierGetter func(sessionId string) (string, error) +type KeyType int + +const ( + KeyTypeSECP256K1 KeyType = iota + KeyTypeSECP256R1 +) type Intent struct { Version string `json:"version"` @@ -78,24 +83,19 @@ func (intent *Intent) Hash() ([]byte, error) { return crypto.Keccak256(packetBytes), nil } -func (intent *Intent) Signers(verifierGetter IntentVerifierGetter) []string { +func (intent *Intent) Signers() []string { var signers []string for _, signature := range intent.signatures { - sessionVerifier, err := verifierGetter(signature.SessionId) - if err != nil { - continue - } - - if intent.isValidSignature(sessionVerifier, signature.Signature) { - signers = append(signers, sessionVerifier) + if intent.isValidSignature(signature.SessionId, signature.Signature) { + signers = append(signers, signature.SessionId) } } return signers } -func (intent *Intent) IsValid(verifierGetter IntentVerifierGetter) (bool, error) { +func (intent *Intent) IsValid() (bool, error) { // Check if the packet is valid var packet packets.BasePacket err := json.Unmarshal(intent.Packet, &packet) @@ -118,12 +118,7 @@ func (intent *Intent) IsValid(verifierGetter IntentVerifierGetter) (bool, error) // Check if all signatures are valid for _, signature := range intent.signatures { - sessionVerifier, err := verifierGetter(signature.SessionId) - if err != nil { - return false, fmt.Errorf("intent: %w", err) - } - - if !intent.isValidSignature(sessionVerifier, signature.Signature) { + if !intent.isValidSignature(signature.SessionId, signature.Signature) { return false, fmt.Errorf("intent: %w", ErrInvalidSignature) } } @@ -135,20 +130,40 @@ func (intent *Intent) IsValid(verifierGetter IntentVerifierGetter) (bool, error) return true, nil } -func (intent *Intent) isValidSECP256R1Session(session string, signature string) bool { - return strings.HasPrefix(session, "r1:") && strings.HasPrefix(signature, "r1:") +func (intent *Intent) isValidP256R1Session(sessionId string, signature string) bool { + // handle old session ids + if len(sessionId) <= 42 { + return false + } + + // handle key typed session ids + sessionIdBytes := common.FromHex(sessionId) + switch KeyType(sessionIdBytes[0]) { + case KeyTypeSECP256K1: + return false + case KeyTypeSECP256R1: + return true + default: + return false + } } func (intent *Intent) isValidSignature(session string, signature string) bool { - if intent.isValidSECP256R1Session(session, signature) { - return intent.isValidSignatureSECP256R1(session, signature) + if intent.isValidP256R1Session(session, signature) { + return intent.isValidSignatureP256R1(session, signature) } else { - return intent.isValidSignatureSPECP256K1(session, signature) + return intent.isValidSignatureP256K1(session, signature) } } -// isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256k1 session -func (intent *Intent) isValidSignatureSPECP256K1(session string, signature string) bool { +// isValidSignatureP256K1 checks if the signature is valid for the given secp256k1 session +func (intent *Intent) isValidSignatureP256K1(sessionAddress string, signature string) bool { + // handle typed session address + if len(sessionAddress) > 42 { + sessionAddressBytes := common.FromHex(sessionAddress) + sessionAddress = fmt.Sprintf("0x%s", common.Bytes2Hex(sessionAddressBytes[1:])) + } + // Get hash of the packet hash, err := intent.Hash() if err != nil { @@ -173,18 +188,17 @@ func (intent *Intent) isValidSignatureSPECP256K1(session string, signature strin addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) // Check if the recovered address matches the session address - return strings.ToLower(addr.Hex()) == strings.ToLower(session) + return strings.ToLower(addr.Hex()) == strings.ToLower(sessionAddress) } -// isValidSignatureSPECP256K1 checks if the signature is valid for the given secp256r1 session -func (intent *Intent) isValidSignatureSECP256R1(session string, signature string) bool { - // session - sessionBuff := common.FromHex(session[3:]) +// isValidSignatureP256R1 checks if the signature is valid for the given secp256r1 session +func (intent *Intent) isValidSignatureP256R1(publicKey string, signature string) bool { + publicKeyBuff := common.FromHex(publicKey)[1:] // public key // TODO: check if can use ecdh instead of unmarshal // NOTE: no way to convert ecdh pub key into elliptic pub key? - x, y := elliptic.Unmarshal(elliptic.P256(), sessionBuff) + x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBuff) if x == nil || y == nil { return false } diff --git a/intents/intent_send_transaction_test.go b/intents/intent_send_transaction_test.go index 6e2f0b8..de433f4 100644 --- a/intents/intent_send_transaction_test.go +++ b/intents/intent_send_transaction_test.go @@ -2,7 +2,6 @@ package intents import ( "encoding/json" - "fmt" "math/big" "testing" @@ -102,15 +101,7 @@ func TestRecoverTransactionIntent(t *testing.T) { assert.Nil(t, err) assert.Equal(t, common.Bytes2Hex(hash), "2feb22d5631075041c5aaafce98da8950d706a9eca8d9ea2b28ea95142d8e890") - getSessionVerifier := func(sessionId string) (string, error) { - if sessionId == "afaf60c0-67ba-4c9b-89ae-b115c78026a4" { - return "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", nil - } else { - return "", fmt.Errorf("invalid session id") - } - } - - signers := intent.Signers(getSessionVerifier) + signers := intent.Signers() assert.Equal(t, 1, len(signers)) assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) diff --git a/intents/intent_sign_message_test.go b/intents/intent_sign_message_test.go index abf4b1f..71448b0 100644 --- a/intents/intent_sign_message_test.go +++ b/intents/intent_sign_message_test.go @@ -2,7 +2,6 @@ package intents import ( "encoding/json" - "fmt" "math/big" "testing" @@ -24,7 +23,7 @@ func TestRecoverMessageIntent(t *testing.T) { "message": "0xdeadbeef" }, "signatures": [{ - "sessionId": "afaf60c0-67ba-4c9b-89ae-b115c78026a4", + "sessionId": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", "signature": "0x827b2a2afbf4a8a79e761fdb26e567b519a56a06e897dce5517b3ccfb408b55f20aaba276c1dade28112f51fe7262fbd0508da0019c0f8582c41b2be451ddede1b" }] }` @@ -40,15 +39,7 @@ func TestRecoverMessageIntent(t *testing.T) { assert.Nil(t, err) assert.Equal(t, common.Bytes2Hex(hash), "5b15538a25716e951630dde1cf38ae056d764976145d1134576461203a621ddb") - getSessionVerifier := func(sessionId string) (string, error) { - if sessionId == "afaf60c0-67ba-4c9b-89ae-b115c78026a4" { - return "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", nil - } else { - return "", fmt.Errorf("invalid session id") - } - } - - signers := intent.Signers(getSessionVerifier) + signers := intent.Signers() assert.Equal(t, 1, len(signers)) assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) diff --git a/intents/intent_test.go b/intents/intent_test.go index 81a7885..8bff319 100644 --- a/intents/intent_test.go +++ b/intents/intent_test.go @@ -11,13 +11,10 @@ import ( "github.com/0xsequence/ethkit/go-ethereum/common" "github.com/davecgh/go-spew/spew" - "github.com/google/uuid" "github.com/stretchr/testify/assert" ) func TestParseAndRecoverIntent(t *testing.T) { - fmt.Println(uuid.New().String()) - data := `{ "version": "1.0.0", "packet": { @@ -35,7 +32,7 @@ func TestParseAndRecoverIntent(t *testing.T) { }] }, "signatures": [{ - "sessionId": "afaf60c0-67ba-4c9b-89ae-b115c78026a4", + "sessionId": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", "signature": "0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c" }] }` @@ -51,15 +48,7 @@ func TestParseAndRecoverIntent(t *testing.T) { assert.NotNil(t, hash) assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - getSessionVerifier := func(sessionId string) (string, error) { - if sessionId == "afaf60c0-67ba-4c9b-89ae-b115c78026a4" { - return "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", nil - } else { - return "", fmt.Errorf("invalid session id") - } - } - - signers := intent.Signers(getSessionVerifier) + signers := intent.Signers() assert.Equal(t, 1, len(signers)) assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) assert.Equal(t, intent.PacketCode(), "sendTransactions") @@ -79,7 +68,77 @@ func TestParseAndRecoverIntent(t *testing.T) { assert.NotEqual(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") assert.Equal(t, intent.PacketCode(), "sendTransactions2") - signers = intent.Signers(getSessionVerifier) + signers = intent.Signers() + assert.Equal(t, 0, len(signers)) + + // Parsing the JSON without tabs, spaces, newlines, etc. should still work + // and produce the same hash + data2 := `{"signatures":[{"signature":"0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c","session":"0x1111BD4F3233e7a7f552AdAf32C910fD30de598B"}],"version":"1.0.0","packet":{"transactions":[{"token":"0x0000000000000000000000000000000000000000","value":"0","type":"erc20send","to":"0x0dc9603d4da53841C1C83f3B550C6143e60e0425"}],"wallet":"0xD67FC48b298B09Ed3D03403d930769C527186c4e","expires":1600086400,"code":"sendTransactions","network":"1","identifier":"test-identifier","issued":1600000000}}` + intent2 := &Intent{} + err = json.Unmarshal([]byte(data2), intent2) + assert.Nil(t, err) + + hash2, err := intent2.Hash() + assert.Nil(t, err) + assert.NotNil(t, hash2) + assert.Equal(t, common.Bytes2Hex(hash2), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") +} + +func TestParseAndRecoverIntent_SessionKeyP256K1Typed(t *testing.T) { + data := `{ + "version": "1.0.0", + "packet": { + "code": "sendTransactions", + "identifier": "test-identifier", + "issued": 1600000000, + "expires": 1600086400, + "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", + "network": "1", + "transactions": [{ + "type": "erc20send", + "token": "0x0000000000000000000000000000000000000000", + "to": "0x0dc9603d4da53841C1C83f3B550C6143e60e0425", + "value": "0" + }] + }, + "signatures": [{ + "sessionId": "0x001111BD4F3233e7a7f552AdAf32C910fD30de598B", + "signature": "0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c" + }] + }` + + intent := &Intent{} + err := json.Unmarshal([]byte(data), intent) + assert.Nil(t, err) + + assert.Equal(t, "1.0.0", intent.Version) + + hash, err := intent.Hash() + assert.Nil(t, err) + assert.NotNil(t, hash) + assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") + + signers := intent.Signers() + assert.Equal(t, 1, len(signers)) + assert.Equal(t, "0x001111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) + assert.Equal(t, intent.PacketCode(), "sendTransactions") + + // Changing the version should not affect the hash + intent.Version = "2.0.0" + hash, err = intent.Hash() + assert.Nil(t, err) + assert.NotNil(t, hash) + assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") + + // Changing the packet code SHOULD affect the hash (and make Signers() return empty) + intent.Packet = json.RawMessage(`{"code": "sendTransactions2"}`) + hash, err = intent.Hash() + assert.Nil(t, err) + assert.NotNil(t, hash) + assert.NotEqual(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") + assert.Equal(t, intent.PacketCode(), "sendTransactions2") + + signers = intent.Signers() assert.Equal(t, 0, len(signers)) // Parsing the JSON without tabs, spaces, newlines, etc. should still work @@ -104,6 +163,8 @@ func TestECDSAP256SessionSig(t *testing.T) { message := "0x7a7e5a0913e63cac5886afcafedba93b17baae3eb4066534ffdd5e3da3e8c714" signature := "r1:0x4038376385b045c19754bb69fa6cde925674778e6a1a78b8fa3135ec96b695aef5b8126c78dc17a2cc0be522a4e6154bf5152c908d763fb1c28e47cf419a3ea5" + fmt.Println(len(sessionId)) + // get public key from sessionId sessionIdBuff := common.FromHex(sessionId[3:]) From 9210e57642a0bcc83f9b80732d1ce22bb9d438c4 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 14:01:09 +0100 Subject: [PATCH 21/28] Fix: openSession --- intents/packets/open_session.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go index b2bbfc7..7291285 100644 --- a/intents/packets/open_session.go +++ b/intents/packets/open_session.go @@ -7,8 +7,8 @@ import ( type OpenSessionPacket struct { BasePacket - SessionVerifier string `json:"sessionVerifier"` - Proof OpenSessionPacketProof `json:"proof"` + SessionId string `json:"sessionId"` + Proof OpenSessionPacketProof `json:"proof"` } type OpenSessionPacketProof struct { From e2374f3314d826e5a51d62e0e9e6838b417098dc Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 14:33:51 +0100 Subject: [PATCH 22/28] Fix: p256r1 signature check --- intents/intent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intents/intent.go b/intents/intent.go index 8d22fe6..4504aa0 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -214,7 +214,7 @@ func (intent *Intent) isValidSignatureP256R1(publicKey string, signature string) messageHash2 := sha256.Sum256(messageHash) // signature - signatureBytes := common.FromHex(signature[3:]) + signatureBytes := common.FromHex(signature) if len(signatureBytes) != 64 { return false } From 41507a8115cb3e749be8b16e3dbcc138f9f8fbeb Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 14:37:32 +0100 Subject: [PATCH 23/28] remove: OpenSessionPacketResponse --- intents/packets/open_session.go | 57 --------------------------------- 1 file changed, 57 deletions(-) diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go index 7291285..95d63ef 100644 --- a/intents/packets/open_session.go +++ b/intents/packets/open_session.go @@ -38,60 +38,3 @@ func (p *OpenSessionPacket) HasEmail() bool { func (p *OpenSessionPacket) HasIDToken() bool { return p.Proof.IDToken != "" } - -const OpenSessionPacketResponseCode = "sessionOpened" - -type OpenSessionPacketResponse struct { - Code string `json:"code"` - Data OpenSessionPacketResponseData `json:"data"` -} - -type OpenSessionPacketResponseData struct { - SessionId string `json:"sessionId"` - Wallet string `json:"wallet"` -} - -func (p *OpenSessionPacketResponse) UnmarshalFromAny(a any) error { - m, ok := a.(map[string]any) - if !ok { - return fmt.Errorf("packet is not a map") - } - - if code, ok := m["code"].(string); ok && code == OpenSessionPacketResponseCode { - p.Code = code - } else { - return fmt.Errorf("packet code is not '%s', got '%s'", OpenSessionPacketResponseCode, m["code"]) - } - - data, ok := m["data"].(map[string]any) - if !ok { - return fmt.Errorf("packet data is not a map") - } - - if sessionId, ok := data["sessionId"].(string); ok { - p.Data.SessionId = sessionId - } else { - return fmt.Errorf("packet data is missing sessionId") - } - - if wallet, ok := data["wallet"].(string); ok { - p.Data.Wallet = wallet - } else { - return fmt.Errorf("packet data is missing wallet") - } - - return nil -} - -func (p *OpenSessionPacketResponse) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != OpenSessionPacketResponseCode { - return fmt.Errorf("packet code is not '%s', got '%s'", OpenSessionPacketResponseCode, p.Code) - } - - return nil -} From d726072ee8276f1818fa139d068b90a87dc4da4f Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 15:46:10 +0100 Subject: [PATCH 24/28] Fix: intent test --- intents/intent_send_transaction_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intents/intent_send_transaction_test.go b/intents/intent_send_transaction_test.go index de433f4..d870529 100644 --- a/intents/intent_send_transaction_test.go +++ b/intents/intent_send_transaction_test.go @@ -84,7 +84,7 @@ func TestRecoverTransactionIntent(t *testing.T) { }, "signatures": [ { - "sessionId": "afaf60c0-67ba-4c9b-89ae-b115c78026a4", + "sessionId": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", "signature": "0xdd137166e6e73fcaa710e822aa3eef3d501ef1b7969d59e8583cb602a32233e0628d4e28ea5a562a1ccf6bd85bfccfcd1004673a28763640cca33002fbedbb3a1b" } ] From bc84c5c86fc3c99aed38e9eb7dc012518884e934 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 15:53:50 +0100 Subject: [PATCH 25/28] refactor Intent.isValidSignature --- intents/intent.go | 28 ++++++++++++++------------- intents/intent_test.go | 44 ------------------------------------------ 2 files changed, 15 insertions(+), 57 deletions(-) diff --git a/intents/intent.go b/intents/intent.go index 4504aa0..9e976cc 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -27,6 +27,7 @@ type KeyType int const ( KeyTypeSECP256K1 KeyType = iota KeyTypeSECP256R1 + KeyTypeUnknown ) type Intent struct { @@ -130,32 +131,33 @@ func (intent *Intent) IsValid() (bool, error) { return true, nil } -func (intent *Intent) isValidP256R1Session(sessionId string, signature string) bool { +func (intent *Intent) keyType(sessionId string) KeyType { + // handle empty session ids + if len(sessionId) == 0 { + return KeyTypeUnknown + } + // handle old session ids if len(sessionId) <= 42 { - return false + return KeyTypeSECP256K1 } // handle key typed session ids sessionIdBytes := common.FromHex(sessionId) - switch KeyType(sessionIdBytes[0]) { + return KeyType(sessionIdBytes[0]) +} + +func (intent *Intent) isValidSignature(sessionId string, signature string) bool { + switch intent.keyType(sessionId) { case KeyTypeSECP256K1: - return false + return intent.isValidSignatureP256K1(sessionId, signature) case KeyTypeSECP256R1: - return true + return intent.isValidSignatureP256R1(sessionId, signature) default: return false } } -func (intent *Intent) isValidSignature(session string, signature string) bool { - if intent.isValidP256R1Session(session, signature) { - return intent.isValidSignatureP256R1(session, signature) - } else { - return intent.isValidSignatureP256K1(session, signature) - } -} - // isValidSignatureP256K1 checks if the signature is valid for the given secp256k1 session func (intent *Intent) isValidSignatureP256K1(sessionAddress string, signature string) bool { // handle typed session address diff --git a/intents/intent_test.go b/intents/intent_test.go index 8bff319..0e2e1af 100644 --- a/intents/intent_test.go +++ b/intents/intent_test.go @@ -1,16 +1,10 @@ package intents import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/sha256" "encoding/json" - "fmt" - "math/big" "testing" "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" ) @@ -153,41 +147,3 @@ func TestParseAndRecoverIntent_SessionKeyP256K1Typed(t *testing.T) { assert.NotNil(t, hash2) assert.Equal(t, common.Bytes2Hex(hash2), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") } - -func TestECDSAP256SessionSig(t *testing.T) { - //sessionId := "r1:0x04d0ca179fec17ca13e0a05dfee4dd6e56c4f14975001ab093f6306f96f915c04d437047e1ca0244080fb2741d0518fe78c9e893c2a5b7d0549816198414c39335" - //message := "0x7b2270726f6a6563744964223a31312c226964546f6b656e223a2265794a68624763694f694a53557a49314e694973496d74705a434936496a67315a5455314d5441334e445932596a646c4d6a6b344d7a59784f546c6a4e54686a4e7a55344d575931596a6b794d324a6c4e4451694c434a30655841694f694a4b5631516966512e65794a7063334d694f694a6f64485277637a6f764c32466a59323931626e527a4c6d6476623264735a53356a623230694c434a68656e41694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a68645751694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a7a645749694f6949784d5451324f4455344d5467784e4441784f44597a4e446b304d4451694c434a6c6257467062434936496d3168636d6c75627a4d355147647459576c734c6d4e7662534973496d567459576c7358335a6c636d6c6d6157566b496a7030636e566c4c434a75596d59694f6a45334d4459334f44677a4f546773496d3568625755694f694a4e59584a6a6157346752384f7a636e70357859527a61326b694c434a7761574e3064584a6c496a6f696148523063484d364c79397361444d755a3239765a32786c64584e6c636d4e76626e526c626e5175593239744c32457651554e6e4f47396a536b464c623031435956424e5a3064494e3246355a6e644e54485236596931786431706f526b524b527a645656485a70556c705854566f7850584d354e69316a496977695a326c325a573566626d46745a534936496b3168636d4e7062694973496d5a6862576c73655639755957316c496a6f6952384f7a636e70357859527a61326b694c434a7362324e68624755694f694a7762434973496d6c68644349364d5463774e6a63344f4459354f4377695a586877496a6f784e7a41324e7a6b794d6a6b344c434a7164476b694f6949314e4759315a5745334d6a646a4f546b314f544a684e446b784e6d497a59574d31593245344f4755334e7a637a596a677a4e546b35496e302e4d346c73304f434d6a30586d4152354c7358385852707074644f55724e4a34346845364d476e4e664770334d444e495642396e7043524567386d64415033674f427171784e4d4e3464566d50734d334e5a54385779575a546871616e736254484a6d586e556d355245767357675f456d7954525a436d486545414c4e6877636e57477a386d375636664a7a7050366543554474534942722d376832756f364f637a6b704c534b382d74324569796b533773706c54475952486f42427265364d787762685044377074574a767957466c6f35594a617a74574c347454376c795957504264504470334456555743687679622d707944654e2d5066756530716a333165326b7448756c6b6a576b7a35325a5a73723758314876734164767642436838614c5f3553354f2d314a3649475f455f4e497168373254596f4141683968374e7639474c706e746a6d5a566764687769755f6f485067222c2273657373696f6e41646472657373223a2272313a307830346430636131373966656331376361313365306130356466656534646436653536633466313439373530303161623039336636333036663936663931356330346434333730343765316361303234343038306662323734316430353138666537386339653839336332613562376430353439383136313938343134633339333335222c22667269656e646c794e616d65223a22f09f95b72072697475616c2076696c6c616765222c22696e74656e744a534f4e223a227b5c2276657273696f6e5c223a5c22302e302e302d646576315c222c5c227061636b65745c223a7b5c226973737565645c223a313730363738383639392c5c22657870697265735c223a313730363738383939392c5c22636f64655c223a5c226f70656e53657373696f6e5c222c5c2273657373696f6e5c223a5c2272313a3078303464306361313739666563313763613133653061303564666565346464366535366334663134393735303031616230393366363330366639366639313563303464343337303437653163613032343430383066623237343164303531386665373863396538393363326135623764303534393831363139383431346333393333355c222c5c2270726f6f665c223a7b5c226964546f6b656e5c223a5c2265794a68624763694f694a53557a49314e694973496d74705a434936496a67315a5455314d5441334e445932596a646c4d6a6b344d7a59784f546c6a4e54686a4e7a55344d575931596a6b794d324a6c4e4451694c434a30655841694f694a4b5631516966512e65794a7063334d694f694a6f64485277637a6f764c32466a59323931626e527a4c6d6476623264735a53356a623230694c434a68656e41694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a68645751694f6949354e7a41354f4463334e5459324e6a41744d7a56684e6e526a4e44686f646d6b34593256324f574e75613235774d476c315a335935634739684d6a4d75595842776379356e6232396e624756316332567959323975644756756443356a623230694c434a7a645749694f6949784d5451324f4455344d5467784e4441784f44597a4e446b304d4451694c434a6c6257467062434936496d3168636d6c75627a4d355147647459576c734c6d4e7662534973496d567459576c7358335a6c636d6c6d6157566b496a7030636e566c4c434a75596d59694f6a45334d4459334f44677a4f546773496d3568625755694f694a4e59584a6a6157346752384f7a636e70357859527a61326b694c434a7761574e3064584a6c496a6f696148523063484d364c79397361444d755a3239765a32786c64584e6c636d4e76626e526c626e5175593239744c32457651554e6e4f47396a536b464c623031435956424e5a3064494e3246355a6e644e54485236596931786431706f526b524b527a645656485a70556c705854566f7850584d354e69316a496977695a326c325a573566626d46745a534936496b3168636d4e7062694973496d5a6862576c73655639755957316c496a6f6952384f7a636e70357859527a61326b694c434a7362324e68624755694f694a7762434973496d6c68644349364d5463774e6a63344f4459354f4377695a586877496a6f784e7a41324e7a6b794d6a6b344c434a7164476b694f6949314e4759315a5745334d6a646a4f546b314f544a684e446b784e6d497a59574d31593245344f4755334e7a637a596a677a4e546b35496e302e4d346c73304f434d6a30586d4152354c7358385852707074644f55724e4a34346845364d476e4e664770334d444e495642396e7043524567386d64415033674f427171784e4d4e3464566d50734d334e5a54385779575a546871616e736254484a6d586e556d355245767357675f456d7954525a436d486545414c4e6877636e57477a386d375636664a7a7050366543554474534942722d376832756f364f637a6b704c534b382d74324569796b533773706c54475952486f42427265364d787762685044377074574a767957466c6f35594a617a74574c347454376c795957504264504470334456555743687679622d707944654e2d5066756530716a333165326b7448756c6b6a576b7a35325a5a73723758314876734164767642436838614c5f3553354f2d314a3649475f455f4e497168373254596f4141683968374e7639474c706e746a6d5a566764687769755f6f4850675c227d7d2c5c227369676e6174757265735c223a5b5d7d227d" - //signature := "r1:0x5ca339d779449af10bc0dd74eb7e1ff6f9b584a52681bbb6dc27bb8923ec3ef629cdb9b17d4b6ed44fd125c4bb9376f869a2478513326613b03eb80a2bbfaeae" - - sessionId := "r1:0x040714f2ed82b5748ba30e3d81df81d481371b20c43cdbec81a89cbdb74e149e73ee083a1306328236c7de6d26b6f8d4494951d7423946422a04700ed182092a45" - message := "0x7a7e5a0913e63cac5886afcafedba93b17baae3eb4066534ffdd5e3da3e8c714" - signature := "r1:0x4038376385b045c19754bb69fa6cde925674778e6a1a78b8fa3135ec96b695aef5b8126c78dc17a2cc0be522a4e6154bf5152c908d763fb1c28e47cf419a3ea5" - - fmt.Println(len(sessionId)) - - // get public key from sessionId - sessionIdBuff := common.FromHex(sessionId[3:]) - - x, y := elliptic.Unmarshal(elliptic.P256(), sessionIdBuff) - assert.NotNil(t, x) - - pub := ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: x, - Y: y, - } - - spew.Dump(pub) - - // get message hash - messageBytes := common.FromHex(message) - messageHash := sha256.Sum256(messageBytes) - - // get signature - signatureBytes := common.FromHex(signature[3:]) - - r := new(big.Int).SetBytes(signatureBytes[:32]) - s := new(big.Int).SetBytes(signatureBytes[32:64]) - verified := ecdsa.Verify(&pub, messageHash[:], r, s) - assert.True(t, verified) -} From 84631e77620940d2ce9737a9c8f3d1b3372b2751 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Tue, 6 Feb 2024 15:57:37 +0100 Subject: [PATCH 26/28] refactor Intent.isValidSignature - 2 --- intents/intent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intents/intent.go b/intents/intent.go index 9e976cc..df01a50 100644 --- a/intents/intent.go +++ b/intents/intent.go @@ -138,7 +138,7 @@ func (intent *Intent) keyType(sessionId string) KeyType { } // handle old session ids - if len(sessionId) <= 42 { + if len(sessionId) == 42 { return KeyTypeSECP256K1 } From 68925526fe64c970322a150ffab38581424b6edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20G=C3=B3rzy=C5=84ski?= Date: Wed, 14 Feb 2024 09:50:11 +0100 Subject: [PATCH 27/28] waas new intent (#115) * new intent struct * new intent struct generate typescript * intent session signers * IntentTyped.ToIntent -> IntentTyped.AsIntent * fix: typo * Intent.isValidSignature to return error * applied review remarks * intent data validators support * update intent.ridl * add transaction extensions * sign message ext * rename * fix: tests * fix: the transaction structs, rem packet * move to intents & add responses stubs * intent.ridl update * Add missing delayedEncode transaction type * intent.ridl update * intent.ridl update * intent.ridl update * intent.ridl update * fix sig verification * revert: fix sig verification * intents: add comment & IntentTyped.AsIntent to IntentTyped.ToIntent --------- Co-authored-by: Patryk Kalinowski --- intents/intent.gen.go | 289 ++++++++++++ intents/intent.gen.ts | 424 ++++++++++++++++++ intents/intent.go | 235 ---------- intents/intent.ridl | 183 ++++++++ intents/intent_data_ext.go | 14 + ...age.go => intent_data_sign_message_ext.go} | 34 +- intents/intent_data_sign_message_ext_test.go | 67 +++ ...=> intent_data_transaction_delayed_abi.go} | 2 +- ...tent_data_transaction_delayed_abi_test.go} | 2 +- ...tion.go => intent_data_transaction_ext.go} | 169 +++---- ...go => intent_data_transaction_ext_test.go} | 86 ++-- intents/intent_ext.go | 231 ++++++++++ intents/intent_sign_message_test.go | 60 --- intents/intent_test.go | 149 ------ intents/intent_typed.go | 108 +++++ intents/intent_typed_test.go | 216 +++++++++ intents/packets/close_session.go | 26 -- intents/packets/finish_validate_session.go | 38 -- intents/packets/open_session.go | 40 -- intents/packets/packet.go | 39 -- intents/packets/validate_session.go | 71 --- intents/proto.go | 5 + intents/session.go | 119 +++++ 23 files changed, 1774 insertions(+), 833 deletions(-) create mode 100644 intents/intent.gen.go create mode 100644 intents/intent.gen.ts delete mode 100644 intents/intent.go create mode 100644 intents/intent.ridl create mode 100644 intents/intent_data_ext.go rename intents/{packets/sign_message.go => intent_data_sign_message_ext.go} (52%) create mode 100644 intents/intent_data_sign_message_ext_test.go rename intents/{packets/delayed_abi.go => intent_data_transaction_delayed_abi.go} (99%) rename intents/{packets/delayed_abi_test.go => intent_data_transaction_delayed_abi_test.go} (99%) rename intents/{packets/send_transaction.go => intent_data_transaction_ext.go} (53%) rename intents/{intent_send_transaction_test.go => intent_data_transaction_ext_test.go} (77%) create mode 100644 intents/intent_ext.go delete mode 100644 intents/intent_sign_message_test.go delete mode 100644 intents/intent_test.go create mode 100644 intents/intent_typed.go create mode 100644 intents/intent_typed_test.go delete mode 100644 intents/packets/close_session.go delete mode 100644 intents/packets/finish_validate_session.go delete mode 100644 intents/packets/open_session.go delete mode 100644 intents/packets/packet.go delete mode 100644 intents/packets/validate_session.go create mode 100644 intents/proto.go create mode 100644 intents/session.go diff --git a/intents/intent.gen.go b/intents/intent.gen.go new file mode 100644 index 0000000..06b5473 --- /dev/null +++ b/intents/intent.gen.go @@ -0,0 +1,289 @@ +// sequence-waas-intents v0.1.0 5f79da1499857910e938fa990736008e8c0e3bc5 +// -- +// Code generated by webrpc-gen@v0.14.0-dev with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=intent.ridl -target=golang -pkg=intents -client -out=./intent.gen.go +package intents + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v0.1.0" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "5f79da1499857910e938fa990736008e8c0e3bc5" +} + +// +// Types +// + +type Intent struct { + Version string `json:"version"` + Name string `json:"name"` + ExpiresAt uint64 `json:"expiresAt"` + IssuedAt uint64 `json:"issuedAt"` + Data interface{} `json:"data"` + Signatures []*Signature `json:",omitempty"` +} + +type Signature struct { + SessionID string `json:"sessionId"` + Signature string `json:"signature"` +} + +type IntentDataOpenSession struct { + SessionID string `json:"sessionId"` + Email *string `json:",omitempty"` + IdToken *string `json:",omitempty"` +} + +type IntentDataCloseSession struct { + SessionID string `json:"sessionId"` +} + +type IntentDataValidateSession struct { + SessionID string `json:"sessionId"` + Wallet string `json:"wallet"` + DeviceMetadata string `json:"deviceMetadata"` +} + +type IntentDataFinishValidateSession struct { + SessionID string `json:"sessionId"` + Wallet string `json:"wallet"` + Salt string `json:"salt"` + Challenge string `json:"challenge"` +} + +type IntentDataListSessions struct { + Wallet string `json:"wallet"` +} + +type IntentDataGetSession struct { + SessionID string `json:"sessionId"` + Wallet string `json:"wallet"` +} + +type IntentDataSignMessage struct { + Network string `json:"network"` + Wallet string `json:"wallet"` + Message string `json:"message"` +} + +type IntentDataSendTransaction struct { + Network string `json:"network"` + Wallet string `json:"wallet"` + Identifier string `json:"identifier"` + Transactions []json.RawMessage `json:"transactions"` +} + +type TransactionRaw struct { + Type string `json:"type"` + To string `json:"to"` + Value string `json:"value"` + Data string `json:"data"` +} + +type TransactionERC20 struct { + Type string `json:"type"` + TokenAddress string `json:"tokenAddress"` + To string `json:"to"` + Value string `json:"value"` +} + +type TransactionERC721 struct { + Type string `json:"type"` + TokenAddress string `json:"tokenAddress"` + To string `json:"to"` + Id string `json:"id"` + Safe *bool `json:"safe"` + Data *string `json:"data"` +} + +type TransactionERC1155Value struct { + ID string `json:"id"` + Amount string `json:"amount"` +} + +type TransactionDelayedEncode struct { + Type string `json:"type"` + To string `json:"to"` + Value string `json:"value"` + Data json.RawMessage `json:"data"` +} + +type TransactionERC1155 struct { + Type string `json:"type"` + TokenAddress string `json:"tokenAddress"` + To string `json:"to"` + Vals []*TransactionERC1155Value `json:"vals"` + Data *string `json:"data"` +} + +type IntentResponse struct { + Code string `json:"code"` + Data interface{} `json:"data"` +} + +type IntentResponseSessionOpened struct { + SessionID string `json:"sessionId"` + Wallet string `json:"wallet"` +} + +type IntentResponseSessionClosed struct { +} + +type IntentResponseValidateSession struct { +} + +type IntentResponseValidationRequired struct { + SessionID string `json:"sessionId"` +} + +type IntentResponseValidationStarted struct { + Salt string `json:"salt"` +} + +type IntentResponseValidationFinished struct { + IsValid bool `json:"isValid"` +} + +type IntentResponseListSessions struct { + Sessions []string `json:"sessions"` +} + +type IntentResponseGetSession struct { + SessionID string `json:"sessionId"` + Wallet string `json:"wallet"` + Validated bool `json:"validated"` +} + +type IntentResponseSignedMessage struct { + Signature string `json:"signature"` + Message string `json:"message"` +} + +type IntentResponseTransactionReceipt struct { + Request interface{} `json:"request"` + TxHash string `json:"txHash"` + MetaTxHash string `json:"metaTxHash"` + Receipt interface{} `json:"receipt"` + NativeReceipt interface{} `json:"nativeReceipt"` + Simulations interface{} `json:"simulations"` +} + +type IntentResponseTransactionFailed struct { + Error string `json:"error"` + Request interface{} `json:"request"` + Simulations interface{} `json:"simulations"` +} + +// +// Helpers +// + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) diff --git a/intents/intent.gen.ts b/intents/intent.gen.ts new file mode 100644 index 0000000..f209b3d --- /dev/null +++ b/intents/intent.gen.ts @@ -0,0 +1,424 @@ +/* eslint-disable */ +// sequence-waas-intents v0.1.0 5f79da1499857910e938fa990736008e8c0e3bc5 +// -- +// Code generated by webrpc-gen@v0.14.0-dev with typescript generator. DO NOT EDIT. +// +// webrpc-gen -schema=intent.ridl -target=typescript -client -out=./intent.gen.ts + +// WebRPC description and code-gen version +export const WebRPCVersion = "v1" + +// Schema version of your RIDL schema +export const WebRPCSchemaVersion = "v0.1.0" + +// Schema hash generated from your RIDL schema +export const WebRPCSchemaHash = "5f79da1499857910e938fa990736008e8c0e3bc5" + +// +// Types +// + + +export interface Intent { + version: string + name: string + expiresAt: number + issuedAt: number + data: any + signatures: Array +} + +export interface Signature { + sessionId: string + signature: string +} + +export interface IntentDataOpenSession { + sessionId: string + email?: string + idToken?: string +} + +export interface IntentDataCloseSession { + sessionId: string +} + +export interface IntentDataValidateSession { + sessionId: string + wallet: string + deviceMetadata: string +} + +export interface IntentDataFinishValidateSession { + sessionId: string + wallet: string + salt: string + challenge: string +} + +export interface IntentDataListSessions { + wallet: string +} + +export interface IntentDataGetSession { + sessionId: string + wallet: string +} + +export interface IntentDataSignMessage { + network: string + wallet: string + message: string +} + +export interface IntentDataSendTransaction { + network: string + wallet: string + identifier: string + transactions: Array +} + +export interface TransactionRaw { + type: string + to: string + value: string + data: string +} + +export interface TransactionERC20 { + type: string + tokenAddress: string + to: string + value: string +} + +export interface TransactionERC721 { + type: string + tokenAddress: string + to: string + id: string + safe?: boolean + data?: string +} + +export interface TransactionERC1155Value { + id: string + amount: string +} + +export interface TransactionDelayedEncode { + type: string + to: string + value: string + data: any +} + +export interface TransactionERC1155 { + type: string + tokenAddress: string + to: string + vals: Array + data?: string +} + +export interface IntentResponse { + code: string + data: any +} + +export interface IntentResponseSessionOpened { + sessionId: string + wallet: string +} + +export interface IntentResponseSessionClosed { +} + +export interface IntentResponseValidateSession { +} + +export interface IntentResponseValidationRequired { + sessionId: string +} + +export interface IntentResponseValidationStarted { + salt: string +} + +export interface IntentResponseValidationFinished { + isValid: boolean +} + +export interface IntentResponseListSessions { + sessions: Array +} + +export interface IntentResponseGetSession { + sessionId: string + wallet: string + validated: boolean +} + +export interface IntentResponseSignedMessage { + signature: string + message: string +} + +export interface IntentResponseTransactionReceipt { + request: any + txHash: string + metaTxHash: string + receipt: any + nativeReceipt: any + simulations: any +} + +export interface IntentResponseTransactionFailed { + error: string + request: any + simulations: any +} + + + + const createHTTPRequest = (body: object = {}, headers: object = {}, signal: AbortSignal | null = null): object => { + return { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}), + signal + } +} + +const buildResponse = (res: Response): Promise => { + return res.text().then(text => { + let data + try { + data = JSON.parse(text) + } catch(error) { + let message = '' + if (error instanceof Error) { + message = error.message + } + throw WebrpcBadResponseError.new({ + status: res.status, + cause: `JSON.parse(): ${message}: response text: ${text}`}, + ) + } + if (!res.ok) { + const code: number = (typeof data.code === 'number') ? data.code : 0 + throw (webrpcErrorByCode[code] || WebrpcError).new(data) + } + return data + }) +} + +// +// Errors +// + +export class WebrpcError extends Error { + name: string + code: number + message: string + status: number + cause?: string + + /** @deprecated Use message instead of msg. Deprecated in webrpc v0.11.0. */ + msg: string + + constructor(name: string, code: number, message: string, status: number, cause?: string) { + super(message) + this.name = name || 'WebrpcError' + this.code = typeof code === 'number' ? code : 0 + this.message = message || `endpoint error ${this.code}` + this.msg = this.message + this.status = typeof status === 'number' ? status : 0 + this.cause = cause + Object.setPrototypeOf(this, WebrpcError.prototype) + } + + static new(payload: any): WebrpcError { + return new this(payload.error, payload.code, payload.message || payload.msg, payload.status, payload.cause) + } +} + +// Webrpc errors + +export class WebrpcEndpointError extends WebrpcError { + constructor( + name: string = 'WebrpcEndpoint', + code: number = 0, + message: string = 'endpoint error', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcEndpointError.prototype) + } +} + +export class WebrpcRequestFailedError extends WebrpcError { + constructor( + name: string = 'WebrpcRequestFailed', + code: number = -1, + message: string = 'request failed', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcRequestFailedError.prototype) + } +} + +export class WebrpcBadRouteError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRoute', + code: number = -2, + message: string = 'bad route', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRouteError.prototype) + } +} + +export class WebrpcBadMethodError extends WebrpcError { + constructor( + name: string = 'WebrpcBadMethod', + code: number = -3, + message: string = 'bad method', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadMethodError.prototype) + } +} + +export class WebrpcBadRequestError extends WebrpcError { + constructor( + name: string = 'WebrpcBadRequest', + code: number = -4, + message: string = 'bad request', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadRequestError.prototype) + } +} + +export class WebrpcBadResponseError extends WebrpcError { + constructor( + name: string = 'WebrpcBadResponse', + code: number = -5, + message: string = 'bad response', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcBadResponseError.prototype) + } +} + +export class WebrpcServerPanicError extends WebrpcError { + constructor( + name: string = 'WebrpcServerPanic', + code: number = -6, + message: string = 'server panic', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcServerPanicError.prototype) + } +} + +export class WebrpcInternalErrorError extends WebrpcError { + constructor( + name: string = 'WebrpcInternalError', + code: number = -7, + message: string = 'internal error', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcInternalErrorError.prototype) + } +} + +export class WebrpcClientDisconnectedError extends WebrpcError { + constructor( + name: string = 'WebrpcClientDisconnected', + code: number = -8, + message: string = 'client disconnected', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcClientDisconnectedError.prototype) + } +} + +export class WebrpcStreamLostError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamLost', + code: number = -9, + message: string = 'stream lost', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamLostError.prototype) + } +} + +export class WebrpcStreamFinishedError extends WebrpcError { + constructor( + name: string = 'WebrpcStreamFinished', + code: number = -10, + message: string = 'stream finished', + status: number = 0, + cause?: string + ) { + super(name, code, message, status, cause) + Object.setPrototypeOf(this, WebrpcStreamFinishedError.prototype) + } +} + + +// Schema errors + + +export enum errors { + WebrpcEndpoint = 'WebrpcEndpoint', + WebrpcRequestFailed = 'WebrpcRequestFailed', + WebrpcBadRoute = 'WebrpcBadRoute', + WebrpcBadMethod = 'WebrpcBadMethod', + WebrpcBadRequest = 'WebrpcBadRequest', + WebrpcBadResponse = 'WebrpcBadResponse', + WebrpcServerPanic = 'WebrpcServerPanic', + WebrpcInternalError = 'WebrpcInternalError', + WebrpcClientDisconnected = 'WebrpcClientDisconnected', + WebrpcStreamLost = 'WebrpcStreamLost', + WebrpcStreamFinished = 'WebrpcStreamFinished', +} + +const webrpcErrorByCode: { [code: number]: any } = { + [0]: WebrpcEndpointError, + [-1]: WebrpcRequestFailedError, + [-2]: WebrpcBadRouteError, + [-3]: WebrpcBadMethodError, + [-4]: WebrpcBadRequestError, + [-5]: WebrpcBadResponseError, + [-6]: WebrpcServerPanicError, + [-7]: WebrpcInternalErrorError, + [-8]: WebrpcClientDisconnectedError, + [-9]: WebrpcStreamLostError, + [-10]: WebrpcStreamFinishedError, +} + +export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise + diff --git a/intents/intent.go b/intents/intent.go deleted file mode 100644 index df01a50..0000000 --- a/intents/intent.go +++ /dev/null @@ -1,235 +0,0 @@ -package intents - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/sha256" - "encoding/json" - "fmt" - "math/big" - "strings" - - "github.com/0xsequence/go-sequence/intents/packets" - "github.com/gibson042/canonicaljson-go" - - "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/0xsequence/ethkit/go-ethereum/crypto" -) - -var ( - ErrInvalidPacket = fmt.Errorf("invalid packet") - ErrNoSignatures = fmt.Errorf("no signatures") - ErrInvalidSignature = fmt.Errorf("invalid signature") -) - -type KeyType int - -const ( - KeyTypeSECP256K1 KeyType = iota - KeyTypeSECP256R1 - KeyTypeUnknown -) - -type Intent struct { - Version string `json:"version"` - Packet json.RawMessage `json:"packet"` - - // signatures is made private to prevent it from being accidentally used raw - // by the user. The Signers() method should be used instead, it will return - // *only* the valid signatures. - signatures []Signature -} - -type JSONIntent struct { - Version string `json:"version"` - Packet json.RawMessage `json:"packet"` - Signatures []Signature `json:"signatures"` -} - -type Signature struct { - SessionId string `json:"sessionId"` - Signature string `json:"signature"` -} - -func (i *Intent) UnmarshalJSON(data []byte) error { - var intent JSONIntent - err := json.Unmarshal(data, &intent) - if err != nil { - return err - } - i.Version = intent.Version - i.Packet = intent.Packet - i.signatures = intent.Signatures - return nil -} - -func (i *Intent) MarshalJSON() ([]byte, error) { - return json.Marshal(JSONIntent{ - Version: i.Version, - Packet: i.Packet, - Signatures: i.signatures, - }) -} - -func (intent *Intent) Hash() ([]byte, error) { - packet := intent.Packet - - // Convert packet to bytes - packetBytes, err := canonicaljson.Marshal(packet) - if err != nil { - return nil, err - } - - // Calculate keccak256 hash - return crypto.Keccak256(packetBytes), nil -} - -func (intent *Intent) Signers() []string { - var signers []string - - for _, signature := range intent.signatures { - if intent.isValidSignature(signature.SessionId, signature.Signature) { - signers = append(signers, signature.SessionId) - } - } - - return signers -} - -func (intent *Intent) IsValid() (bool, error) { - // Check if the packet is valid - var packet packets.BasePacket - err := json.Unmarshal(intent.Packet, &packet) - if err != nil { - return false, fmt.Errorf("intent: %w", ErrInvalidPacket) - } - - // OpenSession packets do not require signatures - if packet.Code == packets.OpenSessionPacketCode { - if ok, err := packet.IsValid(); !ok { - return false, fmt.Errorf("intent: %w", err) - } - return true, nil - } - - // Check if there are any signatures - if len(intent.signatures) == 0 { - return false, fmt.Errorf("intent: %w", ErrNoSignatures) - } - - // Check if all signatures are valid - for _, signature := range intent.signatures { - if !intent.isValidSignature(signature.SessionId, signature.Signature) { - return false, fmt.Errorf("intent: %w", ErrInvalidSignature) - } - } - - // Check if the packet is valid - if ok, err := packet.IsValid(); !ok { - return false, fmt.Errorf("intent: %w", err) - } - return true, nil -} - -func (intent *Intent) keyType(sessionId string) KeyType { - // handle empty session ids - if len(sessionId) == 0 { - return KeyTypeUnknown - } - - // handle old session ids - if len(sessionId) == 42 { - return KeyTypeSECP256K1 - } - - // handle key typed session ids - sessionIdBytes := common.FromHex(sessionId) - return KeyType(sessionIdBytes[0]) -} - -func (intent *Intent) isValidSignature(sessionId string, signature string) bool { - switch intent.keyType(sessionId) { - case KeyTypeSECP256K1: - return intent.isValidSignatureP256K1(sessionId, signature) - case KeyTypeSECP256R1: - return intent.isValidSignatureP256R1(sessionId, signature) - default: - return false - } -} - -// isValidSignatureP256K1 checks if the signature is valid for the given secp256k1 session -func (intent *Intent) isValidSignatureP256K1(sessionAddress string, signature string) bool { - // handle typed session address - if len(sessionAddress) > 42 { - sessionAddressBytes := common.FromHex(sessionAddress) - sessionAddress = fmt.Sprintf("0x%s", common.Bytes2Hex(sessionAddressBytes[1:])) - } - - // Get hash of the packet - hash, err := intent.Hash() - if err != nil { - return false - } - - // Add Ethereum prefix to the hash - prefixedHash := crypto.Keccak256Hash([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash))) - - // Convert the signature to bytes - sigBytes := common.FromHex(signature) - if sigBytes[64] == 27 || sigBytes[64] == 28 { - sigBytes[64] -= 27 - } - - // Recover the public key from the signature - pubKey, err := crypto.Ecrecover(prefixedHash.Bytes(), sigBytes) - if err != nil { - return false - } - - addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) - - // Check if the recovered address matches the session address - return strings.ToLower(addr.Hex()) == strings.ToLower(sessionAddress) -} - -// isValidSignatureP256R1 checks if the signature is valid for the given secp256r1 session -func (intent *Intent) isValidSignatureP256R1(publicKey string, signature string) bool { - publicKeyBuff := common.FromHex(publicKey)[1:] - - // public key - // TODO: check if can use ecdh instead of unmarshal - // NOTE: no way to convert ecdh pub key into elliptic pub key? - x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBuff) - if x == nil || y == nil { - return false - } - - pub := ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: x, - Y: y, - } - - // message hash - messageHash, _ := intent.Hash() - messageHash2 := sha256.Sum256(messageHash) - - // signature - signatureBytes := common.FromHex(signature) - if len(signatureBytes) != 64 { - return false - } - - r := new(big.Int).SetBytes(signatureBytes[:32]) - s := new(big.Int).SetBytes(signatureBytes[32:64]) - return ecdsa.Verify(&pub, messageHash2[:], r, s) -} - -func (intent *Intent) PacketCode() string { - var packetCode struct { - Code string `json:"code"` - } - json.Unmarshal(intent.Packet, &packetCode) - return packetCode.Code -} diff --git a/intents/intent.ridl b/intents/intent.ridl new file mode 100644 index 0000000..8c77961 --- /dev/null +++ b/intents/intent.ridl @@ -0,0 +1,183 @@ +webrpc = v1 + +name = sequence-waas-intents +version = v0.1.0 + +struct Intent + - version: string + - name: string + - expiresAt: uint64 + - issuedAt: uint64 + - data: any + - signatures: []Signature + + go.tag.json = ,omitempty + +struct Signature + - sessionId: string + + go.field.name = SessionID + - signature: string + +# no way to generate string enums +# enum IntentName: string +# - openSession +# - closeSession +# - validateSession +# - finishValidateSession +# - listSessions +# - getSession +# - signMessage +# - sendTransaction + +struct IntentDataOpenSession + - sessionId: string + + go.field.name = SessionID + - email?: string + + go.tag.json = ,omitempty + - idToken?: string + + go.tag.json = ,omitempty + +struct IntentDataCloseSession + - sessionId: string + + go.field.name = SessionID + +struct IntentDataValidateSession + - sessionId: string + + go.field.name = SessionID + - wallet: string + - deviceMetadata: string + +struct IntentDataFinishValidateSession + - sessionId: string + + go.field.name = SessionID + - wallet: string + - salt: string + - challenge: string + +struct IntentDataListSessions + - wallet: string + + +struct IntentDataGetSession + - sessionId: string + + go.field.name = SessionID + - wallet: string + +struct IntentDataSignMessage + - network: string + - wallet: string + - message: string + +struct IntentDataSendTransaction + - network: string + - wallet: string + - identifier: string # is used to generate nonce space + - transactions: []any + + go.field.type = []json.RawMessage + +struct TransactionRaw + - type: string + - to: string + - value: string + - data: string + +# no way to generate string enums +#enum TransactionType: string +# - transaction +# - erc20send +# - erc721send +# - erc1155send +# - delayedEncode + +struct TransactionERC20 + - type: string + - tokenAddress: string + - to: string + - value: string + +struct TransactionERC721 + - type: string + - tokenAddress: string + - to: string + - id: string + - safe?: bool + - data?: string + +struct TransactionERC1155Value + - id: string + + go.field.name = ID + - amount: string + +struct TransactionDelayedEncode + - type: string + - to: string + - value: string + - data: any + + go.field.type = json.RawMessage + +struct TransactionERC1155 + - type: string + - tokenAddress: string + - to: string + - vals: []TransactionERC1155Value + - data?: string + +struct IntentResponse + - code: string + - data: any + +# no way to generate string enums +#enum IntentResponseCode: string +# - sessionOpened +# - validationRequired +# - validationStarted +# - validationFinished +# - signedMessage +# - transactionReceipt +# - transactionFailed +# - getSessionResponse + +struct IntentResponseSessionOpened + - sessionId: string + + go.field.name = SessionID + - wallet: string + +struct IntentResponseSessionClosed + +struct IntentResponseValidateSession + +struct IntentResponseValidationRequired + - sessionId: string + + go.field.name = SessionID + +struct IntentResponseValidationStarted + - salt: string + +struct IntentResponseValidationFinished + - isValid: bool + +struct IntentResponseListSessions + - sessions: []string + +struct IntentResponseGetSession + - sessionId: string + + go.field.name = SessionID + - wallet: string + - validated: bool + +struct IntentResponseSignedMessage + - signature: string + - message: string + +struct IntentResponseTransactionReceipt + - request: any + - txHash: string + - metaTxHash: string + - receipt: any + - nativeReceipt: any + - simulations: any + +struct IntentResponseTransactionFailed + - error: string + - request: any + - simulations: any + diff --git a/intents/intent_data_ext.go b/intents/intent_data_ext.go new file mode 100644 index 0000000..5cbb2c4 --- /dev/null +++ b/intents/intent_data_ext.go @@ -0,0 +1,14 @@ +package intents + +import "fmt" + +func (id *IntentDataOpenSession) IsValid() error { + if id.SessionID == "" { + return fmt.Errorf("session id is empty") + } + + if id.IdToken == nil && id.Email == nil { + return fmt.Errorf("idToken and email are both nil") + } + return nil +} diff --git a/intents/packets/sign_message.go b/intents/intent_data_sign_message_ext.go similarity index 52% rename from intents/packets/sign_message.go rename to intents/intent_data_sign_message_ext.go index 8b69b9b..c49a511 100644 --- a/intents/packets/sign_message.go +++ b/intents/intent_data_sign_message_ext.go @@ -1,8 +1,7 @@ -package packets +package intents import ( "bytes" - "encoding/json" "fmt" "math/big" @@ -10,28 +9,7 @@ import ( "github.com/0xsequence/go-sequence" ) -type SignMessagePacket struct { - BasePacketForWallet - Network string `json:"network"` - Message string `json:"message"` -} - -const SignMessagePacketCode = "signMessage" - -func (p *SignMessagePacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != SignMessagePacketCode { - return fmt.Errorf("packet code is not '%s', got '%s'", SignMessagePacketCode, p.Code) - } - - return nil -} - -func (p *SignMessagePacket) chainID() (*big.Int, error) { +func (p *IntentDataSignMessage) chainID() (*big.Int, error) { n, ok := sequence.ParseHexOrDec(p.Network) if !ok { return nil, fmt.Errorf("invalid network id '%s'", p.Network) @@ -40,15 +18,15 @@ func (p *SignMessagePacket) chainID() (*big.Int, error) { return n, nil } -func (p *SignMessagePacket) message() []byte { +func (p *IntentDataSignMessage) message() []byte { return common.FromHex(p.Message) } -func (p *SignMessagePacket) wallet() common.Address { +func (p *IntentDataSignMessage) wallet() common.Address { return common.HexToAddress(p.Wallet) } -func (p *SignMessagePacket) subdigest() ([]byte, error) { +func (p *IntentDataSignMessage) subdigest() ([]byte, error) { chainID, err := p.chainID() if err != nil { return nil, err @@ -60,7 +38,7 @@ func (p *SignMessagePacket) subdigest() ([]byte, error) { // A SignMessagePacket (intent) *MUST* be mapped to a regular "SignMessage" Sequence action, this means that // it must adhere to the following rules: // - the subdigest must match `SubDigest(chainID, Wallet, Digest(Message))` -func (p *SignMessagePacket) IsValidInterpretation(subdigest common.Hash) bool { +func (p *IntentDataSignMessage) IsValidInterpretation(subdigest common.Hash) bool { selfSubDigest, err := p.subdigest() if err != nil { return false diff --git a/intents/intent_data_sign_message_ext_test.go b/intents/intent_data_sign_message_ext_test.go new file mode 100644 index 0000000..7409954 --- /dev/null +++ b/intents/intent_data_sign_message_ext_test.go @@ -0,0 +1,67 @@ +package intents + +import ( + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/0xsequence/ethkit/ethwallet" + "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/go-sequence" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRecoverMessageIntent(t *testing.T) { + data := `{ + "version": "1", + "name": "signMessage", + "issued": 0, + "expires": 0, + "data": { + "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", + "network": "1", + "message": "0xdeadbeef" + }, + "signatures": [] + }` + + intent := &Intent{} + err := json.Unmarshal([]byte(data), intent) + assert.Nil(t, err) + + assert.Equal(t, "1", intent.Version) + assert.Equal(t, "signMessage", intent.Name) + + hash, err := intent.Hash() + assert.Nil(t, err) + assert.NotNil(t, common.Bytes2Hex(hash)) + + intent.IssuedAt = uint64(time.Now().Unix()) + intent.ExpiresAt = uint64(time.Now().Unix()) + 60 + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.Nil(t, err) + + session := NewSessionP256K1(wallet) + + err = session.Sign(intent) + require.Nil(t, err) + + intentTyped, err := NewIntentTypedFromIntent[IntentDataSignMessage](intent) + require.NoError(t, err) + + signers := intent.Signers() + assert.Equal(t, 1, len(signers)) + assert.Equal(t, "0x"+common.Bytes2Hex(append([]byte{0x00}, wallet.Address().Bytes()...)), signers[0]) + + subdigest, err := sequence.SubDigest( + big.NewInt(1), + common.HexToAddress("0xD67FC48b298B09Ed3D03403d930769C527186c4e"), + sequence.MessageDigest(common.Hex2Bytes("deadbeef")), + ) + + assert.Nil(t, err) + assert.True(t, intentTyped.Data.IsValidInterpretation(common.BytesToHash(subdigest))) +} diff --git a/intents/packets/delayed_abi.go b/intents/intent_data_transaction_delayed_abi.go similarity index 99% rename from intents/packets/delayed_abi.go rename to intents/intent_data_transaction_delayed_abi.go index 0e14198..4c04645 100644 --- a/intents/packets/delayed_abi.go +++ b/intents/intent_data_transaction_delayed_abi.go @@ -1,4 +1,4 @@ -package packets +package intents import ( "encoding/json" diff --git a/intents/packets/delayed_abi_test.go b/intents/intent_data_transaction_delayed_abi_test.go similarity index 99% rename from intents/packets/delayed_abi_test.go rename to intents/intent_data_transaction_delayed_abi_test.go index 9a216e5..da1315b 100644 --- a/intents/packets/delayed_abi_test.go +++ b/intents/intent_data_transaction_delayed_abi_test.go @@ -1,4 +1,4 @@ -package packets +package intents import ( "encoding/json" diff --git a/intents/packets/send_transaction.go b/intents/intent_data_transaction_ext.go similarity index 53% rename from intents/packets/send_transaction.go rename to intents/intent_data_transaction_ext.go index d6cda08..6d02c34 100644 --- a/intents/packets/send_transaction.go +++ b/intents/intent_data_transaction_ext.go @@ -1,4 +1,4 @@ -package packets +package intents import ( "bytes" @@ -13,30 +13,7 @@ import ( "github.com/0xsequence/go-sequence/relayer/proto" ) -type SendTransactionsPacket struct { - BasePacketForWallet - Identifier string `json:"identifier"` - Wallet string `json:"wallet"` - Network string `json:"network"` - Transactions []json.RawMessage `json:"transactions"` -} - -const SendTransactionCode = "sendTransaction" - -func (p *SendTransactionsPacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != SendTransactionCode { - return fmt.Errorf("packet code is not '%s', got '%s'", SendTransactionCode, p.Code) - } - - return nil -} - -func (p *SendTransactionsPacket) chainID() (*big.Int, error) { +func (p *IntentDataSendTransaction) chainID() (*big.Int, error) { n, ok := sequence.ParseHexOrDec(p.Network) if !ok { return nil, fmt.Errorf("invalid network id '%s'", p.Network) @@ -45,7 +22,7 @@ func (p *SendTransactionsPacket) chainID() (*big.Int, error) { return n, nil } -func (p *SendTransactionsPacket) wallet() common.Address { +func (p *IntentDataSendTransaction) wallet() common.Address { return common.HexToAddress(p.Wallet) } @@ -55,38 +32,34 @@ type ExpectedValuesForTransaction struct { Data []byte } -func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) (*ExpectedValuesForTransaction, error) { - // Get the subpacket type - var subpacketType struct { +func (p *IntentDataSendTransaction) ExpectedValuesFor(txRaw *json.RawMessage) (*ExpectedValuesForTransaction, error) { + // Get the tx type + var baseTransactionType struct { Type string `json:"type"` } - err := json.Unmarshal(*subpacket, &subpacketType) + err := json.Unmarshal(*txRaw, &baseTransactionType) if err != nil { return nil, err } - switch subpacketType.Type { + switch baseTransactionType.Type { case "transaction": - // This packet explicitly defines the transaction values - var subpacketTransactionType struct { - To string `json:"to"` - Value string `json:"value"` - Data string `json:"data"` - } + // This struct explicitly defines the transaction values + var tx TransactionRaw - err := json.Unmarshal(*subpacket, &subpacketTransactionType) + err := json.Unmarshal(*txRaw, &tx) if err != nil { return nil, err } - to := common.HexToAddress(subpacketTransactionType.To) - value, ok := sequence.ParseHexOrDec(subpacketTransactionType.Value) + to := common.HexToAddress(tx.To) + value, ok := sequence.ParseHexOrDec(tx.Value) if !ok { - return nil, fmt.Errorf("invalid value '%s'", subpacketTransactionType.Value) + return nil, fmt.Errorf("invalid value '%s'", tx.Value) } - data := common.FromHex(subpacketTransactionType.Data) + data := common.FromHex(tx.Data) return &ExpectedValuesForTransaction{ To: &to, @@ -95,25 +68,21 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( }, nil case "erc20send": - // This packet defines the transaction values for an ERC20 transfer + // This struct defines the transaction values for an ERC20 transfer // so this should be an ABI encoded transfer call to `to`. The `value` // field must be 0. - var subpacketERC20SendType struct { - Token string `json:"token"` - To string `json:"to"` - Value string `json:"value"` - } + var tx TransactionERC20 - err := json.Unmarshal(*subpacket, &subpacketERC20SendType) + err := json.Unmarshal(*txRaw, &tx) if err != nil { return nil, err } - to := common.HexToAddress(subpacketERC20SendType.To) - token := common.HexToAddress(subpacketERC20SendType.Token) - value, ok := sequence.ParseHexOrDec(subpacketERC20SendType.Value) + to := common.HexToAddress(tx.To) + token := common.HexToAddress(tx.TokenAddress) + value, ok := sequence.ParseHexOrDec(tx.Value) if !ok { - return nil, fmt.Errorf("invalid value '%s'", subpacketERC20SendType.Value) + return nil, fmt.Errorf("invalid value '%s'", tx.Value) } // Encode the transfer call @@ -129,39 +98,36 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( }, nil case "erc721send": - // This packet defines the transaction values for an ERC721 transfer + // This struct defines the transaction values for an ERC721 transfer // so this should be an ABI encoded transfer call to `to`. The `value` // field must be 0. - var subpacketERC721SendType struct { - Token string `json:"token"` - To string `json:"to"` - ID string `json:"id"` - Safe bool `json:"safe,omitempty"` - Data string `json:"data,omitempty"` - } + var tx TransactionERC721 // Safe defaults to false - if err := json.Unmarshal(*subpacket, &subpacketERC721SendType); err != nil { + if err := json.Unmarshal(*txRaw, &tx); err != nil { return nil, err } // If data is not empty, then safe *must* be true - to := common.HexToAddress(subpacketERC721SendType.To) - token := common.HexToAddress(subpacketERC721SendType.Token) - id, ok := sequence.ParseHexOrDec(subpacketERC721SendType.ID) + to := common.HexToAddress(tx.To) + token := common.HexToAddress(tx.TokenAddress) + id, ok := sequence.ParseHexOrDec(tx.Id) if !ok { - return nil, fmt.Errorf("invalid id '%s'", subpacketERC721SendType.ID) + return nil, fmt.Errorf("invalid id '%s'", tx.Id) + } + var data []byte + if tx.Data != nil { + data = common.FromHex(*tx.Data) } - data := common.FromHex(subpacketERC721SendType.Data) // If data is not empty, then safe *must* be true - if len(data) > 0 && !subpacketERC721SendType.Safe { + if len(data) > 0 && tx.Safe != nil && !*tx.Safe { return nil, fmt.Errorf("safe must be true if data is not empty") } var encodedData []byte - if subpacketERC721SendType.Safe { + if tx.Safe != nil && *tx.Safe { // Encode the safe transfer call encodedData, err = ethcoder.AbiEncodeMethodCalldata("safeTransferFrom(address,address,uint256,bytes)", []interface{}{p.wallet(), to, id, data}) if err != nil { @@ -182,33 +148,23 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( }, nil case "erc1155send": - // This packet defines the transaction values for an ERC1155 transfer + // This struct defines the transaction values for an ERC1155 transfer // so this should be an ABI encoded transfer call to `to`. The `value` // field must be 0. - type subpacketERC1155SendValsType struct { - ID string `json:"id"` - Amount string `json:"amount"` - } + var tx TransactionERC1155 - var subpacketERC1155SendType struct { - Token string `json:"token"` - To string `json:"to"` - Vals []subpacketERC1155SendValsType `json:"vals"` - Data string `json:"data,omitempty"` - } - - err := json.Unmarshal(*subpacket, &subpacketERC1155SendType) + err := json.Unmarshal(*txRaw, &tx) if err != nil { return nil, err } - to := common.HexToAddress(subpacketERC1155SendType.To) - token := common.HexToAddress(subpacketERC1155SendType.Token) + to := common.HexToAddress(tx.To) + token := common.HexToAddress(tx.TokenAddress) var parsedIDs []*big.Int var parsedAmounts []*big.Int - for _, val := range subpacketERC1155SendType.Vals { + for _, val := range tx.Vals { id, ok := sequence.ParseHexOrDec(val.ID) if !ok { return nil, fmt.Errorf("invalid id '%s'", val.ID) @@ -223,7 +179,10 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( parsedAmounts = append(parsedAmounts, amount) } - data := common.FromHex(subpacketERC1155SendType.Data) + var data []byte + if tx.Data != nil { + data = common.FromHex(*tx.Data) + } encodedData, err := ethcoder.AbiEncodeMethodCalldata("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", []interface{}{p.wallet(), to, parsedIDs, parsedAmounts, data}) @@ -238,19 +197,15 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( }, nil case "delayedEncode": - var subpacketDelayedEncodeType struct { - To string `json:"to"` - Value string `json:"value"` - Data json.RawMessage `json:"data"` - } + var tx TransactionDelayedEncode - err := json.Unmarshal(*subpacket, &subpacketDelayedEncodeType) + err := json.Unmarshal(*txRaw, &tx) if err != nil { return nil, err } nst := &delayedEncodeType{} - err = json.Unmarshal(subpacketDelayedEncodeType.Data, nst) + err = json.Unmarshal(tx.Data, nst) if err != nil { return nil, err } @@ -260,10 +215,10 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( return nil, err } - to := common.HexToAddress(subpacketDelayedEncodeType.To) - value, ok := sequence.ParseHexOrDec(subpacketDelayedEncodeType.Value) + to := common.HexToAddress(tx.To) + value, ok := sequence.ParseHexOrDec(tx.Value) if !ok { - return nil, fmt.Errorf("invalid value '%s'", subpacketDelayedEncodeType.Value) + return nil, fmt.Errorf("invalid value '%s'", tx.Value) } return &ExpectedValuesForTransaction{ @@ -272,11 +227,11 @@ func (p *SendTransactionsPacket) ExpectedValuesFor(subpacket *json.RawMessage) ( Data: common.FromHex(encoded), }, nil default: - return nil, fmt.Errorf("invalid subpacket type '%s'", subpacketType.Type) + return nil, fmt.Errorf("invalid transaction type '%s'", baseTransactionType.Type) } } -func (p *SendTransactionsPacket) Nonce() (*big.Int, error) { +func (p *IntentDataSendTransaction) Nonce() (*big.Int, error) { // Hash the identifier, it will be used as the nonce // space. The nonce number is always 0. hashed := ethcoder.Keccak256([]byte(p.Identifier)) @@ -285,7 +240,7 @@ func (p *SendTransactionsPacket) Nonce() (*big.Int, error) { return sequence.EncodeNonce(big.NewInt(0).SetBytes(hashed[:20]), common.Big0) } -func (p *SendTransactionsPacket) IsValidInterpretation(subdigest common.Hash, txns sequence.Transactions, nonce *big.Int) bool { +func (p *IntentDataSendTransaction) IsValidInterpretation(subdigest common.Hash, txns sequence.Transactions, nonce *big.Int) bool { // Nonce must be the expected one // (defined by the identifier) enonce, err := p.Nonce() @@ -319,7 +274,7 @@ func (p *SendTransactionsPacket) IsValidInterpretation(subdigest common.Hash, tx return false } - // Now check that every transaction maps 1:1 to the transactions in the packet + // Now check that every transaction maps 1:1 to the transactions in the intent // meaning that they follow the intent signed by it if len(txns) != len(p.Transactions) { return false @@ -354,11 +309,11 @@ func (p *SendTransactionsPacket) IsValidInterpretation(subdigest common.Hash, tx type SendTransactionResponse struct { Code string `json:"code"` Data struct { - Request *SendTransactionsPacket `json:"request"` - TxHash string `json:"txHash"` - Receipt *proto.MetaTxnReceipt `json:"receipt"` - NativeReceipt *types.Receipt `json:"nativeReceipt"` - Simulations []*proto.SimulateResult `json:"simulations"` + Request *IntentDataSendTransaction `json:"request"` + TxHash string `json:"txHash"` + Receipt *proto.MetaTxnReceipt `json:"receipt"` + NativeReceipt *types.Receipt `json:"nativeReceipt"` + Simulations []*proto.SimulateResult `json:"simulations"` } } @@ -367,8 +322,8 @@ const SendTransactionResponseCode = "transactionReceipt" type SendTransactionFailed struct { Code string `json:"code"` Data struct { - Request *SendTransactionsPacket `json:"request"` - Simulations []*proto.SimulateResult `json:"simulations"` + Request *IntentDataSendTransaction `json:"request"` + Simulations []*proto.SimulateResult `json:"simulations"` } } diff --git a/intents/intent_send_transaction_test.go b/intents/intent_data_transaction_ext_test.go similarity index 77% rename from intents/intent_send_transaction_test.go rename to intents/intent_data_transaction_ext_test.go index d870529..0f338ee 100644 --- a/intents/intent_send_transaction_test.go +++ b/intents/intent_data_transaction_ext_test.go @@ -4,24 +4,27 @@ import ( "encoding/json" "math/big" "testing" + "time" + "github.com/0xsequence/ethkit/ethwallet" "github.com/0xsequence/ethkit/go-ethereum/common" "github.com/0xsequence/go-sequence" - "github.com/0xsequence/go-sequence/intents/packets" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRecoverTransactionIntent(t *testing.T) { data := `{ "version": "1", - "packet": { + "name": "sendTransaction", + "issued": 0, + "expires": 0, + "data": { "code": "sendTransaction", "identifier": "test-identifier", - "issued": 1600000000, - "expires": 1600086400, - "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", - "network": "10", - "transactions": [ + "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", + "network": "10", + "transactions": [ { "type": "transaction", "to": "0x479F6a5b0C1728947318714963a583C56A78366A", @@ -30,13 +33,13 @@ func TestRecoverTransactionIntent(t *testing.T) { }, { "type": "erc20send", - "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "to": "0x7b1Bd3474D789e18e2E329E2c53F819B6E687b4A", "value": "1000" }, { "type": "erc721send", - "token": "0xF87E31492Faf9A91B02Ee0dEAAd50d51d56D5d4d", + "tokenAddress": "0xF87E31492Faf9A91B02Ee0dEAAd50d51d56D5d4d", "to": "0x17fFA2d95b58228e1ECb0C6Ac25A6EfD20BA08E4", "id": "7", "safe": true, @@ -44,7 +47,7 @@ func TestRecoverTransactionIntent(t *testing.T) { }, { "type": "erc1155send", - "token": "0x631998e91476da5b870d741192fc5cbc55f5a52e", + "tokenAddress": "0x631998e91476da5b870d741192fc5cbc55f5a52e", "to": "0x91E8aC543C5fEDf9F3Ef8b9dA1500dB84305681F", "vals": [ { @@ -82,12 +85,7 @@ func TestRecoverTransactionIntent(t *testing.T) { } ] }, - "signatures": [ - { - "sessionId": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", - "signature": "0xdd137166e6e73fcaa710e822aa3eef3d501ef1b7969d59e8583cb602a32233e0628d4e28ea5a562a1ccf6bd85bfccfcd1004673a28763640cca33002fbedbb3a1b" - } - ] + "signatures": [] }` intent := &Intent{} @@ -95,24 +93,36 @@ func TestRecoverTransactionIntent(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "1", intent.Version) - assert.Equal(t, "sendTransaction", intent.PacketCode()) + assert.Equal(t, "sendTransaction", intent.Name) hash, err := intent.Hash() assert.Nil(t, err) - assert.Equal(t, common.Bytes2Hex(hash), "2feb22d5631075041c5aaafce98da8950d706a9eca8d9ea2b28ea95142d8e890") + assert.NotNil(t, common.Bytes2Hex(hash)) + + intent.IssuedAt = uint64(time.Now().Unix()) + intent.ExpiresAt = uint64(time.Now().Unix()) + 60 + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.Nil(t, err) + + session := NewSessionP256K1(wallet) + + err = session.Sign(intent) + require.Nil(t, err) + + intentTyped, err := NewIntentTypedFromIntent[IntentDataSendTransaction](intent) + assert.Nil(t, err) signers := intent.Signers() assert.Equal(t, 1, len(signers)) - assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) + assert.Equal(t, "0x"+common.Bytes2Hex(append([]byte{0x00}, wallet.Address().Bytes()...)), signers[0]) - packet := &packets.SendTransactionsPacket{} - err = packet.Unmarshal(intent.Packet) - assert.Nil(t, err) + require.NoError(t, intentTyped.IsValid()) - assert.Equal(t, "sendTransaction", packet.Code) + sendTransactionData := intentTyped.Data // Generate transactions as sequence.Wallet would - nonce, err := packet.Nonce() + nonce, err := sendTransactionData.Nonce() assert.Nil(t, err) chainID := big.NewInt(10) @@ -178,41 +188,41 @@ func TestRecoverTransactionIntent(t *testing.T) { ) assert.Nil(t, err) - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) // changing any transaction value should invalidate the interpretation for i := range transactions { prev := transactions[i].Value transactions[i].Value = big.NewInt(123) - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) transactions[i].Value = prev - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) } // changing any transaction data should invalidate the interpretation for i := range transactions { prev := transactions[i].Data transactions[i].Data = common.Hex2Bytes("0x1234") - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) transactions[i].Data = prev - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) } // changing any to address should invalidate the interpretation for i := range transactions { prev := transactions[i].To transactions[i].To = common.HexToAddress("0xd1333D70A344c26041a869077381209462e586F8") - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) transactions[i].To = prev - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) } // setting any delegate call should invalidate the interpretation for i := range transactions { transactions[i].DelegateCall = true - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) transactions[i].DelegateCall = false - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) } // changing revert on error should NOT invalidate the interpretation @@ -232,7 +242,7 @@ func TestRecoverTransactionIntent(t *testing.T) { ) assert.Nil(t, err) - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(nxtsubdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(nxtsubdigest), transactions, nonce)) transactions[i].RevertOnError = true } @@ -254,7 +264,7 @@ func TestRecoverTransactionIntent(t *testing.T) { ) assert.Nil(t, err) - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(nxtsubdigest), transactions, nonce)) + assert.True(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(nxtsubdigest), transactions, nonce)) transactions[i].GasLimit = prev } @@ -272,10 +282,10 @@ func TestRecoverTransactionIntent(t *testing.T) { nxtdigest, ) assert.Nil(t, err) - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(nxtsubdigest), transactions, big.NewInt(123))) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(nxtsubdigest), transactions, big.NewInt(123))) // removing a transaction should invalidate the interpretation - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions[1:], nonce)) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions[1:], nonce)) // adding an extra transaction should invalidate the interpretation transactions = append(transactions, &sequence.Transaction{ @@ -287,5 +297,5 @@ func TestRecoverTransactionIntent(t *testing.T) { Data: common.FromHex("0x3251ba32"), }) - assert.False(t, packet.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) + assert.False(t, sendTransactionData.IsValidInterpretation(common.BytesToHash(subdigest), transactions, nonce)) } diff --git a/intents/intent_ext.go b/intents/intent_ext.go new file mode 100644 index 0000000..a16cf4f --- /dev/null +++ b/intents/intent_ext.go @@ -0,0 +1,231 @@ +package intents + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "fmt" + "math/big" + "strings" + "time" + + "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/ethkit/go-ethereum/crypto" + "github.com/gibson042/canonicaljson-go" +) + +const IntentValidTimeInSec = 60 +const IntentAllowedTimeDriftInSec = 5 + +type KeyType int + +const ( + KeyTypeSECP256K1 KeyType = iota + KeyTypeSECP256R1 + KeyTypeUnknown +) + +const ( + IntentNameOpenSession = "openSession" + IntentNameCloseSession = "closeSession" + IntentNameValidateSession = "validateSession" + IntentNameFinishValidateSession = "finishValidateSession" + IntentNameListSessions = "listSessions" + IntentNameGetSession = "getSession" + IntentNameSignMessage = "signMessage" + IntentNameSendTransaction = "sendTransaction" +) + +const ( + IntentResponseCodeSessionOpened = "sessionOpened" + IntentResponseCodeValidationRequired = "validationRequired" + IntentResponseCodeValidationStarted = "validationStarted" + IntentResponseCodeValidationFinished = "validationFinished" + IntentResponseCodeSignedMessage = "signedMessage" + IntentResponseCodeTransactionReceipt = "transactionReceipt" + IntentResponseCodeTransactionFailed = "transactionFailed" + IntentResponseCodeGetSessionResponse = "getSessionResponse" +) + +type IntentDataValidator interface { + IsValid() error +} + +func (intent *Intent) Hash() ([]byte, error) { + // copy intent and remove signatures + var intentCopy = *intent + intentCopy.Signatures = nil + + // Convert packet to bytes + packetBytes, err := canonicaljson.Marshal(intentCopy) + if err != nil { + return nil, err + } + + // Calculate keccak256 hash + return crypto.Keccak256(packetBytes), nil +} + +func (intent *Intent) IsValid() error { + if len(intent.Signatures) == 0 { + return fmt.Errorf("no signatures") + } + + // check if the intent is expired + if intent.ExpiresAt+IntentAllowedTimeDriftInSec < uint64(time.Now().Unix()) { + return fmt.Errorf("intent expired") + } + + // check if the intent is issued in the future + if intent.IssuedAt-IntentAllowedTimeDriftInSec > uint64(time.Now().Unix()) { + return fmt.Errorf("intent issued in the future") + } + + // check if at least one signature is valid + if len(intent.Signers()) == 0 { + return fmt.Errorf("invalid signature") + } + + // the intent is valid + return nil +} + +func (intent *Intent) keyType(sessionId string) KeyType { + // handle empty session ids + if len(sessionId) == 0 { + return KeyTypeUnknown + } + + // handle old session ids + if len(sessionId) == 42 { + return KeyTypeSECP256K1 + } + + // handle key typed session ids + sessionIdBytes := common.FromHex(sessionId) + return KeyType(sessionIdBytes[0]) +} + +func (intent *Intent) isValidSignature(sessionId string, signature string) error { + switch intent.keyType(sessionId) { + case KeyTypeSECP256K1: + return intent.isValidSignatureP256K1(sessionId, signature) + case KeyTypeSECP256R1: + return intent.isValidSignatureP256R1(sessionId, signature) + default: + return fmt.Errorf("unknown session key type") + } +} + +// isValidSignatureP256K1 checks if the signature is valid for the given secp256k1 session +func (intent *Intent) isValidSignatureP256K1(sessionId string, signature string) error { + // validate session address and signature + if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionId, "0x") { + // invalid params + return fmt.Errorf("invalid sessionId or signature format") + } + + // validate session address + sessionAddressBytes := common.FromHex(sessionId) + if len(sessionAddressBytes) != 21 && len(sessionAddressBytes) != 20 { + // invalid session address + return fmt.Errorf("invalid sessionId length") + } + + // validate signature + sigBytes := common.FromHex(signature) + if len(sigBytes) != 65 { + return fmt.Errorf("invalid signature length") + } + + // handle typed session address + if len(sessionAddressBytes) == 21 { + sessionAddressBytes = sessionAddressBytes[1:] + } + + sessionAddress := fmt.Sprintf("0x%s", common.Bytes2Hex(sessionAddressBytes)) + + // Get hash of the packet + hash, err := intent.Hash() + if err != nil { + return fmt.Errorf("failed to hash intent: %w", err) + } + + // Add Ethereum prefix to the hash + prefixedHash := crypto.Keccak256Hash([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash))) + + // handle recovery byte + if sigBytes[64] == 27 || sigBytes[64] == 28 { + sigBytes[64] -= 27 + } + + // Recover the public key from the signature + pubKey, err := crypto.Ecrecover(prefixedHash.Bytes(), sigBytes) + if err != nil { + return fmt.Errorf("failed to recover public key: %w", err) + } + + addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) + + // Check if the recovered address matches the session address + if strings.ToLower(addr.Hex()) != strings.ToLower(sessionAddress) { + return fmt.Errorf("invalid signature") + } + return nil +} + +// isValidSignatureP256R1 checks if the signature is valid for the given secp256r1 session +func (intent *Intent) isValidSignatureP256R1(sessionId string, signature string) error { + // validate session address and signature + if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionId, "0x") { + // invalid params + return fmt.Errorf("invalid sessionId or signature format") + } + + // validate session id + sessionIdBytes := common.FromHex(sessionId) + if len(sessionIdBytes) != 66 { + return fmt.Errorf("invalid sessionId length") + } + + // validate signature + signatureBytes := common.FromHex(signature) + if len(signatureBytes) != 64 { + return fmt.Errorf("invalid signature length") + } + + // message hash + messageHash, _ := intent.Hash() + messageHash2 := sha256.Sum256(messageHash) + + // public key + publicKeyBuff := common.FromHex(sessionId)[1:] + x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBuff) + if x == nil || y == nil { + return fmt.Errorf("invalid public key") + } + + pub := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + // signature + r := new(big.Int).SetBytes(signatureBytes[:32]) + s := new(big.Int).SetBytes(signatureBytes[32:64]) + if !ecdsa.Verify(&pub, messageHash2[:], r, s) { + return fmt.Errorf("invalid signature") + } + return nil +} + +func (intent *Intent) Signers() []string { + var signers []string + for _, signature := range intent.Signatures { + if err := intent.isValidSignature(signature.SessionID, signature.Signature); err == nil { + signers = append(signers, signature.SessionID) + } + } + return signers +} diff --git a/intents/intent_sign_message_test.go b/intents/intent_sign_message_test.go deleted file mode 100644 index 71448b0..0000000 --- a/intents/intent_sign_message_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package intents - -import ( - "encoding/json" - "math/big" - "testing" - - "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/0xsequence/go-sequence" - "github.com/0xsequence/go-sequence/intents/packets" - "github.com/stretchr/testify/assert" -) - -func TestRecoverMessageIntent(t *testing.T) { - data := `{ - "version": "1.0.0", - "packet": { - "code": "signMessage", - "issued": 1600000000, - "expires": 1600086400, - "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", - "network": "1", - "message": "0xdeadbeef" - }, - "signatures": [{ - "sessionId": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", - "signature": "0x827b2a2afbf4a8a79e761fdb26e567b519a56a06e897dce5517b3ccfb408b55f20aaba276c1dade28112f51fe7262fbd0508da0019c0f8582c41b2be451ddede1b" - }] - }` - - intent := &Intent{} - err := json.Unmarshal([]byte(data), intent) - assert.Nil(t, err) - - assert.Equal(t, "1.0.0", intent.Version) - assert.Equal(t, "signMessage", intent.PacketCode()) - - hash, err := intent.Hash() - assert.Nil(t, err) - assert.Equal(t, common.Bytes2Hex(hash), "5b15538a25716e951630dde1cf38ae056d764976145d1134576461203a621ddb") - - signers := intent.Signers() - assert.Equal(t, 1, len(signers)) - assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) - - packet := &packets.SignMessagePacket{} - err = packet.Unmarshal(intent.Packet) - assert.Nil(t, err) - - assert.Equal(t, "signMessage", packet.Code) - - subdigest, err := sequence.SubDigest( - big.NewInt(1), - common.HexToAddress("0xD67FC48b298B09Ed3D03403d930769C527186c4e"), - sequence.MessageDigest(common.Hex2Bytes("deadbeef")), - ) - - assert.Nil(t, err) - assert.True(t, packet.IsValidInterpretation(common.BytesToHash(subdigest))) -} diff --git a/intents/intent_test.go b/intents/intent_test.go deleted file mode 100644 index 0e2e1af..0000000 --- a/intents/intent_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package intents - -import ( - "encoding/json" - "testing" - - "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/stretchr/testify/assert" -) - -func TestParseAndRecoverIntent(t *testing.T) { - data := `{ - "version": "1.0.0", - "packet": { - "code": "sendTransactions", - "identifier": "test-identifier", - "issued": 1600000000, - "expires": 1600086400, - "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", - "network": "1", - "transactions": [{ - "type": "erc20send", - "token": "0x0000000000000000000000000000000000000000", - "to": "0x0dc9603d4da53841C1C83f3B550C6143e60e0425", - "value": "0" - }] - }, - "signatures": [{ - "sessionId": "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", - "signature": "0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c" - }] - }` - - intent := &Intent{} - err := json.Unmarshal([]byte(data), intent) - assert.Nil(t, err) - - assert.Equal(t, "1.0.0", intent.Version) - - hash, err := intent.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash) - assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - - signers := intent.Signers() - assert.Equal(t, 1, len(signers)) - assert.Equal(t, "0x1111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) - assert.Equal(t, intent.PacketCode(), "sendTransactions") - - // Changing the version should not affect the hash - intent.Version = "2.0.0" - hash, err = intent.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash) - assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - - // Changing the packet code SHOULD affect the hash (and make Signers() return empty) - intent.Packet = json.RawMessage(`{"code": "sendTransactions2"}`) - hash, err = intent.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash) - assert.NotEqual(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - assert.Equal(t, intent.PacketCode(), "sendTransactions2") - - signers = intent.Signers() - assert.Equal(t, 0, len(signers)) - - // Parsing the JSON without tabs, spaces, newlines, etc. should still work - // and produce the same hash - data2 := `{"signatures":[{"signature":"0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c","session":"0x1111BD4F3233e7a7f552AdAf32C910fD30de598B"}],"version":"1.0.0","packet":{"transactions":[{"token":"0x0000000000000000000000000000000000000000","value":"0","type":"erc20send","to":"0x0dc9603d4da53841C1C83f3B550C6143e60e0425"}],"wallet":"0xD67FC48b298B09Ed3D03403d930769C527186c4e","expires":1600086400,"code":"sendTransactions","network":"1","identifier":"test-identifier","issued":1600000000}}` - intent2 := &Intent{} - err = json.Unmarshal([]byte(data2), intent2) - assert.Nil(t, err) - - hash2, err := intent2.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash2) - assert.Equal(t, common.Bytes2Hex(hash2), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") -} - -func TestParseAndRecoverIntent_SessionKeyP256K1Typed(t *testing.T) { - data := `{ - "version": "1.0.0", - "packet": { - "code": "sendTransactions", - "identifier": "test-identifier", - "issued": 1600000000, - "expires": 1600086400, - "wallet": "0xD67FC48b298B09Ed3D03403d930769C527186c4e", - "network": "1", - "transactions": [{ - "type": "erc20send", - "token": "0x0000000000000000000000000000000000000000", - "to": "0x0dc9603d4da53841C1C83f3B550C6143e60e0425", - "value": "0" - }] - }, - "signatures": [{ - "sessionId": "0x001111BD4F3233e7a7f552AdAf32C910fD30de598B", - "signature": "0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c" - }] - }` - - intent := &Intent{} - err := json.Unmarshal([]byte(data), intent) - assert.Nil(t, err) - - assert.Equal(t, "1.0.0", intent.Version) - - hash, err := intent.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash) - assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - - signers := intent.Signers() - assert.Equal(t, 1, len(signers)) - assert.Equal(t, "0x001111BD4F3233e7a7f552AdAf32C910fD30de598B", signers[0]) - assert.Equal(t, intent.PacketCode(), "sendTransactions") - - // Changing the version should not affect the hash - intent.Version = "2.0.0" - hash, err = intent.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash) - assert.Equal(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - - // Changing the packet code SHOULD affect the hash (and make Signers() return empty) - intent.Packet = json.RawMessage(`{"code": "sendTransactions2"}`) - hash, err = intent.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash) - assert.NotEqual(t, common.Bytes2Hex(hash), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") - assert.Equal(t, intent.PacketCode(), "sendTransactions2") - - signers = intent.Signers() - assert.Equal(t, 0, len(signers)) - - // Parsing the JSON without tabs, spaces, newlines, etc. should still work - // and produce the same hash - data2 := `{"signatures":[{"signature":"0xcca6253c4fd281247ddd0fa487252ef91932eaec8d68b61f0901ccaa70345bf66fdbbd98ed3e3c9752f9e35ef2a7bc88dd9c8ae23c594241b476fe988824ab881c","session":"0x1111BD4F3233e7a7f552AdAf32C910fD30de598B"}],"version":"1.0.0","packet":{"transactions":[{"token":"0x0000000000000000000000000000000000000000","value":"0","type":"erc20send","to":"0x0dc9603d4da53841C1C83f3B550C6143e60e0425"}],"wallet":"0xD67FC48b298B09Ed3D03403d930769C527186c4e","expires":1600086400,"code":"sendTransactions","network":"1","identifier":"test-identifier","issued":1600000000}}` - intent2 := &Intent{} - err = json.Unmarshal([]byte(data2), intent2) - assert.Nil(t, err) - - hash2, err := intent2.Hash() - assert.Nil(t, err) - assert.NotNil(t, hash2) - assert.Equal(t, common.Bytes2Hex(hash2), "893060f818437f8e3d9b4d8e103c5eb3c325fa25dd0221fb7b61cca6dd03a79e") -} diff --git a/intents/intent_typed.go b/intents/intent_typed.go new file mode 100644 index 0000000..3a38eae --- /dev/null +++ b/intents/intent_typed.go @@ -0,0 +1,108 @@ +package intents + +import ( + "encoding/json" + "fmt" + "time" +) + +func IntentDataTypeToName[T any](t *T) string { + var data any = t + switch data.(type) { + case *IntentDataOpenSession: + return IntentNameOpenSession + case *IntentDataCloseSession: + return IntentNameCloseSession + case *IntentDataValidateSession: + return IntentNameValidateSession + case *IntentDataFinishValidateSession: + return IntentNameFinishValidateSession + case *IntentDataListSessions: + return IntentNameListSessions + case *IntentDataGetSession: + return IntentNameGetSession + case *IntentDataSignMessage: + return IntentNameSignMessage + case *IntentDataSendTransaction: + return IntentNameSendTransaction + default: + return "" + } +} + +type IntentTyped[T any] struct { + Intent + Data T +} + +func NewIntentTyped[T any](data T) *IntentTyped[T] { + return &IntentTyped[T]{ + Intent: Intent{ + Version: "1", + ExpiresAt: uint64(time.Now().Unix()) + IntentValidTimeInSec, + IssuedAt: uint64(time.Now().Unix()), + Name: IntentDataTypeToName(&data), + Data: data, + }, + Data: data, + } +} + +func NewIntentTypedFromIntent[T any](intent *Intent) (*IntentTyped[T], error) { + switch intent.Data.(type) { + case T: + return &IntentTyped[T]{ + Intent: *intent, + Data: intent.Data.(T), + }, nil + case map[string]any: + data := intent.Data.(map[string]any) + + // convert to json + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + + // convert to typed data + var typedData T + err = json.Unmarshal(dataJSON, &typedData) + if err != nil { + return nil, err + } + + // check if intent name and data type match + if IntentDataTypeToName(&typedData) != intent.Name { + return nil, fmt.Errorf("intent name and data type mismatch") + } + + return &IntentTyped[T]{ + Intent: *intent, + Data: typedData, + }, nil + default: + return nil, fmt.Errorf("invalid intent data type") + } +} + +func (i *IntentTyped[T]) IsValid() error { + // check if the intent is valid + if err := i.Intent.IsValid(); err != nil { + return err + } + + // check if the intent data is valid + var data any = &i.Data + if validator, ok := data.(IntentDataValidator); ok { + if err := validator.IsValid(); err != nil { + return fmt.Errorf("invalid intent data: %w", err) + } + } + // the intent is valid + return nil +} + +func (i *IntentTyped[T]) ToIntent() *Intent { + i.Intent.Data = i.Data + return &i.Intent +} diff --git a/intents/intent_typed_test.go b/intents/intent_typed_test.go new file mode 100644 index 0000000..161b275 --- /dev/null +++ b/intents/intent_typed_test.go @@ -0,0 +1,216 @@ +package intents + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/0xsequence/ethkit" + "github.com/0xsequence/ethkit/ethwallet" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntentNewIntentTyped(t *testing.T) { + t.Run(IntentNameOpenSession, func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234"}) + assert.Equal(t, IntentNameOpenSession, intent.Name) + }) + + t.Run(IntentNameCloseSession, func(t *testing.T) { + intent := NewIntentTyped(IntentDataCloseSession{SessionID: "0x1234"}) + assert.Equal(t, IntentNameCloseSession, intent.Name) + }) + + t.Run(IntentNameValidateSession, func(t *testing.T) { + intent := NewIntentTyped(IntentDataValidateSession{SessionID: "0x1234"}) + assert.Equal(t, IntentNameValidateSession, intent.Name) + }) + + t.Run(IntentNameFinishValidateSession, func(t *testing.T) { + intent := NewIntentTyped(IntentDataFinishValidateSession{SessionID: "0x1234"}) + assert.Equal(t, IntentNameFinishValidateSession, intent.Name) + }) + + t.Run(IntentNameListSessions, func(t *testing.T) { + intent := NewIntentTyped(IntentDataListSessions{}) + assert.Equal(t, IntentNameListSessions, intent.Name) + }) + + t.Run(IntentNameGetSession, func(t *testing.T) { + intent := NewIntentTyped(IntentDataGetSession{SessionID: "0x1234"}) + assert.Equal(t, IntentNameGetSession, intent.Name) + }) + + t.Run(IntentNameSignMessage, func(t *testing.T) { + intent := NewIntentTyped(IntentDataSignMessage{Network: "ethereum", Message: "0x1234"}) + assert.Equal(t, IntentNameSignMessage, intent.Name) + }) + + t.Run(IntentNameSendTransaction, func(t *testing.T) { + intent := NewIntentTyped(IntentDataSendTransaction{}) + assert.Equal(t, IntentNameSendTransaction, intent.Name) + }) + + t.Run("unknown", func(t *testing.T) { + intent := NewIntentTyped(map[string]interface{}{}) + assert.Equal(t, "", intent.Name) + }) +} + +func TestIntentNewIntentTypedFromIntent(t *testing.T) { + t.Run("openSession", func(t *testing.T) { + intent := Intent{ + Version: "1", + Name: IntentNameOpenSession, + Data: map[string]interface{}{"sessionId": "0x1234"}, + } + + intentTyped, err := NewIntentTypedFromIntent[IntentDataOpenSession](&intent) + assert.NoError(t, err) + assert.Equal(t, "openSession", intentTyped.Name) + }) + + t.Run("openSessionNameMismatch", func(t *testing.T) { + intent := Intent{ + Version: "1", + Name: "openSession2", + Data: map[string]interface{}{"sessionId": "0x1234"}, + } + + _, err := NewIntentTypedFromIntent[IntentDataCloseSession](&intent) + assert.ErrorContains(t, err, "mismatch") + }) +} + +func TestIntentIsValid(t *testing.T) { + + t.Run("valid", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("valid_p256k1Signature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("valid_p256r1Signature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + session := NewSessionP256R1(privateKey) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("valid_legacySignature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + err = SignIntentWithWalletLegacy(wallet, intent) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("expired", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + intent.ExpiresAt = 0 + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.ErrorContains(t, intent.IsValid(), "expired") + }) + + t.Run("issuedInFuture", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + intent.IssuedAt = uint64(1 << 63) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.ErrorContains(t, intent.IsValid(), "issued in the future") + }) + + t.Run("noSignatures", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + assert.ErrorContains(t, intent.IsValid(), "no signatures") + }) + + t.Run("invalidSignature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + intent.Signatures[0].Signature = "0x1234" + + assert.ErrorContains(t, intent.IsValid(), "invalid signature") + }) +} + +func TestIntentDataValidator(t *testing.T) { + t.Run("valid", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("invalid", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234"}) + + wallet, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + session := NewSessionP256K1(wallet) + err = session.Sign(intent.ToIntent()) + require.NoError(t, err) + + assert.ErrorContains(t, intent.IsValid(), "invalid intent data") + }) +} diff --git a/intents/packets/close_session.go b/intents/packets/close_session.go deleted file mode 100644 index 4aef3a7..0000000 --- a/intents/packets/close_session.go +++ /dev/null @@ -1,26 +0,0 @@ -package packets - -import ( - "encoding/json" - "fmt" -) - -type CloseSessionPacket struct { - BasePacketForWallet - SessionId string `json:"sessionId"` -} - -const CloseSessionPacketCode = "closeSession" - -func (p *CloseSessionPacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != CloseSessionPacketCode { - return fmt.Errorf("packet code is not '%s', got '%s'", CloseSessionPacketCode, p.Code) - } - - return nil -} diff --git a/intents/packets/finish_validate_session.go b/intents/packets/finish_validate_session.go deleted file mode 100644 index 6fec3cc..0000000 --- a/intents/packets/finish_validate_session.go +++ /dev/null @@ -1,38 +0,0 @@ -package packets - -import ( - "encoding/json" - "fmt" -) - -type FinishValidateSessionPacket struct { - Code string `json:"code"` - SessionId string `json:"sessionId"` - Salt string `json:"salt"` - Challenge string `json:"challenge"` -} - -const FinishValidateSessionPacketCode = "finishValidateSession" - -func (p *FinishValidateSessionPacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != FinishValidateSessionPacketCode { - return fmt.Errorf("packet code is not '%s', got '%s'", FinishValidateSessionPacketCode, p.Code) - } - - return nil -} - -const FinishValidateSessionPacketResponseCode = "finishedSessionValidation" - -type FinishedSessionValidationPacketResponse struct { - Code string `json:"code"` - - Data struct { - IsValid bool `json:"isValid"` - } -} diff --git a/intents/packets/open_session.go b/intents/packets/open_session.go deleted file mode 100644 index 95d63ef..0000000 --- a/intents/packets/open_session.go +++ /dev/null @@ -1,40 +0,0 @@ -package packets - -import ( - "encoding/json" - "fmt" -) - -type OpenSessionPacket struct { - BasePacket - SessionId string `json:"sessionId"` - Proof OpenSessionPacketProof `json:"proof"` -} - -type OpenSessionPacketProof struct { - IDToken string `json:"idToken"` - Email string `json:"email"` -} - -const OpenSessionPacketCode = "openSession" - -func (p *OpenSessionPacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != OpenSessionPacketCode { - return fmt.Errorf("packet code is not '%s', got '%s'", OpenSessionPacketCode, p.Code) - } - - return nil -} - -func (p *OpenSessionPacket) HasEmail() bool { - return p.Proof.Email != "" -} - -func (p *OpenSessionPacket) HasIDToken() bool { - return p.Proof.IDToken != "" -} diff --git a/intents/packets/packet.go b/intents/packets/packet.go deleted file mode 100644 index ecf25af..0000000 --- a/intents/packets/packet.go +++ /dev/null @@ -1,39 +0,0 @@ -package packets - -import ( - "fmt" - "time" -) - -// allowedTimeDrift is the amount of time in seconds that a packet can be -// issued in the future or expired in the past and still be considered valid. -const allowedTimeDrift = 5 - -var ( - ErrInvalidPacketCode = fmt.Errorf("invalid packet code") - ErrorPackedIssuedInFuture = fmt.Errorf("packet issued time is in the future") - ErrorPacketExpired = fmt.Errorf("packet expired") -) - -type BasePacket struct { - Code string `json:"code"` - Issued uint64 `json:"issued"` - Expires uint64 `json:"expires"` -} - -func (p *BasePacket) IsValid() (bool, error) { - now := uint64(time.Now().Unix()) - if p.Code == "" { - return false, ErrInvalidPacketCode - } else if p.Issued > now+allowedTimeDrift { - return false, ErrorPackedIssuedInFuture - } else if p.Expires < now-allowedTimeDrift { - return false, ErrorPacketExpired - } - return true, nil -} - -type BasePacketForWallet struct { - BasePacket - Wallet string `json:"wallet"` -} diff --git a/intents/packets/validate_session.go b/intents/packets/validate_session.go deleted file mode 100644 index d6287d4..0000000 --- a/intents/packets/validate_session.go +++ /dev/null @@ -1,71 +0,0 @@ -package packets - -import ( - "encoding/json" - "fmt" -) - -type ValidateSessionPacket struct { - BasePacketForWallet - SessionId string `json:"sessionId"` - DeviceMetadata string `json:"deviceMetadata"` - RedirectURL *string `json:"redirectURL"` -} - -const ValidateSessionPacketCode = "validateSession" - -func (p *ValidateSessionPacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != ValidateSessionPacketCode { - return fmt.Errorf("packet code is not '%s', got '%s'", SignMessagePacketCode, p.Code) - } - - return nil -} - -const ValidateSessionPacketResponseCode = "validateSessionStarted" - -type ValidateSessionPacketResponse struct { - Code string `json:"code"` - - Data struct { - EmailSent bool `json:"emailSent"` - SmsSent bool `json:"smsSent"` - } -} - -const GetSessionPacketCode = "getSession" - -type GetSessionPacket struct { - BasePacketForWallet - SessionId string `json:"sessionId"` -} - -func (p *GetSessionPacket) Unmarshal(packet json.RawMessage) error { - err := json.Unmarshal(packet, &p) - if err != nil { - return err - } - - if p.Code != GetSessionPacketCode { - return fmt.Errorf("packet code is not '%s', got '%s'", GetSessionPacketCode, p.Code) - } - - return nil -} - -const GetSessionPacketResponseCode = "getSessionResponse" - -type GetSessionPacketResponse struct { - Code string `json:"code"` - - Data struct { - SessionId string `json:"sessionId"` - Wallet string `json:"wallet"` - Validated bool `json:"validated"` - } -} diff --git a/intents/proto.go b/intents/proto.go new file mode 100644 index 0000000..5b46951 --- /dev/null +++ b/intents/proto.go @@ -0,0 +1,5 @@ +// Client +//go:generate go run github.com/webrpc/webrpc/cmd/webrpc-gen -schema=intent.ridl -target=golang -pkg=intents -client -out=./intent.gen.go +//go:generate go run github.com/webrpc/webrpc/cmd/webrpc-gen -schema=intent.ridl -target=typescript -client -out=./intent.gen.ts + +package intents diff --git a/intents/session.go b/intents/session.go new file mode 100644 index 0000000..2d31e20 --- /dev/null +++ b/intents/session.go @@ -0,0 +1,119 @@ +package intents + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "fmt" + "strings" + + "github.com/0xsequence/ethkit/ethwallet" + "github.com/0xsequence/ethkit/go-ethereum/common" +) + +type Session interface { + SessionID() string + Sign(intent *Intent) error +} + +type session256K1 struct { + wallet *ethwallet.Wallet +} + +func NewSessionP256K1(wallet *ethwallet.Wallet) Session { + return &session256K1{wallet: wallet} +} + +func (s session256K1) SessionID() string { + return strings.ToLower( + fmt.Sprintf( + "0x%s", + common.Bytes2Hex( + append([]byte{byte(KeyTypeSECP256K1)}, s.wallet.Address().Bytes()...), + ), + ), + ) +} + +func (s session256K1) Sign(intent *Intent) error { + hash, err := intent.Hash() + if err != nil { + return err + } + + signature, err := s.wallet.SignMessage(hash) + if err != nil { + return err + } + + intent.Signatures = append(intent.Signatures, &Signature{ + SessionID: s.SessionID(), + Signature: bytesToSignature(signature), + }) + return nil +} + +type session256R1 struct { + privateKey *ecdsa.PrivateKey +} + +func NewSessionP256R1(privateKey *ecdsa.PrivateKey) Session { + return &session256R1{privateKey: privateKey} +} + +func (s session256R1) SessionID() string { + pubKey := elliptic.Marshal(s.privateKey.Curve, s.privateKey.PublicKey.X, s.privateKey.PublicKey.Y) + return strings.ToLower( + fmt.Sprintf( + "0x%s", + common.Bytes2Hex( + append([]byte{byte(KeyTypeSECP256R1)}, pubKey...), + ), + ), + ) +} + +func (s session256R1) Sign(intent *Intent) error { + hash, err := intent.Hash() + if err != nil { + return err + } + + sha256Hash := sha256.Sum256(hash) + + sr, ss, err := ecdsa.Sign(rand.Reader, s.privateKey, sha256Hash[:]) + if err != nil { + return err + } + + signature := append(sr.Bytes(), ss.Bytes()...) + + intent.Signatures = append(intent.Signatures, &Signature{ + SessionID: s.SessionID(), + Signature: bytesToSignature(signature), + }) + return nil +} + +func SignIntentWithWalletLegacy[T any](wallet *ethwallet.Wallet, intent *IntentTyped[T]) error { + hash, err := intent.Hash() + if err != nil { + return err + } + + signature, err := wallet.SignMessage(hash) + if err != nil { + return err + } + + intent.Signatures = append(intent.Signatures, &Signature{ + SessionID: strings.ToLower(wallet.Address().String()), + Signature: strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(signature))), + }) + return nil +} + +func bytesToSignature(sig []byte) string { + return strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(sig))) +} From 03a5ef4a2143ad35527dc7657e078ff7e3cbe500 Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Thu, 15 Feb 2024 09:51:51 +0100 Subject: [PATCH 28/28] intents: apply review remarks --- intents/intent.gen.go | 10 +-- intents/intent.gen.ts | 4 +- intents/intent.ridl | 6 +- intents/intent_ext.go | 150 +-------------------------------- intents/intent_typed_test.go | 29 ++++++- intents/session.go | 158 ++++++++++++++++++++++++++++++++++- 6 files changed, 198 insertions(+), 159 deletions(-) diff --git a/intents/intent.gen.go b/intents/intent.gen.go index 06b5473..18fb65d 100644 --- a/intents/intent.gen.go +++ b/intents/intent.gen.go @@ -1,4 +1,4 @@ -// sequence-waas-intents v0.1.0 5f79da1499857910e938fa990736008e8c0e3bc5 +// sequence-waas-intents v0.1.0 e8e022f41dd9c03ff373c616d8f484fbefdfe7d9 // -- // Code generated by webrpc-gen@v0.14.0-dev with golang generator. DO NOT EDIT. // @@ -25,7 +25,7 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "5f79da1499857910e938fa990736008e8c0e3bc5" + return "e8e022f41dd9c03ff373c616d8f484fbefdfe7d9" } // @@ -38,7 +38,7 @@ type Intent struct { ExpiresAt uint64 `json:"expiresAt"` IssuedAt uint64 `json:"issuedAt"` Data interface{} `json:"data"` - Signatures []*Signature `json:",omitempty"` + Signatures []*Signature `json:"signatures,omitempty"` } type Signature struct { @@ -48,8 +48,8 @@ type Signature struct { type IntentDataOpenSession struct { SessionID string `json:"sessionId"` - Email *string `json:",omitempty"` - IdToken *string `json:",omitempty"` + Email *string `json:"email,omitempty"` + IdToken *string `json:"idToken,omitempty"` } type IntentDataCloseSession struct { diff --git a/intents/intent.gen.ts b/intents/intent.gen.ts index f209b3d..67ff992 100644 --- a/intents/intent.gen.ts +++ b/intents/intent.gen.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// sequence-waas-intents v0.1.0 5f79da1499857910e938fa990736008e8c0e3bc5 +// sequence-waas-intents v0.1.0 e8e022f41dd9c03ff373c616d8f484fbefdfe7d9 // -- // Code generated by webrpc-gen@v0.14.0-dev with typescript generator. DO NOT EDIT. // @@ -12,7 +12,7 @@ export const WebRPCVersion = "v1" export const WebRPCSchemaVersion = "v0.1.0" // Schema hash generated from your RIDL schema -export const WebRPCSchemaHash = "5f79da1499857910e938fa990736008e8c0e3bc5" +export const WebRPCSchemaHash = "e8e022f41dd9c03ff373c616d8f484fbefdfe7d9" // // Types diff --git a/intents/intent.ridl b/intents/intent.ridl index 8c77961..b00796c 100644 --- a/intents/intent.ridl +++ b/intents/intent.ridl @@ -10,7 +10,7 @@ struct Intent - issuedAt: uint64 - data: any - signatures: []Signature - + go.tag.json = ,omitempty + + go.tag.json = signatures,omitempty struct Signature - sessionId: string @@ -32,9 +32,9 @@ struct IntentDataOpenSession - sessionId: string + go.field.name = SessionID - email?: string - + go.tag.json = ,omitempty + + go.tag.json = email,omitempty - idToken?: string - + go.tag.json = ,omitempty + + go.tag.json = idToken,omitempty struct IntentDataCloseSession - sessionId: string diff --git a/intents/intent_ext.go b/intents/intent_ext.go index a16cf4f..6e2fac1 100644 --- a/intents/intent_ext.go +++ b/intents/intent_ext.go @@ -1,15 +1,9 @@ package intents import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/sha256" "fmt" - "math/big" - "strings" "time" - "github.com/0xsequence/ethkit/go-ethereum/common" "github.com/0xsequence/ethkit/go-ethereum/crypto" "github.com/gibson042/canonicaljson-go" ) @@ -17,14 +11,6 @@ import ( const IntentValidTimeInSec = 60 const IntentAllowedTimeDriftInSec = 5 -type KeyType int - -const ( - KeyTypeSECP256K1 KeyType = iota - KeyTypeSECP256R1 - KeyTypeUnknown -) - const ( IntentNameOpenSession = "openSession" IntentNameCloseSession = "closeSession" @@ -81,8 +67,8 @@ func (intent *Intent) IsValid() error { return fmt.Errorf("intent issued in the future") } - // check if at least one signature is valid - if len(intent.Signers()) == 0 { + // check if all signatures are valid + if validSingers := len(intent.Signers()); validSingers == 0 || validSingers != len(intent.Signatures) { return fmt.Errorf("invalid signature") } @@ -90,140 +76,10 @@ func (intent *Intent) IsValid() error { return nil } -func (intent *Intent) keyType(sessionId string) KeyType { - // handle empty session ids - if len(sessionId) == 0 { - return KeyTypeUnknown - } - - // handle old session ids - if len(sessionId) == 42 { - return KeyTypeSECP256K1 - } - - // handle key typed session ids - sessionIdBytes := common.FromHex(sessionId) - return KeyType(sessionIdBytes[0]) -} - -func (intent *Intent) isValidSignature(sessionId string, signature string) error { - switch intent.keyType(sessionId) { - case KeyTypeSECP256K1: - return intent.isValidSignatureP256K1(sessionId, signature) - case KeyTypeSECP256R1: - return intent.isValidSignatureP256R1(sessionId, signature) - default: - return fmt.Errorf("unknown session key type") - } -} - -// isValidSignatureP256K1 checks if the signature is valid for the given secp256k1 session -func (intent *Intent) isValidSignatureP256K1(sessionId string, signature string) error { - // validate session address and signature - if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionId, "0x") { - // invalid params - return fmt.Errorf("invalid sessionId or signature format") - } - - // validate session address - sessionAddressBytes := common.FromHex(sessionId) - if len(sessionAddressBytes) != 21 && len(sessionAddressBytes) != 20 { - // invalid session address - return fmt.Errorf("invalid sessionId length") - } - - // validate signature - sigBytes := common.FromHex(signature) - if len(sigBytes) != 65 { - return fmt.Errorf("invalid signature length") - } - - // handle typed session address - if len(sessionAddressBytes) == 21 { - sessionAddressBytes = sessionAddressBytes[1:] - } - - sessionAddress := fmt.Sprintf("0x%s", common.Bytes2Hex(sessionAddressBytes)) - - // Get hash of the packet - hash, err := intent.Hash() - if err != nil { - return fmt.Errorf("failed to hash intent: %w", err) - } - - // Add Ethereum prefix to the hash - prefixedHash := crypto.Keccak256Hash([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash))) - - // handle recovery byte - if sigBytes[64] == 27 || sigBytes[64] == 28 { - sigBytes[64] -= 27 - } - - // Recover the public key from the signature - pubKey, err := crypto.Ecrecover(prefixedHash.Bytes(), sigBytes) - if err != nil { - return fmt.Errorf("failed to recover public key: %w", err) - } - - addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) - - // Check if the recovered address matches the session address - if strings.ToLower(addr.Hex()) != strings.ToLower(sessionAddress) { - return fmt.Errorf("invalid signature") - } - return nil -} - -// isValidSignatureP256R1 checks if the signature is valid for the given secp256r1 session -func (intent *Intent) isValidSignatureP256R1(sessionId string, signature string) error { - // validate session address and signature - if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionId, "0x") { - // invalid params - return fmt.Errorf("invalid sessionId or signature format") - } - - // validate session id - sessionIdBytes := common.FromHex(sessionId) - if len(sessionIdBytes) != 66 { - return fmt.Errorf("invalid sessionId length") - } - - // validate signature - signatureBytes := common.FromHex(signature) - if len(signatureBytes) != 64 { - return fmt.Errorf("invalid signature length") - } - - // message hash - messageHash, _ := intent.Hash() - messageHash2 := sha256.Sum256(messageHash) - - // public key - publicKeyBuff := common.FromHex(sessionId)[1:] - x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBuff) - if x == nil || y == nil { - return fmt.Errorf("invalid public key") - } - - pub := ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: x, - Y: y, - } - - // signature - r := new(big.Int).SetBytes(signatureBytes[:32]) - s := new(big.Int).SetBytes(signatureBytes[32:64]) - if !ecdsa.Verify(&pub, messageHash2[:], r, s) { - return fmt.Errorf("invalid signature") - } - return nil -} - func (intent *Intent) Signers() []string { var signers []string for _, signature := range intent.Signatures { - if err := intent.isValidSignature(signature.SessionID, signature.Signature); err == nil { + if err := IsValidSessionSignature(signature.SessionID, signature.Signature, intent); err == nil { signers = append(signers, signature.SessionID) } } diff --git a/intents/intent_typed_test.go b/intents/intent_typed_test.go index 161b275..b78e44a 100644 --- a/intents/intent_typed_test.go +++ b/intents/intent_typed_test.go @@ -131,7 +131,7 @@ func TestIntentIsValid(t *testing.T) { wallet, err := ethwallet.NewWalletFromRandomEntropy() require.NoError(t, err) - err = SignIntentWithWalletLegacy(wallet, intent) + err = SignIntentWithWalletLegacy(wallet, intent.ToIntent()) require.NoError(t, err) assert.NoError(t, intent.IsValid()) @@ -214,3 +214,30 @@ func TestIntentDataValidator(t *testing.T) { assert.ErrorContains(t, intent.IsValid(), "invalid intent data") }) } + +func TestIntent_Hash(t *testing.T) { + wallet, _ := ethwallet.NewWalletFromRandomEntropy() + + session := NewSessionP256K1(wallet) + + intentTyped := NewIntentTyped(IntentDataOpenSession{SessionID: "0x1234", Email: ethkit.ToPtr("test@test.com")}) + + intent := Intent{ + Version: intentTyped.Version, + Name: intentTyped.Name, + ExpiresAt: intentTyped.ExpiresAt, + IssuedAt: intentTyped.IssuedAt, + Data: map[string]interface{}{"sessionId": "0x1234", "email": "test@test.com"}, + } + + err := session.Sign(intentTyped.ToIntent()) + require.NoError(t, err) + + err = session.Sign(&intent) + require.NoError(t, err) + + hashIntentTyped, _ := intentTyped.Hash() + hashIntent, _ := intent.Hash() + + assert.Equal(t, hashIntentTyped, hashIntent) +} diff --git a/intents/session.go b/intents/session.go index 2d31e20..f9c491b 100644 --- a/intents/session.go +++ b/intents/session.go @@ -6,10 +6,20 @@ import ( "crypto/rand" "crypto/sha256" "fmt" + "math/big" "strings" "github.com/0xsequence/ethkit/ethwallet" "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/ethkit/go-ethereum/crypto" +) + +type KeyType int + +const ( + KeyTypeSECP256K1 KeyType = iota + KeyTypeSECP256R1 + KeyTypeUnknown ) type Session interface { @@ -96,7 +106,7 @@ func (s session256R1) Sign(intent *Intent) error { return nil } -func SignIntentWithWalletLegacy[T any](wallet *ethwallet.Wallet, intent *IntentTyped[T]) error { +func SignIntentWithWalletLegacy(wallet *ethwallet.Wallet, intent *Intent) error { hash, err := intent.Hash() if err != nil { return err @@ -114,6 +124,152 @@ func SignIntentWithWalletLegacy[T any](wallet *ethwallet.Wallet, intent *IntentT return nil } +func SignIntentP256K1(wallet *ethwallet.Wallet, intent *Intent) error { + return NewSessionP256K1(wallet).Sign(intent) +} + +func SignIntentP256R1(privateKey *ecdsa.PrivateKey, intent *Intent) error { + return NewSessionP256R1(privateKey).Sign(intent) +} + +func IsValidSessionSignature(sessionId string, signature string, intent *Intent) error { + switch KeyTypeFromSessionId(sessionId) { + case KeyTypeSECP256K1: + return IsValidSessionSignatureP256K1(sessionId, signature, intent) + case KeyTypeSECP256R1: + return IsValidSessionSignatureP256R1(sessionId, signature, intent) + default: + return fmt.Errorf("unknown session key type") + } +} + +// IsValidSessionSignatureP256K1 checks if the signature is valid for the given secp256k1 session +func IsValidSessionSignatureP256K1(sessionId string, signature string, intent *Intent) error { + // validate session address and signature + if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionId, "0x") { + // invalid params + return fmt.Errorf("invalid sessionId or signature format") + } + + // validate session address + sessionAddressBytes := common.FromHex(sessionId) + if len(sessionAddressBytes) != 21 && len(sessionAddressBytes) != 20 { + // invalid session address + return fmt.Errorf("invalid sessionId length") + } + + if len(sessionAddressBytes) == 21 && sessionAddressBytes[0] != byte(KeyTypeSECP256K1) { + return fmt.Errorf("invalid sessionId format") + } + + // validate signature + sigBytes := common.FromHex(signature) + if len(sigBytes) != 65 { + return fmt.Errorf("invalid signature length") + } + + // handle typed session address + if len(sessionAddressBytes) == 21 { + sessionAddressBytes = sessionAddressBytes[1:] + } + + sessionAddress := fmt.Sprintf("0x%s", common.Bytes2Hex(sessionAddressBytes)) + + // Get hash of the packet + hash, err := intent.Hash() + if err != nil { + return fmt.Errorf("failed to hash intent: %w", err) + } + + // Add Ethereum prefix to the hash + prefixedHash := crypto.Keccak256Hash([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash))) + + // handle recovery byte + if sigBytes[64] == 27 || sigBytes[64] == 28 { + sigBytes[64] -= 27 + } + + // Recover the public key from the signature + pubKey, err := crypto.Ecrecover(prefixedHash.Bytes(), sigBytes) + if err != nil { + return fmt.Errorf("failed to recover public key: %w", err) + } + + addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) + + // Check if the recovered address matches the session address + if strings.ToLower(addr.Hex()) != strings.ToLower(sessionAddress) { + return fmt.Errorf("invalid signature") + } + return nil +} + +// IsValidSessionSignatureP256R1 checks if the signature is valid for the given secp256r1 session +func IsValidSessionSignatureP256R1(sessionId string, signature string, intent *Intent) error { + // validate session address and signature + if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionId, "0x") { + // invalid params + return fmt.Errorf("invalid sessionId or signature format") + } + + // validate session id + sessionIdBytes := common.FromHex(sessionId) + if len(sessionIdBytes) != 66 { + return fmt.Errorf("invalid sessionId length") + } + + if sessionIdBytes[0] != byte(KeyTypeSECP256R1) { + return fmt.Errorf("invalid sessionId format") + } + + // validate signature + signatureBytes := common.FromHex(signature) + if len(signatureBytes) != 64 { + return fmt.Errorf("invalid signature length") + } + + // message hash + messageHash, _ := intent.Hash() + messageHash2 := sha256.Sum256(messageHash) + + // public key + publicKeyBuff := common.FromHex(sessionId)[1:] + x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBuff) + if x == nil || y == nil { + return fmt.Errorf("invalid public key") + } + + pub := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + // signature + r := new(big.Int).SetBytes(signatureBytes[:32]) + s := new(big.Int).SetBytes(signatureBytes[32:64]) + if !ecdsa.Verify(&pub, messageHash2[:], r, s) { + return fmt.Errorf("invalid signature") + } + return nil +} + +func KeyTypeFromSessionId(sessionId string) KeyType { + // handle empty session ids + if len(sessionId) == 0 { + return KeyTypeUnknown + } + + // handle old session ids + if len(sessionId) == 42 { + return KeyTypeSECP256K1 + } + + // handle key typed session ids + sessionIdBytes := common.FromHex(sessionId) + return KeyType(sessionIdBytes[0]) +} + func bytesToSignature(sig []byte) string { return strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(sig))) }