generated from Patagia/template-nix
parent
5ea8635a54
commit
b459e6a3c1
13 changed files with 535 additions and 7 deletions
controller
|
@ -7,12 +7,21 @@ version.workspace = true
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
dropshot.workspace = true
|
||||
ed25519-dalek.workspace = true
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
hkdf.workspace = true
|
||||
http.workspace = true
|
||||
instrumentation = { path = "../instrumentation" }
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
sha2.workspace = true
|
||||
slog-async.workspace = true
|
||||
slog.workspace = true
|
||||
sqlx = { version = "0.8.5", default-features = false, features = [
|
||||
|
@ -23,6 +32,7 @@ trace-request = { path = "../trace-request" }
|
|||
tracing-slog.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
x25519-dalek.workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["http"]
|
||||
|
|
|
@ -4,6 +4,7 @@ use dropshot::ApiDescription;
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::context::ControllerContext;
|
||||
use crate::node;
|
||||
use crate::user;
|
||||
use crate::version;
|
||||
|
||||
|
@ -11,6 +12,7 @@ type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
|
|||
|
||||
pub fn api() -> Result<ControllerApiDescription> {
|
||||
let mut api = ControllerApiDescription::new();
|
||||
node::register_api(&mut api)?;
|
||||
user::register_api(&mut api)?;
|
||||
api.register(version::version)?;
|
||||
Ok(api)
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
use sqlx::postgres::PgPool;
|
||||
|
||||
use crate::node::NodeController;
|
||||
|
||||
pub struct ControllerContext {
|
||||
pub pg_pool: PgPool,
|
||||
// pub node_controller: Mutex<NodeController>,
|
||||
pub node_controller: NodeController,
|
||||
}
|
||||
|
||||
impl ControllerContext {
|
||||
pub fn new(pg_pool: PgPool) -> ControllerContext {
|
||||
ControllerContext { pg_pool }
|
||||
// let node_controller = Mutex::new(NodeController::new(pg_pool.to_owned()));
|
||||
let node_controller = NodeController::new(pg_pool.to_owned());
|
||||
ControllerContext {
|
||||
pg_pool,
|
||||
node_controller,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod api;
|
||||
pub mod context;
|
||||
|
||||
mod node;
|
||||
mod user;
|
||||
mod version;
|
||||
|
|
43
controller/src/node/api.rs
Normal file
43
controller/src/node/api.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use dropshot::{endpoint, HttpError, HttpResponseOk, Query, RequestContext};
|
||||
use dropshot::{ApiDescription, ApiDescriptionRegisterError};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use trace_request::trace_request;
|
||||
use x25519_dalek::PublicKey;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::context::ControllerContext;
|
||||
use crate::node::controller::RegistrationInfo;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)]
|
||||
pub struct RegistrationParams {
|
||||
#[serde_as(as = "serde_with::base64::Base64<serde_with::base64::UrlSafe>")]
|
||||
#[schemars(with = "String")]
|
||||
key: [u8; 32],
|
||||
}
|
||||
|
||||
pub fn register_api(
|
||||
api: &mut ApiDescription<Arc<ControllerContext>>,
|
||||
) -> Result<(), ApiDescriptionRegisterError> {
|
||||
api.register(get_registration_info)
|
||||
}
|
||||
|
||||
/// Get registration info
|
||||
#[endpoint {
|
||||
method = GET,
|
||||
path = "/nodes/register",
|
||||
tags = [ "node" ],
|
||||
}]
|
||||
#[trace_request]
|
||||
async fn get_registration_info(
|
||||
rqctx: RequestContext<Arc<ControllerContext>>,
|
||||
params: Query<RegistrationParams>,
|
||||
) -> Result<HttpResponseOk<RegistrationInfo>, HttpError> {
|
||||
let key = PublicKey::from(params.into_inner().key);
|
||||
tracing::debug!("Registration info for public key: {:?}", key);
|
||||
let info = rqctx.context().node_controller.new_registration(&key);
|
||||
Ok(HttpResponseOk(info))
|
||||
}
|
179
controller/src/node/controller.rs
Normal file
179
controller/src/node/controller.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use chrono::DateTime;
|
||||
use ed25519_dalek::ed25519::signature::SignerMut;
|
||||
use hkdf::Hkdf;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use sha2::Sha256;
|
||||
use sqlx::postgres::PgPool;
|
||||
use uuid::Uuid;
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize, JsonSchema, Serialize)]
|
||||
pub struct RegistrationInfo {
|
||||
node_id: Uuid,
|
||||
challenge: Challenge,
|
||||
|
||||
#[serde_as(as = "serde_with::base64::Base64")]
|
||||
#[schemars(with = "String")]
|
||||
challenge_signature: [u8; 64],
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize, JsonSchema, Serialize)]
|
||||
pub struct Challenge {
|
||||
timestamp: DateTime<chrono::Utc>,
|
||||
|
||||
#[serde_as(as = "serde_with::base64::Base64")]
|
||||
#[schemars(with = "String")]
|
||||
nonce: [u8; 32],
|
||||
|
||||
#[serde_as(as = "serde_with::base64::Base64")]
|
||||
#[schemars(with = "String")]
|
||||
server_dh_pub: [u8; 32],
|
||||
|
||||
params: ArgonParams,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize, JsonSchema, Serialize)]
|
||||
struct ArgonParams {
|
||||
iterations: u32,
|
||||
memory_kb: u32,
|
||||
threads: u32,
|
||||
}
|
||||
|
||||
pub struct NodeController {
|
||||
_pg_pool: PgPool,
|
||||
signing_key: RwLock<Option<Arc<SecretKey>>>,
|
||||
}
|
||||
|
||||
type SecretKey = [u8; 32];
|
||||
type Nonce256 = [u8; 32];
|
||||
|
||||
trait SecretKeyExt {
|
||||
fn derive_dh_secret(&self, nonce: &Nonce256) -> StaticSecret;
|
||||
}
|
||||
|
||||
impl SecretKeyExt for SecretKey {
|
||||
fn derive_dh_secret(&self, nonce: &Nonce256) -> StaticSecret {
|
||||
let hk = Hkdf::<Sha256>::new(Some(nonce), self);
|
||||
let mut out = [0u8; 32];
|
||||
hk.expand(b"x25519-secret", &mut out).unwrap();
|
||||
StaticSecret::from(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistrationInfo {
|
||||
pub fn from(server_secret: &SecretKey, client_dh_public_key: &PublicKey) -> RegistrationInfo {
|
||||
let mut rng = rand::thread_rng();
|
||||
let nonce = rng.gen();
|
||||
|
||||
let server_dh_secret = server_secret.derive_dh_secret(&nonce);
|
||||
let _shared_secret = server_dh_secret.diffie_hellman(client_dh_public_key);
|
||||
let server_dh_public_key = PublicKey::from(&server_dh_secret);
|
||||
|
||||
let timestamp = chrono::Utc::now();
|
||||
|
||||
let params = ArgonParams {
|
||||
iterations: 10,
|
||||
memory_kb: 220 * 1024, // 220 MiB
|
||||
threads: 4,
|
||||
};
|
||||
|
||||
let challenge = Challenge {
|
||||
timestamp,
|
||||
nonce,
|
||||
server_dh_pub: server_dh_public_key.to_bytes(),
|
||||
params,
|
||||
};
|
||||
|
||||
let challenge_json = serde_json::to_vec(&challenge).unwrap();
|
||||
|
||||
let mut sk = ed25519_dalek::SigningKey::from_bytes(server_secret);
|
||||
let sig = sk.sign(challenge_json.as_slice());
|
||||
|
||||
let info = RegistrationInfo {
|
||||
node_id: Uuid::new_v4(),
|
||||
challenge,
|
||||
challenge_signature: sig.to_bytes(),
|
||||
};
|
||||
|
||||
tracing::debug!("Info: {:?}", info);
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeController {
|
||||
pub fn new(pg_pool: PgPool) -> NodeController {
|
||||
NodeController {
|
||||
_pg_pool: pg_pool,
|
||||
signing_key: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_registration(&self, client_dh_public_key: &PublicKey) -> RegistrationInfo {
|
||||
let server_secret = self.master_key();
|
||||
RegistrationInfo::from(&server_secret, client_dh_public_key)
|
||||
}
|
||||
|
||||
fn master_key(&self) -> Arc<SecretKey> {
|
||||
self.signing_key
|
||||
.write()
|
||||
.unwrap()
|
||||
.get_or_insert_with(|| {
|
||||
tracing::debug!("Generating key");
|
||||
Arc::new(rand::rngs::OsRng.gen())
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_registration_info() {
|
||||
let client_key: SecretKey = rand::rngs::OsRng.gen();
|
||||
let server_key = rand::rngs::OsRng.gen();
|
||||
let client_public_key = PublicKey::from(client_key);
|
||||
|
||||
let info = RegistrationInfo::from(&server_key, &client_public_key);
|
||||
let info2 = RegistrationInfo::from(&server_key, &client_public_key);
|
||||
|
||||
// Randomness
|
||||
assert_ne!(info.node_id, info2.node_id, "Node ID should be unique");
|
||||
assert_ne!(
|
||||
info.challenge.nonce, info2.challenge.nonce,
|
||||
"Nonce should be unique"
|
||||
);
|
||||
assert_ne!(
|
||||
info.challenge.server_dh_pub, info2.challenge.server_dh_pub,
|
||||
"Server public key derived from nonce should be unique"
|
||||
);
|
||||
|
||||
// Timestamp
|
||||
let now = chrono::Utc::now();
|
||||
assert!(
|
||||
info.challenge.timestamp <= now,
|
||||
"Timestamp is in the future"
|
||||
);
|
||||
assert!(
|
||||
info.challenge.timestamp >= now - chrono::Duration::milliseconds(5),
|
||||
"Timestamp is too old"
|
||||
);
|
||||
|
||||
// Public key
|
||||
let server_dh_secret = server_key.derive_dh_secret(&info.challenge.nonce);
|
||||
let server_dh_public_key = PublicKey::from(&server_dh_secret).to_bytes();
|
||||
assert_eq!(
|
||||
info.challenge.server_dh_pub, server_dh_public_key,
|
||||
"Server public key should be derived from nonce"
|
||||
);
|
||||
}
|
||||
}
|
5
controller/src/node/mod.rs
Normal file
5
controller/src/node/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod api;
|
||||
mod controller;
|
||||
|
||||
pub use self::api::register_api;
|
||||
pub(crate) use self::controller::NodeController;
|
Loading…
Add table
Add a link
Reference in a new issue