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

Integrate message signing #28

Merged
merged 6 commits into from
Jul 15, 2021
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
322 changes: 143 additions & 179 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"dependencies": {
"@babel/runtime": "7.11.2",
"@elrondnetwork/bls-wasm": "0.3.3",
"@elrondnetwork/hw-app-elrond": "0.2.0",
"@elrondnetwork/hw-app-elrond": "0.3.0",
"@ledgerhq/hw-transport-u2f": "5.28.0",
"@ledgerhq/hw-transport-webusb": "5.28.0",
"@walletconnect/client": "1.4.1",
Expand All @@ -42,7 +42,7 @@
"ed25519-hd-key": "1.1.2",
"json-bigint": "1.0.0",
"json-duplicate-key-handle": "1.0.0",
"keccak": "3.0.1",
"keccak": "^3.0.1",
"platform": "1.3.6",
"protobufjs": "6.10.2",
"scryptsy": "2.1.0",
Expand Down
12 changes: 12 additions & 0 deletions src/dapp/hwProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Signature } from "../signature";
import { compareVersions } from "../versioning";
import { LEDGER_TX_HASH_SIGN_MIN_VERSION } from "./constants";
import {TransactionOptions, TransactionVersion} from "../networkParams";
import {SignableMessage} from "../signableMessage";

export class HWProvider implements IHWProvider {
provider: IProvider;
Expand Down Expand Up @@ -131,6 +132,17 @@ export class HWProvider implements IHWProvider {
return transaction;
}

async signMessage(message: SignableMessage): Promise<SignableMessage> {
if (!this.hwApp) {
throw new Error("HWApp not initialised, call init() first");
}

const signature = await this.hwApp.signMessage(message.serializeForSigningRaw());
message.applySignature(new Signature(signature));

return message;
}

private async shouldSignUsingHash(): Promise<boolean> {
if (!this.hwApp) {
throw new Error("HWApp not initialised, call init() first");
Expand Down
3 changes: 3 additions & 0 deletions src/dapp/interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Transaction } from "../transaction";
import {SignableMessage} from "../signableMessage";

export interface IDappProvider {
init(): Promise<boolean>;
Expand All @@ -9,6 +10,7 @@ export interface IDappProvider {
isConnected(): Promise<boolean>;
sendTransaction(transaction: Transaction, options?: {callbackUrl?: string}): Promise<Transaction>;
signTransaction(transaction: Transaction, options?: {callbackUrl?: string}): Promise<Transaction>;
signMessage(transaction: SignableMessage, options?: {callbackUrl?: string}): Promise<SignableMessage>;
}

export interface IHWProvider extends IDappProvider {
Expand All @@ -34,6 +36,7 @@ export interface IHWElrondApp {
chainCode?: string;
}>;
signTransaction(rawTx: Buffer, usingHash: boolean): Promise<string>;
signMessage(rawMessage: Buffer): Promise<string>;
getAppConfiguration(): Promise<{
version: string;
contractData: number;
Expand Down
10 changes: 10 additions & 0 deletions src/dapp/walletConnectProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { IDappProvider } from "./interface";
import { Signature } from "../signature";
import { WALLETCONNECT_ELROND_CHAIN_ID } from "./constants";
import { Logger } from "../logger";
import {SignableMessage} from "../signableMessage";
import {ErrNotImplemented} from "../errors";

interface IClientConnect {
onClientLogin: () => void;
Expand Down Expand Up @@ -108,6 +110,14 @@ export class WalletConnectProvider implements IDappProvider {
return transaction;
}

/**
* Method will be available once the Maiar wallet connect hook is implemented
* @param _
*/
async signMessage(_: SignableMessage): Promise<SignableMessage> {
throw new ErrNotImplemented();
}

/**
* Signs a transaction and returns it
* @param transaction
Expand Down
10 changes: 10 additions & 0 deletions src/dapp/walletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
} from "./constants";
import {mainFrameStyle} from "./dom";
import {Transaction} from "../transaction";
import {SignableMessage} from "../signableMessage";
import {ErrNotImplemented} from "../errors";

export class WalletProvider implements IDappProvider {
walletUrl: string;
Expand Down Expand Up @@ -338,6 +340,14 @@ export class WalletProvider implements IDappProvider {
});
}

/**
* Method will be available once the ElrondWallet hook will be implemented
* @param _
*/
async signMessage(_: SignableMessage): Promise<SignableMessage> {
throw new ErrNotImplemented();
}

static prepareWalletTransaction(transaction: Transaction): any {
let plainTransaction = transaction.toPlainObject();

Expand Down
9 changes: 9 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,12 @@ export class ErrInvalidEsdtTransferDataField extends Err {
super("Invalid ESDT transfer call data field");
}
}

/**
* Signals that a method is not yet implemented
*/
export class ErrNotImplemented extends Err {
public constructor() {
super("Method not yet implemented");
}
}
23 changes: 21 additions & 2 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { NetworkConfig } from "./networkConfig";
import { Signature } from "./signature";
import { Address } from "./address";
import { AccountOnNetwork } from "./account";
import { Query } from "./smartcontracts/query";
import { QueryResponse } from "./smartcontracts/queryResponse";
import { Query } from "./smartcontracts";
import { QueryResponse } from "./smartcontracts";
import { NetworkStake } from "./networkStake";
import { Stats } from "./stats";
import { NetworkStatus } from "./networkStatus";
import { TransactionOnNetwork } from "./transactionOnNetwork";
import { ESDTToken } from "./esdtToken";
import {SignableMessage} from "./signableMessage";

/**
* An interface that defines the endpoints of an HTTP API Provider.
Expand Down Expand Up @@ -106,6 +107,10 @@ export interface ISigner {
sign(signable: ISignable): Promise<void>;
}

export interface IVerifier {
verify(message: IVerifiable): boolean;
}

/**
* An interface that defines a signable object (e.g. a {@link Transaction}).
*/
Expand All @@ -124,6 +129,20 @@ export interface ISignable {
applySignature(signature: Signature, signedBy: Address): void;
}

/**
* Interface that defines a signed and verifiable object
*/
export interface IVerifiable {
/**
* Returns the signature that should be verified
*/
getSignature(): Signature;
/**
* Returns the signable object in its raw form - a sequence of bytes to be verified.
*/
serializeForSigning(signedBy?: Address): Buffer;
}

/**
* An interface that defines a disposable object.
*/
Expand Down
41 changes: 41 additions & 0 deletions src/signableMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {ISignable} from "./interface";
import {Signature} from "./signature";
const createKeccakHash = require("keccak");

export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n";

export class SignableMessage implements ISignable {

/**
* Actual message being signed.
*/
message: Buffer;
/**
* Signature obtained by a signer of type @param signer .
*/
signature: Signature;

public constructor(init?: Partial<SignableMessage>) {
this.message = Buffer.from([]);
this.signature = new Signature();

Object.assign(this, init);
}

serializeForSigning(): Buffer {
let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), this.message]);
return createKeccakHash("keccak256").update(bytesToHash).digest();
}

serializeForSigningRaw(): Buffer {
return this.message;
}

getSignature(): Signature {
return this.signature;
}

applySignature(signature: Signature): void {
this.signature = signature;
}
}
4 changes: 4 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ export class Transaction implements ISignable {
serializeForSigning(signedBy: Address): Buffer {
// TODO: for appropriate tx.version, interpret tx.options accordingly and sign using the content / data hash
let plain = this.toPlainObject(signedBy);
// Make sure we never sign the transaction with another signature set up (useful when using the same method for verification)
if (plain.signature) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be part of toPlainObject() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, we should be able to convert toPlainObject while still keeping the signature. The sig removal should only be done while verifying the signature

delete plain.signature;
}
let serialized = JSON.stringify(plain);

return Buffer.from(serialized);
Expand Down
13 changes: 13 additions & 0 deletions src/walletcore/userKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as tweetnacl from "tweetnacl";
import { Address } from "../address";
import { guardLength } from "../utils";
import { parseUserKey } from "./pem";
import {SignableMessage} from "../signableMessage";
import {Logger} from "../logger";

export const USER_SEED_LENGTH = 32;
export const USER_PUBKEY_LENGTH = 32;
Expand Down Expand Up @@ -60,6 +62,17 @@ export class UserPublicKey {
this.buffer = buffer;
}

verify(message: Buffer, signature: Buffer): boolean {
try {
const unopenedMessage = Buffer.concat([signature, message]);
const unsignedMessage = tweetnacl.sign.open(unopenedMessage, this.buffer);
return unsignedMessage != null;
} catch (err) {
Logger.error(err);
return false;
}
}

hex(): string {
return this.buffer.toString("hex");
}
Expand Down
29 changes: 29 additions & 0 deletions src/walletcore/userVerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {IVerifiable, IVerifier} from "../interface";
import {Address} from "../address";
import {UserPublicKey} from "./userKeys";

/**
* ed25519 signature verification
*/
export class UserVerifier implements IVerifier {

publicKey: UserPublicKey;
constructor(publicKey: UserPublicKey) {
this.publicKey = publicKey;
}

static fromAddress(address: Address): IVerifier {
let publicKey = new UserPublicKey(address.pubkey());
return new UserVerifier(publicKey);
}

/**
* Verify a message's signature.
* @param message the message to be verified.
*/
verify(message: IVerifiable): boolean {
return this.publicKey.verify(
message.serializeForSigning(this.publicKey.toAddress()),
Buffer.from(message.getSignature().hex(), 'hex'));
}
}
19 changes: 17 additions & 2 deletions src/walletcore/users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { UserSigner } from "./userSigner";
import { Transaction } from "../transaction";
import { Nonce } from "../nonce";
import { Balance } from "../balance";
import { ChainID, GasLimit, GasPrice, TransactionVersion } from "../networkParams";
import { ChainID, GasLimit, GasPrice } from "../networkParams";
import { TransactionPayload } from "../transactionPayload";
import {UserVerifier} from "./userVerifier";
import {SignableMessage} from "../signableMessage";

describe("test user wallets", () => {
let wallets = new TestWallets();
Expand Down Expand Up @@ -120,6 +122,7 @@ describe("test user wallets", () => {

it("should sign transactions", async () => {
let signer = new UserSigner(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf"));
let verifier = new UserVerifier(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf").generatePublicKey());
let sender = new Address("erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz");
let receiver = new Address("erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r");

Expand All @@ -139,7 +142,7 @@ describe("test user wallets", () => {

assert.equal(serialized, `{"nonce":0,"value":"0","receiver":"erd1cux02zersde0l7hhklzhywcxk4u9n4py5tdxyx7vrvhnza2r4gmq4vw35r","sender":"erd1l453hd0gt5gzdp7czpuall8ggt2dcv5zwmfdf3sd3lguxseux2fsmsgldz","gasPrice":1000000000,"gasLimit":50000,"data":"Zm9v","chainID":"1","version":1}`);
assert.equal(transaction.getSignature().hex(), "b5fddb8c16fa7f6123cb32edc854f1e760a3eb62c6dc420b5a4c0473c58befd45b621b31a448c5b59e21428f2bc128c80d0ee1caa4f2bf05a12be857ad451b00");

assert.isTrue(verifier.verify(transaction));
// Without data field
transaction = new Transaction({
nonce: new Nonce(8),
Expand Down Expand Up @@ -173,4 +176,16 @@ describe("test user wallets", () => {
await signer.sign(transaction);
assert.equal(transaction.getSignature().hex(), "c0bd2b3b33a07b9cc5ee7435228acb0936b3829c7008aacabceea35163e555e19a34def2c03a895cf36b0bcec30a7e11215c11efc0da29294a11234eb2b3b906");
});

it("signs a general message", function() {
let signer = new UserSigner(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf"));
let verifier = new UserVerifier(UserSecretKey.fromString("1a927e2af5306a9bb2ea777f73e06ecc0ac9aaa72fb4ea3fecf659451394cccf").generatePublicKey());
const message = new SignableMessage({
message: Buffer.from("hello world")
});

signer.sign(message);
assert.isNotEmpty(message.signature);
assert.isTrue(verifier.verify(message));
});
});