mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-19 05:43:51 +01:00
Schnorr Data Structures (#1564)
* Pulled down crypto things from dlc branch including: Schnorr data structures, Schnorr Bouncy Castle implementation, FieldElement, and tests for these things * Call fromValidHex in CryptoUtil
This commit is contained in:
parent
001d1c9d7a
commit
baf49b7452
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
119
crypto-test/src/test/scala/org/bitcoins/crypto/BIP340Test.scala
Normal file
119
crypto-test/src/test/scala/org/bitcoins/crypto/BIP340Test.scala
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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] {
|
||||
|
132
crypto/src/main/scala/org/bitcoins/crypto/FieldElement.scala
Normal file
132
crypto/src/main/scala/org/bitcoins/crypto/FieldElement.scala
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
89
crypto/src/main/scala/org/bitcoins/crypto/SchnorrNonce.scala
Normal file
89
crypto/src/main/scala/org/bitcoins/crypto/SchnorrNonce.scala
Normal file
@ -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)
|
||||
}
|
||||
}
|
114
crypto/src/main/scala/org/bitcoins/crypto/SchnorrPublicKey.scala
Normal file
114
crypto/src/main/scala/org/bitcoins/crypto/SchnorrPublicKey.scala
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user