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. It is designed to work hand in hand with holochain's secure keystore, Lair.
The most common call to Deepkey is key_state((Key, Timestamp)) to query the validity of a key at a particular time.
Lair is designed to generate new keys from randomness, or generate new keys from a seed with derivation instructions. In order for DeepKey to accomplish it's purpose of being able to regenerate your app keys from a device seed or a master seed for all of your devices, DeepKey must store the derivation patterns and instruct Lair to produce keys using them.
- When you install holochain, DeepKey needs to inject seeds (master, revocation, device) and provide a UI for password encrypting and exporting your seeds for off device storage.
- Every time a new holochain app is installed, DeepKey must specify the derivation to generate the new agent keys in Lair, and store in a private entry the derivation and app DNA it was used with.
- Replacing compromised keys
- Deleting or revoking keys which have been abaonadoned
- generating an invitation for a new device to join your keyspace
- accepting an invitation for a new device
- changing the rules for managing your keys
- approve a key change or deletion, whether doing that locally with a revocation key or receiving that remotely as a social signing request
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_ephemeralHDK 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
KeysetRootstruct 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
KeysetRootandChangeRulerecords 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
ActionHashreferring to the invitor's KSR. - Parent: An
ActionHashreferring 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
AgentPubKeybeing invited.
The structure of a DeviceInviteAcceptance (written to the invitee's chain) is:
- The
ActionHashof the KSR. - The
ActionHashof theDeviceInvite.
Create: The validation that happens when you create a new DeviceInvite:
- A
DeviceInvitemust deserialize cleanly from the validating record. - The KSR must be fetched and deserialized into a
KeysetRoot. - An invitee must have a different
AgentPubkeythan 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
DeviceInviteAcceptancein their chain. - The invite in that
DeviceInviteAcceptancemust fetch and deserialize to aDeviceInvite. - That deserialized
DeviceInvitemust have the same KSR authority as the newDeviceInvitecurrently 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
AgentPubKeyto 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
DeviceInviteAcceptancethe invitee must commit to their chain. - Invites are always under the current keyset.
- Input is the
Create:
- A
DeviceInviteAcceptancemust deserialize cleanly from the validating record - A
DeviceInvitemust be fetched and deserialize from theinviteaction hash on theDeviceInviteAcceptance - The author of the
DeviceInviteAcceptancemust be the referencedAgentPubKeyon theDeviceInvite - The
KeysetRootmust be the same on both theDeviceInviteand 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
ActionHashof 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
ChangeRulemust deserialize cleanly from the record being validated. - A
KeysetRootmust 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
DeviceInviteAcceptancerecords in the validation package. - The
KeysetRootFDA must be the author of theChangeRule. - The
ChangeRuleprev_actionmust be theKeysetRootrecord. - The
ChangeRuleauthorization of the new spec must have exactly one authorization signature. - The
ChangeRuleauthorization signature must be valid as being from theKeysetRootroot (throwaway) pubkey. - The
ChangeRulespec_changespecifies an 1 of 1 signing rule. - In the
ChangeRulespec,sigs_required= 1.
Read: There are no exposed zome function for looking up ChangeRules.
Update: The validation that happens when you update a ChangeRule
- A
ChangeRulemust deserialize cleanly from the record being validated. - A
KeysetRootmust fetch and deserialize cleanly from the keyset root on theChangeRule - The previous
ChangeRulemust fetch and deserialize cleanly from theoriginal_action_addressof the update record. - The previous
ChangeRulerecord must be the original create record. (Every newChangeRuleupdates the originalChangeRulerecord; 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
DeviceInviteAcceptancerecords in the validation package. - The
KeysetRootof the proposedChangeRulemust be the same as in the previousChangeRule - The proposed
ChangeRuleauthorization must authorize the new spec according to the rules of the previousChangeRule - The
ChangeRulespec_changespecifies an m of n signing rule where n >= m. - In the
ChangeRulespec,sigs_required>= 1.
Delete: Not allowed.
new_change_rule:- The inputs are the
ActionHashof the old change rule, and the newChangeRule. - Updates the original
ChangeRuleentry (Create only happens when creating aKeysetRootwith the original throwaway key.) - Output is the
ActionHashof 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
Generatormust deserialize cleanly from the record. - The
change_rulemust fetch and deserialize cleanly from the referencedActionHash. - The
new_keymust be authorized by the authorization vec in theChangeaccording to theChangeRulerules.
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
Generatormust fetch and deserialize cleanly for theKeyGenerationgenerator - The
Generatorauthor must be the same as theKeyGenerationauthor - The
Signatureof thenew_keysigning in the author of theKeyGenerationmust be valid - The
Signatureof thenew_keyfrom theAgentPubKeyof theGeneratormust 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
KeyRegistrationmust deserialize cleanly from the record - The
KeyRegistrationmust be aCreateorCreateOnly - The
KeyGenerationmust 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
KeyRegistrationmust deserialize cleanly from the record - The
KeyRegistrationmust be anUpdateorDelete - The prior key registration from the
KeyRevocationmust fetch and deserialize to aKeyRegistration - The prior
KeyRegistrationmust be aCreateorUpdate - The prior
Generatormust fetch and deserialize cleanly from the priorKeyRegistration - The prior
ChangeRulemust fetch and deserialize cleanly from the priorGenerator - The proposed
KeyRevocationmust validate according to the priorChangeRule - If the
KeyRegistrationis anUpdatethen theKeyGenerationmust be valid
Delete:
- A
KeyRegistrationmust fetch and deserialize cleanly from theDeleteaction'sdeletes_address - The
KeyRegistrationmust be aKeyRegistration::Deletevariant - The proposed
KeyRevocationmust validate according to the priorChangeRule
new_key_registration:- input is a
KeyRegistration - IF the
KeyRegistrationisCreateorCreateOnly- creates the
KeyRegistrationrecord - creates the
KeyAnchorrecord
- creates the
- IF the
KeyRegistrationisUpdate- updates the prior key registration to the new key registration
- looks up the revoked
KeyAnchorand updates it to the newKeyAnchor
- IF the
KeyRegistrationisDelete- updates the prior key registration to the new key registration
- deletes that update
- looks up the revoked
KeyAnchorand deletes it
- input is a
The structure of a KeyRevocation is:
pub struct KeyRevocation {
prior_key_registration: ActionHash,
// To be validated according to the change rule of the generator of the prior key.
revocation_authorization: Vec<Authorization>,
}- The
KeyRevocationrecord must be anUpdate - The
original_action_addressof theUpdateaction must be theprior_key_registrationof theKeyRevocation - The prior change rule from the prior generator (see above) must
authorizethe priorKeyRegistrationwith theKeyRevocationauthorization 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
KeyAnchormust deserialize cleanly from the record - A
KeyRegistrationmust fetch and deserialize cleanly from theKeyAnchorprev action - The
KeyRegistrationmust be aCreateorCreateOnlyop - The
KeyGenerationnew key's raw 32 bytes must be theKeyAnchorbytes
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
KeyAnchormust deserialize cleanly from the record - A
KeyRegistrationmust fetch and deserialize cleanly from theKeyAnchorprev action - The
KeyRegistrationmust be anUpdateop - The
KeyGenerationnew key's raw 32 bytes must be the content of theKeyAnchorentry - A
KeyAnchormust fetch and deserialize cleanly from theoriginal_action_addressof the newKeyAnchorupdate action - A
KeyRegistrationmust fetch and deserialize cleanly from the prior key registration - The revoked
KeyRegistrationmust be aCreateorUpdate - The new key of the
KeyGenerationof the revokedKeyRegistrationmust match the revokedKeyAnchorbytes
Delete:
- Must be able to fetch an
Recordfrom theKeyAnchordeletion record - A
KeyAnchormust fetch and deserialize from thedeletes_addressof the deletion record - A
KeyRegistrationmust fetch and deserialize from thedeletes_addressof the previous record - The
KeyRegistrationmust be anUpdateof typeKeyRegistration::Delete - The
KeyRevocationfrom theKeyRegistrationmust be revoking the deletedKeyAnchor
key_state:- input is
(KeyAnchor, Timestamp)tuple Timestampdoesn't do anything yet- output is
KeyStatewhich isValid/Invalidated/NotFoundasSignedActionHashed- If nothing found,
KeyState::NotFoundis 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_keyreferencing aKeyRegistrationby itsActionHashderivation_pathas 32 bytes encoding a derivation path for generating the registered keyderivation_indexas a u32 representing the index for generating the registered key from thederivation_pathkey_typeas an enum ofAppUI,AppSig,AppEncryption,TLSTODO: confirm compatibility with Lair Key API
Create:
- A
KeyMetamust deserialize cleanly from theRecord - The
new_keymust fetch and deserialize to aKeyRegistrationrecord - The author of the
KeyMetaand theKeyRegistrationmust be the same
Read: TODO
Update: Not allowed
Delete: Not allowed
new_key_meta- input is
key_meta - output is
ActionHashof the createdKeyMeta - creates a
KeyMeta
- input is
A DnaBinding is:
- A
key_metaasActionHashreferencing aKeyMeta - A
dna_hashof the DNA the key is bound to - An
app_nameas strings ofbundle_nameandcell_nickTODO: 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
ActionHashof the createdDnaBinding - creates a
DnaBinding
- input is
install_an_app- TODO
TODO: Discuss Rate Limiting