Compare commits

...

30 Commits

Author SHA1 Message Date
Ryan Heywood 8ddebfc3bb
keyfork-shard: traitify functionality 2024-02-12 08:46:27 -05:00
Ryan Heywood 278e5c84fd
crates: make Cargo.toml not include defaulted bin deps across crates 2024-02-12 03:09:35 -05:00
Ryan Heywood e441ef520f
keyforkd: appropriately handle or debug disconnects 2024-02-12 03:08:54 -05:00
Ryan Heywood f1c24fb33e
keyforkd: allow performing multiple requests on the same socket 2024-02-12 02:36:54 -05:00
Ryan Heywood a24a0166cc
keyforkd-client: add examples and integrity checks 2024-02-12 02:31:22 -05:00
Ryan Heywood 1209549532
keyforkd: impl test_util::run_test 2024-02-12 01:28:04 -05:00
Ryan Heywood 053902bf43
keyfork-derive-util: make variable-length seeds opt-in 2024-02-12 00:30:28 -05:00
Ryan Heywood 4354be4304
keyfork-derive-util: add arbitrary length seeds, remove length-based errors 2024-02-11 20:35:26 -05:00
Ryan Heywood 8108f5e61a
keyfork-derive-util, keyforkd-client: support fearless conversions 2024-02-11 20:20:56 -05:00
Ryan Heywood 4e2c4487e9
keyfork-qrcode: default to rqrr, keyfork default 2024-02-11 19:46:06 -05:00
Ryan Heywood 38b73b670e
docs: get mega link file, add rustdoc builder 2024-02-11 19:45:33 -05:00
Ryan Heywood 086e56bef0
keyfork-derive-util: minor refactor, tidy up publicness of modules 2024-02-11 01:04:13 -05:00
Ryan Heywood 0375ce7bdf
keyfork-derive-util: more docs!!! 2024-02-11 00:32:10 -05:00
Ryan Heywood 7817c3514e
keyfork-derive-util: add README / lib.rs docs 2024-02-10 23:58:25 -05:00
Ryan Heywood 5096df993e
keyfork-derive-util: add doctests/examples 2024-02-10 03:50:55 -05:00
Ryan Heywood f2250d00e1
keyfork-entropy: add doctests 2024-02-10 03:50:23 -05:00
Ryan Heywood 4e66367376
keyfork-slip10-test-data: fixup docs 2024-02-10 01:39:48 -05:00
Ryan Heywood aa5fde533c
keyfork-crossterm: fixup docs 2024-02-10 01:39:35 -05:00
Ryan Heywood 2b8c90fcd5
keyfork: add more documentation, unlink root README from crate 2024-02-10 01:30:50 -05:00
Ryan Heywood 1879a250c8
keyfork-shard: add instructions for sending QR code to operators 2024-02-05 20:29:43 -05:00
Ryan Heywood d6b52a8f0a
docs/shard: fixup documentation for new QR code scanning system 2024-02-04 23:06:30 -05:00
Ryan Heywood b3a05277e8
keyfork-shard: increase QR code read timeout from 30 to 60 seconds 2024-02-04 17:51:38 -05:00
Ryan Heywood e37b5f0e6a
keyfork-qrcode: add zbar as bin dep 2024-02-04 17:51:16 -05:00
Ryan Heywood 6af5ab663d
keyfork-shard: always use highest level of error correction 2024-02-02 01:23:37 -05:00
Ryan Heywood f47d7c92b8
keyfork-qrcode: enforce use of MPG1 video streams 2024-02-01 22:29:09 -05:00
Ryan Heywood 60261aa3e9
Cargo.lock: bump dependencies
Not included: generic-array 1.0.0. No significant changes require a
major version bump yet.
2024-01-26 03:35:52 -05:00
Ryan Heywood 4c0521473f
docs: add security section 2024-01-25 01:18:05 -05:00
Ryan Heywood 304b1f9baa
docs: add dependency information 2024-01-25 01:17:57 -05:00
Ryan Heywood 1112fe0870
keyfork wizard generate-shard-secret: create output file if not exists 2024-01-22 18:18:35 -05:00
Ryan Heywood 3b42ba5f00
keyfork-derive-openpgp: when converting ed25519 to cv25519, apply clamp operation 2024-01-21 18:10:36 -05:00
77 changed files with 2500 additions and 537 deletions

88
Cargo.lock generated
View File

@ -244,7 +244,7 @@ dependencies = [
"futures-lite 2.2.0",
"parking",
"polling 3.3.2",
"rustix 0.38.30",
"rustix 0.38.31",
"slab",
"tracing",
"windows-sys 0.52.0",
@ -510,9 +510,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytemuck"
version = "1.14.0"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9"
[[package]]
name = "byteorder"
@ -622,16 +622,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.31"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.48.5",
"windows-targets 0.52.0",
]
[[package]]
@ -681,9 +681,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.4.7"
version = "4.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb0d4825b75ff281318c393e8e1b80c4da9fb75a6b1d98547d389d6fe1f48d2"
checksum = "df631ae429f6613fcd3a7c1adbdb65f637271e561b03680adaa6573015dfb106"
dependencies = [
"clap",
]
@ -1023,9 +1023,9 @@ dependencies = [
[[package]]
name = "ed25519-dalek"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek",
"ed25519",
@ -1610,7 +1610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
dependencies = [
"hermit-abi",
"rustix 0.38.30",
"rustix 0.38.31",
"windows-sys 0.52.0",
]
@ -1663,6 +1663,8 @@ dependencies = [
"ecdsa",
"elliptic-curve",
"once_cell",
"sha2",
"signature",
]
[[package]]
@ -1870,6 +1872,7 @@ dependencies = [
"keyfork-slip10-test-data",
"keyforkd-models",
"serde",
"tempfile",
"thiserror",
"tokio",
"tower",
@ -1883,14 +1886,14 @@ name = "keyforkd-client"
version = "0.1.0"
dependencies = [
"bincode",
"ed25519-dalek",
"k256",
"keyfork-derive-util",
"keyfork-frame",
"keyfork-slip10-test-data",
"keyforkd",
"keyforkd-models",
"tempfile",
"thiserror",
"tokio",
]
[[package]]
@ -2255,9 +2258,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.62"
version = "0.10.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671"
checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@ -2281,9 +2284,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.98"
version = "0.9.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7"
checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
dependencies = [
"cc",
"libc",
@ -2412,18 +2415,18 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
@ -2533,7 +2536,7 @@ dependencies = [
"cfg-if",
"concurrent-queue",
"pin-project-lite",
"rustix 0.38.30",
"rustix 0.38.31",
"tracing",
"windows-sys 0.52.0",
]
@ -2583,9 +2586,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.76"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@ -2651,13 +2654,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.2"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.3",
"regex-automata 0.4.5",
"regex-syntax 0.8.2",
]
@ -2672,9 +2675,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.3"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
@ -2806,9 +2809,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.30"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
@ -2972,9 +2975,9 @@ dependencies = [
[[package]]
name = "sha1collisiondetection"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0b86a052106b16741199985c9ec2bf501f619f70c48fa479b44b093ad9a68"
checksum = "f1d5c4be690002e8a5d7638b0b7323f03c268c7a919bd8af69ce963a4dc83220"
dependencies = [
"const-oid",
"digest",
@ -3014,9 +3017,9 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
@ -3081,9 +3084,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b187f0231d56fe41bfb12034819dd2bf336422a5866de41bc3fec4b2e3883e8"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smex"
@ -3184,14 +3187,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.9.0"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [
"cfg-if",
"fastrand 2.0.1",
"redox_syscall",
"rustix 0.38.30",
"rustix 0.38.31",
"windows-sys 0.52.0",
]
@ -3212,7 +3214,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix 0.38.30",
"rustix 0.38.31",
"windows-sys 0.48.0",
]
@ -3606,7 +3608,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.30",
"rustix 0.38.31",
]
[[package]]

View File

@ -10,6 +10,20 @@ define clone-repo
test `git -C $(1) rev-parse HEAD` = $(3)
endef
docs/book: docs/src/links.md $(shell find docs/src -type f -name '*.md')
mdbook build docs
mkdir -p docs/book/rustdoc
cargo doc --no-deps
cp -r ${CARGO_TARGET_DIR}/doc/* docs/book/rustdoc/
docs/src/links.md: docs/src/links.md.template
echo "<!-- DO NOT EDIT THIS FILE MANUALLY, edit links.md.template -->" > $@
envsubst < $< >> $@
.PHONY: touch
touch:
touch docs/src/links.md.template
.PHONY: review
review:
$(eval BASE_REF_PARSED := $(shell git rev-parse $(BASE_REF)))

View File

@ -8,8 +8,8 @@ license = "MIT"
[features]
default = ["ed25519", "secp256k1"]
ed25519 = ["keyfork-derive-util/ed25519"]
secp256k1 = ["keyfork-derive-util/secp256k1"]
ed25519 = ["keyfork-derive-util/ed25519", "ed25519-dalek"]
secp256k1 = ["keyfork-derive-util/secp256k1", "k256"]
[dependencies]
keyfork-derive-util = { version = "0.1.0", path = "../../derive/keyfork-derive-util", default-features = false }
@ -17,9 +17,9 @@ keyfork-frame = { version = "0.1.0", path = "../../util/keyfork-frame" }
keyforkd-models = { version = "0.1.0", path = "../keyforkd-models" }
bincode = "1.3.3"
thiserror = "1.0.49"
k256 = { version = "0.13.3", optional = true }
ed25519-dalek = { version = "2.1.1", optional = true }
[dev-dependencies]
keyfork-slip10-test-data = { path = "../../util/keyfork-slip10-test-data" }
keyforkd = { path = "../keyforkd" }
tempfile = "3.9.0"
tokio = { version = "1.32.0", features = ["rt", "sync", "rt-multi-thread"] }

View File

@ -1,9 +1,56 @@
//! A client for Keyforkd.
//! # The Keyforkd Client
//!
//! Keyfork allows securing the master key and highest-level derivation keys by having derivation
//! requests performed against a server, "Keyforkd" or the "Keyfork Server". The server is operated
//! on a UNIX socket with messages sent using the Keyfork Frame format.
//!
//! Programs using the Keyfork Client should ensure they are built against a compatible version of
//! the Keyfork Server. For versions prior to `1.0.0`, all versions within a "minor" version (i.e.,
//! `0.5.x`) will be compatible, but `0.5.x` will not be compatible with `0.6.x`. For versions
//! after `1.0.0`, all versions within a "major" version (i.e., `1.0.0`) will be compatible, but
//! `1.x.y` will not be compatible with `2.0.0`.
//!
//! Presently, the Keyfork server only supports the following requests:
//!
//! * Derive Key
//!
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//! // use k256::SecretKey as PrivateKey;
//! // use ed25519_dalek::SigningKey as PrivateKey;
//!
//! # let seed = b"funky accordion noises";
//! # keyforkd::test_util::run_test(seed, |socket_path| {
//! # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap();
//! ```
use std::{collections::HashMap, os::unix::net::UnixStream, path::PathBuf};
pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf};
use keyfork_derive_util::{
request::{AsAlgorithm, DerivationRequest},
DerivationPath, ExtendedPrivateKey, PrivateKey,
};
use keyfork_frame::{try_decode_from, try_encode_to, DecodeError, EncodeError};
use keyforkd_models::{Request, Response, Error as KeyforkdError};
use keyforkd_models::{Error as KeyforkdError, Request, Response};
#[cfg(test)]
mod tests;
@ -11,6 +58,10 @@ mod tests;
/// An error occurred while interacting with Keyforkd.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The response from the server did not match the request.
#[error("The response from the server did not match the request")]
InvalidResponse,
/// The environment variables used for determining a Keyforkd socket path were not set.
#[error("Neither KEYFORK_SOCKET_PATH nor XDG_RUNTIME_DIR were set")]
EnvVarsNotFound,
@ -37,7 +88,7 @@ pub enum Error {
/// An error encountered in Keyforkd.
#[error("Error in Keyforkd: {0}")]
Keyforkd(#[from] KeyforkdError)
Keyforkd(#[from] KeyforkdError),
}
#[allow(missing_docs)]
@ -81,7 +132,22 @@ pub struct Client {
}
impl Client {
/// Create a new client from a given already-connected [`UnixStream`].
/// Create a new client from a given already-connected [`UnixStream`]. This function is
/// provided in case a specific UnixStream has to be used; otherwise,
/// [`Client::discover_socket`] should be preferred.
///
/// # Examples
/// ```rust
/// use keyforkd_client::{Client, get_socket};
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut socket = get_socket().unwrap();
/// let mut client = Client::new(socket);
/// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap();
/// ```
pub fn new(socket: UnixStream) -> Self {
Self { socket }
}
@ -91,10 +157,74 @@ impl Client {
/// # Errors
/// An error may be returned if the required environment variables were not set or if the
/// socket could not be connected to.
///
/// # Examples
/// ```rust
/// use keyforkd_client::Client;
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let mut client = Client::discover_socket().unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap();
/// ```
pub fn discover_socket() -> Result<Self> {
get_socket().map(|socket| Self { socket })
}
/// Request an [`ExtendedPrivateKey`] for a given [`DerivationPath`].
///
/// # Errors
/// An error may be returned if:
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
/// * Keyforkd returned invalid data.
///
/// # Examples
/// ```rust
/// use std::str::FromStr;
///
/// use keyforkd_client::Client;
/// use keyfork_derive_util::DerivationPath;
/// # use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
/// // use k256::SecretKey as PrivateKey;
/// // use ed25519_dalek::SigningKey as PrivateKey;
///
/// # let seed = b"funky accordion noises";
/// # keyforkd::test_util::run_test(seed, |socket_path| {
/// # std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
/// let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
/// let mut client = Client::discover_socket().unwrap();
/// let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
/// # keyforkd::test_util::Infallible::Ok(())
/// # }).unwrap();
/// ```
pub fn request_xprv<K>(&mut self, path: &DerivationPath) -> Result<ExtendedPrivateKey<K>>
where
K: PrivateKey + Clone + AsAlgorithm,
{
let algo = K::as_algorithm();
let request = Request::Derivation(DerivationRequest::new(algo.clone(), path));
let response = self.request(&request)?;
match response {
Response::Derivation(d) => {
if d.algorithm != algo {
return Err(Error::InvalidResponse);
}
let depth = path.len() as u8;
Ok(ExtendedPrivateKey::new_from_parts(
&d.data,
depth,
d.chain_code,
))
}
_ => Err(Error::InvalidResponse),
}
}
/// Serialize and send a [`Request`] to the server, awaiting a [`Result<Response>`].
///
/// # Errors
@ -102,6 +232,7 @@ impl Client {
/// * Reading or writing from or to the socket encountered an error.
/// * Bincode could not serialize the request or deserialize the response.
/// * An error occurred in Keyforkd.
#[doc(hidden)]
pub fn request(&mut self, req: &Request) -> Result<Response> {
try_encode_to(&bincode::serialize(&req)?, &mut self.socket)?;
let resp = try_decode_from(&mut self.socket)?;

View File

@ -1,100 +1,113 @@
use crate::Client;
use keyfork_derive_util::{request::*, DerivationPath};
use keyfork_slip10_test_data::test_data;
use std::sync::mpsc::channel;
use keyforkd::test_util::{run_test, Infallible};
use std::{os::unix::net::UnixStream, str::FromStr};
use tokio::runtime::Builder;
#[test]
fn secp256k1() {
#[cfg(feature = "secp256k1")]
fn secp256k1_test_suite() {
use k256::SecretKey;
let tests = test_data()
.unwrap()
.remove(&"secp256k1".to_string())
.unwrap();
// note: since client is non async, can't be single threaded
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
let tempdir = tempfile::tempdir().unwrap();
for (i, per_seed) in tests.into_iter().enumerate() {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
let (tx, rx) = channel();
let handle = rt.spawn({
let socket_path = socket_path.clone();
async move {
let seed = per_seed.seed.clone();
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
tx.send(()).unwrap();
let service = keyforkd::ServiceBuilder::new()
.layer(keyforkd::middleware::BincodeLayer::new())
.service(keyforkd::Keyforkd::new(seed));
server.run(service).await.unwrap();
}
});
rx.recv().unwrap();
for test in &per_seed.tests {
for seed_test in tests {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
if chain.len() < 2 {
let chain_len = chain.len();
if chain_len < 2 {
continue;
}
// Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session
let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let right_path = path.inner()[i..]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key);
assert_eq!(&response.data, test.private_key.as_slice());
}
handle.abort();
Ok(())
})
.unwrap();
}
}
#[test]
fn ed25519() {
#[cfg(feature = "ed25519")]
fn ed25519_test_suite() {
use ed25519_dalek::SigningKey;
let tests = test_data().unwrap().remove(&"ed25519".to_string()).unwrap();
let rt = Builder::new_multi_thread().enable_io().build().unwrap();
let tempdir = tempfile::tempdir().unwrap();
for (i, per_seed) in tests.into_iter().enumerate() {
let mut socket_name = i.to_string();
socket_name.push_str("-keyforkd.sock");
let socket_path = tempdir.path().join(socket_name);
let (tx, rx) = channel();
let handle = rt.spawn({
let socket_path = socket_path.clone();
async move {
let seed = per_seed.seed.clone();
let mut server = keyforkd::UnixServer::bind(&socket_path).unwrap();
tx.send(()).unwrap();
let service = keyforkd::ServiceBuilder::new()
.layer(keyforkd::middleware::BincodeLayer::new())
.service(keyforkd::Keyforkd::new(seed));
server.run(service).await.unwrap();
}
});
rx.recv().unwrap();
for test in &per_seed.tests {
for seed_test in tests {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| {
for test in seed_test.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
if chain.len() < 2 {
let chain_len = chain.len();
if chain_len < 2 {
continue;
}
for i in 2..chain_len {
// Consistency check: ensure the server and the client can each derive the same
// key using an XPrv, for all but the last XPrv, which is verified after this
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let right_path = path.inner()[i..]
.iter()
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SigningKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let keyforkd_xprv = client.request_xprv::<SigningKey>(&path).unwrap();
assert_eq!(
derived_xprv, keyforkd_xprv,
"{left_path} + {right_path} != {path}"
);
}
let req = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str(test.chain).unwrap(),
);
let response =
DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap();
assert_eq!(response.data, test.private_key);
assert_eq!(&response.data, test.private_key.as_slice());
}
handle.abort();
Infallible::Ok(())
})
.unwrap();
}
}

View File

@ -31,6 +31,7 @@ tower = { version = "0.4.13", features = ["tokio", "util"] }
# Personally audited
thiserror = "1.0.47"
serde = { version = "1.0.186", features = ["derive"] }
tempfile = { version = "3.10.0", default-features = false }
[dev-dependencies]
hex-literal = "0.4.1"

View File

@ -30,6 +30,8 @@ pub use error::Keyforkd as KeyforkdError;
pub use server::UnixServer;
pub use service::Keyforkd;
pub mod test_util;
/// Set up a Tracing subscriber, defaulting to debug mode.
#[cfg(feature = "tracing")]
pub fn setup_registry() {

View File

@ -1,6 +1,9 @@
//! A UNIX socket server to run a Tower Service.
use keyfork_frame::asyncext::{try_decode_from, try_encode_to};
use keyfork_frame::{
asyncext::{try_decode_from, try_encode_to},
DecodeError, EncodeError,
};
use std::{
io::Error,
path::{Path, PathBuf},
@ -17,6 +20,34 @@ pub struct UnixServer {
listener: UnixListener,
}
/// This feels like a hack, but this is a convenient way to use the same method to quickly verify
/// something across two different error types.
trait IsDisconnect {
fn is_disconnect(&self) -> bool;
}
impl IsDisconnect for DecodeError {
fn is_disconnect(&self) -> bool {
if let Self::Io(e) = self {
if let std::io::ErrorKind::UnexpectedEof = e.kind() {
return true;
}
}
false
}
}
impl IsDisconnect for EncodeError {
fn is_disconnect(&self) -> bool {
if let Self::Io(e) = self {
if let std::io::ErrorKind::UnexpectedEof = e.kind() {
return true;
}
}
false
}
}
impl UnixServer {
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
///
@ -68,9 +99,19 @@ impl UnixServer {
#[cfg(feature = "tracing")]
debug!("new socket connected");
tokio::spawn(async move {
let mut has_processed_request = false;
// Process requests until an error occurs or a client disconnects
loop {
let bytes = match try_decode_from(&mut socket).await {
Ok(bytes) => bytes,
Err(e) => {
if e.is_disconnect() {
#[cfg(feature = "tracing")]
if !has_processed_request {
debug!("client disconnected before sending any response");
}
return;
}
#[cfg(feature = "tracing")]
debug!(%e, "Error reading DerivationPath from socket");
let content = e.to_string().bytes().collect::<Vec<_>>();
@ -107,17 +148,37 @@ impl UnixServer {
let result = try_encode_to(&content[..], &mut socket).await;
#[cfg(feature = "tracing")]
if let Err(error) = result {
debug!(%error, "Error sending error to client");
if error.is_disconnect() {
#[cfg(feature = "tracing")]
if has_processed_request {
debug!("client disconnected while sending error frame");
}
return;
}
debug!(%error, "Error sending error to client");
}
has_processed_request = true;
// The error has been successfully sent, the client may perform
// another request.
continue;
}
}
.into();
if let Err(e) = try_encode_to(&response[..], &mut socket).await {
if e.is_disconnect() {
#[cfg(feature = "tracing")]
if has_processed_request {
debug!("client disconnected while sending success frame");
}
return;
}
#[cfg(feature = "tracing")]
debug!(%e, "Error sending response to client");
}
has_processed_request = true;
}
});
}
}

View File

@ -76,7 +76,7 @@ impl Service<Request> for Keyforkd {
info!("Deriving path: {}", req.path());
}
req.derive_with_master_seed((*seed).clone())
req.derive_with_master_seed(seed.as_ref())
.map(Response::Derivation)
.map_err(|e| DerivationError::Derivation(e.to_string()).into())
}),
@ -120,7 +120,7 @@ mod tests {
.unwrap()
.try_into()
.unwrap();
assert_eq!(response.data, test.private_key);
assert_eq!(&response.data, test.private_key.as_slice());
assert_eq!(response.chain_code.as_slice(), test.chain_code);
}
}
@ -150,7 +150,7 @@ mod tests {
.unwrap()
.try_into()
.unwrap();
assert_eq!(response.data, test.private_key);
assert_eq!(&response.data, test.private_key.as_slice());
assert_eq!(response.chain_code.as_slice(), test.chain_code);
}
}

View File

@ -0,0 +1,85 @@
//! # Keyforkd Test Utilities
//!
//! This module adds a helper to set up a Tokio runtime, start a Tokio runtime with a given seed,
//! start a Keyfork server on that runtime, and run a given test closure.
use crate::{middleware, Keyforkd, ServiceBuilder, UnixServer};
use tokio::runtime::Builder;
#[derive(Debug, thiserror::Error)]
#[error("This error can never be instantiated")]
#[doc(hidden)]
pub struct InfallibleError {
protected: (),
}
/// An infallible result. This type can be used to represent a function that should never error.
///
/// ```rust
/// use keyforkd::test_util::Infallible;
/// let closure = || {
/// Infallible::Ok(())
/// };
/// assert!(closure().is_ok());
/// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
/// provided as the only argument to the closure. The closure is expected to return a Result; the
/// Error field of the Result may be an error returned by a test.
///
/// # Panics
///
/// The function is not expected to run in production; therefore, the function plays "fast and
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where
F: FnOnce(&std::path::Path) -> Result<(), E> + Send + 'static,
E: Send + 'static,
{
let rt = Builder::new_multi_thread()
.worker_threads(2)
.enable_io()
.build()
.expect("tokio threaded IO runtime");
let socket_dir = tempfile::tempdir().expect("can't create tempdir");
let socket_path = socket_dir.path().join("keyforkd.sock");
rt.block_on(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let server_handle = tokio::spawn({
let socket_path = socket_path.clone();
let seed = seed.to_vec();
async move {
let mut server = UnixServer::bind(&socket_path).expect("can't bind unix socket");
tx.send(()).await.expect("couldn't send server start signal");
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec()));
server.run(service).await.unwrap();
}
});
rx.recv()
.await
.expect("can't receive server start signal from channel");
let test_handle = tokio::task::spawn_blocking(move || closure(&socket_path));
let result = test_handle.await;
server_handle.abort();
result
})
.expect("runtime could not join all threads")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_test() {
let seed = b"beefbeef";
run_test(seed, |_path| Infallible::Ok(())).expect("infallible");
}
}

View File

@ -4,7 +4,7 @@ use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationError, DerivationRequest, DerivationResponse},
DerivationPath,
DerivationPath, PathError,
};
use keyforkd_client::Client;
@ -17,7 +17,7 @@ pub enum Error {
/// The given path could not be parsed.
#[error("Could not parse the given path: {0}")]
PathFormat(#[from] keyfork_derive_util::path::Error),
PathFormat(#[from] PathError),
/// The request to derive data failed.
#[error("Unable to perform key derivation request: {0}")]

View File

@ -2,12 +2,10 @@
use std::time::{Duration, SystemTime, SystemTimeError};
use derive_util::{
request::{DerivationResponse, TryFromDerivationResponseError},
DerivationIndex, ExtendedPrivateKey, PrivateKey,
};
use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
use ed25519_dalek::SigningKey;
pub use keyfork_derive_util as derive_util;
pub use sequoia_openpgp as openpgp;
use sequoia_openpgp::{
packet::{
key::{Key4, PrimaryRole, SubordinateRole},
@ -17,7 +15,9 @@ use sequoia_openpgp::{
types::{KeyFlags, SignatureType},
Cert, Packet,
};
pub use sequoia_openpgp as openpgp;
pub type XPrvKey = SigningKey;
pub type XPrv = ExtendedPrivateKey<SigningKey>;
/// An error occurred while creating an OpenPGP key.
#[derive(Debug, thiserror::Error)]
@ -31,13 +31,9 @@ pub enum Error {
#[error("Key configured with both encryption and non-encryption key flags: {0:?}")]
InvalidKeyFlags(KeyFlags),
/// The derivation response contained incorrect data.
#[error("Incorrect derived data: {0}")]
IncorrectDerivedData(#[from] TryFromDerivationResponseError),
/// A derivation index could not be created from the given index.
#[error("Could not create derivation index: {0}")]
Index(#[from] keyfork_derive_util::index::Error),
Index(#[from] IndexError),
/// A derivation operation could not be performed against the private key.
#[error("Could not perform operation against private key: {0}")]
@ -65,7 +61,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.get(0) {
Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert),
@ -75,7 +71,6 @@ pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> R
let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
// Create certificate with initial key and signature
let xprv = ExtendedPrivateKey::<SigningKey>::try_from(data)?;
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
let primary_key = Key::from(Key4::<_, PrimaryRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_primary_key.private_key()),
@ -110,21 +105,21 @@ pub fn derive(data: DerivationResponse, keys: &[KeyFlags], userid: &UserID) -> R
let subkey = if is_enc && is_non_enc {
return Err(Error::InvalidKeyFlags(subkey_flags.clone()));
} else if is_enc {
Key::from(
Key4::<_, SubordinateRole>::import_secret_cv25519(
&PrivateKey::to_bytes(derived_key.private_key()),
None,
None,
epoch,
)?
)
// Clamp key before exporting as OpenPGP. Reference:
// https://gitlab.com/sequoia-pgp/sequoia/-/blob/main/openpgp/src/crypto/backend/rust/asymmetric.rs (see: generate_ecc constructor)
// https://github.com/jedisct1/libsodium/blob/b4c5d37fb5ee2736caa4823433926b588911e893/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c#L91-L93
let mut bytes = PrivateKey::to_bytes(derived_key.private_key());
bytes[0] &= 0b1111_1000;
bytes[31] &= !0b1000_0000;
bytes[31] |= 0b0100_0000;
Key::from(Key4::<_, SubordinateRole>::import_secret_cv25519(
&bytes, None, None, epoch,
)?)
} else {
Key::from(
Key4::<_, SubordinateRole>::import_secret_ed25519(
Key::from(Key4::<_, SubordinateRole>::import_secret_ed25519(
&PrivateKey::to_bytes(derived_key.private_key()),
epoch,
)?
)
)?)
};
// As per OpenPGP spec, signing keys must backsig the primary key

View File

@ -2,12 +2,16 @@
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyforkd_client::Client;
use sequoia_openpgp::{packet::UserID, types::KeyFlags, armor::{Kind, Writer}, serialize::Marshal};
use ed25519_dalek::SigningKey;
use sequoia_openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
};
#[derive(Debug, thiserror::Error)]
enum Error {
@ -108,16 +112,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
_ => panic!("Usage: {program_name} path subkey_format default_userid"),
};
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let derived_xprv = Client::discover_socket()?.request_xprv::<SigningKey>(&path)?;
let subkeys = subkey_format
.iter()
.map(|kt| kt.inner().clone())
.collect::<Vec<_>>();
let cert = keyfork_derive_openpgp::derive(derived_data, subkeys.as_slice(), &default_userid)?;
let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;

View File

@ -0,0 +1,51 @@
# Keyfork Derive: BIP-0032 Key Derivation
Keyfork offers a [BIP-0032] based hierarchial key derivation system enabling
the ability to create keys based on a [BIP-0032] seed, a value between 128 to
512 bits. The keys can be made using any algorithm supported by Keyfork Derive.
Newtypes can be added to wrap around foreign key types that aren't supported by
Keyfork.
Keys derived with the same parameters, from the same seed, will _always_ return
the same value. This makes Keyfork a reliable backend for generating encryption
or signature keys, as every key can be recovered using the previously used
derivation algorithm. However, this may be seen as a concern, as all an
attacker may need to recreate all previously-used seeds would be the original
derivation seed. For this reason, it is recommended to use the Keyfork server
for derivation from the root seed. The Keyfork server will ensure the root seed
and any highest-level keys (such as BIP-44, BIP-85, etc.) keys are not leaked.
The primary use case of Keyfork Derive will be the creation of Derivation
Requests, to be used by Keyforkd Client. In the included example, derivation is
performed directly on a master seed. This is how Keyforkd works internally.
## Example
```rust
use std::str::FromStr;
use keyfork_mnemonic_util::Mnemonic;
use keyfork_derive_util::{*, request::*};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mnemonic = Mnemonic::from_str(
"enter settle kiwi high shift absorb protect sword talent museum lazy okay"
)?;
let path = DerivationPath::from_str("m/44'/0'/0'/0/0")?;
let request = DerivationRequest::new(
DerivationAlgorithm::Secp256k1,
&path
);
let key1 = request.derive_with_mnemonic(&mnemonic)?;
let seed = mnemonic.seed(None)?;
let key2 = request.derive_with_master_seed(&seed)?;
assert_eq!(key1, key2);
Ok(())
}
```
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

View File

@ -1,4 +1,49 @@
//! # Extended Key Derivation
//!
//! Oftentimes, a client will want to create multiple keys. Some examples may include deriving
//! non-hardened public keys to see how many wallets have been used, deriving multiple OpenPGP
//! keys, or generally avoiding key reuse. While Keyforkd locks the root mnemonic and the
//! first-level derivation, any second-level derivations acquired from Keyforkd (for example,
//! `"m/44'/0'"`) can be used to derive further keys by converting the key to an Extended Public
//! Key or Extended Private Key and calling [`ExtendedPublicKey::derive_child`] or
//! [`ExtendedPrivateKey::derive_child`].
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//! use keyfork_mnemonic_util::Mnemonic;
//! use keyfork_derive_util::{*, request::*};
//! use k256::SecretKey;
//!
//! # fn check_wallet<T: PublicKey>(_: ExtendedPublicKey<T>) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! # let mnemonic = Mnemonic::from_str(
//! # "enter settle kiwi high shift absorb protect sword talent museum lazy okay"
//! # )?;
//! let path = DerivationPath::from_str("m/44'/0'/0'/0")?;
//! let request = DerivationRequest::new(
//! DerivationAlgorithm::Secp256k1, // The algorithm of k256::SecretKey
//! &path,
//! );
//!
//! let response = // perform a Keyforkd Client request...
//! # request.derive_with_mnemonic(&mnemonic)?;
//! let key: ExtendedPrivateKey<SecretKey> = response.try_into()?;
//! let pubkey = key.extended_public_key();
//! drop(key);
//!
//! for account in (0..20).map(|i| DerivationIndex::new(i, false).unwrap()) {
//! let derived_key = pubkey.derive_child(&account)?;
//! check_wallet(derived_key);
//! }
//!
//! Ok(())
//! }
//! ```
///
pub mod private_key;
///
pub mod public_key;
pub use {private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey};

View File

@ -10,18 +10,10 @@ const KEY_SIZE: usize = 256;
/// Errors associated with creating or deriving Extended Private Keys.
#[derive(Error, Clone, Debug)]
pub enum Error {
/// The seed has an unsuitable length; supported lengths are 16 bytes, 32 bytes, or 64 bytes.
#[error("Seed had an unsuitable length: {0}")]
BadSeedLength(usize),
/// The maximum depth for key derivation has been reached. The supported maximum depth is 255.
#[error("Reached maximum depth for key derivation")]
Depth,
/// This should never happen. HMAC keys should be able to take any size input.
#[error("Invalid length for HMAC key while generating master key (report me!)")]
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
/// An unknown error occurred while deriving a child key.
#[error("Unknown error while deriving child key")]
Derivation,
@ -39,17 +31,104 @@ type Result<T, E = Error> = std::result::Result<T, E>;
type ChainCode = [u8; 32];
type HmacSha512 = Hmac<Sha512>;
/// A reference to a variable-length seed. Keyfork automatically supports a seed of 128 bits,
/// 256 bits, or 512 bits, but because the master key is derived from a hashed seed, in theory
/// any amount of bytes could be used. It is not advised to use a variable-length seed longer
/// than 256 bits, as a brute-force attack on the master key could be performed in 2^256
/// attempts.
///
/// Mnemonics use a 512 bit seed, as knowledge of the mnemonics' words (such as through a side
/// channel attack) could leak which individual word is used, but not the order the words are
/// used in. Using a 512 bit hash to generate the seed results in a more computationally
/// expensive brute-force requirement.
pub struct VariableLengthSeed<'a> {
seed: &'a [u8],
}
impl<'a> VariableLengthSeed<'a> {
/// Create a new VariableLengthSeed.
///
/// # Examples
/// ```rust
/// use sha2::{Sha256, Digest};
/// use keyfork_derive_util::VariableLengthSeed;
///
/// let data = b"the missile is very eepy and wants to take a small sleeb";
/// let seed = VariableLengthSeed::new(data);
/// ```
pub fn new(seed: &'a [u8]) -> Self {
Self { seed }
}
}
mod as_private_key {
use super::VariableLengthSeed;
pub trait AsPrivateKey {
fn as_private_key(&self) -> &[u8];
}
impl AsPrivateKey for [u8; 16] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for [u8; 32] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for [u8; 64] {
fn as_private_key(&self) -> &[u8] {
self
}
}
impl AsPrivateKey for VariableLengthSeed<'_> {
fn as_private_key(&self) -> &[u8] {
self.seed
}
}
}
/// Extended private keys derived using BIP-0032.
///
/// Generic over types implementing [`PrivateKey`].
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
/// The internal private key data.
#[serde(with = "serde_with")]
private_key: K,
depth: u8,
chain_code: ChainCode,
}
mod serde_with {
use super::*;
pub(crate) fn serialize<S, K>(value: &K, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
K: PrivateKey + Clone,
{
serializer.serialize_bytes(&value.to_bytes())
}
pub(crate) fn deserialize<'de, D, K>(deserializer: D) -> Result<K, D::Error>
where
D: serde::Deserializer<'de>,
K: PrivateKey + Clone,
{
let variable_len_bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 32] = variable_len_bytes
.try_into()
.expect("unable to parse serialized private key; no support for static len");
Ok(K::from_bytes(&bytes))
}
}
impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtendedPrivateKey")
@ -68,32 +147,40 @@ where
/// mnemonic, but may take 16-byte seeds.
///
/// # Panics
///
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
///
/// # Errors
///
/// An error may be returned if:
/// * The given seed had an incorrect length.
/// * A `HmacSha512` can't be constructed - this should be impossible.
pub fn new(seed: impl AsRef<[u8]>) -> Result<Self> {
Self::new_internal(seed.as_ref())
/// * A `HmacSha512` can't be constructed.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// ```
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Self {
Self::new_internal(seed.as_private_key())
}
fn new_internal(seed: &[u8]) -> Result<Self> {
let len = seed.len();
if ![16, 32, 64].contains(&len) {
return Err(Error::BadSeedLength(len));
}
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())?
fn new_internal(seed: &[u8]) -> Self {
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())
.expect("HmacSha512 InvalidLength should be infallible")
.chain_update(seed)
.finalize()
.into_bytes();
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
Self::new_from_parts(
private_key,
private_key
.try_into()
.expect("KEY_SIZE / 8 did not give a 32 byte slice"),
0,
// Checked: chain_code is always the same length, hash is static size
chain_code.try_into().expect("Invalid chain code length"),
@ -104,35 +191,137 @@ where
///
/// # Errors
/// The function may error if a private key can't be created from the seed.
pub fn new_from_parts(seed: &[u8], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
Ok(Self {
private_key: K::from_bytes(seed.try_into()?),
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// ```
pub fn new_from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Self {
Self {
private_key: K::from_bytes(key),
depth,
chain_code,
})
}
}
/// Returns a reference to the [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::PrivateKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.private_key(), &PrivateKey::from_bytes(key));
/// ```
pub fn private_key(&self) -> &K {
&self.private_key
}
/// Create an [`ExtendedPublicKey`] for the current [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::PrivateKey as _,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// # let known_key: [u8; 33] = [
/// # 0, 242, 26, 9, 159, 68, 199, 0, 206, 71, 248,
/// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27,
/// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251
/// # ];
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let xpub = xprv.extended_public_key();
/// assert_eq!(known_key, xpub.public_key().to_bytes());
/// # Ok(())
/// # }
/// ```
pub fn extended_public_key(&self) -> ExtendedPublicKey<K::PublicKey> {
ExtendedPublicKey::new(self.public_key(), self.chain_code)
ExtendedPublicKey::new_from_parts(self.public_key(), self.depth, self.chain_code)
}
/// Return a public key for the current [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::PrivateKey as _,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let pubkey = xprv.public_key();
/// # Ok(())
/// # }
/// ```
pub fn public_key(&self) -> K::PublicKey {
self.private_key.public_key()
}
/// Returns the current depth.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(xprv.depth(), 4);
/// ```
pub fn depth(&self) -> u8 {
self.depth
}
/// Returns a copy of the current chain code.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// let key: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new_from_parts(key, 4, *chain_code);
/// assert_eq!(chain_code, &xprv.chain_code());
/// ```
pub fn chain_code(&self) -> [u8; 32] {
self.chain_code
}
@ -140,9 +329,29 @@ where
/// Derive a child using the given [`DerivationPath`].
///
/// # Errors
///
/// An error may be returned under the same circumstances as
/// [`ExtendedPrivateKey::derive_child`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, false)?);
/// let derived_xprv = root_xprv.derive_path(&path)?;
/// # Ok(())
/// # }
/// ```
pub fn derive_path(&self, path: &DerivationPath) -> Result<Self> {
if path.path.is_empty() {
Ok(self.clone())
@ -165,11 +374,38 @@ where
/// * The depth exceeds the maximum depth [`u8::MAX`].
/// * A `HmacSha512` can't be constructed - this should be impossible.
/// * Deriving a child key fails. Check the documentation for your [`PrivateKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn check_empty(p: &ExtendedPrivateKey<PrivateKey>) -> Result<(), std::io::Error> {
/// # Ok(())
/// # }
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let root_xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// let bip44_wallet = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(0, false)?);
/// let change_xprv = root_xprv.derive_path(&bip44_wallet)?;
/// for account in (0..20).map(|i| DerivationIndex::new(i, false).unwrap()) {
/// let account_xprv = change_xprv.derive_child(&account)?;
/// check_empty(&account_xprv)?;
/// }
/// # Ok(())
/// # }
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
let mut hmac =
HmacSha512::new_from_slice(&self.chain_code).map_err(Error::HmacInvalidLength)?;
let mut hmac = HmacSha512::new_from_slice(&self.chain_code)
.expect("HmacSha512 InvalidLength should be infallible");
if index.is_hardened() {
hmac.update(&[0]);
hmac.update(&self.private_key.to_bytes());

View File

@ -44,15 +44,51 @@ where
K: PublicKey,
{
/// Create a new [`ExtendedPublicKey`] from previously known values.
pub fn new(public_key: K, chain_code: ChainCode) -> Self {
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let key: &[u8; 33] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let chain_code: &[u8; 32] = //
/// # b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// let pubkey = PublicKey::from_bytes(key);
/// let xpub = ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// # Ok(())
/// # }
/// ```
pub fn new_from_parts(public_key: K, depth: u8, chain_code: ChainCode) -> Self {
Self {
public_key,
depth: 0,
depth,
chain_code,
}
}
/// Return the internal [`PublicKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let key: &[u8; 33] = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let pubkey = xpub.public_key();
/// # Ok(())
/// # }
/// ```
pub fn public_key(&self) -> &K {
&self.public_key
}
@ -70,6 +106,25 @@ where
/// * The depth exceeds the maximum depth [`u8::MAX`].
/// * A `HmacSha512` can't be constructed - this should be impossible.
/// * Deriving a child key fails. Check the documentation for your [`PublicKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # public_key::PublicKey as _,
/// # public_key::TestPublicKey as PublicKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # let key: &[u8; 33] = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// # let chain_code: &[u8; 32] = b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
/// # let pubkey = PublicKey::from_bytes(key);
/// let xpub = //
/// # ExtendedPublicKey::<PublicKey>::new_from_parts(pubkey, 0, *chain_code);
/// let index = DerivationIndex::new(0, false)?;
/// let child = xpub.derive_child(&index)?;
/// # Ok(())
/// # }
/// ```
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
if index.is_hardened() {
return Err(Error::HardenedIndex);

View File

@ -23,8 +23,20 @@ impl DerivationIndex {
/// Creates a new [`DerivationIndex`].
///
/// # Errors
///
/// Returns an error if the index is larger than the hardened flag.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// let bip44 = DerivationIndex::new(44, true).unwrap();
/// ```
///
/// Using a derivation index that is higher than 2^31 returns an error:
///
/// ```rust,should_panic
/// # use keyfork_derive_util::*;
/// let too_high = DerivationIndex::new(u32::MAX, true).unwrap();
/// ```
pub const fn new(index: u32, hardened: bool) -> Result<Self> {
if index & (0b1 << 31) > 0 {
return Err(Error::IndexTooLarge(index));
@ -46,6 +58,13 @@ impl DerivationIndex {
/// Return the internal derivation index. Note that if the derivation index is hardened, the
/// highest bit will be set, and the value can't be used to create a new derivation index.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// assert_eq!(DerivationIndex::new(44, true).unwrap().inner(), 2147483692);
/// assert_eq!(DerivationIndex::new(200, false).unwrap().inner(), 200);
/// ```
pub fn inner(&self) -> u32 {
self.0
}
@ -54,7 +73,15 @@ impl DerivationIndex {
self.0.to_be_bytes()
}
/// Whether or not the index is hardened, allowing deriving the key from a known parent key.
/// Whether or not the index is hardened, allowing deriving the key from a known parent public
/// key.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// assert_eq!(DerivationIndex::new(0, true).unwrap().is_hardened(), true);
/// assert_eq!(DerivationIndex::new(0, false).unwrap().is_hardened(), false);
/// ```
pub fn is_hardened(&self) -> bool {
self.0 & (0b1 << 31) != 0
}

View File

@ -1,27 +1,27 @@
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
#![doc = include_str!("../README.md")]
//! BIP-0032 derivation utilities.
///
pub mod extended_key;
///
pub mod index;
///
pub mod path;
///
pub mod private_key;
///
pub mod public_key;
///
pub mod request;
mod index;
mod path;
#[doc(hidden)]
pub mod private_key;
#[doc(hidden)]
pub mod public_key;
#[cfg(test)]
mod tests;
#[doc(inline)]
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
pub use crate::{
extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey},
index::DerivationIndex,
path::DerivationPath,
private_key::PrivateKey,
public_key::PublicKey,
index::{DerivationIndex, Error as IndexError},
path::{DerivationPath, Error as PathError},
private_key::{PrivateKey, PrivateKeyError},
public_key::{PublicKey, PublicKeyError},
};

View File

@ -51,7 +51,30 @@ impl DerivationPath {
self.path.push(index);
}
/// Return the inner path.
pub fn inner(&self) -> &Vec<DerivationIndex> {
&self.path
}
/// Append an index to the path, returning self to allow chaining method calls.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::*;
/// # fn discover_wallet(_p: DerivationPath) -> Result<bool, std::io::Error> { Ok(true) }
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let account = 0;
/// let path = DerivationPath::default()
/// .chain_push(DerivationIndex::new(44, true)?)
/// .chain_push(DerivationIndex::new(0, true)?)
/// .chain_push(DerivationIndex::new(account, true)?);
/// let mut has_wallet = false;
/// for index in (0..20).map(|i| DerivationIndex::new(i, true).unwrap()) {
/// has_wallet = has_wallet || discover_wallet(path.clone().chain_push(index))?;
/// }
/// # Ok(())
/// # }
/// ```
pub fn chain_push(mut self, index: DerivationIndex) -> Self {
self.path.push(index);
self

View File

@ -13,9 +13,32 @@ pub trait PrivateKey: Sized {
type Err: std::error::Error;
/// Create a Self from bytes.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// ```
fn from_bytes(b: &PrivateKeyBytes) -> Self;
/// Convert a &Self to bytes.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// assert_eq!(key_data, &private_key.to_bytes());
/// ```
fn to_bytes(&self) -> PrivateKeyBytes;
/*
@ -27,12 +50,35 @@ pub trait PrivateKey: Sized {
*/
/// The initial key for BIP-0032 and SLIP-0010 derivation, such as secp256k1's "Bitcoin seed".
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// assert_eq!(OurPrivateKey::key(), "testing seed");
/// ```
fn key() -> &'static str;
/// Generate a [`Self::PublicKey`].
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let public_key = private_key.public_key();
/// ```
fn public_key(&self) -> Self::PublicKey;
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`.
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`. The implementation of
/// derivation is algorithm-specific and a specification should be consulted when implementing
/// this method.
///
/// # Errors
///
@ -129,3 +175,48 @@ impl PrivateKey for ed25519_dalek::SigningKey {
true
}
}
use crate::public_key::TestPublicKey;
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TestPrivateKey {
key: [u8; 32],
}
impl TestPrivateKey {
pub(crate) fn public_key(&self) -> TestPublicKey {
let mut bytes = [0u8; 33];
for (i, byte) in self.key.iter().enumerate() {
bytes[i + 1] = byte ^ 0xFF;
}
TestPublicKey { key: bytes }
}
}
impl PrivateKey for TestPrivateKey {
type PublicKey = TestPublicKey;
type Err = PrivateKeyError;
fn from_bytes(b: &PrivateKeyBytes) -> Self {
Self {
key: *b
}
}
fn to_bytes(&self) -> PrivateKeyBytes {
self.key
}
fn key() -> &'static str {
"testing seed"
}
fn public_key(&self) -> Self::PublicKey {
self.public_key()
}
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
Ok(Self { key: *other })
}
}

View File

@ -19,9 +19,23 @@ pub trait PublicKey: Sized {
*/
/// Convert a &Self to bytes.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let public_key_bytes = private_key.public_key().to_bytes();
/// ```
fn to_bytes(&self) -> PublicKeyBytes;
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`.
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`. The implementation of
/// derivation is algorithm-specific and a specification should be consulted when implementing
/// this method.
///
/// # Errors
///
@ -31,6 +45,18 @@ pub trait PublicKey: Sized {
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
/// Create a BIP-0032/SLIP-0010 fingerprint from the public key.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # private_key::TestPrivateKey as OurPrivateKey,
/// # };
/// let key_data: &[u8; 32] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let private_key = OurPrivateKey::from_bytes(key_data);
/// let fingerprint = private_key.public_key().fingerprint();
/// ```
fn fingerprint(&self) -> [u8; 4] {
let hash = Sha256::new().chain_update(self.to_bytes()).finalize();
let hash = Ripemd160::new().chain_update(hash).finalize();
@ -112,3 +138,33 @@ impl PublicKey for VerifyingKey {
Err(Self::Err::DerivationUnsupported)
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct TestPublicKey {
pub(crate) key: [u8; 33],
}
impl TestPublicKey {
#[doc(hidden)]
#[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self {
Self {
key: b.try_into().unwrap(),
}
}
}
impl PublicKey for TestPublicKey {
type Err = PublicKeyError;
fn to_bytes(&self) -> PublicKeyBytes {
self.key
}
fn derive_child(&self, _other: PrivateKeyBytes) -> Result<Self, Self::Err> {
// whatever it takes for tests to pass...
Ok(self.clone())
}
}

View File

@ -1,9 +1,29 @@
// Because all algorithms make use of wildcard matching
#![allow(clippy::match_wildcard_for_single_variants)]
//! # Derivation Requests
//!
//! Derivation requests can be sent to Keyforkd using Keyforkd Client to request derivation from a
//! mnemonic or seed that has been loaded into Keyforkd.
//!
//! # Examples
//! ```rust
//! use std::str::FromStr;
//! use keyfork_derive_util::{DerivationPath, request::{DerivationRequest, DerivationAlgorithm}};
//!
//! let path = DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap();
//! let request = DerivationRequest::new(
//! DerivationAlgorithm::Secp256k1,
//! &path
//! );
//! ```
use crate::{
extended_key::private_key::Error as XPrvError, DerivationPath, ExtendedPrivateKey, PrivateKey,
extended_key::private_key::{Error as XPrvError, VariableLengthSeed},
private_key::{PrivateKey, TestPrivateKey},
DerivationPath, ExtendedPrivateKey,
};
use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError};
use serde::{Deserialize, Serialize};
@ -29,12 +49,15 @@ pub type Result<T, E = DerivationError> = std::result::Result<T, E>;
/// The algorithm to derive a key for. The choice of algorithm will result in a different resulting
/// derivation.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DerivationAlgorithm {
#[allow(missing_docs)]
Ed25519,
#[allow(missing_docs)]
Secp256k1,
#[doc(hidden)]
Internal,
}
impl DerivationAlgorithm {
@ -42,11 +65,12 @@ impl DerivationAlgorithm {
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
pub fn derive(&self, seed: Vec<u8>, path: &DerivationPath) -> Result<DerivationResponse> {
fn derive(&self, seed: &[u8], path: &DerivationPath) -> Result<DerivationResponse> {
let seed = VariableLengthSeed::new(seed);
match self {
#[cfg(feature = "ed25519")]
Self::Ed25519 => {
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed)?;
let key = ExtendedPrivateKey::<ed25519_dalek::SigningKey>::new(seed);
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
@ -55,7 +79,15 @@ impl DerivationAlgorithm {
}
#[cfg(feature = "secp256k1")]
Self::Secp256k1 => {
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed)?;
let key = ExtendedPrivateKey::<k256::SecretKey>::new(seed);
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
&derived_key,
))
}
Self::Internal => {
let key = ExtendedPrivateKey::<TestPrivateKey>::new(seed);
let derived_key = key.derive_path(path)?;
Ok(DerivationResponse::with_algo_and_xprv(
self.clone(),
@ -80,8 +112,20 @@ impl std::str::FromStr for DerivationAlgorithm {
}
}
/// Acquire the associated [`DerivationAlgorithm`] for a [`PrivateKey`].
pub trait AsAlgorithm: PrivateKey {
/// Return the appropriate [`DerivationAlgorithm`].
fn as_algorithm() -> DerivationAlgorithm;
}
impl AsAlgorithm for TestPrivateKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Internal
}
}
/// A derivation request.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct DerivationRequest {
algorithm: DerivationAlgorithm,
path: DerivationPath,
@ -89,6 +133,23 @@ pub struct DerivationRequest {
impl DerivationRequest {
/// Create a new derivation request.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// # Ok(())
/// # }
pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self {
Self {
algorithm,
@ -97,6 +158,24 @@ impl DerivationRequest {
}
/// Return the path of the derivation request.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// assert_eq!(&path, request.path());
/// # Ok(())
/// # }
pub fn path(&self) -> &DerivationPath {
&self.path
}
@ -105,28 +184,71 @@ impl DerivationRequest {
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mnemonic: keyfork_mnemonic_util::Mnemonic = //
/// # keyfork_mnemonic_util::Mnemonic::from_entropy(
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
/// # Default::default(),
/// # )?;
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// let response = request.derive_with_mnemonic(&mnemonic)?;
/// # Ok(())
/// # }
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
// TODO: passphrase support and/or store passphrase within mnemonic
self.derive_with_master_seed(mnemonic.seed(None)?)
self.derive_with_master_seed(&mnemonic.seed(None)?)
}
/// Derive an [`ExtendedPrivateKey`] using the given seed.
///
/// # Errors
/// The method may error if the derivation fails or if the algorithm is not supported.
pub fn derive_with_master_seed(&self, seed: Vec<u8>) -> Result<DerivationResponse> {
///
/// # Examples
/// ```rust
/// # use keyfork_derive_util::{
/// # *,
/// # request::*,
/// # public_key::TestPublicKey as PublicKey,
/// # private_key::TestPrivateKey as PrivateKey,
/// # };
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let seed: &[u8; 64] = //
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let algo: DerivationAlgorithm = //
/// # DerivationAlgorithm::Internal;
/// let path: DerivationPath = //
/// # DerivationPath::default();
/// let request = DerivationRequest::new(algo, &path);
/// let response = request.derive_with_master_seed(seed)?;
/// # Ok(())
/// # }
pub fn derive_with_master_seed(&self, seed: &[u8]) -> Result<DerivationResponse> {
self.algorithm.derive(seed, &self.path)
}
}
/// A response to a [`DerivationRequest`]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct DerivationResponse {
/// The algorithm used to derive the data.
pub algorithm: DerivationAlgorithm,
/// The derived private key.
pub data: Vec<u8>,
pub data: [u8; 32],
/// The chain code, used for further derivation.
pub chain_code: [u8; 32],
@ -137,13 +259,13 @@ pub struct DerivationResponse {
impl DerivationResponse {
/// Create a [`DerivationResponse`] with the given values.
pub fn with_algo_and_xprv<T: PrivateKey + Clone>(
fn with_algo_and_xprv<T: PrivateKey + Clone>(
algorithm: DerivationAlgorithm,
xprv: &ExtendedPrivateKey<T>,
) -> Self {
Self {
algorithm,
data: PrivateKey::to_bytes(xprv.private_key()).to_vec(),
data: PrivateKey::to_bytes(xprv.private_key()),
chain_code: xprv.chain_code(),
depth: xprv.depth(),
}
@ -164,47 +286,71 @@ pub enum TryFromDerivationResponseError {
}
#[cfg(feature = "secp256k1")]
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
mod secp256k1 {
use super::*;
use k256::SecretKey;
impl AsAlgorithm for SecretKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Secp256k1
}
}
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SecretKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Secp256k1 => {
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
}
DerivationAlgorithm::Secp256k1 => Ok(Self::new_from_parts(
&value.data,
value.depth,
value.chain_code,
)),
_ => Err(Self::Error::Algorithm),
}
}
}
}
#[cfg(feature = "secp256k1")]
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SecretKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
ExtendedPrivateKey::<k256::SecretKey>::try_from(&value)
}
}
#[cfg(feature = "ed25519")]
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Ed25519 => {
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
}
_ => Err(Self::Error::Algorithm),
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<SecretKey>::try_from(&value)
}
}
}
#[cfg(feature = "ed25519")]
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
mod ed25519 {
use super::*;
use ed25519_dalek::SigningKey;
impl AsAlgorithm for SigningKey {
fn as_algorithm() -> DerivationAlgorithm {
DerivationAlgorithm::Ed25519
}
}
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
ExtendedPrivateKey::<ed25519_dalek::SigningKey>::try_from(&value)
fn try_from(value: &DerivationResponse) -> Result<Self, Self::Error> {
match value.algorithm {
DerivationAlgorithm::Ed25519 => Ok(Self::new_from_parts(
&value.data,
value.depth,
value.chain_code,
)),
_ => Err(Self::Error::Algorithm),
}
}
}
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<SigningKey> {
type Error = TryFromDerivationResponseError;
fn try_from(value: DerivationResponse) -> Result<Self, Self::Error> {
ExtendedPrivateKey::<SigningKey>::try_from(&value)
}
}
}

View File

@ -30,7 +30,8 @@ fn secp256k1() {
} = test;
// Tests for ExtendedPrivateKey
let xkey = ExtendedPrivateKey::<SecretKey>::new(seed).unwrap();
let varlen_seed = VariableLengthSeed::new(&seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
derived_key.chain_code().as_slice(),
@ -50,8 +51,8 @@ fn secp256k1() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response = request.derive_with_master_seed(seed.clone()).unwrap();
assert_eq!(&response.data, private_key, "test: {chain}");
let response = request.derive_with_master_seed(&seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
}
}
}
@ -75,7 +76,8 @@ fn ed25519() {
} = test;
// Tests for ExtendedPrivateKey
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
let varlen_seed = VariableLengthSeed::new(&seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
derived_key.chain_code().as_slice(),
@ -95,8 +97,8 @@ fn ed25519() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
let response = request.derive_with_master_seed(seed.to_vec()).unwrap();
assert_eq!(&response.data, private_key, "test: {chain}");
let response = request.derive_with_master_seed(&seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
}
}
}
@ -108,7 +110,7 @@ fn panics_with_unhardened_derivation() {
use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
let xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
xkey.derive_path(&DerivationPath::from_str("m/0").unwrap())
.unwrap();
}
@ -120,7 +122,7 @@ fn panics_at_depth() {
use ed25519_dalek::SigningKey;
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed).unwrap();
let mut xkey = ExtendedPrivateKey::<SigningKey>::new(seed);
for i in 0..=u32::from(u8::MAX) {
xkey = xkey
.derive_child(&DerivationIndex::new(i, true).unwrap())

View File

@ -7,15 +7,15 @@ license = "AGPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["openpgp", "openpgp-card", "qrcode"]
default = ["openpgp", "openpgp-card", "qrcode", "bin"]
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
openpgp = ["sequoia-openpgp", "anyhow"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
qrcode = ["keyfork-qrcode"]
bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"]
[dependencies]
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", optional = true, default-features = false }
smex = { version = "0.1.0", path = "../util/smex" }
sharks = "0.5.0"

View File

@ -7,50 +7,33 @@ use std::{
process::ExitCode,
};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Vec<Cert>)> {
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(shard)?, certs))
Ok((File::open(shard)?, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let openpgp = OpenPGP;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
let mut bytes = vec![];
combine(
cert_list,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?;
print!("{}", smex::encode(&bytes));

View File

@ -7,47 +7,33 @@ use std::{
process::ExitCode,
};
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate<'a>(
messages_file: impl AsRef<Path>,
key_discovery: impl Into<Option<&'a str>>,
) -> Result<(File, Vec<Cert>)> {
let key_discovery = key_discovery.into().map(PathBuf::from);
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(messages_file)?, certs))
Ok((File::open(shard)?, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
[messages_file] => validate(messages_file, None)?,
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let openpgp = OpenPGP;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
decrypt(
&cert_list,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?;
Ok(())
}

View File

@ -1,10 +1,13 @@
#![doc = include_str!("../README.md")]
use std::io::{stdin, stdout, Write};
use std::{
io::{stdin, stdout, Read, Write},
path::Path,
};
use aes_gcm::{
aead::{Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit,
aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce,
};
use hkdf::Hkdf;
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
@ -16,9 +19,279 @@ use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
const ENC_LEN: u8 = 4 * 16;
#[cfg(feature = "openpgp")]
pub mod openpgp;
/// A format to use for splitting and combining secrets.
pub trait Format {
/// The error type returned from any failed operations.
type Error: std::error::Error + 'static;
/// A type encapsulating the public key recipients of shards.
type PublicKeyData;
/// A type encapsulating the private key recipients of shards.
type PrivateKeyData;
/// A type representing the parsed, but encrypted, Shard data.
type ShardData;
/// A type representing a Signer derived from the secret.
type Signer;
/// Parse the public key data from a readable type.
///
/// # Errors
/// The method may return an error if private key data could not be properly parsed from the
/// path.
/// occurred while parsing the public key data.
fn parse_public_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> Result<Self::PublicKeyData, Self::Error>;
/// Parse the private key data from a readable type. The private key may not be accessible (it
/// may be hardware only, such as a smartcard), for which this method may return None.
///
/// # Errors
/// The method may return an error if private key data could not be properly parsed from the
/// path.
fn parse_private_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> Result<Self::PrivateKeyData, Self::Error>;
/// Parse the Shard file into a processable type.
///
/// # Errors
/// The method may return an error if the Shard file could not be read from or if the Shard
/// file could not be properly parsed.
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Self::ShardData, Self::Error>;
/// Write the Shard data to a Shard file.
///
/// # Errors
/// The method may return an error if the Shard data could not be properly serialized or if the
/// Shard file could not be written to.
fn format_shard_file(
&self,
shard_data: Self::ShardData,
shard_file: impl Write,
) -> Result<(), Self::Error>;
/// Derive a Signer from the secret.
///
/// # Errors
/// This function may return an error if a Signer could not be properly created.
fn derive_signer(&self, secret: &[u8]) -> Result<Self::Signer, Self::Error>;
/// Encrypt multiple shares to public keys.
///
/// # Errors
/// The method may return an error if the share could not be encrypted to a public key or if
/// the ShardData could not be created.
fn generate_shard_data(
&self,
shares: &[Share],
signer: &Self::Signer,
public_keys: Self::PublicKeyData,
) -> Result<Self::ShardData, Self::Error>;
/// Decrypt shares and associated metadata from a readable input. For the current version of
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
/// secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if all shards
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
shard_data: Self::ShardData,
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
/// be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
shard_data: Self::ShardData,
) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret.
///
/// # Errors
/// The method may return an error if the shares can't be decrypted or if the shares can't
/// be combined into a secret.
fn decrypt_all_shards_to_secret(
&self,
private_key_data_path: Option<impl AsRef<Path>>,
reader: impl Read + Send + Sync,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_data_path
.map(|p| self.parse_private_key_data(p))
.transpose()?;
let shard_data = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(private_keys, shard_data)?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
Ok(secret)
}
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
/// shard to the AES key.
///
/// # Errors
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
fn decrypt_one_shard_for_transport(
&self,
private_key_data_path: Option<impl AsRef<Path>>,
reader: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
// parse input
let private_keys = private_key_data_path
.map(|p| self.parse_private_key_data(p))
.transpose()?;
let shard_data = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(
"Press enter, then present QR code to camera".to_string(),
))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else {
pm.prompt_message(PromptMessage::Text(
"Unable to detect QR code, falling back to text".to_string(),
))?;
};
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] =
pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?;
let nonce = nonce_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload
let (share, threshold) = self.decrypt_one_shard(private_keys, shard_data)?;
let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
);
// encrypt data
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
// convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
// difficulty when entering in prompts manually, as one's place could be lost due to
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
// but not including the last byte.
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
.iter_mut()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic =
unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Lowest) {
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
}
}
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
#[derive(thiserror::Error, Debug)]
pub enum SharksError {
@ -44,6 +317,11 @@ pub struct InvalidData;
pub(crate) const HUNK_VERSION: u8 = 1;
pub(crate) const HUNK_OFFSET: usize = 2;
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
const QRCODE_TIMEOUT: u64 = 60; // One minute
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
/// shares, and combine them.
///
@ -64,8 +342,10 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
let mut shares = vec![];
let mut threshold = 0;
let mut iter = 0;
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
@ -78,13 +358,27 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = nonce_mnemonic.to_bytes();
qrcode_data.extend(key_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Medium) {
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(format!(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code to only shardholder {iter}. ",
"Nobody else should scan this QR code."
),
iter = iter
)))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {nonce_mnemonic} {key_mnemonic}"
concat!(
"Upon request, these words should be sent to shardholder {iter}: ",
"{nonce_mnemonic} {key_mnemonic}"
),
iter = iter,
nonce_mnemonic = nonce_mnemonic,
key_mnemonic = key_mnemonic,
)))?;
let mut pubkey_data: Option<[u8; 32]> = None;
@ -92,19 +386,15 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(
"Press enter, then present QR code to camera".to_string(),
))?;
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{
let decoded_data = smex::decode(&hex)?;
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
let _ = payload_data.insert(decoded_data[32..].to_vec());
} else {
pm.prompt_message(PromptMessage::Text(
"Unable to detect QR code, falling back to text".to_string(),
))?;
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
@ -115,8 +405,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 48],
};
let [pubkey_mnemonic, payload_mnemonic] =
pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?;
let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist(
QRCODE_COULDNT_READ,
&wordlist,
3,
validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()

View File

@ -13,9 +13,9 @@ use aes_gcm::{
Aes256Gcm, Error as AesError, KeyInit, Nonce,
};
use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength};
use keyfork_derive_openpgp::derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
DerivationPath,
use keyfork_derive_openpgp::{
derive_util::{DerivationPath, PathError, VariableLengthSeed},
XPrv,
};
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
use keyfork_prompt::{
@ -56,7 +56,11 @@ use smartcard::SmartcardManager;
const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2;
use super::{InvalidData, SharksError, HUNK_VERSION};
use super::{
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
QRCODE_TIMEOUT,
Format,
};
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
const ENC_LEN: u8 = 4 * 16;
@ -120,9 +124,13 @@ pub enum Error {
#[error("IO error: {0}")]
Io(#[source] std::io::Error),
/// An error occurred while deriving data.
#[error("Derivation: {0}")]
Derivation(#[from] keyfork_derive_openpgp::derive_util::extended_key::private_key::Error),
/// An error occurred while parsing a derivation path.
#[error("Derivation path: {0}")]
DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error),
DerivationPath(#[from] PathError),
/// An error occurred while requesting derivation.
#[error("Derivation request: {0}")]
@ -156,6 +164,18 @@ impl EncryptedMessage {
}
}
/// Serialize all contents of the message to a writer.
///
/// # Errors
/// The function may error for any condition in Sequoia's Serialize trait.
pub fn serialize(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> {
for pkesk in &self.pkesks {
pkesk.serialize(o)?;
}
self.message.serialize(o)?;
Ok(())
}
/// Decrypt the message with a Sequoia policy and decryptor.
///
/// This method creates a container containing the packets and passes the serialized container
@ -200,12 +220,273 @@ impl EncryptedMessage {
}
}
///
pub struct OpenPGP;
impl OpenPGP {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
if path.is_file() {
let mut vec = vec![];
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
}
Ok(vec)
}
}
}
impl Format for OpenPGP {
type Error = Error;
type PublicKeyData = Vec<Cert>;
type PrivateKeyData = Vec<Cert>;
type ShardData = Vec<EncryptedMessage>;
type Signer = openpgp::crypto::KeyPair;
fn parse_public_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> std::result::Result<Self::PublicKeyData, Self::Error> {
Self::discover_certs(key_data_path)
}
fn parse_private_key_data(
&self,
key_data_path: impl AsRef<Path>,
) -> std::result::Result<Self::PrivateKeyData, Self::Error> {
Self::discover_certs(key_data_path)
}
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Self::ShardData, Self::Error> {
let mut pkesks = Vec::new();
let mut encrypted_messages = vec![];
for packet in PacketPile::from_reader(shard_file)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s));
}
s => {
panic!("Invalid variant found: {}", s.tag());
}
}
}
Ok(encrypted_messages)
}
fn derive_signer(&self, secret: &[u8]) -> Result<Self::Signer, Self::Error> {
let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'")?;
let seed = VariableLengthSeed::new(secret);
let xprv = XPrv::new(seed).derive_path(&path)?;
let derived_cert = keyfork_derive_openpgp::derive(
xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)?;
let signing_key = derived_cert
.primary_key()
.parts_into_secret()
.map_err(Error::Sequoia)?
.key()
.clone()
.into_keypair()
.map_err(Error::Sequoia)?;
Ok(signing_key)
}
fn format_shard_file(
&self,
shard_data: Self::ShardData,
shard_file: impl Write,
) -> Result<(), Self::Error> {
let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?;
for message in shard_data {
message.serialize(&mut writer).map_err(Error::Sequoia)?;
}
writer.finalize().map_err(Error::SequoiaIo)?;
Ok(())
}
fn generate_shard_data(
&self,
shares: &[Share],
signer: &Self::Signer,
public_keys: Self::PublicKeyData,
) -> std::result::Result<Self::ShardData, Self::Error> {
let policy = StandardPolicy::new();
let mut total_recipients = vec![];
let mut messages = vec![];
for (share, cert) in shares.iter().zip(public_keys) {
total_recipients.push(cert.clone());
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let message = Encryptor2::for_recipients(
message,
encryption_keys
.iter()
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
)
.build()
.map_err(Error::Sequoia)?;
let message = Signer::new(message, signer.clone())
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(message)
.build()
.map_err(Error::Sequoia)?;
// NOTE: This shouldn't be an alloc, but it's a minor alloc, so it's fine.
message
.write_all(&Vec::from(share))
.map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
messages.push(message_output);
}
// A little bit of back and forth, we're going to parse the messages just to serialize them
// later.
let message = messages.into_iter().flatten().collect::<Vec<_>>();
let data = self.parse_shard_file(message.as_slice())?;
Ok(data)
}
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
mut shard_data: Self::ShardData,
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let metadata = shard_data.remove(0);
let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
// Generate a controlled binding from certificates to encrypted messages. This is stable
// because we control the order packets are encrypted and certificates are stored.
// TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(shard_data)
.collect();
let mut decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
// clean decrypted messages from encrypted messages
messages.retain(|k, _v| !decrypted_messages.contains_key(k));
let left_from_threshold = threshold as usize - decrypted_messages.len();
if left_from_threshold > 0 {
#[allow(clippy::cast_possible_truncation)]
let new_messages = decrypt_with_manager(
left_from_threshold as u8,
&mut messages,
&certs,
&policy,
&mut manager,
)?;
decrypted_messages.extend(new_messages);
}
let shares = decrypted_messages
.values()
.map(|message| Share::try_from(message.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| SharksError::Share(e.to_string()))?;
Ok((shares, threshold))
}
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
mut shard_data: Self::ShardData,
) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let metadata = shard_data.remove(0);
let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(shard_data)
.collect();
let decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
if let Some(message) = decrypted_messages.into_values().next() {
let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?;
return Ok((share, threshold));
}
let decrypted_messages =
decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?;
if let Some(message) = decrypted_messages.into_values().next() {
let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?;
return Ok((share, threshold));
}
panic!("unable to decrypt shard");
}
}
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
#[deprecated]
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
@ -238,6 +519,7 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
/// # Panics
/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics.
/// The `split` utility should never give packets that are not in this format.
#[deprecated]
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
let mut pkesks = Vec::new();
let mut encrypted_messages = VecDeque::new();
@ -409,6 +691,7 @@ fn decrypt_metadata(
})
}
#[deprecated]
fn decrypt_one(
messages: Vec<EncryptedMessage>,
certs: &[Cert],
@ -458,6 +741,8 @@ fn decrypt_one(
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
/// atypical usage and should not be encountered in normal usage, unless something that is not a
/// Keyfork seed has been fed into [`split`].
#[deprecated]
#[allow(deprecated)]
pub fn decrypt(
certs: &[Cert],
metadata: &EncryptedMessage,
@ -471,17 +756,15 @@ pub fn decrypt(
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(
"Press enter, then present QR code to camera".to_string(),
))?;
if let Ok(Some(hex)) = keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0) {
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
{
let decoded_data = smex::decode(&hex)?;
let _ = nonce_data.insert(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
let _ = pubkey_data.insert(decoded_data[12..].try_into().map_err(|_| InvalidData)?);
} else {
pm.prompt_message(PromptMessage::Text(
"Unable to detect QR code, falling back to text".to_string(),
))?;
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
@ -492,7 +775,7 @@ pub fn decrypt(
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] =
pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?;
pm.prompt_validated_wordlist(QRCODE_COULDNT_READ, &wordlist, 3, validator.to_fn())?;
let nonce = nonce_mnemonic
.as_bytes()
@ -562,13 +845,21 @@ pub fn decrypt(
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Lowest) {
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
"Our words: {our_pubkey_mnemonic} {payload_mnemonic}"
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
@ -634,13 +925,11 @@ pub fn combine(
// TODO: extract as function
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.clone())?;
let path = DerivationPath::from_str("m/7366512'/0'")?;
let seed = VariableLengthSeed::new(&secret);
let xprv = XPrv::new(seed).derive_path(&path)?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)?;
@ -669,15 +958,13 @@ pub fn combine(
/// The function may panic if the metadata can't properly store the certificates used to generate
/// the encrypted shares.
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
let seed = VariableLengthSeed::new(secret);
// build cert to sign encrypted shares
let userid = UserID::from("keyfork-sss");
let kdr = DerivationRequest::new(
DerivationAlgorithm::Ed25519,
&DerivationPath::from_str("m/7366512'/0'")?,
)
.derive_with_master_seed(secret.to_vec())?;
let path = DerivationPath::from_str("m/7366512'/0'")?;
let xprv = XPrv::new(seed).derive_path(&path)?;
let derived_cert = keyfork_derive_openpgp::derive(
kdr,
xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)?;

View File

@ -30,8 +30,8 @@ keyfork-derive-util = { version = "0.1.0", path = "../derive/keyfork-derive-util
keyfork-entropy = { version = "0.1.0", path = "../util/keyfork-entropy" }
keyfork-mnemonic-util = { version = "0.1.0", path = "../util/keyfork-mnemonic-util" }
keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode" }
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard" }
keyfork-qrcode = { version = "0.1.0", path = "../qrcode/keyfork-qrcode", default-features = false }
keyfork-shard = { version = "0.1.0", path = "../keyfork-shard", default-features = false, features = ["openpgp", "openpgp-card", "qrcode"] }
smex = { version = "0.1.0", path = "../util/smex" }
clap = { version = "4.4.2", features = ["derive", "env", "wrap_help"] }

55
crates/keyfork/README.md Normal file
View File

@ -0,0 +1,55 @@
# Keyfork: The Kitchen Sink of Entropy
**Note:** Keyfork operations are meant to be run on an airgapped machine and
Keyfork will error if either any network interfaces are detected or if Keyfork
is running on a system with a kernel using an insecure random number generator.
An all-inclusive crate encapsulating end-user functionality of the Keyfork
ecosystem, the Keyfork binary includes all mechanisms that should be exposed to
the user when running Keyfork. Information about what operations Keyfork
performs are available in detail by running `keyfork help` (each subcommand has
thorough documentation) or in the [`docs`] mdBook, but here's a quick overview:
## Getting Started with Keyfork
Keyfork offers two options for getting started. For multi-user setups, it is
best to look at the detailed documentation for Keyfork Shard. For single-user
setups, `keyfork mnemonic generate` will (by default) create a 256-bit mnemonic
phrase that can be used to start Keyfork. *Store this phrase*, as it's the only
way you'll be able to start Keyfork in the future. It is recommended to use a
mnemonic recovery sheet or a printed-steel solution such as the [Billfodl] or
[Cryptosteel Capsule].
```sh
keyfork mnemonic generate
```
Once a mnemonic has been generated and stored in a secure manner, Keyfork can
be started by "recovering" the server from the mnemonic backup mechanism:
```sh
keyfork recover mnemonic
```
## Deriving Keys
Keyfork's primary goal is to derive keys. These keys can later be used for
things such as signing documents and artifacts or decrypting payloads.
Keyfork's first derivation target is OpenPGP, a protocol supporting many
cryptographic operations. OpenPGP keys require a User ID, which can be used to
identify the owner of the key, either by name or by email. To get an OpenPGP
public key (more accurately known as a "cert"), the [`sq`][sq] tool is used to
convert a key to a certificate:
```sh
keyfork derive openpgp "John Doe <jdoe@example.com>" | sq key extract-cert
```
All Keyfork derivations are intended to be reproducible. Because of this,
Keyfork derived keys can be recreated at any time, only requiring the knowledge
of how the key was made.
[`docs`]: /public/keyfork/src/branch/main/docs/src/SUMMARY.md
[Billfodl]: https://privacypros.io/products/the-billfodl/
[Cryptosteel Capsule]: https://cryptosteel.com/product/cryptosteel-capsule-solo/
[sq]: https://gitlab.com/sequoia-pgp/sequoia-sq/

View File

@ -1,16 +1,16 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use keyfork_derive_openpgp::openpgp::{
use keyfork_derive_openpgp::{
openpgp::{
armor::{Kind, Writer},
packet::UserID,
serialize::Marshal,
types::KeyFlags,
},
XPrvKey,
};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest, DerivationResponse},
DerivationIndex, DerivationPath,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyforkd_client::Client;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -19,6 +19,9 @@ type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
pub enum DeriveSubcommands {
/// Derive an OpenPGP Transferable Secret Key (private key). The key is encoded using OpenPGP
/// ASCII Armor, a format usable by most programs using OpenPGP.
///
/// The key is generated with a 24-hour expiration time. The operation to set the expiration
/// time to a higher value is left to the user to ensure the key is usable by the user.
#[command(name = "openpgp")]
OpenPGP {
/// Default User ID for the certificate, using the OpenPGP User ID format.
@ -45,12 +48,9 @@ impl DeriveSubcommands {
.set_storage_encryption(),
KeyFlags::empty().set_authentication(),
];
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let derived_data: DerivationResponse = Client::discover_socket()?
.request(&request.into())?
.try_into()?;
let xprv = Client::discover_socket()?.request_xprv::<XPrvKey>(&path)?;
let default_userid = UserID::from(user_id.as_str());
let cert = keyfork_derive_openpgp::derive(derived_data, &subkeys, &default_userid)?;
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;
@ -72,6 +72,10 @@ pub struct Derive {
command: DeriveSubcommands,
/// Account ID. Required for all derivations.
///
/// An account ID may not be relevant for the derivation being performed, but the lack of an
/// account ID can often come as a hindrance in the future. As such, it is always required. If
/// the account ID is not relevant, it is assumed to be `0`.
#[arg(long, global = true, default_value = "0")]
account_id: u32,
}

View File

@ -9,7 +9,7 @@ mod wizard;
/// The Kitchen Sink of Entropy.
#[derive(Parser, Clone, Debug)]
#[command(author, version, about, long_about = None)]
#[command(author, version, about, long_about)]
pub struct Keyfork {
// Global options
#[command(subcommand)]
@ -20,25 +20,51 @@ pub struct Keyfork {
pub enum KeyforkCommands {
/// Derive keys of various formats. These commands require that the Keyfork server is running,
/// which can be started by running a `keyfork recover` command.
///
/// Derived keys are reproducible: assuming the same arguments are used when deriving a key for
/// a second time, the key will be _functionally_ equivalent. This means keys don't need to be
/// persisted to cold storage or left hot in a running program. They can be derived when
/// they're needed and forgotten when they're not.
Derive(derive::Derive),
/// Mnemonic generation and persistence utilities.
Mnemonic(mnemonic::Mnemonic),
/// Splitting and combining secrets, using Shamir's Secret Sharing.
///
/// Keys can be split such that a certain amount of users, from a potentially even-larger
/// amount of users, can be used to recreate a key. This creates resilience for a key, as in a
/// "seven of nine" scenario, nine people in total are capable of recreating a key, but only
/// seven may be required.
Shard(shard::Shard),
/// Derive and deploy keys to hardware.
///
/// Keys existing in hardware creates a situation where it is unlikely (but not impossible) for
/// a key to be extracted. While a key in memory could be captured by a rootkit or some other
/// privilege escalation mechanism, a key in hardware would require a hardware exploit to
/// extract the key.
///
/// It is recommended to provision keys whenever possible, as opposed to deriving them.
#[command(subcommand_negates_reqs(true))]
Provision(provision::Provision),
/// Recover a seed using the requested recovery mechanism and start the Keyfork server.
///
/// Once the Keyfork server is started, derivation requests can be performed. The Keyfork seed
/// is kept solely in the Keyfork server. Derivations with less than two indices are not
/// permitted, to ensure a seed often used to derive keys for multiple different paths is not
/// leaked by any individual deriver.
Recover(recover::Recover),
/// Utilities to automatically manage the setup of Keyfork.
Wizard(wizard::Wizard),
/// Print an autocompletion file to standard output.
///
/// Keyfork does not manage the installation of completion files. Consult the documentation for
/// the shell for which documentation has been generated on the appropriate location to store
/// completion files.
#[cfg(feature = "completion")]
Completion {
#[arg(value_enum)]

View File

@ -1,15 +1,15 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use std::{collections::HashSet, fs::OpenOptions, io::IsTerminal, path::PathBuf};
use std::{collections::HashSet, fs::File, io::IsTerminal, path::PathBuf};
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
use keyfork_derive_openpgp::openpgp::{self, packet::UserID, types::KeyFlags, Cert};
use keyfork_derive_util::{
request::{DerivationAlgorithm, DerivationRequest},
DerivationIndex, DerivationPath,
use keyfork_derive_openpgp::{
openpgp::{self, packet::UserID, types::KeyFlags, Cert},
XPrv,
};
use keyfork_derive_util::{DerivationIndex, DerivationPath};
use keyfork_prompt::{
validators::{PinValidator, Validator},
Message, PromptHandler, Terminal,
@ -21,7 +21,7 @@ pub struct PinLength(usize);
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn derive_key(seed: &[u8], index: u8) -> Result<Cert> {
fn derive_key(seed: [u8; 32], index: u8) -> Result<Cert> {
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
@ -42,10 +42,9 @@ fn derive_key(seed: &[u8], index: u8) -> Result<Cert> {
.chain_push(chain)
.chain_push(account)
.chain_push(subkey);
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
let response = request.derive_with_master_seed(seed.to_vec())?;
let xprv = XPrv::new(seed).derive_path(&path)?;
let userid = UserID::from(format!("Keyfork Shard {index}"));
let cert = keyfork_derive_openpgp::derive(response, &subkeys, &userid)?;
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
Ok(cert)
}
@ -103,7 +102,7 @@ fn generate_shard_secret(
keys_per_shard: u8,
output_file: &Option<PathBuf>,
) -> Result<()> {
let seed = keyfork_entropy::generate_entropy_of_size(256 / 8)?;
let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?;
let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?;
let mut certs = vec![];
let mut seen_cards: HashSet<String> = HashSet::new();
@ -127,7 +126,7 @@ fn generate_shard_secret(
.to_fn();
for index in 0..max {
let cert = derive_key(&seed, index)?;
let cert = derive_key(seed, index)?;
for i in 0..keys_per_shard {
pm.prompt_message(Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
@ -165,7 +164,7 @@ fn generate_shard_secret(
}
if let Some(output_file) = output_file {
let output = OpenOptions::new().write(true).open(output_file)?;
let output = File::create(output_file)?;
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
} else {
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;

View File

@ -1,4 +1,4 @@
#![doc = include_str!("../../../README.md")]
#![doc = include_str!("../README.md")]
#![allow(clippy::module_name_repetitions)]

View File

@ -9,6 +9,7 @@ license = "MIT"
[features]
default = []
bin = ["decode-backend-rqrr"]
decode-backend-rqrr = ["dep:rqrr"]
decode-backend-zbar = ["dep:keyfork-zbar"]

View File

@ -9,6 +9,8 @@ use std::{
use v4l::{
buffer::Type,
io::{userptr::Stream, traits::CaptureStream},
video::Capture,
FourCC,
Device,
};
@ -100,6 +102,9 @@ pub fn qrencode(
#[cfg(feature = "decode-backend-rqrr")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device.format().expect("Failed to read format");
fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = SystemTime::now();
@ -128,6 +133,9 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
#[cfg(feature = "decode-backend-zbar")]
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device.format().expect("Failed to read format");
fmt.fourcc = FourCC::new(b"MPG1");
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = SystemTime::now();
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();

View File

@ -22,9 +22,9 @@ impl Image {
///
/// A FourCC code can be given in the format:
///
/// ```no_run
/// ```rust,ignore
/// self.set_format(b"Y800")
/// ````
/// ```
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
let fourcc: u64 = fourcc[0] as u64
| ((fourcc[1] as u64) << 8)

View File

@ -13,7 +13,7 @@ use crate::{
/// The top left cell is represented as `(0, 0)`.
///
/// On unix systems, this function will block and possibly time out while
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
pub fn position() -> io::Result<(u16, u16)> {
if is_raw_mode_enabled() {
read_position_raw()

View File

@ -170,7 +170,7 @@ pub fn available_color_count() -> u16 {
///
/// # Notes
///
/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output.
/// crossterm supports NO_COLOR (<https://no-color.org/>) to disabled colored output.
///
/// This API allows applications to override that behavior and force colorized output
/// even if NO_COLOR is set.

View File

@ -71,7 +71,7 @@ impl Colored {
}
/// Checks whether ansi color sequences are disabled by setting of NO_COLOR
/// in environment as per https://no-color.org/
/// in environment as per <https://no-color.org/>
pub fn ansi_color_disabled() -> bool {
!std::env::var("NO_COLOR")
.unwrap_or("".to_string())

View File

@ -199,7 +199,7 @@ pub struct WindowSize {
/// Returns the terminal size `[WindowSize]`.
///
/// The width and height in pixels may not be reliably implemented or default to 0.
/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused".
/// For unix, <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> documents them as "unused".
/// For windows it is not implemented.
pub fn window_size() -> io::Result<WindowSize> {
sys::window_size()

View File

@ -144,7 +144,7 @@ pub(crate) fn disable_raw_mode() -> io::Result<()> {
/// Queries the terminal's support for progressive keyboard enhancement.
///
/// On unix systems, this function will block and possibly time out while
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
#[cfg(feature = "events")]
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
if is_raw_mode_enabled() {

View File

@ -7,7 +7,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
default = ["bin"]
bin = ["smex"]
[dependencies]

View File

@ -1,6 +1,9 @@
//! Utilities for reading entropy from secure sources.
use std::{fs::{read_dir, read_to_string, File}, io::Read};
use std::{
fs::{read_dir, read_to_string, File},
io::Read,
};
static WARNING_LINKS: [&str; 1] =
["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"];
@ -48,6 +51,12 @@ fn ensure_offline() {
}
/// Ensure the system is safe.
///
/// # Examples
/// ```rust
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// keyfork_entropy::ensure_safe();
/// ```
pub fn ensure_safe() {
if !std::env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
@ -61,6 +70,16 @@ pub fn ensure_safe() {
///
/// # Errors
/// An error may be returned if an error occurred while reading from the random source.
///
/// # Examples
/// ```rust,no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// let entropy = keyfork_entropy::generate_entropy_of_size(64)?;
/// assert_eq!(entropy.len(), 64);
/// # Ok(())
/// # }
/// ```
pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::Error> {
ensure_safe();
let mut vec = vec![0u8; byte_count];
@ -68,3 +87,24 @@ pub fn generate_entropy_of_size(byte_count: usize) -> Result<Vec<u8>, std::io::E
entropy_file.read_exact(&mut vec[..])?;
Ok(vec)
}
/// Read system entropy of a constant size.
///
/// # Errors
/// An error may be returned if an error occurred while reading from the random source.
///
/// # Examples
/// ```rust,no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// let entropy = keyfork_entropy::generate_entropy_of_const_size::<64>()?;
/// assert_eq!(entropy.len(), 64);
/// # Ok(())
/// # }
/// ```
pub fn generate_entropy_of_const_size<const N: usize>() -> Result<[u8; N], std::io::Error> {
let mut output = [0u8; N];
let mut entropy_file = File::open("/dev/urandom")?;
entropy_file.read_exact(&mut output[..])?;
Ok(output)
}

View File

@ -7,7 +7,7 @@ edition = "2021"
license = "MIT"
[features]
default = []
default = ["bin"]
bin = ["smex"]
[dependencies]

View File

@ -276,7 +276,7 @@ impl Mnemonic {
}
/// Clone the existing entropy.
#[deprecated]
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone()
}
@ -353,8 +353,8 @@ mod tests {
random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc();
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let new_entropy = mnemonic.entropy();
assert_eq!(&new_entropy, entropy);
let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy);
}
#[test]

View File

@ -22,8 +22,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3,
transport_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].entropy().len(), 12);
assert_eq!(mnemonics[1].entropy().len(), 32);
assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].as_bytes().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist(
"Enter a 24 and 48-word mnemonic: ",
@ -31,8 +31,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3,
combine_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].entropy().len(), 32);
assert_eq!(mnemonics[1].entropy().len(), 64);
assert_eq!(mnemonics[0].as_bytes().len(), 32);
assert_eq!(mnemonics[1].as_bytes().len(), 64);
Ok(())
}

View File

@ -1,5 +1,5 @@
//! SLIP-0010 test data for use by derivation tests.
//! Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors
//! Source: <https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vectors>
use std::collections::HashMap;

View File

@ -1,4 +1,22 @@
# Installing Keyfork
{{#include links.md}}
## Dependencies
Keyfork has different dependencies depending on the feature set used for
installation, but the default build dependencies may be installed on a Debian
system by running:
```sh
sudo apt install pkg-config nettle-dev libpcsclite-dev clang llvm
```
The runtime dependencies are:
```sh
sudo apt install libnettle8 libpcsclite1 pcscd
```
## Installing Keyfork
Keyfork is hosted using the Distrust Cargo repository. For the fastest
installation path (this is not recommended), crates may be installed directly
@ -50,5 +68,3 @@ cargo install --index https://git.distrust.co/public/_cargo-index keyfork-entrop
# Confirmed to work as of 2024-01-17.
cargo install --locked --path crates/util/keyfork-entropy --bin keyfork-entropy --features bin
```
[SBOM]: https://en.wikipedia.org/wiki/SBOM

View File

@ -1,9 +1,12 @@
<!-- vim:set et sts=0 sw=2 ts=2: -->
{{ #include links.md }}
# Summary
# User Guide
- [Introduction to Keyfork](./introduction.md)
- [Installing Keyfork](./INSTALL.md)
- [Security Considerations](./security.md)
- [Shard Commands](./shard.md)
- [Common Usage](./usage.md)
- [Configuration File](./config-file.md)

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# keyfork-derive-key
Derive a key from a given derivation path.
@ -18,5 +20,3 @@ the shell silently ignoring the single quotes in the derivation path.
Hex-encoded private key. Note that this is not the _extended_ private key, and
can't be used to derive further data.
[`keyforkd`]: ./bin/keyforkd.md

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# keyfork-derive-openpgp
Derive a key from a given derivation path.
@ -28,5 +30,3 @@ the shell silently ignoring the single quotes in the derivation path.
## Output
OpenPGP ASCII armored key, signed to be valid for 24 hours.
[`keyforkd`]: ./bin/keyforkd.md

View File

@ -1,3 +1,5 @@
{{#include ../../links.md}}
# keyfork-entropy
Retrieve system entropy, output in hex format. The machine must be running a

View File

@ -1,3 +1,5 @@
{{#include ../../links.md}}
# keyfork-mnemonic-from-seed
Generate a mnemonic from a seed passed by input.

View File

@ -1,3 +1,5 @@
{{#include ../../links.md}}
# keyfork-shard
<!-- Linked to: keyfork-user-guide/src/bin/keyfork/shard/index.md -->
@ -7,13 +9,9 @@ data. All binaries use Shamir's Secret Sharing through the [`sharks`] crate.
## OpenPGP
Keyfork provides OpenPGP compatible [`split`][openpgp-split] and
[`combine`][openpgp-combine] versions of Shard binaries. These binaries use
Keyfork provides OpenPGP compatible [`split`][kshard-opgp-split] and
[`combine`][kshard-opgp-combine] versions of Shard binaries. These binaries use
Sequoia OpenPGP and while they require all the necessary certificates for the
splitting stage, the certificates are included in the payload, and once Keyfork
supports decrypting using OpenPGP smartcards, certificates will not be required
to decrypt the shares.
[`sharks`]: https://docs.rs/sharks/latest/sharks/
[openpgp-split]: ./openpgp/split.md
[openpgp-combine]: ./openpgp/combine.md

View File

@ -1,6 +1,8 @@
{{#include ../../../links.md}}
# keyfork-shard-combine-openpgp
Combine `threshold` shares into a previously [`split`] secret.
Combine shares into a previously [`split`][kshard-opgp-split] secret.
## Arguments
@ -31,5 +33,3 @@ keyfork-shard-combine-openpgp shard.pgp
# Decrypt using on-disk private keys
keyfork-shard-combine-openpgp key_discovery.pgp shard.pgp
```
[`split`]: ./split.md

View File

@ -1,3 +1,5 @@
{{#include ../../../links.md}}
# keyfork-shard-split-openpgp
<!-- Linked to: keyfork-user-guide/src/bin/keyfork-shard/index.md -->

View File

@ -1,3 +1,5 @@
{{#include ../../../links.md}}
# `keyfork derive`
Derive keys of various formats.

View File

@ -1,3 +1,5 @@
{{#include ../../links.md}}
# keyfork
The primary interface for interacting with Keyfork utilities.
@ -33,5 +35,3 @@ been recovered, the Keyfork server starts, and derivation requests can begin.
Utilities to automatically manage the setup of Keyfork. This includes
generating a seed, splitting it into a Shard file, and provisioning smart cards
with the capability to decrypt the shards.
[BIP-0044]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

View File

@ -1,3 +1,5 @@
{{#include ../../../links.md}}
# `keyfork mnemonic`
Utilities for managing mnemonics.

View File

@ -1,3 +1,5 @@
{{#include ../../../links.md}}
# `keyfork recover`
Recover a seed to memory from a mnemonic, shard, or other format, then launch
@ -38,5 +40,3 @@ shardholders.
For every shardholder, the recovery command will prompt 33 words to be sent to
the shardholder, followed by an input prompt of 72 words to be received from
the shardholder.
[`keyfork shard transport`]: ../shard/index.md#keyfork-shard-transport

View File

@ -1,3 +1,5 @@
{{#include ../../../links.md}}
# `keyfork shard`
<!-- Linked to: keyfork-user-guide/src/bin/keyfork-shard/index.md -->
@ -128,5 +130,3 @@ keyfork shard transport shard.pgp
# Transport using on-disk private keys
keyfork shard transport key_discovery.pgp shard.pgp
```
[`keyfork recover remote-shard`]: ../recover/index.md#keyfork-recover-remote-shard

View File

@ -1,3 +1,5 @@
{{#include ../../../links.md}}
# `keyfork wizard`
Set up Keyfork using a guided setup process.
@ -63,5 +65,3 @@ shardholder.
An OpenPGP-encrypted Shard file, if not previously configured to be written to
a file using `--output`.
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# keyforkd
Keyforkd is the backend for deriving data using Keyfork. A mnemonic is loaded
@ -13,7 +15,7 @@ are not leaked. In the future, `keyforkd` could implement GUI or TTY approval
for users to approve the path requested by the client, such as `m/44'/0'` being
"Bitcoin", or `m/7366512'` being "OpenPGP".
The protocol for the UNIX socket is a framed, [bincode] format. While it is
The protocol for the UNIX socket is a framed, [`bincode`] format. While it is
custom to Keyfork, it is easy to implement. The crate `keyfork-frame` provides
a sync (`Read`, `Write`) and Tokio-compatible async (`AsyncRead`, `AsyncWrite`)
pair of methods for encoding and decoding frames.
@ -27,6 +29,3 @@ For encoding the data, the process is reversed. A SHA-256 hash is created, and
the length of the hash and the data is encoded to big-endian and written to the
stream. Then, the hash is written to the stream. Lastly, the data itself is
written as-is to the stream.
[bincode]: https://docs.rs/bincode/latest/bincode/
[BIP-0044]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

View File

@ -1,3 +1,5 @@
{{#include links.md}}
# Configuration File
The Keyfork configuration file is used to store the integrity of the mnemonic

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# Auditing Dependencies
Dependencies must be reviewed before being added to the repository, and must
@ -35,7 +37,7 @@ These dependencies will show up often:
A command line interface for generating, deriving from, and managing secrets.
* [`card-backend-pcsc`]: Interacting with smartcards using PCSC. Used as a card
backend for `openpgp-card`.
backend for [`openpgp-card`].
* [`clap`]: Command line argument parsing, helps building an intuitive command
line interface.
* [`clap_complete`]: Shell autocompletion file generator. Helps the user
@ -221,40 +223,6 @@ Test data for SLIP10/BIP-0032 derivation.
Zero-dependency hex encoding and decoding.
[`aes-gcm`]: https://github.com/RustCrypto/AEADs/tree/master/aes-gcm
[`anyhow`]: https://github.com/dtolnay/anyhow
[`bincode`]: https://github.com/bincode-org/bincode
[`card-backend`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/card-backend
[`card-backend-pcsc`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/pcsc
[`clap`]: https://github.com/clap-rs/clap/
[`clap_complete`]: https://github.com/clap-rs/clap/tree/master/clap_complete
[`digest`]: https://github.com/RustCrypto/traits/tree/master/digest
[`ed25519-dalek`]: https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek
[`hakari`]: https://docs.rs/cargo-hakari/latest/cargo_hakari/index.html
[`hkdf`]: https://github.com/RustCrypto/KDFs/tree/master/hkdf
[`hmac`]: https://github.com/RustCrypto/MACs/tree/master/hmac
[`image`]: https://github.com/image-rs/image
[`k256`]: https://github.com/RustCrypto/elliptic-curves/tree/master/k256
[`openpgp-card`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main
[`openpgp-card-sequoia`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/openpgp-card-sequoia
[`pbkdf2`]: https://github.com/RustCrypto/password-hashes/tree/master/pbkdf2
[`ripemd`]: https://github.com/RustCrypto/hashes/tree/master/ripemd
[`rqrr`]: https://github.com/WanzenBug/rqrr/
[`sequoia-openpgp`]: https://gitlab.com/sequoia-pgp/sequoia
[`serde`]: https://github.com/dtolnay/serde
[`sha2`]: https://github.com/RustCrypto/hashes/tree/master/sha2
[`thiserror`]: https://github.com/dtolnay/thiserror
[`tokio`]: https://github.com/tokio-rs/tokio
[`tower`]: https://github.com/tower-rs/tower
[`tracing`]: https://github.com/tokio-rs/tracing
[`tracing-error`]: https://github.com/tokio-rs/tracing/tree/master/tracing-error
[`tracing-subscriber`]: https://github.com/tokio-rs/tracing/tree/master/tracing-subscriber
[`v4l`]: https://github.com/raymanfx/libv4l-rs/
[`zbar`]: https://github.com/mchehab/zbar
[`bindgen`]: https://github.com/rust-lang/rust-bindgen
[`pkg-config`]: https://github.com/rust-lang/pkg-config-rs
[`keyfork-crossterm`]: #keyfork-crossterm
[`keyfork-derive-openpgp`]: #keyfork-derive-openpgp
[`keyfork-derive-path-data`]: #keyfork-derive-path-data

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# Entropy Guide
Keyfork provides a `keyfork-entropy` crate for generating entropy. The crate

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# Handling Data
In Rust, it is common to name things `as_*`, `to_*`, and `into_*`. These three

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# Writing Binaries
### Binaries - Porcelain and Plumbing

View File

@ -1,3 +1,5 @@
{{#include ../links.md}}
# Developing Provisioners
**Note:** This document makes heavy use of references to OpenPGP and assumes
@ -75,6 +77,3 @@ device. The porcelain provisioner code should make a best-effort attempt to
derive unique keys for each use, such as OpenPGP capabilities or PIV slots.
Additionally, when provisioning to a key, the configuration for that
provisioner should be stored to the configuration file.
[application identifier]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.application_identifier
[cardholder name]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.cardholder_name

14
docs/src/introduction.md Normal file
View File

@ -0,0 +1,14 @@
{{#include links.md}}
# Introduction
Keyfork is a tool to help manage the creation and derivation of binary data
using [BIP-0039] mnemonics. A mnemonic is, in simple terms, a way of encoding a
large number between 128 and 256 bits, as a list of 12 to 24 words that can be
easily stored or memorized. Once a user has a mnemonic, Keyfork utilizes
[BIP-0032] to derive cryptographic keys, which can be utilized by a variety of
applications.
## Rust documentation
Documentation is [automatically built][keyfork-rustdoc].

71
docs/src/links.md Normal file
View File

@ -0,0 +1,71 @@
<!-- DO NOT EDIT THIS FILE MANUALLY, edit links.md.template -->
<!-- vim:set et sw=4 ts=4 tw=79 ft=markdown: -->
[comments]: <> (
Please keep all links contained in this file, so they can be reused if
necessary across multiple pages.
)
[comments]: <> (
External links
)
[application identifier]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.application_identifier
[cardholder name]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.cardholder_name
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[BIP-0039]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
[BIP-0044]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
[SBOM]: https://en.wikipedia.org/wiki/SBOM
[Sequoia]: https://sequoia-pgp.org
[comments]: <> (
Crate source links
)
[`aes-gcm`]: https://github.com/RustCrypto/AEADs/tree/master/aes-gcm
[`anyhow`]: https://github.com/dtolnay/anyhow
[`bincode`]: https://github.com/bincode-org/bincode
[`card-backend`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/card-backend
[`card-backend-pcsc`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/pcsc
[`clap`]: https://github.com/clap-rs/clap/
[`clap_complete`]: https://github.com/clap-rs/clap/tree/master/clap_complete
[`digest`]: https://github.com/RustCrypto/traits/tree/master/digest
[`ed25519-dalek`]: https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek
[`hakari`]: https://docs.rs/cargo-hakari/latest/cargo_hakari/index.html
[`hkdf`]: https://github.com/RustCrypto/KDFs/tree/master/hkdf
[`hmac`]: https://github.com/RustCrypto/MACs/tree/master/hmac
[`image`]: https://github.com/image-rs/image
[`k256`]: https://github.com/RustCrypto/elliptic-curves/tree/master/k256
[`openpgp-card`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main
[`openpgp-card-sequoia`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/openpgp-card-sequoia
[`pbkdf2`]: https://github.com/RustCrypto/password-hashes/tree/master/pbkdf2
[`ripemd`]: https://github.com/RustCrypto/hashes/tree/master/ripemd
[`rqrr`]: https://github.com/WanzenBug/rqrr/
[`sequoia-openpgp`]: https://gitlab.com/sequoia-pgp/sequoia
[`serde`]: https://github.com/dtolnay/serde
[`sha2`]: https://github.com/RustCrypto/hashes/tree/master/sha2
[`sharks`]: https://github.com/c0dearm/sharks
[`thiserror`]: https://github.com/dtolnay/thiserror
[`tokio`]: https://github.com/tokio-rs/tokio
[`tower`]: https://github.com/tower-rs/tower
[`tracing`]: https://github.com/tokio-rs/tracing
[`tracing-error`]: https://github.com/tokio-rs/tracing/tree/master/tracing-error
[`tracing-subscriber`]: https://github.com/tokio-rs/tracing/tree/master/tracing-subscriber
[`v4l`]: https://github.com/raymanfx/libv4l-rs/
[`zbar`]: https://github.com/mchehab/zbar
[`bindgen`]: https://github.com/rust-lang/rust-bindgen
[`pkg-config`]: https://github.com/rust-lang/pkg-config-rs
[comments]: <> (
Internal links, based on root path
)
[`keyforkd`]: /bin/keyforkd.md
[`keyfork shard transport`]: /bin/keyfork/shard/index.md#keyfork-shard-transport
[`keyfork recover remote-shard`]: /bin/keyfork/recover/index.md#keyfork-recover-remote-shard
[kshard-opgp-split]: /bin/keyfork-shard/openpgp/split.md
[kshard-opgp-combine]: /bin/keyfork-shard/openpgp/combine.md
[keyfork-rustdoc]: ./rustdoc/keyfork/index.html

View File

@ -0,0 +1,70 @@
<!-- vim:set et sw=4 ts=4 tw=79 ft=markdown: -->
[comments]: <> (
Please keep all links contained in this file, so they can be reused if
necessary across multiple pages.
)
[comments]: <> (
External links
)
[application identifier]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.application_identifier
[cardholder name]: https://docs.rs/openpgp-card-sequoia/latest/openpgp_card_sequoia/struct.Card.html#method.cardholder_name
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[BIP-0039]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
[BIP-0044]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
[SBOM]: https://en.wikipedia.org/wiki/SBOM
[Sequoia]: https://sequoia-pgp.org
[comments]: <> (
Crate source links
)
[`aes-gcm`]: https://github.com/RustCrypto/AEADs/tree/master/aes-gcm
[`anyhow`]: https://github.com/dtolnay/anyhow
[`bincode`]: https://github.com/bincode-org/bincode
[`card-backend`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/card-backend
[`card-backend-pcsc`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/pcsc
[`clap`]: https://github.com/clap-rs/clap/
[`clap_complete`]: https://github.com/clap-rs/clap/tree/master/clap_complete
[`digest`]: https://github.com/RustCrypto/traits/tree/master/digest
[`ed25519-dalek`]: https://github.com/dalek-cryptography/curve25519-dalek/tree/main/ed25519-dalek
[`hakari`]: https://docs.rs/cargo-hakari/latest/cargo_hakari/index.html
[`hkdf`]: https://github.com/RustCrypto/KDFs/tree/master/hkdf
[`hmac`]: https://github.com/RustCrypto/MACs/tree/master/hmac
[`image`]: https://github.com/image-rs/image
[`k256`]: https://github.com/RustCrypto/elliptic-curves/tree/master/k256
[`openpgp-card`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main
[`openpgp-card-sequoia`]: https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/openpgp-card-sequoia
[`pbkdf2`]: https://github.com/RustCrypto/password-hashes/tree/master/pbkdf2
[`ripemd`]: https://github.com/RustCrypto/hashes/tree/master/ripemd
[`rqrr`]: https://github.com/WanzenBug/rqrr/
[`sequoia-openpgp`]: https://gitlab.com/sequoia-pgp/sequoia
[`serde`]: https://github.com/dtolnay/serde
[`sha2`]: https://github.com/RustCrypto/hashes/tree/master/sha2
[`sharks`]: https://github.com/c0dearm/sharks
[`thiserror`]: https://github.com/dtolnay/thiserror
[`tokio`]: https://github.com/tokio-rs/tokio
[`tower`]: https://github.com/tower-rs/tower
[`tracing`]: https://github.com/tokio-rs/tracing
[`tracing-error`]: https://github.com/tokio-rs/tracing/tree/master/tracing-error
[`tracing-subscriber`]: https://github.com/tokio-rs/tracing/tree/master/tracing-subscriber
[`v4l`]: https://github.com/raymanfx/libv4l-rs/
[`zbar`]: https://github.com/mchehab/zbar
[`bindgen`]: https://github.com/rust-lang/rust-bindgen
[`pkg-config`]: https://github.com/rust-lang/pkg-config-rs
[comments]: <> (
Internal links, based on root path
)
[`keyforkd`]: ${ROOT_PATH}/bin/keyforkd.md
[`keyfork shard transport`]: ${ROOT_PATH}/bin/keyfork/shard/index.md#keyfork-shard-transport
[`keyfork recover remote-shard`]: ${ROOT_PATH}/bin/keyfork/recover/index.md#keyfork-recover-remote-shard
[kshard-opgp-split]: ${ROOT_PATH}/bin/keyfork-shard/openpgp/split.md
[kshard-opgp-combine]: ${ROOT_PATH}/bin/keyfork-shard/openpgp/combine.md
[keyfork-rustdoc]: ./rustdoc/keyfork/index.html

81
docs/src/security.md Normal file
View File

@ -0,0 +1,81 @@
# Security Considerations
Keyfork handles data that is considered sensitive. As such, there are a few
base considerations we'd like to make about the environment Keyfork is run in.
This ensures that the amount of mitigations needed to run Keyfork are reduced.
## Build Process
Keyfork should be built using a secure toolchain, such as through Guix or the
Distrust Packages system. Using something such as `rustup` means Rust can't
properly be verified from source. Ideally, Keyfork should be built by multiple
developers and verified between them to ensure the results are deterministic.
## Hardware
Keyfork is expected to run on hardware detached from the Internet and from any
other computers. This helps ensure the Keyfork seed is never exposed to any
online system. Exposing the Keyfork seed may result in a compromise of data
derived from Keyfork. The hardware is expected to be stored in a safe location
along with the removable storage containing the operating system and (if using
Keyfork Shard) the shard file, where adversaries are not able to tamper with
the hardware, OS, or shard file.
## Software
Keyfork is intended to be one of few programs running on a given system. The
ideal system to run Keyfork under is an OS whose only dependencies are Keyfork
and Keyfork's runtime dependencies. Because of these restrictions, Keyfork does
not necessarily need to include memory-locking or memory-hardening
functionality, although such functionality may be included upon further
releases.
## Keys in Memory
As Keyfork is expected to be the only program running on a given system, it is
not expected for Keyfork to defend against malicious software on a system
scanning the memory of Keyfork and extracting the keys. As such, at this time,
Keyfork does not zero out previously-used memory. Additionally, if such
software did exist, because Keyfork is intended to run on hardware detached
from the Internet and from any other computers, the risk of practical covert
channels is reduced. Tempest and side channel attacks may be mitigated by
running Keyfork on hardware located in a Faraday cage.
## Security of Local Shards
The threat model of Keyfork in a "local shard" configuration is that an
adversary can, without leaking the seed:
* Compromise `M-1` shard holders or shards
The threat model of Keyfork in a "local shard" configuration does not include:
* Compromise of the system running Keyfork
Keyfork does not provide a mechanism by itself to ensure the operating system
or the Keyfork binary has not been tampered with. Users of Keyfork on a shared
system should verify the system has not been tampered with before starting
Keyfork.
## Security of Remote Shards
The threat model of Keyfork in a "remote shard" configuration is that an
adversary can, without leaking the seed:
* Compromise `M-1` shard holders, shard holder devices, or shards
* Eavesdrop upon (but not intercept or tamper with) secure communications
The threat model of Keyfork in a "remote shard" configuration does not include:
* The compromise of the system initiating the "remote shard" requests.
Keyfork has a "remote shard" mode, where shards may be transport-encrypted to
an ephemeral key and combined on a system run by a user we will call the
"administrator". In this design, it is expected that a secure communications
channel is established that can be spied upon but can't be tampered with. The
administrator can then begin distributing encoded (not encrypted!) public keys
to remote shardholders, who then decrypt and re-encrypt the shards to an ECDH
AES-256-GCM key. Because the shard is re-encrypted, it can't be intercepted by
anyone intercepting the communication. However, it is possible for the
administrator to leak either the Keyfork seed or any number of shards if they
are the only user operating the system combining the shares.

View File

@ -1,3 +1,5 @@
{{#include links.md}}
# Keyfork Shard Commands
Sharding a seed allows "M-of-N" recovery of the seed, which is useful for
@ -35,24 +37,31 @@ to be entered. Once the shard is decrypted, the Keyfork server will start.
## Starting Keyfork using remote systems
A line of communication should be established with the shardholders, but can be
public and/or insecure. On the system intended to run the Keyfork server, the
public and/or recorded. On the system intended to run the Keyfork server, the
following command can be run:
```sh
keyfork recover remote-shard
```
This command will continuously prompt 33 words followed by a QR code containing
the words, and read in 72 words until all necessary shards are recovered.
The command will continuously prompt a QR code, followed by 33 words, to be
sent to the remote operator. The operator must then perform their operations
and send back their own QR code, optionally followed by 72 words. The QR code
must be scanned by Keyfork, else the 72 words will be required.
Shardholders should run the following command to transport their shards:
### Shard Transport
Upon receiving the QR code and/or the 33 words, Shardholders should run the
following command to transport their shards:
```sh
keyfork shard transport < shards.pgp
```
This command will read in 33 words, prompt for a smartcard PIN, and prompt 72
words, followed by a QR code containing the words.
The QR code must be scanned by Keyfork, else the 33 words will be required.
Once entered, Keyfork will prompt with a new QR code and 72 words. A picture of
the QR code and (if requested by the lead operator) 72 words should be sent
back.
## Example: Deriving an OpenPGP key for Encryption
@ -70,5 +79,3 @@ The key, including the secret portions, can be retrieved by running the command
without the `sq` portion, but should not be run on a system with a persistent
filesystem, to avoid keeping the key on written memory for longer than
necessary.
[Sequoia]: https://sequoia-pgp.org

View File

@ -1,3 +1,5 @@
{{#include links.md}}
# Common Usage
Keyfork is a tool to help manage the creation and derivation of binary data
@ -74,6 +76,3 @@ the following command for an OpenPGP certificate with one of each subkey:
```sh
keyfork derive openpgp "John Doe <jdoe@example.com>"
```
[BIP-0039]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
[BIP-0032]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki