Skip to content

Commit

Permalink
feat: add verifyArbitrary util function
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronCQL committed Jan 4, 2024
1 parent 7eff51e commit 919f138
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 4 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## `v0.0.49`

### Features

- Added the `verifyArbitrary` helper function (exported by `cosmes/wallet`) to verify signatures signed using `ConnectedWallet.signArbitrary`

### Fixes

- Fixed the `recoverPubKeyFromEthSignature` helper function to calculate and use the correct recovery bit when generating the `secp256k1` model

## `v0.0.48`

### Features
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cosmes",
"version": "0.0.48",
"version": "0.0.49",
"private": false,
"packageManager": "pnpm@8.3.0",
"sideEffects": false,
Expand Down
1 change: 1 addition & 0 deletions src/codec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export {
signAmino,
signDirect,
} from "./sign";
export { verifyADR36, verifyECDSA, verifyEIP191 } from "./verify";
11 changes: 8 additions & 3 deletions src/codec/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ export function recoverPubKeyFromEthSignature(
if (signature.length !== 65) {
throw new Error("Invalid signature");
}
const digest = hashEthArbitraryMessage(message);
const r = signature.slice(0, 32);
const s = signature.slice(32, 64);
const v = signature[64];
// Adapted from https://github.com/ethers-io/ethers.js/blob/6017d3d39a4d428793bddae33d82fd814cacd878/src.ts/crypto/signature.ts#L255-L265
const yParity = v <= 1 ? v : (v + 1) % 2;
const secpSignature = secp256k1.Signature.fromCompact(
Uint8Array.from([...signature.slice(0, 32), ...signature.slice(32, 64)])
).addRecoveryBit(1);
Uint8Array.from([...r, ...s])
).addRecoveryBit(yParity);
const digest = hashEthArbitraryMessage(message);
return secpSignature.recoverPublicKey(digest).toRawBytes(true);
}
100 changes: 100 additions & 0 deletions src/codec/verify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { base64, utf8 } from "@scure/base";
import { describe, expect, it } from "vitest";

import { verifyADR36, verifyECDSA, verifyEIP191 } from "./verify";

const DATA = utf8.decode(
"Hi from CosmeES! This is a test message just to prove that the wallet is working."
);
// Generated using coin type "330" and seed phrase "poverty flat amazing draw goose clay sorry nothing erase switch law intact only invest find memory what weasel fan connect tilt detect trap viable"
const VALID_PUBKEY_1 = base64.decode(
"Ai7ZXTtRWFte/tX7Z6MlKWVd9XA49p3cDNqd61RuKTdT"
);
// Generated using coin type "118" and seed phrase "poverty flat amazing draw goose clay sorry nothing erase switch law intact only invest find memory what weasel fan connect tilt detect trap viable"
const VALID_PUBKEY_2 = base64.decode(
"A8i9vMNUGcTtUgpbmiZqcFtsIrPZ6n8ZYN4/PVRlQvGr"
);
// Generated using coin type "60" and seed phrase "poverty flat amazing draw goose clay sorry nothing erase switch law intact only invest find memory what weasel fan connect tilt detect trap viable"
const VALID_PUBKEY_3 = base64.decode(
"AmGjuPKUsuIAuGgJ3xH7KGWlSU9cwVnsesrwWwyYLbMg"
);

describe("verifyECDSA", () => {
it("should verify correctly", () => {
// Signed using Station wallet on Terra
const signature = base64.decode(
"Od87qNoOyXuDOVdLCGTXB6dFN7U0XF9Oegc8KDa+AWwX3jkrDXG++2nlPfsF4VJzlDHsoikPeZpxrB7v9PINnw=="
);
const res1 = verifyECDSA({
pubKey: VALID_PUBKEY_1,
data: DATA,
signature,
});
expect(res1).toBe(true);

// Different pub key
const res2 = verifyECDSA({
pubKey: VALID_PUBKEY_2,
data: DATA,
signature,
});
expect(res2).toBe(false);
});
});

describe("verifyADR36", () => {
it("should verify correctly", () => {
// Signed using Keplr wallet on Osmosis
const signature = base64.decode(
"nvlcV0x0Ge8ADXLSAQGtfMw6EJkOfpmkDxgP7UI79uR8MhnAOp9T+e+ofgW9kY4bEIr0yhyBG+vSVAZRv9uCxA=="
);
const res1 = verifyADR36({
bech32Prefix: "osmo",
pubKey: VALID_PUBKEY_2,
data: DATA,
signature,
});
expect(res1).toBe(true);

// Different bech32 prefix
const res2 = verifyADR36({
bech32Prefix: "terra",
pubKey: VALID_PUBKEY_2,
data: DATA,
signature,
});
expect(res2).toBe(false);

// Different pub key
const res3 = verifyADR36({
bech32Prefix: "osmo",
pubKey: VALID_PUBKEY_1,
data: DATA,
signature,
});
expect(res3).toBe(false);
});
});

describe("verifyEIP191", () => {
it("should verify correctly", () => {
// Signed using MetaMask wallet on Injective
const signature = base64.decode(
"MpriWY0Kq7C+/jR3eOfNB5vUQM144tQk7KkzKyYCTFB5QHGLZjzJyeOSr8/ENFES0k+aaEF47Wepk7OHoZuLzxs="
);
const res1 = verifyEIP191({
pubKey: VALID_PUBKEY_3,
data: DATA,
signature,
});
expect(res1).toBe(true);

// Different pub key
const res2 = verifyEIP191({
pubKey: VALID_PUBKEY_2,
data: DATA,
signature,
});
expect(res2).toBe(false);
});
});
70 changes: 70 additions & 0 deletions src/codec/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { sha256 } from "@noble/hashes/sha256";
import * as secp256k1 from "@noble/secp256k1";
import { base64 } from "@scure/base";

import { resolveBech32Address } from "./address";
import { serialiseSignDoc } from "./serialise";
import { recoverPubKeyFromEthSignature } from "./sign";

type VerifyArbitraryParams = {
/** The public key which created the signature */
pubKey: Uint8Array;
/** The bech32 account address prefix of the signer */
bech32Prefix: string;
/** The arbitrary bytes that was signed */
data: Uint8Array;
/** The signature bytes */
signature: Uint8Array;
};

export function verifyECDSA({
pubKey,
data,
signature,
}: Omit<VerifyArbitraryParams, "bech32Prefix">): boolean {
return secp256k1.verify(signature, sha256(data), pubKey);
}

export function verifyADR36({
pubKey,
bech32Prefix,
data,
signature,
}: VerifyArbitraryParams): boolean {
const msg = serialiseSignDoc({
chain_id: "",
account_number: "0",
sequence: "0",
fee: {
gas: "0",
amount: [],
},
msgs: [
{
type: "sign/MsgSignData",
value: {
signer: resolveBech32Address(pubKey, bech32Prefix),
data: base64.encode(data),
},
},
],
memo: "",
});
return verifyECDSA({
pubKey,
data: msg,
signature,
});
}

export function verifyEIP191({
pubKey,
data,
signature,
}: Omit<VerifyArbitraryParams, "bech32Prefix">): boolean {
const recoveredPubKey = recoverPubKeyFromEthSignature(data, signature);
return (
pubKey.length === recoveredPubKey.length &&
pubKey.every((v, i) => v === recoveredPubKey[i])
);
}
1 change: 1 addition & 0 deletions src/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { WalletName } from "./constants/WalletName";
export { WalletType } from "./constants/WalletType";
export { isAndroid, isIOS, isMobile } from "./utils/os";
export { verifyArbitrary } from "./utils/verify";
export {
ConnectedWallet,
type PollTxOptions,
Expand Down
63 changes: 63 additions & 0 deletions src/wallet/utils/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
base64,
utf8,
verifyADR36,
verifyECDSA,
verifyEIP191,
} from "cosmes/codec";

import { WalletName } from "../constants/WalletName";

type VerifyArbitraryParams = {
/** The identifier of the wallet which created the signature */
wallet: WalletName;
/** The base64 encoded public key which created the signature */
pubKey: string;
/** The bech32 account address prefix of the signer */
bech32Prefix: string;
/** The utf-8 encoded arbitrary string that was signed */
data: string;
/** The base64 encoded string of the signature */
signature: string;
};

/**
* Verifies the signature output of a valid call to `ConnectedWallet.signArbitrary`.
* Returns `true` if and only if the signature is valid.
*
* @param wallet The identifier of the wallet which created the signature
* @param pubKey The base64 encoded public key which created the signature
* @param bech32Prefix The bech32 account address prefix of the signer
* @param data The utf-8 encoded arbitrary string that was signed
* @param signature The base64 encoded string of the signature
*/
export function verifyArbitrary({
wallet,
pubKey,
bech32Prefix,
data,
signature,
}: VerifyArbitraryParams): boolean {
const params = {
wallet,
pubKey: base64.decode(pubKey),
bech32Prefix,
data: utf8.decode(data),
signature: utf8.decode(signature),
};
try {
switch (wallet) {
case WalletName.STATION:
return verifyECDSA(params);
case WalletName.COMPASS:
case WalletName.COSMOSTATION:
case WalletName.KEPLR:
case WalletName.LEAP:
return verifyADR36(params);
case WalletName.METAMASK_INJECTIVE:
return verifyEIP191(params);
}
} catch (err) {
return false;
}
}

0 comments on commit 919f138

Please sign in to comment.