Skip to content

Commit

Permalink
native: make recoverable signatures work with structuredClone()
Browse files Browse the repository at this point in the history
  • Loading branch information
mrnerdhair committed Nov 9, 2021
1 parent b98dfa3 commit a72bbcc
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 52 deletions.
22 changes: 14 additions & 8 deletions packages/hdwallet-native/src/crypto/isolation/adapters/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ export class SignerAdapter extends ethers.Signer {
}

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

async signTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<string> {
Expand All @@ -48,9 +50,11 @@ export class SignerAdapter extends ethers.Signer {
}

const txBuf = ethers.utils.arrayify(ethers.utils.serializeTransaction(unsignedTx));
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);
const recoverableSig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, "keccak256", txBuf);
const sig = SecP256K1.RecoverableSignature.sig(recoverableSig);
const recoveryParam = SecP256K1.RecoverableSignature.recoveryParam(recoverableSig);
const ethSig = ethers.utils.splitSignature(core.compatibleBufferConcat([sig, Buffer.from([recoveryParam])]));
return ethers.utils.serializeTransaction(unsignedTx, ethSig);
}

async signMessage(messageData: ethers.Bytes | string): Promise<string> {
Expand All @@ -59,9 +63,11 @@ 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 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);
const recoverableSig = await SecP256K1.RecoverableSignature.signCanonically(this._isolatedKey, "keccak256", messageBuf);
const sig = SecP256K1.RecoverableSignature.sig(recoverableSig);
const recoveryParam = SecP256K1.RecoverableSignature.recoveryParam(recoverableSig);
const ethSig = ethers.utils.splitSignature(core.compatibleBufferConcat([sig, Buffer.from([recoveryParam])]));
return ethers.utils.joinSignature(ethSig);
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/hdwallet-native/src/crypto/isolation/adapters/fio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import bs58 from "bs58"

import { SecP256K1 } from "../core";
import * as Digest from "../core/digest";
import { CurvePoint } from "../core/secp256k1";
import { CurvePoint, RecoverableSignature } from "../core/secp256k1";
import { checkType } from "../types";

function bs58FioEncode(raw: Uint8Array, keyType: string = ""): string {
Expand Down Expand Up @@ -34,7 +34,7 @@ export class ExternalSignerAdapter implements FIOExternalPrivateKey {

async sign(signBuf: Uint8Array): Promise<string> {
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)]);
const fioSigBuf = core.compatibleBufferConcat([Buffer.from([RecoverableSignature.recoveryParam(sig) + 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
Expand Up @@ -19,10 +19,10 @@ export interface ECDSAKey {
}

export interface ECDSARecoverableKey extends ECDSAKey {
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>;
ecdsaSignRecoverable(digestAlgorithm: null, message: ByteArray<32>): Promise<NonNullable<RecoverableSignature>>;
ecdsaSignRecoverable(digestAlgorithm: null, message: ByteArray<32>, counter: Uint32): Promise<NonNullable<RecoverableSignature> | undefined>;
ecdsaSignRecoverable(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array): Promise<NonNullable<RecoverableSignature>>;
ecdsaSignRecoverable(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
@@ -1,12 +1,11 @@

import * as core from "@shapeshiftoss/hdwallet-core"
import * as ethers from "ethers"
import { Literal, Partial, Object as Obj, Static, Union } from "funtypes";
import { Literal, Object as Obj, Static, Union } from "funtypes";
import * as tinyecc from "tiny-secp256k1";

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

const fieldElementBase = BigEndianInteger(32).withConstraint(
x => tinyecc.isPrivate(safeBufferFrom(x)) || `expected ${x} to be within the order of the curve`,
Expand Down Expand Up @@ -105,9 +104,7 @@ const signatureBase = ByteArray(64).withConstraint(
).withConstraint(
x => FieldElement.test(x.slice(32, 64)) || `expected ${x}.s to be within the order of the curve`,
{name: "Signature.s"},
).And(Partial({
recoveryParam: RecoveryParam,
}));
);
export type Signature = Static<typeof signatureBase>;
const signatureStatic = {
r: (x: Signature): FieldElement => { return checkType(FieldElement, x.slice(0, 32)); },
Expand Down Expand Up @@ -142,41 +139,56 @@ const signatureStatic = {
const signature = Object.assign(signatureBase, ByteArray, signatureStatic);
export const Signature: typeof signature = signature;

const recoverableSignatureBase = Signature.And(Obj({
recoveryParam: RecoveryParam,
}));
const recoverableSignatureBase = ByteArray(65).And(Obj({
64: RecoveryParam,
})).withConstraint(
x => Signature.test(x.slice(0, 64)) || `expected ${x}.sig to be a valid signature`,
{name: "Signature"},
);
export type RecoverableSignature = Static<typeof recoverableSignatureBase>;
const recoverableSignatureStatic = {
from: (x: ByteArray<65>) => {
const out = checkType(Signature, x.slice(0, 64));
out.recoveryParam = checkType(RecoveryParam, x[64]);
return checkType(RecoverableSignature, out);
from: (x: Signature, recoveryParam: RecoveryParam): RecoverableSignature => {
return checkType(RecoverableSignature, core.compatibleBufferConcat([x, new Uint8Array([recoveryParam])]));
},
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, 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}${digestAlgorithm !== null ? `using digest algorithm ${digestAlgorithm}` : ""}`);
for (let recoveryParam: RecoveryParam = 0; recoveryParam < 4; recoveryParam++) {
const out = RecoverableSignature.from(x, recoveryParam);
if (!CurvePoint.equal(publicKey, RecoverableSignature.recoverPublicKey(out, digestAlgorithm, message))) continue;
return out;
}
throw new Error(`couldn't find recovery parameter producing public key ${publicKey} for signature ${x} over message ${message}`);
},
isLowRecoveryParam: (x: RecoverableSignature) => x.recoveryParam === 0 || x.recoveryParam === 1,
isCanonical: (x: RecoverableSignature): boolean => Signature.isCanonical(x) && RecoverableSignature.isLowRecoveryParam(x),
sig: (x: RecoverableSignature): Signature => checkType(Signature, x.slice(0, 64)),
recoveryParam: (x: RecoverableSignature): RecoveryParam => checkType(RecoveryParam, x[64]),
isLowRecoveryParam: (x: RecoverableSignature) => [0, 1].includes(RecoverableSignature.recoveryParam(x)),
isCanonical: (x: RecoverableSignature): boolean => Signature.isCanonical(checkType(Signature, RecoverableSignature.sig(x))) && RecoverableSignature.isLowRecoveryParam(x),
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);

const isIndexable = (x: unknown): x is Record<string, unknown> => x !== null && ["object", "function"].includes(typeof x);
const isECDSARecoverableKey = (x: ECDSAKey): x is ECDSARecoverableKey => isIndexable(x) && "ecdsaSignRecoverable" in x && typeof x.ecdsaSignRecoverable === "function";

const ecdsaSignRecoverable = isECDSARecoverableKey(x) ? async (digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, counter?: Uint32) => {
if (digestAlgorithm === null) {
assertType(ByteArray(32), message);
return counter === undefined ? await x.ecdsaSignRecoverable(digestAlgorithm, message) : await x.ecdsaSignRecoverable(digestAlgorithm, message, counter);
} else {
return counter === undefined ? await x.ecdsaSignRecoverable(digestAlgorithm, message) : await x.ecdsaSignRecoverable(digestAlgorithm, message, counter);
}
} : async (digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, counter?: Uint32) => {
const sig = await Signature.signCanonically(x, digestAlgorithm, message, counter);
if (sig === undefined) return undefined;
return RecoverableSignature.fromSignature(sig, digestAlgorithm, message, publicKey);
};

// Technically, this may waste cycles; if Signature.signCanonically grinds the counter to find a canonical signature which then
// ends up to have a non-canonical recovery parameter, those values will all be re-ground. However, signatures can have
// non-canonical recovery parameters only with negligible probability, so optimization for that case would be silly.
for (let i = counter; i === undefined || i < (counter ?? 0) + 128; i = (i ?? -1) + 1) {
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, digestAlgorithm, message, publicKey);
const recoverableSig = await ecdsaSignRecoverable(digestAlgorithm, message, i);
if (recoverableSig === undefined) break;
//TODO: do integrated lowS correction
if (RecoverableSignature.isCanonical(recoverableSig)) return recoverableSig;
}
Expand All @@ -186,14 +198,22 @@ const recoverableSignatureStatic = {
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(msgOrDigest, ethers.utils.splitSignature(ethSigBytes));
const sig = RecoverableSignature.sig(x);
const recoveryParam = RecoverableSignature.recoveryParam(x);
const ethSig = core.compatibleBufferConcat([sig, Buffer.from([recoveryParam])]);
const ethRecovered = ethers.utils.recoverPublicKey(msgOrDigest, ethers.utils.splitSignature(ethSig));
return checkType(UncompressedPoint, Buffer.from(ethRecovered.slice(2), "hex"));
},
r: (x: RecoverableSignature): FieldElement => Signature.r(RecoverableSignature.sig(x)),
s: (x: RecoverableSignature): FieldElement => Signature.s(RecoverableSignature.sig(x)),
isLowR: (x: RecoverableSignature): boolean => Signature.isLowR(RecoverableSignature.sig(x)),
isLowS: (x: RecoverableSignature): boolean => Signature.isLowS(RecoverableSignature.sig(x)),
verify: (x: RecoverableSignature, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, publicKey: CurvePoint): boolean => {
return Signature.verify(RecoverableSignature.sig(x), digestAlgorithm, message, publicKey);
},
};
const recoverableSignature = Object.assign(
recoverableSignatureBase,
signatureStatic as Omit<typeof signatureStatic, keyof typeof recoverableSignatureStatic>,
recoverableSignatureStatic
);
export const RecoverableSignature: typeof recoverableSignature = recoverableSignature;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as bip32crypto from "bip32/src/crypto";
import * as tinyecc from "tiny-secp256k1";
import { TextEncoder } from "web-encoding";

import { ByteArray, Uint32, checkType, safeBufferFrom } from "../../types";
import { ByteArray, Uint32, checkType, safeBufferFrom, assertType } from "../../types";
import { Digest, SecP256K1 } from "../../core";
import { ChainCode } from "../../core/bip32";

Expand Down Expand Up @@ -54,9 +54,23 @@ export class Node implements BIP32.Node, SecP256K1.ECDSARecoverableKey, SecP256K
return this.#publicKey;
}

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> {
async ecdsaSign(digestAlgorithm: null, msg: ByteArray<32>, counter?: Uint32): Promise<SecP256K1.Signature>
async ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, msg: Uint8Array, counter?: Uint32): Promise<SecP256K1.Signature>
async ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32> | null, msg: Uint8Array, counter?: Uint32): Promise<SecP256K1.Signature> {
const recoverableSig = await (async () =>{
if (digestAlgorithm === null) {
assertType(ByteArray(32), msg);
return await this.ecdsaSignRecoverable(digestAlgorithm, msg, counter);
} else {
return await this.ecdsaSignRecoverable(digestAlgorithm, msg, counter);
}
})();
return SecP256K1.RecoverableSignature.sig(recoverableSig)
}

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

Expand Down

0 comments on commit a72bbcc

Please sign in to comment.