allow using explicit admin address for issuing freepasses
This commit is contained in:
+2
-2
@@ -11,7 +11,7 @@ authors = [
|
||||
"Drazen Urch <durch@users.noreply.github.com>",
|
||||
]
|
||||
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"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AccountId>,
|
||||
bandwidth_contract_admin: Option<AccountId>,
|
||||
},
|
||||
|
||||
#[error("failed to verify signature on the provided free pass request")]
|
||||
|
||||
@@ -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<dyn APICommunicationChannel + Send + Sync>,
|
||||
pub(crate) storage: NymApiStorage,
|
||||
pub(crate) freepass_nonce: Arc<RwLock<[u8; 16]>>,
|
||||
pub(crate) authorised_freepass_requester: Arc<RwLock<AuthorisedFreepassRequester>>,
|
||||
}
|
||||
|
||||
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<AccountId>,
|
||||
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<AccountId> {
|
||||
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<AccountId> {
|
||||
{
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user