From 67eb7df2351da48671c8c8894299fe605ec88c66 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Fri, 20 Dec 2019 12:47:10 -0600 Subject: [PATCH] 2019 12 17 sign ext key (#959) * Create ExtSign, use it on ExtPrivateKey * Add documentation on how to Sign things with ExtSign * Add ExtSign.sign for a bip32 path * Make sign(bytes,path) not return a Future, add documentation * Address nadav's code review * Fix mdoc sign.md --- .../org/bitcoins/core/crypto/ExtKeySpec.scala | 34 +++++------ .../bitcoins/core/crypto/ExtSignTest.scala | 27 ++++++++ .../org/bitcoins/core/crypto/SignTest.scala | 2 - .../org/bitcoins/core/crypto/ExtKey.scala | 20 +++++- .../scala/org/bitcoins/core/crypto/Sign.scala | 12 ++++ docs/core/core-intro.md | 33 ---------- docs/core/hd-keys.md | 4 ++ docs/core/sign.md | 61 +++++++++++++++++++ website/sidebars.json | 1 + 9 files changed, 139 insertions(+), 55 deletions(-) create mode 100644 core-test/src/test/scala/org/bitcoins/core/crypto/ExtSignTest.scala create mode 100644 docs/core/sign.md diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeySpec.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeySpec.scala index 3e5b64dd85..2229308025 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeySpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeySpec.scala @@ -3,29 +3,29 @@ package org.bitcoins.core.crypto import org.bitcoins.testkit.core.gen.CryptoGenerators import org.bitcoins.core.number.UInt32 import org.bitcoins.core.util.BitcoinSLogger +import org.bitcoins.testkit.util.BitcoinSUnitTest import org.scalacheck.{Gen, Prop, Properties} import scala.util.Success -class ExtKeySpec extends Properties("ExtKeySpec") { - private val logger = BitcoinSLogger.logger - property("serialization symmetry") = { +class ExtKeySpec extends BitcoinSUnitTest { + private val nonHardened: Gen[UInt32] = + Gen.choose(0L, ((1L << 31) - 1)).map(UInt32(_)) + private val hardened: Gen[UInt32] = + Gen.choose(1L << 31, (1L << 32) - 1).map(UInt32(_)) + + it must "have serialization symmetry" in { Prop.forAll(CryptoGenerators.extKey) { extKey => ExtKey.fromString(extKey.toString) == Success(extKey) && - ExtKey(extKey.bytes) == extKey + ExtKey(extKey.bytes) == extKey } } - private def nonHardened: Gen[UInt32] = - Gen.choose(0L, ((1L << 31) - 1)).map(UInt32(_)) - private def hardened: Gen[UInt32] = - Gen.choose(1L << 31, (1L << 32) - 1).map(UInt32(_)) - - property("derivation identity 1") = { + it must "have derivation identity 1" in { Prop.forAllNoShrink(CryptoGenerators.extPrivateKey, - nonHardened, - nonHardened, - nonHardened) { (m, a, b, c) => + nonHardened, + nonHardened, + nonHardened) { (m, a, b, c) => //https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#the-key-tree //N(m/a/b/c) = N(m/a/b)/c = N(m/a)/b/c = N(m)/a/b/c = M/a/b/c val path1 = m @@ -57,11 +57,11 @@ class ExtKeySpec extends Properties("ExtKeySpec") { } } - property("derivation identity 2") = { + it must "derivation identity 2" in { Prop.forAllNoShrink(CryptoGenerators.extPrivateKey, - hardened, - nonHardened, - nonHardened) { (m, aH, b, c) => + hardened, + nonHardened, + nonHardened) { (m, aH, b, c) => //https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#the-key-tree //N(m/aH/b/c) = N(m/aH/b)/c = N(m/aH)/b/c val path1 = m diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/ExtSignTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/ExtSignTest.scala new file mode 100644 index 0000000000..6e483dcc79 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/ExtSignTest.scala @@ -0,0 +1,27 @@ +package org.bitcoins.core.crypto + +import org.bitcoins.testkit.core.gen.{CryptoGenerators, HDGenerators} +import org.bitcoins.testkit.util.{BitcoinSAsyncTest, BitcoinSUnitTest} + +class ExtSignTest extends BitcoinSAsyncTest { + + it must "be able to sign something that extends ExtSignKey" in { + forAll(CryptoGenerators.extPrivateKey, CryptoGenerators.sha256Digest) { + case (extPrivKey, hash) => + val sig = extPrivKey.sign(hash.bytes) + assert(extPrivKey.publicKey.verify(hash,sig)) + } + } + + it must "be able to sign a specific path of a ext key" in { + forAllAsync(CryptoGenerators.extPrivateKey, CryptoGenerators.sha256Digest, HDGenerators.bip32Path) { + case (extPrivKey, hash, path) => + val sigF = extPrivKey.deriveAndSignFuture(hash.bytes,path) + val childPubKey = extPrivKey.deriveChildPubKey(path).get + sigF.map { sig => + assert(childPubKey.key.verify(hash,sig)) + } + + } + } +} diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/SignTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/SignTest.scala index 0daa2893c2..acc15684c5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/SignTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/SignTest.scala @@ -29,8 +29,6 @@ class SignTest extends BitcoinSUnitTest { val sigF = signTestImpl.signFunction(hash.bytes) sigF.map(sig => assert(pubKey.verify(hash.hex, sig))) - } } - } 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 3649bbbf3e..6090b2ce40 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala @@ -5,10 +5,10 @@ import org.bitcoins.core.hd.{BIP32Node, BIP32Path} import org.bitcoins.core.number.{UInt32, UInt8} import org.bitcoins.core.protocol.NetworkElement import org.bitcoins.core.util._ -import scodec.bits.ByteVector -import scodec.bits.HexStringSyntax +import scodec.bits.{ByteVector, HexStringSyntax} import scala.annotation.tailrec +import scala.concurrent.Future import scala.util.{Failure, Success, Try} /** @@ -157,7 +157,7 @@ object ExtKey extends Factory[ExtKey] { } } -sealed abstract class ExtPrivateKey extends ExtKey { +sealed abstract class ExtPrivateKey extends ExtKey with ExtSign { import ExtKeyVersion._ override protected type VersionType = ExtKeyPrivVersion @@ -210,6 +210,20 @@ sealed abstract class ExtPrivateKey extends ExtKey { def deriveChildPrivKey(idx: Long): Try[ExtPrivateKey] = { Try(UInt32(idx)).map(deriveChildPrivKey) } + + override def publicKey: ECPublicKey = key.publicKey + + override def signFunction: ByteVector => Future[ECDigitalSignature] = { + key.signFunction + } + + /** Signs the given bytes with the given [[BIP32Path path]] */ + override def deriveAndSignFuture: ( + ByteVector, + BIP32Path) => Future[ECDigitalSignature] = { + case (bytes, path) => + deriveChildPrivKey(path).signFunction(bytes) + } } object ExtPrivateKey extends Factory[ExtPrivateKey] { diff --git a/core/src/main/scala/org/bitcoins/core/crypto/Sign.scala b/core/src/main/scala/org/bitcoins/core/crypto/Sign.scala index 675bcc3715..83dce267c2 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/Sign.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/Sign.scala @@ -1,5 +1,6 @@ package org.bitcoins.core.crypto +import org.bitcoins.core.hd.BIP32Path import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -58,3 +59,14 @@ object Sign { }, publicKey) } } + +/** A signing interface for [[ExtKey]] */ +trait ExtSign extends Sign { + + def deriveAndSignFuture: (ByteVector, BIP32Path) => Future[ECDigitalSignature] + + /** First derives the child key that corresponds to [[BIP32Path path]] and then signs */ + def sign(bytes: ByteVector, path: BIP32Path): ECDigitalSignature = { + Await.result(deriveAndSignFuture(bytes, path), 30.seconds) + } +} diff --git a/docs/core/core-intro.md b/docs/core/core-intro.md index 6f9ef19c58..10c12247c7 100644 --- a/docs/core/core-intro.md +++ b/docs/core/core-intro.md @@ -65,39 +65,6 @@ After providing this information, you can generate a validly signed bitcoin tran To see a complete example of this, see [our `TxBuilder` example](txbuilder.md) -### The [`Sign` API](/api/org/bitcoins/core/crypto/Sign) - -This is the API we define to sign things with. It takes in an arbitrary byte vector and returns a `Future[ECDigitalSignature]`. The reason we incorporate `Future`s here is for extensibility of this API. We would like to provide implementations of this API for hardware devices, which need to be asynchrnous since they may require user input. - -From [`core/src/main/scala/org/bitcoins/core/crypto/Sign.scala`](/api/org/bitcoins/core/crypto/Sign): - -```scala mdoc -import scodec.bits._ -import org.bitcoins.core.crypto._ -import scala.concurrent._ -import scala.concurrent.duration._ - -trait Sign { - def signFunction: ByteVector => Future[ECDigitalSignature] - - def signFuture(bytes: ByteVector): Future[ECDigitalSignature] = - signFunction(bytes) - - def sign(bytes: ByteVector): ECDigitalSignature = { - Await.result(signFuture(bytes), 30.seconds) - } - - def publicKey: ECPublicKey -} - -``` - -The `ByteVector` that is input to the `signFunction` should be the hash that is output from [`TransactionSignatureSerializer`](/api/org/bitcoins/core/crypto/TransactionSignatureSerializer)'s `hashForSignature` method. Our in-memory [`ECKey`](/api/org/bitcoins/core/crypto/ECKey) types implement the `Sign` API. - -If you wanted to implement a new `Sign` api for a hardware wallet, you can easily pass it into the `TxBuilder`/`Signer` classes to allow for you to use those devices to sign with Bitcoin-S. - -This API is currently used to sign ordinary transactions with our [`Signer`](/api/org/bitcoins/core/wallet/signer/Signer)s. The `Signer` subtypes (i.e. `P2PKHSigner`) implement the specific functionality needed to produce a valid digital signature for their corresponding script type. - ### Verifying a transaction's script is valid (does not check if UTXO is valid) Transactions are run through the interpreter to check their validity. These are packaged up into an object called `ScriptProgram`, which contains the following: diff --git a/docs/core/hd-keys.md b/docs/core/hd-keys.md index e68e24da6d..26c77c531e 100644 --- a/docs/core/hd-keys.md +++ b/docs/core/hd-keys.md @@ -127,3 +127,7 @@ firstAccountAddress.value // HD path to generate an address at: val nextAddressPath: SegWitHDPath = firstAddressPath.next ``` + +### Signing things with HD keys + +Please see [sign.md](sign.md) for information on how to sign things with HD keys. \ No newline at end of file diff --git a/docs/core/sign.md b/docs/core/sign.md new file mode 100644 index 0000000000..c8aa6ab7d0 --- /dev/null +++ b/docs/core/sign.md @@ -0,0 +1,61 @@ +--- +id: sign +title: Sign api +--- + +### The [`Sign` API](org/bitcoins/core/crypto/Sign.scala) + +This is the API we define to sign things with. It takes in an arbitrary byte vector and returns a `Future[ECDigitalSignature]`. The reason we incorporate `Future`s here is for extensibility of this API. We would like to provide implementations of this API for hardware devices, which need to be asynchrnous since they may require user input. + +From [Sign.scala](../../core/src/main/scala/org/bitcoins/core/crypto/Sign.scala): + +```scala mdoc +import scodec.bits._ +import org.bitcoins.core.crypto._ +import scala.concurrent._ +import scala.concurrent.duration._ + +trait Sign { + def signFunction: ByteVector => Future[ECDigitalSignature] + + def signFuture(bytes: ByteVector): Future[ECDigitalSignature] = + signFunction(bytes) + + def sign(bytes: ByteVector): ECDigitalSignature = { + Await.result(signFuture(bytes), 30.seconds) + } + + def publicKey: ECPublicKey +} + +``` + +The `ByteVector` that is input to the `signFunction` should be the hash that is output from [`TransactionSignatureSerializer`](/api/org/bitcoins/core/crypto/TransactionSignatureSerializer)'s `hashForSignature` method. Our in-memory [`ECKey`](/api/org/bitcoins/core/crypto/ECKey) types implement the `Sign` API. + +If you wanted to implement a new `Sign` api for a hardware wallet, you can easily pass it into the `TxBuilder`/`Signer` classes to allow for you to use those devices to sign with Bitcoin-S. + +This API is currently used to sign ordinary transactions with our [`Signer`](/api/org/bitcoins/core/wallet/signer/Signer)s. The `Signer` subtypes (i.e. `P2PKHSigner`) implement the specific functionality needed to produce a valid digital signature for their corresponding script type. + + +### The [`ExtSign`](../../core/src/main/scala/org/bitcoins/core/crypto/Sign.scala) API. + +An [ExtKey](org/bitcoins/core/crypto/ExtKey.scala) is a data structure that can be used to generate more keys from a parent key. For more information look at [hd-keys.md](hd-keys.md) + +You can sign with `ExtPrivateKey` the same way you could with a normal `ECPrivateKey`. + +```scala mdoc:to-string +import org.bitcoins.core.hd._ +import org.bitcoins.core.crypto._ + +val extPrivKey = ExtPrivateKey(ExtKeyVersion.SegWitMainNetPriv) + +extPrivKey.sign(DoubleSha256Digest.empty.bytes) + +val path = BIP32Path(Vector(BIP32Node(0,false))) + +extPrivKey.sign(DoubleSha256Digest.empty.bytes,path) +``` + +With `ExtSign`, you can use `ExtPrivateKey` to sign transactions inside of `TxBuilder` since `UTXOSpendingInfo` takes in `Sign` as a parameter. + +You can also provide a `path` to use to derive a child `ExtPrivateKey`, and then sign with that child private key \ No newline at end of file diff --git a/website/sidebars.json b/website/sidebars.json index 99aa76db88..57e1248c35 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -5,6 +5,7 @@ "core/core-intro", "core/addresses", "core/hd-keys", + "core/sign", "core/txbuilder" ], "RPC clients": [