diff --git a/Cargo.lock b/Cargo.lock index 7c2db6de8a2..a8672b22a38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,6 +847,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bip32" version = "0.5.3" @@ -942,6 +948,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "blake3" version = "1.8.2" @@ -1332,6 +1349,17 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -1868,6 +1896,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctr" version = "0.9.2" @@ -2424,6 +2458,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.16", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -2488,6 +2532,8 @@ dependencies = [ "ff", "generic-array 0.14.7", "group", + "hkdf", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -3349,6 +3395,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.11" @@ -4088,6 +4158,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -5553,6 +5649,7 @@ dependencies = [ "generic-array 0.14.7", "hkdf", "hmac", + "jwt-simple", "nym-pemstore", "nym-sphinx-types", "rand 0.8.5", @@ -7108,6 +7205,22 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-upgrade-mode-check" +version = "0.1.0" +dependencies = [ + "anyhow", + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.12.22", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tracing", +] + [[package]] name = "nym-validator-client" version = "0.1.0" @@ -7613,6 +7726,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "pairing" version = "0.23.0" @@ -8593,6 +8718,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle 2.6.1", @@ -9872,6 +9998,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.16", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "syn" version = "1.0.109" @@ -11369,6 +11508,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" diff --git a/Cargo.toml b/Cargo.toml index 6cf7625af6e..d3f7b88a300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,7 +96,7 @@ members = [ "common/ticketbooks-merkle", "common/topology", "common/tun", - "common/types", + "common/types", "common/upgrade-mode-check", "common/verloc", "common/wasm/client-core", "common/wasm/storage", @@ -271,6 +271,7 @@ inquire = "0.6.2" ip_network = "0.4.1" ipnetwork = "0.20" itertools = "0.14.0" +jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] } k256 = "0.13" lazy_static = "1.5.0" ledger-transport = "0.10.0" diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 6e1f8cfb12b..4e5af9129ad 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -18,6 +18,7 @@ digest = { workspace = true, optional = true } generic-array = { workspace = true, optional = true } hkdf = { workspace = true, optional = true } hmac = { workspace = true, optional = true } +jwt-simple = { workspace = true, optional = true } cipher = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true } ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true } @@ -39,6 +40,7 @@ rand_chacha = { workspace = true } [features] default = [] aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] +naive_jwt = ["asymmetric", "jwt-simple"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"] diff --git a/common/crypto/src/asymmetric/ed25519/mod.rs b/common/crypto/src/asymmetric/ed25519/mod.rs index 387034a8d19..8d23ee7d8c4 100644 --- a/common/crypto/src/asymmetric/ed25519/mod.rs +++ b/common/crypto/src/asymmetric/ed25519/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 pub use ed25519_dalek::SignatureError; -use ed25519_dalek::{SecretKey, Signer, SigningKey}; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; + +use ed25519_dalek::Signer; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; @@ -13,6 +14,9 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; #[cfg(feature = "serde")] pub mod serde_helpers; +#[cfg(feature = "serde")] +pub use serde_helpers::*; + #[cfg(feature = "sphinx")] use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; @@ -81,8 +85,8 @@ impl KeyPair { } } - pub fn from_secret(secret: SecretKey, index: u32) -> Self { - let ed25519_signing_key = SigningKey::from(secret); + pub fn from_secret(secret: ed25519_dalek::SecretKey, index: u32) -> Self { + let ed25519_signing_key = ed25519_dalek::SigningKey::from(secret); KeyPair { private_key: PrivateKey(ed25519_signing_key.to_bytes()), @@ -276,7 +280,7 @@ impl Display for PrivateKey { impl<'a> From<&'a PrivateKey> for PublicKey { fn from(pk: &'a PrivateKey) -> Self { - PublicKey(SigningKey::from_bytes(&pk.0).verifying_key()) + PublicKey(ed25519_dalek::SigningKey::from_bytes(&pk.0).verifying_key()) } } @@ -320,7 +324,7 @@ impl PrivateKey { } pub fn sign>(&self, message: M) -> Signature { - let signing_key: SigningKey = self.0.into(); + let signing_key: ed25519_dalek::SigningKey = self.0.into(); let sig = signing_key.sign(message.as_ref()); Signature(sig) } @@ -425,9 +429,57 @@ impl<'d> Deserialize<'d> for Signature { } } +#[cfg(feature = "naive_jwt")] +impl PublicKey { + pub fn to_jwt_compatible_key(&self) -> jwt_simple::algorithms::Ed25519PublicKey { + (*self).into() + } +} + +#[cfg(feature = "naive_jwt")] +impl From for jwt_simple::algorithms::Ed25519PublicKey { + fn from(value: PublicKey) -> Self { + // SAFETY: we have a valid ed25519 pubkey, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519PublicKey::from_bytes(&value.to_bytes()).unwrap() + } +} + +#[cfg(feature = "naive_jwt")] +impl PrivateKey { + pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair { + let pub_key = self.public_key(); + let mut bytes = zeroize::Zeroizing::new([0u8; 64]); + + bytes[..SECRET_KEY_LENGTH] + .copy_from_slice(zeroize::Zeroizing::new(self.to_bytes()).as_ref()); + bytes[SECRET_KEY_LENGTH..].copy_from_slice(&pub_key.to_bytes()); + + // SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap() + } +} + +#[cfg(feature = "naive_jwt")] +impl KeyPair { + pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair { + let mut bytes = zeroize::Zeroizing::new([0u8; 64]); + + bytes[..SECRET_KEY_LENGTH] + .copy_from_slice(zeroize::Zeroizing::new(self.private_key.to_bytes()).as_ref()); + bytes[SECRET_KEY_LENGTH..].copy_from_slice(&self.public_key.to_bytes()); + + // SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap() + } +} + #[cfg(test)] mod tests { use super::*; + use rand::thread_rng; fn assert_zeroize_on_drop() {} @@ -438,4 +490,29 @@ mod tests { assert_zeroize::(); assert_zeroize_on_drop::(); } + + #[test] + #[cfg(all(feature = "naive_jwt", feature = "rand"))] + fn check_jwt_key_compat_conversion() { + let mut rng = thread_rng(); + let keys = KeyPair::new(&mut rng); + let jwt_keys = keys.to_jwt_compatible_keys(); + + // internally they're represented by hidden `Edwards25519KeyPair` (plus key_id) + // which has way nicer API for assertions + let jwt_keys_inner = + jwt_simple::algorithms::Edwards25519KeyPair::from_bytes(&jwt_keys.to_bytes()).unwrap(); + + let compact_ed25519 = jwt_keys_inner.as_ref(); + assert!(compact_ed25519 + .sk + .validate_public_key(&compact_ed25519.pk) + .is_ok()); + + let dummy_message = "hello world"; + let sig1 = keys.private_key.sign(dummy_message).to_bytes(); + let sig2 = compact_ed25519.sk.sign(dummy_message, None).to_vec(); + + assert_eq!(sig1.to_vec(), sig2); + } } diff --git a/common/upgrade-mode-check/Cargo.toml b/common/upgrade-mode-check/Cargo.toml new file mode 100644 index 00000000000..50e0cb3f7c8 --- /dev/null +++ b/common/upgrade-mode-check/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "nym-upgrade-mode-check" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +jwt-simple = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +time = { workspace = true, features = ["serde"] } +thiserror = { workspace = true } +tracing = { workspace = true } + +nym-http-api-client = { path = "../http-api-client", default-features = false } +nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] } + +[dev-dependencies] +anyhow = { workspace = true } +time = { workspace = true, features = ["macros"] } + +[lints] +workspace = true diff --git a/common/upgrade-mode-check/src/attestation.rs b/common/upgrade-mode-check/src/attestation.rs new file mode 100644 index 00000000000..42c115f3cd9 --- /dev/null +++ b/common/upgrade-mode-check/src/attestation.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA +// 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)] +pub struct UpgradeModeAttestation { + #[serde(flatten)] + pub content: UpgradeModeAttestationContent, + + #[serde(with = "ed25519::bs58_ed25519_signature")] + pub signature: ed25519::Signature, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +#[serde(tag = "type")] +#[serde(rename = "upgrade_mode")] +pub struct UpgradeModeAttestationContent { + #[serde(with = "time::serde::timestamp")] + pub starting_time: OffsetDateTime, + + #[serde(with = "ed25519::bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, +} + +impl UpgradeModeAttestation { + pub fn verify(&self) -> bool { + self.content + .attester_public_key + .verify(self.content.as_json(), &self.signature) + .is_ok() + } +} + +impl UpgradeModeAttestationContent { + pub fn as_json(&self) -> String { + // SAFETY: Serialize impl is valid and we have no non-string map keys + #[allow(clippy::unwrap_used)] + serde_json::to_string(&self).unwrap() + } +} + +pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation { + generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc()) +} + +pub fn generate_new_attestation_with_starting_time( + key: &ed25519::PrivateKey, + starting_time: OffsetDateTime, +) -> UpgradeModeAttestation { + let content = UpgradeModeAttestationContent { + starting_time, + attester_public_key: key.into(), + }; + UpgradeModeAttestation { + signature: key.sign(content.as_json()), + content, + } +} + +pub async fn attempt_retrieve( + url: &str, +) -> Result, UpgradeModeCheckError> { + let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure { + url: url.to_string(), + source, + }; + + let attestation = reqwest::ClientBuilder::new() + .user_agent(generate_user_agent!()) + .timeout(Duration::from_secs(5)) + .build() + .map_err(retrieval_failure)? + .get(url) + .send() + .await + .map_err(retrieval_failure)? + .json::>() + .await + .map_err(retrieval_failure)?; + + Ok(attestation) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn upgrade_mode_attestation_serde_json() -> anyhow::Result<()> { + // unix timestamp: 1629720000 + let starting_time = time::macros::datetime!(2021-08-23 12:00 UTC); + + let key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 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 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"}"#; + + assert_eq!(attestation_content_json, expected_content); + assert_eq!(attestation_json, expected_attestation); + + let recovered_attestation = serde_json::from_str(&attestation_json)?; + assert_eq!(attestation, recovered_attestation); + + let recovered_content = serde_json::from_str(&attestation_content_json)?; + assert_eq!(attestation.content, recovered_content); + + Ok(()) + } +} diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs new file mode 100644 index 00000000000..d1f26b7c5f7 --- /dev/null +++ b/common/upgrade-mode-check/src/error.rs @@ -0,0 +1,23 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum UpgradeModeCheckError { + #[error("failed to decode jwt metadata")] + TokenMetadataDecodeFailure { source: jwt_simple::Error }, + + #[error("the jwt metadata didn't contain explicit public key")] + MissingTokenPublicKey, + + #[error("the attached public key was not valid ed25519 public key")] + MalformedEd25519PublicKey { source: Ed25519RecoveryError }, + + #[error("failed to verify the jwt: {source}")] + JwtVerificationFailure { source: jwt_simple::Error }, + + #[error("failed to retrieve attestation from {url}:{source}")] + AttestationRetrievalFailure { url: String, source: reqwest::Error }, +} diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs new file mode 100644 index 00000000000..93be406ab22 --- /dev/null +++ b/common/upgrade-mode-check/src/jwt.rs @@ -0,0 +1,119 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{UpgradeModeAttestation, UpgradeModeCheckError}; +use jwt_simple::claims::Claims; +use jwt_simple::common::{KeyMetadata, VerificationOptions}; +use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike}; +use jwt_simple::token::Token; +use nym_crypto::asymmetric::ed25519; +use std::collections::HashSet; +use std::time::Duration; + +// for now use static issuer such as "nym-credential-proxy" +pub fn generate_jwt_for_upgrade_mode_attestation( + attestation: UpgradeModeAttestation, + validity: Duration, + keys: &ed25519::KeyPair, + issuer: Option<&'static str>, +) -> String { + let claim = Claims::with_custom_claims(attestation, validity.into()); + let mut claim = if let Some(issuer) = issuer { + claim.with_issuer(issuer) + } else { + claim + }; + claim.create_nonce(); + + let md = KeyMetadata::default().with_public_key(keys.public_key().to_base58_string()); + + let mut jwt_keys = keys.to_jwt_compatible_keys(); + // SAFETY: trait impl for EdDSA is infallible + #[allow(clippy::unwrap_used)] + jwt_keys.attach_metadata(md).unwrap(); + + // SAFETY: our construction of the jwt is valid + #[allow(clippy::unwrap_used)] + jwt_keys.sign(claim).unwrap() +} + +pub fn validate_upgrade_mode_jwt( + token: &str, + expected_issuer: Option<&'static str>, +) -> Result { + // for now, we completely ignore the validity of the pubkey (I know, I know). + // that will be changed later on + // so as a bypass we have to extract the claimed issuer from the jwt to verify against it + let metadata = Token::decode_metadata(token) + .map_err(|source| UpgradeModeCheckError::TokenMetadataDecodeFailure { source })?; + + let pub_key = metadata + .public_key() + .ok_or(UpgradeModeCheckError::MissingTokenPublicKey)?; + + let ed25519_pub_key = ed25519::PublicKey::from_base58_string(pub_key) + .map_err(|source| UpgradeModeCheckError::MalformedEd25519PublicKey { source })?; + + let mut opts = VerificationOptions::default(); + if let Some(issuer) = expected_issuer { + opts.allowed_issuers = Some(HashSet::from_iter(vec![issuer.to_string()])); + } + + let attestation = ed25519_pub_key + .to_jwt_compatible_key() + .verify_token::(token, Some(opts)) + .map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })? + .custom; + + Ok(attestation) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::generate_new_attestation; + use nym_crypto::asymmetric::ed25519; + + #[test] + fn generate_and_validate_jwt() { + let attestation_key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + let jwt_key = ed25519::PrivateKey::from_bytes(&[ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, + 2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]) + .unwrap(); + let keys = ed25519::KeyPair::from(jwt_key); + + let attestation = generate_new_attestation(&attestation_key); + let jwt_issuer = generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + Some("nym-credential-proxy"), + ); + // we expect 'nym-credential-proxy' issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok()); + + // we don't care about issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok()); + + // we expect another-issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err()); + + let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + None, + ); + // we expect 'nym-credential-proxy' issuer + assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err()); + + // we don't care about issuer + assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); + } +} diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs new file mode 100644 index 00000000000..21bc5402ecf --- /dev/null +++ b/common/upgrade-mode-check/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod attestation; +pub(crate) mod error; +pub(crate) mod jwt; + +pub use attestation::{ + attempt_retrieve, generate_new_attestation, generate_new_attestation_with_starting_time, + UpgradeModeAttestation, +}; +pub use error::UpgradeModeCheckError; +pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};