Deepkey is a happ to provide a decentralized public key infrastructure (DPKI) for keys associated with Holochain conductors and applications. Similar to centralised services like Keybase, we want users to be able to manage their "keyset" by adding and removing devices and public/private keypairs.
The keys for happs installed on each device are also tracked under the keyset for the device.
Because humans are notoriously bad at managing cryptographic keys, we believe a project like Holochain must provide key management tools to help people deal with real-world messiness such as lost/stolen keys or devices. How many billions of dollars have been lost due to the lack of a key management infrastructure?
Deepkey provides the ability to:
- Register keys under the authority of a keyset.
- Replace keys with new ones.
- Revoke keys / declare them dead.
- Associate multiple devices under unified keyset management.
- Check the validity of a key.
- Store private instructions to rebuild app keys from a master seed to reestablish authority after data loss.
- Deepkey provides the ability to do social management of keys through m of n signatures (the initial default is a 1 of 1 signature using a revocation key).
Deepkey is a foundational app for all other Holochain app keys. Therefore, it is the first happ every conductor must install, and all other happs rely on it to query the status of keys.
The most common call to Deepkey is key_state((Key, Timestamp))
to query the validity of a key at a particular time.
The Deepkey JoiningProof
involves two proofs. One is the membrane proof which is true for all happs, and the other is the keyset proof described in the next section.
The purpose of a membrane_proof
is to make it hard to flood the network with fake accounts.
TODO: The membrane logic is not currently implemented.
Future membrane logic implementations planned:
ProofOfWork
: Agent must prove that they've performed some computational work to prevent low-effort spam bots.ProofOfStake
: Agent must put up value that can be taken in case of bad behaviour.ProofOfAuthority
: Agent must have a signature from a pre-defined authority to join.
There are a few external details to resolve before we require membrane proofs:
- Ability to have different versions of Deepkey apps to choose from, and configure their own joining proof.
- Ability for hosts to call external functions before joining the network, e.g. to generate a proof of work before completing installation of the app.
- More thought is needed about the types of membrane proofs we might like for the default behavior of Deepkey.
The keyset_proof
is the proof that the device has authority to participate in some keyset. The keyset defines the rules which determine the methods for revoking or replacing its keys.
The first entry the app makes in each user's source chain is a KeysetRoot
, creating a new keyset space.
A source chain may later reference a valid DeviceInvite
, in the form of a DeviceInviteAcceptance
, to abandon the initial keyset and join another already existing keyset space.
(This will be at least the fifth entry in the chain, after the three genesis entries and the init_complete
.)
If the keyset proof is a new KeysetRoot
then it must be immediately followed by a valid ChangeRule
to define how key management works within this keyset. On the other hand, if you join an existing keyset through a DeviceInvite
, the ChangeRule
of that keyset is what governs keys made on this chain.
A keyset is the set of keys governed by a ruleset, presumably under the control of one person.
When you install a new app in Holochain, by default a new keypair is generated to control the source chain of that app. The public key of that keypair serves as the address of your agent in that app's DHT. The private key signs all of your network communications and all actions on your chain. Deepkey registers, manages, and reports on the validity of each AgentPubKey
installed on your conductor.
A KeysetRoot
(KSR) is self-declared onto the network using a single-purpose throwaway keypair.
The structure of a KeysetRoot
is:
- The
first_deepkey_agent
(FDA), the author of theKeysetRoot
. - The
root_pub_key
, the public part of a throwaway keypair which is only used to generate this KSR. (Using thesign_ephemeral
HDK function.) - A
Signature
: the authority of the FDA is established using the private part of the throwaway keypair to sign the FDA's pubkey.
Note that if a device is to issue a KSR it must do so as its very first action in Deepkey.
A device can NEVER create another KSR without starting a new source chain.
Create: The validation that happens when you create a new KeysetRoot
- A
KeysetRoot
struct must deserialize cleanly from the record being validated. - Must be created at index 4 (5th item) in the author's chain.
- The author must be the FDA.
- The signature of the FDA from the root/ephemeral pubkey must be valid.
Read: There is currently no read functions or lookups exposed as zome calls, but this may change in the future as people may want to use KeysetRoot
as a unifying identity.
Update: Not allowed.
Delete: Not allowed.
Zome Calls:
create_keyset_root
- Input is a
(KeysetRoot, ChangeRule)
tuple. - Creates both the
KeysetRoot
andChangeRule
records sequentially. - Output is a
(ActionHash, ActionHash)
tuple of the created records.
- Input is a
Under each KeysetRoot
is an arbitrary tree of DeviceInvite
and DeviceInviteAcceptance
pairs as entries. A DeviceInviteAcceptance
brings a device under the management of the KeysetRoot
that it references. This has the effect of saying, "the same entity that created the keyset also controls this device."
Each device can only be managed by a single keyset at one time.
Accepting an invite moves ownership to a new entity, and removes the device along with its keys from the previous keyset. This is why both the invitor and invitee need to commit corresponding entries to their chains. The invitation contain cryptographic signatures of the process of transferring ownership.
The structure of a DeviceInvite
(written to the invitor's chain) is:
- KSR: An
ActionHash
referring to the invitor's KSR. - Parent: An
ActionHash
referring to the invitor's direct parent in the keyset tree, which is either its KSR or its currentDeviceInviteAcceptance
. This is used to establish the chain of authority from the original KSR. - Invitee: The
AgentPubKey
being invited.
The structure of a DeviceInviteAcceptance
(written to the invitee's chain) is:
- The
ActionHash
of the KSR. - The
ActionHash
of theDeviceInvite
.
Create: The validation that happens when you create a new DeviceInvite
:
- A
DeviceInvite
must deserialize cleanly from the validating record. - The KSR must be fetched and deserialized into a
KeysetRoot
. - An invitee must have a different
AgentPubkey
than the invitor. - If the author of the invitation is the FDA in the invitation's KSR
- Do a hash-bounded query from the invite hash back to the KSR in the invitor's source chain.
- Check that that range contains no invite acceptances (have abandoned the Keyset they are inviting a new device into).
- Else (author of invitation and FDA of KSR are not the same):
- Search from invite backwards (must_get_agent_activity of the invitor), find the first
DeviceInviteAcceptance
in their chain. - The invite in that
DeviceInviteAcceptance
must fetch and deserialize to aDeviceInvite
. - That deserialized
DeviceInvite
must have the same KSR authority as the newDeviceInvite
currently being validated. - Also in that
DeviceInvite
, the invitee must be the author of the newDeviceInvite
.
- Search from invite backwards (must_get_agent_activity of the invitor), find the first
We do not check whether the invitee exists on the DHT yet because they likely don't, that's why we're inviting them. If the DeviceInviteAcceptance
is valid, and the DeviceInvite
is valid, we trust that the parent's DeviceInviteAcceptance
was properly validated, which ensures chain of authority to the KSR.
Read: No direct read or lookup functions exposed in zome calls. The keyset tree structure is used internally for validation of key registration/revocation logic.
Update: Not allowed.
Delete: Not allowed.
Zome Calls:
invite_agent
- Input is the
AgentPubKey
to invite.- This agent does not exist on the DHT yet if they are planning to use the invite as their joining proof.
- Output is the exact
DeviceInviteAcceptance
the invitee must commit to their chain. - Invites are always under the current keyset.
- Input is the
Create:
- A
DeviceInviteAcceptance
must deserialize cleanly from the validating record - A
DeviceInvite
must be fetched and deserialize from theinvite
action hash on theDeviceInviteAcceptance
- The author of the
DeviceInviteAcceptance
must be the referencedAgentPubKey
on theDeviceInvite
- The
KeysetRoot
must be the same on both theDeviceInvite
and theDeviceInviteAcceptance
Read: No exposed zome calls for read or lookup. For validation, the most recent DeviceInviteAcceptance
is used to determine the current keyset.
Update: Not allowed.
Delete: Not allowed.
accept_invite
- input is a
DeviceInviteAcceptance
- output is the
ActionHash
of the entry created - creates the entry as-is from input
- input is a
A ChangeRule
defines the rules within a keyset for changing keys. It is used to validate replacement or revocation of any key. It can be configured to support social signing through m of n signatures of trusted agents, but by default it is configured as a 1 of 1 signature by a revocation key.
The structures involved in a ChangeRule
are:
AuthoritySpec
describes the authority/ies involved in replacing or revoking a key. Number of signatures required, and the public keys that are allowed to sign for the authorization.
pub struct AuthoritySpec {
/// set to 1 for a single signer scenario
pub sigs_required: u8,
/// These signers probably do NOT exist on the DHT.
/// E.g. a revocation key used to create the first change rule.
pub authorized_signers: Vec<AgentPubKey>,
}
AuthorizedSpecChange
exists to make a change to the authorization rules. It includes the new AuthoritySpec
, and a set of authorizing signatures, valid according to the existing spec that this spec change replaces.
pub struct AuthorizedSpecChange {
pub new_spec: AuthoritySpec,
/// Signature of the content of the authority_spec field,
/// signed by throwaway RootKey on Create,
/// or according to previous authoritative AuthSpec upon Update.
pub authorization_of_new_spec: Vec<Authorization>, // required sigs
}
Authorization
is a tuple containing a u8 index into authorized_signers
, and a valid signature from that key.
pub type Authorization = (u8, Signature);
A ChangeRule
ties the new authorization spec to a KeysetRoot. It includes a proof of authority in the form of the keyset_leaf
.
pub struct ChangeRule {
pub keyset_root: ActionHash, // reference to a `KeysetRoot`
pub keyset_leaf: ActionHash, // reference to either the `KeysetRoot` or a `DeviceInviteAcceptance` that proves the authority to change the rules for this Keyset
pub spec_change: AuthorizedSpecChange, // defining the new multisig rules
}
A ChangeRule
can only be created immediately following a KeysetRoot
entry on a source chain. ChangeRule
records can be updated on the chain of any device currently under the authority of the same KeysetRoot
.
Note that the spec change signature validation does NOT require that all the signers exist as agents in Deepkey. This means hardware wallets, FIDO-compliant keys, smart cards, etc. could be used to provide signatures into your multisig.
The create for a ChangeRule
is expected to be signed by a "1 of 1" revocation key that is not the key of a Deepkey agent.
Create: The validation that happens when you create a new ChangeRule
- A
ChangeRule
must deserialize cleanly from the record being validated. - A
KeysetRoot
must fetch and deserialize cleanly from the keyset root on theChangeRule
. - The keyset leaf must be in the author's chain.
- There must NOT be any newer
DeviceInviteAcceptance
records in the validation package. - The
KeysetRoot
FDA must be the author of theChangeRule
. - The
ChangeRule
prev_action
must be theKeysetRoot
record. - The
ChangeRule
authorization of the new spec must have exactly one authorization signature. - The
ChangeRule
authorization signature must be valid as being from theKeysetRoot
root (throwaway) pubkey. - The
ChangeRule
spec_change
specifies an 1 of 1 signing rule. - In the
ChangeRule
spec,sigs_required
= 1.
Read: There are no exposed zome function for looking up ChangeRule
s.
Update: The validation that happens when you update a ChangeRule
- A
ChangeRule
must deserialize cleanly from the record being validated. - A
KeysetRoot
must fetch and deserialize cleanly from the keyset root on theChangeRule
- The previous
ChangeRule
must fetch and deserialize cleanly from theoriginal_action_address
of the update record. - The previous
ChangeRule
record must be the original create record. (Every newChangeRule
updates the originalChangeRule
record; that update action is referenced from the originalChangeRule
, the fifth entry on the source chain.) - The keyset leaf must be in the the author's chain.
- There must NOT be any newer
DeviceInviteAcceptance
records in the validation package. - The
KeysetRoot
of the proposedChangeRule
must be the same as in the previousChangeRule
- The proposed
ChangeRule
authorization must authorize the new spec according to the rules of the previousChangeRule
- The
ChangeRule
spec_change
specifies an m of n signing rule where n >= m. - In the
ChangeRule
spec,sigs_required
>= 1.
Delete: Not allowed.
new_change_rule
:- The inputs are the
ActionHash
of the old change rule, and the newChangeRule
. - Updates the original
ChangeRule
entry (Create only happens when creating aKeysetRoot
with the original throwaway key.) - Output is the
ActionHash
of the new change rule.
- The inputs are the
Agents can register public keys on their source chain in Deepkey.
In the design space of all possible deepkey implementations, any type of public key could be registered and managed, e.g. for blockchains, TLS certificates, etc.
In the default Deepkey instance (the one that needs to be installed into a conductor so that other happs can register their keys in a shared space), only Holochain AgentPubKey
public keys are supported for registration.
The KeyRegistration
entry is the start of the process to manage a public key. There are other entry types that track and control how key registrations can work, i.e. which rules apply, which authority keys are registered under.
The KeyAnchor
entry contains only the core 32 bytes of the registered key, stripped of the 3 byte multihash prefix and 4 byte DHT location suffix. Using this KeyAnchor
entry, the status (valid, revoked, replaced, etc.) of a key can be looked up in a single get_details
call, without needing to first lookup the corresponding KeyRegistration
.
By default, Deepkey change rules support multisignature logic. This is through collecting multiple signatures and applying Holochain validation, not via a cryptographic threshold signature scheme. The ChangeRule
defines the multisig rules that apply to all keys under the management of a KeysetRoot
.
Key replacements or revocations must be fully authorized by a ChangeRule
multisig.
A Generator
is a special purpose key required for registering new keys. Holochain's default key store, "Lair" allows for layers of password encryption of keys. We introduced the concept of a Generator
in order to make registering a new key require typing an additional password to unlock the Generator
private key. This makes it so someone can't register a new key on your chain if they're sitting at your workstation with Holochain unlocked.
The structures comprising a Generator
are:
pub struct Generator {
change_rule: ActionHash, // `ChangeRule` action that authorizes this `Generator`
change: Change,
}
pub struct Change {
new_key: AgentPubKey, // A new special-purpose key being authorized as a `Generator`
authorization: Vec<Authorization>, // authorizes the `new_key` according to the `ChangeRule` rules
}
Create: The validation that happens when you create a new Generator
- A
Generator
must deserialize cleanly from the record. - The
change_rule
must fetch and deserialize cleanly from the referencedActionHash
. - The
new_key
must be authorized by the authorization vec in theChange
according to theChangeRule
rules.
Read: There is no read or lookup zome call exposed for Generator
Update: Not allowed
Delete: Not allowed
new_generator
- input is a
Generator
- output is a
ActionHash
- creates a
Generator
- input is a
The structure of a KeyGeneration
is:
pub struct KeyGeneration {
new_key: AgentPubKey, // New key associated with current chain and KSR
new_key_signing_of_author: Signature, // The author of this chain must sign the new key
// Ensure the generator has the same author as the KeyRegistration.
generator: ActionHash, // This is the key authorized to generate new keys on this chain
generator_signature: Signature, // The generator key signing the new key
}
- A
Generator
must fetch and deserialize cleanly for theKeyGeneration
generator - The
Generator
author must be the same as theKeyGeneration
author - The
Signature
of thenew_key
signing in the author of theKeyGeneration
must be valid - The
Signature
of thenew_key
from theAgentPubKey
of theGenerator
must be valid
A KeyRegistration
enum supports 4 ops/variants:
pub enum KeyRegistration {
Create(KeyGeneration), // Creates a key under management of current KSR on this chain
CreateOnly(KeyGeneration), // Keys for hosted web users may be of this type, cannot replace/revoke
Update(KeyRevocation, KeyGeneration), // revokes a key and replaces it with a newly generated one
Delete(KeyRevocation) // permanently revokes a key (Note: still uses an update action.)
}
KeyGeneration
and KeyRevocation
always use the same validation logic regardless of which KeyRegistration
variant they are included in.
The Delete
variant for a KeyRegistration
uses the update action type for a Record. This is because delete actions don't register their change on the entry hash (just the action hash).
CRUD operations for a KeyRegistration
must always be performed in the correct sequence with the corresponding CRUD operations for a KeyAnchor
. Validation will enforce that the KeyAnchor
is always preceded by its KeyRegistration
.
Note: CreateOnly
serves the temporary purpose of allowing Holo Hosts to register keys of web users without being able to manage those keys. This feature will most likely be replaced with adding a claim key for web users to claim their unmanaged keys if/when they become a self-hosted Holochain user. TODO
Create: The validation that happens when you create a new KeyRegistration
- A
KeyRegistration
must deserialize cleanly from the record - The
KeyRegistration
must be aCreate
orCreateOnly
- The
KeyGeneration
must be valid
Read: No zome calls exposed for direct lookups. The status of a key is read by getting the KeyAnchor
for a KeyRegistration
.
Update: The validation that happens when you update a KeyRegistration
- A
KeyRegistration
must deserialize cleanly from the record - The
KeyRegistration
must be anUpdate
orDelete
- The prior key registration from the
KeyRevocation
must fetch and deserialize to aKeyRegistration
- The prior
KeyRegistration
must be aCreate
orUpdate
- The prior
Generator
must fetch and deserialize cleanly from the priorKeyRegistration
- The prior
ChangeRule
must fetch and deserialize cleanly from the priorGenerator
- The proposed
KeyRevocation
must validate according to the priorChangeRule
- If the
KeyRegistration
is anUpdate
then theKeyGeneration
must be valid
Delete:
- A
KeyRegistration
must fetch and deserialize cleanly from theDelete
action'sdeletes_address
- The
KeyRegistration
must be aKeyRegistration::Delete
variant - The proposed
KeyRevocation
must validate according to the priorChangeRule
new_key_registration
:- input is a
KeyRegistration
- IF the
KeyRegistration
isCreate
orCreateOnly
- creates the
KeyRegistration
record - creates the
KeyAnchor
record
- creates the
- IF the
KeyRegistration
isUpdate
- updates the prior key registration to the new key registration
- looks up the revoked
KeyAnchor
and updates it to the newKeyAnchor
- IF the
KeyRegistration
isDelete
- updates the prior key registration to the new key registration
- deletes that update
- looks up the revoked
KeyAnchor
and deletes it
- input is a
The structure of a KeyRevocation
is:
- The
prior_key_registration
being revoked - The
revocation_authorization
as aVec<Authorization>
- The
KeyRevocation
record must be anUpdate
- The
original_action_address
of theUpdate
action must be theprior_key_registration
of theKeyRevocation
- The prior change rule from the prior generator (see above) must
authorize
the priorKeyRegistration
with theKeyRevocation
authorization vec
The KeyAnchor
entry contains only the core 32 bytes of the registered key, stripped of the 3 byte multihash prefix and 4 byte DHT location suffix. Using this KeyAnchor
entry, the status (valid, revoked, replaced, etc.) of a key can be looked up in a single get
call, without needing to first lookup the corresponding KeyRegistration
.
This also means that any external consumer of Deepkey (other DNA's, Holochain apps, etc.) can query the key status with the core 32 bytes of the key. They do NOT need to know its registration details.
The KeyAnchor
record must always be written onto the chain immediately following its KeyRegistration
. This is to avoid the need to manage links between the two entries, the KeyAnchor
prev_action
always references the KeyRegistration
.
Create: The validation that happens when you create a KeyAnchor
- A
KeyAnchor
must deserialize cleanly from the record - A
KeyRegistration
must fetch and deserialize cleanly from theKeyAnchor
prev action - The
KeyRegistration
must be aCreate
orCreateOnly
op - The
KeyGeneration
new key's raw 32 bytes must be theKeyAnchor
bytes
Read:KeyAnchor
entries are designed to be looked up by hashing the core 32 bytes of a key. The key_state
zome call returns relevant details about the status of key (valid, replaced, revoked, etc.)
Update: The validation that happens when you update a KeyAnchor
- A
KeyAnchor
must deserialize cleanly from the record - A
KeyRegistration
must fetch and deserialize cleanly from theKeyAnchor
prev action - The
KeyRegistration
must be anUpdate
op - The
KeyGeneration
new key's raw 32 bytes must be the content of theKeyAnchor
entry - A
KeyAnchor
must fetch and deserialize cleanly from theoriginal_action_address
of the newKeyAnchor
update action - A
KeyRegistration
must fetch and deserialize cleanly from the prior key registration - The revoked
KeyRegistration
must be aCreate
orUpdate
- The new key of the
KeyGeneration
of the revokedKeyRegistration
must match the revokedKeyAnchor
bytes
Delete:
- Must be able to fetch an
Record
from theKeyAnchor
deletion record - A
KeyAnchor
must fetch and deserialize from thedeletes_address
of the deletion record - A
KeyRegistration
must fetch and deserialize from thedeletes_address
of the previous record - The
KeyRegistration
must be anUpdate
of typeKeyRegistration::Delete
- The
KeyRevocation
from theKeyRegistration
must be revoking the deletedKeyAnchor
key_state
:- input is
(KeyAnchor, Timestamp)
tuple Timestamp
doesn't do anything yet- output is
KeyState
which isValid/Invalidated/NotFound
asSignedActionHashed
- If nothing found,
KeyState::NotFound
is returned - If any updates found, first update is returned in
KeyState::Invalidated
- If any deletes found, first delete is returned in
KeyState::Invalidated
- If any actions found, first action is returned in
KeyState::Valid
- If nothing found,
- input is
In addition to shared/public keysets and registrations, each agent can keep private data for personal records about registered keys. This private data can be used to rebuild keypairs for apps from a master seed.
KeyMeta
records record the derivation path and index used to generate a previously registered key.
DnaBinding
records track how registered keys are being used by happs.
The structure of KeyMeta
is:
new_key
referencing aKeyRegistration
by itsActionHash
derivation_path
as 32 bytes encoding a derivation path for generating the registered keyderivation_index
as a u32 representing the index for generating the registered key from thederivation_path
key_type
as an enum ofAppUI
,AppSig
,AppEncryption
,TLS
TODO: confirm compatibility with Lair Key API
Create:
- A
KeyMeta
must deserialize cleanly from theRecord
- The
new_key
must fetch and deserialize to aKeyRegistration
record - The author of the
KeyMeta
and theKeyRegistration
must be the same
Read: TODO
Update: Not allowed
Delete: Not allowed
new_key_meta
- input is
key_meta
- output is
ActionHash
of the createdKeyMeta
- creates a
KeyMeta
- input is
A DnaBinding
is:
- A
key_meta
asActionHash
referencing aKeyMeta
- A
dna_hash
of the DNA the key is bound to - An
app_name
as strings ofbundle_name
andcell_nick
TODO: make names compatible with new naming
Create: A DnaBinding
must deserialize cleanly from the Record
Read: TODO
Update: Not allowed
Delete: Not allowed
new_dna_binding
- input is
DnaBinding
- output is
ActionHash
of the createdDnaBinding
- creates a
DnaBinding
- input is
install_an_app
- TODO
TODO: Discuss Rate Limiting