/request better authentication

- moved agent API calls to Client struct
This commit is contained in:
dynco-nym
2024-11-19 23:41:13 +01:00
parent f5ab647a7a
commit e4df6416f5
9 changed files with 165 additions and 107 deletions
Generated
+4
View File
@@ -6053,12 +6053,15 @@ name = "nym-node-status-agent"
version = "0.1.6"
dependencies = [
"anyhow",
"bincode",
"chrono",
"clap 4.5.20",
"nym-bin-common",
"nym-common-models",
"nym-crypto",
"rand",
"reqwest 0.12.4",
"serde",
"serde_json",
"tempfile",
"tokio",
@@ -6073,6 +6076,7 @@ version = "0.1.6"
dependencies = [
"anyhow",
"axum 0.7.7",
"bincode",
"chrono",
"clap 4.5.20",
"cosmwasm-std",
+16 -1
View File
@@ -1,6 +1,21 @@
use nym_crypto::asymmetric::ed25519::Signature;
use nym_crypto::asymmetric::ed25519::{PublicKey, Signature};
use serde::{Deserialize, Serialize};
pub mod get_testrun {
use super::*;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Payload {
pub agent_public_key: PublicKey,
pub timestamp: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GetTestrunRequest {
pub payload: Payload,
pub signature: Signature,
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TestrunAssignment {
pub testrun_id: i64,
+3
View File
@@ -16,6 +16,8 @@ readme.workspace = true
[dependencies]
anyhow = { workspace = true}
bincode = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
nym-bin-common = { path = "../common/bin-common", features = ["models"]}
nym-common-models = { path = "../common/models" }
@@ -26,6 +28,7 @@ tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
+3 -3
View File
@@ -1,9 +1,9 @@
#!/bin/bash
set -eu
environment="qa"
export ENVIRONMENT=${ENVIRONMENT:-"sandbox"}
probe_git_ref="0dd5dacdda92b1ddd51cd30a3399515e45613371"
probe_git_ref="nym-vpn-core-v1.0.0-rc.6"
crate_root=$(dirname $(realpath "$0"))
monorepo_root=$(dirname "${crate_root}")
@@ -13,7 +13,7 @@ echo "gateway_probe_src=$gateway_probe_src"
echo "crate_root=$crate_root"
set -a
source "${monorepo_root}/envs/${environment}.env"
source "${monorepo_root}/envs/${ENVIRONMENT}.env"
set +a
export RUST_LOG="debug"
+95 -76
View File
@@ -1,33 +1,34 @@
use anyhow::{bail, Context};
use nym_common_models::ns_api::{SubmitResults, TestrunAssignment};
use nym_crypto::asymmetric::ed25519::{PrivateKey, PublicKey};
use nym_common_models::ns_api::{get_testrun, SubmitResults, TestrunAssignment};
use nym_crypto::asymmetric::ed25519::{PrivateKey, Signature};
use std::fmt::Display;
use tracing::instrument;
use crate::cli::GwProbe;
const URL_BASE: &str = "internal/testruns";
const INTERNAL_TESTRUNS: &str = "internal/testruns";
pub(crate) async fn run_probe(
server_address: &str,
server_ip: &str,
server_port: u16,
ns_api_auth_key: &str,
probe_path: &str,
) -> anyhow::Result<()> {
let server_address = format!("{}:{}", server_address, server_port);
test_ns_api_conn(&server_address).await?;
let auth_key = PrivateKey::from_base58_string(ns_api_auth_key)
.context("Couldn't parse auth key, exiting")?;
let ns_api_client = Client::new(server_ip, server_port, auth_key);
let probe = GwProbe::new(probe_path.to_string());
let version = probe.version().await;
tracing::info!("Probe version:\n{}", version);
if let Some(testrun) = request_testrun(auth_key.public_key(), &server_address).await? {
if let Some(testrun) = ns_api_client.request_testrun().await? {
let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key));
submit_results(auth_key, &server_address, testrun.testrun_id, log).await?;
ns_api_client
.submit_results(testrun.testrun_id, log)
.await?;
} else {
tracing::info!("No testruns available, exiting")
}
@@ -35,84 +36,102 @@ pub(crate) async fn run_probe(
Ok(())
}
async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> {
reqwest::get(server_addr)
.await
.map(|res| {
tracing::info!(
"Testing connection to NS API at {server_addr}: {}",
res.status()
);
})
.map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err))
struct Client {
server_address: String,
client: reqwest::Client,
auth_key: PrivateKey,
}
#[instrument(level = "debug", skip_all)]
pub(crate) async fn request_testrun(
auth_key: PublicKey,
server_addr: &str,
) -> anyhow::Result<Option<TestrunAssignment>> {
let target_url = format!("{}/{}", server_addr, URL_BASE);
let client = reqwest::Client::new();
let res = client
.get(target_url)
.body(auth_key.to_base58_string())
.send()
.await?;
let status = res.status();
let response_text = res.text().await?;
impl Client {
pub fn new(server_ip: &str, server_port: u16, auth_key: PrivateKey) -> Self {
let server_address = format!("{}:{}", server_ip, server_port);
let client = reqwest::Client::new();
if status.is_client_error() {
bail!("{}: {}", status, response_text);
} else if status.is_server_error() {
if matches!(status, reqwest::StatusCode::SERVICE_UNAVAILABLE)
&& response_text.contains("No testruns available")
{
return Ok(None);
} else {
bail!("{}: {}", status, response_text);
Self {
server_address,
client,
auth_key,
}
}
serde_json::from_str(&response_text)
.map(|testrun| {
tracing::info!("Received testrun assignment: {:?}", testrun);
testrun
})
.map_err(|err| {
tracing::error!("err");
err.into()
})
}
#[instrument(level = "debug", skip_all)]
pub(crate) async fn request_testrun(&self) -> anyhow::Result<Option<TestrunAssignment>> {
let target_url = self.api_with_subpath(None::<String>);
#[instrument(level = "debug", skip(auth_key, probe_outcome))]
pub(crate) async fn submit_results(
auth_key: PrivateKey,
server_addr: &str,
testrun_id: i64,
probe_outcome: String,
) -> anyhow::Result<()> {
let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id);
let payload = get_testrun::Payload {
agent_public_key: self.auth_key.public_key(),
timestamp: chrono::offset::Utc::now().timestamp(),
};
let signature = self.sign_message(&payload)?;
let request = get_testrun::GetTestrunRequest { payload, signature };
let results = sign_message(auth_key, probe_outcome);
let res = self.client.get(target_url).json(&request).send().await?;
let status = res.status();
let response_text = res.text().await?;
let client = reqwest::Client::new();
let res = client
.post(target_url)
.json(&results)
.send()
.await
.and_then(|response| response.error_for_status())?;
if status.is_client_error() {
bail!("{}: {}", status, response_text);
} else if status.is_server_error() {
if matches!(status, reqwest::StatusCode::SERVICE_UNAVAILABLE)
&& response_text.contains("No testruns available")
{
return Ok(None);
} else {
bail!("{}: {}", status, response_text);
}
}
tracing::debug!("Submitted results: {})", res.status());
Ok(())
}
serde_json::from_str(&response_text)
.map(|testrun| {
tracing::info!("Received testrun assignment: {:?}", testrun);
testrun
})
.map_err(|err| {
tracing::error!("err");
err.into()
})
}
fn sign_message(key: PrivateKey, probe_outcome: String) -> SubmitResults {
let signature = key.sign(&probe_outcome);
#[instrument(level = "debug", skip(self, probe_outcome))]
pub(crate) async fn submit_results(
&self,
testrun_id: i64,
probe_outcome: String,
) -> anyhow::Result<()> {
let target_url = self.api_with_subpath(Some(testrun_id));
SubmitResults {
message: probe_outcome,
signature,
let signature = self.sign_message(&probe_outcome)?;
let submit_results = SubmitResults {
message: probe_outcome,
signature,
};
let res = self
.client
.post(target_url)
.json(&submit_results)
.send()
.await
.and_then(|response| response.error_for_status())?;
tracing::debug!("Submitted results: {})", res.status());
Ok(())
}
fn sign_message<T>(&self, message: &T) -> anyhow::Result<Signature>
where
T: serde::Serialize,
{
let serialized = bincode::serialize(message)?;
let signed = self.auth_key.sign(&serialized);
Ok(signed)
}
fn api_with_subpath(&self, subpath: Option<impl Display>) -> String {
if let Some(subpath) = subpath {
format!("{}/{}/{}", self.server_address, INTERNAL_TESTRUNS, subpath)
} else {
format!("{}/{}", self.server_address, INTERNAL_TESTRUNS)
}
}
}
+1
View File
@@ -15,6 +15,7 @@ rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
axum = { workspace = true, features = ["tokio", "macros"] }
bincode = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive", "env", "string"] }
cosmwasm-std = { workspace = true }
@@ -19,12 +19,12 @@ Eujj4GmvwQBgHZaNSyqUbjMFSsnXWPSjEYUPgAsKmx1A,
5ZnfSGxW6EKcFxB8jftb9V3f897VpwpZtf7kCPYzB595,
H9kuRd8BGjEUD8Grh5U9YUPN5ZaQmSYz8U44R72AffKM"
export ENVIRONMENT="qa.env"
export ENVIRONMENT=${ENVIRONMENT:-"sandbox"}
function run_bare() {
# export necessary env vars
set -a
source ../envs/$ENVIRONMENT
source ../envs/${ENVIRONMENT}.env
set +a
export RUST_LOG=debug
@@ -33,8 +33,10 @@ pub(crate) async fn get_in_progress_testrun_by_id(
TestRunStatus::InProgress as i64,
)
.fetch_one(conn.as_mut())
.await
.context(format!("Couldn't retrieve testrun {testrun_id}"))
.await.map_err(|e| {
anyhow::anyhow!("Couldn't retrieve testrun {testrun_id}: {e}")
})
}
pub(crate) async fn get_testruns_assigned_to_agent(
+37 -23
View File
@@ -4,8 +4,8 @@ use axum::{
extract::{Path, State},
Router,
};
use nym_common_models::ns_api::SubmitResults;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_common_models::ns_api::{get_testrun, SubmitResults};
use nym_crypto::asymmetric::ed25519::{PublicKey, Signature};
use reqwest::StatusCode;
use crate::db::models::TestRunStatus;
@@ -32,10 +32,12 @@ pub(crate) fn routes() -> Router<AppState> {
#[tracing::instrument(level = "debug", skip_all)]
async fn request_testrun(
State(state): State<AppState>,
body: String,
Json(request): Json<get_testrun::GetTestrunRequest>,
) -> HttpResult<Json<TestrunAssignment>> {
// TODO dz log agent's network probe version
let agent_pubkey = authenticate_agent(&body, &state)?;
authenticate(&request, &state)?;
let agent_pubkey = request.payload.agent_public_key;
tracing::debug!("Agent {} requested testrun", agent_pubkey);
@@ -104,12 +106,11 @@ async fn submit_testrun(
return Err(HttpError::invalid_input("Invalid testrun submitted"));
}
agent_pubkey
.verify(&probe_results.message, &probe_results.signature)
.map_err(|_| {
tracing::warn!("Message verification failed, rejecting");
HttpError::unauthorized()
})?;
verify_message(
&agent_pubkey,
&probe_results.message,
&probe_results.signature,
)?;
let gw_identity = db::queries::select_gateway_identity(&mut conn, assigned_testrun.gateway_id)
.await
@@ -162,21 +163,34 @@ async fn submit_testrun(
Ok(StatusCode::CREATED)
}
fn authenticate_agent(base58_pubkey: &str, state: &AppState) -> HttpResult<PublicKey> {
let agent_pubkey = PublicKey::from_base58_string(base58_pubkey).map_err(|_| {
if base58_pubkey.is_empty() {
tracing::warn!("Auth key missing from request body, rejecting");
} else {
tracing::warn!("Failed to deserialize key from request body, rejecting");
}
HttpError::unauthorized()
})?;
if !state.is_registered(&agent_pubkey) {
tracing::warn!("Public key {} not registered, rejecting", agent_pubkey);
// TODO dz this should be middleware
fn authenticate(request: &get_testrun::GetTestrunRequest, state: &AppState) -> HttpResult<()> {
if !state.is_registered(&request.payload.agent_public_key) {
tracing::warn!("Public key not registered with NS API, rejecting");
return Err(HttpError::unauthorized());
}
};
Ok(agent_pubkey)
verify_message(
&request.payload.agent_public_key,
&request.payload,
&request.signature,
)
.inspect_err(|_| tracing::warn!("Signature verification failed, rejecting"))?;
Ok(())
}
fn verify_message<T>(public_key: &PublicKey, message: &T, signature: &Signature) -> HttpResult<()>
where
T: serde::Serialize,
{
bincode::serialize(message)
.map_err(HttpError::invalid_input)
.and_then(|serialized| {
public_key
.verify(serialized, signature)
.map_err(|_| HttpError::unauthorized())
})
}
fn get_result_from_log(log: &str) -> String {