Skip to content

Commit fa2b82e

Browse files
authored
Merge pull request #286 from red-dev-inc/tutorials-contest
"sig-verify-tutorial" for Round 2
2 parents a3292a9 + 7c881b0 commit fa2b82e

File tree

2 files changed

+349
-0
lines changed

2 files changed

+349
-0
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
# Tutorial: Avalanche Signature Verification in a dApp
2+
3+
## Introduction
4+
5+
This tutorial will show you how to use an Avalanche C-Chain dApp to verify the signature of a message like this that has been signed using the Avalanche Wallet.
6+
7+
![WALLET MESSAGE PIC HERE](sig-verify-tutoria-00-walletsign.png)
8+
9+
We at [red·dev](https://www.red.dev) needed to do this for our current software project under development, [RediYeti](https://www.rediyeti.com). We have a use-case where we need to verify ownership of an Avalanche X-Chain address before the dApp sends funds related to this address. To prevent fraud, the verification must take place inside of the dApp.
10+
11+
If you need to implement the same kind of signature verification in your dApp, you will find this tutorial useful. You may also find this tutorial useful if you would like to learn how Avalanche signatures work and how cryptography is implemented on the X-Chain and on the C-Chain.
12+
13+
We have included code snippets throughout, and you can find the entire project [here](https://github.com/red-dev-inc/sig-verify-tutorial). Many steps are involved, but once you understand how they fit together, you will have a deeper understanding of how this aspect of Avalanche—and indeed cryptocurrencies in general—works.
14+
15+
## Audience
16+
17+
To get the most out of this tutorial, you will need to have a basic understanding of Javascript, Node, Solidity, and how to develop dApps in Avalanche. You should also know the basics of public key cryptography. Avalanche uses Elliptic Curve Cryptography (ECC) as do Bitcoin, Ethereum, and many others. The ECC algorithm used for digital signatures is called ECDSA (Elliptic Curve Digital Signature Algorithm). If you do not yet know about these topics, see the **Resources** section at the end for links to learn more.
18+
19+
## Why are digital signatures important?
20+
21+
A digital signature system allows you to generate your own private/public key pair. You can then use the private key to generate digital signatures which let you prove that (1) you are the owner of the public key and (2) that the signed message has not been altered—both without having to reveal the private key. With cryptocurrencies, keeping your private key secret is what lets you hold onto your funds, and signing messages is how you transfer funds to others, so digital signatures are indeed foundational to Avalanche.
22+
23+
## Overview
24+
25+
At the very highest level, here is an overview of the process we will take you through in this tutorial. First we use a simple webapp to gather the three inputs: signing address, message, and signature. We extract the cryptographic data from them that will be passed into the dApp to verify the signature.
26+
27+
The dApp then verifies in two steps. First, it makes sure that all the values passed to it are provably related to each other. Then, assuming that they are, it uses the [elliptic-curve-solidity](https://github.com/tdrerup/elliptic-curve-solidity) library, which we have slightly modified to work with Avalanche, to verify the signature.
28+
29+
The dApp returns its result to the webapp which displays it. Of course, in your dApp, you will want to take some more actions in the dApp based on the result, depending on your needs.
30+
31+
(**Note:** If you're already a Solidity coder, you might think that there is an easier way to do this using the EVM's built-in function **ecrecover**. However, there is one small hitch that makes using **ecrecover** impossible: it uses a different hashing method. While Avalanche uses SHA-256 followed by ripemd160, the EVM uses Keccak-256.)
32+
33+
We have set up a [demo webpage here](https://rediyeti.com/avax-sig-verify-demo).
34+
35+
## Requirements
36+
37+
Metamask needs to be installed on your browser, and you need to be connected to the Avalanche Fuji test network (for this tutorial). You can add a few lines of codes to check if your browser has Metamask installed, and if installed, then to which network you are connected. For instance:
38+
39+
```typescript
40+
function checkMetamaskStatus() {
41+
if((window as any).ethereum) {
42+
if((window as any).ethereum.chainId != '0xa869') {
43+
result = "Failed: Not connected to Avalanche Fuji Testnet via Metamask."
44+
}
45+
else {
46+
//call required function
47+
}
48+
}
49+
else {
50+
result = "Failed: Metamask is not installed."
51+
}
52+
}
53+
```
54+
55+
## Dependencies
56+
57+
1. NodeJS v8.9.4 or later.
58+
2. AvalancheJS library, which you can install with `npm install avalanche`
59+
3. Elliptic Curve library, which can be installed with `npm install elliptic`
60+
4. Ethers.js library, which can be installed with `npm install ethers`
61+
62+
## Steps that need to be performed in the webapp
63+
64+
To verify the signature and retrieve the signer X-Chain address, you first need to extract cryptographic data from the message and signature in your webapp, which will then be passed to the dApp. (The example code uses a **Vue** webapp, but you could use any framework you like or just Vanilla Javascript.) These are:
65+
66+
1. The hashed message
67+
2. The r, s, and v parameters of the signature
68+
3. The x and y coordinates of the public key and the 33-byte compressed public key
69+
70+
We extract them using Javascript instead of in the dApp because the Solidity EC library needs them to be separated, and it is easier to do it in Javascript. There is no security risk in doing it here off-chain as we can verify in the dApp that they are indeed related to each other, returning a signature failure if they are not.
71+
72+
### 1. Hash the message
73+
74+
First, you will need to hash the original message. Here is the standard way of hashing the message based on the Bitcoin Script format and Ethereum format:
75+
76+
***sha256(length(prefix) + prefix + length(message) + message)***
77+
78+
The prefix is a so-called "magic prefix" string `\x1AAvalanche Signed Message:\n`, where `0x1A` is the length of the prefix text and `length(message)` is an integer of the message size. After concatenating these together, hash the result with `sha256`. For example:
79+
80+
```typescript
81+
function hashMessage(message: string) {
82+
let mBuf: Buffer = Buffer.from(message, 'utf8')
83+
let msgSize: Buffer = Buffer.alloc(4)
84+
msgSize.writeUInt32BE(mBuf.length, 0)
85+
let msgBuf: Buffer = Buffer.from(`\x1AAvalanche Signed Message:\n${msgSize}${message}`, 'utf8')
86+
let hash: Buffer = createHash('sha256').update(msgBuf).digest()
87+
let hashex: string = hash.toString('hex')
88+
let hashBuff: Buffer = Buffer.from(hashex, 'hex')
89+
let messageHash: string = '0x' + hashex
90+
return {hashBuff, messageHash}
91+
}
92+
```
93+
94+
### 2. Split the Signature
95+
96+
Avalanche Wallet displays the signature in CB58 Encoded form, so first you will need to decode the signature from CB58.
97+
98+
Then, with the decoded signature, you can recover the public key by parsing out the r, s, and v parameters from it. The signature is stored as a 65-byte buffer `[R || S || V]` where `V` is 0 or 1 to allow public key recoverability.
99+
100+
Note, while decoding the signature, if the signature has been altered, the **cb58Decode** function may throw an error, so remember to catch the error. Also, don't forget to import **bintools** from AvalancheJS library first.
101+
102+
```typescript
103+
function splitSig(signature: string) {
104+
try{
105+
let bintools: Bintools = BinTools.getInstance()
106+
let decodedSig: Buffer = bintools.cb58Decode(signature)
107+
const r: BN = new BN(bintools.copyFrom(decodedSig, 0, 32))
108+
const s: BN = new BN(bintools.copyFrom(decodedSig, 32, 64))
109+
const v: number = bintools.copyFrom(decodedSig, 64, 65).readUIntBE(0, 1)
110+
const sigParam: any = {
111+
r: r,
112+
s: s,
113+
v: v
114+
}
115+
let rhex: string = '0x' + r.toString('hex') //converts r to hex
116+
let shex: string = '0x' + s.toString('hex') //converts s to hex
117+
let sigHex: Array<string> = [rhex, shex]
118+
return {sigParam, sigHex}
119+
}
120+
catch{
121+
result = "Failed: Invalid signature."
122+
}
123+
},
124+
```
125+
126+
### 3. Recover the public key
127+
128+
The public key can be recovered from the hashed message, r, s, and v parameters of the signature together with the help of Elliptic Curve JS library. You need to extract x and y coordinates of the public key to verify the signature as well as the 33-byte compressed public key to later recover the signer's X-Chain address.
129+
130+
```typescript
131+
function recover(msgHash: Buffer, sig: any) {
132+
let ec: EC = new EC('secp256k1')
133+
const pubk: any = ec.recoverPubKey(msgHash, sig, sig.v)
134+
const pubkx: string = '0x' + pubk.x.toString('hex')
135+
const pubky: string = '0x' + pubk.y.toString('hex')
136+
let pubkCord: Array<string> = [pubkx, pubky]
137+
let pubkBuff: Buffer = Buffer.from(pubk.encodeCompressed())
138+
return {pubkCord, pubkBuff}
139+
}
140+
```
141+
142+
Here is the full code for verification, including the call to the dApp function **recoverAddress** at the end, which we will cover next:
143+
144+
```typescript
145+
async function verify() {
146+
//Create the provider and contract object to access the dApp functions
147+
const provider: any = new ethers.providers.Web3Provider((window as any).ethereum)
148+
const elliptic: any = new ethers.Contract(contractAddress.Contract, ECArtifact.abi, provider)
149+
//Extract all the data needed for signature verification
150+
let message: any = hashMessage(msg)
151+
let sign: any = splitSig(sig)
152+
let publicKey: any = recover(message.hashBuff, sign.sigParam)
153+
//prefix and hrp for Bech32 encoding
154+
let prefix: string = "fuji"
155+
let hrp: Array<any> = []
156+
for (var i=0; i<prefix.length; i++) {
157+
hrp[i] = prefix.charCodeAt(i)
158+
}
159+
//Call recoverAddress function from dApp. xchain and msg are user inputs in webapp
160+
const tx: string = await elliptic.recoverAddress(message.messageHash, sign.sigHex, publicKey.pubkCord, publicKey.pubkBuff, msg, xchain, prefix, hrp)
161+
result = tx
162+
}
163+
```
164+
165+
## Recover the signer X-Chain address in dApp
166+
167+
In the dApp, receive as a parameter the 33-byte compressed public key to recover the X-Chain Address.
168+
169+
Addresses on the X-Chain use the Bech32 standard with an Avalanche-specific prefix of **X-**. Then there are four parts to the Bech32 address scheme that follow.
170+
171+
1. A human-readable part (HRP). On Avalanche mainnet this is **avax** and on Fuji testnet it is **fuji**.
172+
2. The number **1**, which separates the HRP from the address and error correction code.
173+
3. A base-32 encoded string representing the 20 byte address.
174+
4. A 6-character base-32 encoded error correction code.
175+
176+
Like Bitcoin, the addressing scheme of the Avalanche X-Chain relies on the **secp256k1** elliptic curve. Avalanche follows a similar approach as Bitcoin and hashes the ECDSA public key, so the 33-byte compressed public key is hashed with **sha256** first and then the result is hashed with **ripemd160** to produce a 20-byte address.
177+
178+
Next, this 20-byte value is converted to a **Bech32** address.
179+
180+
The `recoverAddress` function is called in the dApp from the webapp.
181+
182+
**recoverAddress** takes the following parameters:
183+
184+
* messageHash — the hashed message
185+
* rs — r and s value of the signature
186+
* publicKey — x and y coordinates of the public key
187+
* pubk — 33-byte compressed public key
188+
* xchain — X-Chain address
189+
* prefix — Prefix for Bech32 addressing scheme (avax or fuji)
190+
* hrp — Array of each unicode character in the prefix
191+
192+
Then it performs the following steps:
193+
194+
1. Gets the 20-byte address by hashing the 33-byte compressed public key with sha256 followed by ripemd160.
195+
2. Calls the Bech32 functions to convert the 20-byte address to Bech32 address (which is the unique part of the X-Chain address).
196+
3. Verifies that the extracted X-Chain address matches with the X-Chain address from the webapp.
197+
4. If X-Chain Address matches then validates the signature.
198+
5. Returns the result.
199+
200+
Here is the recoverAddress function that does all this:
201+
202+
```solidity
203+
function recoverAddress(bytes32 messageHash, uint[2] memory rs, uint[2] memory publicKey, bytes memory pubk, string memory xchain, string memory prefix, uint[] memory hrp) public view returns(string memory){
204+
bool signVerification = false;
205+
string memory result;
206+
bytes32 sha = sha256(abi.encodePacked(pubk));
207+
bytes20 ripesha = ripemd160(abi.encodePacked(sha));
208+
uint[] memory rp = new uint[](20);
209+
for(uint i=0;i<20;i++) {
210+
rp[i] = uint(uint8(ripesha[i]));
211+
}
212+
bytes memory pre = bytes(prefix);
213+
string memory xc = encode(pre, hrp, convert(rp, 8, 5));
214+
if(keccak256(abi.encodePacked(xc)) == keccak256(abi.encodePacked(xchain))) {
215+
signVerification = validateSignature(messageHash, rs, publicKey);
216+
if(signVerification == true) {
217+
result = "Signature verified!";
218+
}
219+
else {
220+
result = "Signature verification failed!";
221+
}
222+
}
223+
else {
224+
result = string(abi.encodePacked("Failed: Addresses do not match. Address for this signature/message combination should be ", xc));
225+
}
226+
return result;
227+
}
228+
```
229+
230+
Let's take a closer look at its supporting functions and key features.
231+
232+
### Bech32 Encoding
233+
234+
We have ported Bech32 to Solidity from the [Bech32 Javascript library](https://github.com/bitcoinjs/bech32).
235+
There are four functions, **polymod**, **prefixChk**, **encode** and **convert**, used to convert to Bech32 address.
236+
237+
```solidity
238+
bytes constant CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
239+
240+
function polymod(uint256 pre) internal view returns(uint) {
241+
uint256 chk = pre >> 25;
242+
chk = ((pre & 0x1ffffff) << 5)^(-((chk >> 0) & 1) & 0x3b6a57b2) ^
243+
(-((chk >> 1) & 1) & 0x26508e6d) ^
244+
(-((chk >> 2) & 1) & 0x1ea119fa) ^
245+
(-((chk >> 3) & 1) & 0x3d4233dd) ^
246+
(-((chk >> 4) & 1) & 0x2a1462b3);
247+
return chk;
248+
}
249+
250+
function prefixCheck(uint[] memory hrp) public view returns (uint) {
251+
uint chk = 1;
252+
uint c;
253+
uint v;
254+
for (uint pm = 0; pm < hrp.length; ++pm) {
255+
c = hrp[pm];
256+
chk = polymod(chk) ^ (c >> 5);
257+
}
258+
chk = polymod(chk);
259+
for (uint pm = 0; pm < hrp.length; ++pm) {
260+
v = hrp[pm];
261+
chk = polymod(chk) ^ (v & 0x1f);
262+
}
263+
return chk;
264+
}
265+
266+
function encode(bytes memory prefix, uint[] memory hrp, uint[] memory data) public view returns (string memory) {
267+
uint256 chk = prefixCheck(hrp);
268+
bytes memory add = '1';
269+
bytes memory result = abi.encodePacked(prefix, add);
270+
for (uint pm = 0; pm < data.length; ++pm) {
271+
uint256 x = data[pm];
272+
chk = polymod(chk) ^ x;
273+
result = abi.encodePacked(result, CHARSET[x]);
274+
}
275+
for (uint i = 0; i < 6; ++i) {
276+
chk = polymod(chk);
277+
}
278+
chk ^= 1;
279+
for (uint i = 0; i < 6; ++i) {
280+
uint256 v = (chk >> ((5 - i) * 5)) & 0x1f;
281+
result = abi.encodePacked(result, CHARSET[v]);
282+
}
283+
bytes memory chainid = 'X-';
284+
string memory s = string(abi.encodePacked(chainid, result));
285+
return s;
286+
}
287+
288+
function convert(uint[] memory data, uint inBits, uint outBits) public view returns (uint[] memory) {
289+
uint value = 0;
290+
uint bits = 0;
291+
uint maxV = (1 << outBits) - 1;
292+
uint[] memory ret = new uint[](32);
293+
uint j = 0;
294+
for (uint i = 0; i < data.length; ++i) {
295+
value = (value << inBits) | data[i];
296+
bits += inBits;
297+
while (bits >= outBits) {
298+
bits -= outBits;
299+
ret[j] = (value >> bits) & maxV;
300+
j += 1;
301+
}
302+
}
303+
return ret;
304+
}
305+
```
306+
307+
### Verify X-Chain address
308+
309+
It is a simple step, but it is very important to check to see if the extracted X-Chain address from the public key matches with the X-Chain address that was passed from the webapp. Otherwise, you may have a perfectly valid message signature but for a _different_ X-Chain address than the webapp requested. Only if they match can you proceed to verify the signature. Otherwise, return an error message.
310+
311+
### Validate signature
312+
313+
For verifying the signature, we start with this [Solidity project on Elliptic Curve](https://github.com/tdrerup/elliptic-curve-solidity).
314+
315+
However, this project uses the **secp256r1** curve. As Avalanche uses **secp256k1** curve, we need to modify the constant values based on this curve. (**Note:** Since modifying cryptographic functions is risky, these are the only modifications we have made.) The constants now look like this:
316+
317+
```solidity
318+
uint constant a = 0;
319+
uint constant b = 7;
320+
uint constant gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798;
321+
uint constant gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8;
322+
uint constant p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F;
323+
uint constant n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
324+
```
325+
326+
The key function we need is **validateSignature**.
327+
328+
```solidity
329+
function validateSignature(bytes32 messageHash, uint[2] memory rs, uint[2] memory publicKey) public view returns (bool)
330+
```
331+
332+
**validateSignature** takes the same first three parameters as **recoverAddress**:
333+
334+
* messageHash — the hashed message
335+
* rs — r and s value of the signature
336+
* publicKey — x and y coordinates of the public key
337+
338+
### Finishing up
339+
340+
After performing these tests, the dApp returns its decision whether the signature is valid or not to the webapp, and the webapp is then responsible for showing the final output to the user. As we mentioned above, in your dApp, you will probably want to take further actions accordingly.
341+
342+
## Resources
343+
344+
Here are some resources that can use to teach yourself the subjects you need in order to understand this tutorial.
345+
346+
1. This is a useful documentation from Ava Labs on cryptographic primitives: <https://docs.avax.network/build/references/cryptographic-primitives>
347+
2. Here is a great YouTube video by Connor Daly of Ava Labs on how to use Hardhat to deploy and run your smart contract on Avalanche network: <https://www.youtube.com/watch?v=UdzHxdfMKkE&t=1812s>
348+
3. If you want to learn more on how the private/public keys and the wallets work, you may enjoy going through this awesome tutorial by Greg Walker: <https://learnmeabitcoin.com/technical/>
349+
4. Andrea Corbellini has done great work explaining Elliptic Curve Cryptography in detail in her blog post: <https://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/>
41.2 KB
Loading

0 commit comments

Comments
 (0)