Skip to content

Commit

Permalink
Feat: Add a way to verify signature inside the Ts SDK (#135)
Browse files Browse the repository at this point in the history
* Feat: messages signatures can't be only verified on the Aleph's Network

Solution: Add a way to verify signatures directly in the SDK

* Feat: signature can't be verified without an instanced account

Solution: move verify method into utils/signature

* Feat: Add signature verification for Solana inside the ts sdk

* Feat: Add signature verification for Substrate inside the ts sdk

* Feat: Add signature verification for Tezos inside the ts sdk

* verifCosmos script added without test

* fix imports

* rename "verif" to "verify" everywhere

* use default import for verify functions

---------

Co-authored-by: leirbag95 <[email protected]>
Co-authored-by: mhh <[email protected]>
  • Loading branch information
3 people authored Jan 4, 2024
1 parent dc5a82e commit 32caa63
Show file tree
Hide file tree
Showing 20 changed files with 534 additions and 15 deletions.
8 changes: 7 additions & 1 deletion src/accounts/avalanche.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { BaseProviderWallet } from "./providers/BaseProviderWallet";
import { providers } from "ethers";
import { privateToAddress } from "ethereumjs-util";
import { ProviderEncryptionLabel, ProviderEncryptionLib } from "./providers/ProviderEncryptionLib";
import verifyAvalanche from "../utils/signature/verifyAvalanche";

/**
* AvalancheAccount implements the Account class for the Avalanche protocol.
* It is used to represent an Avalanche account when publishing a message on the Aleph network.
Expand Down Expand Up @@ -129,7 +131,10 @@ export class AvalancheAccount extends ECIESAccount {
const signatureBuffer = this.signer?.sign(digestBuff);

const bintools = BinTools.getInstance();
return bintools.cb58Encode(signatureBuffer);
const signature = bintools.cb58Encode(signatureBuffer);
if (await verifyAvalanche(buffer, signature, this.signer.getPublicKey().toString("hex"))) return signature;

throw new Error("Cannot proof the integrity of the signature");
} else if (this.provider) {
return await this.provider.signMessage(buffer);
}
Expand Down Expand Up @@ -176,6 +181,7 @@ export async function getKeyPair(privateKey?: string, chain = ChainType.X_CHAIN)
* It creates an Avalanche keypair containing information about the account, extracted in the AvalancheAccount constructor.
*
* @param privateKey The private key of the account to import.
* @param chain The Avalanche subnet to use the account with.
*/
export async function ImportAccountFromPrivateKey(
privateKey: string,
Expand Down
10 changes: 7 additions & 3 deletions src/accounts/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { ethers } from "ethers";
import { ECIESAccount } from "./account";
import { GetVerificationBuffer } from "../messages";
import { BaseMessage, Chain } from "../messages/types";
import verifyEthereum from "../utils/signature/verifyEthereum";
import { BaseProviderWallet } from "./providers/BaseProviderWallet";
import { decrypt as secp256k1_decrypt, encrypt as secp256k1_encrypt } from "eciesjs";
import { ChangeRpcParam, JsonRPCWallet, RpcChainType } from "./providers/JsonRPCWallet";
import { BaseProviderWallet } from "./providers/BaseProviderWallet";
import { ProviderEncryptionLabel, ProviderEncryptionLib } from "./providers/ProviderEncryptionLib";

/**
Expand Down Expand Up @@ -109,9 +110,12 @@ export class ETHAccount extends ECIESAccount {
const buffer = GetVerificationBuffer(message);
const signMethod = this.wallet || this.provider;

if (signMethod) return signMethod.signMessage(buffer.toString());
if (!signMethod) throw new Error("Cannot sign message");

const signature = await signMethod.signMessage(buffer.toString());
if (verifyEthereum(buffer, signature, this.address)) return signature;

throw new Error("Cannot sign message");
throw new Error("Cannot proof the integrity of the signature");
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/accounts/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GetVerificationBuffer } from "../messages";
import { Keypair, PublicKey } from "@solana/web3.js";
import nacl from "tweetnacl";
import base58 from "bs58";
import verifySolana from "../utils/signature/verifySolana";

type WalletSignature = {
signature: Uint8Array;
Expand Down Expand Up @@ -63,10 +64,13 @@ export class SOLAccount extends Account {
throw new Error("Cannot sign message");
}

return JSON.stringify({
const parsedSignature = JSON.stringify({
signature: base58.encode(signature),
publicKey: this.address,
});
if (verifySolana(buffer, parsedSignature)) return parsedSignature;

throw new Error("Cannot proof the integrity of the signature");
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/accounts/substrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { GetVerificationBuffer } from "../messages";
import { InjectedExtension } from "@polkadot/extension-inject/types";
import { Keyring } from "@polkadot/keyring";
import { KeyringPair } from "@polkadot/keyring/types";
import { cryptoWaitReady, signatureVerify } from "@polkadot/util-crypto";
import { cryptoWaitReady } from "@polkadot/util-crypto";
import { generateMnemonic } from "@polkadot/util-crypto/mnemonic/bip39";
import verifySubstrate from "../utils/signature/verifySubstrate";
import { stringToHex } from "@polkadot/util";

/**
Expand Down Expand Up @@ -58,12 +59,13 @@ export class DOTAccount extends Account {
}
}

if (!signatureVerify(buffer, signed, this.address).isValid) throw new Error("Data can't be signed.");

return JSON.stringify({
const signature = JSON.stringify({
curve: "sr25519",
data: signed,
});
if (verifySubstrate(message, signature, this.address)) return signature;

throw new Error("Cannot proof the integrity of the signature");
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/accounts/tezos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class TEZOSAccount extends Account {
} else {
signature = (await this.wallet.sign(payloadBytes)).sig;
}

return JSON.stringify({
signature: signature,
publicKey: await this.GetPublicKey(),
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as accounts from "./accounts/index";
import * as messages from "./messages";
import * as Ledger from "./accounts/providers/Ledger";
import * as utils from "./utils/signature";

export { accounts, Ledger, messages };
export { accounts, Ledger, messages, utils };
8 changes: 8 additions & 0 deletions src/utils/signature/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import verifyEthereum from "./verifyEthereum";
import verifyAvalanche from "./verifyAvalanche";
import verifySolana from "./verifySolana";
import verifySubstrate from "./verifySubstrate";
import verifyTezos from "./verifyTezos";
import verifyCosmos from "./verifyCosmos";

export { verifyEthereum, verifyAvalanche, verifySolana, verifySubstrate, verifyTezos, verifyCosmos };
39 changes: 39 additions & 0 deletions src/utils/signature/verifyAvalanche.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BaseMessage } from "../../messages/types";
import { GetVerificationBuffer } from "../../messages";
import { Avalanche, BinTools, Buffer as AvaBuff } from "avalanche";
import shajs from "sha.js";

async function digestMessage(message: Buffer) {
const msgSize = Buffer.alloc(4);
msgSize.writeUInt32BE(message.length, 0);
const msgStr = message.toString("utf-8");
const msgBuf = Buffer.from(`\x1AAvalanche Signed Message:\n${msgSize}${msgStr}`, "utf8");

return new shajs.sha256().update(msgBuf).digest();
}

/**
* Provide a way to verify the authenticity of a signature associated with a given message.
* This method rely on the Keypair.recover() implementation.
*
* @param message The content of the signature to verify. It can be the result of GetVerificationBuffer() or directly a BaseMessage object.
* @param signature The signature associated with the first params of this method.
* @param signerPKey Optional, The publicKey associated with the signature to verify. It Needs to be under a hex serialized string.
*/
async function verifyAvalanche(message: Buffer | BaseMessage, signature: string, signerPKey: string): Promise<boolean> {
if (!(message instanceof Buffer)) message = GetVerificationBuffer(message);
const ava = new Avalanche();
const keyPair = ava.XChain().keyChain().makeKey();

const bintools = BinTools.getInstance();
const readableSignature = bintools.cb58Decode(signature);

const digest = await digestMessage(message);
const digestHex = digest.toString("hex");
const digestBuff = AvaBuff.from(digestHex, "hex");

const recovered = keyPair.recover(digestBuff, readableSignature);
return signerPKey === recovered.toString("hex");
}

export default verifyAvalanche;
40 changes: 40 additions & 0 deletions src/utils/signature/verifyCosmos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { BaseMessage } from "../../messages/types";
import { GetVerificationBuffer } from "../../messages";
import elliptic from "elliptic";

/**
* Provide a way to verify the authenticity of a signature associated with a given message.
* This method rely on the ethers.utils.verifyMessage() implementation.
*
* @param message The content of the signature to verify. It can be the result of GetVerificationBuffer() or directly a BaseMessage object.
* @param serializedSignature The signature associated with the first params of this method.
*/
async function verifyCosmos(message: Buffer | BaseMessage, serializedSignature: string): Promise<boolean> {
if (!(message instanceof Buffer)) message = GetVerificationBuffer(message);

const { signature, pub_key } = JSON.parse(serializedSignature);
const secp256k1 = new elliptic.ec("secp256k1");

// unsupported curve checking
if (pub_key?.type !== "tendermint/PubKeySecp256k1") return false;

// Decode the Base64-encoded signature
const publicKey = Buffer.from(pub_key.value, "base64");
const signatureBuffer = Buffer.from(signature, "base64");

// Extract the r and s values from the signature
const r = signatureBuffer.slice(0, 32);
const s = signatureBuffer.slice(32, 64);

// Create a signature object with the r and s values
const signatureObj = { r, s };

try {
const key = secp256k1.keyFromPublic(publicKey);
return key.verify(message, signatureObj);
} catch (e: unknown) {
return false;
}
}

export default verifyCosmos;
24 changes: 24 additions & 0 deletions src/utils/signature/verifyEthereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BaseMessage } from "../../messages/types";
import { GetVerificationBuffer } from "../../messages";
import { ethers } from "ethers";

/**
* Provide a way to verify the authenticity of a signature associated with a given message.
* This method rely on the ethers.utils.verifyMessage() implementation.
*
* @param message The content of the signature to verify. It can be the result of GetVerificationBuffer() or directly a BaseMessage object.
* @param signature The signature associated with the first params of this method.
* @param signerAddress Optional, The address associated with the signature to verify. The current account address is used by default.
*/
function verifyEthereum(message: Buffer | BaseMessage, signature: string, signerAddress: string): boolean {
if (!(message instanceof Buffer)) message = GetVerificationBuffer(message);

try {
const address = ethers.utils.verifyMessage(message, signature);
return address === signerAddress;
} catch (e: unknown) {
return false;
}
}

export default verifyEthereum;
24 changes: 24 additions & 0 deletions src/utils/signature/verifySolana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BaseMessage } from "../../messages/types";
import { GetVerificationBuffer } from "../../messages";
import nacl from "tweetnacl";
import bs58 from "bs58";

/**
* Provide a way to verify the authenticity of a signature associated with a given message.
* This method rely on the nacl.sign.detached.verify() implementation.
*
* @param message The content of the signature to verify. It can be the result of GetVerificationBuffer() or directly a BaseMessage object.
* @param serializedSignature The signature associated with the first params of this method.
*/
function verifySolana(message: Buffer | BaseMessage, serializedSignature: string): boolean {
if (!(message instanceof Buffer)) message = GetVerificationBuffer(message);
const { signature, publicKey } = JSON.parse(serializedSignature);

try {
return nacl.sign.detached.verify(message, bs58.decode(signature), bs58.decode(publicKey));
} catch (e: unknown) {
return false;
}
}

export default verifySolana;
26 changes: 26 additions & 0 deletions src/utils/signature/verifySubstrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BaseMessage } from "../../messages/types";
import { GetVerificationBuffer } from "../../messages";
import { signatureVerify } from "@polkadot/util-crypto";

/**
* Provide a way to verify the authenticity of a signature associated with a given message.
* This method rely on the signatureVerify() implementation from @polkadot/util-crypto.
*
* @param message The content of the signature to verify. It can be the result of GetVerificationBuffer() or directly a BaseMessage object.
* @param signature The signature associated with the first params of this method.
* @param signerAddress Optional, The address associated with the signature to verify. The current account address is used by default.
*/
function verifySubstrate(message: Buffer | BaseMessage, signature: string, signerAddress: string): boolean {
if (!(message instanceof Buffer)) message = GetVerificationBuffer(message);
const parsedSignature = JSON.parse(signature);

try {
const result = signatureVerify(message, parsedSignature.data, signerAddress);

return result.isValid;
} catch (e: unknown) {
return false;
}
}

export default verifySubstrate;
29 changes: 29 additions & 0 deletions src/utils/signature/verifyTezos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { BaseMessage } from "../../messages/types";
import { GetVerificationBuffer } from "../../messages";
import { char2Bytes, verifySignature } from "@taquito/utils";

/**
* Provide a way to verify the authenticity of a signature associated with a given message.
* This method rely on the verifySignature() implementation from taquito/utils.
*
* @param message The content of the signature to verify. It needs to be a BaseMessage object.
* @param signature The signature associated with the first params of this method.
*/
function verifyTezos(message: BaseMessage, signature: string): boolean {
const { signature: parsedSignature, publicKey, dAppUrl } = JSON.parse(signature);

const buffer = GetVerificationBuffer(message);
const ISO8601formattedTimestamp = new Date(message.time).toISOString();
const formattedInput: string = [
"Tezos Signed Message:",
dAppUrl,
ISO8601formattedTimestamp,
buffer.toString(),
].join(" ");
const bytes = char2Bytes(formattedInput);
const payloadBytes = "05" + "0100" + char2Bytes(String(bytes.length)) + bytes;

return verifySignature(payloadBytes, publicKey, parsedSignature);
}

export default verifyTezos;
60 changes: 60 additions & 0 deletions tests/accounts/avalanche.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { avalanche, post } from "../index";
import { ItemType, MessageType } from "../../src/messages/types";
import { EthereumProvider } from "../providers/ethereumProvider";
import { GetVerificationBuffer } from "../../src/messages";
import verifyAvalanche from "../index";
import { EphAccountList } from "../testAccount/entryPoint";
import fs from "fs";

Expand Down Expand Up @@ -177,4 +180,61 @@ describe("Avalanche accounts", () => {
expect(amends.posts[0].content).toStrictEqual(content);
});
});

it("Should success to verif the authenticity of a signature", async () => {
const { account } = await avalanche.NewAccount();

const message = {
chain: account.GetChain(),
sender: account.address,
type: MessageType.post,
channel: "TEST",
confirmed: true,
signature: "signature",
size: 15,
time: 15,
item_type: ItemType.storage,
item_content: "content",
item_hash: "hash",
content: { address: account.address, time: 15 },
};
if (!account.publicKey) throw Error();
const signature = await account.Sign(message);
const verif = await verifyAvalanche(GetVerificationBuffer(message), signature, account.publicKey);
const verifB = await verifyAvalanche(message, signature, account.publicKey);

expect(verif).toStrictEqual(true);
expect(verifB).toStrictEqual(true);
});

it("Should fail to verif the authenticity of a signature", async () => {
const { account: account } = await avalanche.NewAccount();
const { account: fakeAccount } = await avalanche.NewAccount();

const message = {
chain: account.GetChain(),
sender: account.address,
type: MessageType.post,
channel: "TEST",
confirmed: true,
signature: "signature",
size: 15,
time: 15,
item_type: ItemType.storage,
item_content: "content",
item_hash: "hash",
content: { address: account.address, time: 15 },
};
const fakeMessage = {
...message,
item_hash: "FAKE",
};
if (!account.publicKey || !fakeAccount.publicKey) throw Error();
const fakeSignature = await account.Sign(fakeMessage);
const verif = await verifyAvalanche(GetVerificationBuffer(message), fakeSignature, account.publicKey);
const verifB = await verifyAvalanche(fakeMessage, fakeSignature, fakeAccount.publicKey);

expect(verif).toStrictEqual(false);
expect(verifB).toStrictEqual(false);
});
});
Loading

0 comments on commit 32caa63

Please sign in to comment.