diff --git a/account.go b/account.go index ba761a1..a0fc634 100644 --- a/account.go +++ b/account.go @@ -10,6 +10,7 @@ import ( "sync" "github.com/aurora-is-near/near-api-go/keystore" + "github.com/aurora-is-near/near-api-go/types" "github.com/aurora-is-near/near-api-go/utils" "github.com/btcsuite/btcd/btcutil/base58" "github.com/near/borsh-go" @@ -126,10 +127,10 @@ func (a *Account) SendMoney( receiverID string, amount big.Int, ) (map[string]interface{}, error) { - return a.SignAndSendTransaction(receiverID, []Action{ + return a.SignAndSendTransaction(receiverID, []types.Action{ { Enum: 3, - Transfer: Transfer{ + Transfer: types.Transfer{ Deposit: amount, }, }, @@ -138,15 +139,15 @@ func (a *Account) SendMoney( // AddKeys adds the given publicKeys to the account with full access. func (a *Account) AddKeys( - publicKeys ...utils.PublicKey, + publicKeys ...types.PublicKey, ) (map[string]interface{}, error) { fullAccessKey := fullAccessKey() - actions := make([]Action, 0) + actions := make([]types.Action, 0) for _, pk := range publicKeys { - actions = append(actions, Action{ + actions = append(actions, types.Action{ Enum: 5, - AddKey: AddKey{ - PublicKey: pk, + AddKey: types.AddKey{ + PublicKey: types.PublicKey(pk), AccessKey: fullAccessKey, }, }) @@ -156,14 +157,14 @@ func (a *Account) AddKeys( // DeleteKeys deletes the given publicKeys from the account. func (a *Account) DeleteKeys( - publicKeys ...utils.PublicKey, + publicKeys ...types.PublicKey, ) (map[string]interface{}, error) { - actions := make([]Action, 0) + actions := make([]types.Action, 0) for _, pk := range publicKeys { - actions = append(actions, Action{ + actions = append(actions, types.Action{ Enum: 6, - DeleteKey: DeleteKey{ - PublicKey: pk, + DeleteKey: types.DeleteKey{ + PublicKey: types.PublicKey(pk), }, }) } @@ -173,24 +174,24 @@ func (a *Account) DeleteKeys( // CreateAccount creates the newAccountID with the given publicKey and amount. func (a *Account) CreateAccount( newAccountID string, - publicKey utils.PublicKey, + publicKey types.PublicKey, amount big.Int, ) (map[string]interface{}, error) { - return a.SignAndSendTransaction(newAccountID, []Action{ + return a.SignAndSendTransaction(newAccountID, []types.Action{ { Enum: 0, CreateAccount: 0, }, { Enum: 3, - Transfer: Transfer{ + Transfer: types.Transfer{ Deposit: amount, }, }, { Enum: 5, - AddKey: AddKey{ - PublicKey: publicKey, + AddKey: types.AddKey{ + PublicKey: types.PublicKey(publicKey), AccessKey: fullAccessKey(), }, }, @@ -202,10 +203,10 @@ func (a *Account) CreateAccount( func (a *Account) DeleteAccount( beneficiaryID string, ) (map[string]interface{}, error) { - return a.SignAndSendTransaction(a.fullAccessKeyPair.AccountID, []Action{ + return a.SignAndSendTransaction(a.fullAccessKeyPair.AccountID, []types.Action{ { Enum: 7, - DeleteAccount: DeleteAccount{ + DeleteAccount: types.DeleteAccount{ BeneficiaryID: beneficiaryID, }, }, @@ -215,7 +216,7 @@ func (a *Account) DeleteAccount( // SignAndSendTransaction signs the given actions and sends them as a transaction to receiverID. func (a *Account) SignAndSendTransaction( receiverID string, - actions []Action, + actions []types.Action, ) (map[string]interface{}, error) { buf, err := utils.ExponentialBackoff(txNonceRetryWait, txNonceRetryNumber, txNonceRetryWaitBackoff, func() ([]byte, error) { @@ -241,7 +242,7 @@ func (a *Account) SignAndSendTransaction( func (a *Account) SignAndSendTransactionWithKey( receiverID string, publicKey string, - actions []Action, + actions []types.Action, ) (map[string]interface{}, error) { buf, err := utils.ExponentialBackoff(txNonceRetryWait, txNonceRetryNumber, txNonceRetryWaitBackoff, func() ([]byte, error) { @@ -268,7 +269,7 @@ func (a *Account) SignAndSendTransactionWithKeyAndNonce( receiverID string, publicKey string, nonce uint64, - actions []Action, + actions []types.Action, ) (map[string]interface{}, error) { buf, err := utils.ExponentialBackoff(txNonceRetryWait, txNonceRetryNumber, txNonceRetryWaitBackoff, func() ([]byte, error) { @@ -291,7 +292,7 @@ func (a *Account) SignAndSendTransactionWithKeyAndNonce( // SignAndSendTransactionAsync signs the given actions and sends it immediately func (a *Account) SignAndSendTransactionAsync( receiverID string, - actions []Action, + actions []types.Action, ) (string, error) { _, signedTx, err := a.signTransaction(receiverID, actions) if err != nil { @@ -309,7 +310,7 @@ func (a *Account) SignAndSendTransactionAsync( func (a *Account) SignAndSendTransactionAsyncWithKey( receiverID string, publicKey string, - actions []Action, + actions []types.Action, ) (string, error) { _, signedTx, err := a.signTransactionWithKey(receiverID, publicKey, actions) if err != nil { @@ -325,8 +326,8 @@ func (a *Account) SignAndSendTransactionAsyncWithKey( func (a *Account) signTransaction( receiverID string, - actions []Action, -) (txHash []byte, signedTx *SignedTransaction, err error) { + actions []types.Action, +) (txHash []byte, signedTx *types.SignedTransaction, err error) { _, ak, err := a.findAccessKey() if err != nil { return nil, nil, err @@ -362,8 +363,8 @@ func (a *Account) signTransaction( func (a *Account) signTransactionWithKey( receiverID string, publicKey string, - actions []Action, -) ([]byte, *SignedTransaction, error) { + actions []types.Action, +) ([]byte, *types.SignedTransaction, error) { ak, err := a.findAccessKeyWithPublicKey(publicKey) if err != nil { @@ -401,8 +402,8 @@ func (a *Account) signTransactionWithKeyAndNonce( receiverID string, publicKey string, nonce uint64, - actions []Action, -) ([]byte, *SignedTransaction, error) { + actions []types.Action, +) ([]byte, *types.SignedTransaction, error) { // get current block hash block, err := a.conn.Block() @@ -453,9 +454,9 @@ func (a *Account) FunctionCall( gas uint64, amount big.Int, ) (map[string]interface{}, error) { - return a.SignAndSendTransaction(contractID, []Action{{ + return a.SignAndSendTransaction(contractID, []types.Action{{ Enum: 2, - FunctionCall: FunctionCall{ + FunctionCall: types.FunctionCall{ MethodName: methodName, Args: args, Gas: gas, @@ -474,11 +475,11 @@ func (a *Account) FunctionCallWithMultiActionAndKey( amount big.Int, ) (map[string]interface{}, error) { - actions := make([]Action, 0) + actions := make([]types.Action, 0) for _, args := range argsSlice { - actions = append(actions, Action{ + actions = append(actions, types.Action{ Enum: 2, - FunctionCall: FunctionCall{ + FunctionCall: types.FunctionCall{ MethodName: methodName, Args: args, Gas: gas, @@ -501,11 +502,11 @@ func (a *Account) FunctionCallWithMultiActionAndKeyAndNonce( amount big.Int, ) (map[string]interface{}, error) { - actions := make([]Action, 0) + actions := make([]types.Action, 0) for _, args := range argsSlice { - actions = append(actions, Action{ + actions = append(actions, types.Action{ Enum: 2, - FunctionCall: FunctionCall{ + FunctionCall: types.FunctionCall{ MethodName: methodName, Args: args, Gas: gas, @@ -523,9 +524,9 @@ func (a *Account) FunctionCallAsync( gas uint64, amount big.Int, ) (string, error) { - return a.SignAndSendTransactionAsync(contractID, []Action{{ + return a.SignAndSendTransactionAsync(contractID, []types.Action{{ Enum: 2, - FunctionCall: FunctionCall{ + FunctionCall: types.FunctionCall{ MethodName: methodName, Args: args, Gas: gas, @@ -543,11 +544,11 @@ func (a *Account) FunctionCallAsyncWithMultiActionAndKey( gas uint64, amount big.Int, ) (string, error) { - actions := make([]Action, 0) + actions := make([]types.Action, 0) for _, args := range argsSlice { - actions = append(actions, Action{ + actions = append(actions, types.Action{ Enum: 2, - FunctionCall: FunctionCall{ + FunctionCall: types.FunctionCall{ MethodName: methodName, Args: args, Gas: gas, diff --git a/transaction.go b/transaction.go index 7b61122..aadf1f5 100644 --- a/transaction.go +++ b/transaction.go @@ -5,129 +5,32 @@ import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" - "math/big" - "github.com/aurora-is-near/near-api-go/utils" + "github.com/aurora-is-near/near-api-go/types" "github.com/near/borsh-go" ) -// AccessKey encodes a NEAR access key. -type AccessKey struct { - Nonce uint64 - Permission AccessKeyPermission -} - -// AccessKeyPermission encodes a NEAR access key permission. -type AccessKeyPermission struct { - Enum borsh.Enum `borsh_enum:"true"` // treat struct as complex enum when serializing/deserializing - FunctionCall FunctionCallPermission - FullAccess borsh.Enum -} - -// FunctionCallPermission encodes a NEAR function call permission (an access -// key permission). -type FunctionCallPermission struct { - Allowance *big.Int - ReceiverId string - MethodNames []string -} - -func fullAccessKey() AccessKey { - return AccessKey{ +func fullAccessKey() types.AccessKey { + return types.AccessKey{ Nonce: 0, - Permission: AccessKeyPermission{ + Permission: types.AccessKeyPermission{ Enum: 1, FullAccess: 1, }, } } -// A Transaction encodes a NEAR transaction. -type Transaction struct { - SignerID string - PublicKey utils.PublicKey - Nonce uint64 - ReceiverID string - BlockHash [32]byte - Actions []Action -} - -// Action simulates an enum for Borsh encoding. -type Action struct { - Enum borsh.Enum `borsh_enum:"true"` // treat struct as complex enum when serializing/deserializing - CreateAccount borsh.Enum - DeployContract DeployContract - FunctionCall FunctionCall - Transfer Transfer - Stake Stake - AddKey AddKey - DeleteKey DeleteKey - DeleteAccount DeleteAccount -} - -// The DeployContract action. -type DeployContract struct { - Code []byte -} - -// The FunctionCall action. -type FunctionCall struct { - MethodName string - Args []byte - Gas uint64 - Deposit big.Int // u128 -} - -// The Transfer action. -type Transfer struct { - Deposit big.Int // u128 -} - -// The Stake action. -type Stake struct { - Stake big.Int // u128 - PublicKey utils.PublicKey -} - -// The AddKey action. -type AddKey struct { - PublicKey utils.PublicKey - AccessKey AccessKey -} - -// The DeleteKey action. -type DeleteKey struct { - PublicKey utils.PublicKey -} - -// The DeleteAccount action. -type DeleteAccount struct { - BeneficiaryID string -} - -// A Signature used for signing transaction. -type Signature struct { - KeyType uint8 - Data [64]byte -} - -// SignedTransaction encodes signed transactions for NEAR. -type SignedTransaction struct { - Transaction Transaction - Signature Signature -} - func createTransaction( signerID string, - publicKey utils.PublicKey, + publicKey ed25519.PublicKey, receiverID string, nonce uint64, blockHash []byte, - actions []Action, -) *Transaction { - var tx Transaction + actions []types.Action, +) *types.Transaction { + var tx types.Transaction tx.SignerID = signerID - tx.PublicKey = publicKey + tx.PublicKey.FromEd25519(publicKey) tx.ReceiverID = receiverID tx.Nonce = nonce copy(tx.BlockHash[:], blockHash) @@ -136,10 +39,9 @@ func createTransaction( } func signTransactionObject( - tx *Transaction, + tx *types.Transaction, privKey ed25519.PrivateKey, - accountID string, -) (txHash []byte, signedTx *SignedTransaction, err error) { +) (txHash []byte, signedTx *types.SignedTransaction, err error) { buf, err := borsh.Serialize(*tx) if err != nil { return nil, nil, err @@ -152,11 +54,11 @@ func signTransactionObject( return nil, nil, err } - var signature Signature - signature.KeyType = utils.ED25519 + var signature types.Signature + signature.KeyType = types.ED25519 copy(signature.Data[:], sig) - var stx SignedTransaction + var stx types.SignedTransaction stx.Transaction = *tx stx.Signature = signature @@ -166,18 +68,18 @@ func signTransactionObject( func signTransaction( receiverID string, nonce uint64, - actions []Action, + actions []types.Action, blockHash []byte, publicKey ed25519.PublicKey, privKey ed25519.PrivateKey, accountID string, -) (txHash []byte, signedTx *SignedTransaction, err error) { +) (txHash []byte, signedTx *types.SignedTransaction, err error) { // create transaction - tx := createTransaction(accountID, utils.PublicKeyFromEd25519(publicKey), + tx := createTransaction(accountID, publicKey, receiverID, nonce, blockHash, actions) // sign transaction object - txHash, signedTx, err = signTransactionObject(tx, privKey, accountID) + txHash, signedTx, err = signTransactionObject(tx, privKey) if err != nil { return nil, nil, err } diff --git a/transaction_test.go b/transaction_test.go index 46ddd6f..673e1c8 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "testing" + "github.com/aurora-is-near/near-api-go/types" "github.com/near/borsh-go" ) @@ -14,8 +15,16 @@ func TestSignedTransactionStruct(t *testing.T) { if err != nil { t.Fatal(err) } - var signedTx SignedTransaction + var signedTx types.SignedTransaction if err = borsh.Deserialize(&signedTx, buf); err != nil { + t.Fatalf("Deserialization error: %v", err) + } + + ok, err := VerifyTransactionSignature(&signedTx) + if err != nil { t.Fatal(err) } + if !ok { + t.Fatal("signature is not valid") + } } diff --git a/types/crypto.go b/types/crypto.go new file mode 100644 index 0000000..7d851c2 --- /dev/null +++ b/types/crypto.go @@ -0,0 +1,126 @@ +package types + +import ( + "crypto/ed25519" + "math/big" + + "github.com/near/borsh-go" +) + +// All supported key types +const ( + ED25519 = 0 +) + +// PublicKey encoding for NEAR. +type PublicKey struct { + KeyType uint8 + Data [32]byte +} + + +func (pubKey *PublicKey) FromEd25519(edPk ed25519.PublicKey) { + pubKey.KeyType = ED25519 + copy(pubKey.Data[:], edPk) +} + + +func (pubKey *PublicKey) ToEd25519() ed25519.PublicKey { + return ed25519.PublicKey(pubKey.Data[:]) +} + +// AccessKey encodes a NEAR access key. +type AccessKey struct { + Nonce uint64 + Permission AccessKeyPermission +} + +// AccessKeyPermission encodes a NEAR access key permission. +type AccessKeyPermission struct { + Enum borsh.Enum `borsh_enum:"true"` // treat struct as complex enum when serializing/deserializing + FunctionCall FunctionCallPermission + FullAccess borsh.Enum +} + +// FunctionCallPermission encodes a NEAR function call permission (an access +// key permission). +type FunctionCallPermission struct { + Allowance *big.Int + ReceiverId string + MethodNames []string +} + +// A Transaction encodes a NEAR transaction. +type Transaction struct { + SignerID string + PublicKey PublicKey + Nonce uint64 + ReceiverID string + BlockHash [32]byte + Actions []Action +} + +// Action simulates an enum for Borsh encoding. +type Action struct { + Enum borsh.Enum `borsh_enum:"true"` // treat struct as complex enum when serializing/deserializing + CreateAccount borsh.Enum + DeployContract DeployContract + FunctionCall FunctionCall + Transfer Transfer + Stake Stake + AddKey AddKey + DeleteKey DeleteKey + DeleteAccount DeleteAccount +} + +// The DeployContract action. +type DeployContract struct { + Code []byte +} + +// The FunctionCall action. +type FunctionCall struct { + MethodName string + Args []byte + Gas uint64 + Deposit big.Int // u128 +} + +// The Transfer action. +type Transfer struct { + Deposit big.Int // u128 +} + +// The Stake action. +type Stake struct { + Stake big.Int // u128 + PublicKey PublicKey +} + +// The AddKey action. +type AddKey struct { + PublicKey PublicKey + AccessKey AccessKey +} + +// The DeleteKey action. +type DeleteKey struct { + PublicKey PublicKey +} + +// The DeleteAccount action. +type DeleteAccount struct { + BeneficiaryID string +} + +// A Signature used for signing transaction. +type Signature struct { + KeyType uint8 + Data [64]byte +} + +// SignedTransaction encodes signed transactions for NEAR. +type SignedTransaction struct { + Transaction Transaction + Signature Signature +} diff --git a/utils/key_pair.go b/utils/key_pair.go index abb346c..cd56217 100644 --- a/utils/key_pair.go +++ b/utils/key_pair.go @@ -5,24 +5,14 @@ import ( "fmt" "strings" + "github.com/aurora-is-near/near-api-go/types" "github.com/btcsuite/btcd/btcutil/base58" ) -// All supported key types -const ( - ED25519 = 0 -) - -// PublicKey encoding for NEAR. -type PublicKey struct { - KeyType uint8 - Data [32]byte -} - // PublicKeyFromEd25519 derives a public key in NEAR encoding from pk. -func PublicKeyFromEd25519(pk ed25519.PublicKey) PublicKey { - var pubKey PublicKey - pubKey.KeyType = ED25519 +func PublicKeyFromEd25519(pk ed25519.PublicKey) types.PublicKey { + var pubKey types.PublicKey + pubKey.KeyType = types.ED25519 copy(pubKey.Data[:], pk) return pubKey } diff --git a/verify_signature.go b/verify_signature.go new file mode 100644 index 0000000..ce992d2 --- /dev/null +++ b/verify_signature.go @@ -0,0 +1,76 @@ +package near + +import ( + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "errors" + + "github.com/aurora-is-near/near-api-go/types" + "github.com/near/borsh-go" +) + +// VerifyTransactionSignature verifies the signature of a given signed transaction. +// It returns true if the signature is valid, false otherwise. +func VerifyTransactionSignature(signedTx *types.SignedTransaction) (bool, error) { + // Serialize the unsigned transaction + unsignedTx := signedTx.Transaction + serializedTx, err := borsh.Serialize(unsignedTx) + if err != nil { + return false, err + } + + // Compute the hash of the serialized transaction + txHash := sha256.Sum256(serializedTx) + + // Extract the public key from the transaction + publicKeyData := unsignedTx.PublicKey.Data[:] + if unsignedTx.PublicKey.KeyType != types.ED25519 { + return false, errors.New("unsupported key type") + } + + // Extract the signature from the signed transaction + signatureData := signedTx.Signature.Data[:] + if signedTx.Signature.KeyType != types.ED25519 { + return false, errors.New("unsupported signature key type") + } + + // Verify the signature + isValid := ed25519.Verify(publicKeyData, txHash[:], signatureData) + if !isValid { + return false, errors.New("invalid signature") + } + + return true, nil +} + +// Verify signature of a given public key and signature +func VerifySignatureBytes(publicKey []byte, signature []byte, message []byte) (bool, error) { + pubKey := types.PublicKey{} + pubKey.FromEd25519(publicKey) + + signatureData := signature[:] + if len(signatureData) != ed25519.SignatureSize { + return false, errors.New("invalid signature size") + } + + hash := sha256.Sum256(message) + return ed25519.Verify(pubKey.ToEd25519(), hash[:], signatureData), nil +} + +// Verify signature of a given public key and signature as a hex string +func VerifySignatureHex(publicKeyHex string, signatureHex string, messageHex string) (bool, error) { + publicKey, err := hex.DecodeString(publicKeyHex) + if err != nil { + return false, err + } + signature, err := hex.DecodeString(signatureHex) + if err != nil { + return false, err + } + message, err := hex.DecodeString(messageHex) + if err != nil { + return false, err + } + return VerifySignatureBytes(publicKey, signature, message) +} diff --git a/verify_signature_test.go b/verify_signature_test.go new file mode 100644 index 0000000..d524356 --- /dev/null +++ b/verify_signature_test.go @@ -0,0 +1,87 @@ +package near + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "testing" +) + +func TestVerifySignature(t *testing.T) { + // Generate a new key pair + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("Failed to generate key pair: %v", err) + } + + // Create a message to sign + message := []byte("test message") + messageHex := hex.EncodeToString(message) + + // Hash the message + hash := sha256.Sum256(message) + + // Sign the hashed message + signature := ed25519.Sign(privKey, hash[:]) + signatureHex := hex.EncodeToString(signature) + + // Verify the signature + isValid, err := VerifySignatureBytes(pubKey, signature, message) + if err != nil { + t.Fatalf("Failed to verify signature: %v", err) + } + if !isValid { + t.Fatal("Signature is not valid") + } + + // Test invariant: Verify that the signature is invalid for a different message + otherMessage := []byte("different message") + isValid, err = VerifySignatureBytes(pubKey, signature, otherMessage) + if err != nil { + t.Fatalf("Failed to verify signature: %v", err) + } + if isValid { + t.Fatal("Signature should not be valid for a different message") + } + + // Test invariant: Verify that the signature is invalid for a different public key + otherPubKey, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("Failed to generate key pair: %v", err) + } + isValid, err = VerifySignatureBytes(otherPubKey, signature, message) + if err != nil { + t.Fatalf("Failed to verify signature: %v", err) + } + if isValid { + t.Fatal("Signature should not be valid for a different public key") + } + + // Test VerifySignatureHex + isValid, err = VerifySignatureHex(hex.EncodeToString(pubKey), signatureHex, messageHex) + if err != nil { + t.Fatalf("Failed to verify signature: %v", err) + } + if !isValid { + t.Fatal("Signature should be valid for the given public key") + } +} + +func TestVerifySignaturePrecomputed(t *testing.T) { + message := "+16005551111evm.test-account.testnet" + pubKey := "86fd8e75c00c88ad489b3c0c7dd8d13ef0953d4b03788acb05b281b2acd2bf86" + signature := "a45ea2a6999a69b35a8a9fc3eb60f2440c1d4f7a45641eb81f309a27dbd0342258159d99cd4f322deec8f028214e72072480c9d145b25d5f0f0f3df8f8f9c30b" + + // Convert message to hex + messageHex := hex.EncodeToString([]byte(message)) + + // Verify the signature + isValid, err := VerifySignatureHex(pubKey, signature, messageHex) + if err != nil { + t.Fatalf("Failed to verify signature: %v", err) + } + if !isValid { + t.Fatal("Signature should be valid for the given public key") + } +}