diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/binance.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/binance.ts index f496febcf..49af6befb 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/binance.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/binance.ts @@ -7,9 +7,8 @@ export default { async create(keyPair: BIP32.Node): Promise { return async (tx: Transaction, signMsg?: any): Promise => { 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; }; diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/bitcoin.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/bitcoin.ts index ca2a1fc87..1218fc2fe 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/bitcoin.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/bitcoin.ts @@ -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 & Pick; @@ -41,8 +42,21 @@ export class ECPairAdapter implements SecP256K1.ECDSAKey, SignerAsync, ECPairInt } async sign(hash: bcrypto.NonDigest | bcrypto.Digest<"hash256">, lowR?: boolean): Promise { + 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(); } @@ -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); } } diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/cosmos.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/cosmos.ts index effeade5a..8b3628e67 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/cosmos.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/cosmos.ts @@ -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; @@ -19,8 +19,7 @@ export class WalletAdapter { async sign(signMessage: string): Promise { 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)); } } diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/ethereum.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/ethereum.ts index 7996bb98c..d13e5c261 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/ethereum.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/ethereum.ts @@ -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; @@ -30,7 +30,7 @@ export class SignerAdapter extends ethers.Signer { } async signDigest(digest: ethers.BytesLike): Promise { - 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])])); } @@ -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); } @@ -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); } } diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/fio.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/fio.ts index 947d6e5ff..c7ceb19e7 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/fio.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/fio.ts @@ -33,8 +33,7 @@ export class ExternalSignerAdapter implements FIOExternalPrivateKey { } async sign(signBuf: Uint8Array): Promise { - 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")}`; } diff --git a/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/interfaces.ts b/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/interfaces.ts index 9ef8af7c2..900429458 100644 --- a/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/interfaces.ts +++ b/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/interfaces.ts @@ -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 { @@ -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>; - ecdsaSign(message: Message, counter: Uint32): Promise | undefined>; + ecdsaSign(digestAlgorithm: null, message: ByteArray<32>): Promise>; + ecdsaSign(digestAlgorithm: null, message: ByteArray<32>, counter: Uint32): Promise | undefined>; + ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array): Promise>; + ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array, counter: Uint32): Promise | undefined>; } export interface ECDSARecoverableKey extends ECDSAKey { - ecdsaSign(message: Message): Promise>; - ecdsaSign(message: Message, counter: Uint32): Promise | undefined>; + ecdsaSign(digestAlgorithm: null, message: ByteArray<32>): Promise>; + ecdsaSign(digestAlgorithm: null, message: ByteArray<32>, counter: Uint32): Promise | undefined>; + ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array): Promise>; + ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, message: Uint8Array, counter: Uint32): Promise | undefined>; } export interface ECDHKey { diff --git a/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/types.ts b/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/types.ts index 8edfcbaa1..471ee59d6 100644 --- a/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/types.ts +++ b/packages/hdwallet-native/src/crypto/isolation/core/secp256k1/types.ts @@ -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( @@ -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; const messageWithPreimageStatic = {}; const messageWithPreimage = Object.assign(messageWithPreimageBase, ByteArray, messageWithPreimageStatic); @@ -115,10 +115,18 @@ 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 => { + signCanonically: async (x: ECDSAKey, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, counter?: Uint32): Promise => { + 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; @@ -126,8 +134,9 @@ const signatureStatic = { // 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); @@ -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 => { + signCanonically: async (x: ECDSAKey, digestAlgorithm: Digest.AlgorithmName<32> | null, message: Uint8Array, counter?: Uint32): Promise => { 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")); }, }; diff --git a/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts b/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts index 64842d015..aa4759256 100644 --- a/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts +++ b/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts @@ -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.Message.assert(msg); + async ecdsaSign(digestAlgorithm: null, msg: ByteArray<32>, counter?: Uint32): Promise + async ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32>, msg: Uint8Array, counter?: Uint32): Promise + async ecdsaSign(digestAlgorithm: Digest.AlgorithmName<32> | null, msg: Uint8Array, counter?: Uint32): Promise { 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, ); } diff --git a/packages/hdwallet-native/src/crypto/isolation/types.ts b/packages/hdwallet-native/src/crypto/isolation/types.ts index ed9a937d1..aa05f5d15 100644 --- a/packages/hdwallet-native/src/crypto/isolation/types.ts +++ b/packages/hdwallet-native/src/crypto/isolation/types.ts @@ -74,9 +74,13 @@ const boundedStringStatic = {}; const boundedString = Object.assign(boundedStringBase, boundedStringStatic); export const BoundedString: typeof boundedString = boundedString; -export function checkType>(rt: T, value: any): Static { +export function assertType>(rt: T, value: unknown): asserts value is Static { rt.assert(value); - return value as Static; +} + +export function checkType>(rt: T, value: unknown): Static { + assertType(rt, value); + return value; } // export function ParseWorkaround>(rt: U) {