From 07cc47a0ff759b0417dcae29d7d523afcd2e0716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 14 May 2024 16:58:11 +0100 Subject: [PATCH] allow using explicit admin address for issuing freepasses --- nym-api/Cargo.toml | 4 +- nym-api/src/coconut/api_routes/mod.rs | 64 +++++++++++++++-------- nym-api/src/coconut/error.rs | 14 ++++- nym-api/src/coconut/state.rs | 75 ++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 27 deletions(-) diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4ab459165c..5f692d92ab 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -11,7 +11,7 @@ authors = [ "Drazen Urch ", ] edition = "2021" -rust-version = "1.70.0" +rust-version = "1.76.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -48,7 +48,7 @@ tokio = { version = "1.24.1", features = [ tokio-stream = "0.1.11" url = { workspace = true } -ts-rs = { workspace = true, optional = true} +ts-rs = { workspace = true, optional = true } anyhow = { workspace = true } getset = "0.1.1" diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs index 31bfd4bf6a..d56202f067 100644 --- a/nym-api/src/coconut/api_routes/mod.rs +++ b/nym-api/src/coconut/api_routes/mod.rs @@ -1,12 +1,15 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::api_routes::helpers::build_credentials_response; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::helpers::{accepted_vote_err, blind_sign}; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; +use std::ops::Deref; + use k256::ecdsa::signature::Verifier; +use rand::rngs::OsRng; +use rand::RngCore; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use time::OffsetDateTime; + use nym_api_requests::coconut::models::{ CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest, IssuedCredentialResponse, IssuedCredentialsResponse, @@ -23,12 +26,12 @@ use nym_credentials::coconut::bandwidth::{ bandwidth_credential_params, CredentialType, IssuanceBandwidthCredential, }; use nym_validator_client::nyxd::Coin; -use rand::rngs::OsRng; -use rand::RngCore; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use std::ops::Deref; -use time::OffsetDateTime; + +use crate::coconut::api_routes::helpers::build_credentials_response; +use crate::coconut::error::{CoconutError, Result}; +use crate::coconut::helpers::{accepted_vote_err, blind_sign}; +use crate::coconut::state::State; +use crate::coconut::storage::CoconutStorageExt; mod helpers; @@ -98,17 +101,30 @@ pub async fn post_free_pass( validate_freepass_public_attributes(&freepass_request_body)?; - // grab the admin of the bandwidth contract - let Some(authorised_admin) = state.get_bandwidth_contract_admin().await? else { - error!("our bandwidth contract does not have an admin set! We won't be able to migrate the contract! We should redeploy it ASAP"); - return Err(CoconutError::MissingBandwidthContractAdmin); + // check for explicit admin + let explicit_admin = state.get_authorised_freepass_requester().await; + + // otherwise fallback to bandwidth contract admin + let bandwidth_contract_admin = state + .get_bandwidth_contract_admin() + .await + .cloned() + .inspect_err(|_| error!("our bandwidth contract does not have an admin set! We won't be able to migrate the contract! We should redeploy it ASAP")) + .ok() + .flatten(); + + // extract account prefix + let prefix = match (&explicit_admin, &bandwidth_contract_admin) { + (None, None) => { + error!("neither explicit admin nor bandwidth contract admin has been set!"); + return Err(CoconutError::MissingBandwidthContractAddress); + } + (Some(addr), _) => addr.prefix(), + (None, Some(addr)) => addr.prefix(), }; // derive the address out of the provided pubkey - let requester = match freepass_request_body - .cosmos_pubkey - .account_id(authorised_admin.prefix()) - { + let requester = match freepass_request_body.cosmos_pubkey.account_id(prefix) { Ok(address) => address, Err(err) => { return Err(CoconutError::AdminAccountDerivationFailure { @@ -116,12 +132,16 @@ pub async fn post_free_pass( }) } }; - debug!("derived the following address out of the provided public key: {requester}. Going to check it against the authorised admin ({authorised_admin})"); + debug!("derived the following address out of the provided public key: {requester}. Going to check it against the authorised admin ({explicit_admin:?}) and fallback to bandwidth contract admin: {bandwidth_contract_admin:?}"); - if &requester != authorised_admin { + // check if request matches any address + if Some(&requester) != explicit_admin.as_ref() + && Some(&requester) != bandwidth_contract_admin.as_ref() + { return Err(CoconutError::UnauthorisedFreePassAccount { requester, - authorised_admin: authorised_admin.clone(), + explicit_admin, + bandwidth_contract_admin, }); } diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index b4587191bb..3fa75c443f 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -37,10 +37,20 @@ pub enum CoconutError { #[error("failed to derive the admin account from the provided public key: {formatted_source}")] AdminAccountDerivationFailure { formatted_source: String }, - #[error("the requester of the free pass ({requester}) is not authorised. the only allowed account is {authorised_admin}.")] + #[error("failed to query for the authorised freepass requester address: {source}")] + FreepassAuthorisedFreepassRequesterQueryFailure { + #[from] + source: reqwest::Error, + }, + + #[error("the provided authorised freepass requester address ({address}) is not a valid cosmos address")] + MalformedAuthorisedFreepassRequesterAddress { address: String }, + + #[error("the requester of the free pass ({requester}) is not authorised. the only allowed account is {explicit_admin:?} or {bandwidth_contract_admin:?}.")] UnauthorisedFreePassAccount { requester: AccountId, - authorised_admin: AccountId, + explicit_admin: Option, + bandwidth_contract_admin: Option, }, #[error("failed to verify signature on the provided free pass request")] diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs index 3e04110888..ac7c3632f8 100644 --- a/nym-api/src/coconut/state.rs +++ b/nym-api/src/coconut/state.rs @@ -4,7 +4,7 @@ use crate::coconut::client::Client as LocalClient; use crate::coconut::comm::APICommunicationChannel; use crate::coconut::deposit::validate_deposit_tx; -use crate::coconut::error::Result; +use crate::coconut::error::{CoconutError, Result}; use crate::coconut::keys::KeyPair; use crate::coconut::storage::CoconutStorageExt; use crate::support::storage::NymApiStorage; @@ -17,6 +17,7 @@ use nym_validator_client::nyxd::{AccountId, Hash, TxResponse}; use rand::rngs::OsRng; use rand::RngCore; use std::sync::Arc; +use time::{Duration, OffsetDateTime}; use tokio::sync::{OnceCell, RwLock}; pub use nym_credentials::coconut::bandwidth::bandwidth_credential_params; @@ -30,6 +31,25 @@ pub struct State { pub(crate) comm_channel: Arc, pub(crate) storage: NymApiStorage, pub(crate) freepass_nonce: Arc>, + pub(crate) authorised_freepass_requester: Arc>, +} + +const FREEPASS_REQUESTER_TTL: Duration = Duration::hours(1); +const AUTHORISED_FREEPASS_REQUESTER_ENDPOINT: &str = + "https://nymtech.net/.wellknown/authorised-freepass-requester.txt"; + +pub struct AuthorisedFreepassRequester { + address: Option, + refreshed_at: OffsetDateTime, +} + +impl Default for AuthorisedFreepassRequester { + fn default() -> Self { + AuthorisedFreepassRequester { + address: None, + refreshed_at: OffsetDateTime::UNIX_EPOCH, + } + } } impl State { @@ -60,6 +80,7 @@ impl State { comm_channel, storage, freepass_nonce: Arc::new(RwLock::new(nonce)), + authorised_freepass_requester: Arc::new(Default::default()), } } @@ -83,6 +104,58 @@ impl State { .await } + async fn try_get_authorised_freepass_requester(&self) -> Result { + let address = reqwest::Client::builder() + .user_agent(format!( + "nym-api / {} identity: {}", + env!("CARGO_PKG_VERSION"), + self.identity_keypair.public_key().to_base58_string() + )) + .build()? + .get(AUTHORISED_FREEPASS_REQUESTER_ENDPOINT) + .send() + .await? + .text() + .await?; + let trimmed = address.trim(); + + address.parse().map_err( + |_| CoconutError::MalformedAuthorisedFreepassRequesterAddress { + address: trimmed.to_string(), + }, + ) + } + + pub async fn get_authorised_freepass_requester(&self) -> Option { + { + let cached = self.authorised_freepass_requester.read().await; + + // the entry hasn't expired + if cached.refreshed_at + FREEPASS_REQUESTER_TTL >= OffsetDateTime::now_utc() { + if let Some(cached_address) = cached.address.as_ref() { + return Some(cached_address.clone()); + } + } + } + + // refresh cache + let mut cache = self.authorised_freepass_requester.write().await; + + // whatever happens, update refresh time + cache.refreshed_at = OffsetDateTime::now_utc(); + + let refreshed = match self.try_get_authorised_freepass_requester().await { + Ok(upstream) => upstream, + Err(err) => { + warn!("failed to obtain authorised freepass requester address: {err}"); + return None; + } + }; + + cache.address = Some(refreshed.clone()); + Some(refreshed) + } + pub async fn validate_request( &self, request: &BlindSignRequestBody,