├── docs ├── images │ └── ERD.png ├── prototype │ ├── Membrane_proof.md │ └── Generators.md └── limbo │ └── Multi_device_management.md ├── .gitignore ├── zomes ├── deepkey │ ├── src │ │ ├── source_of_authority.rs │ │ ├── validation │ │ │ ├── create_link.rs │ │ │ ├── delete_link.rs │ │ │ ├── create_private_entry.rs │ │ │ ├── delete_entry.rs │ │ │ ├── create_entry.rs │ │ │ └── update_entry.rs │ │ ├── lib.rs │ │ ├── joining_proof.rs │ │ ├── error.rs │ │ ├── validation.rs │ │ └── utils.rs │ └── Cargo.toml └── deepkey_csr │ ├── Cargo.toml │ └── src │ ├── device.rs │ ├── source_of_authority.rs │ ├── key_meta.rs │ ├── utils.rs │ ├── app_binding.rs │ ├── keyset_root.rs │ ├── lib.rs │ ├── change_rule.rs │ ├── key_anchor.rs │ └── key_registration.rs ├── crates └── holochain_deepkey_dna │ ├── src │ └── lib.rs │ └── Cargo.toml ├── pkgs.nix ├── dnas └── deepkey │ ├── dna.yaml │ ├── Makefile │ └── zomelets │ ├── package.json │ ├── README.md │ ├── webpack.config.js │ └── src │ ├── types.js │ └── index.js ├── package.json ├── Cargo.toml ├── tests ├── utils.js ├── key_store.js └── integration │ ├── test_basic.js │ ├── test_change_rules.js │ └── test_claim_unmanaged_key.js ├── flake.nix ├── .github └── workflows │ ├── all-tests.yml │ └── build-pages.yml ├── flake.lock ├── CONTRIBUTING.md ├── Makefile ├── INTEGRITY_MODEL.md └── README.md /docs/images/ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/deepkey/HEAD/docs/images/ERD.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo 2 | /.rustup 3 | /target 4 | /node_modules 5 | /dnas/*/zomelets/node_modules 6 | *.dna 7 | *.wasm 8 | -------------------------------------------------------------------------------- /zomes/deepkey/src/source_of_authority.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use hdi::prelude::*; 3 | 4 | #[hdk_entry_helper] 5 | #[derive(Clone)] 6 | pub enum SourceOfAuthority { 7 | KeysetRoot(KeysetRoot), 8 | } 9 | -------------------------------------------------------------------------------- /crates/holochain_deepkey_dna/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Just exports the bytes of the canonical Deepkey DNA bundle. 2 | 3 | /// Get the hard-coded Deepkey DNA provided by this crate. 4 | /// This can be decoded with `holochain_types::DnaBundle::decode()` 5 | pub const DEEPKEY_DNA_BUNDLE_BYTES: &[u8] = include_bytes!("deepkey.dna"); 6 | -------------------------------------------------------------------------------- /pkgs.nix: -------------------------------------------------------------------------------- 1 | { pkgs, system }: 2 | 3 | import (pkgs.fetchFromGitHub { 4 | owner = "spartan-holochain-counsel"; 5 | repo = "nix-overlay"; 6 | rev = "4bf90e85448392512d8bf4dac91fdeb56bc7d610"; 7 | sha256 = "lxGLA0KMecdt6xRy9SqApDfh9UiQd9OYnwj9xeMLJcQ="; 8 | }) { 9 | inherit pkgs; 10 | inherit system; 11 | } 12 | -------------------------------------------------------------------------------- /dnas/deepkey/dna.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | manifest_version: "1" 3 | name: deepkey 4 | integrity: 5 | network_seed: ~ 6 | properties: ~ 7 | origin_time: 1669408001130688 8 | zomes: 9 | - name: deepkey 10 | hash: ~ 11 | bundled: "../../zomes/deepkey.wasm" 12 | dependencies: ~ 13 | coordinator: 14 | zomes: 15 | - name: deepkey_csr 16 | hash: ~ 17 | bundled: "../../zomes/deepkey_csr.wasm" 18 | dependencies: 19 | - name: deepkey 20 | -------------------------------------------------------------------------------- /zomes/deepkey/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepkey" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | name = "deepkey" 9 | 10 | [dependencies] 11 | hc_deepkey_types = { workspace = true } 12 | hdi = { workspace = true, features = ["trace"] } 13 | holo_hash = { workspace = true } 14 | rmp-serde = { workspace = true } 15 | serde = { workspace = true } 16 | serde_bytes = { workspace = true } 17 | thiserror = "1.0" 18 | whi_hdi_extensions = { workspace = true } 19 | -------------------------------------------------------------------------------- /crates/holochain_deepkey_dna/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "holochain_deepkey_dna" 3 | version = "0.0.8-dev.2" 4 | edition = "2021" 5 | authors = ["Michael dougherty "] 6 | license = "CAL-1.0" 7 | repository = "https://github.com/holochain/deepkey" 8 | description = "A compilation of the Deepkey DNA for use in Holochain" 9 | 10 | include = ["*", "src/deepkey.dna"] 11 | 12 | [lib] 13 | name = "holochain_deepkey_dna" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | 18 | [features] 19 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepkey_csr" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | name = "deepkey_csr" 9 | 10 | [dependencies] 11 | deepkey = { workspace = true } 12 | hc_deepkey_sdk = { workspace = true } 13 | hdk = { workspace = true } 14 | rmp-serde = { workspace = true } 15 | serde = { workspace = true } 16 | serde_bytes = { workspace = true } 17 | whi_hdk_extensions = { workspace = true } 18 | 19 | [dev-dependencies] 20 | ed25519-dalek = { version = "2.1", features = [ "rand_core" ] } 21 | rand = "0.8" 22 | -------------------------------------------------------------------------------- /dnas/deepkey/Makefile: -------------------------------------------------------------------------------- 1 | 2 | NAME = deepkey 3 | 4 | fix-rust-compile-issue: # Force rebuild to fix rust issue (typically after dry-run) 5 | touch types/src/lib.rs 6 | 7 | 8 | 9 | # 10 | # Types package 11 | # 12 | preview-types-crate: 13 | cargo publish -p hc_$(NAME)_types --dry-run --allow-dirty 14 | make fix-rust-compile-issue 15 | publish-types-crate: 16 | cargo publish -p hc_$(NAME)_types 17 | 18 | 19 | # 20 | # SDK package 21 | # 22 | preview-sdk-crate: 23 | cargo publish -p hc_$(NAME)_sdk --dry-run --allow-dirty 24 | make fix-rust-compile-issue 25 | publish-sdk-crate: 26 | cargo publish -p hc_$(NAME)_sdk 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing", 3 | "version": "0.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": {}, 8 | "author": "", 9 | "license": "ISC", 10 | "devDependencies": { 11 | "@holochain/deepkey-zomelets": "file:dnas/deepkey/zomelets", 12 | "@noble/ed25519": "^2.0.0", 13 | "@noble/hashes": "^1.4.0", 14 | "@spartan-hc/app-interface-client": "^0.7.2", 15 | "@spartan-hc/holo-hash": "^0.7.0", 16 | "@spartan-hc/holochain-backdrop": "^4.5.1", 17 | "@whi/json": "^0.1.8", 18 | "@whi/weblogger": "^0.4.0", 19 | "chai": "^4.3.4", 20 | "markdown-doctest": "^1.1.0", 21 | "mocha": "^10.1.0", 22 | "tweetnacl": "^1.0.3", 23 | "typescript-docs-verifier": "^2.5.0", 24 | "why-is-node-running": "^2.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.dev] 2 | opt-level = "z" 3 | 4 | [profile.release] 5 | opt-level = "z" 6 | 7 | [workspace] 8 | resolver = "2" 9 | members = ["crates/holochain_deepkey_dna", "zomes/*"] 10 | 11 | [workspace.dependencies] 12 | serde = "1" 13 | rmp-serde = "1" 14 | serde_bytes = "0.11" 15 | hc_deepkey_types = { version = "0.8.0-dev.3" } 16 | hc_deepkey_sdk = { version = "0.7.0-dev.3" } 17 | holo_hash = { version = "=0.4.0-dev.11", features = ["hashing", "encoding"] } 18 | holochain_integrity_types = { version = "=0.4.0-dev.12" } 19 | hdi = { version = "=0.5.0-dev.12" } 20 | hdk = { version = "=0.4.0-dev.14" } 21 | whi_hdi_extensions = { version = "0.12" } 22 | whi_hdk_extensions = { version = "0.12" } 23 | 24 | [workspace.dependencies.deepkey] 25 | path = "zomes/deepkey" 26 | 27 | [workspace.dependencies.deepkey_csr] 28 | path = "zomes/deepkey_csr" 29 | -------------------------------------------------------------------------------- /dnas/deepkey/zomelets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holochain/deepkey-zomelets", 3 | "version": "0.1.1", 4 | "description": "Zomelets for Holochain's DeepKey DNA", 5 | "type": "module", 6 | "main": "src/index.js", 7 | "module": "src/index.js", 8 | "browser": "dist/deepkey-zomelets.js", 9 | "files": [ 10 | "README.md", 11 | "src/index.js", 12 | "src/types.js", 13 | "dist/**" 14 | ], 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "Matthew Brisebois ", 19 | "license": "ISC", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/holochain/deepkey/" 23 | }, 24 | "dependencies": { 25 | "@spartan-hc/holo-hash": "^0.6.1", 26 | "@spartan-hc/zomelets": "^0.2.0", 27 | "@whi/bytes-class": "^0.1.0", 28 | "@whi/into-struct": "^0.2.1" 29 | }, 30 | "devDependencies": { 31 | "webpack": "^5.88.2", 32 | "webpack-cli": "^5.1.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation/create_link.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | LinkTypes, 3 | }; 4 | 5 | use hdi::prelude::*; 6 | use hdi_extensions::{ 7 | // AnyLinkableHashTransformer, 8 | // verify_app_entry_struct, 9 | // Macros 10 | valid, // invalid, 11 | }; 12 | 13 | 14 | pub fn validation( 15 | _base_address: AnyLinkableHash, 16 | _target_address: AnyLinkableHash, 17 | link_type: LinkTypes, 18 | _tag: LinkTag, 19 | _create: CreateLink, 20 | ) -> ExternResult { 21 | match link_type { 22 | LinkTypes::KeysetRootToKeyAnchors => { 23 | // verify_app_entry_struct::( &target_address )?; 24 | 25 | valid!() 26 | }, 27 | LinkTypes::KSRToChangeRule | 28 | LinkTypes::DeviceToKeyAnchor | 29 | LinkTypes::DeviceName | 30 | LinkTypes::AppBindingToKeyMeta => { 31 | valid!() 32 | }, 33 | // _ => invalid!(format!("Create link validation not implemented for link type: {:#?}", create.link_type )), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/device.rs: -------------------------------------------------------------------------------- 1 | // use crate::hdi_extensions::{ 2 | // guest_error, 3 | // }; 4 | use crate::hdk_extensions::{ 5 | agent_id, 6 | must_get, 7 | }; 8 | 9 | use deepkey::*; 10 | use hdk::prelude::*; 11 | 12 | 13 | #[hdk_extern] 14 | pub fn get_device_key_links(author: AgentPubKey) -> ExternResult> { 15 | get_links( 16 | GetLinksInputBuilder::try_new( 17 | author, 18 | LinkTypes::DeviceToKeyAnchor, 19 | )?.build() 20 | ) 21 | } 22 | 23 | 24 | #[hdk_extern] 25 | pub fn get_device_keys(agent: Option) -> ExternResult> { 26 | Ok( 27 | get_device_key_links( agent.unwrap_or( agent_id()? ) )? 28 | .into_iter() 29 | .filter_map( |link| must_get( &link.target.into_any_dht_hash()? ).ok() ) 30 | .filter_map( |record| Some(( 31 | record.action().entry_hash()?.to_owned(), 32 | KeyAnchor::try_from( record ).ok()?, 33 | ))) 34 | .collect() 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from 'chai'; 3 | 4 | 5 | export async function expect_reject ( cb, error, message ) { 6 | let failed = false; 7 | try { 8 | await cb(); 9 | } catch (err) { 10 | failed = true; 11 | expect( () => { throw err } ).to.throw( error, message ); 12 | } 13 | expect( failed ).to.be.true; 14 | } 15 | 16 | 17 | export function linearSuite ( name, setup_fn, args_fn ) { 18 | describe( name, function () { 19 | beforeEach(function () { 20 | let parent_suite = this.currentTest.parent; 21 | if ( parent_suite.tests.some(test => test.state === "failed") ) 22 | this.skip(); 23 | if ( parent_suite.parent?.tests.some(test => test.state === "failed") ) 24 | this.skip(); 25 | }); 26 | setup_fn.call( this, args_fn ); 27 | }); 28 | } 29 | 30 | 31 | export default { 32 | expect_reject, 33 | linearSuite, 34 | }; 35 | -------------------------------------------------------------------------------- /dnas/deepkey/zomelets/README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/npm/v/@holochain/deepkey-zomelets/latest?style=flat-square)](http://npmjs.com/package/@holochain/deepkey-zomelets) 2 | 3 | # DeepKey Zomelets 4 | Zomelet implementations for the [DeepKey DNA](https://github.com/holochain/deepkey). 5 | 6 | [![](https://img.shields.io/github/issues-raw/holochain/deepkey?style=flat-square)](https://github.com/holochain/deepkey/issues) 7 | [![](https://img.shields.io/github/issues-closed-raw/holochain/deepkey?style=flat-square)](https://github.com/holochain/deepkey/issues?q=is%3Aissue+is%3Aclosed) 8 | [![](https://img.shields.io/github/issues-pr-raw/holochain/deepkey?style=flat-square)](https://github.com/holochain/deepkey/pulls) 9 | 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm i @holochain/deepkey-zomelets 15 | ``` 16 | 17 | ## Basic Usage 18 | 19 | ```js 20 | import { DeepKeyCell } from '@holochain/deepkey-zomelets'; 21 | 22 | // Then use `DeepKeyCell` in your Zomelet compatible client 23 | ``` 24 | 25 | See [@spartan-hc/app-interface-client](https://www.npmjs.com/package/@spartan-hc/app-interface-client) for how to use Zomelets. 26 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Holochain Development Env"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, flake-utils }: 9 | flake-utils.lib.eachDefaultSystem (system: 10 | let 11 | pkgs = import ./pkgs.nix { 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | inherit system; 14 | }; 15 | in 16 | { 17 | devShell = pkgs.mkShell { 18 | buildInputs = with pkgs; [ 19 | holochain_0-4 20 | lair-keystore_0-5 21 | hc_0-4 22 | 23 | rustup 24 | cargo 25 | rustc 26 | 27 | nodejs_22 28 | ]; 29 | 30 | shellHook = '' 31 | export PS1="\[\e[1;32m\](flake-env)\[\e[0m\] \[\e[1;34m\]\u@\h:\w\[\e[0m\]$ " 32 | export CARGO_HOME=$(pwd)/.cargo 33 | export RUSTUP_HOME=$(pwd)/.rustup 34 | rustup default stable 35 | rustup target add wasm32-unknown-unknown 36 | ''; 37 | }; 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /docs/prototype/Membrane_proof.md: -------------------------------------------------------------------------------- 1 | [back to CONTRIBUTING.md](../../CONTRIBUTING.md) 2 | 3 | 4 | ## Joining the DHT 5 | 6 | The Deepkey `JoiningProof` involves two proofs. One is the membrane proof which is true for all 7 | happs, and the other is the keyset proof described in the next section. 8 | 9 | ### Membrane Proof 10 | 11 | The purpose of a `membrane_proof` is to make it hard to flood the network with fake accounts. 12 | 13 | _TODO: The membrane logic is not currently implemented._ 14 | 15 | Future membrane logic implementations planned: 16 | 17 | - `ProofOfWork`: Agent must prove that they've performed some computational work to prevent low-effort spam bots. 18 | - `ProofOfStake`: Agent must put up value that can be taken in case of bad behaviour. 19 | - `ProofOfAuthority`: Agent must have a signature from a pre-defined authority to join. 20 | 21 | There are a few external details to resolve before we require membrane proofs: 22 | 23 | - Ability to have different versions of Deepkey apps to choose from, and configure their own joining proof. 24 | - 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. 25 | - More thought is needed about the types of membrane proofs we might like for the default behavior of Deepkey. 26 | -------------------------------------------------------------------------------- /dnas/deepkey/zomelets/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | import webpack from 'webpack'; 3 | import TerserPlugin from 'terser-webpack-plugin'; 4 | 5 | 6 | const MODE = process.env.MODE || "development"; 7 | const FILENAME = process.env.FILENAME || "deepkey-zomelets"; 8 | const FILEEXT = MODE === "production" ? "min.js" : "js"; 9 | 10 | 11 | export default { 12 | "target": "web", 13 | "mode": MODE, 14 | "entry": { 15 | "main": { 16 | "import": "./src/index.js", 17 | "filename": `${FILENAME}.${FILEEXT}`, 18 | "library": { 19 | "type": "module", 20 | }, 21 | }, 22 | }, 23 | "resolve": { 24 | "mainFields": [ "module", "browser", "main" ], 25 | }, 26 | "experiments": { 27 | "outputModule": true, 28 | }, 29 | "optimization": { 30 | "minimizer": [ 31 | new TerserPlugin({ 32 | "terserOptions": { 33 | "keep_classnames": true, 34 | }, 35 | }), 36 | ], 37 | }, 38 | "devtool": "source-map", 39 | "stats": { 40 | "colors": true, 41 | }, 42 | "plugins": [ 43 | new webpack.optimize.LimitChunkCountPlugin({ 44 | "maxChunks": 1, 45 | }), 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/source_of_authority.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use crate::hdi_extensions::{ 3 | guest_error, 4 | }; 5 | use crate::hdk_extensions::{ 6 | must_get, 7 | }; 8 | 9 | use deepkey::*; 10 | use hdk::prelude::*; 11 | 12 | 13 | // This function queries for the keyset authority for this conductor. 14 | // It first checks if a device invite acceptance has been committed to the DHT 15 | // If this is the case, we return the keyset root authority action hash from 16 | // the device invite acceptance entry 17 | // If this is not the case, we find and return the actual keyset root entry on this chain. 18 | #[hdk_extern] 19 | pub fn query_keyset_authority_action_hash(_: ()) -> ExternResult { 20 | query_keyset_root_action_hash(()) 21 | } 22 | 23 | // This function queries for the keyset root, and returns its action hash. 24 | #[hdk_extern] 25 | pub fn query_keyset_root_action_hash(_: ()) -> ExternResult { 26 | match utils::query_entry_type_latest( EntryTypesUnit::KeysetRoot )? { 27 | Some(keyset_root) => Ok(keyset_root.action_address().to_owned()), 28 | None => Err(guest_error!("No KeysetFound on chain".to_string())) 29 | } 30 | } 31 | 32 | 33 | pub fn query_keyset_root_addr() -> ExternResult { 34 | query_keyset_root_action_hash(()) 35 | } 36 | 37 | 38 | pub fn query_keyset_root() -> ExternResult { 39 | must_get( &query_keyset_root_addr()? )?.try_into() 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/all-tests.yml: -------------------------------------------------------------------------------- 1 | name: All Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Nix 21 | uses: cachix/install-nix-action@v27 22 | with: 23 | nix_path: nixpkgs=channel:nixos-unstable 24 | 25 | - name: Build Packages 26 | run: | 27 | nix develop --command make dnas/deepkey.dna 28 | 29 | - name: Run Tests 30 | run: | 31 | output=$(DEBUG_LEVEL=trace nix develop --command bash -c 'make test | tee >(cat >&2); exit ${PIPESTATUS[0]}'); 32 | if [ ${PIPESTATUS[0]} -ne 0 ]; then exit ${PIPESTATUS[0]}; fi 33 | passing=$(echo "$output" | grep -oP '\d+ passing' | grep -oP '\d+' | awk '{sum+=$1} END {print sum}') 34 | failing=$(echo "$output" | grep -oP '\d+ failing' | grep -oP '\d+' | awk '{sum+=$1} END {print sum}') 35 | pending=$(echo "$output" | grep -oP '\d+ pending' | grep -oP '\d+' | awk '{sum+=$1} END {print sum}') 36 | echo "## Summary" >> $GITHUB_STEP_SUMMARY 37 | echo "" >> $GITHUB_STEP_SUMMARY 38 | echo "| Passed | Failed | Pending |" >> $GITHUB_STEP_SUMMARY 39 | echo "|----------|----------|----------|" >> $GITHUB_STEP_SUMMARY 40 | echo "| $passing | $failing | $pending |" >> $GITHUB_STEP_SUMMARY 41 | if [ "$failing" -ne 0 ]; then exit 1; fi 42 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "id": "flake-utils", 17 | "type": "indirect" 18 | } 19 | }, 20 | "nixpkgs": { 21 | "locked": { 22 | "lastModified": 1722640603, 23 | "narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=", 24 | "owner": "NixOS", 25 | "repo": "nixpkgs", 26 | "rev": "81610abc161d4021b29199aa464d6a1a521e0cc9", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "NixOS", 31 | "ref": "nixpkgs-unstable", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/build-pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy cargo doc to Pages 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | build: 24 | name: Build 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | - name: Setup Rust 30 | uses: dtolnay/rust-toolchain@stable 31 | - name: Setup pages 32 | id: pages 33 | uses: actions/configure-pages@v4 #v5 available 34 | - name: Clean docs folder 35 | run: cargo clean --doc 36 | - name: Build docs 37 | run: | 38 | cargo doc --no-deps --all-features -p hc_deepkey_types -p hc_deepkey_sdk -p deepkey -p deepkey_csr 39 | - name: Add redirect 40 | run: echo '' > target/doc/index.html 41 | - name: Remove lock file 42 | run: rm target/doc/.lock 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: target/doc 47 | 48 | deploy: 49 | name: Deploy 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | runs-on: ubuntu-latest 54 | needs: build 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation/delete_link.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | LinkTypes, 3 | }; 4 | 5 | use hdi::prelude::*; 6 | use hdi_extensions::{ 7 | // Macros 8 | valid, invalid, 9 | }; 10 | 11 | 12 | pub fn validation( 13 | original_action_hash: ActionHash, 14 | _base_address: AnyLinkableHash, 15 | _delete: DeleteLink, 16 | ) -> ExternResult { 17 | let record = must_get_valid_record( original_action_hash )?; 18 | let create_link = match record.action() { 19 | Action::CreateLink(action) => action, 20 | _ => invalid!(format!("Original action hash does not belong to create link action")), 21 | }; 22 | let link_type = match LinkTypes::from_type( create_link.zome_index, create_link.link_type )? { 23 | Some(lt) => lt, 24 | None => invalid!(format!("No match for LinkTypes")), 25 | }; 26 | 27 | match link_type { 28 | LinkTypes::KeysetRootToKeyAnchors => { 29 | invalid!(format!("KeysetRootToKeyAnchors links cannot be deleted")) 30 | }, 31 | LinkTypes::KSRToChangeRule => { 32 | invalid!(format!("KSRToChangeRule links cannot be deleted")) 33 | }, 34 | LinkTypes::DeviceToKeyAnchor => { 35 | invalid!(format!("DeviceToKeyAnchor links cannot be deleted")) 36 | }, 37 | LinkTypes::DeviceName => { 38 | valid!() 39 | }, 40 | LinkTypes::AppBindingToKeyMeta => { 41 | valid!() 42 | }, 43 | // _ => { 44 | // // if create_link.author != delete.author { 45 | // // invalid!(format!( 46 | // // "Not authorized to delete link created by author {}", 47 | // // create_link.author 48 | // // )) 49 | // // } 50 | 51 | // valid!() 52 | // }, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /zomes/deepkey/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod validation; 2 | 3 | pub mod utils; 4 | pub mod error; 5 | pub mod joining_proof; 6 | pub mod source_of_authority; 7 | 8 | // Re-exports 9 | pub use hc_deepkey_types as deepkey_types; 10 | 11 | pub use hc_deepkey_types::*; 12 | pub use error::*; 13 | pub use joining_proof::*; 14 | pub use source_of_authority::*; 15 | 16 | use hdi::prelude::*; 17 | use hdi_extensions::{ 18 | scoped_type_connector, 19 | ScopedTypeConnector, 20 | }; 21 | 22 | 23 | #[derive(Serialize, Deserialize)] 24 | #[serde(tag = "type")] 25 | #[hdk_entry_types] 26 | #[unit_enum(EntryTypesUnit)] 27 | pub enum EntryTypes { 28 | KeysetRoot(KeysetRoot), 29 | 30 | ChangeRule(ChangeRule), 31 | 32 | KeyRegistration(KeyRegistration), 33 | KeyAnchor(KeyAnchor), 34 | 35 | #[entry_type(visibility = "private")] 36 | KeyMeta(KeyMeta), 37 | #[entry_type(visibility = "private")] 38 | AppBinding(AppBinding), 39 | } 40 | 41 | scoped_type_connector!( 42 | EntryTypesUnit::KeysetRoot, 43 | EntryTypes::KeysetRoot( KeysetRoot ) 44 | ); 45 | scoped_type_connector!( 46 | EntryTypesUnit::ChangeRule, 47 | EntryTypes::ChangeRule( ChangeRule ) 48 | ); 49 | scoped_type_connector!( 50 | EntryTypesUnit::KeyRegistration, 51 | EntryTypes::KeyRegistration( KeyRegistration ) 52 | ); 53 | scoped_type_connector!( 54 | EntryTypesUnit::KeyAnchor, 55 | EntryTypes::KeyAnchor( KeyAnchor ) 56 | ); 57 | scoped_type_connector!( 58 | EntryTypesUnit::KeyMeta, 59 | EntryTypes::KeyMeta( KeyMeta ) 60 | ); 61 | scoped_type_connector!( 62 | EntryTypesUnit::AppBinding, 63 | EntryTypes::AppBinding( AppBinding ) 64 | ); 65 | 66 | 67 | #[derive(Serialize, Deserialize)] 68 | #[hdk_link_types] 69 | pub enum LinkTypes { 70 | KSRToChangeRule, 71 | KeysetRootToKeyAnchors, 72 | DeviceToKeyAnchor, 73 | DeviceName, 74 | AppBindingToKeyMeta, 75 | } 76 | -------------------------------------------------------------------------------- /zomes/deepkey/src/joining_proof.rs: -------------------------------------------------------------------------------- 1 | use hdi::prelude::*; 2 | 3 | use crate::SourceOfAuthority; 4 | 5 | // @todo - e.g. configurable difficulty over hashing the DNA - https://docs.rs/pow/0.2.0/pow/ 6 | #[hdk_entry_helper] 7 | #[derive(Clone)] 8 | pub struct ProofOfWork([u8; 32]); 9 | 10 | // @todo 11 | #[hdk_entry_helper] 12 | #[derive(Clone)] 13 | pub struct ProofOfStake([u8; 32]); 14 | 15 | // @todo 16 | #[hdk_entry_helper] 17 | #[derive(Clone)] 18 | pub struct ProofOfExternalAuthority([u8; 32]); 19 | 20 | #[hdk_entry_helper] 21 | #[derive(Clone)] 22 | pub enum MembraneProof { 23 | // No additional membrane. 24 | None, 25 | // Proof of Work membrane. 26 | ProofOfWork(ProofOfWork), 27 | // Proof of Stake membrane. 28 | ProofOfStake(ProofOfStake), 29 | // Proof of Authority membrane. 30 | ProofOfExternalAuthority(ProofOfExternalAuthority), 31 | } 32 | 33 | #[hdk_entry_helper] 34 | #[derive(Clone)] 35 | pub struct JoiningProof { 36 | pub source_of_authority: SourceOfAuthority, 37 | pub membrane_proof: MembraneProof, 38 | } 39 | 40 | impl JoiningProof { 41 | pub fn new(source_of_authority: SourceOfAuthority, membrane_proof: MembraneProof) -> Self { 42 | Self { 43 | source_of_authority, 44 | membrane_proof, 45 | } 46 | } 47 | } 48 | 49 | pub fn validate_create_joining_proof( 50 | _action: EntryCreationAction, 51 | _joining_proof: JoiningProof, 52 | ) -> ExternResult { 53 | Ok(ValidateCallbackResult::Valid) 54 | } 55 | pub fn validate_update_joining_proof( 56 | _action: Update, 57 | _joining_proof: JoiningProof, 58 | _original_action: EntryCreationAction, 59 | _original_joining_proof: JoiningProof, 60 | ) -> ExternResult { 61 | Ok(ValidateCallbackResult::Invalid(String::from( 62 | "Joining Proofs cannot be updated", 63 | ))) 64 | } 65 | pub fn validate_delete_joining_proof( 66 | _action: Delete, 67 | _original_action: EntryCreationAction, 68 | _original_joining_proof: JoiningProof, 69 | ) -> ExternResult { 70 | Ok(ValidateCallbackResult::Invalid(String::from( 71 | "Joining Proofs cannot be deleted", 72 | ))) 73 | } 74 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation/create_private_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | EntryTypes, 3 | // EntryTypesUnit, 4 | 5 | KeyMeta, 6 | 7 | utils, 8 | }; 9 | 10 | use hdi::prelude::*; 11 | use hdi_extensions::{ 12 | // Macros 13 | valid, invalid, 14 | }; 15 | 16 | 17 | pub fn key_metas_for_app_index ( 18 | author: &AgentPubKey, 19 | chain_top: &ActionHash, 20 | app_index: u32, 21 | ) -> ExternResult> { 22 | let key_metas = get_activity_for_entry_type( 23 | EntryTypesUnit::KeyMeta, 24 | author, 25 | chain_top, 26 | )? 27 | .into_iter() 28 | .filter_map( |activity| { 29 | let key_meta : KeyMeta = match activity.cached_entry { 30 | Some(entry) => entry, 31 | None => must_get_entry( 32 | activity.action.action().entry_hash().unwrap().to_owned() 33 | ).ok()?.content, 34 | }.try_into().ok()?; 35 | 36 | Some(( activity, key_meta )) 37 | }) 38 | .filter( |(_, key_meta)| ) 39 | .collect(); 40 | } 41 | 42 | 43 | pub fn validation( 44 | app_entry: EntryTypes, 45 | create: Create 46 | ) -> ExternResult { 47 | match app_entry { 48 | EntryTypes::KeyMeta(key_meta_entry) => { 49 | // Check that the app index exists 50 | 51 | // Check that the key index is incremented by 1 52 | let prev_key_meta = utils::prev_key_meta( 53 | &create.author, 54 | &create.prev_action, 55 | app_binding.app_index, 56 | )?; 57 | 58 | match prev_key_meta { 59 | Some(prev_key_meta) => { 60 | if (prev_key_meta.key_index + 1) != key_meta_entry.key_index { 61 | invalid!(format!( 62 | "Key Meta for App Binding (index: {}) should have key_index: {}", 63 | app_binding.app_index, prev_key_meta.key_index + 1, 64 | )) 65 | } 66 | }, 67 | None => { 68 | if key_meta_entry.key_index != 0 { 69 | invalid!("First Key Meta for an App Binding should have key_index: 0".to_string()) 70 | } 71 | }, 72 | } 73 | 74 | valid!() 75 | } 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation/delete_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | EntryTypesUnit, 3 | 4 | KeyAnchor, 5 | KeyRegistration, 6 | }; 7 | 8 | use hdi::prelude::*; 9 | use hdi_extensions::{ 10 | summon_app_entry, 11 | summon_creation_action, 12 | detect_app_entry_unit, 13 | 14 | // Macros 15 | valid, invalid, 16 | }; 17 | 18 | 19 | pub fn validation( 20 | original_action_hash: ActionHash, 21 | _original_entry_hash: EntryHash, 22 | delete: Delete 23 | ) -> ExternResult { 24 | let creation = summon_creation_action( &original_action_hash )?; 25 | 26 | match detect_app_entry_unit( &creation )? { 27 | EntryTypesUnit::KeysetRoot => { 28 | invalid!(format!("Keyset Roots cannot be deleted")) 29 | }, 30 | EntryTypesUnit::ChangeRule => { 31 | invalid!(format!("Change Rules cannot be deleted")) 32 | }, 33 | EntryTypesUnit::KeyRegistration => { 34 | invalid!(format!("Key Registrations cannot be deleted")) 35 | }, 36 | EntryTypesUnit::KeyAnchor => { 37 | // Check previous action is key registration revoke. 38 | let key_anchor_entry : KeyAnchor = summon_app_entry( &original_action_hash.into() )?; 39 | let key_reg : KeyRegistration = summon_app_entry( &delete.prev_action.into() )?; 40 | 41 | let key_rev = match key_reg { 42 | KeyRegistration::Delete(key_rev) => key_rev, 43 | _ => invalid!(format!( 44 | "KeyAnchor update must be preceeded by a KeyRegistration::Delete" 45 | )), 46 | }; 47 | 48 | let prior_key_reg : KeyRegistration = summon_app_entry( 49 | &key_rev.prior_key_registration.into() 50 | )?; 51 | 52 | if prior_key_reg.key_anchor()? != key_anchor_entry { 53 | invalid!(format!( 54 | "Deleted KeyAnchor does not match prior KeyRegistration key anchor: {:#?} != {:#?}", 55 | key_anchor_entry, prior_key_reg.key_anchor()?, 56 | )) 57 | } 58 | 59 | valid!() 60 | }, 61 | EntryTypesUnit::KeyMeta => { 62 | invalid!(format!("Key Metas cannot be deleted")) 63 | }, 64 | EntryTypesUnit::AppBinding => { 65 | invalid!(format!("App Bindings cannot be deleted")) 66 | }, 67 | // entry_type_unit => invalid!(format!("Delete validation not implemented for entry type: {:?}", entry_type_unit )), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/key_meta.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use deepkey::*; 3 | use serde_bytes::ByteArray; 4 | 5 | use hdk::prelude::*; 6 | use hdk_extensions::{ 7 | hdi_extensions::{ 8 | guest_error, 9 | }, 10 | }; 11 | 12 | 13 | #[hdk_extern] 14 | pub fn query_key_metas_for_app_binding(addr: ActionHash) -> ExternResult> { 15 | Ok( 16 | utils::query_entry_type( EntryTypesUnit::KeyMeta )? 17 | .into_iter() 18 | .filter_map( |record| KeyMeta::try_from( record ).ok() ) 19 | .filter( |key_meta| key_meta.app_binding_addr == addr ) 20 | .collect() 21 | ) 22 | } 23 | 24 | 25 | #[hdk_extern] 26 | pub fn query_key_metas_for_app_index(app_index: u32) -> ExternResult> { 27 | query_key_metas_for_app_binding( 28 | crate::app_binding::query_app_binding_by_index( app_index )?.0 29 | ) 30 | } 31 | 32 | 33 | #[hdk_extern] 34 | pub fn query_next_key_index_for_app_index(app_index: u32) -> ExternResult { 35 | Ok( query_key_metas_for_app_index( app_index )?.len() as u32 ) 36 | } 37 | 38 | 39 | #[hdk_extern] 40 | pub fn query_key_meta_records(_: ()) -> ExternResult> { 41 | utils::query_entry_type( EntryTypesUnit::KeyMeta ) 42 | } 43 | 44 | 45 | #[hdk_extern] 46 | pub fn query_key_metas(_: ()) -> ExternResult> { 47 | Ok( 48 | utils::query_entry_type( EntryTypesUnit::KeyMeta )? 49 | .into_iter() 50 | .filter_map( |record| KeyMeta::try_from( record ).ok() ) 51 | .collect() 52 | ) 53 | } 54 | 55 | 56 | #[hdk_extern] 57 | pub fn query_key_meta_for_key_addr(anchor_addr: ActionHash) -> ExternResult { 58 | query_key_metas(())? 59 | .into_iter() 60 | .find( |key_meta| key_meta.key_anchor_addr == anchor_addr ) 61 | .ok_or(guest_error!(format!("No KeyMeta for anchor addr: {}", anchor_addr ))) 62 | } 63 | 64 | 65 | #[hdk_extern] 66 | pub fn query_key_meta_for_key( 67 | key_bytes: ByteArray<32> 68 | ) -> ExternResult { 69 | let key_anchor_addr = crate::key_anchor::query_action_addr_for_key_anchor( key_bytes )?; 70 | 71 | Ok( query_key_meta_for_key_addr( key_anchor_addr )? ) 72 | } 73 | 74 | 75 | #[hdk_extern] 76 | pub fn query_key_meta_for_registration(key_reg_addr: ActionHash) -> ExternResult { 77 | query_key_metas(())? 78 | .into_iter() 79 | .find( |key_meta| key_meta.key_registration_addr == key_reg_addr ) 80 | .ok_or(guest_error!(format!("No KeyMeta for registration addr: {}", key_reg_addr ))) 81 | } 82 | -------------------------------------------------------------------------------- /zomes/deepkey/src/error.rs: -------------------------------------------------------------------------------- 1 | use hdi::prelude::*; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum Error { 6 | #[error("Element missing its ChangeRule")] 7 | EntryMissing, 8 | 9 | #[error("Attempted to delete a ChangeRule")] 10 | DeleteAttempted, 11 | 12 | #[error("Attempted to update a ChangeRule")] 13 | UpdateAttempted, 14 | 15 | #[error("The ChangeRule author is not the FDA on the KeysetRoot")] 16 | AuthorNotFda, 17 | 18 | #[error("Multiple creation signatures found")] 19 | MultipleCreateSignatures, 20 | 21 | #[error("No creation signature found")] 22 | NoCreateSignature, 23 | 24 | #[error("Invalid creation signature")] 25 | BadCreateSignature, 26 | 27 | #[error("The new ChangeRule has a different KeysetRoot")] 28 | KeysetRootMismatch, 29 | 30 | #[error("The new ChangeRule has the wrong number of signatures")] 31 | WrongNumberOfSignatures, 32 | 33 | #[error("The new ChangeRule referenced an authorizor position that doesn't exist")] 34 | AuthorizedPositionOutOfBounds, 35 | 36 | #[error("The new ChangeRule references a KeysetLeaf that is incompatible with its KeysetRoot")] 37 | BadKeysetLeaf, 38 | 39 | #[error("The new ChangeRule references a stale keyset leaf")] 40 | StaleKeysetLeaf, 41 | 42 | #[error("The new ChangeRule has no validation package")] 43 | MissingValidationPackage, 44 | 45 | #[error("The new ChangeRule has an invalid signature")] 46 | BadUpdateSignature, 47 | 48 | #[error( 49 | "The new ChangeRule has fewer authorized signers than the minimum required signatures" 50 | )] 51 | NotEnoughSigners, 52 | 53 | #[error("The new ChangeRule requires zero signatures")] 54 | NotEnoughSignatures, 55 | 56 | #[error("The new ChangeRule update does not reference the root ChangeRule")] 57 | BranchingUpdates, 58 | 59 | #[error("The ChangeRule created does not immediately follow its KeysetRoot")] 60 | CreateNotAfterKeysetRoot, 61 | 62 | #[error("The ChangeRule element has the wrong header")] 63 | WrongHeader, 64 | 65 | #[error("Wasm error {0}")] 66 | Wasm(WasmError), 67 | } 68 | 69 | impl From for ValidateCallbackResult { 70 | fn from(e: Error) -> Self { 71 | ValidateCallbackResult::Invalid(e.to_string()) 72 | } 73 | } 74 | 75 | impl From for ExternResult { 76 | fn from(e: Error) -> Self { 77 | Ok(e.into()) 78 | } 79 | } 80 | 81 | impl From for Error { 82 | fn from(e: WasmError) -> Error { 83 | Error::Wasm(e) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/prototype/Generators.md: -------------------------------------------------------------------------------- 1 | [back to CONTRIBUTING.md](../../CONTRIBUTING.md) 2 | 3 | 4 | ## Generators 5 | 6 | ### Generator API 7 | 8 | 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. 9 | 10 | The structures comprising a `Generator` are: 11 | 12 | ```rust 13 | pub struct Generator { 14 | change_rule: ActionHash, // `ChangeRule` action that authorizes this `Generator` 15 | change: Change, 16 | } 17 | pub struct Change { 18 | new_key: AgentPubKey, // A new special-purpose key being authorized as a `Generator` 19 | authorization: Vec, // authorizes the `new_key` according to the `ChangeRule` rules 20 | } 21 | ``` 22 | 23 | **Create**: The validation that happens when you create a new `Generator` 24 | 25 | - A `Generator` must deserialize cleanly from the record. 26 | - The `change_rule` must fetch and deserialize cleanly from the referenced `ActionHash`. 27 | - The `new_key` must be authorized by the authorization vec in the `Change` according to the `ChangeRule` rules. 28 | 29 | **Read**: There is no read or lookup zome call exposed for `Generator` 30 | 31 | **Update**: Not allowed 32 | 33 | **Delete**: Not allowed 34 | 35 | #### Zome Calls 36 | 37 | - `new_generator` 38 | - input is a `Generator` 39 | - output is a `ActionHash` 40 | - creates a `Generator` 41 | 42 | ### KeyGeneration API 43 | 44 | The structure of a `KeyGeneration` is: 45 | 46 | ```rust 47 | pub struct KeyGeneration { 48 | new_key: AgentPubKey, // New key associated with current chain and KSR 49 | new_key_signing_of_author: Signature, // The new key must sign the Deepkey agent to join the Keyset 50 | // Ensure the generator has the same author as the KeyRegistration. 51 | generator: ActionHash, // This is the key authorized to generate new keys on this chain 52 | generator_signature: Signature, // The generator key signing the new key 53 | } 54 | ``` 55 | 56 | #### Validation 57 | 58 | - A `Generator` must fetch and deserialize cleanly for the `KeyGeneration` generator 59 | - The `Generator` author must be the same as the `KeyGeneration` author 60 | - The `Signature` of the `new_key` signing in the author of the `KeyGeneration` must be valid 61 | - The `Signature` of the `new_key` from the `AgentPubKey` of the `Generator` must be valid 62 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::hdi_extensions; 2 | 3 | use hdi_extensions::{ 4 | guest_error, 5 | }; 6 | use hdk::prelude::*; 7 | use hdk_extensions::{ 8 | must_get, 9 | must_get_record_details, 10 | }; 11 | 12 | pub use crate::source_of_authority::*; 13 | pub use deepkey::{ 14 | utils::*, 15 | }; 16 | 17 | 18 | pub fn query_entry_type(unit: T) -> ExternResult> 19 | where 20 | EntryType: TryFrom, 21 | WasmError: From, 22 | { 23 | Ok( 24 | query( 25 | ChainQueryFilter::new() 26 | .include_entries(true) 27 | .entry_type( EntryType::try_from(unit)? ) 28 | )? 29 | ) 30 | } 31 | 32 | 33 | pub fn query_entry_type_first(unit: T) -> ExternResult> 34 | where 35 | EntryType: TryFrom, 36 | WasmError: From, 37 | { 38 | Ok( query_entry_type( unit )?.pop() ) 39 | } 40 | 41 | 42 | pub fn query_entry_type_latest(unit: T) -> ExternResult> 43 | where 44 | EntryType: TryFrom, 45 | WasmError: From, 46 | { 47 | Ok( query_entry_type( unit )?.pop() ) 48 | } 49 | 50 | 51 | pub fn get_chain_index(index: u32) -> ExternResult> { 52 | Ok( 53 | query( 54 | ChainQueryFilter::new() 55 | .sequence_range( ChainQueryFilterRange::ActionSeqRange(index, index) ) 56 | )?.pop() 57 | ) 58 | } 59 | 60 | 61 | pub fn my_agent_validation_pkg() -> ExternResult { 62 | let action = get_chain_index( 1 )? 63 | .ok_or(guest_error!("Chain is missing index 1".to_string()))? 64 | .signed_action.hashed.content; 65 | 66 | if let Action::AgentValidationPkg(avp) = action { 67 | Ok( avp ) 68 | } else { 69 | Err(guest_error!("Chain index 1 is not an AgentValidationPkg action".to_string()))? 70 | } 71 | } 72 | 73 | 74 | pub fn get_next_update(addr: ActionHash) -> ExternResult> { 75 | let mut details = must_get_record_details( &addr )?; 76 | 77 | // Sort updates in ascending timestamp order so that the last item is the most recent update 78 | details.updates.sort_by( |a,b| { 79 | a.action().timestamp().to_owned().cmp(&b.action().timestamp()) 80 | }); 81 | 82 | Ok( 83 | match details.updates.last() { 84 | Some(update) => Some(update.action_address().to_owned()), 85 | None => None, 86 | } 87 | ) 88 | } 89 | 90 | 91 | pub fn get_latest_record(addr: ActionHash) -> ExternResult { 92 | let mut latest_addr = addr; 93 | 94 | while let Some(update) = get_next_update( latest_addr.clone() )? { 95 | latest_addr = update; 96 | } 97 | 98 | must_get( &latest_addr ) 99 | } 100 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation.rs: -------------------------------------------------------------------------------- 1 | mod create_entry; 2 | mod update_entry; 3 | mod delete_entry; 4 | mod create_link; 5 | mod delete_link; 6 | 7 | use crate::{ 8 | EntryTypes, 9 | LinkTypes, 10 | }; 11 | 12 | use hdi::prelude::*; 13 | use hdi_extensions::{ 14 | // Macros 15 | valid, invalid, 16 | }; 17 | 18 | 19 | #[hdk_extern] 20 | pub fn validate(op: Op) -> ExternResult { 21 | let result = match op.flattened::()? { 22 | FlatOp::StoreRecord(op_record) => match op_record { 23 | OpRecord::CreateEntry { app_entry, action } => 24 | create_entry::validation( app_entry, action ), 25 | OpRecord::UpdateEntry { app_entry, action, original_action_hash, original_entry_hash } => 26 | update_entry::validation( app_entry, action, original_action_hash, original_entry_hash ), 27 | OpRecord::DeleteEntry { original_action_hash, original_entry_hash, action } => 28 | delete_entry::validation( original_action_hash, original_entry_hash, action ), 29 | OpRecord::CreateLink { base_address, target_address, tag, link_type, action } => 30 | create_link::validation( base_address, target_address, link_type, tag, action ), 31 | OpRecord::DeleteLink { original_action_hash, base_address, action } => 32 | delete_link::validation( original_action_hash, base_address, action ), 33 | // OpRecord::CreateAgent { agent, action: create }, 34 | // OpRecord::UpdateAgent { original_key, new_key, original_action_hash, action: update }, 35 | // OpRecord::CreateCapClaim { action: create }, 36 | // OpRecord::CreateCapGrant { action: create }, 37 | // OpRecord::CreatePrivateEntry { app_entry_type, action: create }, 38 | // OpRecord::UpdatePrivateEntry { original_action_hash, original_entry_hash, app_entry_type, action: update }, 39 | // OpRecord::UpdateCapClaim { original_action_hash, original_entry_hash, action: update }, 40 | // OpRecord::UpdateCapGrant { original_action_hash, original_entry_hash, action: update }, 41 | // OpRecord::Dna { dna_hash, action: dna }, 42 | // OpRecord::OpenChain { previous_dna_hash, action: open_chain }, 43 | // OpRecord::CloseChain { new_dna_hash, action: close_chain }, 44 | // OpRecord::AgentValidationPkg { membrane_proof, action: agent_validation_pkg }, 45 | // OpRecord::InitZomesComplete { action: init_zomes_complete }, 46 | _ => valid!(), 47 | }, 48 | // FlatOp::StoreEntry(op_entry), 49 | FlatOp::RegisterAgentActivity(_op_activity) => { 50 | // debug!("RegisterAgentActivity => {:#?}", op_activity ); 51 | valid!() 52 | }, 53 | // FlatOp::RegisterCreateLink { base_address, target_address, tag, link_type, action: create_link }, 54 | // FlatOp::RegisterDeleteLink { original_action, base_address, target_address, tag, link_type, action: delete_link }, 55 | // FlatOp::RegisterUpdate(op_update), 56 | // FlatOp::RegisterDelete(op_delete), 57 | _ => valid!(), 58 | }; 59 | 60 | if let Err(WasmError{ error: WasmErrorInner::Guest(msg), .. }) = result { 61 | invalid!(msg) 62 | } 63 | 64 | result 65 | } 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [back to README.md](README.md) 2 | 3 | 4 | # Contributing 5 | 6 | The purpose of this repo is to build a Holochain DNA that provides a decentralized public key 7 | infrastructure (DPKI) that will be the default for Holochain installations. 8 | 9 | 10 | ## Overview 11 | The Deepkey DNA is designed so that it can work on its own, but architectural decisions are driven 12 | by the necessary integrations required to support Holochain's DPKI service and Lair. 13 | 14 | In the design space of all possible Deepkey implementations, any type of public key could be 15 | registered and managed (eg. for blockchains, TLS certificates, etc). This implementation only 16 | supports the registration of Holochain `AgentPubKey`. 17 | 18 | 19 | ### Entity Relationship Diagram (high-level) 20 | 21 | ![](docs/images/ERD.png) 22 | 23 | 24 | 25 | ## Development 26 | 27 | ### Environment 28 | 29 | - Enter `nix develop` for development environment dependencies. 30 | 31 | ### Documentation 32 | 33 | ```bash 34 | make docs 35 | 36 | # or, build individual packages 37 | make target/doc/deepkey_types/index.html 38 | make target/doc/deepkey_sdk/index.html 39 | make target/doc/deepkey/index.html 40 | make target/doc/deepkey_csr/index.html 41 | ``` 42 | 43 | See [deepkey_csr/index.html](https://holochain.github.io/deepkey/deepkey_csr/index.html) for 44 | detailed API References. 45 | 46 | ### Building 47 | 48 | #### DNA Bundle 49 | 50 | ```bash 51 | make dna/deepkey.dna 52 | ``` 53 | 54 | #### [Deepkey types crate](https://crates.io/crates/hc_deepkey_types) 55 | 56 | Build package without publishing 57 | ```bash 58 | make preview-deepkey-types-crate 59 | ``` 60 | 61 | Publish package 62 | ```bash 63 | make publish-deepkey-types-crate 64 | ``` 65 | 66 | #### [Deepkey SDK crate](https://crates.io/crates/hc_deepkey_sdk) 67 | 68 | Build package without publishing 69 | ```bash 70 | make preview-deepkey-sdk-crate 71 | ``` 72 | 73 | Publish package 74 | ```bash 75 | make publish-deepkey-sdk-crate 76 | ``` 77 | 78 | 79 | ### Testing 80 | 81 | To run all tests with logging 82 | ``` 83 | make test 84 | ``` 85 | 86 | - `make test-unit` - **Rust tests only** 87 | - `make test-integration` - **Integration tests only** 88 | 89 | #### Integration tests 90 | 91 | - `make test-basic` - Test MVP features 92 | - `make test-change-rules` - Test management of change rules 93 | - `make test-key-management` - Test management of key evolution 94 | - `make test-claim-unmanaged-key` - Test ability to handle multiple claims of the same key 95 | 96 | 97 | > **NOTE:** set DEBUG_LEVEL environment variable to run tests with logging (options: fatal, error, 98 | > warn, normal, info, debug, trace) 99 | > 100 | > Example 101 | > ``` 102 | > DEBUG_LEVEL=trace make test 103 | > ``` 104 | 105 | 106 | ### Prospective features 107 | 108 | #### Limbo 109 | Features that were planned but may no longer be implemented. 110 | 111 | - [Multi-device Management](./docs/limbo/Multi_device_management.md) 112 | 113 | 114 | #### Prototype 115 | Features that are planned but may or may not be completed. 116 | 117 | - [Generators](./docs/prototype/Generators.md) 118 | - [Membrane Proof](./docs/prototype/Membrane_proof.md) 119 | 120 | ##### Feature "Claim keys" 121 | `KeyRegistration::CreateOnly` serves the temporary purpose of allowing Holo Hosts to register keys 122 | of web users without being able to manage those keys. This feature could be replaced with adding a 123 | claim key for web users to claim their unmanaged keys if/when they become a self-hosted Holochain 124 | user. 125 | -------------------------------------------------------------------------------- /tests/key_store.js: -------------------------------------------------------------------------------- 1 | import { Logger } from '@whi/weblogger'; 2 | const log = new Logger("key-store", process.env.LOG_LEVEL ); 3 | 4 | import crypto from 'crypto'; 5 | import * as ed from '@noble/ed25519'; 6 | import { hmac } from '@noble/hashes/hmac'; 7 | import { sha256 } from '@noble/hashes/sha256'; 8 | import { Bytes } from '@whi/bytes-class'; 9 | 10 | import { 11 | AgentPubKey, 12 | } from '@spartan-hc/holo-hash'; 13 | 14 | 15 | export class KeyStore { 16 | #device_seed = null; 17 | #name = Buffer.from( crypto.randomBytes( 12 ) ).toString("hex"); 18 | #keys = {}; 19 | 20 | constructor ( device_seed, name ) { 21 | this.#device_seed = new Bytes( device_seed ); 22 | 23 | if ( name ) 24 | this.#name = name; 25 | } 26 | 27 | get name () { 28 | return this.#name; 29 | } 30 | 31 | get seed () { 32 | return this.#device_seed; 33 | } 34 | 35 | async createKey ( path ) { 36 | if ( typeof path !== "string" ) 37 | throw new TypeError(`Path must be a string; not type '${typeof path}'`); 38 | 39 | const secret = hmac( sha256, this.#device_seed, path ); 40 | const key = new Key( secret, Buffer.from(path, "utf8") ) 41 | const agent = await key.getAgent(); 42 | 43 | log.normal("[%s] Created key from derivation path '%s': (agent) %s", this.name, path, agent ); 44 | this.#keys[agent] = key; 45 | 46 | return key; 47 | } 48 | 49 | getKey ( agent ) { 50 | return this.#keys[ new AgentPubKey( agent ) ]; 51 | } 52 | } 53 | 54 | 55 | export class Key { 56 | #secret = null; 57 | #bytes = null; 58 | #derivation_bytes = null; 59 | 60 | constructor ( secret, derivation_bytes ) { 61 | if ( !(secret instanceof Uint8Array) ) 62 | throw new TypeError(`Secret must be a Uint8Array; not type '${secret?.constructor?.name || typeof secret}'`); 63 | if ( secret.length !== 32 ) 64 | throw new Error(`Secret must 32 bytes; not length ${secret.length}`); 65 | 66 | this.#secret = secret; 67 | this.#derivation_bytes = derivation_bytes; 68 | } 69 | 70 | get derivation_bytes () { 71 | return this.#derivation_bytes; 72 | } 73 | 74 | async getBytes () { 75 | if ( this.#bytes === null ) 76 | this.#bytes = await ed.getPublicKeyAsync( this.#secret ); 77 | 78 | return new Uint8Array( this.#bytes ); 79 | } 80 | 81 | async getAgent () { 82 | return new AgentPubKey( await this.getBytes() ); 83 | } 84 | 85 | async sign ( bytes ) { 86 | if ( !(bytes instanceof Uint8Array) ) 87 | throw new TypeError(`Key signing expects a Uint8Array; not type '${bytes?.constructor?.name || typeof bytes}'`); 88 | 89 | return await ed.signAsync( bytes, this.#secret ); 90 | } 91 | } 92 | 93 | 94 | export function random_key () { 95 | const secret = ed.utils.randomPrivateKey(); 96 | return new Key( secret ); 97 | } 98 | 99 | 100 | export default { 101 | KeyStore, 102 | Key, 103 | random_key, 104 | }; 105 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/app_binding.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use deepkey::*; 3 | use serde_bytes::ByteArray; 4 | 5 | use hdk::prelude::*; 6 | use hdk_extensions::{ 7 | must_get, 8 | hdi_extensions::{ 9 | guest_error, 10 | }, 11 | }; 12 | 13 | 14 | #[hdk_extern] 15 | pub fn query_app_binding_records(_: ()) -> ExternResult> { 16 | utils::query_entry_type( EntryTypesUnit::AppBinding ) 17 | } 18 | 19 | #[hdk_extern] 20 | pub fn query_app_bindings(_: ()) -> ExternResult> { 21 | Ok( 22 | query_app_binding_records(())? 23 | .into_iter() 24 | .filter_map( |record| Some(( 25 | record.action_address().to_owned(), 26 | AppBinding::try_from( record ).ok()? 27 | ))) 28 | .collect() 29 | ) 30 | } 31 | 32 | #[hdk_extern] 33 | pub fn query_next_app_index(_: ()) -> ExternResult { 34 | Ok( query_app_bindings(())?.len() as u32 ) 35 | } 36 | 37 | #[hdk_extern] 38 | pub fn query_app_binding_by_index(index: u32) -> ExternResult<(ActionHash, AppBinding)> { 39 | query_app_bindings(())? 40 | .into_iter() 41 | .find( |(_, app_binding)| app_binding.app_index == index ) 42 | .ok_or(guest_error!(format!("No AppBinding with index: {}", index ))) 43 | } 44 | 45 | #[hdk_extern] 46 | pub fn query_app_binding_by_action(addr: ActionHash) -> ExternResult { 47 | Ok( 48 | query_app_bindings(())? 49 | .into_iter() 50 | .find( |(app_binding_addr, _)| *app_binding_addr == addr ) 51 | .ok_or(guest_error!(format!("No AppBinding with action hash: {}", addr )))?.1 52 | ) 53 | } 54 | 55 | #[hdk_extern] 56 | pub fn query_app_bindings_by_installed_app_id(installed_app_id: String) -> ExternResult> { 57 | Ok( 58 | query_app_bindings(())? 59 | .into_iter() 60 | .filter( |(_, app_binding)| app_binding.installed_app_id == installed_app_id ) 61 | .collect() 62 | ) 63 | } 64 | 65 | #[hdk_extern] 66 | pub fn query_app_binding_by_key(key_bytes: ByteArray<32>) -> ExternResult<(ActionHash, AppBinding)> { 67 | let key_meta = crate::key_meta::query_key_meta_for_key( key_bytes )?; 68 | let app_binding = query_app_binding_by_action( key_meta.app_binding_addr.clone() )?; 69 | 70 | debug!("Found AppBinding ({}) for KeyBytes: {:?}", key_meta.app_binding_addr, key_bytes ); 71 | Ok((key_meta.app_binding_addr, app_binding)) 72 | } 73 | 74 | 75 | type KeyInfo = (KeyMeta, KeyRegistration); 76 | type AppKeyInfo = (AppBinding, Vec); 77 | 78 | #[hdk_extern] 79 | pub fn query_key_info(_: ()) -> ExternResult> { 80 | Ok( 81 | query_app_binding_records(())? 82 | .into_iter() 83 | .filter_map( |record| { 84 | let app_binding_addr = record.action_address().to_owned(); 85 | let app_binding = AppBinding::try_from( record ).ok()?; 86 | 87 | let key_infos = crate::key_meta::query_key_metas_for_app_binding( app_binding_addr ).ok()? 88 | .into_iter() 89 | .filter_map( |key_meta| { 90 | let key_reg_addr = key_meta.key_registration_addr.clone(); 91 | 92 | Some(( 93 | key_meta, 94 | KeyRegistration::try_from( 95 | must_get( &key_reg_addr ).ok()? 96 | ).ok()? 97 | )) 98 | }) 99 | .collect::>(); 100 | 101 | Some((app_binding, key_infos)) 102 | }) 103 | .collect::>() 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/keyset_root.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | utils, 3 | key_registration::{ 4 | CreateKeyInput, 5 | AppBindingInput, 6 | DerivationDetailsInput, 7 | }, 8 | }; 9 | use crate::hdi_extensions::{ 10 | guest_error, 11 | ScopedTypeConnector, 12 | }; 13 | use crate::hdk_extensions::{ 14 | agent_id, 15 | must_get, 16 | }; 17 | 18 | use deepkey::*; 19 | use hdk::prelude::*; 20 | 21 | 22 | pub fn create_keyset_root() -> ExternResult { 23 | let fda: AgentPubKey = agent_info()?.agent_latest_pubkey; 24 | let fda_bytes = fda.clone().into_inner(); 25 | 26 | let esigs = sign_ephemeral_raw(vec![ fda_bytes ])?; 27 | let [signed_fda, ..] = esigs.signatures.as_slice() else { 28 | return Err(guest_error!("sign_ephemeral returned wrong number of signatures".to_string())) 29 | }; 30 | 31 | let keyset_root = KeysetRoot::new( 32 | fda.clone(), 33 | esigs.key.get_raw_32().try_into() 34 | .map_err( |e| guest_error!(format!("Failed AgentPubKey to [u8;32] conversion: {:?}", e)) )?, 35 | signed_fda.to_owned() 36 | ); 37 | let create_hash = create_entry( keyset_root.to_input() )?; 38 | 39 | init_change_rule( 1, vec![ 40 | fda.get_raw_32().try_into() 41 | .map_err(|e| guest_error!(format!( 42 | "FDA.get_raw_32() did not have 32 elements; this should be unreachable -> {}", e 43 | )))?, 44 | ])?; 45 | 46 | let dna_hash = dna_info()?.hash; 47 | 48 | // Register the FDA as a key under this KSR 49 | crate::key_registration::create_key(CreateKeyInput { 50 | key_generation: KeyGeneration { 51 | new_key: fda.clone(), 52 | new_key_signing_of_author: sign_raw( fda, agent_id()?.into_inner() )?, 53 | }, 54 | app_binding: AppBindingInput { 55 | app_name: "deepkey".to_string(), 56 | installed_app_id: "deepkey".to_string(), 57 | dna_hashes: vec![ dna_hash ], 58 | metadata: Default::default(), 59 | }, 60 | derivation_details: Some(DerivationDetailsInput { 61 | app_index: 0, 62 | key_index: 0, 63 | derivation_seed: vec![], 64 | derivation_bytes: vec![], 65 | }), 66 | create_only: false, 67 | })?; 68 | 69 | Ok( create_hash ) 70 | } 71 | 72 | 73 | pub fn init_change_rule( 74 | sigs_required: u8, 75 | revocation_keys: Vec 76 | ) -> ExternResult { 77 | let ksr_addr = utils::query_keyset_root_addr()?; 78 | let new_authority_spec = AuthoritySpec::new( 79 | sigs_required, 80 | revocation_keys, 81 | ); 82 | let auth_spec_bytes = utils::serialize( &new_authority_spec )?; 83 | let signed_bytes = sign( agent_id()?, auth_spec_bytes )?; 84 | 85 | let spec_change = AuthorizedSpecChange::new( 86 | new_authority_spec, vec![(0, signed_bytes)] 87 | ); 88 | 89 | let change_rule = ChangeRule::new( 90 | ksr_addr.clone(), 91 | spec_change, 92 | ); 93 | 94 | Ok( crate::change_rule::create_change_rule( change_rule )? ) 95 | } 96 | 97 | 98 | #[hdk_extern] 99 | pub fn get_keyset_root(ksr_addr: ActionHash) -> ExternResult { 100 | must_get( &ksr_addr )?.try_into() 101 | } 102 | 103 | 104 | // Get all of the keys registered on the keyset, across all the deepkey agents 105 | #[hdk_extern] 106 | pub fn query_apps_with_keys(_:()) -> ExternResult)>> { 107 | let key_metas : Vec<(ActionHash, KeyMeta)> = utils::query_entry_type( EntryTypesUnit::KeyMeta )? 108 | .into_iter() 109 | .filter_map( |record| Some(( 110 | record.action_address().to_owned(), 111 | KeyMeta::try_from(record).ok()?, 112 | ))) 113 | .collect(); 114 | 115 | Ok( 116 | utils::query_entry_type( EntryTypesUnit::AppBinding )? 117 | .into_iter() 118 | .filter( |record| record.action().action_type() == ActionType::Create ) 119 | .filter_map( |record| Some(( 120 | record.action_address().to_owned(), 121 | AppBinding::try_from(record).ok()?, 122 | ))) 123 | .map( |(addr, app_binding)| { 124 | ( 125 | app_binding, 126 | key_metas.iter() 127 | .filter( |(_, key_meta)| key_meta.app_binding_addr == addr ) 128 | .map( |(_, key_meta)| key_meta ) 129 | .cloned() 130 | .collect(), 131 | ) 132 | }) 133 | .collect() 134 | ) 135 | } 136 | 137 | 138 | // #[hdk_extern] 139 | // pub fn query_key_registrations(_:()) -> ExternResult)>> { 140 | // } 141 | -------------------------------------------------------------------------------- /docs/limbo/Multi_device_management.md: -------------------------------------------------------------------------------- 1 | [back to CONTRIBUTING.md](../../CONTRIBUTING.md) 2 | 3 | 4 | ## Multi-device Management 5 | 6 | ### Keyset Proof 7 | 8 | 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. 9 | 10 | The first entry the app makes in each user's source chain is a `KeysetRoot`, creating a new keyset space. 11 | 12 | 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. 13 | 14 | (This will be at least the fifth entry in the chain, after the three genesis entries and the `init_complete`.) 15 | 16 | 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. 17 | 18 | 19 | ### Keyset Tree/Leaves 20 | 21 | 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." 22 | 23 | **Each device can only be managed by a single keyset at one time.** 24 | 25 | 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. 26 | 27 | The structure of a `DeviceInvite` (written to the invitor's chain) is: 28 | 29 | - KSR: An `ActionHash` referring to the invitor's KSR. 30 | - Parent: An `ActionHash` referring to the invitor's direct parent in the keyset tree, which is either its KSR or its current `DeviceInviteAcceptance`. This is used to establish the chain of authority from the original KSR. 31 | - Invitee: The `AgentPubKey` being invited. 32 | 33 | The structure of a `DeviceInviteAcceptance` (written to the invitee's chain) is: 34 | 35 | - The `ActionHash` of the KSR. 36 | - The `ActionHash` of the `DeviceInvite`. 37 | 38 | #### Device Invite API 39 | 40 | **Create**: The validation that happens when you create a new `DeviceInvite`: 41 | 42 | - A `DeviceInvite` must deserialize cleanly from the validating record. 43 | - The KSR must be fetched and deserialized into a `KeysetRoot`. 44 | - An invitee must have a different `AgentPubkey` than the invitor. 45 | - If the author of the invitation is the FDA in the invitation's KSR 46 | - Do a hash-bounded query from the invite hash back to the KSR in the invitor's source chain. 47 | - Check that that range contains no invite acceptances (have abandoned the Keyset they are inviting a new device into). 48 | - Else (author of invitation and FDA of KSR are not the same): 49 | - Search from invite backwards (must_get_agent_activity of the invitor), find the first `DeviceInviteAcceptance` in their chain. 50 | - The invite in that `DeviceInviteAcceptance` must fetch and deserialize to a `DeviceInvite`. 51 | - That deserialized `DeviceInvite` must have the same KSR authority as the new `DeviceInvite` currently being validated. 52 | - Also in that `DeviceInvite`, the invitee must be the author of the new `DeviceInvite`. 53 | 54 | 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. 55 | 56 | **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. 57 | 58 | **Update**: Not allowed. 59 | 60 | **Delete**: Not allowed. 61 | 62 | **Zome Calls**: 63 | 64 | - `invite_agent` 65 | - Input is the `AgentPubKey` to invite. 66 | - This agent does not exist on the DHT yet if they are planning to use the invite as their joining proof. 67 | - Output is the exact `DeviceInviteAcceptance` the invitee must commit to their chain. 68 | - Invites are always under the current keyset. 69 | 70 | #### Device Invite Acceptance API 71 | 72 | **Create**: 73 | 74 | - A `DeviceInviteAcceptance` must deserialize cleanly from the validating record 75 | - A `DeviceInvite` must be fetched and deserialize from the `invite` action hash on the `DeviceInviteAcceptance` 76 | - The author of the `DeviceInviteAcceptance` must be the referenced `AgentPubKey` on the `DeviceInvite` 77 | - The `KeysetRoot` must be the same on both the `DeviceInvite` and the `DeviceInviteAcceptance` 78 | 79 | **Read**: No exposed zome calls for read or lookup. For validation, the most recent `DeviceInviteAcceptance` is used to determine the current keyset. 80 | 81 | **Update**: Not allowed. 82 | 83 | **Delete**: Not allowed. 84 | 85 | ##### Zome Calls 86 | 87 | - `accept_invite` 88 | - input is a `DeviceInviteAcceptance` 89 | - output is the `ActionHash` of the entry created 90 | - creates the entry as-is from input 91 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: FORCE 2 | 3 | INT_DIR = zomes/deepkey 4 | CSR_DIR = zomes/deepkey_csr 5 | 6 | # DNAs 7 | DEEPKEY_DNA = dnas/deepkey.dna 8 | 9 | # Integrity Zomes 10 | DEEPKEY_WASM = zomes/deepkey.wasm 11 | 12 | # Coordinator WASMs 13 | DEEPKEY_CSR_WASM = zomes/deepkey_csr.wasm 14 | 15 | 16 | TARGET = release 17 | TARGET_DIR = target/wasm32-unknown-unknown/release 18 | COMMON_SOURCE_FILES = Makefile Cargo.toml 19 | INT_SOURCE_FILES = $(COMMON_SOURCE_FILES) \ 20 | $(INT_DIR)/Cargo.toml $(INT_DIR)/src/*.rs $(INT_DIR)/src/validation/*.rs 21 | CSR_SOURCE_FILES = $(INT_SOURCE_FILES) \ 22 | $(CSR_DIR)/Cargo.toml $(CSR_DIR)/src/*.rs 23 | DNA_CRATE_DNA_SRC = crates/holochain_deepkey_dna/src/deepkey.dna 24 | 25 | 26 | # 27 | # Project 28 | # 29 | $(DEEPKEY_DNA): $(DEEPKEY_WASM) $(DEEPKEY_CSR_WASM) 30 | 31 | dnas/%.dna: dnas/%/dna.yaml 32 | @echo "Packaging '$*': $@" 33 | @hc dna pack -o $@ dnas/$* 34 | cp dnas/$*.dna crates/holochain_deepkey_dna/src 35 | 36 | zomes: 37 | mkdir $@ 38 | zomes/%.wasm: $(TARGET_DIR)/%.wasm 39 | @echo -e "\x1b[38;2mCopying WASM ($<) to 'zomes' directory: $@\x1b[0m"; \ 40 | cp $< $@ 41 | 42 | $(TARGET_DIR)/%.wasm: $(INT_SOURCE_FILES) 43 | rm -f zomes/$*.wasm 44 | @echo -e "\x1b[37mBuilding zome '$*' -> $@\x1b[0m"; 45 | RUST_BACKTRACE=1 cargo build --release \ 46 | --target wasm32-unknown-unknown \ 47 | --package $* 48 | @touch $@ # Cargo must have a cache somewhere because it doesn't update the file time 49 | 50 | $(TARGET_DIR)/%_csr.wasm: $(CSR_SOURCE_FILES) 51 | rm -f zomes/$*_csr.wasm 52 | @echo -e "\x1b[37mBuilding zome '$*_csr' -> $@\x1b[0m"; 53 | RUST_BACKTRACE=1 cargo build --release \ 54 | --target wasm32-unknown-unknown \ 55 | --package $*_csr 56 | @touch $@ # Cargo must have a cache somewhere because it doesn't update the file time 57 | 58 | 59 | GG_REPLACE_LOCATIONS = ':(exclude)*.lock' zomes/ dnas/ tests/ 60 | 61 | # update-tracked-files: 62 | # git grep -l 'dna_binding' -- $(GG_REPLACE_LOCATIONS) | xargs sed -i 's|dna_binding|app_binding|g' 63 | 64 | npm-reinstall-local: 65 | cd tests; npm uninstall $(NPM_PACKAGE); npm i --save-dev $(LOCAL_PATH) 66 | npm-reinstall-public: 67 | cd tests; npm uninstall $(NPM_PACKAGE); npm i --save-dev $(NPM_PACKAGE) 68 | 69 | npm-use-app-interface-client-public: 70 | npm-use-app-interface-client-local: 71 | npm-use-app-interface-client-%: 72 | NPM_PACKAGE=@spartan-hc/app-interface-client LOCAL_PATH=../../app-interface-client-js make npm-reinstall-$* 73 | 74 | npm-use-backdrop-public: 75 | npm-use-backdrop-local: 76 | npm-use-backdrop-%: 77 | NPM_PACKAGE=@spartan-hc/holochain-backdrop LOCAL_PATH=../../node-backdrop make npm-reinstall-$* 78 | 79 | 80 | 81 | # 82 | # Testing 83 | # 84 | DEBUG_LEVEL ?= warn 85 | TEST_ENV_VARS = LOG_LEVEL=$(DEBUG_LEVEL) 86 | MOCHA_OPTS = -n enable-source-maps -t 5000 87 | TEST_DEPS = node_modules dnas/deepkey/zomelets/node_modules 88 | 89 | %/package-lock.json: %/package.json 90 | touch $@ 91 | package-lock.json: package.json 92 | touch $@ 93 | %/node_modules: %/package-lock.json 94 | cd $*; npm install 95 | touch $@ 96 | node_modules: package-lock.json 97 | npm install 98 | touch $@ 99 | 100 | test: 101 | make -s test-unit 102 | make -s test-integration 103 | 104 | test-unit: 105 | RUST_BACKTRACE=1 CARGO_TARGET_DIR=target cargo test -- --nocapture --show-output 106 | 107 | test-integration: 108 | make -s test-integration-basic 109 | make -s test-integration-change-rules 110 | make -s test-integration-key-management 111 | 112 | test-integration-basic: $(DEEPKEY_DNA) $(TEST_DEPS) 113 | cd tests; $(TEST_ENV_VARS) npx mocha $(MOCHA_OPTS) ./integration/test_basic.js 114 | test-integration-change-rules: $(DEEPKEY_DNA) $(TEST_DEPS) 115 | cd tests; $(TEST_ENV_VARS) npx mocha $(MOCHA_OPTS) ./integration/test_change_rules.js 116 | test-integration-key-management: $(DEEPKEY_DNA) $(TEST_DEPS) 117 | cd tests; $(TEST_ENV_VARS) npx mocha $(MOCHA_OPTS) ./integration/test_key_management.js 118 | test-integration-claim-unmanaged-key: $(DEEPKEY_DNA) $(TEST_DEPS) 119 | cd tests; $(TEST_ENV_VARS) npx mocha $(MOCHA_OPTS) ./integration/test_claim_unmanaged_key.js 120 | 121 | 122 | # 123 | # Documentation 124 | # 125 | DEEPKEY_DOCS = target/doc/deepkey/index.html 126 | DEEPKEY_CSR_DOCS = target/doc/deepkey_csr/index.html 127 | DEEPKEY_TYPES_DOCS = target/doc/deepkey_types/index.html 128 | DEEPKEY_SDK_DOCS = target/doc/deepkey_sdk/index.html 129 | 130 | target/doc/%/index.html: zomes/%/src/** 131 | cargo test --doc -p $* 132 | cargo doc --no-deps -p $* 133 | @echo -e "\x1b[37mOpen docs in file://$(shell pwd)/$@\x1b[0m"; 134 | 135 | $(DEEPKEY_TYPES_DOCS): dnas/deepkey/types/src/** 136 | cargo doc --no-deps -p hc_deepkey_types 137 | $(DEEPKEY_SDK_DOCS): dnas/deepkey/sdk/src/** 138 | cargo doc --no-deps -p hc_deepkey_sdk 139 | 140 | docs: FORCE 141 | make $(DEEPKEY_CSR_DOCS) $(DEEPKEY_DOCS) 142 | make $(DEEPKEY_TYPES_DOCS) $(DEEPKEY_SDK_DOCS) 143 | 144 | docs-watch: 145 | @inotifywait -r -m -e modify \ 146 | --includei '.*\.rs' \ 147 | zomes/ \ 148 | dnas/deepkey/types \ 149 | dnas/deepkey/sdk \ 150 | | while read -r dir event file; do \ 151 | echo -e "\x1b[37m$$event $$dir$$file\x1b[0m";\ 152 | make docs; \ 153 | done 154 | 155 | 156 | # 157 | # Publishing Types Packages 158 | # 159 | .cargo/credentials: 160 | mkdir -p .cargo 161 | cp ~/$@ $@ 162 | preview-%-types-crate: .cargo/credentials 163 | cd dnas/$*; make preview-types-crate 164 | publish-%-types-crate: .cargo/credentials 165 | cd dnas/$*; make publish-types-crate 166 | 167 | preview-deepkey-types-crate: 168 | publish-deepkey-types-crate: 169 | 170 | 171 | preview-%-sdk-crate: .cargo/credentials 172 | cd dnas/$*; make preview-sdk-crate 173 | publish-%-sdk-crate: .cargo/credentials 174 | cd dnas/$*; make publish-sdk-crate 175 | 176 | preview-deepkey-sdk-crate: 177 | publish-deepkey-sdk-crate: 178 | 179 | preview-deepkey-dna-crate: .cargo/credentials $(DNA_CRATE_DNA_SRC) 180 | cargo publish -p holochain_deepkey_dna --dry-run --allow-dirty 181 | publish-deepkey-dna-crate: .cargo/credentials $(DNA_CRATE_DNA_SRC) 182 | cargo publish -p holochain_deepkey_dna --allow-dirty 183 | 184 | $(DNA_CRATE_DNA_SRC): $(DEEPKEY_DNA) 185 | cp $< $@ 186 | -------------------------------------------------------------------------------- /tests/integration/test_basic.js: -------------------------------------------------------------------------------- 1 | import { Logger } from '@whi/weblogger'; 2 | const log = new Logger("test-basic", process.env.LOG_LEVEL ); 3 | 4 | // import why from 'why-is-node-running'; 5 | 6 | import path from 'path'; 7 | import crypto from 'crypto'; 8 | 9 | import { expect } from 'chai'; 10 | 11 | import json from '@whi/json'; 12 | import { 13 | HoloHash, 14 | DnaHash, AgentPubKey, 15 | ActionHash, EntryHash, 16 | } from '@spartan-hc/holo-hash'; 17 | import { Holochain } from '@spartan-hc/holochain-backdrop'; 18 | 19 | import { 20 | DeepKeyCell, 21 | } from '@holochain/deepkey-zomelets'; 22 | import { 23 | AppInterfaceClient, 24 | } from '@spartan-hc/app-interface-client'; 25 | 26 | import { 27 | expect_reject, 28 | linearSuite, 29 | } from '../utils.js'; 30 | import { 31 | KeyStore, 32 | } from '../key_store.js'; 33 | 34 | 35 | const __dirname = path.dirname( new URL(import.meta.url).pathname ); 36 | const DEEPKEY_DNA_PATH = path.join( __dirname, "../../dnas/deepkey.dna" ); 37 | const DEEPKEY_DNA_NAME = "deepkey"; 38 | 39 | const dna1_hash = new DnaHash( crypto.randomBytes( 32 ) ); 40 | 41 | const ALICE1_DEVICE_SEED = Buffer.from("jJQhp80zPT+XBMOZmtfwdBqY9ay9k2w520iwaet1if4=", "base64"); 42 | 43 | const alice1_app1_id = "alice1-app1"; 44 | const alice1_key_store = new KeyStore( ALICE1_DEVICE_SEED, "alice1" ); 45 | 46 | let app_port; 47 | let installations; 48 | let app_count = 1; 49 | 50 | function next_app_index () { 51 | return app_count++; 52 | } 53 | 54 | 55 | describe("DeepKey", function () { 56 | const holochain = new Holochain({ 57 | "timeout": 20_000, 58 | "default_stdout_loggers": log.level_rank > 3, 59 | }); 60 | 61 | before(async function () { 62 | this.timeout( 60_000 ); 63 | 64 | installations = await holochain.install([ 65 | "alice1", 66 | ], { 67 | "app_name": "test", 68 | "bundle": { 69 | [DEEPKEY_DNA_NAME]: DEEPKEY_DNA_PATH, 70 | }, 71 | "membrane_proofs": { 72 | [DEEPKEY_DNA_NAME]: { 73 | "joining_proof": crypto.randomBytes( 32 ), 74 | }, 75 | }, 76 | }); 77 | 78 | app_port = await holochain.ensureAppPort(); 79 | }); 80 | 81 | linearSuite("Basic", basic_tests ); 82 | 83 | after(async () => { 84 | await holochain.destroy(); 85 | }); 86 | }); 87 | 88 | 89 | function basic_tests () { 90 | let client; 91 | let alice1_client; 92 | let deepkey; 93 | let alice1_deepkey; 94 | let ksr1_addr; 95 | 96 | before(async function () { 97 | this.timeout( 30_000 ); 98 | 99 | client = new AppInterfaceClient( app_port, { 100 | "logging": process.env.LOG_LEVEL || "normal", 101 | }); 102 | 103 | const alice1_token = installations.alice1.test.auth.token; 104 | alice1_client = await client.app( alice1_token ); 105 | 106 | ({ 107 | deepkey, 108 | } = alice1_client.createInterface({ 109 | [DEEPKEY_DNA_NAME]: DeepKeyCell, 110 | })); 111 | 112 | alice1_deepkey = deepkey.zomes.deepkey_csr.functions; 113 | 114 | ksr1_addr = await alice1_deepkey.query_keyset_authority_action_hash(); 115 | }); 116 | 117 | it("should query keyset root action hash", async function () { 118 | await alice1_deepkey.query_keyset_root_action_hash(); 119 | }); 120 | 121 | it("should get KSR keys (1)", async function () { 122 | const keys = await alice1_deepkey.get_ksr_keys( ksr1_addr ); 123 | log.normal("KSR keys: %s", json.debug(keys) ); 124 | 125 | expect( keys ).to.have.length( 1 ); 126 | }); 127 | 128 | it("should register new key", async function () { 129 | this.timeout( 5_000 ); 130 | 131 | const app_index = next_app_index(); 132 | const key_index = 0; 133 | const path = `app/${app_index}/key/${key_index}`; 134 | const new_key = await alice1_key_store.createKey( path ); 135 | 136 | const [ addr, key_reg, key_meta ] = await alice1_deepkey.create_key({ 137 | "app_binding": { 138 | "app_name": "Alice1 - App #1", 139 | "installed_app_id": "alice1-app1", 140 | "dna_hashes": [ dna1_hash ], 141 | }, 142 | "key_generation": { 143 | "new_key": await new_key.getAgent(), 144 | "new_key_signing_of_author": await new_key.sign( alice1_client.agent_id ), 145 | }, 146 | "derivation_details": { 147 | app_index, 148 | key_index, 149 | "derivation_seed": alice1_key_store.seed, 150 | "derivation_bytes": new_key.derivation_bytes, 151 | }, 152 | }); 153 | log.normal("Key Registration (%s): %s", addr, json.debug(key_reg) ); 154 | log.normal("Key Meta: %s", json.debug(key_meta) ); 155 | log.normal("Key registration (update) addr: %s", addr ); 156 | }); 157 | 158 | it("should get KSR keys (2)", async function () { 159 | const keys = await alice1_deepkey.get_ksr_keys( ksr1_addr ); 160 | log.normal("KSR keys: %s", json.debug(keys) ); 161 | 162 | expect( keys ).to.have.length( 2 ); 163 | }); 164 | 165 | after(async function () { 166 | await client.close(); 167 | }); 168 | } 169 | -------------------------------------------------------------------------------- /zomes/deepkey/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | EntryTypesUnit, 3 | 4 | KeysetRoot, 5 | ChangeRule, 6 | 7 | KeyBytes, 8 | Authorization, 9 | }; 10 | use rmp_serde; 11 | use hdi_extensions::{ 12 | guest_error, 13 | }; 14 | use hdi::prelude::*; 15 | 16 | 17 | pub fn serialize(target: &T) -> ExternResult> 18 | where 19 | T: Serialize + ?Sized, 20 | { 21 | rmp_serde::encode::to_vec( target ) 22 | .map_err( |err| guest_error!(format!( 23 | "Failed to serialize target: {:?}", err 24 | )) ) 25 | } 26 | 27 | 28 | pub fn get_activities_for_entry_type( 29 | entry_type_unit: T, 30 | author: &AgentPubKey, 31 | chain_top: &ActionHash, 32 | ) -> ExternResult> 33 | where 34 | T: Copy, 35 | EntryType: TryFrom, 36 | WasmError: From, 37 | { 38 | debug!("Getting agent activity for {} (chain top: {})", author, chain_top ); 39 | let activities = must_get_agent_activity( 40 | author.to_owned(), 41 | ChainFilter::new( chain_top.to_owned() ) 42 | .include_cached_entries() 43 | )?; 44 | 45 | debug!("Found {} activities", activities.len() ); 46 | let entry_type = EntryType::try_from( entry_type_unit )?; 47 | 48 | let filtered_activities : Vec = activities.into_iter().filter( 49 | |activity| match activity.action.action().entry_type() { 50 | Some(et) => et == &entry_type, 51 | None => false, 52 | } 53 | ).collect(); 54 | debug!("Found {} activities for entry type: {}", filtered_activities.len(), entry_type ); 55 | 56 | Ok( filtered_activities ) 57 | } 58 | 59 | 60 | pub fn get_latest_activity_for_entry_type( 61 | entry_type_unit: T, 62 | author: &AgentPubKey, 63 | chain_top: &ActionHash, 64 | ) -> ExternResult> 65 | where 66 | T: Copy, 67 | EntryType: TryFrom, 68 | WasmError: From, 69 | { 70 | let activities = get_activities_for_entry_type( 71 | entry_type_unit, 72 | author, 73 | chain_top, 74 | )?; 75 | 76 | Ok(activities.first().cloned()) 77 | } 78 | 79 | 80 | pub fn get_keyset_root( 81 | author: &AgentPubKey, 82 | chain_top: &ActionHash, 83 | ) -> ExternResult<(SignedActionHashed, KeysetRoot)> { 84 | let keyset_root_activity = get_latest_activity_for_entry_type( 85 | EntryTypesUnit::KeysetRoot, 86 | &author, 87 | &chain_top, 88 | )?; 89 | 90 | if let Some(activity) = keyset_root_activity { 91 | let entry_hash = activity.action.action().entry_hash() 92 | .ok_or(guest_error!(format!( 93 | "Expected action (seq: {}) to have an entry hash", 94 | activity.action.action().action_seq() 95 | )))?; 96 | 97 | Ok(( 98 | activity.action.clone(), 99 | must_get_entry( entry_hash.to_owned() )?.try_into()? 100 | )) 101 | } 102 | else { 103 | Err(guest_error!(format!("Author ({}) chain is missing a KeysetRoot", author ))) 104 | } 105 | } 106 | 107 | 108 | pub fn prev_change_rule ( 109 | author: &AgentPubKey, 110 | chain_top: &ActionHash 111 | ) -> ExternResult> { 112 | let latest_change_rule = get_latest_activity_for_entry_type( 113 | EntryTypesUnit::ChangeRule, 114 | author, 115 | chain_top, 116 | )?; 117 | 118 | debug!("Latest ChangeRule activity: {:?}", latest_change_rule ); 119 | Ok( 120 | match latest_change_rule { 121 | Some(activity) => { 122 | debug!("ChangeRule cached entry: {:?}", activity.cached_entry ); 123 | Some(match activity.cached_entry { 124 | Some(entry) => entry, 125 | None => must_get_entry( 126 | activity.action.action().entry_hash().unwrap().to_owned() 127 | )?.content, 128 | }.try_into()?) 129 | }, 130 | None => None, 131 | } 132 | ) 133 | } 134 | 135 | 136 | pub fn base_change_rule ( 137 | author: &AgentPubKey, 138 | chain_top: &ActionHash 139 | ) -> ExternResult { 140 | let change_rules = get_activities_for_entry_type( 141 | EntryTypesUnit::ChangeRule, 142 | author, 143 | chain_top, 144 | )?; 145 | 146 | let filtered_activities : Vec = change_rules.into_iter().filter( 147 | |activity| activity.action.action().action_type() == ActionType::Create 148 | ).collect(); 149 | 150 | Ok( 151 | filtered_activities.first() 152 | .ok_or(guest_error!(format!( 153 | "There is no ChangeRule create action on source chain ({}) with chain type: {}", 154 | author, chain_top, 155 | )))? 156 | .to_owned() 157 | .action 158 | ) 159 | } 160 | 161 | 162 | pub fn check_authorities( 163 | authorities: &Vec, 164 | authorizations: &Vec, 165 | signed_content: &Vec, 166 | ) -> ExternResult { 167 | let mut sig_count : u8 = 0; 168 | 169 | debug!( 170 | "Checking {} authorization signature(s) out of {} authoritie(s)", 171 | authorizations.len(), 172 | authorities.len(), 173 | ); 174 | for (auth_index, signature) in authorizations { 175 | let pubkey_bytes = authorities.get( *auth_index as usize ) 176 | .ok_or(guest_error!(format!( 177 | "Auth index ({}) doesn't exist in authorities list: {:#?}", 178 | auth_index, authorities, 179 | )))?; 180 | 181 | let authority = AgentPubKey::from_raw_32( pubkey_bytes.to_owned().to_vec() ); 182 | debug!( 183 | "Checking signature against authority: {}", 184 | authority, 185 | ); 186 | if verify_signature_raw( 187 | authority, 188 | signature.to_owned(), 189 | signed_content.to_owned(), 190 | )? == false { 191 | Err(guest_error!("Authorization has invalid signature".to_string()))? 192 | } 193 | 194 | sig_count += 1; 195 | } 196 | 197 | Ok( sig_count ) 198 | } 199 | 200 | 201 | pub fn keybytes_from_agentpubkey( 202 | agent: &AgentPubKey, 203 | ) -> ExternResult { 204 | agent.get_raw_32().try_into() 205 | .map_err( |e| wasm_error!(WasmErrorInner::Guest(format!( 206 | "Failed AgentPubKey to [u8;32] conversion: {:?}", e 207 | ))) ) 208 | } 209 | -------------------------------------------------------------------------------- /INTEGRITY_MODEL.md: -------------------------------------------------------------------------------- 1 | [back to README.md](README.md) 2 | 3 | 4 | # Integrity Model 5 | 6 | The purpose of this document is to describe the intentions that guide the development of Deepkey's 7 | integrity zome. It assumes an understanding of the purposes and use-cases of Deepkey so that the 8 | validation criteria can be concise and comprehensible. See [README.md](README.md) for more context. 9 | 10 | ### Entity Relationship Diagram (high-level) 11 | 12 | ![](docs/images/ERD.png) 13 | 14 | 15 | ## `KeysetRoot` Validation 16 | 17 | ### Create 18 | 19 | Validation criteria 20 | 21 | - The record must deserialize into the correct struct 22 | - Must be created at index 3 (4th item) in the author's chain. 23 | - This ensures there could only be one create per-chain 24 | - The author must be the FDA. 25 | - The signature (ie. `signed_fda`) must be authored by the root/ephemeral key (ie. `root_pub_key`) 26 | signing the FDA (ie. `first_deepkey_agent`). 27 | 28 | 29 | ### Read 30 | Allowed 31 | 32 | ### Update 33 | Not allowed. 34 | 35 | ### Delete 36 | Not allowed. 37 | 38 | 39 | 40 | ## `ChangeRule` Validation 41 | 42 | Summary of the change rule entry 43 | 44 | - **KSR** - identifies which Keyest root the change rules affect 45 | - **Spec change** (ie. `AuthorizedSpecChange`) 46 | - **New spec** (ie. `AuthoritySpec`) - describes the authority/ies required for change that 47 | include the number of signatures required, and the public keys that are allowed to sign for the 48 | authorization. 49 | - **Number of signature required** - the value of M for "M of N" signing 50 | - **Authorized signers** - the list of public keys that make the value of N for "M of N" signing 51 | - **Authorization of new spec** (ie. `Authorization`) - list of authorizations from current 52 | authorized signers signers 53 | - an index indicating which key was used to make the signature 54 | - the signature of the new spec's serialized bytes signed by a current authorized signer 55 | 56 | ### Create 57 | 58 | Validation criteria 59 | 60 | - The record must deserialize into the correct struct 61 | - Must be created at index 4 (5th item) in the author's chain. 62 | - This ensures there could only be one create per-chain 63 | - The previous record will be the KSR create 64 | - The author must be the FDA. 65 | - The new spec is a 1 of 1 where the FDA is the only authorized signer 66 | 67 | ### Read 68 | Allowed 69 | 70 | ### Update 71 | 72 | Validation criteria 73 | 74 | - The record must deserialize into the correct struct 75 | - The `original_action_hash` must be the create record on the same source chain 76 | - The `keyset_root` cannot be changed 77 | - New spec requirements 78 | - The signatures required cannot be 0 79 | - The signatures required cannot be more than the number of authorized signers 80 | - Authorization requirements - *the 'new spec' from the previous change rule is applied for this 81 | change* 82 | - The 'number of authorizations' for the new spec exceeds the 'signatures required' by the 83 | previous change rule spec 84 | - Each authorization has a valid signature matching an authorized signer from the previous change 85 | rule 86 | - Signed content will be the new `authority_spec` 87 | 88 | ### Delete 89 | Not allowed. 90 | 91 | 92 | 93 | ## `KeyRegistration` Validation 94 | 95 | > **Note:** Key generation and key revocation always use the same validation logic regardless of 96 | > which variant they are in. 97 | 98 | ### Key generation 99 | 100 | Validation criteria 101 | 102 | - Signature of action author by new key is valid 103 | 104 | ### Key revocation 105 | *The latest change rule spec is applied* 106 | 107 | Validation criteria 108 | 109 | - The 'number of authorizations' exceeds the 'signatures required' by the change rule spec 110 | - Each authorization has a valid signature matching an authorized signer from the change rule spec 111 | - Signed content will be the prior key registration action address 112 | 113 | ### Create 114 | 115 | Validation criteria 116 | 117 | - The record must deserialize into the correct struct 118 | - Must be a `Create` or `CreateOnly` variant 119 | - [Key generation requirements](#key-generation) 120 | 121 | ### Read 122 | Allowed 123 | 124 | ### Update 125 | 126 | Validation criteria 127 | 128 | - The record must deserialize into the correct struct 129 | - Must be a `Update` or `Delete` variant 130 | - The `original_action_hash` must be the `prior_key_registration` of the revocation 131 | - There must not be any other `KeyRegistration` on the same source chain referencing the same 132 | `prior_key_registration` 133 | - [Key revocation requirements](#key-revocation) 134 | - If variant is an `Update` 135 | - [Key generation requirements](#key-generation) 136 | 137 | ### Delete 138 | Not allowed. 139 | 140 | 141 | 142 | ## `KeyAnchor` Validation 143 | 144 | CRUD operations must always be performed in the correct sequence. Validation will enforce that the 145 | `KeyAnchor` is always preceded by its `KeyRegistration`. 146 | 147 | ### Create 148 | 149 | Validation criteria 150 | 151 | - The record must deserialize into the correct struct 152 | - The previous action must be the `KeyRegistration` that generated this key 153 | - The key registration must be a `Create` or `CreateOnly` variant 154 | 155 | ### Read 156 | Allowed 157 | 158 | > Key anchor addresses are designed to be deterministic using the core 32 bytes of a key 159 | 160 | ### Update 161 | 162 | Validation criteria 163 | 164 | - The record must deserialize into the correct struct 165 | - The previous action must be the `KeyRegistration` that generated this key 166 | - The key registration must be the `Update` variant 167 | - The key registration's revoked address must match the `original_action_hash` 168 | 169 | ### Delete 170 | 171 | Validation criteria 172 | 173 | - The previous action must be the `KeyRegistration` that is deleting this key 174 | - The key registration must be the `Delete` variant 175 | - The key registration's revoked address must match the `original_action_hash` 176 | 177 | 178 | 179 | ## `AppBinding` Validation 180 | 181 | ### Create 182 | 183 | Validation criteria 184 | 185 | - The app index should be an increment of 1 based on the latest `AppBinding` on the same source 186 | chain 187 | 188 | ### Read 189 | Requires access to the source chain because it is a private entry. 190 | 191 | ### Update 192 | Not allowed. 193 | 194 | ### Delete 195 | Not allowed. 196 | 197 | 198 | 199 | ## `KeyMeta` Validation 200 | Records the derivation path and index used to generate a previously registered key. 201 | 202 | ### Create 203 | 204 | Validation criteria 205 | 206 | - The key index should be an increment of 1 based on the previous `KeyMeta` for the same app binding 207 | - The referenced key anchor should be the one created by the referenced key registration 208 | 209 | ### Read 210 | Requires access to the source chain because it is a private entry. 211 | 212 | ### Update 213 | Not allowed. 214 | 215 | ### Delete 216 | Not allowed. 217 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation/create_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | EntryTypes, 3 | 4 | KeysetRoot, 5 | KeyAnchor, 6 | KeyRegistration, 7 | KeyGeneration, 8 | 9 | utils, 10 | }; 11 | 12 | use hdi::prelude::*; 13 | use hdi_extensions::{ 14 | summon_app_entry, 15 | 16 | // Macros 17 | valid, invalid, 18 | guest_error, 19 | }; 20 | 21 | const KEYSET_ROOT_ACTION_SEQ : u32 = 3; 22 | const CHANGE_RULE_ACTION_SEQ : u32 = 4; 23 | 24 | 25 | pub fn validation( 26 | app_entry: EntryTypes, 27 | create: Create 28 | ) -> ExternResult { 29 | match app_entry { 30 | EntryTypes::KeysetRoot(ksr_entry) => { 31 | // Check action seq 32 | if create.action_seq != KEYSET_ROOT_ACTION_SEQ { 33 | invalid!(format!( 34 | "KeysetRoot has invalid chain index ({}); must be chain index {}", 35 | create.action_seq, KEYSET_ROOT_ACTION_SEQ, 36 | )); 37 | } 38 | 39 | // Check signature matches root pub key 40 | if verify_signature_raw( 41 | ksr_entry.root_pub_key_as_agent(), 42 | ksr_entry.signed_fda, 43 | ksr_entry.first_deepkey_agent.clone().into_inner() 44 | )? == false { 45 | invalid!("KeysetRoot has invalid signature".to_string()); 46 | } 47 | 48 | // Check that FDA is the chain author 49 | if create.author != ksr_entry.first_deepkey_agent { 50 | invalid!(format!( 51 | "KeysetRoot expected FDA to be '{}', not '{}'; FDA must be the action author", 52 | create.author, ksr_entry.first_deepkey_agent, 53 | )); 54 | } 55 | 56 | valid!() 57 | }, 58 | EntryTypes::ChangeRule(change_rule_entry) => { 59 | // Check action seq 60 | if create.action_seq != CHANGE_RULE_ACTION_SEQ { 61 | invalid!(format!( 62 | "ChangeRule has invalid chain index ({}); must be chain index {}", 63 | create.action_seq, CHANGE_RULE_ACTION_SEQ, 64 | )); 65 | } 66 | 67 | // KeysetRoot originates in this chain (perhaps it should also be the previous action) 68 | if create.prev_action != change_rule_entry.keyset_root { 69 | invalid!(format!( 70 | "Change rule keyset root ({}) does not belong to this chain's KSR '{}'", 71 | change_rule_entry.keyset_root, 72 | create.prev_action, 73 | )) 74 | } 75 | 76 | // No signature checks are required because the author of the change rule will always 77 | // match the KSR's FDA. Therefore the internal Holochain validation ensures that the 78 | // action could only be created by the current change rule authority (ie. the FDA). 79 | 80 | // The new spec must be 1 of 1 with the FDA as the authority 81 | let ksr_record = must_get_valid_record( create.prev_action )?; 82 | let ksr : KeysetRoot = ksr_record.try_into()?; 83 | let fda_key_bytes = utils::keybytes_from_agentpubkey( &ksr.first_deepkey_agent )?; 84 | let new_spec = change_rule_entry.spec_change.new_spec; 85 | 86 | if new_spec.sigs_required != 1 { 87 | invalid!(format!( 88 | "The initial change rule must require 1 signature; not '{}'", 89 | new_spec.sigs_required, 90 | )) 91 | } 92 | 93 | if new_spec.authorized_signers.len() != 1 { 94 | invalid!(format!( 95 | "The initial change rule must have 1 authority that is the FDA; not '{}'", 96 | new_spec.authorized_signers.len(), 97 | )) 98 | } 99 | 100 | if new_spec.authorized_signers[0] != fda_key_bytes { 101 | invalid!(format!( 102 | "The initial change rule must have 1 authority that is the FDA ({}); not '{}'", 103 | ksr.first_deepkey_agent, 104 | AgentPubKey::from_raw_32( new_spec.authorized_signers[0].to_vec() ), 105 | )) 106 | } 107 | 108 | valid!() 109 | }, 110 | EntryTypes::KeyRegistration(key_registration_entry) => { 111 | match key_registration_entry { 112 | KeyRegistration::Create( key_gen ) => { 113 | validate_key_generation( &key_gen, &create.into() )?; 114 | 115 | valid!() 116 | }, 117 | KeyRegistration::CreateOnly( key_gen ) => { 118 | validate_key_generation( &key_gen, &create.into() )?; 119 | 120 | valid!() 121 | }, 122 | KeyRegistration::Update(..) | 123 | KeyRegistration::Delete(..) => { 124 | invalid!("KeyRegistration enum must be 'Create' or 'CreateOnly'; not 'Update' or 'Delete'".to_string()) 125 | }, 126 | } 127 | }, 128 | EntryTypes::KeyAnchor(key_anchor_entry) => { 129 | // Check previous action is a key registration that matches this key anchor 130 | let key_reg : KeyRegistration = summon_app_entry( &create.prev_action.into() )?; 131 | 132 | let key_gen = match key_reg { 133 | KeyRegistration::Create(key_gen) => key_gen, 134 | KeyRegistration::CreateOnly(key_gen) => key_gen, 135 | _ => invalid!(format!("KeyAnchor create must be preceeded by a KeyRegistration::[Create | CreateOnly]")), 136 | }; 137 | 138 | if KeyAnchor::try_from( &key_gen.new_key )? != key_anchor_entry { 139 | invalid!(format!( 140 | "KeyAnchor does not match KeyRegistration new key: {:#?} != {}", 141 | key_anchor_entry, key_gen.new_key, 142 | )) 143 | } 144 | 145 | valid!() 146 | }, 147 | 148 | // Private Records 149 | EntryTypes::KeyMeta(_key_meta_entry) => { 150 | valid!() 151 | }, 152 | EntryTypes::AppBinding(_app_binding_entry) => { 153 | valid!() 154 | }, 155 | 156 | // _ => invalid!(format!("Create validation not implemented for entry type: {:#?}", create.entry_type )), 157 | } 158 | } 159 | 160 | 161 | pub fn validate_key_generation(key_gen: &KeyGeneration, creation: &EntryCreationAction) -> ExternResult<()> { 162 | // KeyGeneration { 163 | // new_key: AgentPubKey, 164 | // new_key_signing_of_author: Signature, 165 | // } 166 | 167 | // Signature matches author 168 | if verify_signature_raw( 169 | key_gen.new_key.to_owned(), 170 | key_gen.new_key_signing_of_author.to_owned(), 171 | creation.author().get_raw_39().to_vec(), 172 | )? == false { 173 | Err(guest_error!(format!( 174 | "Signature does not match new key ({})", 175 | key_gen.new_key, 176 | )))?; 177 | } 178 | 179 | Ok(()) 180 | } 181 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Welcome! 2 | //! 3 | //! - Source code - [github.com/holochain/deepkey](https://github.com/holochain/deepkey) 4 | //! 5 | //! 6 | //! ## Usage 7 | //! 8 | //! > **DISCLAIMER:** The real use-case will always be through a client connecting to Holochain's 9 | //! Conductor API. These examples are to help devs contributing to deepkey understand the rust 10 | //! code, though they may also be useful for understanding the sequence of events. 11 | //! 12 | //! For more information about usage, see the 13 | //! [`README.md`](https://github.com/holochain/deepkey/blob/main/README.md) file in the source code 14 | //! repository. 15 | //! 16 | //! ### Minimal usage 17 | //! 18 | //! 1. [Register a new key](key_registration::create_key) 19 | //! 20 | //! ### Minimal usage with updated change rule 21 | //! 22 | //! 1. [Update change rule to use revocation keys](change_rule::update_change_rule) 23 | //! 2. [Register a new key](key_registration::create_key) 24 | //! 25 | //! ### Full-lifecycle usage 26 | //! 27 | //! 1. [Update change rule to use revocation keys](change_rule::update_change_rule) 28 | //! 2. [Register a new key](key_registration::create_key) 29 | //! 3. [Update a key](key_registration::update_key) 30 | //! 4. [Revoke a key](key_registration::revoke_key) 31 | 32 | pub mod change_rule; 33 | pub mod device; 34 | pub mod key_anchor; 35 | pub mod key_registration; 36 | pub mod keyset_root; 37 | pub mod source_of_authority; 38 | pub mod app_binding; 39 | pub mod key_meta; 40 | pub mod utils; 41 | 42 | // Re-exports 43 | pub use hdk_extensions; 44 | pub use hdk_extensions::hdi_extensions; 45 | pub use deepkey; 46 | pub use deepkey::deepkey_types; 47 | pub use hc_deepkey_sdk as deepkey_sdk; 48 | 49 | use deepkey::*; 50 | use hdi_extensions::{ 51 | guest_error, 52 | }; 53 | use hdk::prelude::*; 54 | use hdk_extensions::{ 55 | agent_id, 56 | }; 57 | use keyset_root::create_keyset_root; 58 | 59 | 60 | #[hdk_extern] 61 | pub fn init(_: ()) -> ExternResult { 62 | create_keyset_root()?; 63 | 64 | Ok(InitCallbackResult::Pass) 65 | } 66 | 67 | 68 | #[derive(Serialize, Deserialize, Debug)] 69 | #[serde(tag = "type")] 70 | pub enum Signal { 71 | EntryCreated { 72 | action: SignedActionHashed, 73 | app_entry: EntryTypes, 74 | }, 75 | EntryUpdated { 76 | action: SignedActionHashed, 77 | app_entry: EntryTypes, 78 | original_app_entry: EntryTypes, 79 | }, 80 | EntryDeleted { 81 | action: SignedActionHashed, 82 | original_app_entry: EntryTypes, 83 | }, 84 | LinkCreated { 85 | action: SignedActionHashed, 86 | link_type: LinkTypes, 87 | }, 88 | LinkDeleted { 89 | action: SignedActionHashed, 90 | link_type: LinkTypes, 91 | }, 92 | } 93 | 94 | 95 | #[hdk_extern(infallible)] 96 | pub fn post_commit(committed_actions: Vec) { 97 | for action in committed_actions { 98 | if let Err(err) = signal_action(action) { 99 | error!("Error signaling new action: {:?}", err); 100 | } 101 | } 102 | } 103 | 104 | 105 | fn signal_action(action: SignedActionHashed) -> ExternResult<()> { 106 | match action.hashed.content.clone() { 107 | Action::Create(_create) => { 108 | if let Ok(Some(app_entry)) = get_entry_for_action(&action.hashed.hash) { 109 | emit_signal(Signal::EntryCreated { action, app_entry })?; 110 | } 111 | Ok(()) 112 | } 113 | Action::Update(update) => { 114 | if let Ok(Some(app_entry)) = get_entry_for_action(&action.hashed.hash) { 115 | if let Ok(Some(original_app_entry)) = 116 | get_entry_for_action(&update.original_action_address) 117 | { 118 | emit_signal(Signal::EntryUpdated { 119 | action, 120 | app_entry, 121 | original_app_entry, 122 | })?; 123 | } 124 | } 125 | Ok(()) 126 | } 127 | Action::Delete(delete) => { 128 | if let Ok(Some(original_app_entry)) = get_entry_for_action(&delete.deletes_address) { 129 | emit_signal(Signal::EntryDeleted { 130 | action, 131 | original_app_entry, 132 | })?; 133 | } 134 | Ok(()) 135 | } 136 | Action::CreateLink(create_link) => { 137 | if let Ok(Some(link_type)) = 138 | LinkTypes::from_type(create_link.zome_index, create_link.link_type) 139 | { 140 | emit_signal(Signal::LinkCreated { action, link_type })?; 141 | } 142 | Ok(()) 143 | } 144 | Action::DeleteLink(delete_link) => { 145 | let record = get(delete_link.link_add_address.clone(), GetOptions::default())?.ok_or( 146 | guest_error!( 147 | "Failed to fetch CreateLink action".to_string() 148 | ), 149 | )?; 150 | match record.action() { 151 | Action::CreateLink(create_link) => { 152 | if let Ok(Some(link_type)) = 153 | LinkTypes::from_type(create_link.zome_index, create_link.link_type) 154 | { 155 | emit_signal(Signal::LinkDeleted { action, link_type })?; 156 | } 157 | Ok(()) 158 | } 159 | _ => { 160 | return Err(guest_error!( 161 | "Create Link should exist".to_string() 162 | )); 163 | } 164 | } 165 | } 166 | _ => Ok(()), 167 | } 168 | } 169 | 170 | 171 | fn get_entry_for_action(action_hash: &ActionHash) -> ExternResult> { 172 | let record = match get_details(action_hash.clone(), GetOptions::default())? { 173 | Some(Details::Record(record_details)) => record_details.record, 174 | _ => { 175 | return Ok(None); 176 | } 177 | }; 178 | let entry = match record.entry().as_option() { 179 | Some(entry) => entry, 180 | None => { 181 | return Ok(None); 182 | } 183 | }; 184 | let (zome_index, entry_index) = match record.action().entry_type() { 185 | Some(EntryType::App(AppEntryDef { 186 | zome_index, 187 | entry_index, 188 | .. 189 | })) => (zome_index, entry_index), 190 | _ => { 191 | return Ok(None); 192 | } 193 | }; 194 | Ok(EntryTypes::deserialize_from_type( 195 | zome_index.clone(), 196 | entry_index.clone(), 197 | entry, 198 | )?) 199 | } 200 | 201 | 202 | #[hdk_extern] 203 | pub fn sign(bytes: serde_bytes::ByteBuf) -> ExternResult { 204 | sign_raw( 205 | agent_id()?, 206 | bytes.into_vec(), 207 | ) 208 | } 209 | 210 | 211 | #[hdk_extern] 212 | pub fn query_whole_chain() -> ExternResult> { 213 | Ok( 214 | query( 215 | ChainQueryFilter::new() 216 | .include_entries(true) 217 | )? 218 | ) 219 | } 220 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/change_rule.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | utils, 3 | deepkey_sdk, 4 | }; 5 | use deepkey::*; 6 | use hdk::prelude::*; 7 | use hdk_extensions::{ 8 | agent_id, 9 | must_get, 10 | hdi_extensions::{ 11 | guest_error, 12 | ScopedTypeConnector, 13 | }, 14 | }; 15 | pub use deepkey_sdk::{ 16 | AuthoritySpecInput, 17 | UpdateChangeRuleInput, 18 | }; 19 | 20 | 21 | #[hdk_extern] 22 | pub fn create_change_rule(change_rule: ChangeRule) -> ExternResult { 23 | let create_addr = create_entry( &change_rule.to_input() )?; 24 | 25 | create_link( 26 | change_rule.keyset_root.clone(), 27 | create_addr.clone(), 28 | LinkTypes::KSRToChangeRule, 29 | (), 30 | )?; 31 | 32 | Ok( create_addr ) 33 | } 34 | 35 | 36 | #[hdk_extern] 37 | pub fn get_ksr_change_rule_links(ksr_addr: ActionHash) -> ExternResult> { 38 | get_links( 39 | GetLinksInputBuilder::try_new( 40 | ksr_addr, 41 | LinkTypes::KSRToChangeRule, 42 | )?.build() 43 | ) 44 | } 45 | 46 | 47 | #[hdk_extern] 48 | pub fn get_current_change_rule_for_ksr(ksr_addr: ActionHash) -> ExternResult { 49 | let change_rule_links = get_ksr_change_rule_links( ksr_addr.clone() )?; 50 | let latest_addr = change_rule_links 51 | .iter() 52 | .filter_map( |link| Some( 53 | ( 54 | link.timestamp, 55 | link.target.to_owned().into_any_dht_hash()? 56 | ) 57 | )) 58 | .max_by_key( |(timestamp, _)| timestamp.to_owned() ) 59 | .ok_or(guest_error!(format!("There are no change rules for KSR ({})", ksr_addr )))? 60 | .1; 61 | 62 | Ok( must_get( &latest_addr )?.try_into()? ) 63 | } 64 | 65 | 66 | #[hdk_extern] 67 | pub fn construct_authority_spec(input: AuthoritySpecInput) -> ExternResult<(AuthoritySpec, Vec)> { 68 | let authority_spec = AuthoritySpec::from( input ); 69 | let serialized = utils::serialize( &authority_spec )?; 70 | 71 | Ok(( 72 | authority_spec, 73 | serialized, 74 | )) 75 | } 76 | 77 | 78 | /// Update the rules for updating keys and the rules themselves 79 | /// 80 | /// A `ChangeRule` is created in the init process with the cell agent as the authorized signer. 81 | /// This means that the first change rule update can be authorized by the coordinator zome who has 82 | /// the ability to sign with the cell agent. 83 | /// 84 | /// #### Example usage (first update) 85 | /// ```rust, no_run 86 | /// use hdk::prelude::*; 87 | /// use hc_deepkey_sdk::*; 88 | /// 89 | /// use rand::rngs::OsRng; 90 | /// use ed25519_dalek::SigningKey; 91 | /// use serde_bytes::ByteArray; 92 | /// # fn main() -> ExternResult<()> { 93 | /// 94 | /// // Generate some revocation keys (unsafely) 95 | /// let rev_key1 = SigningKey::generate(&mut OsRng); 96 | /// let rev_key2 = SigningKey::generate(&mut OsRng); 97 | /// 98 | /// let rev_pubkey1 = rev_key1.verifying_key(); 99 | /// let rev_pubkey2 = rev_key2.verifying_key(); 100 | /// 101 | /// let rev_auth1 = ByteArray::<32>::new( rev_pubkey1.to_bytes() ); 102 | /// let rev_auth2 = ByteArray::<32>::new( rev_pubkey2.to_bytes() ); 103 | /// 104 | /// // Define a multi-sig spec with 1 of 2 105 | /// let authority_spec = AuthoritySpecInput { 106 | /// sigs_required: 1, 107 | /// authorized_signers: vec![ 108 | /// rev_auth1, 109 | /// rev_auth2, 110 | /// ], 111 | /// }; 112 | /// 113 | /// let result = deepkey_csr::change_rule::update_change_rule(UpdateChangeRuleInput { 114 | /// authority_spec, 115 | /// authorizations: None, // Not required for the first update because the cell agent 116 | /// // can sign the 'authority_spec' in the coordinator function 117 | /// }); 118 | /// # Ok(()) 119 | /// # } 120 | /// ``` 121 | /// 122 | /// Now that the authorized signers are set to keys outside of Lair, the `authorizations` signatures 123 | /// must be provided in the call. 124 | /// 125 | /// #### Example usage (second update) 126 | /// ```rust, no_run 127 | /// # use hdk::prelude::*; 128 | /// # use hc_deepkey_sdk::*; 129 | /// # use rand::rngs::OsRng; 130 | /// # use ed25519_dalek::SigningKey; 131 | /// use ed25519_dalek::Signer; 132 | /// # use serde_bytes::ByteArray; 133 | /// # fn main() -> ExternResult<()> { 134 | /// # let rev_key1 = SigningKey::generate(&mut OsRng); 135 | /// # let rev_key2 = SigningKey::generate(&mut OsRng); 136 | /// # let rev_pubkey1 = rev_key1.verifying_key(); 137 | /// # let rev_pubkey2 = rev_key2.verifying_key(); 138 | /// # let rev_auth1 = ByteArray::<32>::new( rev_pubkey1.to_bytes() ); 139 | /// # let rev_auth2 = ByteArray::<32>::new( rev_pubkey2.to_bytes() ); 140 | /// 141 | /// let rev_key3 = SigningKey::generate(&mut OsRng); 142 | /// let rev_key4 = SigningKey::generate(&mut OsRng); 143 | /// let rev_key5 = SigningKey::generate(&mut OsRng); 144 | /// 145 | /// let rev_pubkey3 = rev_key3.verifying_key(); 146 | /// let rev_pubkey4 = rev_key4.verifying_key(); 147 | /// let rev_pubkey5 = rev_key4.verifying_key(); 148 | /// 149 | /// let rev_auth3 = ByteArray::<32>::new( rev_pubkey3.to_bytes() ); 150 | /// let rev_auth4 = ByteArray::<32>::new( rev_pubkey4.to_bytes() ); 151 | /// let rev_auth5 = ByteArray::<32>::new( rev_pubkey4.to_bytes() ); 152 | /// 153 | /// // Define a multi-sig spec with 2 of 3 154 | /// let authority_spec = AuthoritySpecInput { 155 | /// sigs_required: 2, 156 | /// authorized_signers: vec![ 157 | /// rev_auth3, 158 | /// rev_auth4, 159 | /// rev_auth5, 160 | /// ], 161 | /// }; 162 | /// // Serialize spec for signing 163 | /// let serialized = deepkey::utils::serialize( &authority_spec )?; 164 | /// 165 | /// let result = deepkey_csr::change_rule::update_change_rule(UpdateChangeRuleInput { 166 | /// authority_spec, 167 | /// authorizations: Some(vec![ 168 | /// // Sign new spec with the 2nd authorized signer from the previous rule. The previous 169 | /// // spec only requires 1 signature. 170 | /// ( 1, Signature( rev_key2.sign( &serialized ).to_bytes() ) ), 171 | /// ]), 172 | /// }); 173 | /// # Ok(()) 174 | /// # } 175 | /// ``` 176 | #[hdk_extern] 177 | pub fn update_change_rule(input: UpdateChangeRuleInput) -> ExternResult { 178 | let new_authority_spec = AuthoritySpec::from( input.authority_spec ); 179 | let authorizations = match input.authorizations { 180 | Some(authorizations) => authorizations, 181 | None => { 182 | let fda = agent_id()?; 183 | debug!("Signing new authority spec with FDA ({})", fda ); 184 | let fda_signature = sign_raw( 185 | fda, 186 | utils::serialize( &new_authority_spec )? 187 | )?; 188 | vec![ (0, fda_signature) ] 189 | } 190 | }; 191 | let spec_change = AuthorizedSpecChange::new( 192 | new_authority_spec, 193 | authorizations, 194 | ); 195 | 196 | let create_change_rule_record = utils::query_entry_type( EntryTypesUnit::ChangeRule )? 197 | .first() 198 | .ok_or(guest_error!(format!( 199 | "There is no change rule to update" 200 | )))? 201 | .to_owned(); 202 | 203 | ChangeRule::try_from( create_change_rule_record.clone() )?; 204 | 205 | let keyset_root_addr = utils::query_keyset_root_addr()?; 206 | let new_change_rule = ChangeRule::new( 207 | keyset_root_addr.clone(), 208 | spec_change, 209 | ); 210 | 211 | let update_addr = update_entry( 212 | create_change_rule_record.action_address().to_owned(), 213 | &new_change_rule, 214 | )?; 215 | 216 | create_link( 217 | keyset_root_addr.clone(), 218 | update_addr, 219 | LinkTypes::KSRToChangeRule, 220 | (), 221 | )?; 222 | 223 | Ok( new_change_rule ) 224 | } 225 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/key_anchor.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | utils, 3 | deepkey_sdk, 4 | }; 5 | use deepkey::*; 6 | use deepkey_sdk::{ 7 | KeyState, 8 | KeyBytes, 9 | }; 10 | use serde_bytes::ByteArray; 11 | 12 | use hdk::prelude::*; 13 | use hdk_extensions::{ 14 | agent_id, 15 | must_get, 16 | must_get_record_details, 17 | hdi_extensions::{ 18 | guest_error, 19 | trace_origin, 20 | trace_origin_root, 21 | ScopedTypeConnector, 22 | }, 23 | }; 24 | 25 | 26 | #[hdk_extern] 27 | pub fn create_key_anchor(key_anchor: KeyAnchor) -> ExternResult { 28 | let create_addr = create_entry( key_anchor.to_input() )?; 29 | 30 | create_link( 31 | agent_id()?, 32 | create_addr.clone(), 33 | LinkTypes::DeviceToKeyAnchor, 34 | "create".as_bytes().to_vec(), 35 | )?; 36 | 37 | Ok( create_addr ) 38 | } 39 | 40 | 41 | #[hdk_extern] 42 | pub fn key_state((key_bytes, timestamp): (ByteArray<32>, Timestamp)) -> ExternResult { 43 | let key_anchor = KeyAnchor::new( key_bytes.into_array() ); 44 | let key_anchor_hash = hash_entry( &key_anchor )?; 45 | let key_anchor_details = get_details( key_anchor_hash.clone(), GetOptions::default() )?; 46 | 47 | if let Some(details) = key_anchor_details { 48 | match details { 49 | Details::Entry(entry_details) => { 50 | debug!( 51 | "Details for KeyAnchor entry '{}': {} create(s), {} update(s), {} delete(s)", 52 | key_anchor_hash, 53 | entry_details.actions.len(), 54 | entry_details.updates.len(), 55 | entry_details.deletes.len(), 56 | ); 57 | if let Some(delete_record) = entry_details.deletes.first() { 58 | if delete_record.action().timestamp() < timestamp { 59 | return Ok(KeyState::Invalid( Some(delete_record.to_owned()) )); 60 | } 61 | else { 62 | debug!( 63 | "Deletion occurred after the given timestamp: [deleted] {} > {}", 64 | delete_record.action().timestamp(), timestamp 65 | ); 66 | } 67 | } 68 | 69 | if let Some(update_record) = entry_details.updates.first() { 70 | if update_record.action().timestamp() < timestamp { 71 | return Ok(KeyState::Invalid( Some(update_record.to_owned()) )); 72 | } 73 | else { 74 | debug!( 75 | "Update occurred after the given timestamp: [updated] {} > {}", 76 | update_record.action().timestamp(), timestamp 77 | ); 78 | } 79 | } 80 | 81 | if let Some(record) = entry_details.actions.first() { 82 | return Ok( 83 | match record.action().timestamp() > timestamp { 84 | true => { 85 | debug!( 86 | "Create occurred after the given timestamp: [created] {} > {}", 87 | record.action().timestamp(), timestamp 88 | ); 89 | KeyState::Invalid( None ) 90 | }, 91 | false => KeyState::Valid( record.to_owned() ), 92 | } 93 | ); 94 | } 95 | 96 | Err(guest_error!("KeyAnchor anchor details has no actions".into()))? 97 | }, 98 | Details::Record(_) => Err(guest_error!("Problem with get_details result".into()))?, 99 | } 100 | } 101 | else { 102 | Ok( KeyState::NotFound ) 103 | } 104 | } 105 | 106 | 107 | #[hdk_extern] 108 | pub fn get_key_anchor_for_registration(addr: ActionHash) -> ExternResult<(ActionHash, KeyAnchor)> { 109 | // Get registration 110 | let key_registration = crate::key_registration::get_key_registration( addr )?; 111 | 112 | // Derive key anchor entry hash 113 | let key_anchor_hash = key_registration.key_anchor_hash()?; 114 | 115 | // Get record for key anchor 116 | let record = must_get( &key_anchor_hash )?; 117 | 118 | Ok(( 119 | record.action_address().to_owned(), 120 | record.try_into()? 121 | )) 122 | } 123 | 124 | 125 | #[hdk_extern] 126 | pub fn query_action_addr_for_key_anchor( 127 | key_bytes: ByteArray<32> 128 | ) -> ExternResult { 129 | let key = key_bytes.into_array(); 130 | let key_anchor = KeyAnchor::new( key.clone() ); 131 | let key_anchor_hash = hash_entry( &key_anchor )?; 132 | 133 | let key_anchor_addr = utils::query_entry_type( EntryTypesUnit::KeyAnchor )? 134 | .into_iter() 135 | .filter_map( |record| Some( 136 | ( 137 | record.action_address().to_owned(), 138 | record.action().entry_hash()?.to_owned(), 139 | ) 140 | )) 141 | .find( |(_, hash)| hash == &key_anchor_hash ) 142 | .ok_or(guest_error!(format!("No KeyMeta for anchor hash: {}", key_anchor_hash )))?.0; 143 | 144 | Ok( key_anchor_addr ) 145 | } 146 | 147 | 148 | #[hdk_extern] 149 | pub fn get_first_key_anchor_for_key(key_bytes: ByteArray<32>) -> ExternResult<(ActionHash, KeyAnchor)> { 150 | let ka_action_addr = query_action_addr_for_key_anchor( key_bytes )?; 151 | let first_ka_action_addr = trace_origin_root( &ka_action_addr )?.0; 152 | 153 | Ok(( 154 | first_ka_action_addr.clone(), 155 | must_get( &first_ka_action_addr )?.try_into()?, 156 | )) 157 | } 158 | 159 | 160 | #[hdk_extern] 161 | pub fn query_key_lineage( 162 | key_bytes: ByteArray<32>, 163 | ) -> ExternResult> { 164 | let ka_action_addr = query_action_addr_for_key_anchor( key_bytes )?; 165 | let key_meta = crate::key_meta::query_key_meta_for_key_addr( ka_action_addr )?; 166 | let key_metas = crate::key_meta::query_key_metas_for_app_binding( key_meta.app_binding_addr )?; 167 | 168 | let mut lineage = vec![]; 169 | 170 | debug!("Lineage has {} keys", key_metas.len() ); 171 | for key_meta in key_metas { 172 | let key_anchor : KeyAnchor = must_get_record_details( &key_meta.key_anchor_addr )? 173 | .record 174 | .try_into()?; 175 | 176 | lineage.push( key_anchor.bytes ); 177 | } 178 | 179 | Ok( lineage ) 180 | } 181 | 182 | 183 | #[hdk_extern] 184 | pub fn get_key_lineage( 185 | key_bytes: ByteArray<32>, 186 | ) -> ExternResult> { 187 | let key_anchor = KeyAnchor::new( key_bytes.into_array() ); 188 | let key_anchor_hash = hash_entry( &key_anchor )?; 189 | let key_anchor_details = get_details( key_anchor_hash.clone(), GetOptions::default() )?; 190 | 191 | let (creation_addr, mut next_addr) = match key_anchor_details { 192 | None => Err(guest_error!(format!("Record not found")))?, 193 | Some(Details::Record(_)) => Err(guest_error!(format!("Should be unreachable")))?, 194 | Some(Details::Entry( entry_details )) => { 195 | ( 196 | entry_details.actions.first() 197 | .ok_or(guest_error!(format!("Should be unreachable")))? 198 | .action_address() 199 | .to_owned(), 200 | entry_details.updates.first() 201 | .map( |action| action.action_address().to_owned() ), 202 | ) 203 | }, 204 | }; 205 | 206 | let mut lineage = vec![]; 207 | 208 | // Trace backwards 209 | let history = trace_origin( &creation_addr )?; 210 | 211 | debug!( 212 | "Found history ({}): {:?}", 213 | history.len(), 214 | history.clone().iter().map( |(addr, _)| addr ).collect::>(), 215 | ); 216 | for (addr, _action) in history { 217 | let key_anchor : KeyAnchor = must_get_record_details( &addr )? 218 | .record 219 | .try_into()?; 220 | 221 | lineage.push( key_anchor.bytes ); 222 | } 223 | 224 | lineage.reverse(); 225 | 226 | // Follow evolutions forwards 227 | while let Some(addr) = next_addr { 228 | let details = must_get_record_details( &addr )?; 229 | let key_anchor : KeyAnchor = details.record.try_into()?; 230 | 231 | debug!("Found update ({}): {:?}", addr, key_anchor.bytes ); 232 | lineage.push( key_anchor.bytes ); 233 | 234 | next_addr = details.updates.first() 235 | .map( |action| action.action_address().to_owned() ); 236 | } 237 | 238 | Ok( lineage ) 239 | } 240 | 241 | 242 | #[hdk_extern] 243 | pub fn query_same_lineage( 244 | (key1_bytes, key2_bytes): (ByteArray<32>, ByteArray<32>), 245 | ) -> ExternResult { 246 | let keys = query_key_lineage( key1_bytes )?; 247 | 248 | Ok( keys.contains( &key2_bytes.into_array() ) ) 249 | } 250 | 251 | 252 | #[hdk_extern] 253 | pub fn same_lineage( 254 | (key1_bytes, key2_bytes): (ByteArray<32>, ByteArray<32>), 255 | ) -> ExternResult { 256 | let keys = get_key_lineage( key1_bytes )?; 257 | 258 | Ok( keys.contains( &key2_bytes.into_array() ) ) 259 | } 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/crates/v/hc_deepkey_types?style=flat-square&label=types)](https://crates.io/crates/hc_deepkey_types) 2 | [![](https://img.shields.io/crates/v/hc_deepkey_sdk?style=flat-square&label=sdk)](https://crates.io/crates/hc_deepkey_sdk) 3 | 4 | # Deepkey 5 | A DNA to provide a decentralized public key infrastructure (DPKI) for keys associated with Holochain 6 | conductors and applications. Similar to centralised services like Keybase, we want users to be able 7 | to manage their "keyset" by adding and removing public/private keypairs. 8 | 9 | [![](https://img.shields.io/github/issues-raw/holochain/deepkey?style=flat-square)](https://github.com/holochain/deepkey/issues) 10 | [![](https://img.shields.io/github/issues-closed-raw/holochain/deepkey?style=flat-square)](https://github.com/holochain/deepkey/issues?q=is%3Aissue+is%3Aclosed) 11 | [![](https://img.shields.io/github/issues-pr-raw/holochain/deepkey?style=flat-square)](https://github.com/holochain/deepkey/pulls) 12 | 13 | See [Pre-2024 Architectual Documentation](docs/2023/README.md) for original design principles. 14 | 15 | 16 | 17 | ## Overview 18 | The keys for happs installed on a device are also tracked under the keyset for the device. A keyset 19 | is the set of keys governed by a ruleset, presumably under the control of one person. 20 | 21 | When you install a new app in Holochain, by default a new keypair is generated to control the source 22 | chain of that app. The public key of that keypair serves as the address of your agent in that app's 23 | DHT. The private key signs all of your network communications and all actions on your chain. Deepkey 24 | registers, manages, and reports on the validity of each `AgentPubKey` installed on your conductor. 25 | 26 | 27 | #### Why do we need Deepkey? 28 | 29 | Because humans are notoriously bad at managing cryptographic keys, we believe a project like 30 | Holochain must provide key management tools to help people deal with real-world messiness such as 31 | lost/stolen keys or devices. How many billions of dollars have been lost due to the lack of a key 32 | management infrastructure? 33 | 34 | Deepkey is a foundational app for all other Holochain app keys. Therefore, it is the first happ 35 | every conductor must install, and all other happs rely on it to query the status of keys. It is 36 | designed to work hand in hand with holochain's secure keystore, 37 | [Lair](https://github.com/holochain/lair). 38 | 39 | The most common call to Deepkey is `key_state((Key, Timestamp))` to query the validity of a key at a 40 | particular time. 41 | 42 | ##### Another reason is for source chain recovery 43 | 44 | In addition to shared/public keysets and registrations, Deepkey supports private data for 45 | remembering how the keys were derived. With each key generation, the relevant "derivation details" 46 | can be saved as private entries. This private data can be used later to rebuild your keypairs and 47 | recover app source chains. 48 | 49 | 50 | ### Features 51 | Deepkey provides the ability to: 52 | 53 | - Register keys under the authority of a keyset. 54 | - Define the revocation rules for a keyset. 55 | - Replace keys with new ones. 56 | - Revoke keys / declare them dead. 57 | - Check the validity of a key. 58 | - Store private instructions to rebuild app keys from a master seed to reestablish authority after 59 | data loss. 60 | 61 | Future features 62 | 63 | - Associate multiple devices under unified keyset management. 64 | - Do social management of keys through m of n signatures. 65 | 66 | 67 | 68 | ## How it works? 69 | 70 | Workflows 71 | 72 | - Initializing Deepkey 73 | - Registering a new key 74 | - Replacing a key 75 | - Revoking a key 76 | - Checking key validity 77 | - Updating the change rules 78 | 79 | 80 | #### Interactions with Lair 81 | 82 | > Although Deepkey is designed to work with Lair, there is no direct dependence on Lair as all 83 | > interactions with Lair are facilitated by the DPKI service in Holochain. 84 | 85 | Lair is designed to generate new keys from randomness, or generate new keys from a seed with 86 | derivation instructions. In order to regenerate your app keys, Deepkey must store the derivation 87 | info that will instruct Lair to reproduce the same keys. 88 | 89 | 90 | #### How are keys determinitic? 91 | 92 | Key uniquenss is determined by a combination of 2 incrementing numbers; an **app index** and a **key 93 | index**. When an app is installed, a new key registration is made using the next unused app index 94 | and a key index of `0`. Replacing keys will increment the key index while the app index stays the 95 | same. 96 | 97 | 98 | ### Initializing Deepkey 99 | 100 | When you install Holochain, Deepkey will create a new Keyset Root (KSR) as the first action. Then 101 | it will set the initial change rules for that KSR and register itself as the first key (ie. app/key 102 | index `0/0`). 103 | 104 | > *There can only be one KSR on a Deepkey source chain.* 105 | 106 | #### What is a KSR? 107 | A keyset is the set of keys governed by a ruleset, presumably under the control of one person. 108 | 109 | 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. 110 | 111 | A `KeysetRoot` (KSR) is self-declared onto the network using a single-purpose throwaway keypair. 112 | 113 | The structure of a `KeysetRoot` is: 114 | 115 | - `first_deepkey_agent` - (FDA) the author of the `KeysetRoot`. 116 | - `root_pub_key` - the public part of a throwaway keypair which is only used to generate this KSR 117 | - `signed_fda` - the authority of the FDA is established by signing thd FDA's pubkey using the 118 | private part of the throwaway keypair 119 | 120 | 121 | ### Registering a new key 122 | 123 | Registering a new key is a 2 step process; generating the new key outside of Deepkey and then 124 | registering it in Deepkey. 125 | 126 | 1. In order to generate the key, Deepkey provides a way to get the "next derivation details" which 127 | can be used to generate a deterministic key. 128 | 2. Then that key can be committed in Deepkey along with all the other registration input such as 129 | details about the app and derivation input. 130 | 131 | App and derivation details are committed as private entries. 132 | 133 | 134 | ### Replacing a key 135 | 136 | Key replacement is simply a combination of key revocation and key generation committed together. It 137 | requires most of the same steps from [Registering a new key](#registering-a-new-key) and [Revoking a 138 | key](#revoking-a-key). 139 | 140 | 1. In order to generate the next key, Deepkey provides a way to get the "next derivation details" 141 | given a specific key which will return the existing `app_index` for that key and the next unsused 142 | `key_index`. 143 | 2. Gather the required signatures for revoking the previous key registration (see [Revoking a 144 | key](#revoking-a-key)). 145 | 3. Then the new key can be committed as a replacement to the previous key. 146 | 147 | Derivation details are committed as a private entry. 148 | 149 | 150 | ### Revoking a key 151 | 152 | The act of revoking a key (without a key update) ends the evolution of that key. This means that 153 | all app chains using that key will not no longer be valid as of the revocation commit timestamp. 154 | 155 | A revocation is done by signing the `KeyRegistration`'s `ActionHash` with the number of keys 156 | required by the current change rules. 157 | 158 | 159 | ### Checking key validity 160 | 161 | Using this `KeyAnchor` entry, the status (valid, revoked, replaced, etc.) of a key can be looked up 162 | in a single `get_details` call, without needing to first lookup the corresponding `KeyRegistration`. 163 | 164 | This also means that any external consumer of Deepkey (other DNA's, Holochain apps, etc.) can query 165 | the key status with the core 32 bytes of the key. They do NOT need to know its registration details. 166 | 167 | A key state can be checked using the key bytes and a timestamp. There are 3 states a key can be in 168 | 169 | - Valid 170 | - the timestamp is after a key create action timestamp and before any delete action timestamps 171 | - Invalid 172 | - the timestamp is before any key create action timestamps 173 | - or, the timestamp is after a key create action timestamp and after a delete action timestamp 174 | - Not found 175 | - there are no create or delete actions 176 | 177 | 178 | ### Updating the change rules 179 | 180 | A `ChangeRule` defines the rules that apply to keys under the management of a keyset. It is 181 | designed to support signing with M of N keys (ie. an `AuthoritySpec`) which is used to validate 182 | changes to keys and the change rules themselves. 183 | 184 | > NOTE: that the spec change signature validation does NOT require that all the signers exist as 185 | > agents in Deepkey. This means hardware wallets, FIDO-compliant keys, smart cards, etc. could be 186 | > used to provide signatures into your multisig. 187 | 188 | Initially, the change rule is has an `AuthoritySpec` set to 1 of 1 with the only authority being the 189 | FDA. 190 | 191 | A change rule can be updated by constructing a new `AuthoritySpec`, signing it with the required 192 | keys (based on the existing change rule), and then commiting the new `ChangeRule` as an update to 193 | the original. 194 | 195 | > *There can only be one `ChangeRule` create action on a Deepkey source chain. Any following 196 | > `ChangeRule` commits must be udpates to the original.* 197 | 198 | 199 | 200 | ## Integrity Model 201 | 202 | 203 | Documentation about the integrity validation. 204 | 205 | See [INTEGRITY_MODEL.md](INTEGRITY_MODEL.md) 206 | 207 | 208 | 209 | ## Contributing 210 | 211 | See [CONTRIBUTING.md](CONTRIBUTING.md) 212 | -------------------------------------------------------------------------------- /tests/integration/test_change_rules.js: -------------------------------------------------------------------------------- 1 | import { Logger } from '@whi/weblogger'; 2 | const log = new Logger("test-basic", process.env.LOG_LEVEL ); 3 | 4 | // import why from 'why-is-node-running'; 5 | 6 | import path from 'path'; 7 | import crypto from 'crypto'; 8 | 9 | import { expect } from 'chai'; 10 | 11 | import * as ed from '@noble/ed25519'; 12 | 13 | import json from '@whi/json'; 14 | import { 15 | HoloHash, 16 | DnaHash, AgentPubKey, 17 | ActionHash, EntryHash, 18 | } from '@spartan-hc/holo-hash'; 19 | import { Holochain } from '@spartan-hc/holochain-backdrop'; 20 | 21 | import { 22 | DeepKeyCell, 23 | } from '@holochain/deepkey-zomelets'; 24 | import { 25 | AppInterfaceClient, 26 | } from '@spartan-hc/app-interface-client'; 27 | 28 | import { 29 | expect_reject, 30 | linearSuite, 31 | } from '../utils.js'; 32 | 33 | 34 | const __dirname = path.dirname( new URL(import.meta.url).pathname ); 35 | const DEEPKEY_DNA_PATH = path.join( __dirname, "../../dnas/deepkey.dna" ); 36 | const DEEPKEY_DNA_NAME = "deepkey"; 37 | 38 | const dna1_hash = new DnaHash( crypto.randomBytes( 32 ) ); 39 | 40 | const revocation_key1 = ed.utils.randomPrivateKey(); 41 | const revocation_key2 = ed.utils.randomPrivateKey(); 42 | const revocation_key3 = ed.utils.randomPrivateKey(); 43 | const revocation_key4 = ed.utils.randomPrivateKey(); 44 | 45 | const rev1_pubkey = await ed.getPublicKeyAsync( revocation_key1 ); 46 | const rev2_pubkey = await ed.getPublicKeyAsync( revocation_key2 ); 47 | const rev3_pubkey = await ed.getPublicKeyAsync( revocation_key3 ); 48 | const rev4_pubkey = await ed.getPublicKeyAsync( revocation_key4 ); 49 | 50 | let app_port; 51 | let installations; 52 | 53 | describe("DeepKey", function () { 54 | const holochain = new Holochain({ 55 | "timeout": 20_000, 56 | "default_stdout_loggers": log.level_rank > 3, 57 | "default_stderr_loggers": log.level_rank > 3, 58 | }); 59 | 60 | before(async function () { 61 | this.timeout( 60_000 ); 62 | 63 | installations = await holochain.install([ 64 | "alice1", 65 | ], { 66 | "app_name": "test", 67 | "bundle": { 68 | [DEEPKEY_DNA_NAME]: DEEPKEY_DNA_PATH, 69 | }, 70 | "membrane_proofs": { 71 | [DEEPKEY_DNA_NAME]: { 72 | "joining_proof": crypto.randomBytes( 32 ), 73 | }, 74 | }, 75 | }); 76 | 77 | app_port = await holochain.ensureAppPort(); 78 | }); 79 | 80 | linearSuite("Change Rules", basic_tests ); 81 | 82 | after(async () => { 83 | await holochain.destroy(); 84 | }); 85 | }); 86 | 87 | 88 | function basic_tests () { 89 | let client; 90 | let alice1_client; 91 | let deepkey; 92 | let alice1_deepkey; 93 | let ksr1_addr; 94 | 95 | before(async function () { 96 | this.timeout( 120_000 ); 97 | 98 | client = new AppInterfaceClient( app_port, { 99 | "logging": process.env.LOG_LEVEL || "normal", 100 | }); 101 | 102 | const alice1_token = installations.alice1.test.auth.token; 103 | alice1_client = await client.app( alice1_token ); 104 | 105 | { 106 | ({ 107 | deepkey, 108 | } = alice1_client.createInterface({ 109 | [DEEPKEY_DNA_NAME]: DeepKeyCell, 110 | })); 111 | 112 | alice1_deepkey = deepkey.zomes.deepkey_csr.functions; 113 | } 114 | 115 | ksr1_addr = await alice1_deepkey.query_keyset_authority_action_hash(); 116 | 117 | await new Promise( f => setTimeout(f, 60_000) ); 118 | }); 119 | 120 | it("should update change rule for (alice1) KSR", async function () { 121 | const auth_spec_package = await alice1_deepkey.construct_authority_spec({ 122 | "sigs_required": 2, 123 | "authorized_signers": [ 124 | rev1_pubkey, 125 | rev2_pubkey, 126 | rev3_pubkey, 127 | ], 128 | }); 129 | // log.normal("Constructed Authority Spec: %s", json.debug(auth_spec_package.authority_spec) ); 130 | 131 | const new_change_rule = await alice1_deepkey.update_change_rule({ 132 | "authority_spec": auth_spec_package.authority_spec, 133 | }); 134 | 135 | log.normal("New Change Rule: %s", json.debug(new_change_rule) ); 136 | }); 137 | 138 | it("should update change rule using revocation key for (alice1) KSR", async function () { 139 | const auth_spec_package = await alice1_deepkey.construct_authority_spec({ 140 | "sigs_required": 2, 141 | "authorized_signers": [ 142 | rev1_pubkey, 143 | rev2_pubkey, 144 | rev3_pubkey, 145 | rev4_pubkey, 146 | ], 147 | }); 148 | // log.normal("Constructed Authority Spec: %s", json.debug(auth_spec_package.authority_spec) ); 149 | 150 | log.info("Signing new auth spec with authority: %s", new AgentPubKey( rev1_pubkey ) ); 151 | const new_change_rule = await alice1_deepkey.update_change_rule({ 152 | "authority_spec": auth_spec_package.authority_spec, 153 | "authorizations": [ 154 | [ 0, await ed.signAsync( auth_spec_package.serialized, revocation_key1 ) ], 155 | [ 2, await ed.signAsync( auth_spec_package.serialized, revocation_key3 ) ], 156 | ], 157 | }); 158 | 159 | log.normal("New Change Rule: %s", json.debug(new_change_rule) ); 160 | 161 | const current_change_rule = await alice1_deepkey.get_current_change_rule_for_ksr( ksr1_addr ); 162 | 163 | expect( current_change_rule ).to.deep.equal( new_change_rule ); 164 | }); 165 | 166 | linearSuite("Errors", function () { 167 | 168 | it("should fail to update change rule entry because invalid signature for (alice1) KSR", async function () { 169 | await expect_reject(async () => { 170 | await alice1_deepkey.update_change_rule({ 171 | "authority_spec": { 172 | "sigs_required": 1, 173 | "authorized_signers": [ 174 | crypto.randomBytes(32), 175 | ], 176 | }, 177 | "authorizations": [ 178 | [ 0, crypto.randomBytes(64) ], 179 | [ 1, crypto.randomBytes(64) ], 180 | ], 181 | }); 182 | }, "Authorization has invalid signature" ); 183 | }); 184 | 185 | it("should fail to update change rule entry because invalid signature for (alice1) KSR", async function () { 186 | await expect_reject(async () => { 187 | await alice1_deepkey.update_change_rule({ 188 | "authority_spec": { 189 | "sigs_required": 2, 190 | "authorized_signers": [ 191 | crypto.randomBytes(32), 192 | ], 193 | }, 194 | "authorizations": [ 195 | [ 0, crypto.randomBytes(64) ], 196 | ], 197 | }); 198 | }, "There are not enough authorities" ); 199 | }); 200 | 201 | it("should fail to update change rule entry because not enough signatures for (alice1) KSR", async function () { 202 | await expect_reject(async () => { 203 | const auth_spec_package = await alice1_deepkey.construct_authority_spec({ 204 | "sigs_required": 1, 205 | "authorized_signers": [ 206 | crypto.randomBytes(32), 207 | ], 208 | }); 209 | await alice1_deepkey.update_change_rule({ 210 | "authority_spec": { 211 | "sigs_required": 1, 212 | "authorized_signers": [ 213 | crypto.randomBytes(32), 214 | ], 215 | }, 216 | "authorizations": [ 217 | [ 0, await ed.signAsync( auth_spec_package.serialized, revocation_key1 ) ], 218 | ], 219 | }); 220 | }, "change rule requires at least" ); 221 | }); 222 | 223 | it("should fail to update change rule entry because invalid signature for (alice1) KSR", async function () { 224 | await expect_reject(async () => { 225 | const auth_spec_package = await alice1_deepkey.construct_authority_spec({ 226 | "sigs_required": 0, 227 | "authorized_signers": [ 228 | crypto.randomBytes(32), 229 | ], 230 | }); 231 | await alice1_deepkey.update_change_rule({ 232 | "authority_spec": auth_spec_package.authority_spec, 233 | "authorizations": [ 234 | [ 0, await ed.signAsync( auth_spec_package.serialized, revocation_key1 ) ], 235 | [ 2, await ed.signAsync( auth_spec_package.serialized, revocation_key3 ) ], 236 | ], 237 | }); 238 | }, "Required signatures cannot be 0" ); 239 | }); 240 | 241 | }); 242 | 243 | after(async function () { 244 | await client.close(); 245 | }); 246 | } 247 | -------------------------------------------------------------------------------- /dnas/deepkey/zomelets/src/types.js: -------------------------------------------------------------------------------- 1 | 2 | import { Bytes } from '@whi/bytes-class'; 3 | import { 4 | AgentPubKey, DnaHash, 5 | ActionHash, EntryHash, 6 | AnyLinkableHash, 7 | } from '@spartan-hc/holo-hash'; 8 | import { 9 | // ScopedEntity, 10 | intoStruct, 11 | AnyType, OptionType, None, 12 | VecType, MapType, 13 | } from '@whi/into-struct'; 14 | 15 | 16 | export const Signature = Bytes; 17 | 18 | 19 | export class EntryTypeEnum { 20 | constructor ( data ) { 21 | if ( "App" in data ) 22 | return intoStruct( data, AppEntryTypeStruct ); 23 | 24 | // console.log("EntryTypeEnum constructor:", data ); 25 | throw new Error(`Unhandled Action entry type: ${Object.keys(data)[0]}`); 26 | } 27 | } 28 | 29 | export const AppEntryTypeStruct = { 30 | "App": { 31 | "entry_index": Number, 32 | "zome_index": Number, 33 | "visibility": AnyType, 34 | }, 35 | }; 36 | 37 | export const WeightStruct = { 38 | "bucket_id": Number, 39 | "units": Number, 40 | "rate_bytes": OptionType( Number ), 41 | }; 42 | 43 | export const ActionBaseStruct = { 44 | "type": String, 45 | "author": AgentPubKey, 46 | "timestamp": Number, 47 | "action_seq": Number, 48 | "prev_action": OptionType( ActionHash ), 49 | } 50 | export const DnaActionStruct = { 51 | "type": String, 52 | "author": AgentPubKey, 53 | "timestamp": Number, 54 | "hash": DnaHash, 55 | }; 56 | export const AgentValidationPkgActionStruct = { 57 | ...ActionBaseStruct, 58 | "membrane_proof": OptionType( Bytes ), 59 | }; 60 | export const NativeCreateActionStruct = { 61 | ...ActionBaseStruct, 62 | "entry_type": String, 63 | "entry_hash": EntryHash, 64 | "weight": WeightStruct, 65 | }; 66 | export const InitZomesCompleteActionStruct = { 67 | ...ActionBaseStruct, 68 | }; 69 | export const CreateActionStruct = { 70 | ...ActionBaseStruct, 71 | "entry_type": EntryTypeEnum, 72 | "entry_hash": EntryHash, 73 | "weight": WeightStruct, 74 | }; 75 | export const UpdateActionStruct = { 76 | ...ActionBaseStruct, 77 | "original_action_address": ActionHash, 78 | "original_entry_address": EntryHash, 79 | "entry_type": EntryTypeEnum, 80 | "entry_hash": EntryHash, 81 | "weight": WeightStruct, 82 | }; 83 | export const DeleteActionStruct = { 84 | ...ActionBaseStruct, 85 | "deletes_address": ActionHash, 86 | "deletes_entry_address": EntryHash, 87 | "weight": WeightStruct, 88 | }; 89 | 90 | export const CreateLinkActionStruct = { 91 | ...ActionBaseStruct, 92 | "base_address": AnyLinkableHash, 93 | "target_address": AnyLinkableHash, 94 | "zome_index": Number, 95 | "link_type": Number, 96 | "tag": Bytes, 97 | "weight": WeightStruct, 98 | }; 99 | 100 | export class ActionEnum { 101 | constructor ( data ) { 102 | if ( data.type === "Dna" ) 103 | return intoStruct( data, DnaActionStruct ); 104 | if ( data.type === "AgentValidationPkg" ) 105 | return intoStruct( data, AgentValidationPkgActionStruct ); 106 | if ( data.type === "InitZomesComplete" ) 107 | return intoStruct( data, InitZomesCompleteActionStruct ); 108 | if ( data.type === "Create" ) { 109 | if ( typeof data.entry_type === "string" ) 110 | return intoStruct( data, NativeCreateActionStruct ); 111 | else 112 | return intoStruct( data, CreateActionStruct ); 113 | } 114 | if ( data.type === "Update" ) 115 | return intoStruct( data, UpdateActionStruct ); 116 | if ( data.type === "Delete" ) 117 | return intoStruct( data, DeleteActionStruct ); 118 | if ( data.type === "CreateLink" ) 119 | return intoStruct( data, CreateLinkActionStruct ); 120 | 121 | // console.log("ActionEnum constructor:", data ); 122 | throw new Error(`Unhandled Action type: ${data.type}`); 123 | } 124 | } 125 | 126 | 127 | export const SignedActionStruct = { 128 | "hashed": { 129 | "content": ActionEnum, 130 | "hash": ActionHash, 131 | }, 132 | "signature": Signature, 133 | }; 134 | 135 | export function SignedAction ( data ) { 136 | return intoStruct( data, SignedActionStruct ); 137 | } 138 | 139 | 140 | export const AuthorizationStruct = [ Number, Signature ]; 141 | 142 | export function Authorization ( data ) { 143 | return intoStruct( data, AuthorizationStruct ); 144 | } 145 | 146 | 147 | export const AuthoritySpecStruct = { 148 | "sigs_required": Number, 149 | "authorized_signers": VecType( Bytes ), 150 | }; 151 | 152 | export function AuthoritySpec ( data ) { 153 | return intoStruct( data, AuthoritySpecStruct ); 154 | } 155 | 156 | 157 | export const AuthorizedSpecChangeStruct = { 158 | "new_spec": AuthoritySpecStruct, 159 | "authorization_of_new_spec": VecType( AuthorizationStruct ), 160 | }; 161 | 162 | export function AuthorizedSpecChange ( data ) { 163 | return intoStruct( data, AuthorizedSpecChangeStruct ); 164 | } 165 | 166 | 167 | export const ChangeRuleStruct = { 168 | "keyset_root": ActionHash, 169 | "spec_change": AuthorizedSpecChangeStruct, 170 | }; 171 | 172 | export function ChangeRule ( data ) { 173 | return intoStruct( data, ChangeRuleStruct ); 174 | } 175 | 176 | 177 | export const KeysetRootStruct = { 178 | "first_deepkey_agent": AgentPubKey, 179 | "root_pub_key": Bytes, 180 | "signed_fda": Signature, 181 | }; 182 | 183 | export function KeysetRoot ( data ) { 184 | return intoStruct( data, KeysetRootStruct ); 185 | } 186 | 187 | 188 | export const DerivationDetails = { 189 | "app_index": Number, 190 | "key_index": Number, 191 | }; 192 | 193 | export const KeyMetaStruct = { 194 | "app_binding_addr": ActionHash, 195 | "key_index": Number, 196 | "key_registration_addr": ActionHash, 197 | "key_anchor_addr": ActionHash, 198 | "derivation_seed": OptionType( Bytes ), 199 | "derivation_bytes": OptionType( Bytes ), 200 | }; 201 | 202 | export function KeyMeta ( data ) { 203 | return intoStruct( data, KeyMetaStruct ); 204 | } 205 | 206 | 207 | export const KeyAnchorStruct = { 208 | "bytes": Bytes, 209 | }; 210 | 211 | export function KeyAnchor ( data ) { 212 | return intoStruct( data, KeyAnchorStruct ); 213 | } 214 | 215 | 216 | export const AppBindingStruct = { 217 | "app_index": Number, 218 | "app_name": String, 219 | "installed_app_id": String, 220 | "dna_hashes": VecType( DnaHash ), 221 | "metadata": Object, 222 | }; 223 | 224 | export function AppBinding ( data ) { 225 | return intoStruct( data, AppBindingStruct ); 226 | } 227 | 228 | 229 | export const KeyGenerationStruct = { 230 | "new_key": AgentPubKey, 231 | "new_key_signing_of_author": Signature, 232 | }; 233 | export const KeyRevocationStruct = { 234 | "prior_key_registration": ActionHash, 235 | "revocation_authorization": VecType( AuthorizationStruct ), 236 | }; 237 | 238 | export function KeyRegistrationEntry ( entry ) { 239 | if ( "Create" in entry ) 240 | entry.Create = intoStruct( entry.Create, KeyGenerationStruct ); 241 | else if ( "CreateOnly" in entry ) 242 | entry.CreateOnly = intoStruct( entry.CreateOnly, KeyGenerationStruct ); 243 | else if ( "Update" in entry ) 244 | entry.Update = intoStruct( entry.Update, [ KeyRevocationStruct, KeyGenerationStruct ] ); 245 | else if ( "Delete" in entry ) 246 | entry.Delete = intoStruct( entry.Delete, KeyRevocationStruct ); 247 | else 248 | throw new TypeError(`Unknown type for KeyRegistration entry: ${Object.keys(entry)[0]}`); 249 | 250 | return entry; 251 | } 252 | 253 | // export class KeyRegistration extends ScopedEntity { 254 | // static STRUCT = KeyRegistrationStruct; 255 | // } 256 | 257 | export const KeyInfoStruct = [ 258 | AppBindingStruct, 259 | KeyMetaStruct, 260 | KeyRegistrationEntry, 261 | ]; 262 | 263 | export function KeyInfo ( data ) { 264 | return intoStruct( data, KeyInfoStruct ); 265 | } 266 | 267 | 268 | export function KeyState ( entry ) { 269 | if ( "NotFound" in entry ) 270 | null; 271 | if ( "Valid" in entry ) 272 | entry.Valid = intoStruct( entry.Valid, SignedActionStruct ); 273 | if ( "Invalid" in entry ) 274 | entry.Invalid = intoStruct( entry.Invalid, OptionType( SignedActionStruct ) ); 275 | 276 | return entry; 277 | } 278 | 279 | 280 | export default { 281 | Signature, 282 | 283 | SignedActionStruct, 284 | SignedAction, 285 | 286 | AuthorizationStruct, 287 | Authorization, 288 | 289 | AuthoritySpecStruct, 290 | AuthoritySpec, 291 | 292 | AuthorizedSpecChangeStruct, 293 | AuthorizedSpecChange, 294 | 295 | ChangeRuleStruct, 296 | ChangeRule, 297 | 298 | KeysetRootStruct, 299 | KeysetRoot, 300 | 301 | KeyMetaStruct, 302 | KeyMeta, 303 | 304 | KeyAnchorStruct, 305 | KeyAnchor, 306 | 307 | AppBindingStruct, 308 | AppBinding, 309 | 310 | KeyGenerationStruct, 311 | KeyRevocationStruct, 312 | KeyRegistrationEntry, 313 | 314 | KeyInfoStruct, 315 | KeyInfo, 316 | 317 | KeyState, 318 | }; 319 | -------------------------------------------------------------------------------- /dnas/deepkey/zomelets/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | AnyDhtHash, 3 | AgentPubKey, 4 | ActionHash, EntryHash, 5 | } from '@spartan-hc/holo-hash'; // approx. 11kb 6 | import { 7 | Zomelet, 8 | CellZomelets, 9 | } from '@spartan-hc/zomelets'; // approx. 7kb 10 | import msgpack from '@msgpack/msgpack'; 11 | import { 12 | Signature, 13 | SignedAction, 14 | Authorization, 15 | AuthoritySpec, 16 | AuthorizedSpecChange, 17 | ChangeRule, 18 | KeysetRoot, 19 | KeyMeta, 20 | KeyAnchor, 21 | KeyState, 22 | AppBinding, 23 | 24 | KeyRegistrationEntry, 25 | KeyInfo, 26 | } from './types.js'; 27 | 28 | 29 | const functions = { 30 | // Local reading 31 | async query_keyset_authority_action_hash () { 32 | const result = await this.call(); 33 | 34 | return new ActionHash( result ); 35 | }, 36 | async query_keyset_root_action_hash () { 37 | const result = await this.call(); 38 | 39 | return new ActionHash( result ); 40 | }, 41 | async query_app_bindings () { 42 | const result = await this.call(); 43 | 44 | return result.map( ([addr, app_binding]) => [ 45 | new ActionHash( addr ), 46 | AppBinding( app_binding ), 47 | ]); 48 | }, 49 | async query_app_bindings_by_installed_app_id ( input ) { 50 | const result = await this.call( input ); 51 | 52 | return result.map( ([addr, app_binding]) => [ 53 | new ActionHash( addr ), 54 | AppBinding( app_binding ), 55 | ]); 56 | }, 57 | async query_app_binding_by_index ( input ) { 58 | const result = await this.call( input ); 59 | 60 | return [ 61 | new ActionHash( result[0] ), 62 | AppBinding( result[1] ), 63 | ]; 64 | }, 65 | async query_app_binding_by_key ( input ) { 66 | const result = await this.call( input ); 67 | 68 | return [ 69 | new ActionHash( result[0] ), 70 | AppBinding( result[1] ), 71 | ]; 72 | }, 73 | async query_apps_with_keys () { 74 | const result = await this.call(); 75 | 76 | return result.map( ([app_binding, key_metas]) => { 77 | return [ 78 | AppBinding( app_binding ), 79 | key_metas.map( key_meta => KeyMeta( key_meta ) ), 80 | ]; 81 | }); 82 | }, 83 | async query_key_lineage ( input ) { 84 | const result = await this.call( input ); 85 | 86 | return result.map( key => new Uint8Array(key) ); 87 | }, 88 | async query_same_lineage ( input ) { 89 | const result = await this.call( input ); 90 | 91 | return result; 92 | }, 93 | async sign ( bytes ) { 94 | const result = await this.call( bytes ); 95 | 96 | return new Signature( result ); 97 | }, 98 | async query_whole_chain() { 99 | const result = await this.call(); 100 | 101 | return result.map( record => { 102 | record.signed_action = SignedAction(record.signed_action); 103 | 104 | if ( record.entry?.Present?.entry ) { 105 | if ( record.entry.Present.entry_type === "App" ) 106 | record.entry.Present.content = msgpack.decode( record.entry.Present.entry ); 107 | else if ( record.entry.Present.entry_type === "Agent" ) 108 | record.entry.Present.content = new AgentPubKey( record.entry.Present.entry ); 109 | else if ( record.entry.Present.entry_type === "CapGrant" ) 110 | record.entry.Present.content = null; 111 | } 112 | 113 | return record; 114 | }); 115 | }, 116 | 117 | // Public reading 118 | async get_keyset_root ( input ) { 119 | const result = await this.call( input ); 120 | 121 | return KeysetRoot( result ); 122 | }, 123 | async get_device_keys ( input ) { 124 | const result = await this.call( input ); 125 | 126 | return result.map( ([ hash, key_anchor ]) => ({ 127 | "hash": new EntryHash( hash ), 128 | "anchor": KeyAnchor( key_anchor ), 129 | }) ); 130 | }, 131 | async get_key_lineage ( input ) { 132 | const result = await this.call( input ); 133 | 134 | return result.map( key => new Uint8Array(key) ); 135 | }, 136 | async same_lineage ( input ) { 137 | const result = await this.call( input ); 138 | 139 | return result; 140 | }, 141 | async key_state ( input, options ) { 142 | if ( !Array.isArray( input ) ) 143 | input = [ input, Date.now() ]; 144 | 145 | // Because the 'Timestamp' type on the other side expects nano seconds 146 | if ( options?.adjust_for_nano_seconds !== false ) 147 | input[1] *= 1000; 148 | 149 | const result = await this.call( input ); 150 | 151 | return KeyState( result ); 152 | }, 153 | 154 | // Key Registration 155 | async next_derivation_details ( input ) { 156 | const result = await this.call( input ); 157 | 158 | return result; 159 | }, 160 | async get_key_derivation_details ( input ) { 161 | const result = await this.call( input ); 162 | 163 | return result; 164 | }, 165 | async check_existing_derivation_details ( input ) { 166 | const result = await this.call( input ); 167 | 168 | return result === null 169 | ? null 170 | : { 171 | "app_binding": AppBinding( result[0] ), 172 | "key_meta": KeyMeta( result[1] ), 173 | }; 174 | }, 175 | async create_key ( input ) { 176 | const result = await this.call( input ); 177 | 178 | return [ 179 | new ActionHash( result[0] ), 180 | KeyRegistrationEntry( result[1] ), 181 | KeyMeta( result[2] ), 182 | ]; 183 | }, 184 | async update_key ( input ) { 185 | const result = await this.call( input ); 186 | 187 | return [ 188 | new ActionHash( result[0] ), 189 | KeyRegistrationEntry( result[1] ), 190 | KeyMeta( result[2] ), 191 | ]; 192 | }, 193 | async revoke_key ( input ) { 194 | const result = await this.call( input ); 195 | 196 | return [ 197 | new ActionHash( result[0] ), 198 | KeyRegistrationEntry( result[1] ), 199 | ]; 200 | }, 201 | async delete_key_registration ( input ) { 202 | const result = await this.call( input ); 203 | 204 | return [ 205 | new ActionHash( result[0] ), 206 | KeyRegistrationEntry( input[1] ), 207 | ]; 208 | }, 209 | 210 | // Change Rules 211 | async update_change_rule ( input ) { 212 | const result = await this.call( input ); 213 | 214 | return new ChangeRule( result ); 215 | }, 216 | async construct_authority_spec ( input ) { 217 | const result = await this.call( input ); 218 | 219 | return { 220 | "authority_spec": AuthoritySpec( result[0] ), 221 | "serialized": new Uint8Array( result[1] ), 222 | }; 223 | }, 224 | async get_current_change_rule_for_ksr ( input ) { 225 | const result = await this.call( input ); 226 | 227 | return new ChangeRule( result ); 228 | }, 229 | "get_ksr_change_rule_links": true, 230 | 231 | 232 | // 233 | // Virtual functions 234 | // 235 | async get_ksr_keys ( input ) { 236 | const ksr = await this.functions.get_keyset_root ( input ); 237 | 238 | return await this.functions.get_device_keys( ksr.first_deepkey_agent ); 239 | }, 240 | }; 241 | 242 | const APP_ENTRY_STRUCTS_MAP = { 243 | ChangeRule, 244 | KeysetRoot, 245 | KeyMeta, 246 | KeyAnchor, 247 | AppBinding, 248 | "KeyRegistration": KeyRegistrationEntry, 249 | }; 250 | 251 | function formatSignal ( signal ) { 252 | if ( signal.action ) { 253 | signal.signed_action = SignedAction( signal.action ); 254 | signal.action = signal.signed_action.hashed.content; 255 | } 256 | 257 | if ( signal.app_entry ) { 258 | const app_entry_type = signal.app_entry.type; 259 | const struct = APP_ENTRY_STRUCTS_MAP[ app_entry_type ]; 260 | 261 | if ( struct === undefined ) 262 | throw new TypeError(`No AppEntry struct for type '${app_entry_type}'`); 263 | 264 | signal.app_entry_type = app_entry_type; 265 | signal.app_entry = struct( signal.app_entry ); 266 | } 267 | 268 | // console.log("Signal", JSON.stringify(signal,null,4) ); 269 | return signal; 270 | } 271 | 272 | const signals = { 273 | EntryCreated ( signal ) { 274 | formatSignal( signal ); 275 | 276 | // if ( signal.action ) { 277 | // console.log( 278 | // " %s Action => [%s]", 279 | // signal.action.type, signal.signed_action.hashed.hash, JSON.stringify(signal.action,null,4) 280 | // ); 281 | // } 282 | 283 | // if ( signal.app_entry ) { 284 | // console.log( 285 | // "SIGNAL: AppEntry => [%s]", signal.app_entry_type, JSON.stringify(signal.app_entry,null,4) 286 | // ); 287 | // } 288 | }, 289 | LinkCreated ( signal ) { 290 | formatSignal( signal ); 291 | 292 | // console.log( 293 | // "SIGNAL: LinkType => [%s]", signal.action.type, signal.link_type 294 | // ); 295 | }, 296 | }; 297 | 298 | export const DeepKeyCSRZomelet = new Zomelet({ 299 | functions, 300 | signals, 301 | }); 302 | 303 | 304 | export const DeepKeyCell = new CellZomelets({ 305 | "deepkey_csr": DeepKeyCSRZomelet, 306 | }); 307 | 308 | 309 | export * from './types.js'; 310 | 311 | export default { 312 | // Zomelets 313 | DeepKeyCSRZomelet, 314 | 315 | // CellZomelets 316 | DeepKeyCell, 317 | }; 318 | -------------------------------------------------------------------------------- /zomes/deepkey/src/validation/update_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | EntryTypes, 3 | EntryTypesUnit, 4 | 5 | KeyAnchor, 6 | KeyRegistration, 7 | KeyRevocation, 8 | ChangeRule, 9 | 10 | utils, 11 | 12 | validation::create_entry::{ 13 | validate_key_generation, 14 | }, 15 | }; 16 | 17 | use hdi::prelude::*; 18 | use hdi_extensions::{ 19 | summon_app_entry, 20 | 21 | // Macros 22 | guest_error, 23 | valid, invalid, 24 | }; 25 | 26 | 27 | pub fn validation( 28 | app_entry: EntryTypes, 29 | update: Update, 30 | original_action_hash: ActionHash, 31 | _original_entry_hash: EntryHash 32 | ) -> ExternResult { 33 | match app_entry { 34 | EntryTypes::KeysetRoot(_) => { 35 | invalid!(format!("Keyset Roots cannot be updated")) 36 | }, 37 | EntryTypes::ChangeRule(change_rule_entry) => { 38 | let base_change_rule_record = must_get_valid_record( original_action_hash.clone() )?; 39 | let base_change_rule_entry : ChangeRule = base_change_rule_record.try_into()?; 40 | 41 | // Keyset Root cannot be updated 42 | if base_change_rule_entry.keyset_root != change_rule_entry.keyset_root { 43 | invalid!(format!( 44 | "The 'keyset_root' of this change rule cannot be changed; original entry 'keyset_root' is: {}", 45 | base_change_rule_entry.keyset_root, 46 | )) 47 | } 48 | 49 | // Original action hash must be the ChangeRule create record 50 | // 51 | // NOTE: we cannot rely on 'original_action_hash' because we don't yet know how to 52 | // ensure it is the same chain. Waiting to find out how 'author' equivalency will be 53 | // checked with agent updates. 54 | let create_change_rule_action = utils::base_change_rule( 55 | &update.author, 56 | &update.prev_action, 57 | )?; 58 | 59 | if create_change_rule_action.action_address() != &original_action_hash { 60 | invalid!(format!( 61 | "The 'original_action_hash' is expected to be the ChangeRule create ({}); not '{}'", 62 | create_change_rule_action.action_address(), 63 | original_action_hash, 64 | )) 65 | } 66 | 67 | let new_authority_spec = change_rule_entry.spec_change.new_spec; 68 | // Cannot require more signatures than there are authorities 69 | if new_authority_spec.sigs_required == 0 { 70 | invalid!("Required signatures cannot be 0".to_string()) 71 | } 72 | 73 | // Cannot require more signatures than there are authorities 74 | if (new_authority_spec.sigs_required as usize) > new_authority_spec.authorized_signers.len() { 75 | invalid!(format!( 76 | "There are not enough authorities ({}) to satisfy the signatures required ({})", 77 | new_authority_spec.authorized_signers.len(), 78 | new_authority_spec.sigs_required, 79 | )) 80 | } 81 | 82 | // Get previous change rule 83 | let prev_change_rule = match utils::prev_change_rule( 84 | &update.author, 85 | &update.prev_action, 86 | )? { 87 | Some(change_rule) => change_rule, 88 | None => invalid!(format!( 89 | "No change rule found before action seq ({}) [{}]", 90 | update.action_seq, update.prev_action 91 | )), 92 | }; 93 | 94 | // Get authorized spec change and check signatures against previous authorities 95 | let sigs_required = &prev_change_rule.spec_change.new_spec.sigs_required; 96 | let authorities = &prev_change_rule.spec_change.new_spec.authorized_signers; 97 | let sig_count = change_rule_entry.spec_change.authorization_of_new_spec.len() as u8; 98 | 99 | if sig_count < *sigs_required { 100 | invalid!(format!( 101 | "Signature count ({}) is not enough; change rule requires at least {} signatures", 102 | sig_count, sigs_required, 103 | )) 104 | } 105 | 106 | utils::check_authorities( 107 | authorities, 108 | &change_rule_entry.spec_change.authorization_of_new_spec, 109 | &utils::serialize( &new_authority_spec )?, 110 | )?; 111 | 112 | valid!() 113 | }, 114 | EntryTypes::KeyRegistration(key_registration_entry) => { 115 | let prior_key_reg_entry : KeyRegistration = summon_app_entry( 116 | &original_action_hash.into() 117 | )?; 118 | 119 | if let KeyRegistration::CreateOnly(_) = prior_key_reg_entry { 120 | invalid!(format!( 121 | "Key registered using 'CreateOnly' cannot be updated" 122 | )) 123 | } 124 | 125 | match key_registration_entry { 126 | KeyRegistration::Create(..) | 127 | KeyRegistration::CreateOnly(..)=> { 128 | invalid!(format!( 129 | "KeyRegistration enum must be 'Update' or 'Delete'; not 'Create' or 'CreateOnly'" 130 | )) 131 | }, 132 | KeyRegistration::Update( key_rev, key_gen ) => { 133 | validate_key_revocation( &key_rev, &update )?; 134 | validate_key_generation( &key_gen, &update.into() )?; 135 | 136 | valid!() 137 | }, 138 | KeyRegistration::Delete( key_rev ) => { 139 | validate_key_revocation( &key_rev, &update )?; 140 | 141 | valid!() 142 | }, 143 | } 144 | }, 145 | EntryTypes::KeyAnchor(key_anchor_entry) => { 146 | // Check previous action is a key registration that matches this key anchor 147 | let key_reg : KeyRegistration = summon_app_entry( &update.prev_action.into() )?; 148 | 149 | let (key_rev, key_gen) = match key_reg { 150 | KeyRegistration::Update(key_rev, key_gen) => (key_rev, key_gen), 151 | _ => invalid!(format!( 152 | "KeyAnchor update must be preceeded by a KeyRegistration::Update" 153 | )), 154 | }; 155 | 156 | // Check new key 157 | if KeyAnchor::try_from( &key_gen.new_key )? != key_anchor_entry { 158 | invalid!(format!( 159 | "KeyAnchor does not match KeyRegistration new key: {:#?} != {}", 160 | key_anchor_entry, key_gen.new_key, 161 | )) 162 | } 163 | 164 | // Check revoked key - updated anchor must match the revoked registrations anchor 165 | let prior_key_anchor_entry : KeyAnchor = summon_app_entry( &original_action_hash.into() )?; 166 | let prior_key_reg : KeyRegistration = summon_app_entry( 167 | &key_rev.prior_key_registration.into() 168 | )?; 169 | 170 | if prior_key_reg.key_anchor()? != prior_key_anchor_entry { 171 | invalid!(format!( 172 | "Original KeyAnchor does not match prior KeyRegistration key anchor: {:#?} != {:#?}", 173 | prior_key_anchor_entry, prior_key_reg.key_anchor()?, 174 | )) 175 | } 176 | 177 | valid!() 178 | }, 179 | EntryTypes::KeyMeta(_key_meta_entry) => { 180 | invalid!(format!("Key Meta cannot be updated")) 181 | }, 182 | EntryTypes::AppBinding(_app_binding_entry) => { 183 | invalid!(format!("App Binding cannot be updated")) 184 | }, 185 | // _ => invalid!(format!("Update validation not implemented for entry type: {:#?}", update.entry_type )), 186 | } 187 | } 188 | 189 | 190 | pub fn validate_key_revocation(key_rev: &KeyRevocation, update: &Update) -> ExternResult<()> { 191 | // KeyRevocation { 192 | // prior_key_registration: ActionHash, 193 | // revocation_authorization: Vec, 194 | // } 195 | 196 | // Make sure the target key belongs to this KSR 197 | let key_registration_action = must_get_action( key_rev.prior_key_registration.to_owned() )?; 198 | 199 | if *key_registration_action.hashed.author() != update.author { 200 | Err(guest_error!(format!( 201 | "Author '{}' cannot revoke key registered by another author ({})", 202 | update.author, key_registration_action.hashed.author(), 203 | )))? 204 | } 205 | 206 | // Prevent duplicate updates to the same 'prior_key_registration' 207 | let activities = must_get_agent_activity( 208 | update.author.to_owned(), 209 | ChainFilter::new( update.prev_action.to_owned() ) 210 | .until( key_rev.prior_key_registration.to_owned() ) 211 | .include_cached_entries() 212 | )?; 213 | 214 | let entry_type = EntryType::try_from( EntryTypesUnit::KeyRegistration )?; 215 | let filtered_activities : Vec = activities.into_iter().filter( 216 | |activity| match activity.action.action().entry_type() { 217 | Some(et) => et == &entry_type, 218 | None => false, 219 | } 220 | ).collect(); 221 | 222 | for activity in filtered_activities { 223 | let prior_key_reg : KeyRegistration = match activity.cached_entry { 224 | Some(entry) => entry, 225 | None => must_get_entry( 226 | activity.action.action().entry_hash().unwrap().to_owned() 227 | )?.content, 228 | }.try_into()?; 229 | 230 | let prior_key_rev = match prior_key_reg { 231 | KeyRegistration::Create(..) | 232 | KeyRegistration::CreateOnly(..)=> continue, 233 | KeyRegistration::Update( prior_key_rev, _ ) => prior_key_rev, 234 | KeyRegistration::Delete( prior_key_rev ) => prior_key_rev, 235 | }; 236 | 237 | if prior_key_rev.prior_key_registration == key_rev.prior_key_registration { 238 | Err(guest_error!(format!( 239 | "There is already a KeyRegistration ({}) that revokes '{}'", 240 | activity.action.action_address(), 241 | key_rev.prior_key_registration, 242 | )))? 243 | } 244 | } 245 | 246 | // Get the current change rule 247 | let prev_change_rule = utils::prev_change_rule( &update.author, &update.prev_action )? 248 | .ok_or(guest_error!(format!( 249 | "No change rule found before action seq ({}) [{}]", 250 | update.action_seq, update.prev_action 251 | )))?; 252 | 253 | let sigs_required = &prev_change_rule.spec_change.new_spec.sigs_required; 254 | let authorities = &prev_change_rule.spec_change.new_spec.authorized_signers; 255 | let sig_count = key_rev.revocation_authorization.len() as u8; 256 | 257 | if sig_count < *sigs_required { 258 | Err(guest_error!(format!( 259 | "Signature count ({}) is not enough; key revocation requires at least {} signatures", 260 | sig_count, sigs_required, 261 | )))? 262 | } 263 | 264 | // Check authorizations against change rule authorities 265 | utils::check_authorities( 266 | authorities, 267 | &key_rev.revocation_authorization, 268 | &key_rev.prior_key_registration.clone().into_inner(), 269 | )?; 270 | 271 | Ok(()) 272 | } 273 | -------------------------------------------------------------------------------- /tests/integration/test_claim_unmanaged_key.js: -------------------------------------------------------------------------------- 1 | import { Logger } from '@whi/weblogger'; 2 | const log = new Logger("test-basic", process.env.LOG_LEVEL ); 3 | 4 | // import why from 'why-is-node-running'; 5 | 6 | import path from 'path'; 7 | import crypto from 'crypto'; 8 | 9 | import { expect } from 'chai'; 10 | 11 | import * as ed from '@noble/ed25519'; 12 | 13 | import json from '@whi/json'; 14 | import { 15 | HoloHash, 16 | DnaHash, AgentPubKey, 17 | ActionHash, EntryHash, 18 | } from '@spartan-hc/holo-hash'; 19 | import { Holochain } from '@spartan-hc/holochain-backdrop'; 20 | 21 | import { 22 | DeepKeyCell, 23 | } from '@holochain/deepkey-zomelets'; 24 | import { 25 | AppInterfaceClient, 26 | } from '@spartan-hc/app-interface-client'; 27 | 28 | import { 29 | expect_reject, 30 | linearSuite, 31 | } from '../utils.js'; 32 | import { 33 | KeyStore, 34 | } from '../key_store.js'; 35 | 36 | 37 | const __dirname = path.dirname( new URL(import.meta.url).pathname ); 38 | const DEEPKEY_DNA_PATH = path.join( __dirname, "../../dnas/deepkey.dna" ); 39 | 40 | const dna1_hash = new DnaHash( crypto.randomBytes( 32 ) ); 41 | 42 | const alice1_app1_id = "alice1-app1"; 43 | const alice2_app1_id = "alice2-app1"; 44 | 45 | const ALICE1_DEVICE_SEED = Buffer.from("jJQhp80zPT+XBMOZmtfwdBqY9ay9k2w520iwaet1if4=", "base64"); 46 | const alice1_key_store = new KeyStore( ALICE1_DEVICE_SEED, "alice1" ); 47 | 48 | const revocation_key1 = ed.utils.randomPrivateKey(); 49 | const revocation_key2 = ed.utils.randomPrivateKey(); 50 | const rev1_pubkey = await ed.getPublicKeyAsync( revocation_key1 ); 51 | const rev2_pubkey = await ed.getPublicKeyAsync( revocation_key2 ); 52 | 53 | let app_port; 54 | let installations; 55 | 56 | 57 | describe("DeepKey", function () { 58 | const holochain = new Holochain({ 59 | "timeout": 20_000, 60 | "default_stdout_loggers": log.level_rank > 3, 61 | }); 62 | 63 | before(async function () { 64 | this.timeout( 60_000 ); 65 | 66 | installations = await holochain.install([ 67 | "alice_host", 68 | "alice1", 69 | "alice2", 70 | ], { 71 | "app_name": "test", 72 | "bundle": { 73 | "deepkey": DEEPKEY_DNA_PATH, 74 | }, 75 | }); 76 | 77 | app_port = await holochain.ensureAppPort(); 78 | }); 79 | 80 | linearSuite("Basic", basic_tests ); 81 | 82 | after(async () => { 83 | await holochain.destroy(); 84 | }); 85 | }); 86 | 87 | 88 | function basic_tests () { 89 | let client; 90 | 91 | // Hosted Alice deepkey 92 | let hosted_alice_client; 93 | let hosted_alice_deepkey; 94 | 95 | // Alice deepkey 96 | let alice1_client; 97 | let alice1_deepkey; 98 | let alice2_client; 99 | let alice2_deepkey; 100 | 101 | // Alice key1 102 | const app_index = 1; // 0 is reserved for the deepkey cell agent 103 | const key_index = 0; 104 | const key1a_path = `app/${app_index}/key/${key_index}`; 105 | let alice1_key1a; 106 | 107 | // Hosted Alice key1 108 | let hosted_alice_key1a_reg, hosted_alice_key1a_reg_addr; 109 | // Alice1 key1 110 | let alice1_key1a_reg, alice1_key1a_reg_addr; 111 | // Alice2 key1 112 | let alice2_key1a_reg, alice2_key1a_reg_addr; 113 | 114 | before(async function () { 115 | this.timeout( 30_000 ); 116 | 117 | client = new AppInterfaceClient( app_port, { 118 | "logging": process.env.LOG_LEVEL || "normal", 119 | }); 120 | 121 | const hosted_alice_token = installations.alice_host.test.auth.token; 122 | hosted_alice_client = await client.app( hosted_alice_token ); 123 | 124 | const alice1_token = installations.alice1.test.auth.token; 125 | alice1_client = await client.app( alice1_token ); 126 | 127 | const alice2_token = installations.alice2.test.auth.token; 128 | alice2_client = await client.app( alice2_token ); 129 | 130 | { 131 | const { 132 | deepkey, 133 | } = hosted_alice_client.createInterface({ 134 | "deepkey": DeepKeyCell, 135 | }); 136 | 137 | hosted_alice_deepkey = deepkey.zomes.deepkey_csr.functions; 138 | } 139 | { 140 | const { 141 | deepkey, 142 | } = alice1_client.createInterface({ 143 | "deepkey": DeepKeyCell, 144 | }); 145 | 146 | alice1_deepkey = deepkey.zomes.deepkey_csr.functions; 147 | } 148 | { 149 | const { 150 | deepkey, 151 | } = alice2_client.createInterface({ 152 | "deepkey": DeepKeyCell, 153 | }); 154 | 155 | alice2_deepkey = deepkey.zomes.deepkey_csr.functions; 156 | } 157 | 158 | const hosted_alice_ksr_addr = await hosted_alice_deepkey.query_keyset_authority_action_hash(); 159 | const alice1_ksr_addr = await alice1_deepkey.query_keyset_authority_action_hash(); 160 | const alice2_ksr_addr = await alice2_deepkey.query_keyset_authority_action_hash(); 161 | 162 | const auth_spec_package = await alice1_deepkey.construct_authority_spec({ 163 | "sigs_required": 1, 164 | "authorized_signers": [ 165 | rev1_pubkey, 166 | rev2_pubkey, 167 | ], 168 | }); 169 | const new_change_rule = await alice1_deepkey.update_change_rule({ 170 | "authority_spec": auth_spec_package.authority_spec, 171 | }); 172 | log.normal("New Change Rule: %s", json.debug(new_change_rule) ); 173 | 174 | alice1_key1a = await alice1_key_store.createKey( key1a_path ); 175 | }); 176 | 177 | it("should register unmanaged key (alice)", async function () { 178 | this.timeout( 5_000 ); 179 | 180 | const [ addr, key_reg, key_meta ] = await hosted_alice_deepkey.create_key({ 181 | "app_binding": { 182 | "app_name": "Alice - App #1", 183 | "installed_app_id": alice1_app1_id, 184 | "dna_hashes": [ dna1_hash ], 185 | }, 186 | "key_generation": { 187 | "new_key": await alice1_key1a.getAgent(), 188 | "new_key_signing_of_author": await alice1_key1a.sign( hosted_alice_client.agent_id ), 189 | }, 190 | "derivation_details": { 191 | app_index, 192 | key_index, 193 | "derivation_seed": alice1_key_store.seed, 194 | "derivation_bytes": alice1_key1a.derivation_bytes, 195 | }, 196 | "create_only": true, 197 | }); 198 | log.normal("Key Registration (%s): %s", addr, json.debug(key_reg) ); 199 | log.normal("Key Meta: %s", json.debug(key_meta) ); 200 | log.normal("Key registration (create) addr: %s", addr ); 201 | 202 | hosted_alice_key1a_reg = key_reg; 203 | hosted_alice_key1a_reg_addr = addr; 204 | 205 | { 206 | const key_state = await hosted_alice_deepkey.key_state( 207 | await alice1_key1a.getBytes() 208 | ); 209 | log.normal("Key (1a) state: %s", json.debug(key_state) ); 210 | 211 | expect( key_state ).to.have.key( "Valid" ); 212 | } 213 | }); 214 | 215 | it("should claim unmanaged key", async function () { 216 | this.timeout( 5_000 ); 217 | 218 | const [ addr, key_reg, key_meta ] = await alice1_deepkey.create_key({ 219 | "app_binding": { 220 | "app_name": "Alice1 - App #1", 221 | "installed_app_id": alice1_app1_id, 222 | "dna_hashes": [ dna1_hash ], 223 | }, 224 | "key_generation": { 225 | "new_key": await alice1_key1a.getAgent(), 226 | "new_key_signing_of_author": await alice1_key1a.sign( alice1_client.agent_id ), 227 | }, 228 | "derivation_details": { 229 | app_index, 230 | key_index, 231 | "derivation_seed": alice1_key_store.seed, 232 | "derivation_bytes": alice1_key1a.derivation_bytes, 233 | }, 234 | }); 235 | log.normal("Key Registration (%s): %s", addr, json.debug(key_reg) ); 236 | log.normal("Key Meta: %s", json.debug(key_meta) ); 237 | log.normal("Key registration (update) addr: %s", addr ); 238 | 239 | alice1_key1a_reg = key_reg; 240 | alice1_key1a_reg_addr = addr; 241 | }); 242 | 243 | it("should fail to update hosted key", async function () { 244 | this.timeout( 10_000 ); 245 | 246 | await expect_reject(async () => { 247 | await hosted_alice_deepkey.revoke_key({ 248 | "key_revocation": { 249 | "prior_key_registration": hosted_alice_key1a_reg_addr, 250 | "revocation_authorization": [ 251 | [ 0, await alice1_key1a.sign( hosted_alice_key1a_reg_addr ) ], 252 | ], 253 | }, 254 | }); 255 | }, "cannot be updated" ); 256 | }); 257 | 258 | it("should update key", async function () { 259 | this.timeout( 10_000 ); 260 | 261 | const [ addr, key_reg ] = await alice1_deepkey.revoke_key({ 262 | "key_revocation": { 263 | "prior_key_registration": alice1_key1a_reg_addr, 264 | "revocation_authorization": [ 265 | [ 0, await ed.signAsync( alice1_key1a_reg_addr, revocation_key1 ) ], 266 | ], 267 | }, 268 | }); 269 | log.normal("Key Registration (%s): %s", addr, json.debug(key_reg) ); 270 | log.normal("Key registration (update) addr: %s", addr ); 271 | 272 | { 273 | const key_state = await hosted_alice_deepkey.key_state( 274 | await alice1_key1a.getBytes() 275 | ); 276 | log.normal("Key (1a) state: %s", json.debug(key_state) ); 277 | 278 | expect( key_state ).to.have.key( "Invalid" ); 279 | } 280 | }); 281 | 282 | it("should claim unmanaged key with another chain", async function () { 283 | this.timeout( 5_000 ); 284 | 285 | const [ addr, key_reg, key_meta ] = await alice2_deepkey.create_key({ 286 | "app_binding": { 287 | "app_name": "Alice2 - App #1", 288 | "installed_app_id": alice2_app1_id, 289 | "dna_hashes": [ dna1_hash ], 290 | }, 291 | "key_generation": { 292 | "new_key": await alice1_key1a.getAgent(), 293 | "new_key_signing_of_author": await alice1_key1a.sign( alice2_client.agent_id ), 294 | }, 295 | "derivation_details": { 296 | app_index, 297 | key_index, 298 | "derivation_seed": alice1_key_store.seed, 299 | "derivation_bytes": alice1_key1a.derivation_bytes, 300 | }, 301 | }); 302 | log.normal("Key Registration (%s): %s", addr, json.debug(key_reg) ); 303 | log.normal("Key Meta: %s", json.debug(key_meta) ); 304 | log.normal("Key registration (update) addr: %s", addr ); 305 | 306 | alice2_key1a_reg = key_reg; 307 | alice2_key1a_reg_addr = addr; 308 | 309 | { 310 | const key_state = await hosted_alice_deepkey.key_state( 311 | await alice1_key1a.getBytes() 312 | ); 313 | log.normal("Key (1a) state: %s", json.debug(key_state) ); 314 | 315 | expect( key_state ).to.have.key( "Invalid" ); 316 | } 317 | }); 318 | 319 | after(async function () { 320 | await client.close(); 321 | }); 322 | } 323 | -------------------------------------------------------------------------------- /zomes/deepkey_csr/src/key_registration.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | utils, 3 | deepkey_sdk, 4 | }; 5 | use serde_bytes::ByteArray; 6 | use deepkey::*; 7 | use hdk::prelude::*; 8 | use hdk_extensions::{ 9 | must_get, 10 | hdi_extensions::{ 11 | guest_error, 12 | ScopedTypeConnector, 13 | }, 14 | }; 15 | pub use deepkey_sdk::{ 16 | AppBindingInput, 17 | CreateKeyInput, 18 | UpdateKeyInput, 19 | RevokeKeyInput, 20 | KeyRevocationInput, 21 | DerivationDetails, 22 | DerivationDetailsInput, 23 | }; 24 | 25 | 26 | #[hdk_extern] 27 | pub fn next_derivation_details( 28 | key_bytes: ByteArray<32> 29 | ) -> ExternResult { 30 | let (_, app_binding) = crate::app_binding::query_app_binding_by_key( key_bytes )?; 31 | 32 | let next_key_index = crate::key_meta::query_next_key_index_for_app_index( 33 | app_binding.app_index 34 | )?; 35 | 36 | Ok(DerivationDetails { 37 | app_index: app_binding.app_index, 38 | key_index: next_key_index, 39 | }) 40 | } 41 | 42 | 43 | #[hdk_extern] 44 | pub fn get_key_derivation_details( 45 | key_bytes: ByteArray<32> 46 | ) -> ExternResult { 47 | let key_meta = crate::key_meta::query_key_meta_for_key( key_bytes.clone() )?; 48 | let (_, app_binding) = crate::app_binding::query_app_binding_by_key( key_bytes.clone() )?; 49 | 50 | Ok( 51 | DerivationDetails { 52 | app_index: app_binding.app_index, 53 | key_index: key_meta.key_index, 54 | } 55 | ) 56 | } 57 | 58 | 59 | #[hdk_extern] 60 | pub fn check_existing_derivation_details( 61 | derivation_details: DerivationDetailsInput, 62 | ) -> ExternResult> { 63 | for key_meta in crate::key_meta::query_key_metas(())? { 64 | if let Some(ref derivation_bytes) = key_meta.derivation_bytes { 65 | if *derivation_bytes == derivation_details.derivation_bytes { 66 | let app_binding = crate::app_binding::query_app_binding_by_action( 67 | key_meta.app_binding_addr.to_owned() 68 | )?; 69 | 70 | return Ok(Some((app_binding, key_meta))); 71 | } 72 | } 73 | 74 | let app_binding = crate::app_binding::query_app_binding_by_action( 75 | key_meta.app_binding_addr.to_owned() 76 | )?; 77 | 78 | if derivation_details.app_index == app_binding.app_index && 79 | Some(derivation_details.derivation_seed.to_owned()) == key_meta.derivation_seed { 80 | return Ok(Some((app_binding, key_meta))); 81 | } 82 | } 83 | 84 | Ok(None) 85 | } 86 | 87 | 88 | /// Register a new app/key pair and create associated private entries 89 | /// 90 | /// #### Example usage 91 | /// ```rust, no_run 92 | /// # use hdk::prelude::*; 93 | /// # use deepkey::*; 94 | /// # use hc_deepkey_sdk::*; 95 | /// # fn main() -> ExternResult<()> { 96 | /// // Generates a new key in Lair 97 | /// let new_key = AgentPubKey::from_raw_32( create_x25519_keypair()?.as_ref().to_vec() ); 98 | /// // Sign this cell's agent with using the new key 99 | /// let new_key_signing_of_author = sign_raw( 100 | /// new_key.clone(), 101 | /// agent_info()?.agent_initial_pubkey.into_inner() 102 | /// )?; 103 | /// 104 | /// let key_generation = KeyGeneration { 105 | /// new_key, 106 | /// new_key_signing_of_author, 107 | /// }; 108 | /// 109 | /// // Mock app info 110 | /// let app_binding = AppBindingInput { 111 | /// app_name: "Example App".to_string(), 112 | /// installed_app_id: "example-app".to_string(), 113 | /// dna_hashes: vec![ 114 | /// DnaHash::try_from("uhC0khcxkMswniVr_dwGJAo2spTGC-hafG0lCEvzS_PJughwa4_6d").unwrap(), 115 | /// ], 116 | /// metadata: Default::default(), 117 | /// }; 118 | /// 119 | /// // Here is an example of submitting the derivation details; however, in this case it should 120 | /// // be set to `None` because we did not derive this key. 121 | /// let derivation_details = Some(DerivationDetailsInput { 122 | /// app_index: 1, // 0 is already used by the deepkey app 123 | /// key_index: 0, 124 | /// derivation_seed: vec![], 125 | /// derivation_bytes: vec![], 126 | /// }); 127 | /// 128 | /// let result = deepkey_csr::key_registration::create_key(CreateKeyInput { 129 | /// key_generation, 130 | /// app_binding, 131 | /// derivation_details, 132 | /// create_only: false, 133 | /// }); 134 | /// # Ok(()) 135 | /// # } 136 | /// ``` 137 | #[hdk_extern] 138 | pub fn create_key(input: CreateKeyInput) -> ExternResult<(ActionHash, KeyRegistration, KeyMeta)> { 139 | let key_gen = input.key_generation; 140 | let next_app_index = crate::app_binding::query_next_app_index(())?; 141 | 142 | // Derive Key Anchor 143 | let key_anchor = KeyAnchor::try_from( &key_gen.new_key )?; 144 | 145 | // Create Registration 146 | let key_gen = KeyGeneration::from(( 147 | &key_gen.new_key, 148 | &key_gen.new_key_signing_of_author 149 | )); 150 | let key_reg = match input.create_only { 151 | true => KeyRegistration::CreateOnly(key_gen), 152 | false => KeyRegistration::Create(key_gen), 153 | }; 154 | let key_reg_addr = create_entry( key_reg.to_input() )?; 155 | 156 | // Create Anchor 157 | let key_anchor_addr = crate::key_anchor::create_key_anchor( key_anchor )?; 158 | 159 | // Create App Binding 160 | let app_binding = AppBinding { 161 | app_index: next_app_index, 162 | app_name: input.app_binding.app_name, 163 | installed_app_id: input.app_binding.installed_app_id, 164 | dna_hashes: input.app_binding.dna_hashes, 165 | metadata: input.app_binding.metadata, 166 | }; 167 | let app_binding_addr = create_entry( app_binding.to_input() )?; 168 | 169 | // Create Meta 170 | let key_meta = KeyMeta { 171 | app_binding_addr: app_binding_addr.clone(), 172 | key_index: 0, 173 | key_registration_addr: key_reg_addr.clone(), 174 | key_anchor_addr: key_anchor_addr.clone(), 175 | derivation_seed: input.derivation_details.as_ref() 176 | .map( |details| details.derivation_seed.to_owned() ), 177 | derivation_bytes: input.derivation_details.as_ref() 178 | .map( |details| details.derivation_bytes.to_owned() ), 179 | }; 180 | create_entry( key_meta.to_input() )?; 181 | 182 | Ok(( 183 | key_reg_addr, 184 | key_reg, 185 | key_meta, 186 | )) 187 | } 188 | 189 | 190 | /// Register a key update for an existing app/key pair and create associated private entries 191 | /// 192 | /// #### Example usage 193 | /// ```rust, no_run 194 | /// # use hdk::prelude::*; 195 | /// # use deepkey::*; 196 | /// # use hc_deepkey_sdk::*; 197 | /// # fn main() -> ExternResult<()> { 198 | /// let prior_key_registration = ActionHash::try_from("uhCkkzhwfnkYh7CWji2KpS2wO6YaKOKPQ4-kr4XGRBRRx9hitvOw9").unwrap(); 199 | /// 200 | /// // Assuming the default change rules are still in place 201 | /// let revocation_authorization = vec![ 202 | /// ( 203 | /// 0, // The index of the FDA authority 204 | /// sign_raw( // Sign prior registration using FDA 205 | /// agent_info()?.agent_initial_pubkey, 206 | /// prior_key_registration.clone().into_inner() 207 | /// )? 208 | /// ), 209 | /// ]; 210 | /// 211 | /// let key_revocation = KeyRevocation { 212 | /// prior_key_registration, 213 | /// revocation_authorization, 214 | /// }; 215 | /// 216 | /// // Generates a new key in Lair 217 | /// let new_key = AgentPubKey::from_raw_32( create_x25519_keypair()?.as_ref().to_vec() ); 218 | /// // Sign this cell's agent with using the new key 219 | /// let new_key_signing_of_author = sign_raw( 220 | /// new_key.clone(), 221 | /// agent_info()?.agent_initial_pubkey.into_inner() 222 | /// )?; 223 | /// 224 | /// let key_generation = KeyGeneration { 225 | /// new_key, 226 | /// new_key_signing_of_author, 227 | /// }; 228 | /// 229 | /// // Here is an example of submitting the derivation details; however, in this case it should 230 | /// // be set to `None` because we did not derive this key. 231 | /// let derivation_details = Some(DerivationDetailsInput { 232 | /// app_index: 1, 233 | /// key_index: 1, // Next key index 234 | /// derivation_seed: vec![], 235 | /// derivation_bytes: vec![], 236 | /// }); 237 | /// 238 | /// let result = deepkey_csr::key_registration::update_key(UpdateKeyInput { 239 | /// key_revocation, 240 | /// key_generation, 241 | /// derivation_details, 242 | /// }); 243 | /// # Ok(()) 244 | /// # } 245 | /// ``` 246 | #[hdk_extern] 247 | pub fn update_key(input: UpdateKeyInput) -> ExternResult<(ActionHash, KeyRegistration, KeyMeta)> { 248 | let key_rev = input.key_revocation; 249 | let key_gen = input.key_generation; 250 | let prior_key_reg_addr = key_rev.prior_key_registration.clone(); 251 | 252 | let prior_key_meta = crate::key_meta::query_key_meta_for_registration( 253 | prior_key_reg_addr.clone() 254 | )?; 255 | let app_binding = crate::app_binding::query_app_binding_by_action( 256 | prior_key_meta.app_binding_addr.clone() 257 | )?; 258 | let next_key_index = crate::key_meta::query_next_key_index_for_app_index( app_binding.app_index )?; 259 | 260 | // Check that derivation details match the chain state 261 | if let Some(derivation_details) = &input.derivation_details { 262 | let given_app_index = derivation_details.app_index; 263 | 264 | // Check that derivation details has the correct 'app_index' 265 | if given_app_index != app_binding.app_index { 266 | Err(guest_error!(format!( 267 | "The derivation app index does not match the app binding: [given] {} != {} [prior]", 268 | given_app_index, app_binding.app_index, 269 | )))? 270 | } 271 | } 272 | 273 | // Derive Key Anchor 274 | let key_anchor = KeyAnchor::try_from( &key_gen.new_key )?; 275 | 276 | // Create Registration 277 | let key_reg = KeyRegistration::Update( key_rev, key_gen ); 278 | let key_reg_addr = update_entry( prior_key_reg_addr.clone(), key_reg.to_input() )?; 279 | 280 | // Create Anchor 281 | let prior_key_addr = crate::key_anchor::get_key_anchor_for_registration( 282 | prior_key_reg_addr.clone() 283 | )?.0; 284 | let key_anchor_addr = update_entry( prior_key_addr, key_anchor.to_input() )?; 285 | 286 | // Create Meta 287 | let key_meta = KeyMeta { 288 | app_binding_addr: prior_key_meta.app_binding_addr.clone(), 289 | key_index: input.derivation_details.as_ref() 290 | .map( |details| details.key_index.to_owned() ) 291 | .unwrap_or( next_key_index ), 292 | key_registration_addr: key_reg_addr.clone(), 293 | key_anchor_addr: key_anchor_addr.clone(), 294 | derivation_seed: input.derivation_details.as_ref() 295 | .map( |details| details.derivation_seed.to_owned() ), 296 | derivation_bytes: input.derivation_details.as_ref() 297 | .map( |details| details.derivation_bytes.to_owned() ), 298 | }; 299 | create_entry( key_meta.to_input() )?; 300 | 301 | Ok(( 302 | key_reg_addr, 303 | key_reg, 304 | key_meta, 305 | )) 306 | } 307 | 308 | 309 | /// Register a key delete for an existing app/key pair 310 | /// 311 | /// #### Example usage 312 | /// ```rust, no_run 313 | /// # use hdk::prelude::*; 314 | /// # use deepkey::*; 315 | /// # use hc_deepkey_sdk::*; 316 | /// # fn main() -> ExternResult<()> { 317 | /// let prior_key_registration = ActionHash::try_from("uhCkkzhwfnkYh7CWji2KpS2wO6YaKOKPQ4-kr4XGRBRRx9hitvOw9").unwrap(); 318 | /// 319 | /// // Assuming the default change rules are still in place 320 | /// let revocation_authorization = vec![ 321 | /// ( 322 | /// 0, // The index of the FDA authority 323 | /// sign_raw( // Sign prior registration using FDA 324 | /// agent_info()?.agent_initial_pubkey, 325 | /// prior_key_registration.clone().into_inner() 326 | /// )? 327 | /// ), 328 | /// ]; 329 | /// 330 | /// let key_revocation = KeyRevocation { 331 | /// prior_key_registration, 332 | /// revocation_authorization, 333 | /// }; 334 | /// 335 | /// let result = deepkey_csr::key_registration::revoke_key(RevokeKeyInput { 336 | /// key_revocation, 337 | /// }); 338 | /// # Ok(()) 339 | /// # } 340 | /// ``` 341 | #[hdk_extern] 342 | pub fn revoke_key(input: RevokeKeyInput) -> ExternResult<(ActionHash, KeyRegistration)> { 343 | let key_rev = input.key_revocation; 344 | let prior_key_reg_addr = key_rev.prior_key_registration.clone(); 345 | 346 | let key_revocation = KeyRevocation { 347 | prior_key_registration: prior_key_reg_addr.clone(), 348 | revocation_authorization: key_rev.revocation_authorization, 349 | }; 350 | let registration_delete = KeyRegistration::Delete(key_revocation); 351 | 352 | let key_reg_addr = update_entry( 353 | prior_key_reg_addr.clone(), 354 | registration_delete.to_input(), 355 | )?; 356 | 357 | // Terminate key anchor 358 | let prior_key_addr = crate::key_anchor::get_key_anchor_for_registration( 359 | prior_key_reg_addr.clone() 360 | )?.0; 361 | delete_entry( prior_key_addr )?; 362 | 363 | Ok(( 364 | key_reg_addr, 365 | registration_delete, 366 | )) 367 | } 368 | 369 | 370 | #[hdk_extern] 371 | pub fn delete_key_registration( 372 | input: (ActionHash, KeyRevocationInput), 373 | ) -> ExternResult { 374 | update_entry( 375 | input.0, 376 | KeyRegistration::Delete( KeyRevocation::try_from( input.1 )? ).to_input(), 377 | ) 378 | } 379 | 380 | 381 | #[hdk_extern] 382 | pub fn get_key_registration(addr: ActionHash) -> ExternResult { 383 | must_get( &addr )?.try_into() 384 | } 385 | 386 | 387 | #[hdk_extern] 388 | fn get_latest_key_registration(addr: ActionHash) -> ExternResult<(ActionHash, KeyRegistration)> { 389 | let record = utils::get_latest_record( addr )?; 390 | 391 | Ok(( 392 | record.action_address().to_owned(), 393 | record.try_into()?, 394 | )) 395 | } 396 | --------------------------------------------------------------------------------