This commit is contained in:
waalge 2025-01-21 07:57:48 +00:00
parent 9c8fcfd768
commit 78ce899036
18 changed files with 837 additions and 307 deletions

9
Cargo.lock generated
View File

@ -466,6 +466,7 @@ version = "0.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"cryptoxide",
"ed25519-dalek", "ed25519-dalek",
"futures", "futures",
"hex", "hex",
@ -583,6 +584,12 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "cryptoxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d"
[[package]] [[package]]
name = "ctr" name = "ctr"
version = "0.9.2" version = "0.9.2"
@ -735,6 +742,7 @@ dependencies = [
"rand_core", "rand_core",
"serde", "serde",
"sha2", "sha2",
"signature",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@ -1568,6 +1576,7 @@ dependencies = [
"libp2p-quic", "libp2p-quic",
"libp2p-relay", "libp2p-relay",
"libp2p-rendezvous", "libp2p-rendezvous",
"libp2p-request-response",
"libp2p-swarm", "libp2p-swarm",
"libp2p-tcp", "libp2p-tcp",
"libp2p-upnp", "libp2p-upnp",

View File

@ -8,13 +8,22 @@ publish = false
[package.metadata.release] [package.metadata.release]
release = false 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] [dependencies]
anyhow = "1.0.95" anyhow = "1.0.95"
clap = { version = "4.5.18", features = ["derive"] } 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" futures = "0.3.30"
hex = "0.4.3" 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"] } libp2p-identity = { version = "0.2.9", features = ["ed25519", "peerid"] }
owo-colors = "4.1.0" owo-colors = "4.1.0"
quick-protobuf = "0.8.1" quick-protobuf = "0.8.1"

View File

@ -57,29 +57,7 @@ An ephemeral key consists of the following properties:
1. Expires at 1. Expires at
1. Signature 1. Signature
### Actions ### Signing Server 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.
#### Add #### Add
@ -103,6 +81,28 @@ database with the obvious fields.
The response is either `Ok()` or `Error("help message!")` 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 #### Revoke
A revoke is the opposite of add. It is also performed exclusively by admin. 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!")` 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 ## Notes
### sqlx ### sqlx
@ -138,3 +154,7 @@ sqlx migration run
``` ```
The database must be initialised before starting the application. The database must be initialised before starting the application.
### libp2p
TODO

141
app/cli.rs Normal file
View File

@ -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<Command>,
}
#[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(())
}

View File

@ -1,5 +1,20 @@
{ {
"nodes": { "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-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -124,16 +139,16 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1728538411, "lastModified": 1715447595,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", "narHash": "sha256-VsVAUQOj/cS1LCOmMjAGeRksXIAdPnFIjCQ0XLkCsT0=",
"owner": "NixOS", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", "rev": "062ca2a9370a27a35c524dc82d540e6e9824b652",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "nixos",
"ref": "nixpkgs-unstable", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@ -143,20 +158,43 @@
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"git-hooks-nix": "git-hooks-nix", "git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay", "rust-flake": "rust-flake",
"treefmt-nix": "treefmt-nix" "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": { "rust-overlay": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_2" "nixpkgs": [
"rust-flake",
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1735352767, "lastModified": 1736700680,
"narHash": "sha256-3zXufMRWUdwmp8/BTmxVW/k4MyqsPjLnnt/IlQyZvhc=", "narHash": "sha256-9gmWIb8xsycWHEYpd2SiVIAZnUULX6Y+IMMZBcDUCQU=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "a16b9a7cac7f4d39a84234d62e91890370c57d76", "rev": "5d1865c0da63b4c949f383d982b6b43519946e8f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -2,7 +2,7 @@
description = "CL L2 V0"; description = "CL L2 V0";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; 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"; flake-parts.url = "github:hercules-ci/flake-parts";
git-hooks-nix.url = "github:cachix/git-hooks.nix"; git-hooks-nix.url = "github:cachix/git-hooks.nix";
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs"; git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
@ -13,17 +13,13 @@
outputs = inputs: outputs = inputs:
inputs.flake-parts.lib.mkFlake {inherit inputs;} { inputs.flake-parts.lib.mkFlake {inherit inputs;} {
imports = [ imports = [
./nix/rust.nix inputs.rust-flake.flakeModules.default
inputs.rust-flake.flakeModules.nixpkgs
inputs.git-hooks-nix.flakeModule inputs.git-hooks-nix.flakeModule
inputs.treefmt-nix.flakeModule inputs.treefmt-nix.flakeModule
]; ];
flake = {
# Put your original flake attributes here.
};
systems = [ systems = [
# systems for which you want to build the `perSystem` attributes
"x86_64-linux" "x86_64-linux"
# ...
]; ];
perSystem = { perSystem = {
self', self',
@ -34,6 +30,21 @@
system, 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 = { treefmt = {
projectRootFile = "flake.nix"; projectRootFile = "flake.nix";
flakeFormatter = true; flakeFormatter = true;
@ -50,13 +61,8 @@
}; };
}; };
pre-commit.settings.hooks.treefmt.enable = true; pre-commit.settings.hooks.treefmt.enable = true;
_module.args.pkgs = import inputs.nixpkgs { };
inherit system; flake = {
overlays = [
inputs.rust-overlay.overlays.default
];
config = {};
};
}; };
}; };
} }

View File

@ -5,9 +5,8 @@ CREATE TABLE IF NOT EXISTS persistent_keys
CREATE TABLE IF NOT EXISTS ephemeral_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, persistent_key BLOB NOT NULL,
expires_at INTEGER NOT NULL, expires_at INTEGER NOT NULL,
signature BLOB NOT NULL,
FOREIGN KEY(persistent_key) REFERENCES persistent_keys(id) FOREIGN KEY(persistent_key) REFERENCES persistent_keys(id)
); );

View File

@ -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;
};
};
}

5
rust-toolchain.toml Normal file
View File

@ -0,0 +1,5 @@
[toolchain]
channel = "stable"
components = [ "rustfmt", "rustc-dev", "rust-analyzer", ]
targets = [ "wasm32-unknown-unknown", "x86_64-unknown-linux-gnu" ]
profile = "default"

74
src/db.rs Normal file
View File

@ -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<i64> {
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<i64> {
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<u64> {
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<VerifyingKey> {
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<Vec<VerifyingKey>> {
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::<Vec<VerifyingKey>>();
Ok(keys)
}

9
src/hash.rs Normal file
View File

@ -0,0 +1,9 @@
use cryptoxide::{blake2b::Blake2b, digest::Digest};
pub fn hash(msg: &Vec<u8>) -> [u8; 32] {
let mut digest = [0u8; 32];
let mut context = Blake2b::new(32);
context.input(msg);
context.result(&mut digest);
digest
}

56
src/keys.rs Normal file
View File

@ -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<u8>) -> Result<VerifyingKey, anyhow::Error> {
let arr: [u8; 32] = TryInto::try_into(bytes).expect("");
Ok(VerifyingKey::from_bytes(&arr)?)
}
pub fn to_bytes(key: &VerifyingKey) -> Vec<u8> {
Into::<Vec<u8>>::into(key.to_bytes())
}
pub fn from_hex(s: &str) -> Result<VerifyingKey, anyhow::Error> {
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<u8>) -> Result<SigningKey, anyhow::Error> {
let arr: [u8; 32] = TryInto::try_into(bytes).expect("");
Ok(SigningKey::from_bytes(&arr))
}
pub fn signing_key_from_hex(s: &str) -> Result<SigningKey, anyhow::Error> {
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<u8>) -> Signature {
key.try_sign(&msg).unwrap()
}
pub fn verify(key: &VerifyingKey, msg: &Vec<u8>, 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
}

5
src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod db;
pub mod hash;
pub mod keys;
pub mod protobuf;
pub mod server;

View File

@ -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<Command>,
}
#[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<i64> {
let mut conn = pool.acquire().await?;
let b = Into::<Vec<u8>>::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<i64> {
let mut conn = pool.acquire().await?;
let ephemeral_key_ = Into::<Vec<u8>>::into(ephemeral_key.to_bytes());
let persistent_key_ = Into::<Vec<u8>>::into(persistent_key.to_bytes());
let signature_ = Into::<Vec<u8>>::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()));
}

View File

@ -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;
}

180
src/protobuf/messages.rs Normal file
View File

@ -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<i32> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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::<messages::Okay>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> Result<()> {
if self.sig != Cow::Borrowed(b"") { w.write_with_tag(10, |w| w.write_bytes(&**&self.sig))?; }
Ok(())
}
}

2
src/protobuf/mod.rs Normal file
View File

@ -0,0 +1,2 @@
// Automatically generated mod.rs
pub mod messages;

206
src/server.rs Normal file
View File

@ -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<u8>,
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<Output = io::Result<Self::Request>>
+ ::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<Output = io::Result<Self::Response>>
+ ::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<Output = io::Result<()>>
+ ::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<Output = io::Result<()>>
+ ::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<dyn Error>> {
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:?}"),
_ => {}
}
}
}