diff --git a/Cargo.lock b/Cargo.lock index a07b1e9..20e7c73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,6 +466,7 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "cryptoxide", "ed25519-dalek", "futures", "hex", @@ -583,6 +584,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "cryptoxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d" + [[package]] name = "ctr" version = "0.9.2" @@ -735,6 +742,7 @@ dependencies = [ "rand_core", "serde", "sha2", + "signature", "subtle", "zeroize", ] @@ -1568,6 +1576,7 @@ dependencies = [ "libp2p-quic", "libp2p-relay", "libp2p-rendezvous", + "libp2p-request-response", "libp2p-swarm", "libp2p-tcp", "libp2p-upnp", diff --git a/Cargo.toml b/Cargo.toml index 75e48ad..64c83fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,22 @@ publish = false [package.metadata.release] release = false +[[bin]] # Bin to run the server +name = "cli" +path = "app/cli.rs" +# +# [[bin]] # Bin to run the client +# name = "client" +# path = "app/client.rs" + [dependencies] anyhow = "1.0.95" clap = { version = "4.5.18", features = ["derive"] } -ed25519-dalek = "2.1.1" +cryptoxide = "0.4.4" +ed25519-dalek = { version = "2.1.1", features = ["digest"] } futures = "0.3.30" hex = "0.4.3" -libp2p = { version = "0.54.1", features = ["tokio", "gossipsub", "mdns", "noise", "macros", "tcp", "yamux", "quic", "identify", "ping", "relay", "dcutr", "rendezvous", "kad"] } +libp2p = { version = "0.54.1", features = ["tokio", "gossipsub", "mdns", "noise", "macros", "tcp", "yamux", "quic", "identify", "ping", "relay", "dcutr", "rendezvous", "kad", "request-response"] } libp2p-identity = { version = "0.2.9", features = ["ed25519", "peerid"] } owo-colors = "4.1.0" quick-protobuf = "0.8.1" diff --git a/README.md b/README.md index c81f433..0605a2d 100644 --- a/README.md +++ b/README.md @@ -57,29 +57,7 @@ An ephemeral key consists of the following properties: 1. Expires at 1. Signature -### Actions - -#### Sign - -A sign action is the "standard" action that will occur. - -A `sign` request has the following fields - -1. Verification key -2. Payload -3. Signature - -The request is deemed valid - -1. The verification key exists in the database -1. The verification key has not expired -1. The signature is valid wrt the payload and verification key -1. The payload is "sensible" (TBC) - this is context specific. - -The response to a valid request - -1. The signature produced by the persistent key associated to the verification - key for the same payload. +### Signing Server Actions #### Add @@ -103,6 +81,28 @@ database with the obvious fields. The response is either `Ok()` or `Error("help message!")` +#### Sign + +A sign action is the "standard" action that will occur. + +A `sign` request has the following fields + +1. Verification key +2. Payload +3. Signature + +The request is deemed valid + +1. The verification key exists in the database +1. The verification key has not expired +1. The signature is valid wrt the payload and verification key +1. The payload is "sensible" (TBC) - this is context specific. + +The response to a valid request + +1. The signature produced by the persistent key associated to the verification + key for the same payload. + #### Revoke A revoke is the opposite of add. It is also performed exclusively by admin. @@ -123,6 +123,22 @@ database. The response is either `Ok()` or `Error("help message!")` +#### SIMPLIFICATION + +The `add` and `revoke` endpoints are needed only by admin, and infrequently, or +in case of emergency. In contrast, `sign` is needed with relatively high +frequency, and called by less trusted machines. + +Thus, we assume that admin accesses the machine directly and simply adds the +ephemeral keys to the db. This does not require a valid signature - if the pub +key is registered, then a signature request is honored. + +### Client server actions + +#### Demo task + +Some way to trigger the client service to make a request to the server. + ## Notes ### sqlx @@ -138,3 +154,7 @@ sqlx migration run ``` The database must be initialised before starting the application. + +### libp2p + +TODO diff --git a/app/cli.rs b/app/cli.rs new file mode 100644 index 0000000..2d088be --- /dev/null +++ b/app/cli.rs @@ -0,0 +1,141 @@ +use std::env; + +use clap::{Parser, Subcommand}; +use ed25519_dalek::{Signature, SigningKey}; +use sqlx::sqlite::SqlitePool; + +use cll2v0::{db, keys}; + +/// cll2v0 is a playground for rust libraries. +/// This is signing service. +#[derive(Parser)] +#[command(arg_required_else_help(true), version, about)] +struct Args { + #[command(subcommand)] + cmd: Option, +} + +#[derive(Subcommand)] +enum Command { + /// Add new ephemeral key to an existing persistent key + Add { + ephemeral_key: String, + persistent_key: String, + expires_at: i64, + }, + /// Revoke an existing ephemeral key + Revoke { ephemeral_key: String }, + /// Check persistent keys are available and display + Check, + /// Gen from seed some sensible ephemeral key args to be used with `add` + Gen { seed: u8 }, + /// Sign a message + Sign { + signing_key: String, + message: String, + }, + /// Verify a message + Verify { + verifying_key: String, + message: String, + signature: String, + }, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + let keys_path = env::var("CLL2V0_KEYS").expect("Expect `CLL2V0_KEYS` to be set"); + let keychain = keys::mk_keychain(&keys_path); + + let db_url = env::var("DATABASE_URL").expect("Expect `DATABASE_URL` to be set"); + let pool = SqlitePool::connect(&db_url).await?; + for vkey_bytes in keychain.keys() { + let vkey = keys::from_bytes(vkey_bytes.into())?; + let _ = db::add_persistent_key(&pool, &vkey).await; + } + + match args.cmd { + Some(Command::Add { + ephemeral_key, + persistent_key, + expires_at, + }) => { + println!("Adding new ephemeral key '{ephemeral_key}'"); + let e = keys::from_hex(&ephemeral_key).expect("cannot parse key"); + let p = keys::from_hex(&persistent_key).expect("cannot parse key"); + let res = db::add_ephemeral_key(&pool, &e, &p, expires_at).await?; + println!("Added new ekey: {}", res); + } + Some(Command::Revoke { ephemeral_key }) => { + let e = keys::from_hex(&ephemeral_key).expect("cannot parse key"); + let res = db::revoke_ephemeral_key(&pool, &e).await?; + println!("Revoked key: {}", res); + } + Some(Command::Check) => { + eprintln!("!Check skipped"); + let res = db::list_persistent_keys(&pool).await?; + println!("Persistent keys:"); + res.into_iter() + .for_each(|key| println!("{}", keys::to_hex(&key))) + } + Some(Command::Gen { seed }) => { + let ekey = SigningKey::from_bytes(&[ + seed, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ]); + println!("Ephemeral signing key:"); + println!("{}", keys::signing_key_to_hex(&ekey)); + let res = db::list_persistent_keys(&pool).await?; + let idx = (seed as usize) % res.len(); + let pkey = res.get(idx); + match pkey { + None => eprintln!("No persistent keys found"), + Some(pkey) => { + let now: usize = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + .try_into() + .unwrap(); + let twenty_mins_from_now = 20usize * 60 * 1000 + now; + println!("Args"); + println!( + "{} {} {}", + keys::to_hex(&ekey.verifying_key()), + keys::to_hex(pkey), + twenty_mins_from_now + ) + } + } + } + Some(Command::Sign { + signing_key, + message, + }) => { + let mut skey = keys::signing_key_from_hex(&signing_key).unwrap(); + let msg = hex::decode(message).unwrap(); + let sig = keys::sign(&mut skey, msg); + println!("{}", hex::encode(sig.to_bytes())) + } + Some(Command::Verify { + verifying_key, + message, + signature, + }) => { + let vkey = keys::from_hex(&verifying_key).unwrap(); + let msg = hex::decode(message).unwrap(); + let sig = Signature::from_bytes( + &TryInto::<[u8; 64]>::try_into(hex::decode(signature).unwrap()).unwrap(), + ); + let res = keys::verify(&vkey, &msg, &sig).unwrap(); + println!("{:?}", res); + } + None => { + println!("See help"); + } + } + + Ok(()) +} diff --git a/flake.lock b/flake.lock index f14e894..823ad2a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1736566337, + "narHash": "sha256-SC0eDcZPqISVt6R0UfGPyQLrI0+BppjjtQ3wcSlk0oI=", + "owner": "ipetkov", + "repo": "crane", + "rev": "9172acc1ee6c7e1cbafc3044ff850c568c75a5a3", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -124,16 +139,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1728538411, - "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", - "owner": "NixOS", + "lastModified": 1715447595, + "narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", + "rev": "062ca2a9370a27a35c524dc82d540e6e9824b652", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", + "owner": "nixos", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -143,20 +158,43 @@ "flake-parts": "flake-parts", "git-hooks-nix": "git-hooks-nix", "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay", + "rust-flake": "rust-flake", "treefmt-nix": "treefmt-nix" } }, + "rust-flake": { + "inputs": { + "crane": "crane", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1736806612, + "narHash": "sha256-WioA+Vk7suDK+Ek77rDlbuxV6WqwFt30JsKHrmDCSiU=", + "owner": "juspay", + "repo": "rust-flake", + "rev": "b5f39885e2fcf137bfaf75decc077f9cca2bd984", + "type": "github" + }, + "original": { + "owner": "juspay", + "repo": "rust-flake", + "type": "github" + } + }, "rust-overlay": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "rust-flake", + "nixpkgs" + ] }, "locked": { - "lastModified": 1735352767, - "narHash": "sha256-3zXufMRWUdwmp8/BTmxVW/k4MyqsPjLnnt/IlQyZvhc=", + "lastModified": 1736700680, + "narHash": "sha256-9gmWIb8xsycWHEYpd2SiVIAZnUULX6Y+IMMZBcDUCQU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a16b9a7cac7f4d39a84234d62e91890370c57d76", + "rev": "5d1865c0da63b4c949f383d982b6b43519946e8f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0bf1a40..0124dc9 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "CL L2 V0"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; - rust-overlay.url = "github:oxalica/rust-overlay"; + rust-flake.url = "github:juspay/rust-flake"; flake-parts.url = "github:hercules-ci/flake-parts"; git-hooks-nix.url = "github:cachix/git-hooks.nix"; git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs"; @@ -13,17 +13,13 @@ outputs = inputs: inputs.flake-parts.lib.mkFlake {inherit inputs;} { imports = [ - ./nix/rust.nix + inputs.rust-flake.flakeModules.default + inputs.rust-flake.flakeModules.nixpkgs inputs.git-hooks-nix.flakeModule inputs.treefmt-nix.flakeModule ]; - flake = { - # Put your original flake attributes here. - }; systems = [ - # systems for which you want to build the `perSystem` attributes "x86_64-linux" - # ... ]; perSystem = { self', @@ -34,6 +30,21 @@ system, ... }: { + devShells.default = pkgs.mkShell { + inputsFrom = [ + self'.devShells.rust + config.pre-commit.devShell + ]; + shellHook = '' + echo 1>&2 "Welcome to the development shell!" + ${config.pre-commit.installationScript} + export DATABASE_URL="sqlite:./db/cll2v0.db"; + export CLL2V0_KEYS="db/keys"; + ''; + name = "cardano-lightning"; + packages = with pkgs; [ + ]; + }; treefmt = { projectRootFile = "flake.nix"; flakeFormatter = true; @@ -50,13 +61,8 @@ }; }; pre-commit.settings.hooks.treefmt.enable = true; - _module.args.pkgs = import inputs.nixpkgs { - inherit system; - overlays = [ - inputs.rust-overlay.overlays.default - ]; - config = {}; - }; + }; + flake = { }; }; } diff --git a/migrations/001_keys.sql b/migrations/001_keys.sql index 9c7f915..daa0246 100644 --- a/migrations/001_keys.sql +++ b/migrations/001_keys.sql @@ -5,9 +5,8 @@ CREATE TABLE IF NOT EXISTS persistent_keys CREATE TABLE IF NOT EXISTS ephemeral_keys ( - id BLOB PRIMARY KEY NOT NULL, + id BLOB PRIMARY KEY NOT NULL, persistent_key BLOB NOT NULL, - expires_at INTEGER NOT NULL, - signature BLOB NOT NULL, + expires_at INTEGER NOT NULL, FOREIGN KEY(persistent_key) REFERENCES persistent_keys(id) ); diff --git a/nix/rust.nix b/nix/rust.nix deleted file mode 100644 index 51c3901..0000000 --- a/nix/rust.nix +++ /dev/null @@ -1,92 +0,0 @@ -{ - perSystem = { - config, - self', - inputs', - pkgs, - lib, - system, - ... - }: let - osxDependencies = with pkgs; - lib.optionals stdenv.isDarwin - [ - darwin.apple_sdk.frameworks.Security - darwin.apple_sdk.frameworks.CoreServices - ]; - - cargoTomlContents = builtins.readFile ../Cargo.toml; - version = (builtins.fromTOML cargoTomlContents).package.version; - echo-net = pkgs.rustPlatform.buildRustPackage { - inherit version; - name = "cll2v0"; - buildInputs = with pkgs; [openssl] ++ osxDependencies; - nativeBuildInputs = with pkgs; [pkg-config openssl.dev]; - src = let - baseDir = ./../.; - fileHasAnySuffix = fileSuffixes: file: (lib.lists.any (s: lib.hasSuffix s file.name) fileSuffixes); - rustFilter = basePath: ( - let - mainFilter = lib.fileset.fileFilter (fileHasAnySuffix [".rs" ".toml"]) basePath; - in - lib.fileset.unions [mainFilter (basePath + "/Cargo.toml") (basePath + "/Cargo.lock")] - ); - in - pkgs.lib.fileset.toSource { - root = baseDir; - fileset = rustFilter baseDir; - }; - cargoLock.lockFile = ../Cargo.lock; - meta = { - description = "Cardano Lightning L2 V0"; - homepage = "cardano-lightning.org"; - # license = licenses.mit; - }; - }; - - packages = { - echo-net = echo-net; - default = packages.echo-net; - }; - - # FIXME :: I don't know if this is necessary, - # but I don't know to fix it. - # overlays.default = final: prev: {echo-net = packages.echo-net;}; - - gitRev = - if (builtins.hasAttr "rev" self') - then self'.rev - else "dirty"; - in { - inherit packages; # overlays; - - devShells.default = pkgs.mkShell { - nativeBuildInputs = [ - config.treefmt.build.wrapper - ]; - shellHook = '' - export GIT_REVISION=${gitRev} - ${config.pre-commit.installationScript} - ''; - buildInputs = with pkgs; - [ - pkg-config - openssl - cargo-insta - (pkgs.rust-bin.stable.latest.default.override { - extensions = ["rust-src" "clippy" "rustfmt" "rust-analyzer"]; - # targets = [ "wasm32-unknown-unknown" ]; - }) - sqlx-cli - sqlite - # nodePackages_latest.nodejs - # nodePackages_latest.typescript-language-server - # cmake - # wasm-pack - # protobuf - # sqlite - ] - ++ osxDependencies; - }; - }; -} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..82a62aa --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "stable" +components = [ "rustfmt", "rustc-dev", "rust-analyzer", ] +targets = [ "wasm32-unknown-unknown", "x86_64-unknown-linux-gnu" ] +profile = "default" diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..eb06bff --- /dev/null +++ b/src/db.rs @@ -0,0 +1,74 @@ +use ed25519_dalek::VerifyingKey; +use sqlx::sqlite::SqlitePool; + +use crate::keys; + +pub async fn add_persistent_key(pool: &SqlitePool, vkey: &VerifyingKey) -> anyhow::Result { + let mut conn = pool.acquire().await?; + let b = keys::to_bytes(&vkey); + let id = sqlx::query!(r#"INSERT INTO persistent_keys ( id ) VALUES ( ?1 )"#, b) + .execute(&mut *conn) + .await? + .last_insert_rowid(); + Ok(id) +} + +pub async fn add_ephemeral_key( + pool: &SqlitePool, + ephemeral: &VerifyingKey, + persistent: &VerifyingKey, + expires_at: i64, +) -> anyhow::Result { + let mut conn = pool.acquire().await?; + let eph = keys::to_bytes(ephemeral); + let per = keys::to_bytes(persistent); + let res = sqlx::query!( + r#" INSERT INTO ephemeral_keys ( id, persistent_key, expires_at) VALUES ( ?1, ?2, ?3 ) "#, + eph, + per, + expires_at, + ) + .execute(&mut *conn) + .await? + .last_insert_rowid(); + Ok(res) +} + +pub async fn revoke_ephemeral_key( + pool: &SqlitePool, + ephemeral_key: &VerifyingKey, +) -> anyhow::Result { + let mut conn = pool.acquire().await?; + let b = keys::to_bytes(ephemeral_key); + let res = sqlx::query!(r#" DELETE FROM ephemeral_keys WHERE id = ? "#, b) + .execute(&mut *conn) + .await? + .rows_affected(); + Ok(res) +} + +pub async fn get_persistent_key( + pool: &SqlitePool, + ephemeral_key: &VerifyingKey, +) -> anyhow::Result { + let b = keys::to_bytes(ephemeral_key); + let rec = sqlx::query!( + r#" SELECT persistent_key FROM ephemeral_keys WHERE id = ? AND expires_at > date(); "#, + b, + ) + .fetch_one(pool) + .await?; + keys::from_bytes(rec.persistent_key) +} + +pub async fn list_persistent_keys(pool: &SqlitePool) -> anyhow::Result> { + let recs = sqlx::query!(r#" SELECT id FROM persistent_keys ORDER BY id "#) + .fetch_all(pool) + .await?; + + let keys = recs + .into_iter() + .map(|b| keys::from_bytes(b.id).expect("illegal")) + .collect::>(); + Ok(keys) +} diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..17babbc --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,9 @@ +use cryptoxide::{blake2b::Blake2b, digest::Digest}; + +pub fn hash(msg: &Vec) -> [u8; 32] { + let mut digest = [0u8; 32]; + let mut context = Blake2b::new(32); + context.input(msg); + context.result(&mut digest); + digest +} diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..b11371c --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,56 @@ +use std::{collections::HashMap, fs::read_to_string}; + +use ed25519_dalek::{ed25519::signature::SignerMut, Signature, SigningKey, VerifyingKey}; + +pub fn from_bytes(bytes: Vec) -> Result { + let arr: [u8; 32] = TryInto::try_into(bytes).expect(""); + Ok(VerifyingKey::from_bytes(&arr)?) +} + +pub fn to_bytes(key: &VerifyingKey) -> Vec { + Into::>::into(key.to_bytes()) +} + +pub fn from_hex(s: &str) -> Result { + from_bytes(hex::decode(s)?.try_into()?) +} + +pub fn to_hex(key: &VerifyingKey) -> String { + hex::encode(key.to_bytes()) +} + +pub fn signing_key_from_bytes(bytes: Vec) -> Result { + let arr: [u8; 32] = TryInto::try_into(bytes).expect(""); + Ok(SigningKey::from_bytes(&arr)) +} + +pub fn signing_key_from_hex(s: &str) -> Result { + signing_key_from_bytes(hex::decode(s)?.try_into()?) +} + +pub fn signing_key_to_hex(key: &SigningKey) -> String { + hex::encode(key.to_bytes()) +} + +pub fn sign(key: &mut SigningKey, msg: Vec) -> Signature { + key.try_sign(&msg).unwrap() +} + +pub fn verify(key: &VerifyingKey, msg: &Vec, sig: &Signature) -> Result<(), anyhow::Error> { + let _ = key.verify_strict(msg, sig)?; + Ok(()) +} + +pub type Keychain = HashMap<[u8; 32], SigningKey>; + +pub fn mk_keychain(keys_path: &str) -> Keychain { + let signing_keys = read_to_string(&keys_path).expect("Error reading file"); + let mut keychain: Keychain = HashMap::new(); + for key in signing_keys.lines() { + let secret_key = hex::decode(key.trim()).unwrap().try_into().unwrap(); + let skey = SigningKey::from_bytes(&secret_key); + let vkey = skey.verifying_key(); + keychain.insert(vkey.to_bytes(), skey); + } + keychain +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b5ff6e9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod hash; +pub mod keys; +pub mod protobuf; +pub mod server; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index a7613be..0000000 --- a/src/main.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::collections::HashMap; -use std::env; -use std::fs::read_to_string; - -use clap::{Parser, Subcommand}; -use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; -use hex; -use sqlx::sqlite::SqlitePool; - -#[derive(Parser)] -struct Args { - #[command(subcommand)] - cmd: Option, -} - -#[derive(Subcommand)] -enum Command { - Add { - ephemeral_key: String, - persistent_key: String, - expires_at: i64, - signature: String, - }, -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - let keys_path = env::var("CLL2V0_KEYS").expect("Expect `CLL2V0_KEYS` to be set"); - // let db_url = env::var("CLL2V0_DB").expect("Expect `CLL2V0_DB` to be set"); - let db_url = env::var("DATABASE_URL").expect("Expect `DATABASE_URL` to be set"); - let signing_keys = read_to_string(&keys_path).expect("Error reading file"); - - let pool = SqlitePool::connect(&db_url).await?; - - let mut keychain: HashMap<[u8; 32], SigningKey> = HashMap::new(); - for key in signing_keys.lines() { - let secret_key = hex::decode(key.trim()).unwrap().try_into().unwrap(); - let skey = SigningKey::from_bytes(&secret_key); - let vkey = skey.verifying_key(); - keychain.insert(vkey.to_bytes(), skey); - let _ = add_persistent_key(&pool, vkey).await; - } - - match args.cmd { - Some(Command::Add { - ephemeral_key, - persistent_key, - expires_at, - signature, - }) => { - println!("Adding new ephemeral key '{ephemeral_key}'"); - let e = VerifyingKey::from_bytes( - &hex::decode(ephemeral_key) - .expect("Cannot parse ephemeral key") - .try_into() - .expect("wrong length"), - ) - .expect("bad key"); - let p = VerifyingKey::from_bytes( - &hex::decode(persistent_key) - .expect("Cannot parse persistent") - .try_into() - .expect("wrong length"), - ) - .expect("bad key"); - let s = Signature::from_bytes( - &hex::decode(signature) - .expect("Cannot parse persistent") - .try_into() - .expect("wrong length"), - ); - - let res = add_ephemeral_key(&pool, &e, &p, expires_at, &s).await?; - println!("Added new ekey: {}", res); - } - None => { - println!("Printing list of all todos"); - list_persistent_keys(&pool).await?; - } - } - - Ok(()) -} - -async fn add_persistent_key(pool: &SqlitePool, vkey: VerifyingKey) -> anyhow::Result { - let mut conn = pool.acquire().await?; - let b = Into::>::into(vkey.to_bytes()); - - // Insert the task, then obtain the ID of this row - let id = sqlx::query!( - r#" -INSERT INTO persistent_keys ( id ) -VALUES ( ?1 ) - "#, - b - ) - .execute(&mut *conn) - .await? - .last_insert_rowid(); - - Ok(id) -} - -async fn add_ephemeral_key( - pool: &SqlitePool, - ephemeral_key: &VerifyingKey, - persistent_key: &VerifyingKey, - expires_at: i64, - signature: &Signature, -) -> anyhow::Result { - let mut conn = pool.acquire().await?; - - let ephemeral_key_ = Into::>::into(ephemeral_key.to_bytes()); - let persistent_key_ = Into::>::into(persistent_key.to_bytes()); - let signature_ = Into::>::into(signature.to_bytes()); - // Insert the task, then obtain the ID of this row - let res = sqlx::query!( - r#" -INSERT INTO ephemeral_keys ( - id, - persistent_key, - expires_at, - signature -) -VALUES ( ?1, ?2, ?3, ?4 ) - "#, - ephemeral_key_, - persistent_key_, - expires_at, - signature_, - ) - .execute(&mut *conn) - .await? - .last_insert_rowid(); - - Ok(res) -} - -async fn list_persistent_keys(pool: &SqlitePool) -> anyhow::Result<()> { - let recs = sqlx::query!( - r#" -SELECT id -FROM persistent_keys -ORDER BY id - "# - ) - .fetch_all(pool) - .await?; - - for rec in recs { - println!("- [{}]", hex::encode(rec.id),); - } - - Ok(()) -} - -fn print_key() { - // Useful for setting up some keys - let mut rng = rand::rngs::OsRng; - println!("{}", hex::encode(SigningKey::generate(&mut rng).to_bytes())); -} diff --git a/src/protobuf/messages.proto b/src/protobuf/messages.proto new file mode 100644 index 0000000..ebd4095 --- /dev/null +++ b/src/protobuf/messages.proto @@ -0,0 +1,25 @@ +syntax="proto3"; +package messages; + +message SignRequest { + bytes vkey = 1; + bytes body = 2; + bytes sig = 3; +} + +message SignResponse { + oneof result { + Okay okay = 1; + Fail fail = 2; + } +} + +message Okay { + bytes sig = 1; +} + +enum Fail { + UnrecognisedKey = 1; + ExpiredKey = 2; + Other = 3; +} diff --git a/src/protobuf/messages.rs b/src/protobuf/messages.rs new file mode 100644 index 0000000..9f3d011 --- /dev/null +++ b/src/protobuf/messages.rs @@ -0,0 +1,180 @@ +// Automatically generated rust module for 'messages.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + + +use std::borrow::Cow; +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Fail { + UnrecognisedKey = 1, + ExpiredKey = 2, + Other = 3, +} + +impl Default for Fail { + fn default() -> Self { + Fail::UnrecognisedKey + } +} + +impl From for Fail { + fn from(i: i32) -> Self { + match i { + 1 => Fail::UnrecognisedKey, + 2 => Fail::ExpiredKey, + 3 => Fail::Other, + _ => Self::default(), + } + } +} + +impl<'a> From<&'a str> for Fail { + fn from(s: &'a str) -> Self { + match s { + "UnrecognisedKey" => Fail::UnrecognisedKey, + "ExpiredKey" => Fail::ExpiredKey, + "Other" => Fail::Other, + _ => Self::default(), + } + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct SignRequest<'a> { + pub vkey: Cow<'a, [u8]>, + pub body: Cow<'a, [u8]>, + pub sig: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for SignRequest<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.vkey = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(18) => msg.body = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(26) => msg.sig = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for SignRequest<'a> { + fn get_size(&self) -> usize { + 0 + + if self.vkey == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.vkey).len()) } + + if self.body == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.body).len()) } + + if self.sig == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.sig).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.vkey != Cow::Borrowed(b"") { w.write_with_tag(10, |w| w.write_bytes(&**&self.vkey))?; } + if self.body != Cow::Borrowed(b"") { w.write_with_tag(18, |w| w.write_bytes(&**&self.body))?; } + if self.sig != Cow::Borrowed(b"") { w.write_with_tag(26, |w| w.write_bytes(&**&self.sig))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct SignResponse<'a> { + pub result: messages::mod_SignResponse::OneOfresult<'a>, +} + +impl<'a> MessageRead<'a> for SignResponse<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.result = messages::mod_SignResponse::OneOfresult::okay(r.read_message::(bytes)?), + Ok(16) => msg.result = messages::mod_SignResponse::OneOfresult::fail(r.read_enum(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for SignResponse<'a> { + fn get_size(&self) -> usize { + 0 + + match self.result { + messages::mod_SignResponse::OneOfresult::okay(ref m) => 1 + sizeof_len((m).get_size()), + messages::mod_SignResponse::OneOfresult::fail(ref m) => 1 + sizeof_varint(*(m) as u64), + messages::mod_SignResponse::OneOfresult::None => 0, + } } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + match self.result { messages::mod_SignResponse::OneOfresult::okay(ref m) => { w.write_with_tag(10, |w| w.write_message(m))? }, + messages::mod_SignResponse::OneOfresult::fail(ref m) => { w.write_with_tag(16, |w| w.write_enum(*m as i32))? }, + messages::mod_SignResponse::OneOfresult::None => {}, + } Ok(()) + } +} + +pub mod mod_SignResponse { + +use super::*; + +#[derive(Debug, PartialEq, Clone)] +pub enum OneOfresult<'a> { + okay(messages::Okay<'a>), + fail(messages::Fail), + None, +} + +impl<'a> Default for OneOfresult<'a> { + fn default() -> Self { + OneOfresult::None + } +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Okay<'a> { + pub sig: Cow<'a, [u8]>, +} + +impl<'a> MessageRead<'a> for Okay<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.sig = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Okay<'a> { + fn get_size(&self) -> usize { + 0 + + if self.sig == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.sig).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.sig != Cow::Borrowed(b"") { w.write_with_tag(10, |w| w.write_bytes(&**&self.sig))?; } + Ok(()) + } +} + diff --git a/src/protobuf/mod.rs b/src/protobuf/mod.rs new file mode 100644 index 0000000..49c19b6 --- /dev/null +++ b/src/protobuf/mod.rs @@ -0,0 +1,2 @@ +// Automatically generated mod.rs +pub mod messages; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..47a2c3c --- /dev/null +++ b/src/server.rs @@ -0,0 +1,206 @@ +use std::{error::Error, time::Duration}; + +use futures::prelude::*; +use libp2p::{ + mdns, noise, ping, + request_response::{self, Codec}, + swarm::SwarmEvent, + tcp, yamux, Multiaddr, +}; +use tracing_subscriber::EnvFilter; + +// We create a custom network behaviour + +#[derive(NetworkBehaviour)] +struct MyBehaviour { + req_res: request_response::Behaviour, + mdns: mdns::tokio::Behaviour, +} + +struct MyRequest { + key: [u8; 32], + body: Vec, + sig: [u8; 64], +} + +struct MyResponse { + sig: [u8; 64], +} + +struct MyCodec; + +impl Codec for MyCodec { + #[doc = " The type of protocol(s) or protocol versions being negotiated."] + type Protocol = String; + + #[doc = " The type of inbound and outbound requests."] + type Request = MyRequest; + + #[doc = " The type of inbound and outbound responses."] + type Response = MyResponse; + + #[doc = " Reads a request from the given I/O stream according to the"] + #[doc = " negotiated protocol."] + #[must_use] + #[allow( + elided_named_lifetimes, + clippy::type_complexity, + clippy::type_repetition_in_bounds + )] + fn read_request<'life0, 'life1, 'life2, 'async_trait, T>( + &'life0 mut self, + _protocol: &'life1 Self::Protocol, + io: &'life2 mut T, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + T: AsyncRead + Unpin + Send, + T: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + todo!() + } + + #[doc = " Reads a response from the given I/O stream according to the"] + #[doc = " negotiated protocol."] + #[must_use] + #[allow( + elided_named_lifetimes, + clippy::type_complexity, + clippy::type_repetition_in_bounds + )] + fn read_response<'life0, 'life1, 'life2, 'async_trait, T>( + &'life0 mut self, + protocol: &'life1 Self::Protocol, + io: &'life2 mut T, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + T: AsyncRead + Unpin + Send, + T: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + todo!() + } + + #[doc = " Writes a request to the given I/O stream according to the"] + #[doc = " negotiated protocol."] + #[must_use] + #[allow( + elided_named_lifetimes, + clippy::type_complexity, + clippy::type_repetition_in_bounds + )] + fn write_request<'life0, 'life1, 'life2, 'async_trait, T>( + &'life0 mut self, + protocol: &'life1 Self::Protocol, + io: &'life2 mut T, + req: Self::Request, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + T: AsyncWrite + Unpin + Send, + T: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + todo!() + } + + #[doc = " Writes a response to the given I/O stream according to the"] + #[doc = " negotiated protocol."] + #[must_use] + #[allow( + elided_named_lifetimes, + clippy::type_complexity, + clippy::type_repetition_in_bounds + )] + fn write_response<'life0, 'life1, 'life2, 'async_trait, T>( + &'life0 mut self, + protocol: &'life1 Self::Protocol, + io: &'life2 mut T, + res: Self::Response, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future> + + ::core::marker::Send + + 'async_trait, + >, + > + where + T: AsyncWrite + Unpin + Send, + T: 'async_trait, + 'life0: 'async_trait, + 'life1: 'async_trait, + 'life2: 'async_trait, + Self: 'async_trait, + { + todo!() + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut swarm = libp2p::SwarmBuilder::with_new_identity() + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + )? + .with_behaviour(|key| { + let mdns = + mdns::tokio::Behaviour::new(mdns::Config::default(), key.public().to_peer_id())?; + let req_res = request_response::Behaviour::with_codec(MyCodec, protocol, config); + Ok(MyBehaviour { req_res, mdns }) + })? + .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(u64::MAX))) + .build(); + + // Tell the swarm to listen on all interfaces and a random, OS-assigned + // port. + swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; + + // Dial the peer identified by the multi-address given as the second + // command-line argument, if any. + if let Some(addr) = std::env::args().nth(1) { + let remote: Multiaddr = addr.parse()?; + swarm.dial(remote)?; + println!("Dialed {addr}") + } + + loop { + match swarm.select_next_some().await { + SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"), + SwarmEvent::Behaviour(event) => println!("{event:?}"), + _ => {} + } + } +}