Skip to content

Commit 76b79dd

Browse files
authored
Credential check (#302)
* Credential check * Loop over credentials * Use bandwidth controller instead * Restructure credentials module * Seperate error mod * Error type reorder * reorder * clippy * Task handling when exiting early * Log tweak * fix * log * clippy * disarm * formatting
1 parent ef128cd commit 76b79dd

File tree

14 files changed

+378
-126
lines changed

14 files changed

+378
-126
lines changed

Cargo.lock

Lines changed: 69 additions & 66 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ serde_json = "1.0.91"
6161
signature = "1"
6262
tap = "1.0.1"
6363
thiserror = "1.0.38"
64+
time = "0.3.36"
6465
tokio = { version = "1.8" }
6566
tokio-util = { version = "0.7.10", features = ["codec"] }
6667
toml = "0.8.12"
@@ -74,17 +75,19 @@ uniffi = { version = "0.27.0", features = ["cli"] }
7475
url = "2.4"
7576
vergen = { version = "8.2.6", default-features = false }
7677

77-
nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
78-
nym-client-core = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
79-
nym-config = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
80-
nym-credential-storage = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
81-
nym-crypto = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
82-
nym-explorer-client = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
83-
nym-id = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
84-
nym-ip-packet-requests = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
85-
nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
86-
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
87-
nym-task = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
88-
nym-topology = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
89-
nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
90-
nym-wireguard-types = { git = "https://github.com/nymtech/nym", rev = "fd238b1" }
78+
nym-bandwidth-controller = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
79+
nym-bin-common = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
80+
nym-client-core = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
81+
nym-config = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
82+
nym-credential-storage = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
83+
nym-credentials = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
84+
nym-crypto = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
85+
nym-explorer-client = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
86+
nym-id = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
87+
nym-ip-packet-requests = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
88+
nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
89+
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
90+
nym-task = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
91+
nym-topology = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
92+
nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }
93+
nym-wireguard-types = { git = "https://github.com/nymtech/nym", rev = "b8b66fa" }

nym-vpn-cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ async fn import_credential(args: commands::ImportCredentialArgs, data_path: Path
163163
ImportCredentialTypeEnum::Data(data) => data,
164164
};
165165
fs::create_dir_all(&data_path)?;
166-
nym_vpn_lib::credentials::import_credential(raw_credential, data_path).await
166+
Ok(nym_vpn_lib::credentials::import_credential(raw_credential, data_path).await?)
167167
}
168168

169169
fn mixnet_data_path() -> Option<PathBuf> {

nym-vpn-lib/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ serde_json.workspace = true
3030
signature.workspace = true
3131
tap.workspace = true
3232
thiserror.workspace = true
33+
time.workspace = true
3334
tokio = { workspace = true, features = ["process", "rt-multi-thread", "fs", "sync"] }
3435
tokio-util = { workspace = true, features = ["codec"] }
3536
tracing-subscriber = { workspace = true, features = ["env-filter"] }
@@ -50,10 +51,12 @@ talpid-tunnel = { git = "https://github.com/nymtech/nym-vpn-mullvad-libs", rev =
5051
talpid-types = { git = "https://github.com/nymtech/nym-vpn-mullvad-libs", rev = "a76d598a7a33e9668f16105b68f6a608b72c9a89" }
5152
talpid-wireguard = { git = "https://github.com/nymtech/nym-vpn-mullvad-libs", rev = "a76d598a7a33e9668f16105b68f6a608b72c9a89" }
5253

54+
nym-bandwidth-controller.workspace = true
5355
nym-bin-common.workspace = true
5456
nym-client-core.workspace = true
5557
nym-config.workspace = true
5658
nym-credential-storage.workspace = true
59+
nym-credentials.workspace = true
5760
nym-crypto.workspace = true
5861
nym-explorer-client.workspace = true
5962
nym-id.workspace = true

nym-vpn-lib/src/credentials.rs

Lines changed: 0 additions & 41 deletions
This file was deleted.

nym-vpn-lib/src/credentials/check.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::{fs, path::PathBuf};
2+
3+
use nym_bandwidth_controller::{BandwidthController, PreparedCredential, RetrievedCredential};
4+
use nym_credentials::{
5+
coconut::bandwidth::{
6+
bandwidth_credential_params, issued::BandwidthCredentialIssuedDataVariant,
7+
},
8+
obtain_aggregate_verification_key, IssuedBandwidthCredential,
9+
};
10+
11+
use tracing::{debug, info};
12+
13+
use super::{
14+
helpers::{get_coconut_api_clients, get_credentials_store, get_nyxd_client, CoconutClients},
15+
CredentialError,
16+
};
17+
18+
pub async fn check_raw_credential(raw_credential: Vec<u8>) -> Result<(), CredentialError> {
19+
let version = None;
20+
let credential = IssuedBandwidthCredential::try_unpack(&raw_credential, version)?;
21+
22+
// Check expiry
23+
match credential.variant_data() {
24+
BandwidthCredentialIssuedDataVariant::Voucher(_) => {
25+
debug!("credential is a bandwidth voucher");
26+
}
27+
BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => {
28+
debug!("credential is a free pass");
29+
if freepass_info.expired() {
30+
return Err(CredentialError::FreepassExpired {
31+
expiry_date: freepass_info.expiry_date().to_string(),
32+
});
33+
}
34+
}
35+
}
36+
37+
// TODO: verify?
38+
39+
Ok(())
40+
}
41+
42+
pub async fn check_credential_base58(credential: &str) -> Result<(), CredentialError> {
43+
let raw_credential = bs58::decode(credential)
44+
.into_vec()
45+
.map_err(|err| CredentialError::FailedToDecodeBase58Credential { source: err })?;
46+
check_raw_credential(raw_credential).await
47+
}
48+
49+
pub async fn check_credential_file(credential_file: PathBuf) -> Result<(), CredentialError> {
50+
let raw_credential = fs::read(credential_file)?;
51+
check_raw_credential(raw_credential).await
52+
}
53+
54+
pub async fn check_imported_credential(
55+
data_path: PathBuf,
56+
gateway_id: &str,
57+
) -> Result<(), CredentialError> {
58+
let client = get_nyxd_client()?;
59+
let (credentials_store, _location) = get_credentials_store(data_path.clone()).await?;
60+
let bandwidth_controller = BandwidthController::new(credentials_store, client);
61+
let usable_credential = bandwidth_controller
62+
.get_next_usable_credential(gateway_id)
63+
.await
64+
.map_err(|err| CredentialError::FailedToGetNextUsableCredential {
65+
location: data_path,
66+
reason: err.to_string(),
67+
})?;
68+
69+
let epoch_id = usable_credential.credential.epoch_id();
70+
let client = get_nyxd_client()?;
71+
let coconut_api_clients = match get_coconut_api_clients(client, epoch_id).await? {
72+
CoconutClients::Clients(clients) => clients,
73+
CoconutClients::NoContactAvailable => {
74+
info!("No Coconut API clients on this network, we are ok");
75+
return Ok(());
76+
}
77+
};
78+
79+
verify_credential(usable_credential, coconut_api_clients).await
80+
}
81+
82+
async fn verify_credential(
83+
usable_credential: RetrievedCredential,
84+
coconut_api_clients: Vec<nym_validator_client::coconut::CoconutApiClient>,
85+
) -> Result<(), CredentialError> {
86+
let verification_key = obtain_aggregate_verification_key(&coconut_api_clients)?;
87+
let spend_request = usable_credential
88+
.credential
89+
.prepare_for_spending(&verification_key)?;
90+
let prepared_credential = PreparedCredential {
91+
data: spend_request,
92+
epoch_id: usable_credential.credential.epoch_id(),
93+
credential_id: usable_credential.credential_id,
94+
};
95+
96+
if !prepared_credential.data.validate_type_attribute() {
97+
return Err(CredentialError::MissingBandwidthTypeAttribute);
98+
}
99+
100+
let params = bandwidth_credential_params();
101+
if prepared_credential.data.verify(params, &verification_key) {
102+
info!("Successfully verified credential");
103+
Ok(())
104+
} else {
105+
Err(CredentialError::FailedToVerifyCredential)
106+
}
107+
}

nym-vpn-lib/src/credentials/error.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#[derive(Debug, thiserror::Error)]
2+
pub enum CredentialError {
3+
#[error(transparent)]
4+
CoconutApiError(#[from] nym_validator_client::coconut::CoconutApiError),
5+
6+
#[error(transparent)]
7+
NymSdkError(#[from] nym_sdk::Error),
8+
9+
#[error(transparent)]
10+
NymCredentialsError(#[from] nym_credentials::Error),
11+
12+
#[error(transparent)]
13+
IoError(#[from] std::io::Error),
14+
15+
#[error("the free pass has already expired! The expiration was set to {expiry_date}")]
16+
FreepassExpired { expiry_date: String },
17+
18+
#[error("failed to import credential to: {location}: {source}")]
19+
FailedToImportCredential {
20+
location: std::path::PathBuf,
21+
source: nym_id::NymIdError,
22+
},
23+
24+
#[error("failed decode base58 credential: {source}")]
25+
FailedToDecodeBase58Credential { source: bs58::decode::Error },
26+
27+
#[error("failed to get next usable credential: {reason}")]
28+
FailedToGetNextUsableCredential {
29+
location: std::path::PathBuf,
30+
reason: String,
31+
},
32+
33+
#[error("missing bandwidth type attribute")]
34+
MissingBandwidthTypeAttribute,
35+
36+
#[error("failed to verify credential")]
37+
FailedToVerifyCredential,
38+
39+
#[error("failed to get nyxd client: {0}")]
40+
NyxdError(#[from] nym_validator_client::nyxd::error::NyxdError),
41+
42+
#[error("no nyxd endpoints found")]
43+
NoNyxdEndpointsFound,
44+
45+
#[error("failed to query contract")]
46+
FailedToQueryContract,
47+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::path::PathBuf;
2+
3+
use nym_credential_storage::persistent_storage::PersistentStorage;
4+
5+
use nym_sdk::{mixnet::StoragePaths, NymNetworkDetails};
6+
use nym_validator_client::{
7+
coconut::{all_coconut_api_clients, CoconutApiError},
8+
nyxd::{error::NyxdError, Config as NyxdClientConfig, NyxdClient},
9+
QueryHttpRpcNyxdClient,
10+
};
11+
use tracing::debug;
12+
13+
use super::CredentialError;
14+
15+
pub(super) async fn get_credentials_store(
16+
data_path: PathBuf,
17+
) -> Result<(PersistentStorage, PathBuf), CredentialError> {
18+
let storage_path = StoragePaths::new_from_dir(data_path)?;
19+
let credential_db_path = storage_path.credential_database_path;
20+
debug!("Credential store: {}", credential_db_path.display());
21+
Ok((
22+
nym_credential_storage::initialise_persistent_storage(credential_db_path.clone()).await,
23+
credential_db_path,
24+
))
25+
}
26+
27+
pub(super) fn get_nyxd_client() -> Result<QueryHttpRpcNyxdClient, CredentialError> {
28+
let network = NymNetworkDetails::new_from_env();
29+
let config = NyxdClientConfig::try_from_nym_network_details(&network)?;
30+
31+
// Safe to use pick the first one?
32+
let nyxd_url = network
33+
.endpoints
34+
.first()
35+
.ok_or(CredentialError::NoNyxdEndpointsFound)?
36+
.nyxd_url();
37+
38+
debug!("Connecting to nyx validator at: {}", nyxd_url);
39+
Ok(NyxdClient::connect(config, nyxd_url.as_str())?)
40+
}
41+
42+
pub(super) enum CoconutClients {
43+
Clients(Vec<nym_validator_client::coconut::CoconutApiClient>),
44+
NoContactAvailable,
45+
}
46+
47+
pub(super) async fn get_coconut_api_clients(
48+
nyxd_client: QueryHttpRpcNyxdClient,
49+
epoch_id: u64,
50+
) -> Result<CoconutClients, CredentialError> {
51+
match all_coconut_api_clients(&nyxd_client, epoch_id).await {
52+
Ok(clients) => Ok(CoconutClients::Clients(clients)),
53+
Err(CoconutApiError::ContractQueryFailure { source }) => match source {
54+
NyxdError::NoContractAddressAvailable(_) => Ok(CoconutClients::NoContactAvailable),
55+
_ => Err(CredentialError::FailedToQueryContract),
56+
},
57+
Err(err) => Err(err.into()),
58+
}
59+
}

nym-vpn-lib/src/credentials/import.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use std::{fs, path::PathBuf};
2+
use tracing::info;
3+
4+
use super::{helpers::get_credentials_store, CredentialError};
5+
6+
// Import binary credential data
7+
pub async fn import_credential(
8+
raw_credential: Vec<u8>,
9+
data_path: PathBuf,
10+
) -> Result<(), CredentialError> {
11+
info!("Importing credential");
12+
let (credentials_store, location) = get_credentials_store(data_path).await?;
13+
let version = None;
14+
nym_id::import_credential(credentials_store, raw_credential, version)
15+
.await
16+
.map_err(|err| CredentialError::FailedToImportCredential {
17+
location,
18+
source: err,
19+
})
20+
}
21+
22+
// Import credential data from a base58 string
23+
pub async fn import_credential_base58(
24+
credential: &str,
25+
data_path: PathBuf,
26+
) -> Result<(), CredentialError> {
27+
let raw_credential = bs58::decode(credential)
28+
.into_vec()
29+
.map_err(|err| CredentialError::FailedToDecodeBase58Credential { source: err })?;
30+
import_credential(raw_credential, data_path).await
31+
}
32+
33+
// Import credential data from a binary file
34+
pub async fn import_credential_file(
35+
credential_file: PathBuf,
36+
data_path: PathBuf,
37+
) -> Result<(), CredentialError> {
38+
let raw_credential = fs::read(credential_file)?;
39+
import_credential(raw_credential, data_path).await
40+
}

nym-vpn-lib/src/credentials/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
mod check;
2+
mod helpers;
3+
mod import;
4+
5+
mod error;
6+
7+
pub use check::{
8+
check_credential_base58, check_credential_file, check_imported_credential, check_raw_credential,
9+
};
10+
pub use error::CredentialError;
11+
pub use import::{import_credential, import_credential_base58, import_credential_file};

nym-vpn-lib/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@ pub enum Error {
183183
#[cfg(windows)]
184184
#[error("administrator privileges required, try rerunning with administrator privileges: `runas /user:Administrator {binary_name} run`")]
185185
AdminPrivilegesRequired { binary_name: String },
186+
187+
#[error("invalid credential: {reason}")]
188+
InvalidCredential {
189+
reason: crate::credentials::CredentialError,
190+
},
191+
192+
#[error(transparent)]
193+
CredentialError(#[from] crate::credentials::CredentialError),
186194
}
187195

188196
// Result type based on our error type

0 commit comments

Comments
 (0)