Uniform request authentication

This commit is contained in:
dynco-nym
2024-11-20 15:25:19 +01:00
parent 65f4c08050
commit 03f40a9ce2
8 changed files with 124 additions and 107 deletions
Generated
+2
View File
@@ -5010,6 +5010,8 @@ dependencies = [
name = "nym-common-models"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"nym-crypto",
"serde",
]
+3 -1
View File
@@ -11,5 +11,7 @@ rust-version.workspace = true
readme.workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }
anyhow = { workspace = true }
bincode = { workspace = true }
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"] }
serde = { workspace = true, features = ["derive"] }
+77 -5
View File
@@ -1,4 +1,4 @@
use nym_crypto::asymmetric::ed25519::{PublicKey, Signature};
use nym_crypto::asymmetric::ed25519::{PublicKey, Signature, SignatureError};
use serde::{Deserialize, Serialize};
pub mod get_testrun {
@@ -14,16 +14,88 @@ pub mod get_testrun {
pub payload: Payload,
pub signature: Signature,
}
impl SignedRequest for GetTestrunRequest {
type Payload = Payload;
fn public_key(&self) -> &PublicKey {
&self.payload.agent_public_key
}
fn signature(&self) -> &Signature {
&self.signature
}
fn payload(&self) -> &Self::Payload {
&self.payload
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TestrunAssignment {
pub testrun_id: i64,
pub assigned_at_utc: i64,
pub gateway_identity_key: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubmitResults {
pub message: String,
pub signature: Signature,
pub mod submit_results {
use super::*;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Payload {
pub probe_result: String,
pub agent_public_key: PublicKey,
pub assigned_at_utc: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubmitResults {
pub payload: Payload,
pub signature: Signature,
}
impl SignedRequest for SubmitResults {
type Payload = Payload;
fn public_key(&self) -> &PublicKey {
&self.payload.agent_public_key
}
fn signature(&self) -> &Signature {
&self.signature
}
fn payload(&self) -> &Self::Payload {
&self.payload
}
}
}
pub trait SignedRequest {
type Payload: serde::Serialize;
fn public_key(&self) -> &PublicKey;
fn signature(&self) -> &Signature;
fn payload(&self) -> &Self::Payload;
}
pub trait VerifiableRequest: SignedRequest {
type Error: From<bincode::Error> + From<SignatureError>;
fn verify_signature(&self) -> Result<(), Self::Error> {
bincode::serialize(self.payload())
.map_err(Self::Error::from)
.and_then(|serialized| {
self.public_key()
.verify(serialized, self.signature())
.map_err(Self::Error::from)
})
}
}
impl<T> VerifiableRequest for T
where
T: SignedRequest,
{
type Error = anyhow::Error;
}
+11 -8
View File
@@ -1,5 +1,5 @@
use anyhow::{bail, Context};
use nym_common_models::ns_api::{get_testrun, SubmitResults, TestrunAssignment};
use nym_common_models::ns_api::{get_testrun, submit_results, TestrunAssignment};
use nym_crypto::asymmetric::ed25519::{PrivateKey, Signature};
use std::fmt::Display;
use tracing::instrument;
@@ -27,7 +27,7 @@ pub(crate) async fn run_probe(
let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key));
ns_api_client
.submit_results(testrun.testrun_id, log)
.submit_results(testrun.testrun_id, log, testrun.assigned_at_utc)
.await?;
} else {
tracing::info!("No testruns available, exiting")
@@ -92,19 +92,22 @@ impl Client {
})
}
#[instrument(level = "debug", skip(self, probe_outcome))]
#[instrument(level = "debug", skip(self, probe_result))]
pub(crate) async fn submit_results(
&self,
testrun_id: i64,
probe_outcome: String,
probe_result: String,
assigned_at_utc: i64,
) -> anyhow::Result<()> {
let target_url = self.api_with_subpath(Some(testrun_id));
let signature = self.sign_message(&probe_outcome)?;
let submit_results = SubmitResults {
message: probe_outcome,
signature,
let payload = submit_results::Payload {
probe_result,
agent_public_key: self.auth_key.public_key(),
assigned_at_utc,
};
let signature = self.sign_message(&payload)?;
let submit_results = submit_results::SubmitResults { payload, signature };
let res = self
.client
-9
View File
@@ -2,7 +2,6 @@ use crate::{
http::{self, models::SummaryHistory},
monitor::NumericalCheckedCast,
};
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_node_requests::api::v1::node::models::NodeDescription;
use serde::{Deserialize, Serialize};
use strum_macros::{EnumString, FromRepr};
@@ -314,14 +313,6 @@ pub struct TestRunDto {
pub last_assigned_utc: Option<i64>,
}
impl TestRunDto {
pub(crate) fn assigned_agent_key(&self) -> Option<PublicKey> {
self.assigned_agent
.as_ref()
.and_then(|value| PublicKey::from_base58_string(value).ok())
}
}
#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)]
#[repr(u8)]
pub(crate) enum TestRunStatus {
+5 -30
View File
@@ -8,35 +8,6 @@ use chrono::Duration;
use nym_crypto::asymmetric::ed25519::PublicKey;
use sqlx::{pool::PoolConnection, Sqlite};
pub(crate) async fn get_in_progress_testrun_by_id(
conn: &mut PoolConnection<Sqlite>,
testrun_id: i64,
) -> anyhow::Result<TestRunDto> {
sqlx::query_as!(
TestRunDto,
r#"SELECT
id as "id!",
gateway_id as "gateway_id!",
status as "status!",
created_utc as "created_utc!",
ip_address as "ip_address!",
log as "log!",
assigned_agent,
last_assigned_utc
FROM testruns
WHERE
id = ?
AND
status = ?
ORDER BY created_utc"#,
testrun_id,
TestRunStatus::InProgress as i64,
)
.fetch_one(conn.as_mut())
.await
.map_err(|e| anyhow::anyhow!("Couldn't retrieve testrun {testrun_id}: {e}"))
}
pub(crate) async fn testrun_in_progress_assigned_to_agent(
conn: &mut PoolConnection<Sqlite>,
agent_key: &PublicKey,
@@ -109,12 +80,14 @@ pub(crate) async fn assign_oldest_testrun(
agent_key: PublicKey,
) -> anyhow::Result<Option<TestrunAssignment>> {
let agent_key = agent_key.to_base58_string();
let now = now_utc().timestamp();
// find & mark as "In progress" in the same transaction to avoid race conditions
let returning = sqlx::query!(
r#"UPDATE testruns
SET
status = ?,
assigned_agent = ?
assigned_agent = ?,
last_assigned_utc = ?
WHERE rowid =
(
SELECT rowid
@@ -129,6 +102,7 @@ pub(crate) async fn assign_oldest_testrun(
"#,
TestRunStatus::InProgress as i64,
agent_key,
now,
TestRunStatus::Queued as i64,
)
.fetch_optional(conn.as_mut())
@@ -151,6 +125,7 @@ pub(crate) async fn assign_oldest_testrun(
Ok(Some(TestrunAssignment {
testrun_id: testrun.id,
gateway_identity_key: gw_identity.gateway_identity_key,
assigned_at_utc: now,
}))
} else {
Ok(None)
+26 -48
View File
@@ -4,8 +4,7 @@ use axum::{
extract::{Path, State},
Router,
};
use nym_common_models::ns_api::{get_testrun, SubmitResults};
use nym_crypto::asymmetric::ed25519::{PublicKey, Signature};
use nym_common_models::ns_api::{get_testrun, submit_results, VerifiableRequest};
use reqwest::StatusCode;
use crate::db::models::TestRunStatus;
@@ -88,47 +87,41 @@ async fn request_testrun(
async fn submit_testrun(
Path(submitted_testrun_id): Path<i64>,
State(state): State<AppState>,
Json(probe_results): Json<SubmitResults>,
Json(submitted_result): Json<submit_results::SubmitResults>,
) -> HttpResult<StatusCode> {
authenticate(&submitted_result, &state)?;
let db = state.db_pool();
let mut conn = db
.acquire()
.await
.map_err(HttpError::internal_with_logging)?;
let submitted_testrun =
queries::testruns::get_in_progress_testrun_by_id(&mut conn, submitted_testrun_id)
.await
.map_err(|e| {
tracing::warn!("testrun_id {} not found: {}", submitted_testrun_id, e);
HttpError::not_found(submitted_testrun_id)
})?;
let agent_pubkey = submitted_testrun
.assigned_agent_key()
.ok_or_else(HttpError::unauthorized)?;
let submitter_pubkey = submitted_result.payload.agent_public_key;
let assigned_testrun =
queries::testruns::testrun_in_progress_assigned_to_agent(&mut conn, &agent_pubkey)
queries::testruns::testrun_in_progress_assigned_to_agent(&mut conn, &submitter_pubkey)
.await
.map_err(|err| {
tracing::warn!("No testruns in progress for agent {agent_pubkey}: {err}");
tracing::warn!("No testruns in progress for agent {submitter_pubkey}: {err}");
HttpError::invalid_input("Invalid testrun submitted")
})?;
if submitted_testrun_id != assigned_testrun.id {
tracing::warn!(
"Agent {} submitted testrun {} but {} was expected",
agent_pubkey,
submitter_pubkey,
submitted_testrun_id,
assigned_testrun.id
);
return Err(HttpError::invalid_input("Invalid testrun submitted"));
}
verify_message(
&agent_pubkey,
&probe_results.message,
&probe_results.signature,
)?;
if Some(submitted_result.payload.assigned_at_utc) != assigned_testrun.last_assigned_utc {
tracing::warn!(
"Submitted testrun timestamp mismatch: {} != {:?}, rejecting",
submitted_result.payload.assigned_at_utc,
assigned_testrun.last_assigned_utc
);
return Err(HttpError::invalid_input("Invalid testrun submitted"));
}
let gw_identity = db::queries::select_gateway_identity(&mut conn, assigned_testrun.gateway_id)
.await
@@ -140,10 +133,10 @@ async fn submit_testrun(
})?;
tracing::debug!(
"Agent {} submitted testrun {} for gateway {} ({} bytes)",
agent_pubkey,
submitter_pubkey,
submitted_testrun_id,
gw_identity,
&probe_results.message.len(),
&submitted_result.payload.probe_result.len(),
);
queries::testruns::update_testrun_status(
@@ -156,11 +149,11 @@ async fn submit_testrun(
queries::testruns::update_gateway_last_probe_log(
&mut conn,
assigned_testrun.gateway_id,
&probe_results.message,
&submitted_result.payload.probe_result,
)
.await
.map_err(HttpError::internal_with_logging)?;
let result = get_result_from_log(&probe_results.message);
let result = get_result_from_log(&submitted_result.payload.probe_result);
queries::testruns::update_gateway_last_probe_result(
&mut conn,
assigned_testrun.gateway_id,
@@ -183,35 +176,20 @@ async fn submit_testrun(
// TODO dz this should be middleware
#[tracing::instrument(level = "debug", skip_all)]
fn authenticate(request: &get_testrun::GetTestrunRequest, state: &AppState) -> HttpResult<()> {
if !state.is_registered(&request.payload.agent_public_key) {
fn authenticate(request: &impl VerifiableRequest, state: &AppState) -> HttpResult<()> {
if !state.is_registered(request.public_key()) {
tracing::warn!("Public key not registered with NS API, rejecting");
return Err(HttpError::unauthorized());
};
verify_message(
&request.payload.agent_public_key,
&request.payload,
&request.signature,
)
.inspect_err(|_| tracing::warn!("Signature verification failed, rejecting"))?;
request.verify_signature().map_err(|_| {
tracing::warn!("Signature verification failed, rejecting");
HttpError::unauthorized()
})?;
Ok(())
}
fn verify_message<T>(public_key: &PublicKey, payload: &T, signature: &Signature) -> HttpResult<()>
where
T: serde::Serialize,
{
bincode::serialize(payload)
.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 {
let re = regex::Regex::new(r"\n\{\s").unwrap();
let result: Vec<_> = re.splitn(log, 2).collect();
-6
View File
@@ -40,12 +40,6 @@ impl HttpError {
status: axum::http::StatusCode::SERVICE_UNAVAILABLE,
}
}
pub(crate) fn not_found(msg: impl Display) -> Self {
Self {
message: serde_json::json!({"message": msg.to_string()}).to_string(),
status: axum::http::StatusCode::NOT_FOUND,
}
}
}
impl axum::response::IntoResponse for HttpError {