From 1e37058d4943ce57b7aee59d4572ac6c4d2244bf Mon Sep 17 00:00:00 2001 From: "aleksej.paschenko" Date: Wed, 2 Aug 2023 11:36:21 +0300 Subject: [PATCH] Add tonconnect.CreateSignedProof to generate proof for tonconnect challenge --- tonconnect/proof.go | 50 ++++++++++++++++++++++++++++ tonconnect/proof_test.go | 71 ++++++++++++++++++++++++++++++++++++++++ tonconnect/server.go | 8 +++-- wallet/wallet.go | 6 ++-- 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 tonconnect/proof.go create mode 100644 tonconnect/proof_test.go diff --git a/tonconnect/proof.go b/tonconnect/proof.go new file mode 100644 index 00000000..5cc0d0b5 --- /dev/null +++ b/tonconnect/proof.go @@ -0,0 +1,50 @@ +package tonconnect + +import ( + "crypto/ed25519" + "encoding/base64" + "time" + + "github.com/tonkeeper/tongo" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" +) + +// ProofOptions configures particular aspects of a proof. +type ProofOptions struct { + Timestamp time.Time + Domain string +} + +// CreateSignedProof returns a proof that the caller posses a private key of a particular account. +// This can be used on the client side, +// when the server side runs tonconnect.Server or any other server implementation of ton-connect. +func CreateSignedProof(payload string, accountID tongo.AccountID, privateKey ed25519.PrivateKey, stateInit tlb.StateInit, options ProofOptions) (*Proof, error) { + stateInitCell := boc.NewCell() + if err := tlb.Marshal(stateInitCell, stateInit); err != nil { + return nil, err + } + stateInitBase64, err := stateInitCell.ToBocBase64() + if err != nil { + return nil, err + } + proof := Proof{ + Address: accountID.String(), + Proof: ProofData{ + Timestamp: options.Timestamp.Unix(), + Domain: options.Domain, + Payload: payload, + StateInit: stateInitBase64, + }, + } + parsedMsg, err := convertTonProofMessage(&proof) + if err != nil { + return nil, err + } + msg, err := createMessage(parsedMsg) + if err != nil { + return nil, err + } + proof.Proof.Signature = base64.StdEncoding.EncodeToString(signMessage(privateKey, msg)) + return &proof, nil +} diff --git a/tonconnect/proof_test.go b/tonconnect/proof_test.go new file mode 100644 index 00000000..fab86ee9 --- /dev/null +++ b/tonconnect/proof_test.go @@ -0,0 +1,71 @@ +package tonconnect + +import ( + "context" + "crypto/ed25519" + "testing" + "time" + + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/wallet" +) + +func TestCreateSignedProof(t *testing.T) { + cli, err := liteapi.NewClient(liteapi.Testnet()) + if err != nil { + t.Fatalf("liteapi.NewClient() failed: %v", err) + } + tests := []struct { + name string + version wallet.Version + secret string + }{ + { + name: "v4r2", + version: wallet.V4R2, + secret: "some-random-secret", + }, + { + name: "v4r1", + version: wallet.V4R1, + secret: "another-random-secret", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv, err := NewTonConnect(cli, tt.secret) + if err != nil { + t.Fatalf("NewTonConnect() failed: %v", err) + } + payload, err := srv.GeneratePayload() + if err != nil { + t.Fatalf("GeneratePayload() failed: %v", err) + } + seed := wallet.RandomSeed() + privateKey, err := wallet.SeedToPrivateKey(seed) + if err != nil { + t.Fatalf("SeedToPrivateKey() failed: %v", err) + } + publicKey := privateKey.Public().(ed25519.PublicKey) + stateInit, err := wallet.GenerateStateInit(publicKey, tt.version, 0, nil) + if err != nil { + t.Fatalf("GenerateStateInit() failed: %v", err) + } + accountID, err := wallet.GenerateWalletAddress(publicKey, tt.version, 0, nil) + if err != nil { + t.Fatalf("GenerateWalletAddress() failed: %v", err) + } + signedProof, err := CreateSignedProof(payload, accountID, privateKey, stateInit, ProofOptions{Timestamp: time.Now(), Domain: "web"}) + if err != nil { + t.Fatalf("CreateSignedProof() failed: %v", err) + } + verified, _, err := srv.CheckProof(context.Background(), signedProof) + if err != nil { + t.Fatalf("CheckProof() failed: %v", err) + } + if verified != true { + t.Fatalf("proof is invalid") + } + }) + } +} diff --git a/tonconnect/server.go b/tonconnect/server.go index a53866a8..c2833a7c 100644 --- a/tonconnect/server.go +++ b/tonconnect/server.go @@ -144,7 +144,7 @@ func (s *Server) CheckProof(ctx context.Context, tp *Proof) (bool, ed25519.Publi return false, nil, fmt.Errorf("failed verify payload") } - parsed, err := s.convertTonProofMessage(tp) + parsed, err := convertTonProofMessage(tp) if err != nil { return false, nil, err } @@ -212,7 +212,7 @@ func (s *Server) checkPayload(payload string) (bool, error) { return true, nil } -func (s *Server) convertTonProofMessage(tp *Proof) (*parsedMessage, error) { +func convertTonProofMessage(tp *Proof) (*parsedMessage, error) { addr := strings.Split(tp.Address, ":") if len(addr) != 2 { return nil, fmt.Errorf("invalid address param: %v", tp.Address) @@ -292,6 +292,10 @@ func signatureVerify(pubKey ed25519.PublicKey, message, signature []byte) bool { return ed25519.Verify(pubKey, message, signature) } +func signMessage(privateKey ed25519.PrivateKey, message []byte) []byte { + return ed25519.Sign(privateKey, message) +} + func ParseStateInit(stateInit string) ([]byte, error) { cells, err := boc.DeserializeBocBase64(stateInit) if err != nil || len(cells) != 1 { diff --git a/wallet/wallet.go b/wallet/wallet.go index bedccc2a..9c0df85d 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -55,7 +55,7 @@ func GenerateWalletAddress( workchain int, subWalletId *int, ) (tongo.AccountID, error) { - state, err := generateStateInit(key, ver, workchain, subWalletId) + state, err := GenerateStateInit(key, ver, workchain, subWalletId) if err != nil { return tongo.AccountID{}, fmt.Errorf("can not generate wallet state: %v", err) } @@ -76,7 +76,7 @@ func GenerateWalletAddress( return tongo.AccountID{Workchain: int32(workchain), Address: hash}, nil } -func generateStateInit( +func GenerateStateInit( key ed25519.PublicKey, ver Version, workchain int, @@ -237,7 +237,7 @@ func (w *Wallet) RawSend( func (w *Wallet) getInit() (tlb.StateInit, error) { publicKey := w.key.Public().(ed25519.PublicKey) id := int(w.subWalletId) - return generateStateInit(publicKey, w.ver, int(w.address.Workchain), &id) + return GenerateStateInit(publicKey, w.ver, int(w.address.Workchain), &id) } func checkMessagesLimit(msgQty int, ver Version) error { // TODO: maybe return bool