allow using explicit admin address for issuing freepasses

This commit is contained in:
Jędrzej Stuczyński
2024-05-14 16:58:11 +01:00
parent 45dd6f2632
commit 07cc47a0ff
4 changed files with 130 additions and 27 deletions
+2 -2
View File
@@ -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"
+42 -22
View File
@@ -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,
});
}
+12 -2
View File
@@ -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")]
+74 -1
View File
@@ -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,