We started working on the topic of key hierarchy a few months ago. The first proposal was this, and while key hierarchy itself is supposed to be app-independent, it was not fully compatible with the applications requirements, which resulted in incorporating deterministic key hierarchy for the demo. However, fully deterministic key hierarchy has multiple downsides and using it for real would be undesirable.
This post describes an intermediate solution that allows to employ deterministic approach temporarily and describes how to upgrade from it as well. I’ll describe the updated hierarchy as a sequence of changes to the current deterministic approach.
1. Authority keys [1]
Deterministic key hierarchy not only influenced how static keys are generated but also explicitly split the identity key from the original key hierarchy into two:
- the wallet-encapsulated key that is used once to initiate key hierarchy
- the identity key that is derived the same way as the other static keys
Unfortunately, the new identity key doesn’t have the originally intended identity key properties. It is primarily used for authenticating transactions and expressing ownership of a resource. Therefore, the first change to the deterministic key hierarchy is to rename the current identity key pair to authority key pair. This key pair is used to authorize transactions via signature and express resource ownership by referencing the authority public key in the value of a resource. [2]
2. Deterministic protection of random static keys
In the deterministic approach to key generation all static keys were generated from a fixed seed (a signature over a fixed message). In the current proposal, we generate the static keys randomly, like in the original proposal. In order to avoid users having to back up their keys, the static keys are stored by a Heliax-provided service. To protect the keys, the deterministically generated keys are used. To summarise:
- static keys are generated randomly
- static keys are stored locally and by a Heliax-operated service in an encrypted form [3]
- the key encryption keys are derived deterministically
Why is it better?
This approach allows to migrate from deterministic keys in the future by changing the encryption keys and deleting the old encrypted keys from the Heliax machine. The user have to trust that the keys will be deleted.
The data stored on-chain will be protected with randomly generated keys, as in the original proposal.
How to upgrade from determinism
When we are ready to get rid of the deterministic key generation, the following has to be done:
- the old encrypted keys have to be deleted from the local storage and remote Heliax-controlled server. This step is extremely important
- if the storage service is still used, new key encryption keys are generated non-deterministically, the rest is done as before (the static keys are encrypted and stored locally and on the server as before). The key encryption has to be backed up by the user
- if the storage service is not used, the static keys are stored locally. If to encrypt the static keys or not for local storage depends on where the key encryption key would be stored. It makes no sense to encrypt the keys and keep the key encryption key next to ciphertext
- Since the static keys are generated randomly, rotating them works as usual and can be done independently of moving away from determinism
3. How to generate keys deterministically
We want to generate a key encryption key from a signature, deterministically. The signature is produced by signing a fixed message with a wallet-encapsulated key. The signature must be treated as a secret. If it leaks, if the user is somehow persuaded to sign the string with their wallet, the key encryption key is leaked. Note that we cannot rely on any of the fixed parameters or the mechanism being unknown.
The key encryption key is a symmetric key used to encrypt the static keys. The ciphertext will not be exposed publicly, it will be stored locally and on the Heliax server.
The simplest way to generate such a key from the signature would be to apply HKDF:
kek = HKDF(ikm, info)
- ikm is input key material, in our case - a signature over a fixed message
- info is a fixed string that gives some information about the purpose and domain of the key. Since we are not planning to build a deterministic key empire and hopefully only use this mechanism in one case, it seems not necessary, but explicitly adding information about the domain won’t hurt.
Should we use salt?
kek = HKDF(signature, salt)
Salt would allow to derive different keys from the same signature, but the salt either have to be stored by the user (what we are trying to avoid with determinism), or can be public. If it is public, it doesn’t add extra protection, since the attacker that has access to the ciphertext and knows the signature will be able to compute the key encryption key.
The proposed key hierarchy
| Name | Derivation | Description | Lifetime |
|---|---|---|---|
| Identity key pair (idsk, idpk) | - | A wallet encapsulated key pair. Used to produce the signature used for deterministic key generation. We assume not having explicit access to the key, only to the signing functionality. | Forever |
| Authority key pair (ask, apk) | ask \xleftarrow{R} \mathbb{F}_p, apk = [ask] * G | Used to authorise actions and express ownership over resources for applications that have a notion of ownership and require explicit authorisation of the owner | Periodically rotated in the identity lifetime |
| Nullifier key pair (nk, cnk) | nk \xleftarrow{R} \mathbb{F}_p, cnk = PRF(nk) | These keys are used to reflect the right to nullify | Periodically rotated in the identity lifetime |
| Static encryption key pair (sesk, sepk) | sesk \xleftarrow{R} \mathbb{F}_p, sepk = [sesk] * G | This static key pair is used to produce resource encryption keys | Periodically rotated for forward secrecy |
| Static discovery key pair (sdsk, sdpk) | sdsk \xleftarrow{R} \mathbb{F}_p, sdpk = [sdsk] * G | This static key pair is used to produce discovery encryption keys | Periodically rotated for forward secrecy |
| Key encryption key kek | kek = HKDF(ikm, info), where ikm = Sign(idsk, fixed\_string) | A symmetric key used to encrypt the static keys stored locally and on the Heliax server | For as long as deterministic mechanism is required. Should be deprecated asap |
| Ephemeral encryption key pair (eesk, eepk) | eesk \xleftarrow{R} \mathbb{F}_p, eepk = [eesk]*P | Ephemeral encryption key pair generated by the sender. Used to derive the resource encryption key | Transaction |
| rek | rek = KDF(DH(sepk, eesk), eepk) = KDF(DH(sesk, eepk), eepk) | Resource symmetric encryption key. Used to encrypt the transmitted resource object | Transaction |
| Ephemeral discovery key pair (edsk, edpk) | edsk \xleftarrow{R} \mathbb{F}_p, edpk = [edsk]*P | Ephemeral discovery key pair generated by the sender. Used to derive the discovery encryption key | Transaction |
| Discovery encryption key dek | dek = KDF(DH(sdpk, edsk), edpk) = KDF(DH(sdsk, edpk), edpk) | Discovery symmetric encryption key. Used to encrypt the discovery message | Transaction |
What is not explicitly covered with this post
- how does it all work in the context of native identity keys
- how to develop the authority key mechanism to better serve the application needs
- the non-deterministic key hierarchy of the future
I use authority key instead of authorization key because this key is not only used for authorization but also to express ownership, which is a related but slightly distinct function. ↩︎
in the future we have to think how to add more properties for these keys, including some sort of blinding and hierarchy for optional domain separation ↩︎
the keys can also be backed up if the user wants (for recovery), but this is optional. If not used, the keys are recovered by sending a request to the Heliax server that stores the encrypted keys ↩︎
