internal/app/wallet/proof.go
Rama main · réplica de lectura del repositorio original.
package wallet
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"unicode/utf8"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/tvm/cell"
)
var ErrOwnershipProof = errors.New("invalid wallet ownership proof")
// Trampoline and revision-00 data layout match wallet-engine's public state
// derivation. A rotated signing key MUST NOT be used as an address anchor.
const trampolineBOC = "te6ccgEBAQEAGgAAMP8AIJgh10mDCLnyQN+Ahfgz0O0eIO1T2Q=="
const trampolineHash = "9149ae51c1e4689710cebf7830297b16acfbadb363a920a537893e7ffeeca768"
// InitialAddress derives the Wallet revision-00 account from its ANCHOR key.
// It does not infer a rotated account's address from its current signing key.
func InitialAddress(anchorKey []byte, testnet bool) (*address.Address, error) {
if len(anchorKey) != ed25519.PublicKeySize {
return nil, ErrOwnershipProof
}
boc, err := base64.StdEncoding.DecodeString(trampolineBOC)
if err != nil {
return nil, err
}
code, err := cell.FromBOC(boc)
if err != nil {
return nil, err
}
expected, _ := hex.DecodeString(trampolineHash)
if !bytes.Equal(code.Hash(), expected) {
return nil, errors.New("wallet trampoline hash mismatch")
}
walletID := uint64(0x7fff7f11)
if testnet {
walletID = 0x7fff7ffd
}
data := cell.BeginCell().MustStoreUInt(0, 8).MustStoreUInt(0, 32).MustStoreUInt(walletID, 32).MustStoreSlice(anchorKey, 256).EndCell()
init, err := tlb.ToCell(tlb.StateInit{Code: code, Data: data})
if err != nil {
return nil, err
}
addr := address.NewAddress(0, 0, init.Hash())
addr.SetTestnetOnly(testnet)
return addr, nil
}
// ProofHash is the TON Connect v2 signing digest used by the current iOS wallet
// link flow. Domain and payload must be the server-issued values, never values
// taken solely from an untrusted request. Replay/expiry checks belong to the
// atomic challenge-consumption transaction in the RPC service.
func ProofHash(workchain int32, accountHash []byte, domain string, timestamp uint64, payload string) ([32]byte, error) {
if len(accountHash) != 32 || len(domain) == 0 || len(domain) > 2048 || len(payload) > 65536 || !utf8.ValidString(domain) || !utf8.ValidString(payload) {
return [32]byte{}, ErrOwnershipProof
}
message := make([]byte, 0, 80+len(domain)+len(payload))
message = append(message, "ton-proof-item-v2/"...)
message = binary.BigEndian.AppendUint32(message, uint32(workchain))
message = append(message, accountHash...)
message = binary.LittleEndian.AppendUint32(message, uint32(len(domain)))
message = append(message, domain...)
message = binary.LittleEndian.AppendUint64(message, timestamp)
message = append(message, payload...)
inner := sha256.Sum256(message)
outer := append([]byte{0xff, 0xff}, "ton-connect"...)
outer = append(outer, inner[:]...)
return sha256.Sum256(outer), nil
}
// VerifyProof verifies the signature for an independently authenticated
// account/key mapping. It does NOT by itself establish that a key owns an
// address: obtain that mapping from validated StateInit or current chain state.
func VerifyProof(workchain int32, accountHash, signingPublicKey []byte, domain string, timestamp uint64, payload string, signature []byte) error {
if len(signingPublicKey) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize {
return ErrOwnershipProof
}
hash, err := ProofHash(workchain, accountHash, domain, timestamp, payload)
if err != nil || !ed25519.Verify(ed25519.PublicKey(signingPublicKey), hash[:], signature) {
return ErrOwnershipProof
}
return nil
}