diff --git a/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala b/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala index 3a1c4d7260..ca756230da 100644 --- a/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/util/CryptoUtilTest.scala @@ -1,7 +1,7 @@ package org.bitcoins.core.util import org.bitcoins.crypto.CryptoUtil -import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator} import org.bitcoins.testkit.util.BitcoinSUnitTest import scodec.bits._ @@ -93,4 +93,18 @@ class CryptoUtilTest extends BitcoinSUnitTest { } } + it must "compute tagged hashes correctly" in { + forAll(NumberGenerator.bytevector) { bytes => + assert( + CryptoUtil.sha256SchnorrChallenge(bytes) == CryptoUtil + .taggedSha256(bytes, "BIP340/challenge")) + assert( + CryptoUtil.sha256SchnorrNonce(bytes) == CryptoUtil + .taggedSha256(bytes, "BIP340/nonce")) + assert( + CryptoUtil.sha256SchnorrAuxRand(bytes) == CryptoUtil + .taggedSha256(bytes, "BIP340/aux")) + } + } + } diff --git a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala index abef4344b4..bffe3d20e2 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala @@ -6,13 +6,13 @@ import org.bitcoins.core.number.{UInt32, UInt8} import org.bitcoins.core.util._ import org.bitcoins.crypto.{ BaseECKey, - BouncyCastleUtil, CryptoContext, CryptoUtil, ECDigitalSignature, ECPrivateKey, ECPublicKey, Factory, + FieldElement, MaskedToString, NetworkElement } @@ -213,12 +213,14 @@ sealed abstract class ExtPrivateKey //parse256(IL) + kpar (mod n) val tweak = CryptoContext.default match { case CryptoContext.LibSecp256k1 => - NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray) + val tweakByteArr = + NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray) + ByteVector(tweakByteArr) case CryptoContext.BouncyCastle => - val sum = BouncyCastleUtil.addNumbers(key.bytes, il) - sum.toByteArray + val sum = key.fieldElement.add(FieldElement(il)) + sum.bytes } - val childKey = ECPrivateKey(ByteVector(tweak)) + val childKey = ECPrivateKey(tweak) val fp = CryptoUtil.sha256Hash160(key.publicKey.bytes).bytes.take(4) ExtPrivateKey(version, depth + UInt8.one, fp, idx, ChainCode(ir), childKey) } @@ -271,6 +273,20 @@ object ExtPrivateKey extends Factory[ExtPrivateKey] { "Fingerprint must be 4 bytes in size, got: " + fingerprint) } + def freshRootKey(version: ExtKeyPrivVersion): ExtPrivateKey = { + val privKey = ECPrivateKey.freshPrivateKey + val chainCode = ChainCode.fromBytes(ECPrivateKey.freshPrivateKey.bytes) + + ExtPrivateKey( + version, + UInt8.zero, + UInt32.zero.bytes, + UInt32.zero, + chainCode, + privKey + ) + } + /** Takes in a base58 string and tries to convert it to an extended private key */ def fromString(base58: String): Try[ExtPrivateKey] = ExtKey.fromString(base58) match { diff --git a/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala b/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala index d1ac8d9d04..0247287d04 100644 --- a/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/NumberUtil.scala @@ -5,6 +5,7 @@ import java.math.BigInteger import org.bitcoins.core.number._ import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.protocol.blockchain.BlockHeader.TargetDifficultyHelper +import org.bitcoins.crypto.FieldElement import scodec.bits.{BitVector, ByteVector} import scala.math.BigInt @@ -28,6 +29,10 @@ sealed abstract class NumberUtil extends BitcoinSLogger { toUnsignedInt(bytes.toArray) } + def uintToFieldElement(bytes: ByteVector): FieldElement = { + FieldElement(toUnsignedInt(bytes)) + } + /** Converts a sequence of bytes to a **big endian** unsigned integer */ def toUnsignedInt(bytes: Array[Byte]): BigInt = { BigInt(new BigInteger(1, bytes)) diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/BIP340Test.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/BIP340Test.scala new file mode 100644 index 0000000000..307e20ce8b --- /dev/null +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/BIP340Test.scala @@ -0,0 +1,119 @@ +package org.bitcoins.crypto + +import org.bitcoins.testkit.util.BitcoinSUnitTest +import org.scalatest.Assertion +import scodec.bits.ByteVector + +import scala.util.{Failure, Success, Try} + +/** Tests from https://github.com/sipa/bips/blob/bip-taproot/bip-0340/test-vectors.csv */ +class BIP340Test extends BitcoinSUnitTest { + behavior of "Schnorr Signing" + + def testSign( + index: Int, + secKey: ECPrivateKey, + auxRand: ByteVector, + msg: ByteVector, + expectedSig: SchnorrDigitalSignature): Assertion = { + val secpSig = secKey.schnorrSign(msg, auxRand) + val bouncyCastleSig = BouncyCastleUtil.schnorrSign(msg, secKey, auxRand) + assert(secpSig == expectedSig, + s"Test $index failed signing for libsecp256k1") + assert(bouncyCastleSig == expectedSig, + s"Test $index failed signing for Bouncy Castle") + assert(bouncyCastleSig == secpSig) + } + + def testVerify( + index: Int, + pubKey: SchnorrPublicKey, + msg: ByteVector, + sig: SchnorrDigitalSignature, + expectedResult: Boolean, + comment: String): Assertion = { + val secpResult = pubKey.verify(msg, sig) + val bouncyCastleResult = BouncyCastleUtil.schnorrVerify(msg, pubKey, sig) + assert(secpResult == expectedResult, + s"Test $index failed verification for libsecp256k1: $comment") + assert(bouncyCastleResult == expectedResult, + s"Test $index failed verification for Bouncy Castle: $comment") + assert(bouncyCastleResult == secpResult) + } + + def test( + index: Int, + secKeyOpt: Option[String], + pubKey: String, + auxRandOpt: Option[String], + msg: String, + sig: String, + result: Boolean, + comment: String): Assertion = { + val pkT = Try(SchnorrPublicKey(pubKey)) + val msgBytes = ByteVector.fromHex(msg).get + val schnorrSigT = Try(SchnorrDigitalSignature(sig)) + + (pkT, schnorrSigT) match { + case (Success(pk), Success(schnorrSig)) => + (secKeyOpt, auxRandOpt) match { + case (Some(secKeyStr), Some(auxRandStr)) => + val secKey = ECPrivateKey(secKeyStr) + assert(secKey.schnorrPublicKey == pk) + val auxRand = ByteVector.fromHex(auxRandStr).get + testSign(index, secKey, auxRand, msgBytes, schnorrSig) + case _ => () + } + + testVerify(index, pk, msgBytes, schnorrSig, result, comment) + case (Failure(_), _) | + (_, Failure(_)) => // Must be verify only test resulting in false + assert(secKeyOpt.isEmpty) + assert(auxRandOpt.isEmpty) + assert(!result, s"Test $index failed to parse signature: $comment") + } + } + + private def toOpt(str: String): Option[String] = { + if (str.isEmpty) { + None + } else { + Some(str) + } + } + + it must "pass the BIP 340 test-vectors with both secp256k1 bindings and bouncy castle" in { + val bufferedSource = + io.Source.fromURL(getClass.getResource("/bip340-test-vectors.csv")) + try { + val lines = bufferedSource.getLines + val _ = lines.next() + for (line <- bufferedSource.getLines) { + val testVec = line.split(",").map(_.trim) + val index = testVec.head.toInt + val secKeyOpt = toOpt(testVec(1)) + val pubKey = testVec(2) + val auxRandOpt = toOpt(testVec(3)) + val msg = testVec(4) + val sig = testVec(5) + val result = testVec(6).toBoolean + val comment = if (testVec.length > 7) { + testVec(7) + } else { + "" + } + + test(index = index, + secKeyOpt = secKeyOpt, + pubKey = pubKey, + auxRandOpt = auxRandOpt, + msg = msg, + sig = sig, + result = result, + comment = comment) + } + } finally { + bufferedSource.close() + } + } +} diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/BouncyCastleSecp256k1Test.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/BouncyCastleSecp256k1Test.scala index d7127d3844..d4824bdaf1 100644 --- a/crypto-test/src/test/scala/org/bitcoins/crypto/BouncyCastleSecp256k1Test.scala +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/BouncyCastleSecp256k1Test.scala @@ -10,6 +10,9 @@ import scodec.bits.ByteVector class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + behavior of "CryptoLibraries" override def withFixture(test: NoArgTest): Outcome = { @@ -24,16 +27,7 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { it must "add private keys the same" in { forAll(CryptoGenerators.privateKey, CryptoGenerators.privateKey) { case (priv1, priv2) => - val sumWithBouncyCastle = - BouncyCastleUtil.addNumbers(priv1.bytes, priv2.bytes) - val sumWithSecp = NativeSecp256k1.privKeyTweakAdd(priv1.bytes.toArray, - priv2.bytes.toArray) - - val sumKeyWithBouncyCastle = - ECPrivateKey(ByteVector(sumWithBouncyCastle.toByteArray)) - val sumKeyWithSecp = ECPrivateKey(ByteVector(sumWithSecp)) - - assert(sumKeyWithBouncyCastle == sumKeyWithSecp) + assert(priv1.addWithBouncyCastle(priv2) == priv1.addWithSecp(priv2)) } } @@ -51,6 +45,15 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { } } + it must "multiply keys the same" in { + forAll(CryptoGenerators.publicKey, CryptoGenerators.fieldElement) { + case (pubKey, tweak) => + assert( + pubKey.tweakMultiplyWithSecp(tweak) == pubKey + .tweakMultiplyWithBouncyCastle(tweak)) + } + } + it must "validate keys the same" in { val keyOrGarbageGen = Gen.oneOf(CryptoGenerators.publicKey.map(_.bytes), NumberGenerator.bytevector(33)) @@ -102,4 +105,66 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest { .verify(bytes, badSig, context = LibSecp256k1)) } } + + /* + it must "compute schnorr signatures the same" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, bytes, auxRand) => + assert( + privKey.schnorrSign(bytes, auxRand, context = BouncyCastle) == privKey + .schnorrSign(bytes, auxRand, context = LibSecp256k1)) + } + } + + it must "compute schnorr signature for fixed nonce the same" in { + forAll(CryptoGenerators.privateKey, + CryptoGenerators.privateKey, + NumberGenerator.bytevector(32)) { + case (privKey, nonceKey, bytes) => + val sigBC = privKey + .schnorrSignWithNonce(bytes, nonceKey, context = BouncyCastle) + val sigSecP = privKey + .schnorrSignWithNonce(bytes, nonceKey, context = LibSecp256k1) + assert(sigBC.bytes == sigSecP.bytes) + } + } + + it must "validate schnorr signatures the same" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + CryptoGenerators.schnorrDigitalSignature) { + case (privKey, bytes, badSig) => + val sig = privKey.schnorrSign(bytes) + val pubKey = privKey.schnorrPublicKey + assert( + pubKey.verify(bytes, sig, context = BouncyCastle) == pubKey + .verify(bytes, sig, context = LibSecp256k1)) + assert( + pubKey.verify(bytes, badSig, context = BouncyCastle) == pubKey + .verify(bytes, badSig, context = LibSecp256k1)) + } + } + + it must "compute schnorr signature points the same" in { + forAll(CryptoGenerators.schnorrPublicKey, + CryptoGenerators.schnorrNonce, + NumberGenerator.bytevector(32)) { + case (pubKey, nonce, bytes) => + val bouncyCastleSigPoint = + pubKey.computeSigPoint(bytes, + nonce, + compressed = true, + context = BouncyCastle) + + val secpSigPoint = pubKey.computeSigPoint(bytes, + nonce, + compressed = true, + context = LibSecp256k1) + + assert(bouncyCastleSigPoint == secpSigPoint) + } + } + */ } diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/ECPrivateKeyTest.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/ECPrivateKeyTest.scala index 2131e7c095..52bd1e52ce 100644 --- a/crypto-test/src/test/scala/org/bitcoins/crypto/ECPrivateKeyTest.scala +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/ECPrivateKeyTest.scala @@ -80,6 +80,7 @@ class ECPrivateKeyTest extends BitcoinSUnitTest { it must "have serialization symmetry" in { forAll(CryptoGenerators.privateKey) { privKey => assert(ECPrivateKey(privKey.hex) == privKey) + assert(ECPrivateKey.fromFieldElement(privKey.fieldElement) == privKey) } } @@ -104,4 +105,16 @@ class ECPrivateKeyTest extends BitcoinSUnitTest { ECPrivateKey().toString must be("Masked(ECPrivateKeyImpl)") } + it must "successfully negate itself" in { + forAll(CryptoGenerators.nonZeroPrivKey) { privKey => + val negPrivKey = privKey.negate + val pubKey = privKey.publicKey + val negPubKey = negPrivKey.publicKey + assert(pubKey.bytes.tail == negPubKey.bytes.tail) + assert(pubKey.bytes.head != negPubKey.bytes.head) + assert( + privKey.fieldElement.add(negPrivKey.fieldElement) == FieldElement.zero) + } + } + } diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/FieldElementTest.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/FieldElementTest.scala new file mode 100644 index 0000000000..2e7bba55f4 --- /dev/null +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/FieldElementTest.scala @@ -0,0 +1,97 @@ +package org.bitcoins.crypto + +import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class FieldElementTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "FieldElement" + + private val N = CryptoParams.curve.getN + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.fieldElement) { fe => + assert(FieldElement(fe.bytes) == fe) + } + } + + it must "add zero correctly" in { + forAll(CryptoGenerators.fieldElement) { fe => + assert(fe.add(FieldElement.zero) == fe) + assert(FieldElement.zero.add(fe) == fe) + } + } + + it must "add small numbers correctly" in { + forAll(CryptoGenerators.smallFieldElement, + CryptoGenerators.smallFieldElement) { + case (fe1, fe2) => + val feSum = fe1.add(fe2).toBigInteger + val bigIntSum = fe1.toBigInteger.add(fe2.toBigInteger) + + assert(feSum == bigIntSum) + } + } + + it must "add large numbers correctly" in { + forAll(CryptoGenerators.largeFieldElement, + CryptoGenerators.largeFieldElement) { + case (fe1, fe2) => + val feSum = fe1.add(fe2).toBigInteger + val bigIntSum = fe1.toBigInteger.add(fe2.toBigInteger).subtract(N) + + assert(feSum == bigIntSum) + } + } + + it must "subtract numbers correctly" in { + forAll(CryptoGenerators.fieldElement, CryptoGenerators.fieldElement) { + case (fe1, fe2) => + if (fe1.toBigInteger.compareTo(fe2.toBigInteger) > 0) { + val feDiff = fe1.subtract(fe2).toBigInteger + val bigIntDiff = fe1.toBigInteger.subtract(fe2.toBigInteger) + + assert(feDiff == bigIntDiff) + } else { + val feDiff = fe2.subtract(fe1).toBigInteger + val bigIntDiff = fe2.toBigInteger.subtract(fe1.toBigInteger) + + assert(feDiff == bigIntDiff) + } + } + } + + it must "wrap around correctly" in { + assert(FieldElement.nMinusOne.add(FieldElement.one) == FieldElement.zero) + assert( + FieldElement.zero.subtract(FieldElement.one) == FieldElement.nMinusOne) + } + + it must "multiply small numbers correctly" in { + forAll(CryptoGenerators.reallySmallFieldElement, + CryptoGenerators.reallySmallFieldElement) { + case (fe1, fe2) => + val feProduct = fe1.multiply(fe2).toBigInteger + val bigIntProduct = fe1.toBigInteger.multiply(fe2.toBigInteger) + + assert(feProduct == bigIntProduct) + } + } + + it must "negate correctly" in { + forAll(CryptoGenerators.fieldElement) { fe => + val negFe = fe.negate + assert(fe.add(negFe) == FieldElement.zero) + } + } + + it must "invert correctly" in { + forAll(CryptoGenerators.fieldElement) { fe => + val feInv = fe.inverse + assert(fe.multiply(feInv) == FieldElement.one) + } + } +} diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrDigitalSignatureTest.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrDigitalSignatureTest.scala new file mode 100644 index 0000000000..77d5b8cd57 --- /dev/null +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrDigitalSignatureTest.scala @@ -0,0 +1,132 @@ +package org.bitcoins.crypto + +import org.bitcoins.core.util.NumberUtil +import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator} +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class SchnorrDigitalSignatureTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "SchnorrDigitalSignature" + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.schnorrDigitalSignature) { sig => + assert(SchnorrDigitalSignature(sig.bytes) == sig) + } + } + + it must "must create and verify a digital signature" in { + forAll(NumberGenerator.bytevector(32), CryptoGenerators.privateKey) { + case (bytes, privKey) => + val sig = privKey.schnorrSign(bytes) + assert(privKey.publicKey.schnorrVerify(bytes, sig)) + } + } + + it must "must not reuse R values" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, bytes1, bytes2) => + val sig1 = privKey.schnorrSign(bytes1) + val sig2 = privKey.schnorrSign(bytes2) + assert(sig1.bytes != sig2.bytes) + assert(sig1.rx != sig2.rx) + } + } + + it must "generate R values correctly" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, auxRand, bytes) => + val nonce = SchnorrNonce.kFromBipSchnorr(privKey, bytes, auxRand) + + val sig1 = privKey.schnorrSign(bytes, auxRand) + val sig2 = privKey.schnorrSignWithNonce(bytes, nonce) + + assert(sig1 == sig2) + } + } + + it must "correctly compute signature points" in { + forAll(CryptoGenerators.privateKey, NumberGenerator.bytevector(32)) { + case (privKey, data) => + val pubKey = privKey.publicKey + val sig = privKey.schnorrSign(data) + + val sigPoint = pubKey.schnorrComputePoint(data, sig.rx) + assert(sigPoint == sig.sig.toPrivateKey.publicKey) + } + } + + it must "correctly compute signature points for sigs with fixed nonces" in { + forAll(CryptoGenerators.privateKey, + NumberGenerator.bytevector(32), + CryptoGenerators.privateKey) { + case (privKey, data, nonce) => + val pubKey = privKey.publicKey + val sig = privKey.schnorrSignWithNonce(data, nonce) + assert(sig.rx == nonce.schnorrNonce) + + val sigPoint = pubKey.schnorrComputePoint(data, sig.rx) + assert(sigPoint == sig.sig.toPrivateKey.publicKey) + } + } + + /** Schnorr signatures have the property that if two messages are signed with the same key + * and nonce, then they are leaked: + * + * sig1 = nonce + message1*privKey + * sig2 = nonce + message2*privKey + * + * => sig1 - sig2 = (message1 - message2)*privKey + * => privKey = (sig1 - sig2) * inverse(message1 - message2) + */ + it must "leak keys if two messages are signed" in { + forAll(CryptoGenerators.nonZeroPrivKey, + CryptoGenerators.nonZeroPrivKey, + NumberGenerator.bytevector(32), + NumberGenerator.bytevector(32)) { + case (privKey, nonce, message1, message2) => + // This will only work if we sign two different messages + assert(message1 != message2) + + // Sign both messages using the same privKey and nonce + val sig1 = privKey.schnorrSignWithNonce(message1, nonce) + val sig2 = privKey.schnorrSignWithNonce(message2, nonce) + + // s1 = nonce + e1*privKey + val s1 = sig1.sig + // s2 = nonce + e2*privKey + val s2 = sig2.sig + + // When signing a message you actually sign SHA256_challenge(Rx || pubKey || message) + val bytesToHash1 = sig1.rx.bytes ++ privKey.schnorrPublicKey.bytes ++ message1 + val e1Bytes = CryptoUtil.sha256SchnorrChallenge(bytesToHash1).bytes + + val bytesToHash2 = sig2.rx.bytes ++ privKey.schnorrPublicKey.bytes ++ message2 + val e2Bytes = CryptoUtil.sha256SchnorrChallenge(bytesToHash2).bytes + + val e1 = NumberUtil.uintToFieldElement(e1Bytes) + val e2 = NumberUtil.uintToFieldElement(e2Bytes) + + val k = nonce.nonceKey.fieldElement + val x = privKey.schnorrKey.fieldElement + + // Test that we have correctly computed the components + assert(k.add(e1.multiply(x)) == s1) + assert(k.add(e2.multiply(x)) == s2) + + // Note that all of s1, s2, e1, and e2 are public information: + // s1 - s2 = nonce + e1*privKey - (nonce + e2*privKey) = privKey*(e1-e2) + // => privKey = (s1 - s2) * modInverse(e1 - e2) + val privNum = s1.subtract(s2).multInv(e1.subtract(e2)) + + // Assert that we've correctly recovered the private key form public info + assert(privNum == x) + } + } +} diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrNonceTest.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrNonceTest.scala new file mode 100644 index 0000000000..8e8c89e7fa --- /dev/null +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrNonceTest.scala @@ -0,0 +1,39 @@ +package org.bitcoins.crypto + +import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class SchnorrNonceTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "SchnorrNonce" + + it must "fail for incorrect lengths" in { + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a")) + + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a010203")) + } + + it must "fail for invalid x coordinate" in { + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34")) + + assertThrows[IllegalArgumentException]( + SchnorrNonce( + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30")) + } + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.schnorrNonce) { pubKey => + assert(SchnorrNonce(pubKey.bytes) == pubKey) + assert(SchnorrNonce(pubKey.xCoord) == pubKey) + } + } +} diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrPublicKeyTest.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrPublicKeyTest.scala new file mode 100644 index 0000000000..9b507296a5 --- /dev/null +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/SchnorrPublicKeyTest.scala @@ -0,0 +1,39 @@ +package org.bitcoins.crypto + +import org.bitcoins.testkit.core.gen.CryptoGenerators +import org.bitcoins.testkit.util.BitcoinSUnitTest + +class SchnorrPublicKeyTest extends BitcoinSUnitTest { + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + behavior of "SchnorrPublicKey" + + it must "fail for incorrect lengths" in { + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a")) + + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "676f8c22de526e0c0904719847e63bda47b4eceb6986bdbaf8695db362811a010203")) + } + + it must "fail for invalid x coordinate" in { + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34")) + + assertThrows[IllegalArgumentException]( + SchnorrPublicKey( + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30")) + } + + it must "have serialization symmetry" in { + forAll(CryptoGenerators.schnorrPublicKey) { pubKey => + assert(SchnorrPublicKey(pubKey.bytes) == pubKey) + assert(SchnorrPublicKey(pubKey.xCoord) == pubKey) + } + } +} diff --git a/crypto/src/main/scala/org/bitcoins/crypto/BouncyCastleUtil.scala b/crypto/src/main/scala/org/bitcoins/crypto/BouncyCastleUtil.scala index 5d41133151..c34875a567 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/BouncyCastleUtil.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/BouncyCastleUtil.scala @@ -11,23 +11,20 @@ import org.bouncycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator} import org.bouncycastle.math.ec.{ECCurve, ECPoint} import scodec.bits.ByteVector -import scala.util.Try +import scala.util.{Failure, Success, Try} object BouncyCastleUtil { private val curve: ECCurve = CryptoParams.curve.getCurve private val G: ECPoint = CryptoParams.curve.getG - private val N: BigInteger = CryptoParams.curve.getN private def getBigInteger(bytes: ByteVector): BigInteger = { new BigInteger(1, bytes.toArray) } - def addNumbers(num1: ByteVector, num2: ByteVector): BigInteger = { - val bigInteger1 = getBigInteger(num1) - val bigInteger2 = getBigInteger(num2) - - bigInteger1.add(bigInteger2).mod(N) + def pubKeyTweakMul(publicKey: ECPublicKey, tweak: ByteVector): ECPublicKey = { + val point = publicKey.toPoint.multiply(getBigInteger(tweak)) + ECPublicKey.fromPoint(point, publicKey.isCompressed) } def decodePoint(bytes: ByteVector): ECPoint = { @@ -109,4 +106,79 @@ object BouncyCastleUtil { } resultTry.getOrElse(false) } + + def schnorrSign( + dataToSign: ByteVector, + privateKey: ECPrivateKey, + auxRand: ByteVector): SchnorrDigitalSignature = { + val nonceKey = + SchnorrNonce.kFromBipSchnorr(privateKey, dataToSign, auxRand) + + schnorrSignWithNonce(dataToSign, privateKey, nonceKey) + } + + def schnorrSignWithNonce( + dataToSign: ByteVector, + privateKey: ECPrivateKey, + nonceKey: ECPrivateKey): SchnorrDigitalSignature = { + val rx = nonceKey.schnorrNonce + val k = nonceKey.nonceKey.fieldElement + val x = privateKey.schnorrKey.fieldElement + val e = CryptoUtil + .sha256SchnorrChallenge( + rx.bytes ++ privateKey.schnorrPublicKey.bytes ++ dataToSign) + .bytes + + val challenge = x.multiply(FieldElement(e)) + val sig = k.add(challenge) + + SchnorrDigitalSignature(rx, sig) + } + + def schnorrVerify( + data: ByteVector, + schnorrPubKey: SchnorrPublicKey, + signature: SchnorrDigitalSignature): Boolean = { + val rx = signature.rx + val sT = Try(signature.sig.toPrivateKey) + + sT match { + case Success(s) => + val eBytes = CryptoUtil + .sha256SchnorrChallenge(rx.bytes ++ schnorrPubKey.bytes ++ data) + .bytes + + val e = FieldElement(eBytes) + val negE = e.negate + + val sigPoint = s.publicKey + val challengePoint = schnorrPubKey.publicKey.tweakMultiply(negE) + val computedR = challengePoint.add(sigPoint) + val yCoord = computedR.toPoint.getRawYCoord + + yCoord != null && yCoord.sqrt() != null && computedR.schnorrNonce == rx + case Failure(_) => false + } + } + + def schnorrComputeSigPoint( + data: ByteVector, + nonce: SchnorrNonce, + pubKey: SchnorrPublicKey, + compressed: Boolean): ECPublicKey = { + val eBytes = CryptoUtil + .sha256SchnorrChallenge(nonce.bytes ++ pubKey.bytes ++ data) + .bytes + + val e = FieldElement(eBytes) + + val compressedSigPoint = + nonce.publicKey.add(pubKey.publicKey.tweakMultiply(e)) + + if (compressed) { + compressedSigPoint + } else { + compressedSigPoint.decompressed + } + } } diff --git a/crypto/src/main/scala/org/bitcoins/crypto/CryptoBytesUtil.scala b/crypto/src/main/scala/org/bitcoins/crypto/CryptoBytesUtil.scala index 45c1663d28..498b7aeb0b 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/CryptoBytesUtil.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/CryptoBytesUtil.scala @@ -67,6 +67,7 @@ trait CryptoBytesUtil { def flipEndianness(bytes: ByteVector): String = encodeHex(bytes.reverse) private val Z: Char = '0' + /** * Adds the amount padding bytes needed to fix the size of the hex string * for instance, ints are required to be 4 bytes. If the number is just 1 @@ -78,7 +79,7 @@ trait CryptoBytesUtil { var counter = 0 while (counter < paddingNeeded) { builder.append(Z) - counter+=1 + counter += 1 } builder.appendAll(hex) builder.result() diff --git a/crypto/src/main/scala/org/bitcoins/crypto/CryptoUtil.scala b/crypto/src/main/scala/org/bitcoins/crypto/CryptoUtil.scala index 6c26d1926f..51217d2fbf 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/CryptoUtil.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/CryptoUtil.scala @@ -37,6 +37,48 @@ trait CryptoUtil { sha256(bits.toByteVector) } + def taggedSha256(bytes: ByteVector, tag: String): Sha256Digest = { + val tagHash = sha256(ByteVector(tag.getBytes())) + val tagBytes = tagHash.bytes ++ tagHash.bytes + sha256(tagBytes ++ bytes) + } + + // The tag "BIP340/challenge" + private val schnorrChallengeTagBytes = { + ByteVector + .fromValidHex( + "07e00dcd3055c1b36ee93effe4d7f266024cdef4116982ff5dfdc1a97e77062907e00dcd3055c1b36ee93effe4d7f266024cdef4116982ff5dfdc1a97e770629" + ) + } + + def sha256SchnorrChallenge(bytes: ByteVector): Sha256Digest = { + sha256(schnorrChallengeTagBytes ++ bytes) + } + + // The tag "BIP340/nonce" + private val schnorrNonceTagBytes = { + ByteVector + .fromValidHex( + "a2ba14a6b39c1c505260bf3aceb07febde3ab34c35c9259d25bd6972f15e6564a2ba14a6b39c1c505260bf3aceb07febde3ab34c35c9259d25bd6972f15e6564" + ) + } + + def sha256SchnorrNonce(bytes: ByteVector): Sha256Digest = { + sha256(schnorrNonceTagBytes ++ bytes) + } + + // The tag "BIP340/aux" + private val schnorrAuxTagBytes = { + ByteVector + .fromValidHex( + "4b07426ad8630dcdbadf8dee1e94f09ac2df4e7ee2629e5e6b27c8666c8cf31e4b07426ad8630dcdbadf8dee1e94f09ac2df4e7ee2629e5e6b27c8666c8cf31e" + ) + } + + def sha256SchnorrAuxRand(bytes: ByteVector): Sha256Digest = { + sha256(schnorrAuxTagBytes ++ bytes) + } + /** Performs SHA1(bytes). */ def sha1(bytes: ByteVector): Sha1Digest = { val hash = MessageDigest.getInstance("SHA-1").digest(bytes.toArray).toList diff --git a/crypto/src/main/scala/org/bitcoins/crypto/ECDigitalSignature.scala b/crypto/src/main/scala/org/bitcoins/crypto/ECDigitalSignature.scala index 40f36475c1..f678ebd55e 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/ECDigitalSignature.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/ECDigitalSignature.scala @@ -10,6 +10,9 @@ sealed abstract class ECDigitalSignature { require(s.signum == 1 || s.signum == 0, s"s must not be negative, got $s") def hex: String = CryptoBytesUtil.encodeHex(bytes) + def ==(p: ECDigitalSignature): Boolean = this.bytes == p.bytes + def !=(p: ECDigitalSignature): Boolean = !(this == p) + def bytes: ByteVector def isEmpty: Boolean = bytes.isEmpty diff --git a/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala b/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala index f8e31e3d5f..03cee0b968 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/ECKey.scala @@ -71,6 +71,104 @@ sealed abstract class ECPrivateKey implicit ec: ExecutionContext): Future[ECDigitalSignature] = Future(sign(hash)) + def schnorrSign(dataToSign: ByteVector): SchnorrDigitalSignature = { + val auxRand = ECPrivateKey.freshPrivateKey.bytes + schnorrSign(dataToSign, auxRand) + } + + // TODO: match on CryptoContext once secp version is added + def schnorrSign( + dataToSign: ByteVector, + auxRand: ByteVector): SchnorrDigitalSignature = { + schnorrSignWithBouncyCastle(dataToSign, auxRand) + } + + /* + def schnorrSignWithSecp( + dataToSign: ByteVector, + auxRand: ByteVector): SchnorrDigitalSignature = { + val sigBytes = + NativeSecp256k1.schnorrSign(dataToSign.toArray, + bytes.toArray, + auxRand.toArray) + SchnorrDigitalSignature(ByteVector(sigBytes)) + } + */ + + def schnorrSignWithBouncyCastle( + dataToSign: ByteVector, + auxRand: ByteVector): SchnorrDigitalSignature = { + BouncyCastleUtil.schnorrSign(dataToSign, this, auxRand) + } + + // TODO: match on CryptoContext once secp version is added + def schnorrSignWithNonce( + dataToSign: ByteVector, + nonce: ECPrivateKey): SchnorrDigitalSignature = { + schnorrSignWithNonceWithBouncyCastle(dataToSign, nonce) + } + + /* + def schnorrSignWithNonceWithSecp( + dataToSign: ByteVector, + nonce: ECPrivateKey): SchnorrDigitalSignature = { + val sigBytes = + NativeSecp256k1.schnorrSignWithNonce(dataToSign.toArray, + bytes.toArray, + nonce.bytes.toArray) + SchnorrDigitalSignature(ByteVector(sigBytes)) + } + */ + + def schnorrSignWithNonceWithBouncyCastle( + dataToSign: ByteVector, + nonce: ECPrivateKey): SchnorrDigitalSignature = { + BouncyCastleUtil.schnorrSignWithNonce(dataToSign, this, nonce) + } + + def nonceKey: ECPrivateKey = { + if (schnorrNonce.publicKey == publicKey) { + this + } else { + this.negate + } + } + + def schnorrKey: ECPrivateKey = { + if (schnorrPublicKey.publicKey == publicKey) { + this + } else { + this.negate + } + } + + def negate: ECPrivateKey = { + val negPrivKeyNum = CryptoParams.curve.getN + .subtract(new BigInteger(1, bytes.toArray)) + ECPrivateKey(ByteVector(negPrivKeyNum.toByteArray)) + } + + def add(other: ECPrivateKey): ECPrivateKey = { + add(other, CryptoContext.default) + } + + def add(other: ECPrivateKey, context: CryptoContext): ECPrivateKey = { + context match { + case CryptoContext.LibSecp256k1 => addWithSecp(other) + case CryptoContext.BouncyCastle => addWithBouncyCastle(other) + } + } + + def addWithSecp(other: ECPrivateKey): ECPrivateKey = { + val sumBytes = + NativeSecp256k1.privKeyTweakAdd(bytes.toArray, other.bytes.toArray) + ECPrivateKey(ByteVector(sumBytes)) + } + + def addWithBouncyCastle(other: ECPrivateKey): ECPrivateKey = { + fieldElement.add(other.fieldElement).toPrivateKey + } + /** Signifies if the this private key corresponds to a compressed public key */ def isCompressed: Boolean @@ -99,6 +197,16 @@ sealed abstract class ECPrivateKey BouncyCastleUtil.computePublicKey(this) } + def schnorrPublicKey: SchnorrPublicKey = { + SchnorrPublicKey(publicKey.bytes) + } + + def schnorrNonce: SchnorrNonce = { + SchnorrNonce(publicKey.bytes) + } + + def fieldElement: FieldElement = FieldElement(bytes) + override def toStringSensitive: String = s"ECPrivateKey($hex,$isCompressed)" } @@ -148,6 +256,10 @@ object ECPrivateKey extends Factory[ECPrivateKey] { def fromHex(hex: String, isCompressed: Boolean): ECPrivateKey = fromBytes(CryptoBytesUtil.decodeHex(hex), isCompressed) + def fromFieldElement(fieldElement: FieldElement): ECPrivateKey = { + fieldElement.toPrivateKey + } + /** Generates a fresh [[org.bitcoins.crypto.ECPrivateKey ECPrivateKey]] that has not been used before. */ def apply(): ECPrivateKey = ECPrivateKey(true) @@ -223,6 +335,23 @@ sealed abstract class ECPublicKey extends BaseECKey { def verify(hex: String, signature: ECDigitalSignature): Boolean = verify(CryptoBytesUtil.decodeHex(hex), signature) + def schnorrVerify( + data: ByteVector, + signature: SchnorrDigitalSignature): Boolean = { + schnorrPublicKey.verify(data, signature) + } + + def schnorrComputePoint( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean = isCompressed): ECPublicKey = { + schnorrPublicKey.computeSigPoint(data, nonce, compressed) + } + + def schnorrPublicKey: SchnorrPublicKey = SchnorrPublicKey(bytes) + + def schnorrNonce: SchnorrNonce = SchnorrNonce(bytes) + override def toString: String = "ECPublicKey(" + hex + ")" /** Checks if the [[org.bitcoins.crypto.ECPublicKey ECPublicKey]] is compressed */ @@ -275,6 +404,30 @@ sealed abstract class ECPublicKey extends BaseECKey { ECPublicKey.fromPoint(sumPoint) } + + def tweakMultiply(tweak: FieldElement): ECPublicKey = { + tweakMultiply(tweak, CryptoContext.default) + } + + def tweakMultiply( + tweak: FieldElement, + context: CryptoContext): ECPublicKey = { + context match { + case CryptoContext.LibSecp256k1 => tweakMultiplyWithSecp(tweak) + case CryptoContext.BouncyCastle => tweakMultiplyWithBouncyCastle(tweak) + } + } + + def tweakMultiplyWithSecp(tweak: FieldElement): ECPublicKey = { + val mulBytes = NativeSecp256k1.pubKeyTweakMul(bytes.toArray, + tweak.bytes.toArray, + isCompressed) + ECPublicKey(ByteVector(mulBytes)) + } + + def tweakMultiplyWithBouncyCastle(tweak: FieldElement): ECPublicKey = { + BouncyCastleUtil.pubKeyTweakMul(this, tweak.bytes) + } } object ECPublicKey extends Factory[ECPublicKey] { diff --git a/crypto/src/main/scala/org/bitcoins/crypto/FieldElement.scala b/crypto/src/main/scala/org/bitcoins/crypto/FieldElement.scala new file mode 100644 index 0000000000..d397c3ad47 --- /dev/null +++ b/crypto/src/main/scala/org/bitcoins/crypto/FieldElement.scala @@ -0,0 +1,132 @@ +package org.bitcoins.crypto + +import java.math.BigInteger + +import org.bouncycastle.math.ec.ECPoint +import scodec.bits.ByteVector + +import scala.util.Try + +/** + * Represents integers modulo the secp256k1 field size: pow(2,256) - 0x1000003D1. + * + * Supports arithmetic for these elements including +, -, *, and inverses. + * Supports 32 byte serialization as is needed for ECPrivateKeys. + */ +case class FieldElement(bytes: ByteVector) extends NetworkElement { + require(bytes.length == 32, s"Field elements must have 32 bytes, got $bytes") + + private val privKeyT: Try[ECPrivateKey] = Try(ECPrivateKey(bytes)) + + require( + privKeyT.isSuccess || isZero, + s"$bytes is not a valid field element: ${privKeyT.failed.get.getMessage}") + + def isZero: Boolean = bytes.toArray.forall(_ == 0.toByte) + + def toPrivateKey: ECPrivateKey = + if (!isZero) { + privKeyT.get + } else { + throw new RuntimeException("Cannot turn zero into a private key") + } + + def toBigInteger: BigInteger = FieldElement.getBigInteger(bytes) + + def add(other: FieldElement): FieldElement = { + FieldElement.add(this, other) + } + + def subtract(other: FieldElement): FieldElement = { + add(other.negate) + } + + def multiply(other: FieldElement): FieldElement = { + FieldElement.multiply(this, other) + } + + def multInv(other: FieldElement): FieldElement = { + multiply(other.inverse) + } + + def negate: FieldElement = { + FieldElement.negate(this) + } + + def getPoint: ECPoint = FieldElement.computePoint(this) + + def getPublicKey: ECPublicKey = toPrivateKey.publicKey + + def inverse: FieldElement = FieldElement.computeInverse(this) +} + +object FieldElement extends Factory[FieldElement] { + override def fromBytes(bytes: ByteVector): FieldElement = { + if (bytes.length < 32) { + new FieldElement(bytes.padLeft(32)) + } else if (bytes.length == 32) { + new FieldElement(bytes) + } else if (bytes.length == 33 && bytes.head == 0.toByte) { + new FieldElement(bytes.tail) + } else { + throw new IllegalArgumentException( + s"Field element cannot have more than 32 bytes, got $bytes") + } + } + + def apply(num: BigInt): FieldElement = { + FieldElement(num.underlying()) + } + + def apply(num: BigInteger): FieldElement = { + FieldElement.fromByteArray(num.mod(N).toByteArray) + } + + def fromByteArray(bytes: Array[Byte]): FieldElement = { + FieldElement(ByteVector(bytes)) + } + + val zero: FieldElement = FieldElement(ByteVector.empty) + val one: FieldElement = FieldElement(ByteVector.fromByte(1)) + + val nMinusOne: FieldElement = FieldElement( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140") + + private val G: ECPoint = CryptoParams.curve.getG + private val N: BigInteger = CryptoParams.curve.getN + + private def getBigInteger(bytes: ByteVector): BigInteger = { + new BigInteger(1, bytes.toArray) + } + + def add(fe1: FieldElement, fe2: FieldElement): FieldElement = { + val num1 = fe1.toBigInteger + val num2 = fe2.toBigInteger + + val sum = num1.add(num2).mod(N) + FieldElement(sum) + } + + def multiply(fe1: FieldElement, fe2: FieldElement): FieldElement = { + val num1 = fe1.toBigInteger + val num2 = fe2.toBigInteger + + val sum = num1.multiply(num2).mod(N) + FieldElement(sum) + } + + def negate(fe: FieldElement): FieldElement = { + val neg = N.subtract(fe.toBigInteger) + FieldElement(neg) + } + + def computePoint(fe: FieldElement): ECPoint = G.multiply(fe.toBigInteger) + + /** Computes the inverse (mod M) of the input using the Euclidean Algorithm (log time) + * Cribbed from [[https://www.geeksforgeeks.org/multiplicative-inverse-under-modulo-m/]] + */ + def computeInverse(fe: FieldElement): FieldElement = { + val inv = fe.toBigInteger.modInverse(N) + FieldElement(inv) + } +} diff --git a/crypto/src/main/scala/org/bitcoins/crypto/SchnorrDigitalSignature.scala b/crypto/src/main/scala/org/bitcoins/crypto/SchnorrDigitalSignature.scala new file mode 100644 index 0000000000..36757468bb --- /dev/null +++ b/crypto/src/main/scala/org/bitcoins/crypto/SchnorrDigitalSignature.scala @@ -0,0 +1,17 @@ +package org.bitcoins.crypto + +import scodec.bits.ByteVector + +case class SchnorrDigitalSignature(rx: SchnorrNonce, sig: FieldElement) + extends NetworkElement { + override def bytes: ByteVector = rx.bytes ++ sig.bytes +} + +object SchnorrDigitalSignature extends Factory[SchnorrDigitalSignature] { + override def fromBytes(bytes: ByteVector): SchnorrDigitalSignature = { + require(bytes.length == 64, + s"SchnorrDigitalSignature must be exactly 64 bytes, got $bytes") + SchnorrDigitalSignature(SchnorrNonce(bytes.take(32)), + FieldElement(bytes.drop(32))) + } +} diff --git a/crypto/src/main/scala/org/bitcoins/crypto/SchnorrNonce.scala b/crypto/src/main/scala/org/bitcoins/crypto/SchnorrNonce.scala new file mode 100644 index 0000000000..5b9bc037cd --- /dev/null +++ b/crypto/src/main/scala/org/bitcoins/crypto/SchnorrNonce.scala @@ -0,0 +1,89 @@ +package org.bitcoins.crypto + +import scodec.bits.ByteVector + +import scala.annotation.tailrec +import scala.util.Try + +case class SchnorrNonce(bytes: ByteVector) extends NetworkElement { + require(bytes.length == 32, s"Schnorr nonce must be 32 bytes, get $bytes") + + private val evenKey: ECPublicKey = ECPublicKey(s"02$hex") + private val oddKey: ECPublicKey = ECPublicKey(s"03$hex") + + private val yCoordEven: Boolean = { + evenKey.toPoint.getRawYCoord.sqrt() != null + } + + /** Computes the public key associated with a SchnorrNonce as specified in bip-schnorr. + * They y-coordinate is chosen to be a quadratic residue. + */ + val publicKey: ECPublicKey = { + if (yCoordEven) { + evenKey + } else { + oddKey + } + } + + require(Try(publicKey).isSuccess, + s"Schnorr nonce must be a valid x coordinate, got $bytes") + require( + publicKey.toPoint.getRawYCoord.sqrt != null, + "Schnorr nonce must be an x coordinate for which a quadratic residue y coordinate exists") + + def xCoord: FieldElement = { + FieldElement(bytes) + } +} + +object SchnorrNonce extends Factory[SchnorrNonce] { + + @tailrec + def fromBytes(bytes: ByteVector): SchnorrNonce = { + if (bytes.length == 32) { + new SchnorrNonce(bytes) + } else if (bytes.length < 32) { + // means we need to pad the private key with 0 bytes so we have 32 bytes + SchnorrNonce.fromBytes(bytes.padLeft(32)) + } else if (bytes.length == 33) { + // this is for the case when java serialies a BigInteger to 33 bytes to hold the signed num representation + SchnorrNonce.fromBytes(bytes.tail) + } else { + throw new IllegalArgumentException( + "Schnorr nonce cannot be greater than 33 bytes in size, got: " + + CryptoBytesUtil.encodeHex(bytes) + " which is of size: " + bytes.size) + } + } + + def kFromBipSchnorr( + privKey: ECPrivateKey, + message: ByteVector, + auxRand: ByteVector): ECPrivateKey = { + val privKeyForUse = privKey.schnorrKey + + val randHash = CryptoUtil.sha256SchnorrAuxRand(auxRand).bytes + val maskedKey = randHash.xor(privKeyForUse.bytes) + + val nonceHash = CryptoUtil.sha256SchnorrNonce( + maskedKey ++ privKey.schnorrPublicKey.bytes ++ message) + + ECPrivateKey(nonceHash.bytes).nonceKey + } + + /** Computes the bip-schnorr nonce for a given message and private key. + * This is intended to ensure that no two messages are signed with the + * same nonce. + */ + def fromBipSchnorr( + privKey: ECPrivateKey, + message: ByteVector, + auxRand: ByteVector): SchnorrNonce = { + val k = kFromBipSchnorr(privKey, message, auxRand) + k.publicKey.schnorrNonce + } + + def apply(xCoor: FieldElement): SchnorrNonce = { + SchnorrNonce(xCoor.bytes) + } +} diff --git a/crypto/src/main/scala/org/bitcoins/crypto/SchnorrPublicKey.scala b/crypto/src/main/scala/org/bitcoins/crypto/SchnorrPublicKey.scala new file mode 100644 index 0000000000..a1d1e60075 --- /dev/null +++ b/crypto/src/main/scala/org/bitcoins/crypto/SchnorrPublicKey.scala @@ -0,0 +1,114 @@ +package org.bitcoins.crypto + +import org.bitcoin.NativeSecp256k1 +import scodec.bits.ByteVector + +import scala.annotation.tailrec +import scala.util.Try + +case class SchnorrPublicKey(bytes: ByteVector) extends NetworkElement { + require(bytes.length == 32, + s"Schnorr public keys must be 32 bytes, got $bytes") + require(Try(publicKey).isSuccess, + s"Schnorr public key must be a valid x coordinate, got $bytes") + + // TODO: match on CryptoContext once secp version is added + def verify(data: ByteVector, signature: SchnorrDigitalSignature): Boolean = { + verifyWithBouncyCastle(data, signature) + } + + /* + def verifyWithSecp( + data: ByteVector, + signature: SchnorrDigitalSignature): Boolean = { + NativeSecp256k1.schnorrVerify(signature.bytes.toArray, + data.toArray, + bytes.toArray) + } + */ + + def verifyWithBouncyCastle( + data: ByteVector, + signature: SchnorrDigitalSignature): Boolean = { + BouncyCastleUtil.schnorrVerify(data, this, signature) + } + + def computeSigPoint(data: ByteVector, nonce: SchnorrNonce): ECPublicKey = { + computeSigPoint(data, nonce, compressed = true) + } + + // TODO: match on CryptoContext once secp version is added + def computeSigPoint( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean): ECPublicKey = { + computeSigPointWithBouncyCastle(data, nonce, compressed) + } + + /* + def computeSigPointWithSecp( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean = true): ECPublicKey = { + val sigPointBytes = NativeSecp256k1.schnorrComputeSigPoint( + data.toArray, + nonce.bytes.toArray, + bytes.toArray, + compressed) + ECPublicKey(ByteVector(sigPointBytes)) + } + */ + + def computeSigPointWithBouncyCastle( + data: ByteVector, + nonce: SchnorrNonce, + compressed: Boolean = true): ECPublicKey = { + BouncyCastleUtil.schnorrComputeSigPoint(data, nonce, this, compressed) + } + + def publicKey: ECPublicKey = { + val pubKeyBytes = ByteVector.fromByte(2) ++ bytes + + val validPubKey = CryptoContext.default match { + case CryptoContext.LibSecp256k1 => + NativeSecp256k1.isValidPubKey(pubKeyBytes.toArray) + case CryptoContext.BouncyCastle => + BouncyCastleUtil.validatePublicKey(pubKeyBytes) + } + + require( + validPubKey, + s"Cannot construct schnorr public key from invalid x coordinate: $bytes") + + ECPublicKey(pubKeyBytes) + } + + def xCoord: FieldElement = FieldElement(bytes) +} + +object SchnorrPublicKey extends Factory[SchnorrPublicKey] { + + @tailrec + def fromBytes(bytes: ByteVector): SchnorrPublicKey = { + require(bytes.length <= 33, + s"XOnlyPublicKey must be less than 33 bytes, got $bytes") + + if (bytes.length == 32) + new SchnorrPublicKey(bytes) + else if (bytes.length < 32) { + // means we need to pad the private key with 0 bytes so we have 32 bytes + SchnorrPublicKey.fromBytes(bytes.padLeft(32)) + } else if (bytes.length == 33) { + // this is for the case when java serialies a BigInteger to 33 bytes to hold the signed num representation + SchnorrPublicKey.fromBytes(bytes.tail) + } else { + throw new IllegalArgumentException( + "XOnlyPublicKey cannot be greater than 33 bytes in size, got: " + + CryptoBytesUtil.encodeHex(bytes) + " which is of size: " + bytes.size) + } + } + + def apply(xCoor: FieldElement): SchnorrPublicKey = { + SchnorrPublicKey(xCoor.bytes) + } +} diff --git a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala index b2e3f728da..56390192a6 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala @@ -15,6 +15,10 @@ import org.bitcoins.crypto.{ ECDigitalSignature, ECPrivateKey, ECPublicKey, + FieldElement, + SchnorrDigitalSignature, + SchnorrNonce, + SchnorrPublicKey, Sha256Digest, Sha256Hash160Digest } @@ -131,6 +135,36 @@ sealed abstract class CryptoGenerators { def privateKey: Gen[ECPrivateKey] = Gen.delay(ECPrivateKey()) + def fieldElement: Gen[FieldElement] = privateKey.map(_.fieldElement) + + def smallFieldElement: Gen[FieldElement] = + NumberGenerator + .bytevector(30) + .map(bytes => FieldElement(ByteVector.fill(2)(0) ++ bytes)) + + def reallySmallFieldElement: Gen[FieldElement] = + NumberGenerator + .bytevector(15) + .map(bytes => FieldElement(ByteVector.fill(17)(0) ++ bytes)) + + def largeFieldElement: Gen[FieldElement] = + NumberGenerator + .bytevector(30) + .map(bytes => FieldElement(ByteVector.fill(2)(Byte.MinValue) ++ bytes)) + + def nonZeroFieldElement: Gen[FieldElement] = + nonZeroPrivKey.map(_.fieldElement) + + /** Generates a random non-zero private key */ + def nonZeroPrivKey: Gen[ECPrivateKey] = + privateKey.filter(_.bytes.toArray.exists(_ != 0.toByte)) + + def schnorrNonce: Gen[SchnorrNonce] = + nonZeroPrivKey.map(_.publicKey.bytes.tail).map(SchnorrNonce.fromBytes) + + def schnorrPublicKey: Gen[SchnorrPublicKey] = + publicKey.map(_.schnorrPublicKey) + /** * Generate a sequence of private keys * @param num maximum number of keys to generate @@ -187,6 +221,13 @@ sealed abstract class CryptoGenerators { hash <- CryptoGenerators.doubleSha256Digest } yield privKey.sign(hash) + def schnorrDigitalSignature: Gen[SchnorrDigitalSignature] = { + for { + privKey <- privateKey + hash <- CryptoGenerators.doubleSha256Digest + } yield privKey.schnorrSign(hash.bytes) + } + def sha256Digest: Gen[Sha256Digest] = for { bytes <- NumberGenerator.bytevector