Skip to content

Commit

Permalink
feat: requestID returned in signCallback and support for Eth sign typ…
Browse files Browse the repository at this point in the history
…ed data and personal sign webhooks
  • Loading branch information
kirtan-amin93 committed Sep 8, 2021
1 parent afad459 commit c17bafd
Show file tree
Hide file tree
Showing 18 changed files with 1,769 additions and 35 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@trustology/trustvault-nodejs-sdk",
"version": "1.3.0",
"version": "1.5.0",
"description": "TrustVault Node.js SDK",
"main": "./index.js",
"types": "./index.d.ts",
Expand Down
13 changes: 12 additions & 1 deletion src/ts/resources/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
CreateChangePolicyRequestResponse,
Environment,
EthereumSignData,
EthereumSignMessageCreated,
EthereumSignMessageWebhookType,
HexString,
Integer,
IntString,
Expand All @@ -12,6 +14,7 @@ import {
TransactionSpeed,
TrustVaultRequest,
} from "../../types";
import { EthereumSignMessage } from "../sign-message/ethereum-sign-message";
import { BitcoinTransaction, EthereumTransaction } from "../transaction";
import { Policy } from "../wallet";

Expand All @@ -31,7 +34,7 @@ export const processRequest = async (
}

// create sign requests
const signRequests: SignRequest[] = await request.getSignRequests(sign);
const signRequests: SignRequest[] = await request.getSignRequests(requestId, sign);

// submit signatures
const id = await tvGraphQLClient.addSignature({
Expand Down Expand Up @@ -130,6 +133,14 @@ export const constructEthereumTransactionRequest = (
request: new EthereumTransaction(signData),
});

export const constructEthereumSignMessageRequest = (
type: EthereumSignMessageWebhookType,
webhookPayload: EthereumSignMessageCreated,
): TrustVaultRequest => ({
requestId: webhookPayload.requestId,
request: new EthereumSignMessage(type, webhookPayload.signData),
});

// Change Policy

export const createChangePolicyRequest = async (
Expand Down
87 changes: 87 additions & 0 deletions src/ts/resources/sign-message/ethereum-sign-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { keccak256 } from "ethereumjs-util";
import {
DigestSignData,
EthereumSignMessageSignData,
EthereumSignMessageWebhookType,
HdWalletPath,
RequestClass,
SignCallback,
SignRequest,
} from "../../types";
import { createSignTypedDataDigest, validateSignTypedDataMessage } from "../../utils/ethereum";
import { createSignRequest, getTransactionSignDataDigest } from "../signature";

export class EthereumSignMessage implements RequestClass {
private webhookType: EthereumSignMessageWebhookType;
private message: string;
private hdWalletPath: HdWalletPath;
private unverifiedDigestData: DigestSignData;

constructor(webhookType: EthereumSignMessageWebhookType, signData: EthereumSignMessageSignData) {
this.webhookType = webhookType;
this.message = signData.data.message;
this.hdWalletPath = signData.hdWalletPath;
this.unverifiedDigestData = signData.unverifiedDigestData;
}

/**
* Invokes the sign callback with the generated transaction sign data
* @param {string} requestId
* @param {SignCallback} sign
* @returns Promise
*/
public async getSignRequests(requestId: string, sign: SignCallback): Promise<SignRequest[]> {
const digest: Buffer = this.generateSignMessageDigest();
const signData = getTransactionSignDataDigest(digest, this.hdWalletPath);
const signRequest = await createSignRequest(requestId, digest, this.unverifiedDigestData, signData, sign);
return [signRequest];
}

/**
* Verifies the unverifiedDigestData
* @throws - throws an error if the unverifiedDigestData is not what is expected
*/
public validate(): boolean {
const digest: Buffer = this.generateSignMessageDigest();
const { signData, shaSignData } = getTransactionSignDataDigest(digest, this.hdWalletPath);
const areSignDigestsCorrect =
signData.toString("hex") === this.unverifiedDigestData.signData &&
shaSignData.toString("hex") === this.unverifiedDigestData.shaSignData &&
digest.toString("hex") === this.unverifiedDigestData.digest;
if (!areSignDigestsCorrect) {
throw new Error(
`The digest data produced does not match with the expected unverified digest from server: "${JSON.stringify({
digest,
signData,
shaSignData,
unverifiedDigestData: this.unverifiedDigestData,
})}`,
);
}
return areSignDigestsCorrect;
}

/**
* Returns a sha3-256 hash of the encoded sign message
*/
private generateSignMessageDigest(): Buffer {
switch (this.webhookType) {
case "ETHEREUM_PERSONAL_SIGN_CREATED":
return this.generatePersonalSignDigest();
case "ETHEREUM_SIGN_TYPED_DATA_CREATED":
const signTypedData = validateSignTypedDataMessage(this.message);
return createSignTypedDataDigest(signTypedData);
default:
throw new Error(`Webhook type ${this.webhookType} not supported`);
}
}

private generatePersonalSignDigest(): Buffer {
const encoding = this.message.startsWith("0x") ? "hex" : "utf-8";
const messageToEncode = this.message.startsWith("0x") ? this.message.substring(2) : this.message;
const encodedMessage = Buffer.from(messageToEncode, encoding);
const prefix = Buffer.from("\u0019Ethereum Signed Message:\n" + encodedMessage.length.toString(), "utf-8");
const digest = keccak256(Buffer.concat([prefix, encodedMessage]));
return digest;
}
}
35 changes: 22 additions & 13 deletions src/ts/resources/signature/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
VALID_SIGNATURE_BYTE_LENGTH,
} from "../../static-data";
import {
DigestSignData,
HdWalletPath,
PolicySchedule,
ProvenanceDataSchema,
Expand All @@ -23,6 +24,7 @@ import {
derEncodeProvenance,
derEncodeRecovererSchedules,
derEncodeTxDigestPath,
isSignMessageDigestData,
isTransactionDigestData,
} from "../../utils";

Expand Down Expand Up @@ -198,34 +200,41 @@ export const getTransactionSignDataDigest = (transactionDigest: Buffer, path: Hd
* @returns {SignRequest}
*/
export const createSignRequest = async (
requestId: string,
digest: Buffer,
path: HdWalletPath | undefined,
unverifiedDigestData: TransactionDigestData | SignData,
unverifiedDigestData: TransactionDigestData | SignData | DigestSignData,
{ signData, shaSignData }: SignDataBuffer, // own generated signData
sign: SignCallback,
): Promise<SignRequest> => {
const digestHex = digest.toString("hex");
const signDataHex = signData.toString("hex");
const shaSignDataHex = shaSignData.toString("hex");
let areSignDigestsCorrect =
signData.toString("hex") === unverifiedDigestData.signData ||
shaSignData.toString("hex") === unverifiedDigestData.shaSignData;
signDataHex === unverifiedDigestData.signData && shaSignDataHex === unverifiedDigestData.shaSignData;

if (isTransactionDigestData(unverifiedDigestData)) {
areSignDigestsCorrect = areSignDigestsCorrect || digest.toString("hex") === unverifiedDigestData.transactionDigest;
areSignDigestsCorrect = areSignDigestsCorrect && digestHex === unverifiedDigestData.transactionDigest;
}

if (isSignMessageDigestData(unverifiedDigestData)) {
areSignDigestsCorrect = areSignDigestsCorrect && digestHex === unverifiedDigestData.digest;
}

if (!areSignDigestsCorrect) {
const digestData = JSON.stringify({
digest: digestHex,
signData: signDataHex,
shaSignData: shaSignDataHex,
unverifiedDigestData,
});
throw new Error(
`The digest data produced does not match with the expected unverified digest from server: ${JSON.stringify({
digest,
signData,
shaSignData,
unverifiedDigestData,
})}`,
`The digest data produced does not match with the expected unverified digest from server: ${digestData}`,
);
}

const publicKeySignaturePair: PublicKeySignaturePairBuffer = await sign({ signData, shaSignData });
const publicKeySignaturePair: PublicKeySignaturePairBuffer = await sign({ signData, shaSignData }, { requestId });

// verify the public key signature pair that the sign callback is correct
// verify the public key signature pair that the sign callback returned is correct
verifyPublicKeySignaturePair(shaSignData, publicKeySignaturePair, NIST_P_256_CURVE);

const signRequest: SignRequest = {
Expand Down
4 changes: 2 additions & 2 deletions src/ts/resources/transaction/bitcoin-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ export class BitcoinTransaction implements RequestClass {
this.btcLibNetwork = this.getBtcLibNetwork(network);
}

public async getSignRequests(sign: SignCallback): Promise<SignRequest[]> {
public async getSignRequests(requestId: string, sign: SignCallback): Promise<SignRequest[]> {
const signRequestPromises: Promise<SignRequest>[] = this.getDigests().map((digest, i) => {
// Get the matching input path for each digest then create a sign request
const matchingInput = this.inputs[i];
const { path } = matchingInput.publicKeyProvenanceData;
const signData = getTransactionSignDataDigest(digest, path);
return createSignRequest(digest, path, matchingInput.unverifiedDigestData, signData, sign);
return createSignRequest(requestId, digest, matchingInput.unverifiedDigestData, signData, sign);
});
return Promise.all(signRequestPromises);
}
Expand Down
8 changes: 5 additions & 3 deletions src/ts/resources/transaction/ethereum-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ export class EthereumTransaction implements RequestClass {

/**
* Invokes the sign callback with the generated transaction sign data
* @returns {SignRequest}
* @param {string} requestId
* @param {SignCallback} sign
* @returns Promise<SignRequest[]>
*/
public async getSignRequests(sign: SignCallback): Promise<SignRequest[]> {
public async getSignRequests(requestId: string, sign: SignCallback): Promise<SignRequest[]> {
const digest: Buffer = this.generateTransactionDigest();
const signData = getTransactionSignDataDigest(digest, this.hdWalletPath);
const signRequest = await createSignRequest(digest, this.hdWalletPath, this.unverifiedDigestData, signData, sign);
const signRequest = await createSignRequest(requestId, digest, this.unverifiedDigestData, signData, sign);
return [signRequest];
}

Expand Down
5 changes: 2 additions & 3 deletions src/ts/resources/wallet/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ export class Policy implements RequestClass {
this.trustVaultPublicKey = trustVaultPublicKey;
}

public async getSignRequests(sign: SignCallback): Promise<SignRequest[]> {
public async getSignRequests(requestId: string, sign: SignCallback): Promise<SignRequest[]> {
const signData: SignDataBuffer = this.getSignData();
const digest: Buffer = signData.shaSignData;
const path = undefined;
const signRequest = await createSignRequest(digest, path, this.unverifiedDigestData, signData, sign);
const signRequest = await createSignRequest(requestId, digest, this.unverifiedDigestData, signData, sign);
return [signRequest];
}

Expand Down
6 changes: 6 additions & 0 deletions src/ts/trust-vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { config } from "./config";
import {
constructBitcoinTransactionRequest,
constructChangePolicyRequest,
constructEthereumSignMessageRequest,
constructEthereumTransactionRequest,
createBitcoinTransaction,
createChangePolicyRequest,
Expand Down Expand Up @@ -94,6 +95,11 @@ export class TrustVault {
// no signature to validate
trustVaultRequest = constructEthereumTransactionRequest(ethPayload.requestId, ethPayload.signData);
break;
case "ETHEREUM_PERSONAL_SIGN_CREATED":
case "ETHEREUM_SIGN_TYPED_DATA_CREATED":
const { type, payload } = webhookMessage;
trustVaultRequest = constructEthereumSignMessageRequest(type, payload);
break;
case "POLICY_CHANGE_REQUEST_CREATED":
trustVaultRequest = constructChangePolicyRequest(webhookMessage.payload, this.trustVaultPublicKey);
// validate recoverer schedules
Expand Down
5 changes: 3 additions & 2 deletions src/ts/types/request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SignCallback, SignRequest } from "./signature";

// A class that is a request to be signed
export interface RequestClass {
getSignRequests(sign: SignCallback): Promise<SignRequest[]>;
// Get the details of how to sign this operation
getSignRequests(requestId: string, sign: SignCallback): Promise<SignRequest[]>;
validate(...args: any[]): boolean;
}

Expand Down
14 changes: 13 additions & 1 deletion src/ts/types/signature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { HexString } from "./data";

export type SignCallback = (signData: SignDataBuffer) => Promise<PublicKeySignaturePairBuffer>;
export type SignCallback = (
signData: SignDataBuffer,
requestInfo: { requestId: string },
) => Promise<PublicKeySignaturePairBuffer>;

export interface SignData {
// The DER encoded transaction digest and the wallet path
Expand All @@ -21,6 +24,15 @@ export interface TransactionDigestData extends SignData {
transactionDigest: string;
}

export interface DigestSignData extends SignData {
digest: HexString;
}

export interface SignData {
signData: HexString; // DER(data)
shaSignData: HexString; // SHA(DER(data))
}

// TODO: docs
export interface PublicKeySignaturePair {
publicKey: HexString; // 130 hex string (`04` hex prefixed)
Expand Down
Loading

0 comments on commit c17bafd

Please sign in to comment.