Skip to content

Commit

Permalink
native: make ecdsaSign params work with structuredClone()
Browse files Browse the repository at this point in the history
  • Loading branch information
mrnerdhair committed Nov 9, 2021
1 parent 418e9c1 commit b98dfa3
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ export default {
async create(keyPair: BIP32.Node): Promise<SigningDelegate> {
return async (tx: Transaction, signMsg?: any): Promise<Transaction> => {
const signBytes = tx.getSignBytes(signMsg);
const signHash = Digest.Algorithms["sha256"](signBytes);
const pubKey = crypto.getPublicKey(Buffer.from(await keyPair.publicKey).toString("hex"));
const sig = Buffer.from(await SecP256K1.Signature.signCanonically(keyPair, signHash));
const sig = Buffer.from(await SecP256K1.Signature.signCanonically(keyPair, "sha256", signBytes));
tx.addSignature(pubKey, sig);
return tx;
};
Expand Down
18 changes: 16 additions & 2 deletions packages/hdwallet-native/src/crypto/isolation/adapters/bitcoin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ECPairInterface, Network, SignerAsync, crypto as bcrypto, networks } from "@shapeshiftoss/bitcoinjs-lib";
import { SecP256K1, IsolationError } from "../core"
import { assertType, ByteArray } from "../types";

export type ECPairInterfaceAsync = Omit<ECPairInterface, "sign"> & Pick<SignerAsync, "sign">;

Expand Down Expand Up @@ -41,8 +42,21 @@ export class ECPairAdapter implements SecP256K1.ECDSAKey, SignerAsync, ECPairInt
}

async sign(hash: bcrypto.NonDigest | bcrypto.Digest<"hash256">, lowR?: boolean): Promise<Buffer> {
assertType(ByteArray(), hash);

lowR = lowR ?? this.lowR;
const sig = (!lowR ? await this._isolatedKey.ecdsaSign(hash) : await SecP256K1.Signature.signCanonically(this._isolatedKey, hash));
const sig = await(async () => {
if (!hash.algorithm) {
assertType(ByteArray(32), hash);
return !lowR
? await this._isolatedKey.ecdsaSign(null, hash)
: await SecP256K1.Signature.signCanonically(this._isolatedKey, null, hash);
} else {
return !lowR
? await this._isolatedKey.ecdsaSign(hash.algorithm, hash.preimage)
: await SecP256K1.Signature.signCanonically(this._isolatedKey, hash.algorithm, hash.preimage);
}
})();
return Buffer.from(sig);
}
get publicKey() { return this.getPublicKey(); }
Expand All @@ -55,7 +69,7 @@ export class ECPairAdapter implements SecP256K1.ECDSAKey, SignerAsync, ECPairInt
toWIF(): never { throw new IsolationError("WIF"); }
verify(hash: Uint8Array, signature: Uint8Array) {
SecP256K1.Signature.assert(signature);
return SecP256K1.Signature.verify(signature, hash, this._publicKey);
return SecP256K1.Signature.verify(signature, null, hash, this._publicKey);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SecP256K1 } from "../core";
import * as Digest from "../core/digest";

export class WalletAdapter {
protected readonly _isolatedKey: SecP256K1.ECDSAKey;
readonly _publicKey: SecP256K1.CurvePoint;
Expand All @@ -19,8 +19,7 @@ export class WalletAdapter {

async sign(signMessage: string): Promise<Buffer> {
const signBuf = Buffer.from(signMessage.normalize("NFKD"), "utf8");
const signBufHash = Digest.Algorithms["sha256"](signBuf);
return Buffer.from(await this._isolatedKey.ecdsaSign(signBufHash));
return Buffer.from(await this._isolatedKey.ecdsaSign("sha256", signBuf));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from "@shapeshiftoss/hdwallet-core"
import * as ethers from "ethers";

import { SecP256K1, Digest } from "../core";
import { SecP256K1 } from "../core";

export class SignerAdapter extends ethers.Signer {
protected readonly _isolatedKey: SecP256K1.ECDSAKey & SecP256K1.ECDHKey;
Expand Down Expand Up @@ -30,7 +30,7 @@ export class SignerAdapter extends ethers.Signer {
}

async signDigest(digest: ethers.BytesLike): Promise<ethers.Signature> {
const rawSig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, digest instanceof Uint8Array ? digest : ethers.utils.arrayify(digest));
const rawSig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, null, digest instanceof Uint8Array ? digest : ethers.utils.arrayify(digest));
return ethers.utils.splitSignature(core.compatibleBufferConcat([rawSig, Buffer.from([rawSig.recoveryParam])]));
}

Expand All @@ -48,7 +48,8 @@ export class SignerAdapter extends ethers.Signer {
}

const txBuf = ethers.utils.arrayify(ethers.utils.serializeTransaction(unsignedTx));
const signature = await this.signDigest(Digest.Algorithms["keccak256"](txBuf));
const rawSig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, "keccak256", txBuf);
const signature = ethers.utils.splitSignature(core.compatibleBufferConcat([rawSig, Buffer.from([rawSig.recoveryParam])]));
return ethers.utils.serializeTransaction(unsignedTx, signature);
}

Expand All @@ -58,7 +59,8 @@ export class SignerAdapter extends ethers.Signer {
? Buffer.from(messageData.normalize("NFKD"), "utf8")
: Buffer.from(ethers.utils.arrayify(messageData));
const messageBuf = core.compatibleBufferConcat([Buffer.from(`\x19Ethereum Signed Message:\n${messageDataBuf.length}`, "utf8"), messageDataBuf]);
const signature = await this.signDigest(Digest.Algorithms["keccak256"](messageBuf));
const rawSig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, "keccak256", messageBuf);
const signature = ethers.utils.splitSignature(core.compatibleBufferConcat([rawSig, Buffer.from([rawSig.recoveryParam])]));
return ethers.utils.joinSignature(signature);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ export class ExternalSignerAdapter implements FIOExternalPrivateKey {
}

async sign(signBuf: Uint8Array): Promise<string> {
const signBufHash = Digest.Algorithms["sha256"](signBuf);
const sig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, signBufHash);
const sig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, "sha256", signBuf);
const fioSigBuf = core.compatibleBufferConcat([Buffer.from([sig.recoveryParam + 4 + 27]), SecP256K1.RecoverableSignature.r(sig), SecP256K1.RecoverableSignature.s(sig)]);
return `SIG_K1_${bs58FioEncode(fioSigBuf, "K1")}`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ByteArray, Uint32 } from "../../types";
import { CurvePoint, Message, RecoverableSignature, Signature } from "./types";
import { CurvePoint, RecoverableSignature, Signature } from "./types";
import * as Digest from "../digest";

export interface ECDSAKey {
Expand All @@ -12,13 +12,17 @@ export interface ECDSAKey {
// This can be used, for example, to find a signature whose r-value does not have the MSB set (i.e. a lowR signature),
// which can be encoded in DER format with one less byte. If an implementation does not support the use of the counter
// value, it MUST return undefined rather than perform a signing operation which ignores it.
ecdsaSign(message: Message): Promise<NonNullable<Signature>>;
ecdsaSign(message: Message, counter: Uint32): Promise<NonNullable<Signature> | undefined>;
ecdsaSign(digestAlgorithm: null, message: ByteArray<32>): Promise<NonNullable<Signature>>;
ecdsaSign(digestAlgorithm: null, message: ByteArray<32>, counter: Uint32): Promise<NonNullable<Signature> | undefined>;
ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array): Promise<NonNullable<Signature>>;
ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array, counter: Uint32): Promise<NonNullable<Signature> | undefined>;
}

export interface ECDSARecoverableKey extends ECDSAKey {
ecdsaSign(message: Message): Promise<NonNullable<RecoverableSignature>>;
ecdsaSign(message: Message, counter: Uint32): Promise<NonNullable<RecoverableSignature> | undefined>;
ecdsaSign(digestAlgorithm: null, message: ByteArray<32>): Promise<NonNullable<RecoverableSignature>>;
ecdsaSign(digestAlgorithm: null, message: ByteArray<32>, counter: Uint32): Promise<NonNullable<RecoverableSignature> | undefined>;
ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array): Promise<NonNullable<RecoverableSignature>>;
ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array, counter: Uint32): Promise<NonNullable<RecoverableSignature> | undefined>;
}

export interface ECDHKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import * as ethers from "ethers"
import { Literal, Partial, Object as Obj, Static, Union } from "funtypes";
import * as tinyecc from "tiny-secp256k1";

import { Digest } from "../digest";
import { BigEndianInteger, ByteArray, Uint32, checkType, safeBufferFrom } from "../../types";
import * as Digest from "../digest";
import { BigEndianInteger, ByteArray, Uint32, checkType, safeBufferFrom, assertType } from "../../types";
import { ECDSAKey } from "./interfaces";

const fieldElementBase = BigEndianInteger(32).withConstraint(
Expand Down Expand Up @@ -87,7 +87,7 @@ const recoveryParamStatic = {};
const recoveryParam = Object.assign(recoveryParamBase, recoveryParamStatic);
export const RecoveryParam: typeof recoveryParam = recoveryParam;

const messageWithPreimageBase = ByteArray(32).And(Digest());
const messageWithPreimageBase = ByteArray(32).And(Digest.Digest());
export type MessageWithPreimage = Static<typeof messageWithPreimageBase>;
const messageWithPreimageStatic = {};
const messageWithPreimage = Object.assign(messageWithPreimageBase, ByteArray, messageWithPreimageStatic);
Expand Down Expand Up @@ -115,19 +115,28 @@ const signatureStatic = {
isLowR: (x: Signature): boolean => { return !FieldElement.isHigh(Signature.r(x)); },
isLowS: (x: Signature): boolean => { return !FieldElement.isHigh(Signature.s(x)); },
isCanonical: (x: Signature): boolean => { return Signature.isLowR(x) && Signature.isLowS(x); },
signCanonically: async (x: ECDSAKey, message: Message, counter?: Uint32): Promise<Signature> => {
signCanonically: async (x: ECDSAKey, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, counter?: Uint32): Promise<Signature> => {
assertType(ByteArray(), message);
counter === undefined || Uint32.assert(counter);
for (let i = counter; i === undefined || i < (counter ?? 0) + 128; i = (i ?? -1) + 1) {
const sig = i === undefined ? await x.ecdsaSign(message) : await x.ecdsaSign(message, i);
const sig = await (async () => {
if (digestAlgorithm === null) {
assertType(ByteArray(32), message);
return i === undefined ? await x.ecdsaSign(digestAlgorithm, message) : await x.ecdsaSign(digestAlgorithm, message, i);
} else {
return i === undefined ? await x.ecdsaSign(digestAlgorithm, message) : await x.ecdsaSign(digestAlgorithm, message, i);
}
})();
if (sig === undefined) break;
//TODO: do integrated lowS correction
if (Signature.isCanonical(sig)) return sig;
}
// This is cryptographically impossible (2^-128 chance) if the key is implemented correctly.
throw new Error(`Unable to generate canonical signature with public key ${x} over message ${message}; is your key implementation broken?`);
},
verify: (x: Signature, message: Message, publicKey: CurvePoint): boolean => {
return tinyecc.verify(Buffer.from(message), Buffer.from(publicKey), Buffer.from(x));
verify: (x: Signature, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, publicKey: CurvePoint): boolean => {
const msgOrDigest = digestAlgorithm === null ? checkType(ByteArray(32), message) : Digest.Algorithms[digestAlgorithm](checkType(ByteArray(), message));
return tinyecc.verify(Buffer.from(msgOrDigest), Buffer.from(publicKey), Buffer.from(x));
},
};
const signature = Object.assign(signatureBase, ByteArray, signatureStatic);
Expand All @@ -143,34 +152,42 @@ const recoverableSignatureStatic = {
out.recoveryParam = checkType(RecoveryParam, x[64]);
return checkType(RecoverableSignature, out);
},
fromSignature: (x: Signature, message: Message, publicKey: CurvePoint): RecoverableSignature => {
if (RecoverableSignature.test(x)) return x;
fromSignature: (x: Signature, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, publicKey: CurvePoint): RecoverableSignature => {
const out = Buffer.from(x) as Uint8Array as RecoverableSignature;
for (out.recoveryParam = 0; out.recoveryParam < 4; out.recoveryParam++) {
if (!CurvePoint.equal(publicKey, RecoverableSignature.recoverPublicKey(out, message))) continue;
if (!CurvePoint.equal(publicKey, RecoverableSignature.recoverPublicKey(out, digestAlgorithm, message))) continue;
return checkType(RecoverableSignature, out);
}
throw new Error(`couldn't find recovery parameter producing public key ${publicKey} for signature ${x} over message ${message}`);
throw new Error(`couldn't find recovery parameter producing public key ${publicKey} for signature ${x} over message ${message}${digestAlgorithm !== null ? `using digest algorithm ${digestAlgorithm}` : ""}`);
},
isLowRecoveryParam: (x: RecoverableSignature) => x.recoveryParam === 0 || x.recoveryParam === 1,
isCanonical: (x: RecoverableSignature): boolean => Signature.isCanonical(x) && RecoverableSignature.isLowRecoveryParam(x),
signCanonically: async (x: ECDSAKey, message: Message, counter?: Uint32): Promise<RecoverableSignature> => {
signCanonically: async (x: ECDSAKey, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, counter?: Uint32): Promise<RecoverableSignature> => {
const publicKey = await x.publicKey;
assertType(ByteArray(), message);
counter === undefined || Uint32.assert(counter);
for (let i = counter; i === undefined || i < (counter ?? 0) + 128; i = (i ?? -1) + 1) {
const sig = i === undefined ? await x.ecdsaSign(message) : await x.ecdsaSign(message, i);
const sig = await (async () => {
if (digestAlgorithm === null) {
assertType(ByteArray(32), message);
return i === undefined ? await x.ecdsaSign(digestAlgorithm, message) : await x.ecdsaSign(digestAlgorithm, message, i);
} else {
return i === undefined ? await x.ecdsaSign(digestAlgorithm, message) : await x.ecdsaSign(digestAlgorithm, message, i);
}
})();
if (sig === undefined) break;
const recoverableSig = RecoverableSignature.fromSignature(sig, message, publicKey);
const recoverableSig = RecoverableSignature.fromSignature(sig, digestAlgorithm, message, publicKey);
//TODO: do integrated lowS correction
if (RecoverableSignature.isCanonical(recoverableSig)) return recoverableSig;
}
// This is cryptographically impossible (2^-128 chance) if the key is implemented correctly.
throw new Error(`Unable to generate canonical recoverable signature with public key ${Buffer.from(publicKey).toString("hex")} over message ${Buffer.from(message).toString("hex")}; is your key implementation broken?`);
},
recoverPublicKey: (x: RecoverableSignature, message: Message): CurvePoint => {
recoverPublicKey: (x: RecoverableSignature, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array): CurvePoint => {
// TODO: do this better
const msgOrDigest = digestAlgorithm === null ? checkType(ByteArray(32), message) : Digest.Algorithms[digestAlgorithm](checkType(ByteArray(), message));
const ethSigBytes = core.compatibleBufferConcat([x, Buffer.from([x.recoveryParam])]);
const ethRecovered = ethers.utils.recoverPublicKey(message, ethers.utils.splitSignature(ethSigBytes));
const ethRecovered = ethers.utils.recoverPublicKey(msgOrDigest, ethers.utils.splitSignature(ethSigBytes));
return checkType(UncompressedPoint, Buffer.from(ethRecovered.slice(2), "hex"));
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,24 @@ export class Node implements BIP32.Node, SecP256K1.ECDSARecoverableKey, SecP256K
return this.#publicKey;
}

async ecdsaSign(msg: SecP256K1.Message, counter?: Uint32): Promise<SecP256K1.RecoverableSignature> {
SecP256K1.Message.assert(msg);
async ecdsaSign(digestAlgorithm: null, msg: ByteArray<32>, counter?: Uint32): Promise<SecP256K1.RecoverableSignature>
async ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, msg: Uint8Array, counter?: Uint32): Promise<SecP256K1.RecoverableSignature>
async ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32> | null, msg: Uint8Array, counter?: Uint32): Promise<SecP256K1.RecoverableSignature> {
counter === undefined || Uint32.assert(counter);
digestAlgorithm === null || Digest.AlgorithmName(32).assert(digestAlgorithm);

// When running tests, this will keep us aware of any codepaths that don't pass in the preimage
if (typeof expect === "function") expect(SecP256K1.MessageWithPreimage.test(msg)).toBeTruthy();
if (typeof expect === "function") expect(digestAlgorithm).not.toBeNull();

const msgOrDigest = digestAlgorithm === null ? checkType(ByteArray(32), msg) : Digest.Algorithms[digestAlgorithm](checkType(ByteArray(), msg));
const entropy = (counter === undefined ? undefined : Buffer.alloc(32));
entropy?.writeUInt32BE(counter ?? 0, 24);
return SecP256K1.RecoverableSignature.fromSignature(
checkType(SecP256K1.Signature, (tinyecc as typeof tinyecc & {
signWithEntropy: (message: Buffer, privateKey: Buffer, entropy?: Buffer) => Buffer,
}).signWithEntropy(Buffer.from(msg), this.#privateKey, entropy)),
msg,
}).signWithEntropy(Buffer.from(msgOrDigest), this.#privateKey, entropy)),
null,
msgOrDigest,
this.publicKey,
);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/hdwallet-native/src/crypto/isolation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ const boundedStringStatic = {};
const boundedString = Object.assign(boundedStringBase, boundedStringStatic);
export const BoundedString: typeof boundedString = boundedString;

export function checkType<T extends Runtype<unknown>>(rt: T, value: any): Static<T> {
export function assertType<T extends Runtype<unknown>>(rt: T, value: unknown): asserts value is Static<T> {
rt.assert(value);
return value as Static<T>;
}

export function checkType<T extends Runtype<unknown>>(rt: T, value: unknown): Static<T> {
assertType(rt, value);
return value;
}

// export function ParseWorkaround<T, U extends Runtype<T>>(rt: U) {
Expand Down

0 comments on commit b98dfa3

Please sign in to comment.