Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: check signature #391

Merged
merged 8 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useState } from "react";

import { checkSignature } from "@meshsdk/core";
import { useWallet } from "@meshsdk/react";

import Input from "~/components/form/input";
Expand Down
4 changes: 0 additions & 4 deletions apps/playground/src/pages/governance/index.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { useState } from "react";

import { checkSignature, generateNonce } from "@meshsdk/core";
import { CardanoWallet, useWallet } from "@meshsdk/react";

import Button from "~/components/button/button";
import LayoutImageHeaderAndBody from "~/components/layouts/image-header-and-body";

Expand Down
17 changes: 15 additions & 2 deletions apps/playground/src/pages/guides/prove-wallet-ownership/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CardanoWallet, useWallet } from "@meshsdk/react";

import Button from "~/components/button/button";
import DemoResult from "~/components/sections/demo-result";
import Codeblock from "~/components/text/codeblock";

export default function Demo() {
const { wallet, connected } = useWallet();
Expand All @@ -16,15 +17,27 @@ export default function Demo() {

setLoading(true);
const nonce = generateNonce("Sign to login in to Mesh: ");
const signature = await wallet.signData(nonce);
const address = (await wallet.getUsedAddresses())[0];
const signature = await wallet.signData(nonce, address);

const result = checkSignature(nonce, signature);
const result = checkSignature(nonce, signature, address);
setResponse(result.toString());
setLoading(false);
}

let code = ``;
code += `import { checkSignature, generateNonce } from "@meshsdk/core";\n`;
code += `\n`;
code += `const nonce = generateNonce("Sign to login in to Mesh: ");\n`;
code += `\n`;
code += `const address = (await wallet.getUsedAddresses())[0];\n`;
code += `const signature = await wallet.signData(nonce, address);\n`;
code += `\n`;
code += `const result = checkSignature(nonce, signature, address);\n`;

return (
<>
<Codeblock data={code} />
<p>Connect your wallet and click on the button to sign a message.</p>
<CardanoWallet />
<Button
Expand Down
18 changes: 7 additions & 11 deletions apps/playground/src/pages/guides/prove-wallet-ownership/index.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { useState } from "react";

import { checkSignature, generateNonce } from "@meshsdk/core";
import { CardanoWallet, useWallet } from "@meshsdk/react";

import Button from "~/components/button/button";
import LayoutImageHeaderAndBody from "~/components/layouts/image-header-and-body";
import { guideownership } from "~/data/links-guides";
Expand Down Expand Up @@ -66,7 +62,7 @@ To ensure the signature is valid, the same cryptographic algorithm is applied to

The User model stored in the database of the backend server must have two compulsory fields: public address and nonce. Furthermore, this address has to be unique. Other details about the user, such as username, Twitter ID, and name fields can be added, but are not essential for this process.

On Cardano, to obtain a user's public address as an identifier, we can use their wallet's staking address. This will be stored in the server side database, so that authorized wallets can be linked. The user never has to worry about manually entering their address, since it can be retrieved using **wallet.getRewardAddresses()**.
On Cardano, to obtain a user's public address as an identifier, we can use their wallet's staking address. This will be stored in the server side database, so that authorized wallets can be linked. The user never has to worry about manually entering their address, since it can be retrieved using **wallet.getUsedAddresses()**.

With the user's wallet connected, the first step is to get the user's staking address and send it to our backend server.

Expand All @@ -75,7 +71,7 @@ const { wallet, connected } = useWallet();
async function frontendStartLoginProcess() {
if (connected) {
const userAddress = (await wallet.getRewardAddresses())[0];
const userAddress = (await wallet.getUsedAddresses())[0];
// do: send request with 'userAddress' to the backend
}
Expand Down Expand Up @@ -117,7 +113,7 @@ We request the user's authorization and show them the message that is to be sign
```
async function frontendSignMessage(nonce) {
try {
const userAddress = (await wallet.getRewardAddresses())[0];
const userAddress = (await wallet.getUsedAddresses())[0];
const signature = await wallet.signData(nonce, userAddress);
// do: send request with 'signature' and 'userAddress' to the backend
Expand All @@ -143,7 +139,7 @@ import { checkSignature } from '@meshsdk/core';
async function backendVerifySignature(userAddress, signature) {
// do: get 'nonce' from user (database) using 'userAddress'
const result = checkSignature(nonce, signature);
const result = checkSignature(nonce, signature, userAddress);
// do: update 'nonce' in the database with another random string
Expand Down Expand Up @@ -177,15 +173,15 @@ export default function Page() {
async function frontendStartLoginProcess() {
if (connected) {
const userAddress = (await wallet.getRewardAddresses())[0];
const userAddress = (await wallet.getUsedAddresses())[0];
const nonce = await backendGetNonce(userAddress);
await frontendSignMessage(nonce);
}
}
async function frontendSignMessage(nonce) {
try {
const userAddress = (await wallet.getRewardAddresses())[0];
const userAddress = (await wallet.getUsedAddresses())[0];
const signature = await wallet.signData(nonce, userAddress);
await backendVerifySignature(userAddress, signature);
} catch (error) {
Expand Down Expand Up @@ -217,7 +213,7 @@ async function backendGetNonce(userAddress) {
async function backendVerifySignature(userAddress, signature) {
// do: get 'nonce' from database
const result = checkSignature(nonce, signature);
const result = checkSignature(nonce, signature, userAddress);
if(result){
// create JWT or approve certain process
}
Expand Down
92 changes: 91 additions & 1 deletion packages/mesh-core-cst/src/message-signing/check-signature.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,102 @@
import { Credential } from "@cardano-sdk/core/dist/cjs/Cardano";
import { Bip32PublicKey } from "@stricahq/bip32ed25519";

import { DataSignature } from "@meshsdk/common";

import {
Address,
BaseAddress,
Ed25519KeyHashHex,
EnterpriseAddress,
Hash28ByteBase16,
NetworkId,
RewardAddress,
} from "../types";
import { CoseSign1, getPublicKeyFromCoseKey } from "./cose-sign1";

/** @param address - Optional Bech32 string of a stake, stake_test1, addr, or addr_test1 address. If provided, this function will validate the signer's address against this value. */
export const checkSignature = (
data: string,
{ key, signature }: DataSignature,
address?: string,
) => {
const builder = CoseSign1.fromCbor(signature);
const publicKeyBuffer = getPublicKeyFromCoseKey(key);

if (address) {
let network = NetworkId.Mainnet;
const paymentAddress = BaseAddress.fromAddress(Address.fromBech32(address));
const coseSign1PublicKey = new Bip32PublicKey(publicKeyBuffer);

const credential: Credential = {
hash: Hash28ByteBase16.fromEd25519KeyHashHex(
Ed25519KeyHashHex(
coseSign1PublicKey.toPublicKey().hash().toString("hex"),
),
),
type: 0,
};

if (address.startsWith("addr")) {
if (address.startsWith("addr_test1")) {
network = NetworkId.Testnet;
}

const stakeCredential = paymentAddress?.getStakeCredential();
if (stakeCredential) {
const paymentAddressBech32 = BaseAddress.fromCredentials(
network,
credential,
stakeCredential,
)
.toAddress()
.toBech32();

if (address !== paymentAddressBech32) {
const extractedRewardAddress = RewardAddress.fromCredentials(
network,
stakeCredential,
)
.toAddress()
.toBech32();

const rewardAddress = RewardAddress.fromCredentials(
network,
credential,
)
.toAddress()
.toBech32();

if (rewardAddress !== extractedRewardAddress) {
return false;
}
}
} else {
const enterpriseAddress = EnterpriseAddress.fromCredentials(
network,
credential,
)
.toAddress()
.toBech32();
if (enterpriseAddress !== address) {
return false;
}
}
} else if (address.startsWith("stake")) {
if (address.startsWith("stake_test1")) {
network = NetworkId.Testnet;
}
const rewardAddress = RewardAddress.fromCredentials(network, credential)
.toAddress()
.toBech32();

if (rewardAddress !== address) {
return false;
}
} else {
return false;
}
}

if (builder.getPayload() === null) {
return false;
Expand All @@ -17,6 +107,6 @@ export const checkSignature = (
}

return builder.verifySignature({
publicKeyBuffer: getPublicKeyFromCoseKey(key),
publicKeyBuffer,
});
};
56 changes: 43 additions & 13 deletions packages/mesh-core-cst/test/message-signing.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
import { checkSignature } from "@meshsdk/core-cst";

const config = {
wallet_address:
"addr_test1qqmrzjhtanauj20wg37uk58adyrqfm82a9qr52vdnv0e54r42v0mu8ngky0f5yxmh3wl3z0da2fryk59kavth0u8xhvsufgmc8",
nonce:
"5369676e20746f2076657269667920746865206164647265737320666f723a207374616b655f74657374317570363478386137726535747a3835367a72646d6368306333386b373479336a74327a6d776b396d6837726e746b6773367a786a70315a424e474e79506d3975565677514a53385837724663476551424843657433",
rewardAddress:
"stake_test1up64x8a7re5tz856zrdmch0c38k74y3jt2zmwk9mh7rntkgs6zxjp",
invalidAddress:
"addr_test1qrqtawercjsj29xyq4kssxeru6s33y68kwmh8tj00q4vkhaeucuvwvhegqxf6ka0ewy0pallk044nnrtsj8zhevw603sjxytg2",
signature: {
signature:
"84582aa201276761646472657373581de075531fbe1e68b11e9a10dbbc5df889edea92325a85b758bbbf8735d9a166686173686564f458805369676e20746f2076657269667920746865206164647265737320666f723a207374616b655f74657374317570363478386137726535747a3835367a72646d6368306333386b373479336a74327a6d776b396d6837726e746b6773367a786a70315a424e474e79506d3975565677514a533858377246634765514248436574335840321349435491a3b5992a9b172aebc2b78de5af639045fdb57703f5e39d22b757c597a00b333ef4130c1259858e4230e7bf9ae51e2fa24cdb63dfcebde810860b",
key: "a40101032720062158201220e6aa326f24f12d644a1011dad9d138965c84566d2b7e20b79db7cf2aa73f",
},
};

describe("MessageSigning", () => {
it("checkSignature", () => {
const config = {
wallet_address:
"addr_test1qqmrzjhtanauj20wg37uk58adyrqfm82a9qr52vdnv0e54r42v0mu8ngky0f5yxmh3wl3z0da2fryk59kavth0u8xhvsufgmc8",
nonce:
"5369676e20746f2076657269667920746865206164647265737320666f723a207374616b655f74657374317570363478386137726535747a3835367a72646d6368306333386b373479336a74327a6d776b396d6837726e746b6773367a786a70315a424e474e79506d3975565677514a53385837724663476551424843657433",
rewardAddress:
"stake_test1up64x8a7re5tz856zrdmch0c38k74y3jt2zmwk9mh7rntkgs6zxjp",
signature: {
signature:
"84582aa201276761646472657373581de075531fbe1e68b11e9a10dbbc5df889edea92325a85b758bbbf8735d9a166686173686564f458805369676e20746f2076657269667920746865206164647265737320666f723a207374616b655f74657374317570363478386137726535747a3835367a72646d6368306333386b373479336a74327a6d776b396d6837726e746b6773367a786a70315a424e474e79506d3975565677514a533858377246634765514248436574335840321349435491a3b5992a9b172aebc2b78de5af639045fdb57703f5e39d22b757c597a00b333ef4130c1259858e4230e7bf9ae51e2fa24cdb63dfcebde810860b",
key: "a40101032720062158201220e6aa326f24f12d644a1011dad9d138965c84566d2b7e20b79db7cf2aa73f",
},
};
const result = checkSignature(config.nonce, config.signature);
expect(result).toBe(true);
});

it("checkSignature validates signature's address against provided rewardAddress", () => {
const result = checkSignature(
config.nonce,
config.signature,
config.rewardAddress,
);
expect(result).toBe(true);
});

it("checkSignature validates signature's address against provided wallet_address", () => {
const result = checkSignature(
config.nonce,
config.signature,
config.wallet_address,
);
expect(result).toBe(true);
});

it("checkSignature validates signature's address against provided invalidAddress", () => {
const result = checkSignature(
config.nonce,
config.signature,
config.invalidAddress,
);
expect(result).toBe(false);
});
});