Implmenetation of ExtPrivateKey, passing BIP32 test vectors

ExtKey serialization symmetry spec passing

Derivation symmetry for non hardened indices

implement ExtKey.deriveChildPubKey

implementing invariants as they are specified in bip32, adding comments

Moving all ECKeys types into one file so we can create an ADT
This commit is contained in:
Chris Stewart 2017-11-14 15:45:43 -06:00
parent 11f46fc248
commit 4e0766538c
9 changed files with 739 additions and 9 deletions

View File

@ -1,6 +1,6 @@
//test in assembly := {}
testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "3")
//testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, "-verbosity", "3")
coverageExcludedPackages := ".*gen"

View File

@ -0,0 +1,14 @@
package org.bitcoins.core.crypto
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.util.{BitcoinSUtil, Factory}
sealed abstract class ChainCode extends NetworkElement
object ChainCode extends Factory[ChainCode] {
private case class ChainCodeImpl(override val hex: String) extends ChainCode {
require(bytes.size == 32, "ChainCode must be 32 bytes in size, got: " + bytes.size)
}
def fromBytes(bytes: Seq[Byte]): ChainCode = ChainCodeImpl(BitcoinSUtil.encodeHex(bytes))
}

View File

@ -1,12 +1,299 @@
package org.bitcoins.core.crypto
import java.math.BigInteger
import java.security.SecureRandom
import org.bitcoin.NativeSecp256k1
import org.bitcoins.core.config.{MainNet, NetworkParameters, TestNet3}
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.protocol.blockchain.{MainNetChainParams, SecretKey, TestNetChainParams}
import org.bitcoins.core.util.{BitcoinSUtil, _}
import org.spongycastle.crypto.AsymmetricCipherKeyPair
import org.spongycastle.crypto.digests.SHA256Digest
import org.spongycastle.crypto.generators.ECKeyPairGenerator
import org.spongycastle.crypto.params.{ECKeyGenerationParameters, ECPrivateKeyParameters, ECPublicKeyParameters}
import org.spongycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator}
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
/**
* Created by chris on 2/16/16.
*/
trait ECKey {
def privateKey : Option[ECPrivateKey]
def publicKey : ECPublicKey
* Created by chris on 2/16/16.
*/
sealed abstract class BaseECKey extends NetworkElement {
def hex : String = BitcoinSUtil.encodeHex(bytes)
def bytes : Seq[Byte]
/**
* Signs a given sequence of bytes with the signingKey
* @param dataToSign the bytes to be signed
* @param signingKey the key to sign the bytes with
* @return the digital signature
*/
private def sign(dataToSign : Seq[Byte], signingKey : BaseECKey): ECDigitalSignature = {
require(dataToSign.length == 32 && signingKey.bytes.length <= 32)
val signature = NativeSecp256k1.sign(dataToSign.toArray, signingKey.bytes.toArray)
ECDigitalSignature(signature)
}
def sign(hash: DoubleSha256Digest, signingKey: BaseECKey): ECDigitalSignature = sign(hash.bytes,signingKey)
def sign(hash: DoubleSha256Digest): ECDigitalSignature = sign(hash,this)
@deprecated("Deprecated in favor of signing algorithm inside of secp256k1", "2/20/2017")
private def oldSign(dataToSign: Seq[Byte], signingKey: BaseECKey): ECDigitalSignature = {
val signer: ECDSASigner = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()))
val privKey: ECPrivateKeyParameters = new ECPrivateKeyParameters(
new BigInteger(1,signingKey.bytes.toArray), CryptoParams.curve)
signer.init(true, privKey)
val components : Array[BigInteger] = signer.generateSignature(dataToSign.toArray)
val (r,s) = (components(0),components(1))
val signature = ECDigitalSignature(r,s)
//make sure the signature follows BIP62's low-s value
//https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures
//bitcoinj implementation
//https://github.com/bitcoinj/bitcoinj/blob/1e66b9a8e38d9ad425507bf5f34d64c5d3d23bb8/core/src/main/java/org/bitcoinj/core/ECKey.java#L551
val signatureLowS = DERSignatureUtil.lowS(signature)
require(signatureLowS.isDEREncoded, "We must create DER encoded signatures when signing a piece of data, got: " + signatureLowS)
signatureLowS
}
}
case class ECKeyImpl(privateKey : Option[ECPrivateKey], publicKey : ECPublicKey) extends ECKey
/**
* Created by chris on 2/16/16.
*/
sealed abstract class ECPrivateKey extends BaseECKey {
/** Signifies if the this private key corresponds to a compressed public key */
def isCompressed : Boolean
/** Derives the public for a the private key */
def publicKey : ECPublicKey = {
val pubKeyBytes : Seq[Byte] = NativeSecp256k1.computePubkey(bytes.toArray, isCompressed)
require(NativeSecp256k1.isValidPubKey(pubKeyBytes.toArray), "secp256k1 failed to generate a valid public key, got: " + BitcoinSUtil.encodeHex(pubKeyBytes))
ECPublicKey(pubKeyBytes)
}
/**
* Converts a [[ECPrivateKey]] to WIF
* https://en.bitcoin.it/wiki/Wallet_import_format
* @return
*/
def toWIF(network: NetworkParameters): String = {
val networkByte = network.privateKey
//append 1 byte to the end of the priv key byte representation if we need a compressed pub key
val fullBytes = if (isCompressed) networkByte +: (bytes ++ Seq(1.toByte)) else networkByte +: bytes
val hash = CryptoUtil.doubleSHA256(fullBytes)
val checksum = hash.bytes.take(4)
val encodedPrivKey = fullBytes ++ checksum
Base58.encode(encodedPrivKey)
}
override def toString = "ECPrivateKey(" + hex + "," + isCompressed +")"
}
object ECPrivateKey extends Factory[ECPrivateKey] {
private case class ECPrivateKeyImpl(override val bytes : Seq[Byte], isCompressed: Boolean) extends ECPrivateKey {
require(NativeSecp256k1.secKeyVerify(bytes.toArray), "Invalid key according to secp256k1, hex: " + BitcoinSUtil.encodeHex(bytes))
}
override def fromBytes(bytes : Seq[Byte]) : ECPrivateKey = fromBytes(bytes,true)
@tailrec
def fromBytes(bytes: Seq[Byte], isCompressed: Boolean): ECPrivateKey = {
if (bytes.size == 32) ECPrivateKeyImpl(bytes,isCompressed)
else if (bytes.size < 32) {
//means we need to pad the private key with 0 bytes so we have 32 bytes
val paddingNeeded = 32 - bytes.size
val padding = for { _ <- 0 until paddingNeeded} yield 0.toByte
ECPrivateKey.fromBytes(padding ++ bytes, isCompressed)
}
//this is for the case when java serialies a BigInteger to 33 bytes to hold the signed num representation
else if (bytes.size == 33) ECPrivateKey.fromBytes(bytes.slice(1,33), isCompressed)
else throw new IllegalArgumentException("Private keys cannot be greater than 33 bytes in size, got: " +
BitcoinSUtil.encodeHex(bytes) + " which is of size: " + bytes.size)
}
def fromHex(hex: String, isCompressed: Boolean): ECPrivateKey = fromBytes(BitcoinSUtil.decodeHex(hex), isCompressed)
/** Generates a fresh [[ECPrivateKey]] that has not been used before. */
def apply() : ECPrivateKey = ECPrivateKey(true)
def apply(isCompressed: Boolean) = freshPrivateKey(isCompressed)
/** Generates a fresh [[ECPrivateKey]] that has not been used before. */
def freshPrivateKey : ECPrivateKey = freshPrivateKey(true)
def freshPrivateKey(isCompressed: Boolean): ECPrivateKey = {
val secureRandom = new SecureRandom
val generator : ECKeyPairGenerator = new ECKeyPairGenerator
val keyGenParams : ECKeyGenerationParameters = new ECKeyGenerationParameters(CryptoParams.curve, secureRandom)
generator.init(keyGenParams)
val keypair : AsymmetricCipherKeyPair = generator.generateKeyPair
val privParams: ECPrivateKeyParameters = keypair.getPrivate.asInstanceOf[ECPrivateKeyParameters]
val priv : BigInteger = privParams.getD
val bytes = priv.toByteArray
ECPrivateKey.fromBytes(bytes,isCompressed)
}
/**
* Takes in a base58 string and converts it into a private key.
* Private keys starting with 'K', 'L', or 'c' correspond to compressed public keys.
* https://en.bitcoin.it/wiki/Wallet_import_format
*
* @param WIF Wallet Import Format. Encoded in Base58
* @return
*/
def fromWIFToPrivateKey(WIF : String): ECPrivateKey = {
val isCompressed = ECPrivateKey.isCompressed(WIF)
val privateKeyBytes = trimFunction(WIF)
ECPrivateKey.fromBytes(privateKeyBytes,isCompressed)
}
/**
* Takes in WIF private key as a sequence of bytes and determines if it corresponds to a compressed public key.
* If the private key corresponds to a compressed public key, the last byte should be 0x01, and
* the WIF string will have started with K or L instead of 5 (or c instead of 9 on testnet).
*
* @param bytes private key in bytes
* @return
*/
def isCompressed(bytes : Seq[Byte]): Boolean = {
val validCompressedBytes: Seq[Byte] =
MainNetChainParams.base58Prefix(SecretKey) ++ TestNetChainParams.base58Prefixes(SecretKey)
val validCompressedBytesInHex: Seq[String] = validCompressedBytes.map(byte => BitcoinSUtil.encodeHex(byte))
val firstByteHex = BitcoinSUtil.encodeHex(bytes.head)
if (validCompressedBytesInHex.contains(firstByteHex)) bytes(bytes.length - 5) == 0x01.toByte
else false
}
def isCompressed(WIF : String) : Boolean = {
val bytes = Base58.decode(WIF)
isCompressed(bytes)
}
/**
* When decoding a WIF private key, we drop the first byte (network byte), and the last 4 bytes (checksum).
* If the private key corresponds to a compressed public key, we drop the last byte again.
* https://en.bitcoin.it/wiki/Wallet_import_format
* @param WIF Wallet Import Format. Encoded in Base58
* @return
*/
private def trimFunction(WIF : String): Seq[Byte] = {
val bytesChecked = Base58.decodeCheck(WIF)
//see https://en.bitcoin.it/wiki/List_of_address_prefixes
//for where '5' and '9' come from
bytesChecked match {
case Success(bytes) if uncompressedKeyPrefixes.contains(WIF.headOption) => bytes.tail
case Success(bytes) if isCompressed(WIF) => bytes.tail.dropRight(1)
case Success(bytes) => bytes.tail
case Failure(exception) => throw exception
}
}
/** The Base58 prefixes that represent compressed private keys */
def compressedKeyPrefixes = Seq(Some('K'), Some('L'), Some('c'))
/** The Base58 prefixes that represent uncompressed private keys */
def uncompressedKeyPrefixes = Seq(Some('5'),Some('9'))
/** Returns the [[NetworkParameters]] from a serialized WIF key */
def parseNetworkFromWIF(wif: String): Try[NetworkParameters] = {
val decoded = Base58.decodeCheck(wif)
decoded.map { bytes =>
val b = bytes.head
if (b == TestNetChainParams.base58Prefixes(SecretKey).head) {
TestNet3
} else if (b == MainNetChainParams.base58Prefixes(SecretKey).head) {
MainNet
} else throw new IllegalArgumentException("Cannot match wif private key with a network, prefix was: " + BitcoinSUtil.encodeHex(b))
}
}
}
/**
* Created by chris on 2/16/16.
*/
sealed abstract class ECPublicKey extends BaseECKey {
def verify(hash : HashDigest, signature : ECDigitalSignature) : Boolean = verify(hash.bytes, signature)
/** Verifies if a given piece of data is signed by the [[ECPrivateKey]]'s corresponding [[ECPublicKey]]. */
def verify(data : Seq[Byte], signature : ECDigitalSignature): Boolean = {
logger.debug("PubKey for verifying: " + BitcoinSUtil.encodeHex(bytes))
logger.debug("Data to verify: " + BitcoinSUtil.encodeHex(data))
logger.debug("Signature to check against data: " + signature.hex)
val result = NativeSecp256k1.verify(data.toArray, signature.bytes.toArray, bytes.toArray)
if (!result) {
//if signature verification fails with libsecp256k1 we need to use our old
//verification function from spongy castle, this is needed because early blockchain
//transactions can have weird non strict der encoded digital signatures
//bitcoin core implements this functionality here:
//https://github.com/bitcoin/bitcoin/blob/master/src/pubkey.cpp#L16-L165
//TODO: Implement functionality in Bitcoin Core linked above
oldVerify(data,signature)
} else result
}
def verify(hex : String, signature : ECDigitalSignature) : Boolean = verify(BitcoinSUtil.decodeHex(hex),signature)
override def toString = "ECPublicKey(" + hex + ")"
@deprecated("Deprecated in favor of using verify functionality inside of secp256k1", "2/20/2017")
private def oldVerify(data: Seq[Byte], signature: ECDigitalSignature): Boolean = {
/** The elliptic curve used by bitcoin. */
def curve = CryptoParams.curve
/** This represents this public key in the bouncy castle library */
def publicKeyParams = new ECPublicKeyParameters(curve.getCurve.decodePoint(bytes.toArray), curve)
val resultTry = Try {
val signer = new ECDSASigner
signer.init(false, publicKeyParams)
signature match {
case EmptyDigitalSignature => signer.verifySignature(data.toArray, java.math.BigInteger.valueOf(0), java.math.BigInteger.valueOf(0))
case sig: ECDigitalSignature =>
logger.debug("Public key bytes: " + BitcoinSUtil.encodeHex(bytes))
val rBigInteger: BigInteger = new BigInteger(signature.r.toString())
val sBigInteger: BigInteger = new BigInteger(signature.s.toString())
signer.verifySignature(data.toArray, rBigInteger, sBigInteger)
}
}
resultTry.getOrElse(false)
}
/** Checks if the [[ECPublicKey]] is compressed */
def isCompressed: Boolean = bytes.size == 33
/** Checks if the [[ECPublicKey]] is valid according to secp256k1 */
def isFullyValid = ECPublicKey.isFullyValid(bytes)
}
object ECPublicKey extends Factory[ECPublicKey] {
private case class ECPublicKeyImpl(override val bytes : Seq[Byte]) extends ECPublicKey {
//unfortunately we cannot place ANY invariants here
//because of old transactions on the blockchain that have weirdly formatted public keys. Look at example in script_tests.json
//https://github.com/bitcoin/bitcoin/blob/master/src/test/data/script_tests.json#L457
//bitcoin core only checks CPubKey::IsValid()
//this means we can have public keys with only one byte i.e. 0x00 or no bytes.
//Eventually we would like this to be CPubKey::IsFullyValid() but since we are remaining backwards compatible
//we cannot do this. If there ever is a hard fork this would be a good thing to add.
}
override def fromBytes(bytes : Seq[Byte]) : ECPublicKey = ECPublicKeyImpl(bytes)
def apply() = freshPublicKey
/** Generates a fresh [[ECPublicKey]] that has not been used before. */
def freshPublicKey = ECPrivateKey.freshPrivateKey.publicKey
/** Checks if the public key is valid according to secp256k1
* Mimics this function in bitcoin core
* [[https://github.com/bitcoin/bitcoin/blob/27765b6403cece54320374b37afb01a0cfe571c3/src/pubkey.cpp#L207-L212]]
*/
def isFullyValid(bytes: Seq[Byte]): Boolean = Try(NativeSecp256k1.isValidPubKey(bytes.toArray)).isSuccess
}

View File

@ -0,0 +1,225 @@
package org.bitcoins.core.crypto
import java.math.BigInteger
import org.bitcoin.NativeSecp256k1
import org.bitcoins.core.number.{UInt32, UInt8}
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.util._
import scala.util.{Failure, Success, Try}
/** Represents an extended key as defined by BIP32
* [[https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki]]
*/
sealed abstract class ExtKey extends NetworkElement {
require(bytes.size == 78, "ExtKey must be 78 bytes in size, got: " + bytes.size)
/** The network and private/public key identifier for this key */
def version: ExtKeyVersion
/** 0 for master nodes, 1 for level-1 derived keys, .... */
def depth: UInt8
/** The fingerprint of the parent key */
def fingerprint: Seq[Byte]
/** Child number. This is ser32(i) for i in xi = xpar/i, with xi the key being serialized.
* (0x00000000 if master key) */
def childNum: UInt32
/** In order to prevent these from depending solely on the key itself,
* we extend both private and public keys first with an extra 256 bits of entropy.
* This extension, called the chain code,
* is identical for corresponding private and public keys, and consists of 32 bytes. */
def chainCode: ChainCode
/** The key at this path */
def key: BaseECKey
def deriveChildPubKey(idx: UInt32): Try[ExtPublicKey] = this match {
case priv: ExtPrivateKey =>
Success(priv.deriveChildPrivKey(idx).extPublicKey)
case pub: ExtPublicKey => pub.deriveChildPubKey(idx)
}
def deriveChildPubKey(idx: Long): Try[ExtPublicKey] = {
Try(UInt32(idx)).flatMap(deriveChildPubKey(_))
}
override def hex: String = key match {
case priv: ECPrivateKey =>
version.hex + depth.hex + BitcoinSUtil.encodeHex(fingerprint) +
childNum.hex + chainCode.hex + "00" + priv.hex
case pub: ECPublicKey =>
version.hex + depth.hex + BitcoinSUtil.encodeHex(fingerprint) +
childNum.hex + chainCode.hex + pub.hex
}
override def toString: String = {
val checksum = CryptoUtil.doubleSHA256(bytes).bytes.take(4)
val encoded = Base58.encode(bytes ++ checksum)
require(Base58.decodeCheck(encoded).isSuccess)
encoded
}
}
object ExtKey extends Factory[ExtKey] {
val hardenedIdx = UInt32(NumberUtil.pow2(31).toLong)
/** Takes in a base58 string and tries to convert it to an extended key */
def fromString(base58: String): Try[ExtKey] = {
val decoded: Try[Seq[Byte]] = Base58.decodeCheck(base58)
val extKey = decoded.flatMap { bytes =>
require(bytes.size == 78, "Not 78 bytes")
val version: Try[ExtKeyVersion] = ExtKeyVersion(bytes.take(4)) match {
case Some(v) => Success(v)
case None => Failure(new IllegalArgumentException("Invalid version for ExtKey"))
}
val depth = UInt8(bytes.slice(4,5))
val fp = bytes.slice(5,9)
val childNum = UInt32(bytes.slice(9,13))
val chainCode = ChainCode(bytes.slice(13,45))
val key: Try[ExtKey] = version.map {
case x @ (MainNetPub | TestNet3Pub) =>
val pub = ECPublicKey(bytes.slice(45,78))
ExtPublicKey(x,depth,fp,childNum,chainCode,pub)
case x @ (MainNetPriv | TestNet3Priv) =>
require(bytes(45) == 0, "Byte at index 46 must be zero for a ExtPrivateKey, got: " + BitcoinSUtil.encodeHex(bytes(45)))
val priv = ECPrivateKey(bytes.slice(46,78))
ExtPrivateKey(x,depth,fp,childNum,chainCode,priv)
}
key
}
extKey
}
override def fromBytes(bytes: Seq[Byte]): ExtKey = {
val privTry = Try(ExtPrivateKey(bytes))
if (privTry.isSuccess) privTry.get
else {
ExtPublicKey(bytes)
}
}
}
sealed abstract class ExtPrivateKey extends ExtKey {
override def key: ECPrivateKey
def deriveChildPrivKey(idx: UInt32): ExtPrivateKey = {
val data: Seq[Byte] = if (idx >= ExtKey.hardenedIdx) {
//derive hardened key
0.toByte +: ((key.bytes) ++ idx.bytes)
} else {
//derive non hardened key
key.publicKey.bytes ++ idx.bytes
}
val hmac = CryptoUtil.hmac512(chainCode.bytes,data)
val (il,ir) = hmac.splitAt(32)
//should be ECGroup addition
//parse256(IL) + kpar (mod n)
val childKey = ECPrivateKey(NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray))
val fp = CryptoUtil.sha256Hash160(key.publicKey.bytes).bytes.take(4)
ExtPrivateKey(version, depth + UInt8.one, fp, idx,
ChainCode(ir),childKey)
}
def extPublicKey: ExtPublicKey = version match {
case MainNetPriv => ExtPublicKey(MainNetPub,depth,fingerprint,childNum,chainCode,key.publicKey)
case TestNet3Priv => ExtPublicKey(TestNet3Pub,depth,fingerprint,childNum,chainCode,key.publicKey)
case MainNetPub | TestNet3Pub => throw new IllegalArgumentException("Cannot have pubkey version in ExtPrivateKey, got: " + version)
}
def deriveChildPrivKey(idx: Long): Try[ExtPrivateKey] = {
Try(UInt32(idx)).map(deriveChildPrivKey(_))
}
}
object ExtPrivateKey extends Factory[ExtPrivateKey] {
private case class ExtPrivateKeyImpl(version: ExtKeyVersion, depth: UInt8,
fingerprint: Seq[Byte], childNum: UInt32,
chainCode: ChainCode, key: ECPrivateKey) extends ExtPrivateKey {
require(fingerprint.size == 4, "Fingerprint must be 4 bytes in size, got: " + fingerprint)
}
override def fromBytes(bytes: Seq[Byte]): ExtPrivateKey = {
require(bytes.size == 78, "ExtPrivateKey can only be 78 bytes")
val base58 = Base58.encode(bytes ++ CryptoUtil.doubleSHA256(bytes).bytes.take(4))
ExtKey.fromString(base58) match {
case Success(priv: ExtPrivateKey) => priv
case Success(_: ExtPublicKey) => throw new IllegalArgumentException("Cannot create ext public in ExtPrivateKey")
case f: Failure[_] => throw f.exception
}
}
def apply(version: ExtKeyVersion, depth: UInt8,
fingerprint: Seq[Byte], child: UInt32,
chainCode: ChainCode, privateKey: ECPrivateKey): ExtPrivateKey = {
ExtPrivateKeyImpl(version, depth, fingerprint, child, chainCode, privateKey)
}
/** Generates a master private key
* https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#master-key-generation
* */
def apply(version: ExtKeyVersion, seedOpt: Option[Seq[Byte]] = None): ExtPrivateKey = {
val seed = seedOpt match {
case Some(bytes) => bytes
case None => ECPrivateKey().bytes
}
val i = CryptoUtil.hmac512("Bitcoin seed".map(_.toByte), seed)
val (il,ir) = i.splitAt(32)
val masterPrivKey = ECPrivateKey(il)
val fp = UInt32.zero.bytes
ExtPrivateKey(version, UInt8.zero, fp, UInt32.zero,
ChainCode(ir),masterPrivKey)
}
}
sealed abstract class ExtPublicKey extends ExtKey {
override def key: ECPublicKey
final override def deriveChildPubKey(idx: UInt32): Try[ExtPublicKey] = {
if (idx >= ExtKey.hardenedIdx) {
Failure(new IllegalArgumentException("Cannot derive hardened child from extended public key"))
} else {
val data = key.bytes ++ idx.bytes
val hmac = CryptoUtil.hmac512(chainCode.bytes, data)
val (il,ir) = hmac.splitAt(32)
val priv = ECPrivateKey(il)
val tweaked = NativeSecp256k1.pubKeyTweakAdd(key.bytes.toArray,
hmac.toArray,
priv.isCompressed)
val childPubKey = ECPublicKey(tweaked)
val bi = BigInt(new BigInteger(1,priv.bytes.toArray))
//we do not handle this case since it is impossible
//In case parse256(IL) n or Ki is the point at infinity, the resulting key is invalid,
//and one should proceed with the next value for i.
//https://botbot.me/freenode/bitcoin-wizards/2017-11-20/?tz=America/Chicago
val cc = ChainCode(ir)
val fp = CryptoUtil.sha256Hash160(key.bytes).bytes.take(4)
Success(ExtPublicKey(version,depth + UInt8.one, fp,idx,cc,childPubKey))
}
}
}
object ExtPublicKey extends Factory[ExtPublicKey] {
private case class ExtPublicKeyImpl(version: ExtKeyVersion, depth: UInt8,
fingerprint: Seq[Byte], childNum: UInt32,
chainCode: ChainCode, key: ECPublicKey) extends ExtPublicKey
def apply(version: ExtKeyVersion, depth: UInt8,
fingerprint: Seq[Byte], child: UInt32, chainCode: ChainCode, publicKey: ECPublicKey): ExtPublicKey = {
ExtPublicKeyImpl(version, depth, fingerprint, child, chainCode, publicKey)
}
override def fromBytes(bytes: Seq[Byte]): ExtPublicKey = {
require(bytes.size == 78, "ExtPublicKey can only be 78 bytes")
val base58 = Base58.encode(bytes ++ CryptoUtil.doubleSHA256(bytes).bytes.take(4))
ExtKey.fromString(base58) match {
case Success(_: ExtPrivateKey) =>
throw new IllegalArgumentException("Cannot create ext privatkey in ExtPublicKey")
case Success(pub: ExtPublicKey) => pub
case f: Failure[_] => throw f.exception
}
}
}

View File

@ -0,0 +1,31 @@
package org.bitcoins.core.crypto
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.util.BitcoinSUtil
sealed abstract class ExtKeyVersion extends NetworkElement {
def bytes: Seq[Byte]
override def hex = BitcoinSUtil.encodeHex(bytes)
}
case object MainNetPub extends ExtKeyVersion {
override def bytes = Seq(0x04, 0x88, 0xb2, 0x1E).map(_.toByte)
}
case object MainNetPriv extends ExtKeyVersion {
override def bytes = Seq(0x04,0x88,0xAD,0xE4).map(_.toByte)
}
case object TestNet3Pub extends ExtKeyVersion {
override def bytes = Seq(0x04,0x35,0x87,0xCF).map(_.toByte)
}
case object TestNet3Priv extends ExtKeyVersion {
override def bytes = Seq(0x04,0x35,0x83,0x94).map(_.toByte)
}
object ExtKeyVersion {
private val all: Seq[ExtKeyVersion] = Seq(MainNetPriv,MainNetPub,TestNet3Pub, TestNet3Priv)
def apply(bytes: Seq[Byte]): Option[ExtKeyVersion] = all.find(_.bytes == bytes)
}

View File

@ -19,5 +19,5 @@ trait NetworkElement {
/** The byte representation of the NetworkElement */
def bytes : Seq[Byte] = BitcoinSUtil.decodeHex(hex)
def logger: BitcoinSLogger = BitcoinSLogger
def logger = BitcoinSLogger.logger
}

View File

@ -3,7 +3,9 @@ package org.bitcoins.core.util
import java.security.MessageDigest
import org.bitcoins.core.crypto._
import org.spongycastle.crypto.digests.RIPEMD160Digest
import org.spongycastle.crypto.digests.{RIPEMD160Digest, SHA512Digest}
import org.spongycastle.crypto.macs.HMac
import org.spongycastle.crypto.params.KeyParameter
/**
* Created by chris on 1/14/16.
@ -62,6 +64,15 @@ trait CryptoUtil {
}
val emptyDoubleSha256Hash = DoubleSha256Digest("0000000000000000000000000000000000000000000000000000000000000000")
def hmac512(key: Seq[Byte], data: Seq[Byte]): Seq[Byte] = {
val hmac512 = new HMac(new SHA512Digest())
hmac512.init(new KeyParameter(key.toArray))
hmac512.update(data.toArray,0,data.size)
val output = new Array[Byte](64)
hmac512.doFinal(output,0)
output
}
}
object CryptoUtil extends CryptoUtil

View File

@ -0,0 +1,44 @@
package org.bitcoins.core.crypto
import org.bitcoins.core.gen.CryptoGenerators
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.util.BitcoinSLogger
import org.scalacheck.{Gen, Prop, Properties}
import scala.util.Success
class ExtKeySpec extends Properties("ExtKeySpec") {
private val logger = BitcoinSLogger.logger
property("serialization symmetry") = {
Prop.forAll(CryptoGenerators.extKey) { extKey =>
ExtKey.fromString(extKey.toString) == Success(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") = {
Prop.forAllNoShrink(CryptoGenerators.extPrivateKey, 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.deriveChildPrivKey(a).deriveChildPrivKey(b).deriveChildPrivKey(c).extPublicKey
val path2 = m.deriveChildPrivKey(a).deriveChildPrivKey(b).extPublicKey.deriveChildPubKey(c).get
val path3 = m.deriveChildPrivKey(a).extPublicKey.deriveChildPubKey(b).get.deriveChildPubKey(c).get
val path4 = m.extPublicKey.deriveChildPubKey(a).get.deriveChildPubKey(b).get.deriveChildPubKey(c).get
path1 == path2 && path2 == path3 && path3 == path4
}
}
property("derivation identity 2") = {
Prop.forAllNoShrink(CryptoGenerators.extPrivateKey, 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.deriveChildPrivKey(aH).deriveChildPrivKey(b).deriveChildPrivKey(c).extPublicKey
val path2 = m.deriveChildPrivKey(aH).deriveChildPrivKey(b).extPublicKey.deriveChildPubKey(c).get
val path3 = m.deriveChildPrivKey(aH).extPublicKey.deriveChildPubKey(b).get.deriveChildPubKey(c).get
path1 == path2 && path2 == path3
}
}
}

View File

@ -0,0 +1,118 @@
package org.bitcoins.core.crypto
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.util.BitcoinSUtil
import org.scalatest.{FlatSpec, MustMatchers}
class ExtKeyTest extends FlatSpec with MustMatchers {
//https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
"ExtKey" must "pass the test vectors in BIP32" in {
//master key
val seed = BitcoinSUtil.decodeHex("000102030405060708090a0b0c0d0e0f")
val masterPriv = ExtPrivateKey(MainNetPriv, Some(seed))
masterPriv.toString must be ("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi")
//master public key
val masterPub = masterPriv.extPublicKey
masterPub.toString must be ("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8")
//derive child
val hidx = ExtKey.hardenedIdx
val m0h = masterPriv.deriveChildPrivKey(hidx)
m0h.toString must be ("xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7")
val m0hPub = m0h.extPublicKey
m0hPub.toString must be ("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw")
val m0h1 = m0h.deriveChildPrivKey(UInt32.one)
m0h1.toString must be ("xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs")
val m0h1Pub = m0h1.extPublicKey
m0h1Pub.toString must be ("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ")
val m0h12h = m0h1.deriveChildPrivKey(UInt32(2) + ExtKey.hardenedIdx)
m0h12h.toString must be ("xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM")
val m0h12hPub = m0h12h.extPublicKey
m0h12hPub.toString must be ("xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5")
val m0h12h2 = m0h12h.deriveChildPrivKey(UInt32(2))
m0h12h2.toString must be ("xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334")
val m0h12h2Pub = m0h12h2.extPublicKey
m0h12h2Pub.toString must be ("xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV")
val m0h12h21000000000 = m0h12h2.deriveChildPrivKey(UInt32(1000000000))
m0h12h21000000000.toString must be ("xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76")
val m0h12h21000000000Pub = m0h12h21000000000.extPublicKey
m0h12h21000000000Pub.toString must be ("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy")
}
it must "pass test vector 2 in BIP32" in {
val seed = BitcoinSUtil.decodeHex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")
val masterPriv = ExtPrivateKey(MainNetPriv,Some(seed))
masterPriv.toString must be ("xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U")
val masterPub = masterPriv.extPublicKey
masterPub.toString must be ("xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB")
val m0 = masterPriv.deriveChildPrivKey(UInt32.zero)
m0.toString must be ("xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt")
val m0Pub = m0.extPublicKey
m0Pub.toString must be ("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH")
val m02147483647h = m0.deriveChildPrivKey(ExtKey.hardenedIdx + UInt32(2147483647))
m02147483647h.toString must be ("xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9")
val m02147483647hPub = m02147483647h.extPublicKey
m02147483647hPub.toString must be ("xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a")
val m02147483647h1 = m02147483647h.deriveChildPrivKey(UInt32.one)
m02147483647h1.toString must be ("xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef")
val m02147483647h1Pub = m02147483647h1.extPublicKey
m02147483647h1Pub.toString must be ("xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon")
val m02147483647h12147483646h = m02147483647h1.deriveChildPrivKey(ExtKey.hardenedIdx + UInt32(2147483646))
m02147483647h12147483646h.toString must be ("xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc")
val m02147483647h12147483646hPub = m02147483647h12147483646h.extPublicKey
m02147483647h12147483646hPub.toString must be ("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL")
val m02147483647h12147483646h2 = m02147483647h12147483646h.deriveChildPrivKey(UInt32(2))
m02147483647h12147483646h2.toString must be ("xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j")
val m02147483647h12147483646h2Pub = m02147483647h12147483646h2.extPublicKey
m02147483647h12147483646h2Pub.toString must be ("xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt")
}
it must "pass test vector 3 in BIP32" in {
val seed = BitcoinSUtil.decodeHex("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be")
val masterPrivKey = ExtPrivateKey(MainNetPriv,Some(seed))
masterPrivKey.toString must be ("xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6")
val masterPubKey = masterPrivKey.extPublicKey
masterPubKey.toString must be ("xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13")
val m0h = masterPrivKey.deriveChildPrivKey(ExtKey.hardenedIdx)
m0h.toString must be ("xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L")
val m0hPub = m0h.extPublicKey
m0hPub.toString must be ("xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y")
}
it must "have derivation symmetry with (1<<31)-1, last i before hardened keys" in {
//xprv9s21ZrQH143K4QWHDnxmxUbzAQYiDavkg14kQcmZjP2KaSB1PZs5BUsyNGSrWXTzZ9qwyJo5yzvDe3fWybykc8CQPDZMaKupTeVbkfG7osL
//actual priv key 68e5ed2b2c8fc5a6605107d29d074e3d6ccb119c2811007e32f48305176f814c
val str = "xprv9s21ZrQH143K4LCRq4tUZUt3fiTNZr6QTiep3HGzMxtSwfxKAhBmNJJnsmoyWuYZCPC4DNsiVwToHJbxZtq4iEkozBhMzWNTiCH4tzJNjPi"
val masterPriv = ExtKey.fromString(str).get.asInstanceOf[ExtPrivateKey]
val idx = UInt32((1L << 31) - 1)
val path1 = masterPriv.deriveChildPrivKey(idx).extPublicKey.key
val path2 = masterPriv.extPublicKey.deriveChildPubKey(idx).get.key
path1 must be (path2)
}
}