With the darkest of all words starts BIP 85, warning us about what awaits us:
Ash deės durbatulûk, ash yek gimbatul, ash htâp thrakatulûk, agh burzum-ishi kriptomul.
We the brave, however, will not be stopped by seeds, keys, paths or drngs. And our reward at the end of everything will be sweet: deterministic derivation of entropy from an extended private key. Never again will we have to backup multitude of seeds. One will suffice. Forever.
The article is part of BIP by BIP series. The complete Haskell code is located in fossil repository that can be viewed online or obtained locally by following these steps. You need to have cabal and GHC installed for it:
fossil clone https://jirijakes.com/code/bip-by-bip
cd bip-by-bip
ls src/BipByBip/Bip85.hs
cabal run bip85-test
Deriving entropy Link to heading
It is not possible to maintain one single (mnemonic) seed backup for all keychains used across various wallets because there are a variety of incompatible standards. Sharing of seeds across multiple wallets is not desirable for security reasons. Physical storage of multiple seeds is difficult depending on the security and redundancy required.
After BIP 32 and BIP 39, we are able to generate a mnemonic code, suitable for backup or even memorizing, and from it an extended private key that can be used for deriving many other private and public keys. However if we have multiple wallets, there is certain danger and inconvenience in handling all their mnemonic codes (seeds).
For each application that requires its own wallet, a unique private key is derived from the BIP32 master root key using a fully hardened derivation path. The resulting private key (k) is then processed with HMAC-SHA512, where the key is “bip-entropy-from-k”, and the message payload is the private key k:
HMAC-SHA512(key="bip-entropy-from-k", msg=k)
. The result produces 512 bits of entropy. Each application SHOULD use up to the required number of bits necessary for their operation truncating the rest.
BIP 85 can help us solve this problem. Using it, we can derive new entropy from an extended private key that can be used to create another BIP 39 mnemonic code. Thanks to this, we can use one wallet to generate one mnemonic code (which we backup) and then, from it, we can generate other mnemonic codes for other wallets. These do not need to be backed up, they can be derived again whenever needed.
-- | Derives new 512 bits of entropy from BIP32 master key and using given
-- derivation path and returns result as ByteString.
deriveEntropy :: (AsDerivationPath p) => XPrv -> p -> Maybe BS.ByteString
deriveEntropy xprv path = BA.convert <$> deriveEntropy' xprv path
-- | Derives new 512 bits of entropy from BIP32 master key and using given
-- derivation path, and returns result as cryptonite's type.
deriveEntropy' :: (AsDerivationPath p) => XPrv -> p -> Maybe (HMAC SHA512)
deriveEntropy' xprv path =
hmacSha512
(BC.pack "bip-entropy-from-k")
. Secp.getSecKey
. privateKey
<$> deriveChild xprv path
Nice, we can reuse deriveChild
from BIP 32!
To derive new entropy, we apply HMAC-SHA512 to a private key of an extended private key that we obtain
by deriving, along a given derivation path (BIP 32), from a master key. The function deriveEntropy
is split into two that
only differ in the type of their results. The library cryptonite, that we use for cryptographic magic,
heavily utilizes types to provide better safety enforced by compiler. For example, in our case the
result of HMAC-SHA512 is a value of type HMAC SHA512
. It is an opaque type in the sense that we cannot
see inside of it. But we can extract bytes and convert them into some other type, for example ByteString
.
This is what we do with BA.convert
.
Generating pseudorandom bits Link to heading
We can now derive 512 bits, or 64 bytes, of entropy that can be used in many different ways. Sometimes, however, 512 bits is not enough. If we wanted to use this derived entropy to generate RSA key pair, we would need more bits than what it can provide.
BIP85-DRNG-SHAKE256 is a deterministic random number generator for cryptographic functions that require deterministic outputs, but where the input to that function requires more than the 64 bytes provided by BIP85’s HMAC output. BIP85-DRNG-SHAKE256 uses BIP85 to seed a SHAKE256 stream (from the SHA-3 standard). The input must be exactly 64 bytes long (from the BIP85 HMAC output).
For that purpose, BIP 85 also specifies a method of deterministic derivation of any number of bits: by applying the hashing function SHAKE256 that can produce output of any length we desire. This method is called BIP85-DRNG (deterministic random number generator).
-- | Generates `len` random bytes, deterministically deriving from BIP32
-- master key and using given derivation path (BIP85-DRNG)
drng :: (AsDerivationPath p) => Natural -> XPrv -> p -> Maybe BS.ByteString
drng len xprv path = case someNatVal (8 * len) of
SomeNat n -> BA.convert <$> drng' n xprv path
-- | Type-level version of `drng`.
drng' ::
(KnownNat n, AsDerivationPath p) =>
proxy n ->
XPrv ->
p ->
Maybe (Digest (SHAKE256 n))
drng' _ xprv path = hash <$> deriveEntropy' xprv path
This Haskell implementation is a little tricky, I admit. The signature of drng
should be clear: we provide a natural
number (length of the result in bytes), an extended private key and a derivation path and we get a ByteString
of that
length.
The actual work is done by helper function drng'
.
The reason for this helper function lies in cryptonite. As I mentioned earlier, the library offers a great degree
of type-safety, for example by encoding length of the result in its type. And that is also the case of
SHAKE256, which needs a literal type parameter determining its length in bits. Therefore the type SHAKE256 64
says
that the value is result of SHAKE256 hashing function with 64-bit long result.
Then if we want to allow users of our drng
function to specify length of its output, we need a way of
converting the runtime value (provided by user) into a type parameter (required by compiler). That is the sole
purpose of the helper function drng'
.
Using someNatVal
we can convert a natural number (living in runtime) into so called proxy, which is kind of a holder of
a type without holding any value of that type. In our case, it holds a type-level representation of the natural
number. This proxy is then passed into drng'
and the type-level representation of the natural number is extracted
from it as n
. That is why the result type of this function is Digest (SHAKE256 n)
. It tells us that it is a
result of a hashing function using algorithm SHAKE256 and having length n
bits. Digest can be converted
to ByteString
by calling BA.convert
.
Applications Link to heading
To bring some standard into derivation paths, BIP 85 specifies their format:
Derivation path uses the format
m/83696968'/{app_no}'/{index}'
where{app_no}
is the path for the application, and{index}
is the index.
For example, for using BIP 85 to derive new mnemonic codes according to BIP 39, the app_no
is 39
and index
has format
{language}'/{words}'/{index}'
where {language}
is code for language of the wordlist (0
for English), {words}
is the
number of mnemonic words and {index}
is the index of the derived mnemonic code.
Using this format, m/83696968'/39'/0'/24'/0'
denotes a derivation path for derivation of the zeroth 24-word mnemonic code
according to BIP 39, using English wordlist. BIP 85 contains several test vectors of various derivation paths,
some of them are also used in a unit test of source code accompanying this article.