VYPR
High severityNVD Advisory· Published May 28, 2026

CVE-2026-45041

CVE-2026-45041

Description

RustFS is a distributed object storage system built in Rust. Prior to 1.0.0-beta.2, crates/appauth/src/token.rs ships a 2048-bit RSA private key as a string constant named TEST_PRIVATE_KEY and uses it in production via parse_license() to "verify" license tokens. Because the key is embedded in every published source release and binary, anyone who can read the repository or extract it from the binary can mint arbitrary license tokens (any subject, any expiration). When the license Cargo feature is enabled, this defeats the entire license-enforcement mechanism. This vulnerability is fixed in 1.0.0-beta.2.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Hard-coded RSA private key in RustFS license verifier allows arbitrary license forgery, defeating license enforcement.

Vulnerability

A hard-coded 2048-bit RSA private key is embedded as a string constant TEST_PRIVATE_KEY in crates/appauth/src/token.rs and used by parse_license() to decrypt license tokens. Affected versions are prior to 1.0.0-beta.2. The key is present in every published source release and binary. Additionally, the license validation uses RSA PKCS#1 v1.5 encryption/decryption instead of a signature scheme, which does not authenticate the issuer. [1]

Exploitation

An attacker who can read the repository or extract the key from a binary can mint arbitrary license tokens with any subject and expiration. No authentication or special network position is required beyond access to the key. The attacker can derive the public key from the private key and encrypt a payload that, when decrypted by the system, yields a valid license. [1]

Impact

Successful exploitation allows an attacker to bypass the license-enforcement mechanism entirely. When the license Cargo feature is enabled, every authenticated S3 access calls license_check(). An attacker with a forged license can gain unauthorized access to storage operations, potentially leading to data disclosure, modification, or denial of service. [1]

Mitigation

The vulnerability is fixed in version 1.0.0-beta.2. Users should upgrade to that version or later. No workaround is available if the license feature is enabled; disabling the feature may be a temporary mitigation. The advisory notes that the cryptographic construction itself is flawed, so even a new key would not fix the underlying design issue. [1]

AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Rustfs/Rustfsinferred2 versions
    <1.0.0-beta.2+ 1 more
    • (no CPE)range: <1.0.0-beta.2
    • (no CPE)range: <1.0.0-beta.2

Patches

1
c9a2fd756c4c

feat(rustfs): add optional license gating feature (#2197)

https://github.com/rustfs/rustfs安正超Mar 18, 2026via body-scan-shorthand
4 files changed · +268 39
  • rustfs/Cargo.toml+1 0 modified
    @@ -36,6 +36,7 @@ metrics = []
     ftps = ["rustfs-protocols/ftps"]
     swift = ["rustfs-protocols/swift"]
     webdav = ["rustfs-protocols/webdav"]
    +license = []
     full = ["metrics", "ftps", "swift", "webdav"]
     
     [lints]
    
  • rustfs/src/license.rs+254 32 modified
    @@ -13,51 +13,273 @@
     // limitations under the License.
     
     use rustfs_appauth::token::Token;
    -use std::io::{Error, Result};
    +use rustfs_appauth::token::parse_license;
    +use std::fmt;
    +use std::io::{Error, ErrorKind, Result};
    +use std::sync::Arc;
     use std::sync::OnceLock;
    -use std::time::SystemTime;
    -use std::time::UNIX_EPOCH;
    -use tracing::error;
    -use tracing::info;
    +use std::sync::RwLock;
    +use std::time::{SystemTime, UNIX_EPOCH};
    +use tracing::{error, info, warn};
     
    -static LICENSE: OnceLock<Token> = OnceLock::new();
    +pub type LicenseResult<T> = std::result::Result<T, LicenseError>;
    +pub type SharedLicenseVerifier = Arc<dyn LicenseVerifier>;
     
    -/// Initialize the license
    -pub fn init_license(license: Option<String>) {
    -    if license.is_none() {
    -        error!("License is None");
    -        return;
    +#[derive(Clone, Debug)]
    +pub enum LicenseError {
    +    /// Internal license state lock could not be acquired.
    +    StatePoisoned,
    +    /// License is required in licensed builds but not provided.
    +    Missing,
    +    /// Token decoding or signature check failed.
    +    Invalid(String),
    +    /// License expiration check failed.
    +    #[cfg(feature = "license")]
    +    Expired { expired_at: u64, now: u64 },
    +    /// System time could not be read.
    +    Clock(String),
    +}
    +
    +impl fmt::Display for LicenseError {
    +    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    +        match self {
    +            LicenseError::StatePoisoned => write!(f, "License state is unavailable"),
    +            LicenseError::Missing => write!(f, "License is required when building with feature `license`."),
    +            LicenseError::Invalid(message) => write!(f, "Incorrect license, please contact RustFS. {message}"),
    +            #[cfg(feature = "license")]
    +            LicenseError::Expired { expired_at, now } => {
    +                write!(f, "Incorrect license, please contact RustFS. expired_at={expired_at}, now={now}")
    +            }
    +            LicenseError::Clock(message) => write!(f, "Failed to read system time: {message}"),
    +        }
         }
    -    let license = license.unwrap();
    -    let token = rustfs_appauth::token::parse_license(&license).unwrap_or_default();
    +}
     
    -    LICENSE.set(token).unwrap_or_else(|_| {
    -        error!("Failed to set license");
    -    });
    +impl std::error::Error for LicenseError {}
    +
    +impl LicenseError {
    +    fn into_io(self) -> Error {
    +        match self {
    +            LicenseError::StatePoisoned | LicenseError::Clock(_) => Error::other(self.to_string()),
    +            LicenseError::Missing | LicenseError::Invalid(_) => Error::new(ErrorKind::PermissionDenied, self.to_string()),
    +            #[cfg(feature = "license")]
    +            LicenseError::Expired { .. } => Error::new(ErrorKind::PermissionDenied, self.to_string()),
    +        }
    +    }
     }
     
    -/// Get the license
    -pub fn get_license() -> Option<Token> {
    -    LICENSE.get().cloned()
    +#[derive(Clone, Debug, Default)]
    +enum LicenseStatus {
    +    /// Internal state has not been evaluated yet.
    +    #[default]
    +    Uninitialized,
    +    /// License has been validated.
    +    Valid,
    +    /// License is missing for strict license builds.
    +    Missing,
    +    /// License validation failed.
    +    Invalid(String),
     }
     
    -/// Check the license
    -/// This function checks if the license is valid.
    -#[allow(unreachable_code)]
    -pub fn license_check() -> Result<()> {
    -    return Ok(());
    -    let invalid_license = LICENSE.get().map(|token| {
    -        if token.expired < SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() {
    -            error!("License expired");
    -            return Err(Error::other("Incorrect license, please contact RustFS."));
    +impl fmt::Display for LicenseStatus {
    +    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    +        match self {
    +            Self::Uninitialized => write!(f, "uninitialized"),
    +            Self::Valid => write!(f, "valid"),
    +            Self::Missing => write!(f, "missing"),
    +            Self::Invalid(message) => write!(f, "{message}"),
    +        }
    +    }
    +}
    +
    +#[derive(Clone, Debug, Default)]
    +struct LicenseState {
    +    token: Option<Token>,
    +    status: LicenseStatus,
    +}
    +
    +/// Verifier for parsing and validating raw license materials.
    +pub trait LicenseVerifier: Send + Sync {
    +    fn validate(&self, raw_license: &str, now: u64) -> LicenseResult<Token>;
    +}
    +
    +#[derive(Debug, Default)]
    +struct AppAuthLicenseVerifier;
    +
    +impl LicenseVerifier for AppAuthLicenseVerifier {
    +    fn validate(&self, raw_license: &str, _now: u64) -> LicenseResult<Token> {
    +        let token = parse_license(raw_license).map_err(|err| LicenseError::Invalid(err.to_string()))?;
    +
    +        #[cfg(feature = "license")]
    +        if token.expired <= _now {
    +            return Err(LicenseError::Expired {
    +                expired_at: token.expired,
    +                now: _now,
    +            });
             }
    -        info!("License is valid ! expired at {}", token.expired);
    -        Ok(())
    +
    +        Ok(token)
    +    }
    +}
    +
    +static LICENSE_STATE: OnceLock<RwLock<LicenseState>> = OnceLock::new();
    +static LICENSE_VERIFIER: OnceLock<SharedLicenseVerifier> = OnceLock::new();
    +
    +fn license_state() -> &'static RwLock<LicenseState> {
    +    LICENSE_STATE.get_or_init(|| RwLock::new(LicenseState::default()))
    +}
    +
    +fn default_license_verifier() -> SharedLicenseVerifier {
    +    Arc::new(AppAuthLicenseVerifier)
    +}
    +
    +fn license_verifier() -> &'static SharedLicenseVerifier {
    +    LICENSE_VERIFIER.get_or_init(default_license_verifier)
    +}
    +
    +fn now_epoch_secs() -> LicenseResult<u64> {
    +    SystemTime::now()
    +        .duration_since(UNIX_EPOCH)
    +        .map_err(|err| LicenseError::Clock(err.to_string()))
    +        .map(|value| value.as_secs())
    +}
    +
    +fn normalized_license(raw_license: Option<String>) -> Option<String> {
    +    raw_license.map(|raw| raw.trim().to_string()).filter(|raw| !raw.is_empty())
    +}
    +
    +fn strict_build_missing_status() -> LicenseStatus {
    +    if cfg!(feature = "license") {
    +        LicenseStatus::Missing
    +    } else {
    +        LicenseStatus::Uninitialized
    +    }
    +}
    +
    +fn apply_missing_status(state: &mut LicenseState) {
    +    state.token = None;
    +    state.status = strict_build_missing_status();
    +}
    +
    +fn apply_invalid_status(state: &mut LicenseState, err: LicenseError) {
    +    state.token = None;
    +    state.status = LicenseStatus::Invalid(match err {
    +        LicenseError::Invalid(message) => message,
    +        #[cfg(feature = "license")]
    +        LicenseError::Expired { expired_at, now } => format!("expired at {expired_at}, now {now}"),
    +        LicenseError::Clock(message) => format!("system clock error: {message}"),
    +        LicenseError::Missing => "license is required".to_string(),
    +        LicenseError::StatePoisoned => "license state is unavailable".to_string(),
         });
    +}
    +
    +fn apply_valid_status(state: &mut LicenseState, token: Token) {
    +    state.token = Some(token);
    +    state.status = LicenseStatus::Valid;
    +}
    +
    +/// Replace the global license verifier.
    +///
    +/// This is the extension point for OEM/build-time overlays.
    +/// Returns `false` if the verifier was already initialized.
    +#[allow(dead_code)]
    +pub fn set_license_verifier(verifier: SharedLicenseVerifier) -> bool {
    +    LICENSE_VERIFIER.set(verifier).is_ok()
    +}
    +
    +/// Initialize the license in memory.
    +///
    +/// This keeps the default API signature stable and is safe to call multiple times.
    +pub fn initialize_license(raw_license: Option<String>) {
    +    if let Err(err) = initialize_license_result(raw_license) {
    +        error!("license initialization failed: {err}");
    +    }
    +}
    +
    +/// Explicit initialization API with typed error return.
    +pub fn initialize_license_result(raw_license: Option<String>) -> LicenseResult<()> {
    +    let normalized = normalized_license(raw_license);
    +    let mut state = license_state().write().map_err(|_| LicenseError::StatePoisoned)?;
    +
    +    match normalized {
    +        Some(raw_license) => {
    +            let now = now_epoch_secs()?;
    +            match license_verifier().validate(&raw_license, now) {
    +                Ok(token) => {
    +                    apply_valid_status(&mut state, token.clone());
    +                    info!("license loaded, subject: {}", token.name);
    +                    Ok(())
    +                }
    +                Err(err) => {
    +                    apply_invalid_status(&mut state, err.clone());
    +                    warn!("license verification failed: {err}");
    +                    Err(err)
    +                }
    +            }
    +        }
    +        None => {
    +            apply_missing_status(&mut state);
    +            if let LicenseStatus::Missing = state.status {
    +                Err(LicenseError::Missing)
    +            } else {
    +                Ok(())
    +            }
    +        }
    +    }
    +}
    +
    +/// Legacy name kept for existing startup code.
    +pub fn init_license(license: Option<String>) {
    +    initialize_license(license);
    +}
    +
    +/// Return the current license information.
    +pub fn get_license() -> Option<Token> {
    +    license_state().read().ok().and_then(|state| state.token.clone())
    +}
    +
    +/// New name for compatibility with external integrations.
    +pub fn current_license() -> Option<Token> {
    +    get_license()
    +}
     
    -    if invalid_license.is_none() || invalid_license.is_some_and(|v| v.is_err()) {
    -        return Err(Error::other("Incorrect license, please contact RustFS."));
    +/// Observe the current license status for observability.
    +pub fn license_status() -> String {
    +    license_state()
    +        .read()
    +        .ok()
    +        .map(|state| state.status.to_string())
    +        .unwrap_or_else(|| LicenseStatus::Uninitialized.to_string())
    +}
    +
    +/// Check whether current in-memory license is currently valid.
    +#[cfg(feature = "license")]
    +pub fn ensure_license() -> LicenseResult<()> {
    +    let state = license_state().read().map_err(|_| LicenseError::StatePoisoned)?;
    +    match &state.status {
    +        LicenseStatus::Missing => return Err(LicenseError::Missing),
    +        LicenseStatus::Invalid(message) => return Err(LicenseError::Invalid(message.to_string())),
    +        LicenseStatus::Uninitialized | LicenseStatus::Valid => {}
    +    };
    +
    +    let token = state.token.as_ref().ok_or(LicenseError::Missing)?;
    +    let now = now_epoch_secs()?;
    +    if token.expired <= now {
    +        return Err(LicenseError::Expired {
    +            expired_at: token.expired,
    +            now,
    +        });
         }
     
         Ok(())
     }
    +
    +#[cfg(not(feature = "license"))]
    +pub fn ensure_license() -> LicenseResult<()> {
    +    Ok(())
    +}
    +
    +/// Compatibility API for call-sites that still use the legacy name.
    +pub fn license_check() -> Result<()> {
    +    ensure_license().map_err(LicenseError::into_io)
    +}
    
  • rustfs/src/main.rs+6 1 modified
    @@ -44,7 +44,7 @@ use crate::server::{
         SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, init_cert, init_event_notifier, shutdown_event_notifier,
         start_audit_system, start_http_server, stop_audit_system, wait_for_shutdown,
     };
    -use license::init_license;
    +use license::{current_license, init_license, license_status};
     use rustfs_common::{GlobalReadiness, SystemStage, set_global_addr};
     use rustfs_credentials::init_global_action_credentials;
     use rustfs_ecstore::store::init_lock_clients;
    @@ -136,6 +136,11 @@ async fn async_main() -> Result<()> {
             }
         }
     
    +    info!("license status: {}", license_status());
    +    if let Some(token) = current_license() {
    +        info!("runtime license loaded: {}", token.name);
    +    }
    +
         // print startup logo
         info!("{}", server::LOGO);
     
    
  • rustfs/src/storage/access.rs+7 6 modified
    @@ -405,6 +405,13 @@ impl S3Access for FS {
     
             let ext = cx.extensions_mut();
             ext.insert(req_info);
    +        license_check().map_err(|er| match er.kind() {
    +            std::io::ErrorKind::PermissionDenied => s3_error!(AccessDenied, "{er}"),
    +            _ => {
    +                tracing::error!("license check failed due to unexpected error: {er}");
    +                s3_error!(InternalError, "License validation failed")
    +            }
    +        })?;
     
             // Verify uniformly here? Or verify separately below?
     
    @@ -415,8 +422,6 @@ impl S3Access for FS {
         ///
         /// This method returns `Ok(())` by default.
         async fn create_bucket(&self, req: &mut S3Request<CreateBucketInput>) -> S3Result<()> {
    -        license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?;
    -
             let req_info = ext_req_info_mut(&mut req.extensions)?;
             req_info.bucket = Some(req.input.bucket.clone());
     
    @@ -480,8 +485,6 @@ impl S3Access for FS {
     
         /// Checks whether the CreateMultipartUpload request has accesses to the resources.
         async fn create_multipart_upload(&self, req: &mut S3Request<CreateMultipartUploadInput>) -> S3Result<()> {
    -        license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?;
    -
             let req_info = ext_req_info_mut(&mut req.extensions)?;
             req_info.bucket = Some(req.input.bucket.clone());
             req_info.object = Some(req.input.key.clone());
    @@ -1318,8 +1321,6 @@ impl S3Access for FS {
         ///
         /// This method returns `Ok(())` by default.
         async fn put_object(&self, req: &mut S3Request<PutObjectInput>) -> S3Result<()> {
    -        license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?;
    -
             let req_info = ext_req_info_mut(&mut req.extensions)?;
             req_info.bucket = Some(req.input.bucket.clone());
             req_info.object = Some(req.input.key.clone());
    

Vulnerability mechanics

Root cause

"A 2048-bit RSA private key is hard-coded as a string constant in the source and used as the decryption key for license token validation, allowing anyone with access to the key to forge arbitrary license tokens."

Attack vector

An attacker who can read the public repository or extract the binary can obtain the embedded 2048-bit RSA private key [CWE-321]. Using any standard RSA library, the attacker encrypts a crafted JSON token (containing an arbitrary subject name and a far-future expiration) with the public key derived from the embedded private key [ref_id=1]. The resulting ciphertext, base64url-encoded, is a valid license token that `parse_license()` will decrypt and accept. When the `license` Cargo feature is enabled, this forged token passes `ensure_license()`, bypassing the S3 access gate in `rustfs/src/storage/access.rs` and defeating the entire license-enforcement mechanism [ref_id=1].

Affected code

The vulnerability lives in `crates/appauth/src/token.rs`, where a 2048-bit RSA private key is embedded as the string constant `TEST_PRIVATE_KEY` (line 68) and used by `parse_license()` to decrypt license tokens via RSA PKCS#1 v1.5 [ref_id=1]. The only registered verifier, `AppAuthLicenseVerifier` in `rustfs/src/license.rs`, calls `parse_license` directly, and under the `license` Cargo feature every authenticated S3 request in `rustfs/src/storage/access.rs` invokes `license_check()` [ref_id=1].

What the fix does

The patch does not remove the embedded private key; instead it refactors the license subsystem to introduce a `LicenseVerifier` trait and a `set_license_verifier()` extension point, allowing the default `AppAuthLicenseVerifier` (which still calls the vulnerable `parse_license`) to be replaced at build time [patch_id=2974406]. The patch also moves the `license_check()` call from individual S3 handler methods (e.g. `create_bucket`, `put_object`) into a single centralized check in the `authorize` method of `access.rs`, and adds proper error mapping to `AccessDenied` [patch_id=2974406]. The advisory recommends a more thorough fix: replace RSA PKCS#1 v1.5 decryption with a signature scheme such as Ed25519, embed only the 32-byte verifying key, and reissue all licenses under a new keypair [ref_id=1].

Preconditions

  • configThe `license` Cargo feature must be enabled in the RustFS build
  • inputAttacker must be able to read the embedded private key from the source repository or binary
  • networkAttacker must have a way to supply the forged license token to a running RustFS instance (e.g. via RUSTFS_LICENSE env var, admin API, or config file)

Reproduction

The advisory includes a complete Python reproduction script using the `cryptography` library [ref_id=1]. The script loads the embedded PEM private key, constructs a JSON token `{"name": "attacker-controlled", "expired": 4102444800}`, encrypts it with RSA PKCS#1 v1.5 using the public key, and base64url-encodes the ciphertext. The output, when supplied as a license to a RustFS instance built with the `license` feature, is accepted as valid by `parse_license()` and yields a token valid until 2100-01-01 for an arbitrary subject [ref_id=1].

Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.