WIP: Registration #10

Draft
dln wants to merge 1 commit from dln/push-zlkzyztrtnrs into main
12 changed files with 511 additions and 4 deletions

185
Cargo.lock generated
View file

@ -307,8 +307,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
@ -467,6 +469,69 @@ dependencies = [
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"serde",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "debug-ignore"
version = "1.0.5"
@ -491,6 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@ -614,6 +680,31 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand_core",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "either"
version = "1.13.0"
@ -676,6 +767,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "flate2"
version = "1.0.35"
@ -1254,6 +1351,12 @@ dependencies = [
"syn",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
@ -1283,6 +1386,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
@ -1822,6 +1926,7 @@ name = "patagia-agent"
version = "0.2.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"futures",
"instrumentation",
@ -1839,12 +1944,21 @@ name = "patagia-controller"
version = "0.2.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"dropshot",
"ed25519-dalek",
"futures",
"hex",
"hkdf",
"http",
"instrumentation",
"rand",
"schemars",
"serde",
"serde_json",
"serde_with",
"sha2",
"slog",
"slog-async",
"sqlx",
@ -1853,6 +1967,7 @@ dependencies = [
"tracing",
"tracing-slog",
"uuid",
"x25519-dalek",
]
[[package]]
@ -2305,6 +2420,15 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.43"
@ -2415,6 +2539,7 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"bytes",
"chrono",
"dyn-clone",
"schemars_derive",
@ -2519,9 +2644,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
"memchr",
@ -2572,6 +2697,36 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.7.0",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
@ -4018,6 +4173,18 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "x25519-dalek"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core",
"serde",
"zeroize",
]
[[package]]
name = "xtask"
version = "0.2.0"
@ -4099,6 +4266,20 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerovec"

View file

@ -24,6 +24,7 @@ name = "patagia-run"
[workspace.dependencies]
anyhow = "1.0.95"
chrono = "0.4.39"
clap = { version = "4.5.26", features = [
"derive",
"deprecated",
@ -32,14 +33,22 @@ clap = { version = "4.5.26", features = [
"string",
] }
dropshot = "0.15.1"
ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] }
futures = "0.3"
hex = "0.4.3"
hkdf = "0.12.4"
http = "1.2.0"
once_cell = "1.20.2"
progenitor = "0.9"
rand = "0.8.5"
reqwest = { version = "0.12.12", features = ["json", "stream", "rustls-tls"] }
schemars = "0.8.21"
schemars = { version = "0.8.21", features = ["bytes", "chrono", "derive"] }
semver = "1.0.24"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
serde_with = { version = "3.12.0", features = ["base64", "hex", "macros", "std"] }
# serde_with_macros = { version = "3.12.0", features = ["schemars_0_8"] }
sha2 = "0.10.8"
slog = "2.7.0"
slog-async = "2.8.0"
tokio = { version = "1.43.0", features = ["full"] }
@ -48,3 +57,4 @@ tracing-core = "0.1.33"
tracing-chrome = "0.7.2"
tracing-slog = { git = "https://github.com/oxidecomputer/tracing-slog", default-features = false }
uuid = { version = "1", features = [ "serde", "v4" ] }
x25519-dalek = { version = "2.0.1", features = ["serde", "static_secrets"] }

View file

@ -6,6 +6,7 @@ version.workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
futures.workspace = true
instrumentation = { path = "../instrumentation" }

View file

@ -5,6 +5,43 @@
"version": "1.0.0"
},
"paths": {
"/nodes/register": {
"get": {
"tags": [
"node"
],
"summary": "Get registration info",
"operationId": "get_registration_info",
"parameters": [
{
"in": "query",
"name": "key",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegistrationInfo"
}
}
}
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
}
}
},
"/users": {
"get": {
"tags": [
@ -141,6 +178,31 @@
"request_id"
]
},
"RegistrationInfo": {
"type": "object",
"properties": {
"node_id": {
"type": "string",
"format": "uuid"
},
"nonce": {
"type": "string"
},
"server_dh_pub": {
"type": "string"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
},
"required": [
"node_id",
"nonce",
"server_dh_pub",
"timestamp"
]
},
"User": {
"description": "User",
"type": "object",
@ -210,6 +272,9 @@
}
},
"tags": [
{
"name": "node"
},
{
"name": "user"
}

View file

@ -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.3", 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"]

View file

@ -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)

View file

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

View file

@ -1,5 +1,6 @@
pub mod api;
pub mod context;
mod node;
mod user;
mod version;

View 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))
}

View 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"
);
}
}

View file

@ -0,0 +1,5 @@
mod api;
mod controller;
pub use self::api::register_api;
pub(crate) use self::controller::NodeController;

View file

@ -146,6 +146,7 @@
rust-dev-toolchain
sqls
sqlx-cli
tpm2-tools
watchexec
]
++ commonArgs.buildInputs;