Feature/credential proxy jwt (#5957)
* squashed feature/credential-proxy-jwt [#5957] post rebasing fixes clippy changed obtain-async endpoint to conditionally return jwt instead of pending zk-nym watching for the attestation file and issuing jwt * changed attestation starting time serialisation into rfc3339 * including authorised JWT issuers in attestation * reduce attestation retrieval error log
This commit is contained in:
committed by
GitHub
parent
e24e094711
commit
d9c2f6ebda
Generated
+9
@@ -5462,8 +5462,11 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-ecash-contract-common",
|
||||
"nym-ecash-signer-check",
|
||||
"nym-http-api-client",
|
||||
"nym-http-api-common",
|
||||
"nym-network-defaults",
|
||||
"nym-pemstore",
|
||||
"nym-upgrade-mode-check",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.22",
|
||||
@@ -5534,6 +5537,7 @@ dependencies = [
|
||||
"nym-http-api-client",
|
||||
"nym-http-api-common",
|
||||
"nym-serde-helpers",
|
||||
"nym-upgrade-mode-check",
|
||||
"reqwest 0.12.22",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
@@ -5661,6 +5665,7 @@ dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"aes-gcm-siv",
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"bs58",
|
||||
@@ -5674,10 +5679,12 @@ dependencies = [
|
||||
"jwt-simple",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"nym-test-utils",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"subtle-encoding",
|
||||
"thiserror 2.0.12",
|
||||
@@ -7378,12 +7385,14 @@ dependencies = [
|
||||
"jwt-simple",
|
||||
"nym-crypto",
|
||||
"nym-http-api-client",
|
||||
"nym-test-utils",
|
||||
"reqwest 0.12.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -453,6 +453,11 @@ opt-level = 'z'
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
suspicious = "deny"
|
||||
complexity = "deny"
|
||||
perf = "deny"
|
||||
style = "deny"
|
||||
|
||||
unwrap_used = "deny"
|
||||
expect_used = "deny"
|
||||
todo = "deny"
|
||||
|
||||
@@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
use std::env;
|
||||
|
||||
let out_dir = env::var("OUT_DIR")?;
|
||||
let out_dir = env::var("OUT_DIR").context("missing OUT_DIR env variable")?;
|
||||
let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite");
|
||||
|
||||
// remove the db file if it already existed from previous build
|
||||
|
||||
@@ -127,6 +127,13 @@ pub enum CredentialProxyError {
|
||||
#[error("failed to create deposit")]
|
||||
DepositFailure,
|
||||
|
||||
#[error("failed to load jwt signing key from {path}: {err}")]
|
||||
JWTSigningKeyLoadFailure {
|
||||
path: String,
|
||||
#[source]
|
||||
err: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("can't obtain sufficient number of credential shares due to unavailable quorum")]
|
||||
UnavailableSigningQuorum,
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::ticketbook_manager::TicketbookManager;
|
||||
use nym_compact_ecash::Base58;
|
||||
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
|
||||
CurrentEpochResponse, DepositResponse, GlobalDataParams, MasterVerificationKeyResponse,
|
||||
PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest,
|
||||
TicketbookObtainParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse,
|
||||
TicketbookWalletSharesResponse,
|
||||
ObtainTicketBookSharesAsyncResponse, PartialVerificationKey, PartialVerificationKeysResponse,
|
||||
TicketbookAsyncRequest, TicketbookObtainParams, TicketbookRequest,
|
||||
TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{Instrument, Level, error, info, span, warn};
|
||||
@@ -65,7 +65,7 @@ impl TicketbookManager {
|
||||
uuid: Uuid,
|
||||
request: TicketbookAsyncRequest,
|
||||
params: TicketbookObtainParams,
|
||||
) -> Result<TicketbookWalletSharesAsyncResponse, CredentialProxyError> {
|
||||
) -> Result<ObtainTicketBookSharesAsyncResponse, CredentialProxyError> {
|
||||
let requested_on = OffsetDateTime::now_utc();
|
||||
let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid);
|
||||
async move {
|
||||
@@ -110,7 +110,7 @@ impl TicketbookManager {
|
||||
}
|
||||
|
||||
// 4. in the meantime, return the id to the user
|
||||
Ok(TicketbookWalletSharesAsyncResponse { id, uuid })
|
||||
Ok(TicketbookWalletSharesAsyncResponse { id, uuid }.into())
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
|
||||
@@ -36,7 +36,11 @@ nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0", default-fea
|
||||
nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
nym-test-utils = { path = "../test-utils" }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -17,6 +17,51 @@ pub mod bs58_ed25519_pubkey {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vec_bs58_ed25519_pubkey {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
keys: &Vec<PublicKey>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut seq = serializer.serialize_seq(Some(keys.len()))?;
|
||||
for key in keys {
|
||||
seq.serialize_element(&Bs58KeyWrapper(*key))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<PublicKey>, D::Error> {
|
||||
let wrapped = Vec::<Bs58KeyWrapper>::deserialize(deserializer)?;
|
||||
Ok(wrapped.into_iter().map(|k| k.0).collect())
|
||||
}
|
||||
|
||||
struct Bs58KeyWrapper(PublicKey);
|
||||
|
||||
impl serde::Serialize for Bs58KeyWrapper {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
bs58_ed25519_pubkey::serialize(&self.0, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Bs58KeyWrapper {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(Bs58KeyWrapper(bs58_ed25519_pubkey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bs58_ed25519_signature {
|
||||
use crate::asymmetric::ed25519::Signature;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
@@ -33,3 +78,53 @@ pub mod bs58_ed25519_signature {
|
||||
Signature::from_base58_string(s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jwt_simple::reexports::{anyhow, serde_json};
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[test]
|
||||
fn vec_bs58_ed25519_pubkey_json() -> anyhow::Result<()> {
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct KeysWrapper(#[serde(with = "vec_bs58_ed25519_pubkey")] Vec<PublicKey>);
|
||||
|
||||
use crate::asymmetric::ed25519;
|
||||
let mut rng = deterministic_rng();
|
||||
let empty = KeysWrapper(vec![]);
|
||||
let single_key = KeysWrapper(vec![PublicKey::from_base58_string(
|
||||
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
|
||||
)?]);
|
||||
let three_keys = KeysWrapper(vec![
|
||||
ed25519::KeyPair::new(&mut rng).public_key,
|
||||
ed25519::KeyPair::new(&mut rng).public_key,
|
||||
ed25519::KeyPair::new(&mut rng).public_key,
|
||||
]);
|
||||
|
||||
let se_empty = serde_json::to_string(&empty)?;
|
||||
let se_single_key = serde_json::to_string(&single_key)?;
|
||||
let se_three_keys = serde_json::to_string(&three_keys)?;
|
||||
|
||||
assert_eq!(se_empty, r#"[]"#);
|
||||
assert_eq!(
|
||||
se_single_key,
|
||||
r#"["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
se_three_keys,
|
||||
r#"["HmgHDV79LpnEaSUp8QZQwSroxVvS4RewF7yM9e7qu8y3","311xRh859qCd5MVqoPRCoNx26eYhLknGwtjzkkTJFGhf","A5BMp8WJ6Uk91U4JpWRv2Bc6X35AaRaSEy8QEWeAkaBv"]"#
|
||||
);
|
||||
|
||||
let empty_de = serde_json::from_str::<KeysWrapper>(&se_empty)?;
|
||||
let single_key_de = serde_json::from_str::<KeysWrapper>(&se_single_key)?;
|
||||
let three_keys_de = serde_json::from_str::<KeysWrapper>(&se_three_keys)?;
|
||||
|
||||
assert_eq!(empty, empty_de);
|
||||
assert_eq!(single_key, single_key_de);
|
||||
assert_eq!(three_keys, three_keys_de);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +902,8 @@ impl Client {
|
||||
|
||||
if self.base_urls.len() > 1 {
|
||||
let orig = self.current_idx.load(Ordering::Relaxed);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut next = (orig + 1) % self.base_urls.len();
|
||||
|
||||
// if fronting is enabled we want to update to a host that has fronts configured
|
||||
|
||||
@@ -15,9 +15,10 @@ jwt-simple = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true, features = ["serde"] }
|
||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
nym-http-api-client = { path = "../http-api-client", default-features = false }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] }
|
||||
@@ -25,6 +26,12 @@ nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
time = { workspace = true, features = ["macros"] }
|
||||
nym-test-utils = { path = "../test-utils" }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
|
||||
|
||||
[features]
|
||||
openapi = ["utoipa"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::UpgradeModeCheckError;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_http_api_client::generate_user_agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct UpgradeModeAttestation {
|
||||
#[serde(flatten)]
|
||||
pub content: UpgradeModeAttestationContent,
|
||||
|
||||
#[serde(with = "ed25519::bs58_ed25519_signature")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub signature: ed25519::Signature,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
impl UpgradeModeAttestation {
|
||||
pub fn authorised_to_issue_jwt(&self, key: &ed25519::PublicKey) -> bool {
|
||||
self.content.authorised_jwt_issuers.contains(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename = "upgrade_mode")]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct UpgradeModeAttestationContent {
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub starting_time: OffsetDateTime,
|
||||
|
||||
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub attester_public_key: ed25519::PublicKey,
|
||||
|
||||
#[serde(with = "ed25519::vec_bs58_ed25519_pubkey")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
|
||||
pub authorised_jwt_issuers: Vec<ed25519::PublicKey>,
|
||||
}
|
||||
|
||||
impl UpgradeModeAttestation {
|
||||
@@ -45,17 +57,26 @@ impl UpgradeModeAttestationContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation {
|
||||
generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc())
|
||||
pub fn generate_new_attestation(
|
||||
key: &ed25519::PrivateKey,
|
||||
authorised_jwt_issuers: Vec<ed25519::PublicKey>,
|
||||
) -> UpgradeModeAttestation {
|
||||
generate_new_attestation_with_starting_time(
|
||||
key,
|
||||
authorised_jwt_issuers,
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_new_attestation_with_starting_time(
|
||||
key: &ed25519::PrivateKey,
|
||||
authorised_jwt_issuers: Vec<ed25519::PublicKey>,
|
||||
starting_time: OffsetDateTime,
|
||||
) -> UpgradeModeAttestation {
|
||||
let content = UpgradeModeAttestationContent {
|
||||
starting_time,
|
||||
attester_public_key: key.into(),
|
||||
authorised_jwt_issuers,
|
||||
};
|
||||
UpgradeModeAttestation {
|
||||
signature: key.sign(content.as_json()),
|
||||
@@ -63,17 +84,19 @@ pub fn generate_new_attestation_with_starting_time(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn attempt_retrieve(
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn attempt_retrieve_attestation(
|
||||
url: &str,
|
||||
) -> Result<Option<UpgradeModeAttestation>, UpgradeModeCheckError> {
|
||||
let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure {
|
||||
user_agent: Option<nym_http_api_client::UserAgent>,
|
||||
) -> Result<Option<UpgradeModeAttestation>, crate::UpgradeModeCheckError> {
|
||||
let retrieval_failure = |source| crate::UpgradeModeCheckError::AttestationRetrievalFailure {
|
||||
url: url.to_string(),
|
||||
source,
|
||||
};
|
||||
|
||||
let attestation = reqwest::ClientBuilder::new()
|
||||
.user_agent(generate_user_agent!())
|
||||
.timeout(Duration::from_secs(5))
|
||||
.user_agent(user_agent.unwrap_or_else(|| nym_http_api_client::generate_user_agent!()))
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.map_err(retrieval_failure)?
|
||||
.get(url)
|
||||
@@ -101,13 +124,20 @@ mod tests {
|
||||
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
|
||||
])?;
|
||||
|
||||
let attestation = generate_new_attestation_with_starting_time(&key, starting_time);
|
||||
let authorised_jwt_issuers = vec![ed25519::PublicKey::from_base58_string(
|
||||
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
|
||||
)?];
|
||||
let attestation = generate_new_attestation_with_starting_time(
|
||||
&key,
|
||||
authorised_jwt_issuers,
|
||||
starting_time,
|
||||
);
|
||||
|
||||
let attestation_json = serde_json::to_string(&attestation)?;
|
||||
let attestation_content_json = attestation.content.as_json();
|
||||
|
||||
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#;
|
||||
let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#;
|
||||
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":"2021-08-23T12:00:00Z","attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","authorised_jwt_issuers":["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"],"signature":"5Kt9dfwvnkdnDcENbwNyitrxghyckWUYycBv8jUUn7hJUMohWEMc6otb3scXQfCrAGSE7FD5m7kr6auBmkAmfczY"}"#;
|
||||
let expected_content = r#"{"type":"upgrade_mode","starting_time":"2021-08-23T12:00:00Z","attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","authorised_jwt_issuers":["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]}"#;
|
||||
|
||||
assert_eq!(attestation_content_json, expected_content);
|
||||
assert_eq!(attestation_json, expected_attestation);
|
||||
|
||||
@@ -12,6 +12,9 @@ pub enum UpgradeModeCheckError {
|
||||
#[error("the jwt metadata didn't contain explicit public key")]
|
||||
MissingTokenPublicKey,
|
||||
|
||||
#[error("the jwt signer does not appear in the authorised attestation set")]
|
||||
UnauthorisedIssuer,
|
||||
|
||||
#[error("the attached public key was not valid ed25519 public key")]
|
||||
MalformedEd25519PublicKey { source: Ed25519RecoveryError },
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ pub fn validate_upgrade_mode_jwt(
|
||||
.map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })?
|
||||
.custom;
|
||||
|
||||
// jwt itself is cryptographically valid,
|
||||
// but let's see if this entity has been permitted to issue the token in the first place
|
||||
if !attestation.authorised_to_issue_jwt(&ed25519_pub_key) {
|
||||
return Err(UpgradeModeCheckError::UnauthorisedIssuer);
|
||||
}
|
||||
|
||||
Ok(attestation)
|
||||
}
|
||||
|
||||
@@ -73,6 +79,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::generate_new_attestation;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
|
||||
#[test]
|
||||
fn generate_and_validate_jwt() {
|
||||
@@ -86,15 +93,25 @@ mod tests {
|
||||
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
|
||||
])
|
||||
.unwrap();
|
||||
let keys = ed25519::KeyPair::from(jwt_key);
|
||||
let jwt_keys = ed25519::KeyPair::from(jwt_key);
|
||||
|
||||
let attestation = generate_new_attestation(&attestation_key);
|
||||
let mut rng = deterministic_rng();
|
||||
let unauthorised_jwt_keys = ed25519::KeyPair::new(&mut rng);
|
||||
|
||||
let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]);
|
||||
let jwt_issuer = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation,
|
||||
attestation.clone(),
|
||||
Duration::from_secs(60 * 60),
|
||||
&keys,
|
||||
&jwt_keys,
|
||||
Some("nym-credential-proxy"),
|
||||
);
|
||||
let unauthorised_jwt = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation.clone(),
|
||||
Duration::from_secs(60 * 60),
|
||||
&unauthorised_jwt_keys,
|
||||
Some("nym-credential-proxy"),
|
||||
);
|
||||
|
||||
// we expect 'nym-credential-proxy' issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok());
|
||||
|
||||
@@ -104,10 +121,15 @@ mod tests {
|
||||
// we expect another-issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err());
|
||||
|
||||
// the key is not in the authorised set inside the attestation
|
||||
assert!(
|
||||
validate_upgrade_mode_jwt(&unauthorised_jwt, Some("nym-credential-proxy")).is_err()
|
||||
);
|
||||
|
||||
let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation,
|
||||
Duration::from_secs(60 * 60),
|
||||
&keys,
|
||||
&jwt_keys,
|
||||
None,
|
||||
);
|
||||
// we expect 'nym-credential-proxy' issuer
|
||||
|
||||
@@ -6,8 +6,10 @@ pub(crate) mod error;
|
||||
pub(crate) mod jwt;
|
||||
|
||||
pub use attestation::{
|
||||
UpgradeModeAttestation, attempt_retrieve, generate_new_attestation,
|
||||
generate_new_attestation_with_starting_time,
|
||||
UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time,
|
||||
};
|
||||
pub use error::UpgradeModeCheckError;
|
||||
pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use attestation::attempt_retrieve_attestation;
|
||||
|
||||
@@ -30,6 +30,7 @@ nym-credentials-interface = { path = "../../common/credentials-interface" }
|
||||
nym-http-api-common = { path = "../../common/http-api-common", optional = true }
|
||||
nym-http-api-client = { path = "../../common/http-api-client" }
|
||||
nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] }
|
||||
nym-upgrade-mode-check = { path = "../../common/upgrade-mode-check" }
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
|
||||
workspace = true
|
||||
@@ -38,5 +39,8 @@ features = ["tokio"]
|
||||
[features]
|
||||
default = ["query-types"]
|
||||
query-types = ["nym-http-api-common", "nym-http-api-common/output"]
|
||||
openapi = ["utoipa", "nym-http-api-common/utoipa"]
|
||||
openapi = ["utoipa", "nym-http-api-common/utoipa", "nym-upgrade-mode-check/openapi"]
|
||||
tsify = ["dep:tsify", "wasm-bindgen"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -12,17 +12,19 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_with::{DisplayFromStr, serde_as};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use time::{Date, OffsetDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "query-types")]
|
||||
use nym_http_api_common::Output;
|
||||
|
||||
#[cfg(feature = "tsify")]
|
||||
use tsify::Tsify;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "tsify")]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
pub use nym_upgrade_mode_check::UpgradeModeAttestation;
|
||||
|
||||
#[derive(JsonSchema)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct PlaceholderJsonSchemaImpl {}
|
||||
@@ -225,6 +227,27 @@ pub struct TicketbookWalletSharesResponse {
|
||||
pub aggregated_expiration_date_signatures: Option<AggregatedExpirationDateSignaturesResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(untagged)]
|
||||
pub enum ObtainTicketBookSharesAsyncResponse {
|
||||
InProgress(TicketbookWalletSharesAsyncResponse),
|
||||
UpgradeMode(Box<UpgradeModeResponse>),
|
||||
}
|
||||
|
||||
impl From<TicketbookWalletSharesAsyncResponse> for ObtainTicketBookSharesAsyncResponse {
|
||||
fn from(response: TicketbookWalletSharesAsyncResponse) -> Self {
|
||||
Self::InProgress(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpgradeModeResponse> for ObtainTicketBookSharesAsyncResponse {
|
||||
fn from(response: UpgradeModeResponse) -> Self {
|
||||
Self::UpgradeMode(Box::new(response))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -319,3 +342,13 @@ pub struct SharesQueryParams {
|
||||
#[serde(flatten)]
|
||||
pub global: GlobalDataParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpgradeModeResponse {
|
||||
pub upgrade_mode_attestation: UpgradeModeAttestation,
|
||||
/// The issued upgrade mode JWT.
|
||||
/// The value is `None` if this credential proxy is not-authorised to be issuing one
|
||||
pub jwt: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![warn(clippy::todo)]
|
||||
#![warn(clippy::dbg_macro)]
|
||||
|
||||
pub mod api;
|
||||
pub mod client;
|
||||
mod helpers;
|
||||
|
||||
pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy";
|
||||
|
||||
macro_rules! absolute_route {
|
||||
( $name:ident, $parent:expr, $suffix:expr ) => {
|
||||
pub fn $name() -> String {
|
||||
|
||||
@@ -62,13 +62,16 @@ nym-http-api-common = { path = "../../common/http-api-common", features = [
|
||||
"utoipa",
|
||||
"middleware",
|
||||
] }
|
||||
nym-http-api-client = { path = "../../common/http-api-client", default-features = false }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = [
|
||||
"openapi",
|
||||
] }
|
||||
nym-upgrade-mode-check = { path = "../../common/upgrade-mode-check" }
|
||||
nym-ecash-signer-check = { path = "../../common/ecash-signer-check" }
|
||||
nym-pemstore = { path = "../../common/pemstore" }
|
||||
|
||||
nym-credential-proxy-lib = { path = "../../common/credential-proxy" }
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::http::state::nyx_upgrade_mode::UpgradeModeState;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_http_api_client::generate_user_agent;
|
||||
use nym_upgrade_mode_check::attempt_retrieve_attestation;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info};
|
||||
use url::Url;
|
||||
|
||||
pub struct AttestationWatcher {
|
||||
// default polling interval
|
||||
regular_polling_interval: Duration,
|
||||
|
||||
// expedited polling interval once upgrade mode is detected
|
||||
expedited_poll_interval: Duration,
|
||||
|
||||
attestation_url: Url,
|
||||
|
||||
jwt_signing_keys: ed25519::KeyPair,
|
||||
|
||||
jwt_validity: Duration,
|
||||
|
||||
upgrade_mode_state: UpgradeModeState,
|
||||
}
|
||||
|
||||
impl AttestationWatcher {
|
||||
pub(crate) fn new(
|
||||
regular_polling_interval: Duration,
|
||||
expedited_poll_interval: Duration,
|
||||
attestation_url: Url,
|
||||
jwt_signing_keys: ed25519::KeyPair,
|
||||
jwt_validity: Duration,
|
||||
) -> Self {
|
||||
AttestationWatcher {
|
||||
regular_polling_interval,
|
||||
expedited_poll_interval,
|
||||
attestation_url,
|
||||
jwt_signing_keys,
|
||||
jwt_validity,
|
||||
upgrade_mode_state: UpgradeModeState {
|
||||
inner: Arc::new(Default::default()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shared_state(&self) -> UpgradeModeState {
|
||||
self.upgrade_mode_state.clone()
|
||||
}
|
||||
|
||||
async fn try_update_state(&self) {
|
||||
match attempt_retrieve_attestation(
|
||||
self.attestation_url.as_str(),
|
||||
Some(generate_user_agent!()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
info!("upgrade mode attestation is not available at this time");
|
||||
debug!("retrieval error: {err}")
|
||||
}
|
||||
Ok(attestation) => {
|
||||
self.upgrade_mode_state
|
||||
.update(attestation, &self.jwt_signing_keys, self.jwt_validity)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_forever(self, cancellation_token: CancellationToken) {
|
||||
info!("starting the attestation watcher task");
|
||||
|
||||
let check_wait = tokio::time::sleep(self.regular_polling_interval);
|
||||
tokio::pin!(check_wait);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancellation_token.cancelled() => {
|
||||
break
|
||||
}
|
||||
_ = &mut check_wait => {
|
||||
self.try_update_state().await;
|
||||
if self.upgrade_mode_state.has_attestation().await {
|
||||
check_wait.as_mut().reset(Instant::now() + self.expedited_poll_interval);
|
||||
} else {
|
||||
check_wait.as_mut().reset(Instant::now() + self.regular_polling_interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ use clap::{Args, Parser};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_credential_proxy_lib::error::CredentialProxyError;
|
||||
use nym_credential_proxy_lib::webhook::ZkNymWebhook;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use std::fs::create_dir_all;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
@@ -20,6 +23,61 @@ fn pretty_build_info_static() -> &'static str {
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
// the reason for `Arc` is that `ArgMatches` impls `Clone`,
|
||||
// so we also need to make the type clone-able
|
||||
// https://github.com/clap-rs/clap/issues/4286#issuecomment-1262527218
|
||||
#[derive(Debug, Clone)]
|
||||
struct PrivateKeyCliWrapper(Arc<ed25519::PrivateKey>);
|
||||
|
||||
impl FromStr for PrivateKeyCliWrapper {
|
||||
type Err = Ed25519RecoveryError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(PrivateKeyCliWrapper(Arc::new(s.parse()?)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(group = clap::ArgGroup::new("jwt-signing-keys").required(true).multiple(false))]
|
||||
pub struct JwtSigningKeysArgs {
|
||||
/// Explicit base58-encoded ed25519 private key used for signing upgrade-mode jwt.
|
||||
#[clap(
|
||||
long,
|
||||
group = "jwt-signing-keys",
|
||||
env = "NYM_CREDENTIAL_PROXY_JWT_SIGNING_KEY"
|
||||
)]
|
||||
jwt_signing_key: Option<PrivateKeyCliWrapper>,
|
||||
|
||||
/// Path to PEM file containing ed25519 private key used for signing upgrade-mode jwt.
|
||||
#[clap(
|
||||
long,
|
||||
group = "jwt-signing-keys",
|
||||
env = "NYM_CREDENTIAL_PROXY_JWT_SIGNING_KEY_PATH"
|
||||
)]
|
||||
jwt_signing_key_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl JwtSigningKeysArgs {
|
||||
pub(crate) fn signing_keys(self) -> Result<ed25519::KeyPair, CredentialProxyError> {
|
||||
if let Some(key) = self.jwt_signing_key {
|
||||
// SAFETY: the arc has never been cloned
|
||||
#[allow(clippy::unwrap_used)]
|
||||
return Ok(Arc::into_inner(key.0).unwrap().into());
|
||||
}
|
||||
|
||||
// SAFETY: due to clap group, clap ensures only one value here is set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let key_path = self.jwt_signing_key_path.unwrap();
|
||||
|
||||
let key: ed25519::PrivateKey = nym_pemstore::load_key(&key_path).map_err(|err| {
|
||||
CredentialProxyError::JWTSigningKeyLoadFailure {
|
||||
path: key_path.to_str().map(|s| s.to_owned()).unwrap_or_default(),
|
||||
err,
|
||||
}
|
||||
})?;
|
||||
Ok(key.into())
|
||||
}
|
||||
}
|
||||
|
||||
// if needed this could be split into subcommands
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author = "Nymtech", version, about, long_version = pretty_build_info_static())]
|
||||
@@ -27,6 +85,12 @@ pub struct Cli {
|
||||
#[clap(flatten)]
|
||||
pub(crate) webhook: ZkNymWebHookConfig,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) upgrade_mode: UpgradeModeConfig,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) jwt_signing_keys: JwtSigningKeysArgs,
|
||||
|
||||
/// Path pointing to an env file that configures the binary.
|
||||
#[clap(short, long)]
|
||||
pub(crate) config_env_file: Option<PathBuf>,
|
||||
@@ -89,6 +153,44 @@ pub struct Cli {
|
||||
pub(crate) persistent_storage_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct UpgradeModeConfig {
|
||||
/// URL for polling for upgrade mode changes.
|
||||
#[clap(
|
||||
long,
|
||||
env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_URL",
|
||||
default_value = "5m"
|
||||
)]
|
||||
pub(crate) attestation_check_url: Url,
|
||||
|
||||
/// Default polling interval of the upgrade mode endpoint.
|
||||
#[clap(
|
||||
long,
|
||||
value_parser = humantime::parse_duration,
|
||||
env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_REGULAR_POLLING_INTERVAL",
|
||||
default_value = "5m",
|
||||
)]
|
||||
pub(crate) attestation_check_regular_polling_interval: Duration,
|
||||
|
||||
/// Expedited polling interval of the upgrade mode endpoint if the UM is enabled.
|
||||
#[clap(
|
||||
long,
|
||||
value_parser = humantime::parse_duration,
|
||||
env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_EXPEDITED_POLLING_INTERVAL",
|
||||
default_value = "1m",
|
||||
)]
|
||||
pub(crate) attestation_check_expedited_polling_interval: Duration,
|
||||
|
||||
/// Validity duration of the issued JWT during upgrade mode.
|
||||
#[clap(
|
||||
long,
|
||||
value_parser = humantime::parse_duration,
|
||||
env = "NYM_CREDENTIAL_PROXY_UPGRADE_MODE_JWT_VALIDITY",
|
||||
default_value = "1h",
|
||||
)]
|
||||
pub(crate) upgrade_mode_jwt_validity: Duration,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct ZkNymWebHookConfig {
|
||||
#[clap(long, env = "WEBHOOK_ZK_NYMS_URL")]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::attestation_watcher::AttestationWatcher;
|
||||
use crate::http::state::ApiState;
|
||||
use crate::{cli::Cli, http::HttpServer};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_credential_proxy_lib::error::CredentialProxyError;
|
||||
@@ -51,6 +53,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> {
|
||||
let mnemonic = cli.mnemonic;
|
||||
let auth_token = cli.http_auth_token;
|
||||
let webhook_cfg = cli.webhook;
|
||||
let jwt_signing_keys = cli.jwt_signing_keys.signing_keys()?;
|
||||
|
||||
let ticketbook_manager = TicketbookManager::new(
|
||||
build_sha_short(),
|
||||
@@ -63,7 +66,24 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let http_server = HttpServer::new(bind_address, ticketbook_manager.clone(), auth_token);
|
||||
let attestation_watcher = AttestationWatcher::new(
|
||||
cli.upgrade_mode.attestation_check_regular_polling_interval,
|
||||
cli.upgrade_mode
|
||||
.attestation_check_expedited_polling_interval,
|
||||
cli.upgrade_mode.attestation_check_url,
|
||||
jwt_signing_keys,
|
||||
cli.upgrade_mode.upgrade_mode_jwt_validity,
|
||||
);
|
||||
|
||||
let api_state = ApiState::new(
|
||||
ticketbook_manager.clone(),
|
||||
attestation_watcher.shared_state(),
|
||||
);
|
||||
|
||||
// spawn the attestation watcher as a separate task
|
||||
api_state.try_spawn_in_background(attestation_watcher.run_forever(api_state.shutdown_token()));
|
||||
|
||||
let http_server = HttpServer::new(bind_address, api_state, auth_token);
|
||||
|
||||
// spawn the http server as a separate task / thread(-ish)
|
||||
http_server.spawn_as_task();
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::http::router::build_router;
|
||||
use crate::http::state::ApiState;
|
||||
use nym_credential_proxy_lib::error::CredentialProxyError;
|
||||
use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager;
|
||||
use std::net::SocketAddr;
|
||||
use tracing::info;
|
||||
|
||||
@@ -12,30 +12,28 @@ pub mod state;
|
||||
|
||||
pub struct HttpServer {
|
||||
bind_address: SocketAddr,
|
||||
ticketbook_manager: TicketbookManager,
|
||||
state: ApiState,
|
||||
auth_token: String,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
pub fn new(
|
||||
bind_address: SocketAddr,
|
||||
ticketbook_manager: TicketbookManager,
|
||||
auth_token: String,
|
||||
) -> Self {
|
||||
pub fn new(bind_address: SocketAddr, state: ApiState, auth_token: String) -> Self {
|
||||
HttpServer {
|
||||
bind_address,
|
||||
ticketbook_manager,
|
||||
state,
|
||||
auth_token,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_as_task(self) {
|
||||
let cancellation = self.ticketbook_manager.shutdown_token();
|
||||
let cancellation = self.state.ticketbooks().shutdown_token();
|
||||
|
||||
let ticketbook_manager = self.ticketbook_manager.clone();
|
||||
// not the best name, but that's due to the branch rotting,
|
||||
// where refactoring would be counter-productive
|
||||
let ticketbook_manager = self.state.ticketbooks().clone();
|
||||
ticketbook_manager.try_spawn_in_background(async move {
|
||||
let address = self.bind_address;
|
||||
let router = build_router(self.ticketbook_manager, self.auth_token);
|
||||
let router = build_router(self.state, self.auth_token);
|
||||
info!("starting the http server on http://{address}");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(address)
|
||||
|
||||
+16
-11
@@ -9,8 +9,8 @@ use nym_credential_proxy_lib::helpers::random_uuid;
|
||||
use nym_credential_proxy_lib::http_helpers::RequestError;
|
||||
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
|
||||
CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse,
|
||||
PartialVerificationKeysResponse, TicketbookAsyncRequest, TicketbookObtainQueryParams,
|
||||
TicketbookRequest, TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse,
|
||||
ObtainTicketBookSharesAsyncResponse, PartialVerificationKeysResponse, TicketbookAsyncRequest,
|
||||
TicketbookObtainQueryParams, TicketbookRequest, TicketbookWalletSharesResponse,
|
||||
};
|
||||
use nym_credential_proxy_requests::routes::api::v1::ticketbook;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
@@ -25,7 +25,7 @@ pub type FormattedPartialVerificationKeysResponse =
|
||||
pub type FormattedTicketbookWalletSharesResponse =
|
||||
FormattedResponse<TicketbookWalletSharesResponse>;
|
||||
pub type FormattedTicketbookWalletSharesAsyncResponse =
|
||||
FormattedResponse<TicketbookWalletSharesAsyncResponse>;
|
||||
FormattedResponse<ObtainTicketBookSharesAsyncResponse>;
|
||||
|
||||
/// Attempt to obtain blinded shares of an ecash ticketbook wallet
|
||||
#[utoipa::path(
|
||||
@@ -63,7 +63,7 @@ pub(crate) async fn obtain_ticketbook_shares(
|
||||
let output = params.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.obtain_ticketbook_shares(uuid, payload, params.obtain_params.global)
|
||||
.await
|
||||
.map_err(|err| RequestError::new_server_error(err, uuid))?;
|
||||
@@ -84,8 +84,8 @@ pub(crate) async fn obtain_ticketbook_shares(
|
||||
),
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(TicketbookWalletSharesAsyncResponse = "application/json"),
|
||||
(TicketbookWalletSharesAsyncResponse = "application/yaml"),
|
||||
(ObtainTicketBookSharesAsyncResponse = "application/json"),
|
||||
(ObtainTicketBookSharesAsyncResponse = "application/yaml"),
|
||||
)),
|
||||
(status = 400, description = "the provided request hasn't been created against correct attributes"),
|
||||
(status = 401, description = "authentication token is missing or is invalid"),
|
||||
@@ -107,8 +107,13 @@ pub(crate) async fn obtain_ticketbook_shares_async(
|
||||
let uuid = random_uuid();
|
||||
let output = params.output.unwrap_or_default();
|
||||
|
||||
// 0. check if we're in 'upgrade-mode' - if so, just return the attestation and associated jwt
|
||||
if let Some(upgrade_mode_response) = state.upgrade_mode_response().await {
|
||||
return Ok(output.to_response(upgrade_mode_response.into()));
|
||||
}
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.obtain_ticketbook_shares_async(uuid, payload, params.obtain_params)
|
||||
.await
|
||||
.map_err(|err| RequestError::new_server_error(err, uuid))?;
|
||||
@@ -142,7 +147,7 @@ pub(crate) async fn current_deposit(
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.current_deposit()
|
||||
.await
|
||||
.map_err(RequestError::new_plain_error)?;
|
||||
@@ -177,7 +182,7 @@ pub(crate) async fn partial_verification_keys(
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.partial_verification_keys()
|
||||
.await
|
||||
.map_err(RequestError::new_plain_error)?;
|
||||
@@ -212,7 +217,7 @@ pub(crate) async fn master_verification_key(
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.master_verification_key()
|
||||
.await
|
||||
.map_err(RequestError::new_plain_error)?;
|
||||
@@ -248,7 +253,7 @@ pub(crate) async fn current_epoch(
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.current_epoch()
|
||||
.await
|
||||
.map_err(RequestError::new_plain_error)?;
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ pub(crate) async fn query_for_shares_by_id(
|
||||
let output = params.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.query_for_shares_by_id(uuid, params.global, share_id)
|
||||
.await
|
||||
.map_err(|err| RequestError::new_server_error(err, uuid))?;
|
||||
@@ -80,7 +80,7 @@ pub(crate) async fn query_for_shares_by_device_id_and_credential_id(
|
||||
let output = params.output.unwrap_or_default();
|
||||
|
||||
let response = state
|
||||
.inner_state()
|
||||
.ticketbooks()
|
||||
.query_for_shares_by_device_id_and_credential_id(
|
||||
uuid,
|
||||
params.global,
|
||||
|
||||
@@ -18,7 +18,7 @@ fn swagger_redirect<S: Clone + Send + Sync + 'static>() -> MethodRouter<S> {
|
||||
get(|| async { Redirect::to("/api/v1/swagger/") })
|
||||
}
|
||||
|
||||
pub fn build_router(state: impl Into<ApiState>, auth_token: String) -> Router {
|
||||
pub fn build_router(state: ApiState, auth_token: String) -> Router {
|
||||
// let auth_layer = from_extractor::<RequireAuth>();
|
||||
let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(auth_token)));
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn build_router(state: impl Into<ApiState>, auth_token: String) -> Router {
|
||||
// we don't have to be using middleware, but we already had that code
|
||||
// we might want something like: https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs#L44 instead
|
||||
.layer(axum::middleware::from_fn(logging::log_request_info))
|
||||
.with_state(state.into());
|
||||
.with_state(state);
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "cors")] {
|
||||
|
||||
@@ -1,21 +1,50 @@
|
||||
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::http::state::nyx_upgrade_mode::UpgradeModeState;
|
||||
use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager;
|
||||
use nym_credential_proxy_requests::api::v1::ticketbook::models::UpgradeModeResponse;
|
||||
use std::future::Future;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub(crate) mod nyx_upgrade_mode;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiState {
|
||||
inner: TicketbookManager,
|
||||
}
|
||||
|
||||
impl From<TicketbookManager> for ApiState {
|
||||
fn from(inner: TicketbookManager) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
ticketbooks: TicketbookManager,
|
||||
upgrade_mode: UpgradeModeState,
|
||||
}
|
||||
|
||||
impl ApiState {
|
||||
pub(crate) fn inner_state(&self) -> &TicketbookManager {
|
||||
&self.inner
|
||||
pub(crate) fn new(ticketbooks: TicketbookManager, upgrade_mode: UpgradeModeState) -> Self {
|
||||
Self {
|
||||
ticketbooks,
|
||||
upgrade_mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ticketbooks(&self) -> &TicketbookManager {
|
||||
&self.ticketbooks
|
||||
}
|
||||
|
||||
pub fn shutdown_token(&self) -> CancellationToken {
|
||||
self.ticketbooks.shutdown_token()
|
||||
}
|
||||
|
||||
pub(crate) fn try_spawn_in_background<F>(&self, task: F) -> Option<JoinHandle<F::Output>>
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
self.ticketbooks().try_spawn_in_background(task)
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade_mode_response(&self) -> Option<UpgradeModeResponse> {
|
||||
let (upgrade_mode_attestation, jwt) = self.upgrade_mode.attestation_with_jwt().await?;
|
||||
Some(UpgradeModeResponse {
|
||||
upgrade_mode_attestation,
|
||||
jwt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_credential_proxy_requests::CREDENTIAL_PROXY_JWT_ISSUER;
|
||||
use nym_credential_proxy_requests::api::v1::ticketbook::models::UpgradeModeAttestation;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_upgrade_mode_check::generate_jwt_for_upgrade_mode_attestation;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UpgradeModeState {
|
||||
pub(crate) inner: Arc<RwLock<Option<UpgradeModeStateInner>>>,
|
||||
}
|
||||
|
||||
impl UpgradeModeState {
|
||||
pub(crate) async fn has_attestation(&self) -> bool {
|
||||
self.inner.read().await.is_some()
|
||||
}
|
||||
|
||||
pub(crate) async fn update(
|
||||
&self,
|
||||
retrieved_attestation: Option<UpgradeModeAttestation>,
|
||||
jwt_signing_keys: &ed25519::KeyPair,
|
||||
jwt_validity: Duration,
|
||||
) {
|
||||
let mut guard = self.inner.write().await;
|
||||
let Some(attestation) = retrieved_attestation else {
|
||||
*guard = None;
|
||||
return;
|
||||
};
|
||||
|
||||
match guard.as_mut() {
|
||||
None => {
|
||||
// no existing state - it's the first time we're going into upgrade mode,
|
||||
// so generate the jwt
|
||||
*guard = Some(UpgradeModeStateInner::new_fresh(
|
||||
attestation,
|
||||
jwt_signing_keys,
|
||||
jwt_validity,
|
||||
));
|
||||
}
|
||||
Some(current_state) => {
|
||||
let mut should_refresh = false;
|
||||
// update the jwt if we have issued one and:
|
||||
// - either the attestation has changed
|
||||
// - or the existing jwt is close to expiry
|
||||
if current_state.attestation != attestation {
|
||||
should_refresh = true;
|
||||
}
|
||||
|
||||
if let Some(issued_jwt) = current_state.jwt.as_ref()
|
||||
&& issued_jwt.close_to_expiry()
|
||||
{
|
||||
should_refresh = true;
|
||||
}
|
||||
|
||||
if should_refresh {
|
||||
current_state.attestation = attestation;
|
||||
current_state.refresh_jwt(jwt_signing_keys, jwt_validity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn attestation_with_jwt(
|
||||
&self,
|
||||
) -> Option<(UpgradeModeAttestation, Option<String>)> {
|
||||
let guard = self.inner.read().await;
|
||||
let inner = guard.as_ref()?;
|
||||
Some((
|
||||
inner.attestation.clone(),
|
||||
inner.jwt.as_ref().map(|jwt| jwt.token.clone()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UpgradeModeStateInner {
|
||||
pub(crate) attestation: UpgradeModeAttestation,
|
||||
pub(crate) jwt: Option<Jwt>,
|
||||
}
|
||||
|
||||
impl UpgradeModeStateInner {
|
||||
fn try_generate_jwt(
|
||||
attestation: &UpgradeModeAttestation,
|
||||
jwt_signing_keys: &ed25519::KeyPair,
|
||||
jwt_validity: Duration,
|
||||
) -> Option<Jwt> {
|
||||
if attestation.authorised_to_issue_jwt(jwt_signing_keys.public_key()) {
|
||||
Some(Jwt::generate(attestation, jwt_signing_keys, jwt_validity))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn new_fresh(
|
||||
attestation: UpgradeModeAttestation,
|
||||
jwt_signing_keys: &ed25519::KeyPair,
|
||||
jwt_validity: Duration,
|
||||
) -> Self {
|
||||
let jwt = Self::try_generate_jwt(&attestation, jwt_signing_keys, jwt_validity);
|
||||
UpgradeModeStateInner { attestation, jwt }
|
||||
}
|
||||
|
||||
fn refresh_jwt(&mut self, keys: &ed25519::KeyPair, validity: Duration) {
|
||||
self.jwt = Self::try_generate_jwt(&self.attestation, keys, validity);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Jwt {
|
||||
pub(crate) issued_at: OffsetDateTime,
|
||||
pub(crate) issued_for: Duration,
|
||||
pub(crate) token: String,
|
||||
}
|
||||
|
||||
impl Jwt {
|
||||
fn generate(
|
||||
upgrade_mode_attestation: &UpgradeModeAttestation,
|
||||
keys: &ed25519::KeyPair,
|
||||
validity: Duration,
|
||||
) -> Self {
|
||||
Jwt {
|
||||
issued_at: OffsetDateTime::now_utc(),
|
||||
issued_for: validity,
|
||||
token: generate_jwt_for_upgrade_mode_attestation(
|
||||
upgrade_mode_attestation.clone(),
|
||||
validity,
|
||||
keys,
|
||||
Some(CREDENTIAL_PROXY_JWT_ISSUER),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_to_expiry(&self) -> bool {
|
||||
// less than 20% of validity left
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let validity_threshold = Duration::from_secs_f32(self.issued_for.as_secs_f32() * 0.8);
|
||||
now - self.issued_at >= validity_threshold
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ cfg_if::cfg_if! {
|
||||
pub mod config;
|
||||
pub mod helpers;
|
||||
pub mod http;
|
||||
pub mod attestation_watcher;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[tokio::main]
|
||||
#[allow(clippy::exit)]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
eprintln!("This tool is only supported on Unix systems");
|
||||
#[allow(clippy::exit)]
|
||||
|
||||
Reference in New Issue
Block a user