diff --git a/package.json b/package.json index 7ec82335..3c0d075c 100644 --- a/package.json +++ b/package.json @@ -91,10 +91,10 @@ "@types/bn.js": "^4.11.3", "bn.js": "^4.11.0", "create-hash": "^1.1.2", + "ethereum-cryptography": "^0.1.3", + "elliptic": "^6.5.2", "ethjs-util": "0.1.6", - "keccak": "^2.0.0", - "rlp": "^2.2.3", - "secp256k1": "^3.0.1" + "rlp": "^2.2.3" }, "devDependencies": { "@ethereumjs/config-prettier": "^1.1.0", diff --git a/src/account.ts b/src/account.ts index 79e8e0a5..5c9e0be7 100644 --- a/src/account.ts +++ b/src/account.ts @@ -1,6 +1,6 @@ const assert = require('assert') const ethjsUtil = require('ethjs-util') -const secp256k1 = require('secp256k1') +const secp256k1 = require('./secp256k1v3-adapter') import BN = require('bn.js') import { toBuffer, addHexPrefix, zeros, bufferToHex, unpad } from './bytes' import { keccak, keccak256, rlphash } from './hash' diff --git a/src/hash.ts b/src/hash.ts index ea837906..d31ef312 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -1,4 +1,4 @@ -const createKeccakHash = require('keccak') +const { keccak224, keccak384, keccak256: k256, keccak512 } = require('ethereum-cryptography/keccak') const createHash = require('create-hash') const ethjsUtil = require('ethjs-util') import rlp = require('rlp') @@ -19,9 +19,23 @@ export const keccak = function(a: any, bits: number = 256): Buffer { if (!bits) bits = 256 - return createKeccakHash(`keccak${bits}`) - .update(a) - .digest() + switch (bits) { + case 224: { + return keccak224(a) + } + case 256: { + return k256(a) + } + case 384: { + return keccak384(a) + } + case 512: { + return keccak512(a) + } + default: { + throw new Error(`Invald algorithm: keccak${bits}`) + } + } } /** diff --git a/src/index.ts b/src/index.ts index c44af02e..a8cbc30d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -const secp256k1 = require('secp256k1') +const secp256k1 = require('./secp256k1v3-adapter') const ethjsUtil = require('ethjs-util') import BN = require('bn.js') import rlp = require('rlp') diff --git a/src/secp256k1v3-adapter.ts b/src/secp256k1v3-adapter.ts new file mode 100644 index 00000000..9c60ef06 --- /dev/null +++ b/src/secp256k1v3-adapter.ts @@ -0,0 +1,410 @@ +const secp256k1 = require('ethereum-cryptography/secp256k1') +const secp256k1v3 = require('./secp256k1v3-lib/index') +const der = require('./secp256k1v3-lib/der') + +export interface SignOptions { + data?: Buffer + noncefn?: ( + message: Buffer, + privateKey: Buffer, + algo: Buffer | null, + data: Buffer | null, + attempt: number, + ) => Buffer +} + +export interface SignOptionsV4 { + data?: Uint8Array + noncefn?: ( + message: Uint8Array, + privateKey: Uint8Array, + algo: Uint8Array | null, + data: Uint8Array | null, + attempt: number, + ) => Uint8Array +} + +/** + * Verify an ECDSA privateKey + * @method privateKeyVerify + * @param {Buffer} privateKey + * @return {boolean} + */ +export const privateKeyVerify = function(privateKey: Buffer): boolean { + // secp256k1 v4 version throws when privateKey length is not 32 + if (privateKey.length !== 32) { + return false + } + + return secp256k1.privateKeyVerify(Uint8Array.from(privateKey)) +} + +/** + * Export a privateKey in DER format + * @method privateKeyExport + * @param {Buffer} privateKey + * @param {boolean} compressed + * @return {boolean} + */ +export const privateKeyExport = function(privateKey: Buffer, compressed?: boolean): Buffer { + // secp256k1 v4 version throws when privateKey length is not 32 + if (privateKey.length !== 32) { + throw new RangeError('private key length is invalid') + } + + const publicKey = secp256k1v3.privateKeyExport(privateKey, compressed) + + return der.privateKeyExport(privateKey, publicKey, compressed) +} + +/** + * Import a privateKey in DER format + * @method privateKeyImport + * @param {Buffer} privateKey + * @return {Buffer} + */ +export const privateKeyImport = function(privateKey: Buffer): Buffer { + // privateKeyImport method is not part of secp256k1 v4 package + // this implementation is based on v3 + privateKey = der.privateKeyImport(privateKey) + if (privateKey !== null && privateKey.length === 32 && privateKeyVerify(privateKey)) { + return privateKey + } + + throw new Error("couldn't import from DER format") +} + +/** + * Negate a privateKey by subtracting it from the order of the curve's base point + * @method privateKeyNegate + * @param {Buffer} privateKey + * @return {Buffer} + */ +export const privateKeyNegate = function(privateKey: Buffer): Buffer { + return Buffer.from(secp256k1.privateKeyNegate(Uint8Array.from(privateKey))) +} + +/** + * Compute the inverse of a privateKey (modulo the order of the curve's base point). + * @method privateKeyModInverse + * @param {Buffer} privateKey + * @return {Buffer} + */ +export const privateKeyModInverse = function(privateKey: Buffer): Buffer { + if (privateKey.length !== 32) { + throw new Error('private key length is invalid') + } + + return Buffer.from(secp256k1v3.privateKeyModInverse(Uint8Array.from(privateKey))) +} + +/** + * Tweak a privateKey by adding tweak to it. + * @method privateKeyTweakAdd + * @param {Buffer} privateKey + * @param {Buffer} tweak + * @return {Buffer} + */ +export const privateKeyTweakAdd = function(privateKey: Buffer, tweak: Buffer): Buffer { + return Buffer.from(secp256k1.privateKeyTweakAdd(Uint8Array.from(privateKey), tweak)) +} + +/** + * Tweak a privateKey by multiplying it by a tweak. + * @method privateKeyTweakMul + * @param {Buffer} privateKey + * @param {Buffer} tweak + * @return {Buffer} + */ +export const privateKeyTweakMul = function(privateKey: Buffer, tweak: Buffer): Buffer { + return Buffer.from( + secp256k1.privateKeyTweakMul(Uint8Array.from(privateKey), Uint8Array.from(tweak)), + ) +} + +/** + * Compute the public key for a privateKey. + * @method publicKeyCreate + * @param {Buffer} privateKey + * @param {boolean} compressed + * @return {Buffer} + */ +export const publicKeyCreate = function(privateKey: Buffer, compressed?: boolean): Buffer { + return Buffer.from(secp256k1.publicKeyCreate(Uint8Array.from(privateKey), compressed)) +} + +/** + * Convert a publicKey to compressed or uncompressed form. + * @method publicKeyConvert + * @param {Buffer} publicKey + * @param {boolean} compressed + * @return {Buffer} + */ +export const publicKeyConvert = function(publicKey: Buffer, compressed?: boolean): Buffer { + return Buffer.from(secp256k1.publicKeyConvert(Uint8Array.from(publicKey), compressed)) +} + +/** + * Verify an ECDSA publicKey. + * @method publicKeyVerify + * @param {Buffer} publicKey + * @return {boolean} + */ +export const publicKeyVerify = function(publicKey: Buffer): boolean { + // secp256k1 v4 version throws when publicKey length is not 33 or 65 + if (publicKey.length !== 33 && publicKey.length !== 65) { + return false + } + + return secp256k1.publicKeyVerify(Uint8Array.from(publicKey)) +} + +/** + * Tweak a publicKey by adding tweak times the generator to it. + * @method publicKeyTweakAdd + * @param {Buffer} publicKey + * @param {Buffer} tweak + * @param {boolean} compressed + * @return {Buffer} + */ +export const publicKeyTweakAdd = function( + publicKey: Buffer, + tweak: Buffer, + compressed?: boolean, +): Buffer { + return Buffer.from( + secp256k1.publicKeyTweakAdd(Uint8Array.from(publicKey), Uint8Array.from(tweak), compressed), + ) +} + +/** + * Tweak a publicKey by multiplying it by a tweak value + * @method publicKeyTweakMul + * @param {Buffer} publicKey + * @param {Buffer} tweak + * @param {boolean} compressed + * @return {Buffer} + */ +export const publicKeyTweakMul = function( + publicKey: Buffer, + tweak: Buffer, + compressed?: boolean, +): Buffer { + return Buffer.from( + secp256k1.publicKeyTweakMul(Uint8Array.from(publicKey), Uint8Array.from(tweak), compressed), + ) +} + +/** + * Add a given publicKeys together. + * @method publicKeyCombine + * @param {Array} publicKeys + * @param {boolean} compressed + * @return {Buffer} + */ +export const publicKeyCombine = function(publicKeys: Buffer[], compressed?: boolean): Buffer { + const keys: Uint8Array[] = [] + publicKeys.forEach((publicKey: Buffer) => { + keys.push(Uint8Array.from(publicKey)) + }) + + return Buffer.from(secp256k1.publicKeyCombine(keys, compressed)) +} + +/** + * Convert a signature to a normalized lower-S form. + * @method signatureNormalize + * @param {Buffer} signature + * @return {Buffer} + */ +export const signatureNormalize = function(signature: Buffer): Buffer { + return Buffer.from(secp256k1.signatureNormalize(Uint8Array.from(signature))) +} + +/** + * Serialize an ECDSA signature in DER format. + * @method signatureExport + * @param {Buffer} signature + * @return {Buffer} + */ +export const signatureExport = function(signature: Buffer): Buffer { + return Buffer.from(secp256k1.signatureExport(Uint8Array.from(signature))) +} + +/** + * Parse a DER ECDSA signature (follow by [BIP66](https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki)). + * @method signatureImport + * @param {Buffer} signature + * @return {Buffer} + */ +export const signatureImport = function(signature: Buffer): Buffer { + return Buffer.from(secp256k1.signatureImport(Uint8Array.from(signature))) +} + +/** + * Parse a DER ECDSA signature (not follow by [BIP66](https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki)). + * @method signatureImportLax + * @param {Buffer} signature + * @return {Buffer} + */ +export const signatureImportLax = function(signature: Buffer): Buffer { + // signatureImportLax method is not part of secp256k1 v4 package + // this implementation is based on v3 + // ensure that signature is greater than 0 + if (signature.length === 0) { + throw new RangeError('signature length is invalid') + } + + const sigObj = der.signatureImportLax(signature) + if (sigObj === null) { + throw new Error("couldn't parse DER signature") + } + + return secp256k1v3.signatureImport(sigObj) +} + +/** + * Create an ECDSA signature. Always return low-S signature. + * @method sign + * @param {Buffer} message + * @param {Buffer} privateKey + * @param {Object} options + * @return {Buffer} + */ +export const sign = function( + message: Buffer, + privateKey: Buffer, + options?: SignOptions, +): { signature: Buffer; recovery: number } { + if (options === null) { + throw new TypeError('options should be an Object') + } + + let signOptions: SignOptionsV4 | undefined = undefined + + if (options) { + signOptions = {} + + if (options.data === null) { + // validate option.data length + throw new TypeError('options.data should be a Buffer') + } + + if (options.data) { + if (options.data.length != 32) { + throw new RangeError('options.data length is invalid') + } + + signOptions.data = new Uint8Array(options.data) + } + + if (options.noncefn === null) { + throw new TypeError('options.noncefn should be a Function') + } + + if (options.noncefn) { + // convert option.noncefn function signature + signOptions.noncefn = ( + message: Uint8Array, + privateKey: Uint8Array, + algo: Uint8Array | null, + data: Uint8Array | null, + attempt: number, + ) => { + const bufferAlgo: Buffer | null = algo != null ? Buffer.from(algo) : null + const bufferData: Buffer | null = data != null ? Buffer.from(data) : null + + let buffer: Buffer = Buffer.from('') + + if (options.noncefn) { + buffer = options.noncefn( + Buffer.from(message), + Buffer.from(privateKey), + bufferAlgo, + bufferData, + attempt, + ) + } + + return new Uint8Array(buffer) + } + } + } + + const sig = secp256k1.ecdsaSign( + Uint8Array.from(message), + Uint8Array.from(privateKey), + signOptions, + ) + + return { + signature: Buffer.from(sig.signature), + recovery: sig.recid, + } +} + +/** + * Verify an ECDSA signature. + * @method verify + * @param {Buffer} message + * @param {Buffer} signature + * @param {Buffer} publicKey + * @return {boolean} + */ +export const verify = function(message: Buffer, signature: Buffer, publicKey: Buffer): boolean { + return secp256k1.ecdsaVerify(Uint8Array.from(signature), Uint8Array.from(message), publicKey) +} + +/** + * Recover an ECDSA public key from a signature. + * @method recover + * @param {Buffer} message + * @param {Buffer} signature + * @param {Number} recid + * @param {boolean} compressed + * @return {Buffer} + */ +export const recover = function( + message: Buffer, + signature: Buffer, + recid: number, + compressed?: boolean, +): Buffer { + return Buffer.from( + secp256k1.ecdsaRecover(Uint8Array.from(signature), recid, Uint8Array.from(message), compressed), + ) +} + +/** + * Compute an EC Diffie-Hellman secret and applied sha256 to compressed public key. + * @method ecdh + * @param {Buffer} publicKey + * @param {Buffer} privateKey + * @return {Buffer} + */ +export const ecdh = function(publicKey: Buffer, privateKey: Buffer): Buffer { + // note: secp256k1 v3 doesn't allow optional parameter + return Buffer.from(secp256k1.ecdh(Uint8Array.from(publicKey), Uint8Array.from(privateKey), {})) +} + +export const ecdhUnsafe = function( + publicKey: Buffer, + privateKey: Buffer, + compressed?: boolean, +): Buffer { + // ecdhUnsafe method is not part of secp256k1 v4 package + // this implementation is based on v3 + // ensure valid publicKey length + if (publicKey.length !== 33 && publicKey.length !== 65) { + throw new RangeError('public key length is invalid') + } + + // ensure valid privateKey length + if (privateKey.length !== 32) { + throw new RangeError('private key length is invalid') + } + + return Buffer.from( + secp256k1v3.ecdhUnsafe(Uint8Array.from(publicKey), Uint8Array.from(privateKey), compressed), + ) +} diff --git a/src/secp256k1v3-lib/der.ts b/src/secp256k1v3-lib/der.ts new file mode 100644 index 00000000..946c05ce --- /dev/null +++ b/src/secp256k1v3-lib/der.ts @@ -0,0 +1,653 @@ +// This file is imported from secp256k1 v3 +// https://github.com/cryptocoinjs/secp256k1-node/blob/master/LICENSE + +import { SigObj } from './index' + +const EC_PRIVKEY_EXPORT_DER_COMPRESSED = Buffer.from([ + // begin + 0x30, + 0x81, + 0xd3, + 0x02, + 0x01, + 0x01, + 0x04, + 0x20, + // private key + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + // middle + 0xa0, + 0x81, + 0x85, + 0x30, + 0x81, + 0x82, + 0x02, + 0x01, + 0x01, + 0x30, + 0x2c, + 0x06, + 0x07, + 0x2a, + 0x86, + 0x48, + 0xce, + 0x3d, + 0x01, + 0x01, + 0x02, + 0x21, + 0x00, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xfe, + 0xff, + 0xff, + 0xfc, + 0x2f, + 0x30, + 0x06, + 0x04, + 0x01, + 0x00, + 0x04, + 0x01, + 0x07, + 0x04, + 0x21, + 0x02, + 0x79, + 0xbe, + 0x66, + 0x7e, + 0xf9, + 0xdc, + 0xbb, + 0xac, + 0x55, + 0xa0, + 0x62, + 0x95, + 0xce, + 0x87, + 0x0b, + 0x07, + 0x02, + 0x9b, + 0xfc, + 0xdb, + 0x2d, + 0xce, + 0x28, + 0xd9, + 0x59, + 0xf2, + 0x81, + 0x5b, + 0x16, + 0xf8, + 0x17, + 0x98, + 0x02, + 0x21, + 0x00, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xfe, + 0xba, + 0xae, + 0xdc, + 0xe6, + 0xaf, + 0x48, + 0xa0, + 0x3b, + 0xbf, + 0xd2, + 0x5e, + 0x8c, + 0xd0, + 0x36, + 0x41, + 0x41, + 0x02, + 0x01, + 0x01, + 0xa1, + 0x24, + 0x03, + 0x22, + 0x00, + // public key + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, +]) + +const EC_PRIVKEY_EXPORT_DER_UNCOMPRESSED = Buffer.from([ + // begin + 0x30, + 0x82, + 0x01, + 0x13, + 0x02, + 0x01, + 0x01, + 0x04, + 0x20, + // private key + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + // middle + 0xa0, + 0x81, + 0xa5, + 0x30, + 0x81, + 0xa2, + 0x02, + 0x01, + 0x01, + 0x30, + 0x2c, + 0x06, + 0x07, + 0x2a, + 0x86, + 0x48, + 0xce, + 0x3d, + 0x01, + 0x01, + 0x02, + 0x21, + 0x00, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xfe, + 0xff, + 0xff, + 0xfc, + 0x2f, + 0x30, + 0x06, + 0x04, + 0x01, + 0x00, + 0x04, + 0x01, + 0x07, + 0x04, + 0x41, + 0x04, + 0x79, + 0xbe, + 0x66, + 0x7e, + 0xf9, + 0xdc, + 0xbb, + 0xac, + 0x55, + 0xa0, + 0x62, + 0x95, + 0xce, + 0x87, + 0x0b, + 0x07, + 0x02, + 0x9b, + 0xfc, + 0xdb, + 0x2d, + 0xce, + 0x28, + 0xd9, + 0x59, + 0xf2, + 0x81, + 0x5b, + 0x16, + 0xf8, + 0x17, + 0x98, + 0x48, + 0x3a, + 0xda, + 0x77, + 0x26, + 0xa3, + 0xc4, + 0x65, + 0x5d, + 0xa4, + 0xfb, + 0xfc, + 0x0e, + 0x11, + 0x08, + 0xa8, + 0xfd, + 0x17, + 0xb4, + 0x48, + 0xa6, + 0x85, + 0x54, + 0x19, + 0x9c, + 0x47, + 0xd0, + 0x8f, + 0xfb, + 0x10, + 0xd4, + 0xb8, + 0x02, + 0x21, + 0x00, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xfe, + 0xba, + 0xae, + 0xdc, + 0xe6, + 0xaf, + 0x48, + 0xa0, + 0x3b, + 0xbf, + 0xd2, + 0x5e, + 0x8c, + 0xd0, + 0x36, + 0x41, + 0x41, + 0x02, + 0x01, + 0x01, + 0xa1, + 0x44, + 0x03, + 0x42, + 0x00, + // public key + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, +]) + +exports.privateKeyExport = function( + privateKey: Buffer, + publicKey: Buffer, + compressed: boolean = true, +) { + const result = Buffer.from( + compressed ? EC_PRIVKEY_EXPORT_DER_COMPRESSED : EC_PRIVKEY_EXPORT_DER_UNCOMPRESSED, + ) + privateKey.copy(result, compressed ? 8 : 9) + publicKey.copy(result, compressed ? 181 : 214) + return result +} + +exports.privateKeyImport = function(privateKey: Buffer): Buffer | null { + const length = privateKey.length + + // sequence header + let index = 0 + if (length < index + 1 || privateKey[index] !== 0x30) return null + index += 1 + + // sequence length constructor + if (length < index + 1 || !(privateKey[index] & 0x80)) return null + + const lenb = privateKey[index] & 0x7f + index += 1 + if (lenb < 1 || lenb > 2) return null + if (length < index + lenb) return null + + // sequence length + const len = privateKey[index + lenb - 1] | (lenb > 1 ? privateKey[index + lenb - 2] << 8 : 0) + index += lenb + if (length < index + len) return null + + // sequence element 0: version number (=1) + if ( + length < index + 3 || + privateKey[index] !== 0x02 || + privateKey[index + 1] !== 0x01 || + privateKey[index + 2] !== 0x01 + ) { + return null + } + index += 3 + + // sequence element 1: octet string, up to 32 bytes + if ( + length < index + 2 || + privateKey[index] !== 0x04 || + privateKey[index + 1] > 0x20 || + length < index + 2 + privateKey[index + 1] + ) { + return null + } + + return privateKey.slice(index + 2, index + 2 + privateKey[index + 1]) +} + +exports.signatureImportLax = function(signature: Buffer): SigObj | null { + const r = Buffer.alloc(32, 0) + const s = Buffer.alloc(32, 0) + + const length = signature.length + let index = 0 + + // sequence tag byte + if (signature[index++] !== 0x30) { + return null + } + + // sequence length byte + let lenbyte = signature[index++] + if (lenbyte & 0x80) { + index += lenbyte - 0x80 + if (index > length) { + return null + } + } + + // sequence tag byte for r + if (signature[index++] !== 0x02) { + return null + } + + // length for r + let rlen = signature[index++] + if (rlen & 0x80) { + lenbyte = rlen - 0x80 + if (index + lenbyte > length) { + return null + } + for (; lenbyte > 0 && signature[index] === 0x00; index += 1, lenbyte -= 1); + for (rlen = 0; lenbyte > 0; index += 1, lenbyte -= 1) rlen = (rlen << 8) + signature[index] + } + if (rlen > length - index) { + return null + } + let rindex = index + index += rlen + + // sequence tag byte for s + if (signature[index++] !== 0x02) { + return null + } + + // length for s + let slen = signature[index++] + if (slen & 0x80) { + lenbyte = slen - 0x80 + if (index + lenbyte > length) { + return null + } + for (; lenbyte > 0 && signature[index] === 0x00; index += 1, lenbyte -= 1); + for (slen = 0; lenbyte > 0; index += 1, lenbyte -= 1) slen = (slen << 8) + signature[index] + } + if (slen > length - index) { + return null + } + let sindex = index + index += slen + + // ignore leading zeros in r + for (; rlen > 0 && signature[rindex] === 0x00; rlen -= 1, rindex += 1); + // copy r value + if (rlen > 32) { + return null + } + const rvalue = signature.slice(rindex, rindex + rlen) + rvalue.copy(r, 32 - rvalue.length) + + // ignore leading zeros in s + for (; slen > 0 && signature[sindex] === 0x00; slen -= 1, sindex += 1); + // copy s value + if (slen > 32) { + return null + } + const svalue = signature.slice(sindex, sindex + slen) + svalue.copy(s, 32 - svalue.length) + + return { r: r, s: s } +} diff --git a/src/secp256k1v3-lib/index.ts b/src/secp256k1v3-lib/index.ts new file mode 100644 index 00000000..210adc3c --- /dev/null +++ b/src/secp256k1v3-lib/index.ts @@ -0,0 +1,79 @@ +// This file is imported from secp256k1 v3 +// https://github.com/cryptocoinjs/secp256k1-node/blob/master/LICENSE + +import BN = require('bn.js') +const EC = require('elliptic').ec + +const ec = new EC('secp256k1') +const ecparams = ec.curve + +export interface SigObj { + r: Buffer + s: Buffer +} + +exports.privateKeyExport = function(privateKey: Buffer, compressed: boolean = true): Buffer { + const d = new BN(privateKey) + if (d.ucmp(ecparams.n) >= 0) { + throw new Error("couldn't export to DER format") + } + + const point = ec.g.mul(d) + return toPublicKey(point.getX(), point.getY(), compressed) +} + +exports.privateKeyModInverse = function(privateKey: Buffer): Buffer { + const bn = new BN(privateKey) + if (bn.ucmp(ecparams.n) >= 0 || bn.isZero()) { + throw new Error('private key range is invalid') + } + + return bn.invm(ecparams.n).toArrayLike(Buffer, 'be', 32) +} + +exports.signatureImport = function(sigObj: SigObj): Buffer { + let r = new BN(sigObj.r) + if (r.ucmp(ecparams.n) >= 0) { + r = new BN(0) + } + + let s = new BN(sigObj.s) + if (s.ucmp(ecparams.n) >= 0) { + s = new BN(0) + } + + return Buffer.concat([r.toArrayLike(Buffer, 'be', 32), s.toArrayLike(Buffer, 'be', 32)]) +} + +exports.ecdhUnsafe = function( + publicKey: Buffer, + privateKey: Buffer, + compressed: boolean = true, +): Buffer { + const point = ec.keyFromPublic(publicKey) + + const scalar = new BN(privateKey) + if (scalar.ucmp(ecparams.n) >= 0 || scalar.isZero()) { + throw new Error('scalar was invalid (zero or overflow)') + } + + const shared = point.pub.mul(scalar) + return toPublicKey(shared.getX(), shared.getY(), compressed) +} + +const toPublicKey = function(x: BN, y: BN, compressed: boolean): Buffer { + let publicKey + + if (compressed) { + publicKey = Buffer.alloc(33) + publicKey[0] = y.isOdd() ? 0x03 : 0x02 + x.toArrayLike(Buffer, 'be', 32).copy(publicKey, 1) + } else { + publicKey = Buffer.alloc(65) + publicKey[0] = 0x04 + x.toArrayLike(Buffer, 'be', 32).copy(publicKey, 1) + y.toArrayLike(Buffer, 'be', 32).copy(publicKey, 33) + } + + return publicKey +} diff --git a/src/signature.ts b/src/signature.ts index 0149b6ba..5489360a 100644 --- a/src/signature.ts +++ b/src/signature.ts @@ -1,4 +1,4 @@ -const secp256k1 = require('secp256k1') +const secp256k1 = require('./secp256k1v3-adapter') import BN = require('bn.js') import { toBuffer, setLength, setLengthLeft, bufferToHex } from './bytes' import { keccak } from './hash' diff --git a/test/index.js b/test/index.js index 9ae9b96f..ee17179e 100644 --- a/test/index.js +++ b/test/index.js @@ -75,12 +75,38 @@ describe('is zero address', function () { }) describe('keccak', function () { - it('should produce a hash', function () { + it('should produce a keccak224 hash', function() { + const msg = '0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1' + const r = '9e66938bd8f32c8610444bb524630db496bd58b689f9733182df63ba' + const hash = ethUtils.keccak(msg, 224) + assert.equal(hash.toString('hex'), r) + }) + it('should produce a keccak256 hash', function() { const msg = '0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1' const r = '82ff40c0a986c6a5cfad4ddf4c3aa6996f1a7837f9c398e17e5de5cbd5a12b28' const hash = ethUtils.keccak(msg) assert.equal(hash.toString('hex'), r) }) + it('should produce a keccak384 hash', function() { + const msg = '0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1' + const r = + '923e0f6a1c324a698139c3f3abbe88ac70bf2e7c02b26192c6124732555a32cef18e81ac91d5d97ce969745409c5bbc6' + const hash = ethUtils.keccak(msg, 384) + assert.equal(hash.toString('hex'), r) + }) + it('should produce a keccak512 hash', function() { + const msg = '0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1' + const r = + '36fdacd0339307068e9ed191773a6f11f6f9f99016bd50f87fd529ab7c87e1385f2b7ef1ac257cc78a12dcb3e5804254c6a7b404a6484966b831eadc721c3d24' + const hash = ethUtils.keccak(msg, 512) + assert.equal(hash.toString('hex'), r) + }) + it('should error if provided incorrect bits', function() { + const msg = '0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1' + assert.throws(function() { + ethUtils.keccak(msg, 1024) + }) + }) }) describe('keccak256', function () { @@ -618,6 +644,13 @@ describe('isValidSignature', function () { const v = 27 assert.equal(ethUtils.isValidSignature(v, r, s, true), false) }) + it('should fail when s is 0 bytes', function () { + const r = Buffer.from('99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9', 'hex') + const s = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex') + + const v = 27 + assert.equal(ethUtils.isValidSignature(v, r, s, true), false) + }) it('should not fail when not on homestead but s > secp256k1n/2', function () { const SECP256K1_N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16) diff --git a/test/secp256k1-adapter.js b/test/secp256k1-adapter.js new file mode 100644 index 00000000..da115646 --- /dev/null +++ b/test/secp256k1-adapter.js @@ -0,0 +1,1336 @@ +// This file is imported from secp256k1 v3 +// https://github.com/cryptocoinjs/secp256k1-node/blob/master/LICENSE + +const assert = require('assert') +const ethUtils = require('../dist/index.js') +const BN = require('bn.js') +const util = require('./util') +const getRandomBytes = require('crypto').randomBytes + +describe('privateKeyVerify', function () { + it('should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyVerify(null) + }) + }) + + it('invalid length', function () { + assert.equal(ethUtils.secp256k1.privateKeyVerify(util.getPrivateKey().slice(1)), false) + }) + + it('zero key', function () { + const privateKey = util.BN_ZERO.toArrayLike(Buffer, 'be', 32) + assert.equal(ethUtils.secp256k1.privateKeyVerify(privateKey), false) + }) + + + it('equal to N', function () { + const privateKey = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + assert.equal(ethUtils.secp256k1.privateKeyVerify(privateKey), false) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + privateKeys.forEach((privateKey) => { + assert.equal(ethUtils.secp256k1.privateKeyVerify(privateKey), true) + }) + }) +}) + +describe('privateKeyExport', function () { + it('private key should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyExport(null) + }) + }) + + it('private key length is invalid', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyExport(util.getPrivateKey().slice(1)) + }) + }) + + it('private key is invalid', function () { + assert.throws(function () { + const privateKey = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyExport(privateKey) + }) + }) +}) + +describe('privateKeyImport', function () { + it('should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyImport(null) + }) + }) + + it('invalid format', function () { + const buffers = [ + Buffer.from([0x00]), + Buffer.from([0x30, 0x7b]), + Buffer.from([0x30, 0x87]), + Buffer.from([0x30, 0x81]), + Buffer.from([0x30, 0x82, 0x00, 0xff]), + Buffer.from([0x30, 0x82, 0x00, 0x00]), + Buffer.from([0x30, 0x82, 0x00, 0x00, 0x02, 0x01, 0x01]) + ] + + buffers.forEach((buffer) => { + assert.throws(function () { + ethUtils.secp256k1.privateKeyImport(buffer) + }) + }) + }) +}) + +describe('privateKeyExport/privateKeyImport', function () { + it('export/import', function() { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const der1 = ethUtils.secp256k1.privateKeyExport(privateKey, true) + const privateKey1 = ethUtils.secp256k1.privateKeyImport(der1) + assert.deepEqual(privateKey1, privateKey) + + const der2 = ethUtils.secp256k1.privateKeyExport(privateKey, false) + const privateKey2 = ethUtils.secp256k1.privateKeyImport(der2) + assert.deepEqual(privateKey2, privateKey) + + const der3 = ethUtils.secp256k1.privateKeyExport(privateKey) + const privateKey3 = ethUtils.secp256k1.privateKeyImport(der3) + assert.deepEqual(privateKey3, privateKey) + }) + }) +}) + +describe('privateKeyNegate', function () { + it('private key should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyNegate(null) + }) + }) + + it('private key length is invalid', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyNegate(util.getPrivateKey().slice(1)) + }) + }) + + it('private key is 0', function () { + const privateKey = util.BN_ZERO.toArrayLike(Buffer, 'be', 32) + + const expected = Buffer.alloc(32) + const result = ethUtils.secp256k1.privateKeyNegate(privateKey) + assert.deepEqual(result, expected) + }) + + it('private key equal to N', function () { + const privateKey = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + + const expected = Buffer.alloc(32) + const result = ethUtils.secp256k1.privateKeyNegate(privateKey) + assert.deepEqual(result, expected) + }) + + it('private key overflow', function () { + const privateKey = util.ec.curve.n.addn(10).toArrayLike(Buffer, 'be', 32) + + const expected = util.ec.curve.n.subn(10).toArrayLike(Buffer, 'be', 32) + const result = ethUtils.secp256k1.privateKeyNegate(privateKey) + assert.deepEqual(result, expected) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + privateKeys.forEach((privateKey) => { + const expected = util.ec.curve.n.sub(new BN(privateKey)) + const result = ethUtils.secp256k1.privateKeyNegate(privateKey) + + assert.deepEqual(result.toString('hex'), expected.toString(16, 64)) + }) + }) +}) + +describe('privateKeyModInverse', function () { + it('private key should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.privateKeyModInverse(null) + }) + }) + + it('private key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey().slice(1) + ethUtils.secp256k1.privateKeyModInverse(privateKey) + }) + }) + + it('private key is 0', function () { + assert.throws(function () { + const privateKey = util.BN_ZERO.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyModInverse(privateKey) + }) + }) + + it('private key equal to N', function () { + assert.throws(function () { + const privateKey = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyModInverse(privateKey) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + privateKeys.forEach((privateKey) => { + const expected = new BN(privateKey).invm(new BN(util.ec.curve.n.toArrayLike(Buffer, 'be', 32))) + const result = ethUtils.secp256k1.privateKeyModInverse(privateKey) + + assert.deepEqual(result.toString('hex'), expected.toString(16, 64)) + }) + }) +}) + +describe('privateKeyTweakAdd', function () { + it('private key should be a Buffer', function () { + assert.throws(function () { + const tweak = util.getTweak() + ethUtils.secp256k1.privateKeyTweakAdd(null, tweak) + }) + }) + + it('private key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey().slice(1) + const tweak = util.getTweak() + ethUtils.secp256k1.privateKeyTweakAdd(privateKey, tweak) + }) + }) + + it('tweak should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.privateKeyTweakAdd(privateKey, null) + }) + }) + + it('tweak length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const tweak = util.getTweak().slice(1) + ethUtils.secp256k1.privateKeyTweakAdd(privateKey, tweak) + }) + }) + + it('tweak overflow', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const tweak = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyTweakAdd(privateKey, tweak) + }) + }) + + it('result is zero: (N - 1) + 1', function () { + assert.throws(function () { + const privateKey = util.ec.curve.n.sub(util.BN_ONE).toArrayLike(Buffer, 'be', 32) + const tweak = util.BN_ONE.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyTweakAdd(privateKey, tweak) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + const tweak = util.getTweak() + + privateKeys.forEach((privateKey) => { + const expected = new BN(privateKey).add(new BN(tweak)).mod(util.ec.curve.n) + if (expected.cmp(util.BN_ZERO) === 0) { + assert.throws(function () { + ethUtils.secp256k1.privateKeyTweakAdd(privateKey, tweak) + }) + } else { + const result = ethUtils.secp256k1.privateKeyTweakAdd(privateKey, tweak) + assert.deepEqual(result.toString('hex'), expected.toString(16, 64)) + } + }) + }) +}) + +describe('privateKeyTweakMul', function () { + it('private key should be a Buffer', function () { + assert.throws(function () { + const tweak = util.getPrivateKey() + ethUtils.secp256k1.privateKeyTweakMul(null, tweak) + }) + }) + + it('private key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey().slice(1) + const tweak = util.getPrivateKey() + ethUtils.secp256k1.privateKeyTweakMul(privateKey, tweak) + }) + }) + + it('tweak should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.privateKeyTweakMul(privateKey, null) + }) + }) + + it('tweak length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const tweak = util.getTweak().slice(1) + ethUtils.secp256k1.privateKeyTweakMul(privateKey, tweak) + }) + }) + + it('tweak equal N', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const tweak = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyTweakMul(privateKey, tweak) + }) + }) + + it('tweak is 0', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const tweak = util.BN_ZERO.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.privateKeyTweakMul(privateKey, tweak) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + const tweak = util.getTweak() + + privateKeys.forEach((privateKey) => { + if (new BN(tweak).cmp(util.BN_ZERO) === 0) { + assert.throws(function () { + ethUtils.secp256k1.privateKeyTweakMul(privateKey, tweak) + }) + } else { + const expected = new BN(privateKey).mul(new BN(tweak)).mod(util.ec.curve.n) + const result = ethUtils.secp256k1.privateKeyTweakMul(privateKey, tweak) + assert.deepEqual(result.toString('hex'), expected.toString(16, 64)) + } + }) + }) +}) + +describe('publicKeyCreate', function () { + it('should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.publicKeyCreate(null) + }) + }) + + it('invalid length', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey().slice(1) + ethUtils.secp256k1.publicKeyCreate(privateKey) + }) + }) + + it('overflow', function () { + assert.throws(function () { + const privateKey = util.ec.curve.p.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.publicKeyCreate(privateKey) + }) + }) + + it('equal zero', function () { + assert.throws(function () { + const privateKey = new BN(0).toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.publicKeyCreate(privateKey) + }) + }) + + it('compressed should be a boolean', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.publicKeyCreate(privateKey, null) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const expected = util.getPublicKey(privateKey) + + const compressed = ethUtils.secp256k1.publicKeyCreate(privateKey, true) + assert.deepEqual(compressed, expected.compressed) + + const uncompressed = ethUtils.secp256k1.publicKeyCreate(privateKey, false) + assert.deepEqual(uncompressed, expected.uncompressed) + }) + }) +}) + +describe('publicKeyConvert', function () { + it('should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.publicKeyConvert(null) + }) + }) + + it('length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + ethUtils.secp256k1.publicKeyConvert(publicKey) + }) + }) + + it('compressed should be a boolean', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.publicKeyConvert(publicKey, null) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const expected = util.getPublicKey(privateKey) + + const compressed = ethUtils.secp256k1.publicKeyConvert(expected.uncompressed, true) + assert.deepEqual(compressed, expected.compressed) + + const uncompressed = ethUtils.secp256k1.publicKeyConvert(expected.uncompressed, false) + assert.deepEqual(uncompressed, expected.uncompressed) + }) + }) +}) + +describe('publicKeyVerify', function () { + it('should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.publicKeyVerify(null) + }) + }) + + it('invalid length', function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('invalid first byte', function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x01 + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('x overflow (first byte is 0x03)', function () { + const publicKey = Buffer.concat([ + Buffer.from([ 0x03 ]), + util.ec.curve.p.toArrayLike(Buffer, 'be', 32) + ]) + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('x overflow', function () { + const publicKey = Buffer.concat([ + Buffer.from([ 0x04 ]), + util.ec.curve.p.toArrayLike(Buffer, 'be', 32) + ]) + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('y overflow', function () { + const publicKey = Buffer.concat([ + Buffer.from([ 0x04 ]), + Buffer.alloc(32), + util.ec.curve.p.toArrayLike(Buffer, 'be', 32) + ]) + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('y is even, first byte is 0x07', function () { + const publicKey = Buffer.concat([ + Buffer.from([ 0x07 ]), + Buffer.alloc(32), + util.ec.curve.p.subn(1).toArrayLike(Buffer, 'be', 32) + ]) + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('y**2 !== x*x*x + 7', function () { + const publicKey = Buffer.concat([Buffer.from([0x04]), util.getTweak(), util.getTweak()]) + assert.equal(ethUtils.secp256k1.publicKeyVerify(publicKey), false) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const expected = util.getPublicKey(privateKey) + + assert.equal(ethUtils.secp256k1.publicKeyVerify(expected.uncompressed), true) + assert.equal(ethUtils.secp256k1.publicKeyVerify(expected.uncompressed), true) + }) + }) +}) + +describe('publicKeyTweakAdd', function () { + it('public key should be a Buffer', function () { + assert.throws(function () { + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakAdd(null, tweak) + }) + }) + + it('public key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak) + }) + }) + + it('public key is invalid (version is 0x01)', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x01 + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak) + }) + }) + + it('tweak should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, null) + }) + }) + + it('tweak length length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = util.getTweak().slice(1) + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak) + }) + }) + + it('tweak overflow', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak) + }) + }) + + it('tweak produce infinity point', function () { + // G * 1 - G = 0 + assert.throws(function () { + const publicKey = Buffer.from(util.ec.g.encode(null, true)) + publicKey[0] = publicKey[0] ^ 0x01 // change sign of G + const tweak = new BN(1).toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak, true) + }) + }) + + it('compressed should be a boolean', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak, null) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const tweak = util.getTweak() + const publicPoint = util.ec.g.mul(new BN(privateKey)) + const publicKey = Buffer.from(publicPoint.encode(null, true)) + const expected = util.ec.g.mul(new BN(tweak)).add(publicPoint) + + const compressed = ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak, true) + assert.deepEqual(compressed.toString('hex'), expected.encode('hex', true)) + + const uncompressed = ethUtils.secp256k1.publicKeyTweakAdd(publicKey, tweak, false) + assert.deepEqual(uncompressed.toString('hex'), expected.encode('hex', false)) + }) + }) +}) + +describe('publicKeyTweakMul', function () { + it('public key should be a Buffer', function () { + assert.throws(function () { + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakMul(null, tweak) + }) + }) + + it('public key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak) + }) + }) + + it('public key is invalid (version is 0x01)', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x01 + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak) + }) + }) + + it('tweak should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.publicKeyTweakMul(publicKey, null) + }) + }) + + it('tweak length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = util.getTweak().slice(1) + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak) + }) + }) + + it('tweak is zero', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = new BN(0).toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak) + }) + }) + + it('tweak overflow', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = util.ec.curve.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak) + }) + }) + + it('compressed should be a boolean', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + const tweak = util.getTweak() + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak, null) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const tweak = util.getTweak() + const publicPoint = util.ec.g.mul(new BN(privateKey)) + const publicKey = Buffer.from(publicPoint.encode(null, true)) + const expected = util.ec.g.mul(new BN(tweak)).add(publicPoint) + + if (new BN(tweak).cmp(util.BN_ZERO) === 0) { + assert.throws(function () { + ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak) + }) + } else { + const expected = publicPoint.mul(tweak) + + const compressed = ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak, true) + assert.deepEqual(compressed.toString('hex'), expected.encode('hex', true)) + + const uncompressed = ethUtils.secp256k1.publicKeyTweakMul(publicKey, tweak, false) + assert.deepEqual(uncompressed.toString('hex'), expected.encode('hex', false)) + } + }) + }) +}) + +describe('publicKeyCombine', function () { + it('public keys should be an Array', function () { + assert.throws(function () { + ethUtils.secp256k1.publicKeyCombine(null) + }) + }) + + it('public keys should have length greater that zero', function () { + assert.throws(function () { + ethUtils.secp256k1.publicKeyCombine([]) + }) + }) + + it('public key should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.publicKeyCombine([null]) + }) + }) + + it('public key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + ethUtils.secp256k1.publicKeyCombine([publicKey]) + }) + }) + + it('public key is invalid (version is 0x01)', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x01 + ethUtils.secp256k1.publicKeyCombine([publicKey]) + }) + }) + + it('compressed should be a boolean', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.publicKeyCombine([publicKey], null) + }) + }) + + it('P + (-P) = 0', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey1 = util.getPublicKey(privateKey).compressed + const publicKey2 = Buffer.from(publicKey1) + publicKey2[0] = publicKey2[0] ^ 0x01 + ethUtils.secp256k1.publicKeyCombine([publicKey1, publicKey2], true) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const cnt = 1 + Math.floor(Math.random() * 3) // 1 <= cnt <= 3 + const privateKeys = [] + while (privateKeys.length < cnt) privateKeys.push(util.getPrivateKey()) + const publicKeys = privateKeys.map(function (privateKey) { + return util.getPublicKey(privateKey).compressed + }) + + let expected = util.ec.g.mul(new BN(privateKeys[0])) + for (let i = 1; i < privateKeys.length; ++i) { + const publicPoint = util.ec.g.mul(new BN(privateKeys[i])) + expected = expected.add(publicPoint) + } + + const compressed = ethUtils.secp256k1.publicKeyCombine(publicKeys, true) + assert.deepEqual(compressed.toString('hex'), expected.encode('hex', true)) + + const uncompressed = ethUtils.secp256k1.publicKeyCombine(publicKeys, false) + assert.deepEqual(uncompressed.toString('hex'), expected.encode('hex', false)) + }) + }) +}) + +describe('signatureNormalize', function () { + it('signature should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.signatureNormalize(null) + }) + }) + + it('invalid length', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey).slice(1) + ethUtils.secp256k1.signatureNormalize(signature) + }) + }) + + it('parse fail (r equal N)', function () { + assert.throws(function () { + const signature = Buffer.concat([ + util.ec.curve.n.toArrayLike(Buffer, 'be', 32), + util.BN_ONE.toArrayLike(Buffer, 'be', 32) + ]) + ethUtils.secp256k1.signatureNormalize(signature) + }) + }) + + it('normalize return same signature (s equal n/2)', function () { + const signature = Buffer.concat([ + util.BN_ONE.toArrayLike(Buffer, 'be', 32), + util.ec.nh.toArrayLike(Buffer, 'be', 32) + ]) + const result = ethUtils.secp256k1.signatureNormalize(signature) + assert.deepEqual(result, signature) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const message = util.getMessage() + + const sigObj = util.sign(message, privateKey) + const result = ethUtils.secp256k1.signatureNormalize(sigObj.signature) + assert.deepEqual(result, sigObj.signatureLowS) + }) + }) +}) + +describe('signatureExport', function () { + it('signature should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.signatureExport(null) + }) + }) + + it('invalid length', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey).slice(1) + ethUtils.secp256k1.signatureExport(signature) + }) + }) + + it('parse fail (r equal N)', function () { + assert.throws(function () { + const signature = Buffer.concat([ + util.ec.n.toArrayLike(Buffer, 'be', 32), + util.BN_ONE.toArrayLike(Buffer, 'be', 32) + ]) + ethUtils.secp256k1.signatureExport(signature) + }) + }) + +}) + +describe('signatureImport', function () { + it('signature should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.signatureImport(null) + }) + }) + + it('parse fail', function () { + assert.throws(function () { + ethUtils.secp256k1.signatureImport(Buffer.alloc(1)) + }) + }) + + it('parse not bip66 signature', function () { + const signature = Buffer.from('308002204171936738571ff75ec0c56c010f339f1f6d510ba45ad936b0762b1b2162d8020220152670567fa3cc92a5ea1a6ead11741832f8aede5ca176f559e8a46bb858e3f6', 'hex') + assert.throws(function () { + ethUtils.secp256k1.signatureImport(signature) + }) + }) + +}) + +describe('signatureImportLax', function () { + it('signature should be a Buffer', function () { + assert.throws(function () { + ethUtils.secp256k1.signatureImportLax(null) + }) + }) + + it('parse fail', function () { + const buffers = [ + Buffer.alloc(0), + Buffer.alloc(1), + Buffer.from([0x30, 0x7b]), + Buffer.from([0x30, 0x87]), + Buffer.from([0x30, 0x80, 0x02, 0x80]), + Buffer.from([0x30, 0x81, 0x00, 0x02, 0x81]), + Buffer.from([0x30, 0x81, 0x00, 0x02, 0x81, 0x01]), + Buffer.from([0x30, 0x82, 0x00, 0x00, 0x02, 0x01, 0x01]), + Buffer.from([0x30, 0x81, 0x00, 0x02, 0x81, 0x01, 0x00, 0x02, 0x81]), + Buffer.from([0x30, 0x81, 0x00, 0x02, 0x81, 0x01, 0x00, 0x02, 0x81, 0x01]), + Buffer.from([0x30, 0x81, 0x00, 0x02, 0x21, 0x01, 0x00, 0x02, 0x81, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02]), + Buffer.from([0x30, 0x81, 0x00, 0x02, 0x05, 0x01, 0x00, 0x02, 0x21, 0x02, 0x02, 0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ] + + buffers.forEach((buffer) => { + assert.throws(function () { + ethUtils.secp256k1.signatureImportLax(buffer) + }) + }) + }) + + it('parse not bip66 signature', function () { + const signature = Buffer.from('308002204171936738571ff75ec0c56c010f339f1f6d510ba45ad936b0762b1b2162d8020220152670567fa3cc92a5ea1a6ead11741832f8aede5ca176f559e8a46bb858e3f6', 'hex') + assert.doesNotThrow(function () { + ethUtils.secp256k1.signatureImportLax(signature) + }) + }) +}) + +describe('signatureExport/signatureImport', function () { + it('signature should be a Buffer', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey) => { + const message = util.getMessage() + + const signature = util.sign(message, privateKey).signatureLowS + + const der = ethUtils.secp256k1.signatureExport(signature) + assert.deepEqual(ethUtils.secp256k1.signatureImport(der), signature) + assert.deepEqual(ethUtils.secp256k1.signatureImportLax(der), signature) + }) + }) +}) + +describe('ecdh', function () { + it('public key should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = null + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('public key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('invalid public key', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x00 + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('secret key should be a Buffer', function () { + assert.throws(function () { + const privateKey = null + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('secret key invalid length', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey().slice(1) + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('secret key equal zero', function () { + assert.throws(function () { + const privateKey = util.ec.curve.zero.fromRed().toArrayLike(Buffer, 'be', 32) + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('secret key equal N', function () { + assert.throws(function () { + const privateKey = util.ec.n.toArrayLike(Buffer, 'be', 32) + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdh(publicKey, privateKey) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey1, i) => { + const privateKey2 = util.getPrivateKey() + const publicKey1 = util.getPublicKey(privateKey1).compressed + const publicKey2 = util.getPublicKey(privateKey2).compressed + + const shared1 = ethUtils.secp256k1.ecdh(publicKey1, privateKey2) + const shared2 = ethUtils.secp256k1.ecdh(publicKey2, privateKey1) + assert.deepEqual(shared1, shared2) + }) + }) +}) + + +describe('ecdhUnsafe', function () { + it('public key should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = null + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('public key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('invalid public key', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x00 + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('secret key should be a Buffer', function () { + assert.throws(function () { + const privateKey = null + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('secret key invalid length', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey().slice(1) + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('secret key equal zero', function () { + assert.throws(function () { + const privateKey = util.ec.curve.zero.fromRed().toArrayLike(Buffer, 'be', 32) + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('secret key equal N', function () { + assert.throws(function () { + const privateKey = util.ec.n.toArrayLike(Buffer, 'be', 32) + const publicKey = util.getPublicKey(util.getPrivateKey()).compressed + ethUtils.secp256k1.ecdhUnsafe(publicKey, privateKey) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey1, i) => { + const privateKey2 = util.getPrivateKey() + const publicKey1 = util.getPublicKey(privateKey1).compressed + const publicKey2 = util.getPublicKey(privateKey2).compressed + + const shared1 = ethUtils.secp256k1.ecdhUnsafe(publicKey1, privateKey2) + const shared2 = ethUtils.secp256k1.ecdhUnsafe(publicKey2, privateKey1) + assert.deepEqual(shared1, shared2) + }) + }) +}) + +describe('sign', function () { + it('message should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.sign(null, privateKey) + }) + }) + + it('message invalid length', function () { + assert.throws(function () { + const message = util.getMessage().slice(1) + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.sign(message, privateKey) + }) + }) + + it('private key should be a Buffer', function () { + assert.throws(function () { + const message = util.getMessage() + ethUtils.secp256k1.sign(message, null) + }) + }) + + it('private key invalid length', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey().slice(1) + ethUtils.secp256k1.sign(message, privateKey) + }) + }) + + it('private key is invalid', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.ec.n.toArrayLike(Buffer, 'be', 32) + ethUtils.secp256k1.sign(message, privateKey) + }) + }) + + it('options should be an Object', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.sign(message, privateKey, null) + }) + }) + + it('options.data should be a Buffer', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.sign(message, privateKey, { data: null }) + }) + }) + + it('options.data length is invalid', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + const data = getRandomBytes(31) + ethUtils.secp256k1.sign(message, privateKey, { data: data }) + }) + }) + + it('options.noncefn should be a Function', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + ethUtils.secp256k1.sign(message, privateKey, { noncefn: null }) + }) + }) + + it('noncefn return not a Buffer', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + const noncefn = function () { return null } + ethUtils.secp256k1.sign(message, privateKey, { noncefn: noncefn }) + }) + }) + + it('noncefn return Buffer with invalid length', function () { + assert.throws(function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + const noncefn = function () { return getRandomBytes(31) } + ethUtils.secp256k1.sign(message, privateKey, { noncefn: noncefn }) + }) + }) + + it('check options.noncefn arguments', function () { + const message = util.getMessage() + const privateKey = util.getPrivateKey() + const data = getRandomBytes(32) + const noncefn = function (message2, privateKey2, algo, data2, attempt) { + assert.deepEqual(message2, message) + assert.deepEqual(privateKey2, privateKey) + assert.deepEqual(algo, null) + assert.deepEqual(data2, data) + assert.deepEqual(attempt, 0) + return getRandomBytes(32) + } + ethUtils.secp256k1.sign(message, privateKey, { data: data, noncefn: noncefn }) + }) + +}) + +describe('verify', function () { + it('message should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.verify(null, signature, publicKey) + }) + }) + + it('message length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage().slice(1) + const signature = util.getSignature(message, privateKey) + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.verify(message, signature, publicKey) + }) + }) + + it('signature should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.verify(message, null, publicKey) + }) + }) + + it('signature length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey).slice(1) + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.verify(message, signature, publicKey) + }) + }) + + it('signature is invalid (r equal N)', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = Buffer.concat([ + util.ec.n.toArrayLike(Buffer, 'be', 32), + getRandomBytes(32) + ]) + const publicKey = util.getPublicKey(privateKey).compressed + ethUtils.secp256k1.verify(message, signature, publicKey) + }) + }) + + it('public key should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + ethUtils.secp256k1.verify(message, signature, null) + }) + }) + + it('public key length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + const publicKey = util.getPublicKey(privateKey).compressed.slice(1) + ethUtils.secp256k1.verify(message, signature, publicKey) + }) + }) + + it('public key is invalid (version is 0x01)', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + const publicKey = util.getPublicKey(privateKey).compressed + publicKey[0] = 0x01 + ethUtils.secp256k1.verify(message, signature, publicKey) + }) + }) + +}) + +describe('recover', function () { + it('message should be a Buffer', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + ethUtils.secp256k1.recover(null, signature, 0) + }) + }) + + it('message length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage().slice(1) + const signature = util.getSignature(message, privateKey) + ethUtils.secp256k1.recover(message, signature, 0) + }) + }) + + it('signature should be a Buffer', function () { + assert.throws(function () { + const message = util.getMessage() + ethUtils.secp256k1.recover(message, null, 0) + }) + }) + + it('signature length is invalid', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey).slice(1) + ethUtils.secp256k1.recover(message, signature, 0) + }) + }) + + it('signature is invalid (r equal N)', function () { + assert.throws(function () { + const message = util.getMessage() + const signature = Buffer.concat([ + util.ec.n.toArrayLike(Buffer, 'be', 32), + getRandomBytes(32) + ]) + ethUtils.secp256k1.recover(message, signature, 0) + }) + }) + + it('recovery should be a Number', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + ethUtils.secp256k1.recover(message, signature, null) + }) + }) + + it('recovery is invalid (equal 4)', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(privateKey, message) + ethUtils.secp256k1.recover(message, signature, 4) + }) + }) + + it('compressed should be a boolean', function () { + assert.throws(function () { + const privateKey = util.getPrivateKey() + const message = util.getMessage() + const signature = util.getSignature(message, privateKey) + ethUtils.secp256k1.recover(message, signature, 0, null) + }) + }) + + it('random tests', function () { + const privateKeys = util.getPrivateKeys(10) + + privateKeys.forEach((privateKey, i) => { + const message = util.getMessage() + const publicKey = util.getPublicKey(privateKey) + const expected = util.sign(message, privateKey) + + const sigObj = ethUtils.secp256k1.sign(message, privateKey) + assert.deepEqual(sigObj.signature, expected.signatureLowS) + assert.deepEqual(sigObj.recovery, expected.recovery) + + const isValid = ethUtils.secp256k1.verify(message, sigObj.signature, publicKey.compressed) + assert.equal(isValid, true) + + const compressed = ethUtils.secp256k1.recover(message, sigObj.signature, sigObj.recovery, true) + assert.deepEqual(compressed, publicKey.compressed) + + const uncompressed = ethUtils.secp256k1.recover(message, sigObj.signature, sigObj.recovery, false) + assert.deepEqual(uncompressed, publicKey.uncompressed) + }) + }) +}) \ No newline at end of file diff --git a/test/util.js b/test/util.js new file mode 100644 index 00000000..777e3b71 --- /dev/null +++ b/test/util.js @@ -0,0 +1,92 @@ +// This file is imported from secp256k1 v3 +// https://github.com/cryptocoinjs/secp256k1-node/blob/master/LICENSE + +const BN = require('bn.js') +const EC = require('elliptic').ec +const ec = new EC('secp256k1') +const getRandomBytes = require('crypto').randomBytes + +function getPrivateKeys(count) { + const privateKeys = [] + for (let i = 0; i < count; i++) { + privateKeys.push(getRandomBytes(32)) + } + + return privateKeys +} + +function getPrivateKey() { + return getRandomBytes(32) +} + +function getTweak() { + return getRandomBytes(32) +} + +function getMessage () { + return getRandomBytes(32) +} + +function getSignature (message, privateKey) { + return sign(message, privateKey).signatureLowS +} + +function getPublicKeys(privateKeys) { + const publicKeys = [] + privateKeys.forEach((privateKey) => { + const publicKey = ec.keyFromPrivate(privateKey).getPublic() + publicKeys.push({ + compressed: Buffer.from(publicKey.encode(null, true)), + uncompressed: Buffer.from(publicKey.encode(null, false)) + }) + }) + + return publicKeys +} + +function getPublicKey(privateKey) { + const publicKey = ec.keyFromPrivate(privateKey).getPublic() + return { + compressed: Buffer.from(publicKey.encode(null, true)), + uncompressed: Buffer.from(publicKey.encode(null, false)) + } +} + +function sign (message, privateKey) { + const ecSig = ec.sign(message, privateKey, { canonical: false }) + + const signature = Buffer.concat([ + ecSig.r.toArrayLike(Buffer, 'be', 32), + ecSig.s.toArrayLike(Buffer, 'be', 32) + ]) + let recovery = ecSig.recoveryParam + if (ecSig.s.cmp(ec.nh) === 1) { + ecSig.s = ec.n.sub(ecSig.s) + recovery ^= 1 + } + const signatureLowS = Buffer.concat([ + ecSig.r.toArrayLike(Buffer, 'be', 32), + ecSig.s.toArrayLike(Buffer, 'be', 32) + ]) + + return { + signature: signature, + signatureLowS: signatureLowS, + recovery: recovery + } +} + +module.exports = { + ec: ec, + BN_ZERO: new BN(0), + BN_ONE: new BN(1), + + getPrivateKeys: getPrivateKeys, + getPrivateKey: getPrivateKey, + getPublicKey: getPublicKey, + getTweak: getTweak, + getMessage: getMessage, + getSignature: getSignature, + + sign: sign, +}