diff --git a/Cargo.lock b/Cargo.lock index 654eaa7d..7a617d37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,6 +1048,7 @@ dependencies = [ "rustls 0.22.4", "rustls-pemfile 2.1.2", "rustls-pki-types", + "se_licensing", "serde", "serde_json", "sha1", @@ -1890,14 +1891,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "enterprise" -version = "0.8.2" -dependencies = [ - "base64 0.22.1", - "ring 0.17.8", -] - [[package]] name = "enum-as-inner" version = "0.6.0" @@ -3581,6 +3574,7 @@ dependencies = [ "jmap_proto", "managesieve", "pop3", + "se_common", "smtp", "store", "tokio", @@ -5592,6 +5586,22 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" +[[package]] +name = "se_common" +version = "0.8.2" +dependencies = [ + "common", + "tracing", +] + +[[package]] +name = "se_licensing" +version = "0.8.2" +dependencies = [ + "base64 0.22.1", + "ring 0.17.8", +] + [[package]] name = "seahash" version = "4.1.0" diff --git a/Cargo.toml b/Cargo.toml index cc70b7da..066915a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,8 @@ members = [ "crates/nlp", "crates/store", "crates/directory", - "crates/enterprise", + "crates/se-licensing", + "crates/se-common", "crates/utils", "crates/common", "crates/cli", diff --git a/LICENSES/LicenseRef-SEL.txt b/LICENSES/LicenseRef-SEL.txt index 85fc84dc..0230d7fe 100644 --- a/LICENSES/LicenseRef-SEL.txt +++ b/LICENSES/LicenseRef-SEL.txt @@ -1,58 +1,93 @@ Stalwart Enterprise License 1.0 (SELv1) Agreement ================================================= -Last Update: June 25, 2024 +Last Update: June 26, 2024 -GRANT OF LICENSE +PLEASE CAREFULLY READ THIS STALWART ENTERPRISE LICENSE AGREEMENT ("AGREEMENT"). THIS AGREEMENT CONSTITUTES A LEGALLY BINDING AGREEMENT BETWEEN YOU AND STALWART LABS LTD AND GOVERNS YOUR USE OF THE SOFTWARE (DEFINED BELOW). IF YOU DO NOT AGREE WITH THIS AGREEMENT, YOU MAY NOT USE THE SOFTWARE. IF YOU ARE USING THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE AUTHORITY TO AGREE TO THIS AGREEMENT ON BEHALF OF SUCH ENTITY. IF YOU DO NOT HAVE SUCH AUTHORITY, DO NOT USE THE SOFTWARE IN ANY MANNER. -Stalwart Labs Ltd. ("Licensor") grants you ("Licensee") a non-exclusive, non-transferable, non-sublicensable, limited license to use the software ("Software") under the terms set forth in this Stalwart Enterprise License Agreement ("Agreement"). +This Agreement is entered into by and between Stalwart Labs Ltd and you, or the legal entity on behalf of whom you are acting. -LICENSE RESTRICTIONS +1. DEFINITIONS -1. Commercial Use: Licensee is authorized to use the Software for commercial purposes, including the provision of services utilizing the Software. However, Licensee is expressly prohibited from reselling, leasing, sublicensing, or otherwise redistributing the Software itself. +1.1. "Software" refers to the Stalwart Mail Server Enterprise Edition software, including all its versions, updates, modifications, accompanying documentation, and related materials. +1.2. "Licensor" refers to Stalwart Labs Ltd, the entity providing the Software. +1.3. "Licensee" refers to the individual or entity installing, accessing, or using the Software. +1.4. "License Key" refers to the unique code provided by Licensor upon purchasing a subscription which activates the full features of the Software. -2. Subscription Requirement: The use of the Software is conditioned upon Licensee maintaining an active and valid paid subscription with Licensor. The Software shall not be used without a valid subscription. +2. GRANT OF LICENSE -3. Source-Available Commercial License: The Software provided under this Agreement is not open source. Accordingly, the Software may not be distributed or modified by Licensee. While the source code may be available for review, it is not open source and remains proprietary. +2.1. Licensor grants Licensee a revocable, non-exclusive, non-transferable, non-sublicensable, limited license to download, install, and use the Software. +2.2. The use of the Software is conditioned upon Licensee maintaining an active and valid paid subscription with Licensor. The paid subscription covers all versions of the Software and all updates and modifications. +2.3. This license grants Licensee the right to use the Software for both personal and commercial purposes. However, Licensee is expressly prohibited from reselling, leasing, sublicensing, or otherwise redistributing the Software itself. +2.4. This license is further governed by the terms and conditions set forth in any licensing agreements separately executed between Licensor and Licensee. In the event of any conflict between the terms of this Agreement and the terms of a signed licensing agreement, the terms of the signed licensing agreement shall control. -4. Additional Licensing Terms: This license is further governed by the terms and conditions set forth in any licensing agreements separately executed between Licensor and Licensee. In the event of any conflict between the terms of this Agreement and the terms of a signed licensing agreement, the terms of the signed licensing agreement shall control. +3. LICENSE KEYS +3.1. The Software shall not be used without a valid License Key issued by Licensor. +3.1. Distribution or sharing of License Keys to third parties, not associated with Licensee, is strictly prohibited. +3.2. License Keys are bound to the subscription period. Should your subscription expire, all License Keys will become invalid after 15 days from the subscription expiration date. +3.3. Any instance of the Software using such an expired key will revert to the Community Edition functionality after the aforementioned 15-day period. -INTELLECTUAL PROPERTY RIGHTS +4. SOURCE CODE USAGE -1. Ownership: The Licensor retains all rights, title, and interest in and to the Software, including all intellectual property rights therein. This Agreement does not transfer any ownership rights to the Licensee. +4.1. Licensee is permitted to view, copy, and modify the Software's source code, as made available by Licensor, solely for Licensee's internal business use and in compliance with this Agreement's terms. +4.2. Any modifications to the source code do not grant Licensee any ownership rights to the original Software or any modifications. All rights, title, and interest to the Software and its source code remain exclusively with Licensor. +4.3. Licensee is strictly prohibited from altering, removing, or in any way tampering with the license key validation system within the Software. Any such unauthorized modifications will be considered a material breach of this Agreement and may result in legal action. +4.4. Licensee is required to use valid License Keys issued by Licensor to run the Software, including any modified versions. Any attempts to bypass the License Key requirement is a violation of this Agreement. +4.5. Notwithstanding the availability of the Software's source code for review and limited modification, the Software and its source code are not open source and remain proprietary to Licensor. The provision of access to the source code does not confer any rights typically associated with open source software, including but not limited to the right to freely distribute, sublicense, or create derivative works for public distribution. All rights not expressly granted herein are reserved by Licensor. -2. Proprietary Notices: The Licensee must not remove, alter, or obscure any proprietary notices (including copyright and trademark notices) on the Software. +5. INTELLECTUAL PROPERTY RIGHTS +5.1. The Licensor retains all rights, title, and interest in and to the Software, including all intellectual property rights therein. This Agreement does not transfer any ownership rights to the Licensee. +5.2. The Licensee must not remove, alter, or obscure any proprietary notices (including copyright and trademark notices) on the Software. -TERMINATION +6. TERMINATION -1. Termination by Licensor: The Licensor may terminate this Agreement immediately if the Licensee fails to comply with any terms and conditions of this Agreement. +6.1. Licensor reserves the right to terminate this Agreement immediately if the Licensee fails to comply with any terms and conditions of this Agreement. +6.2. In the event of a termination, you will be provided with a written notice, sent to the email address used during your subscription to the Software, outlining the reasons for the termination. +6.3. Upon termination, all rights granted to you under this Agreement will cease, and you must promptly cease all use of the Software. -2. Effect of Termination: Upon termination of this Agreement, the Licensee must cease all use of the Software and destroy all copies of the Software in their possession or control. - - -DISCLAIMER OF WARRANTIES - -The Software is provided "as is" without warranty of any kind. The Licensor disclaims all warranties, express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement. - - -LIMITATION OF LIABILITY +7. LIMITATION OF LIABILITY In no event will the Licensor be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from (i) your use or inability to use the Software; (ii) any unauthorized access to or use of our servers and/or any personal information stored therein. +8. GOVERNING LAW & JURISDICTION -GOVERNING LAW +This Agreement shall be governed by and construed under the laws of the United Kingdom. Any disputes arising from or related to this Agreement shall be resolved in the jurisdiction of London, UK. -This Agreement shall be governed by and construed in accordance with the laws of the United Kingdom, without regard to its conflict of law principles. +9. DATA PROTECTION & PRIVACY +By using the Software, you consent to the collection, processing, and use of any personal data as required for the functionality of the Software. The specifics of data handling and storage will be outlined in the company's Privacy Policy, which can be accessed on the company's website. -ENTIRE AGREEMENT +10. ACCEPTANCE + +By downloading, installing, or using the Software software, even without explicitly clicking on an "I Agree" button or a similar mechanism, you acknowledge that you have read, understood, and agreed to be bound by the terms and conditions of this Agreement. + +11. ASSIGNMENT + +This Agreement and the rights granted hereunder may not be transferred or assigned by you but may be assigned by Licensor without restriction. + +12. SEVERABILITY + +If any provision of this Agreement is held to be unenforceable or invalid for any reason, that provision shall be reformed to the extent necessary to make it enforceable and consistent with the intent of the parties, and the remaining provisions shall remain in full force and effect. + +13. ENTIRE AGREEMENT This Agreement constitutes the entire agreement between the Licensor and the Licensee with respect to the subject matter hereof and supersedes all prior or contemporaneous understandings regarding such subject matter. No amendment to or modification of this Agreement will be binding unless in writing and signed by the Licensor. +14. DISCLAIMERS AND WARRANTIES -CONTACT INFORMATION +The Software is provided "AS IS" and "AS AVAILABLE", without warranty of any kind, either express or implied, including, without limitation, warranties of merchantability, fitness for a particular purpose, and non-infringement. Licensor does not warrant that the Software will be error-free, that access thereto will be uninterrupted, or that defects will be corrected. + +15. INDEMNIFICATION + +Licensee agrees to indemnify, defend, and hold harmless Licensor, its officers, directors, employees, agents, licensors, suppliers, and any third-party information providers from and against all claims, losses, expenses, damages, and costs, including reasonable attorneys' fees, resulting from any violation of this Agreement or any activity related to your use or misuse of the Software (including negligent or wrongful conduct). + +16. FORCE MAJEURE + +Neither party shall be in default or otherwise liable for any delay in or failure of its performance under this Agreement if such delay or failure arises by any reason of any event beyond the reasonable control of a party, including acts of God, the elements, earthquakes, floods, fires, epidemics, riots, failures or delays in transportation or communications, or any act or failure to act by the other party or such other party’s officers, employees, agents, or contractors. The parties will promptly inform and consult with each other as to any of the above causes which, in their judgment, may or could be the cause of a delay in the performance of this Agreement. + +17. CONTACT INFORMATION If you have any questions about this Agreement, please contact Stalwart Labs Ltd. at: @@ -60,4 +95,3 @@ Stalwart Labs Ltd. 128 City Road London, United Kingdom hello@stalw.art - diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 563e52eb..9c749bb3 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -10,6 +10,7 @@ nlp = { path = "../nlp" } store = { path = "../store" } directory = { path = "../directory" } jmap_proto = { path = "../jmap-proto" } +se_licensing = { path = "../se-licensing" } sieve-rs = { version = "0.5" } mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] } mail-auth = { version = "0.4" } diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 8af9b850..eb5e7290 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -8,12 +8,14 @@ use std::sync::Arc; use arc_swap::ArcSwap; use directory::{Directories, Directory}; -use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores}; +use jmap_proto::types::collection::Collection; +use se_licensing::license::LicenseValidator; +use store::{BitmapKey, BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores}; use utils::config::Config; use crate::{ expr::*, listener::tls::TlsManager, manager::config::ConfigManager, webhooks::Webhooks, Core, - Network, + Enterprise, Network, }; use self::{ @@ -116,6 +118,48 @@ impl Core { .directories .insert("*".to_string(), directory.clone()); + // SPDX-SnippetBegin + // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + // SPDX-License-Identifier: LicenseRef-SEL + + let enterprise = match config.value("enterprise.license-key").map(|key| { + LicenseValidator::new().try_parse(key).and_then(|key| { + key.into_validated_key(config.value("lookup.default.hostname").unwrap_or_default()) + }) + }) { + Some(Ok(license)) => { + match data + .get_bitmap(BitmapKey::document_ids(u32::MAX, Collection::Principal)) + .await + { + Ok(Some(bitmap)) if bitmap.len() > license.accounts as u64 => { + config.new_build_warning( + "enterprise.license-key", + format!( + "License key is valid but only allows {} accounts, found {}.", + license.accounts, + bitmap.len() + ), + ); + None + } + Err(e) => { + if !matches!(data, Store::None) { + config.new_build_error("enterprise.license-key", e.to_string()); + } + None + } + _ => Some(Enterprise { license }), + } + } + Some(Err(e)) => { + config.new_build_warning("enterprise.license-key", e.to_string()); + None + } + None => None, + }; + // SPDX-SnippetEnd + // If any of the stores are missing, disable all stores to avoid data loss if matches!(data, Store::None) || matches!(&blob.backend, BlobBackend::Store(Store::None)) @@ -154,6 +198,7 @@ impl Core { blobs: stores.blob_stores, ftss: stores.fts_stores, }, + enterprise, } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 380303eb..edc8ca3f 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -33,6 +33,7 @@ use opentelemetry_sdk::{ Resource, }; use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION}; +use se_licensing::license::LicenseKey; use sieve::Sieve; use store::LookupStore; use tokio::sync::{mpsc, oneshot}; @@ -68,6 +69,7 @@ pub struct Core { pub jmap: JmapConfig, pub imap: ImapConfig, pub web_hooks: Webhooks, + pub enterprise: Option, } #[derive(Clone)] @@ -77,6 +79,17 @@ pub struct Network { pub url: IfBlock, } +// SPDX-SnippetBegin +// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd +// SPDX-License-Identifier: LicenseRef-SEL + +#[derive(Clone)] +pub struct Enterprise { + pub license: LicenseKey, +} + +// SPDX-SnippetEnd + pub enum AuthResult { Success(T), Failure, @@ -426,7 +439,7 @@ impl Tracers { | Tracer::Otel { level, .. }) = tracer; let filter = match EnvFilter::builder().parse(format!( - "smtp={level},imap={level},jmap={level},pop3={level},store={level},common={level},utils={level},directory={level}" + "smtp={level},imap={level},jmap={level},pop3={level},store={level},common={level},utils={level},directory={level},se_common={level}" )) { Ok(filter) => { filter diff --git a/crates/enterprise/src/lib.rs b/crates/enterprise/src/lib.rs deleted file mode 100644 index c1c8295e..00000000 --- a/crates/enterprise/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: LicenseRef-SCL - */ - -pub mod license; diff --git a/crates/enterprise/src/license.rs b/crates/enterprise/src/license.rs deleted file mode 100644 index 1a98d3f7..00000000 --- a/crates/enterprise/src/license.rs +++ /dev/null @@ -1,166 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd - * - * SPDX-License-Identifier: LicenseRef-SCL - */ - -use std::time::SystemTime; - -use ring::signature::{Ed25519KeyPair, UnparsedPublicKey, ED25519}; - -use base64::{engine::general_purpose::STANDARD, Engine}; - -pub struct LicenseValidator { - pub public_key: UnparsedPublicKey>, -} - -pub struct LicenseGenerator { - key_pair: Ed25519KeyPair, -} - -#[derive(Debug)] -pub struct LicenseKey { - pub valid_to: u64, - pub valid_from: u64, - pub domain: String, - pub accounts: u32, -} - -#[derive(Debug)] -pub enum LicenseError { - Expired, - Parse, - Validation, - Decode, - Invalid, -} - -const U64_LEN: usize = std::mem::size_of::(); -const U32_LEN: usize = std::mem::size_of::(); - -impl LicenseValidator { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - LicenseValidator { - public_key: UnparsedPublicKey::new( - &ED25519, - vec![ - 118, 10, 182, 35, 89, 111, 11, 60, 154, 47, 205, 127, 107, 229, 55, 104, 72, - 54, 141, 14, 97, 219, 2, 4, 119, 143, 156, 10, 152, 216, 32, 194, - ], - ), - } - } - - pub fn validate(&self, key: impl AsRef) -> Result { - let key = STANDARD - .decode(key.as_ref()) - .map_err(|_| LicenseError::Decode)?; - let valid_from = u64::from_le_bytes( - key.get(..U64_LEN) - .ok_or(LicenseError::Parse)? - .try_into() - .unwrap(), - ); - let valid_to = u64::from_le_bytes( - key.get(U64_LEN..(U64_LEN * 2)) - .ok_or(LicenseError::Parse)? - .try_into() - .unwrap(), - ); - let accounts = u32::from_le_bytes( - key.get((U64_LEN * 2)..(U64_LEN * 2) + U32_LEN) - .ok_or(LicenseError::Parse)? - .try_into() - .unwrap(), - ); - let domain_len = u32::from_le_bytes( - key.get((U64_LEN * 2) + U32_LEN..(U64_LEN * 2) + (U32_LEN * 2)) - .ok_or(LicenseError::Parse)? - .try_into() - .unwrap(), - ) as usize; - let domain = String::from_utf8( - key.get((U64_LEN * 2) + (U32_LEN * 2)..(U64_LEN * 2) + (U32_LEN * 2) + domain_len) - .ok_or(LicenseError::Parse)? - .to_vec(), - ) - .map_err(|_| LicenseError::Parse)?; - let signature = key - .get((U64_LEN * 2) + (U32_LEN * 2) + domain_len..) - .ok_or(LicenseError::Parse)?; - - if valid_from == 0 - || valid_to == 0 - || valid_from >= valid_to - || accounts == 0 - || domain.is_empty() - { - return Err(LicenseError::Invalid); - } - - // Validate signature - self.public_key - .verify( - &key[..(U64_LEN * 2) + (U32_LEN * 2) + domain_len], - signature, - ) - .map_err(|_| LicenseError::Validation)?; - - let key = LicenseKey { - valid_from, - valid_to, - domain, - accounts, - }; - - if !key.is_expired() { - Ok(key) - } else { - Err(LicenseError::Expired) - } - } -} - -impl LicenseKey { - pub fn new(domain: String, accounts: u32, expires_in: u64) -> Self { - let now = SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs(); - LicenseKey { - valid_from: now - 300, - valid_to: now + expires_in + 300, - domain, - accounts, - } - } - - pub fn is_expired(&self) -> bool { - let now = SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs(); - - now >= self.valid_to || now < self.valid_from - } -} - -impl LicenseGenerator { - pub fn new(pkcs8_der: impl AsRef<[u8]>) -> Self { - Self { - key_pair: Ed25519KeyPair::from_pkcs8(pkcs8_der.as_ref()).unwrap(), - } - } - - pub fn generate(&self, key: LicenseKey) -> String { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&key.valid_from.to_le_bytes()); - bytes.extend_from_slice(&key.valid_to.to_le_bytes()); - bytes.extend_from_slice(&key.accounts.to_le_bytes()); - bytes.extend_from_slice(&(key.domain.len() as u32).to_le_bytes()); - bytes.extend_from_slice(key.domain.as_bytes()); - bytes.extend_from_slice(self.key_pair.sign(&bytes).as_ref()); - STANDARD.encode(&bytes) - } -} diff --git a/crates/jmap/src/services/housekeeper.rs b/crates/jmap/src/services/housekeeper.rs index f33b7ea1..361fbb9d 100644 --- a/crates/jmap/src/services/housekeeper.rs +++ b/crates/jmap/src/services/housekeeper.rs @@ -49,6 +49,7 @@ enum ActionClass { Account, Store(usize), Acme(String), + ReloadLicense, } #[derive(Default)] @@ -68,42 +69,57 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver) { tokio::spawn(async move { jmap.fts_index_queued().await; }); - let mut queue = Queue::default(); // Add all events to queue - let core_ = core.core.load(); - queue.schedule( - Instant::now() + core_.jmap.session_purge_frequency.time_to_next(), - ActionClass::Session, - ); - queue.schedule( - Instant::now() + core_.jmap.account_purge_frequency.time_to_next(), - ActionClass::Account, - ); - for (idx, schedule) in core_.storage.purge_schedules.iter().enumerate() { + let mut queue = Queue::default(); + { + let core_ = core.core.load(); queue.schedule( - Instant::now() + schedule.cron.time_to_next(), - ActionClass::Store(idx), + Instant::now() + core_.jmap.session_purge_frequency.time_to_next(), + ActionClass::Session, ); - } + queue.schedule( + Instant::now() + core_.jmap.account_purge_frequency.time_to_next(), + ActionClass::Account, + ); + for (idx, schedule) in core_.storage.purge_schedules.iter().enumerate() { + queue.schedule( + Instant::now() + schedule.cron.time_to_next(), + ActionClass::Store(idx), + ); + } - // Add all ACME renewals to heap - for provider in core_.tls.acme_providers.values() { - match core_.init_acme(provider).await { - Ok(renew_at) => { - queue.schedule( - Instant::now() + renew_at, - ActionClass::Acme(provider.id.clone()), - ); - } - Err(err) => { - tracing::error!( + // Add all ACME renewals to heap + for provider in core_.tls.acme_providers.values() { + match core_.init_acme(provider).await { + Ok(renew_at) => { + queue.schedule( + Instant::now() + renew_at, + ActionClass::Acme(provider.id.clone()), + ); + } + Err(err) => { + tracing::error!( context = "acme", event = "error", error = ?err, "Failed to initialize ACME certificate manager."); - } - }; + } + }; + } + + // SPDX-SnippetBegin + // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + // SPDX-License-Identifier: LicenseRef-SEL + + // Enterprise Edition license management + if let Some(enterprise) = &core_.enterprise { + queue.schedule( + Instant::now() + enterprise.license.expires_in(), + ActionClass::ReloadLicense, + ); + } + // SPDX-SnippetEnd } loop { @@ -327,6 +343,34 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver) { }); } } + + // SPDX-SnippetBegin + // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + // SPDX-License-Identifier: LicenseRef-SEL + ActionClass::ReloadLicense => { + match core_.reload().await { + Ok(result) => { + if let Some(new_core) = result.new_core { + if let Some(enterprise) = &new_core.enterprise { + queue.schedule( + Instant::now() + + enterprise.license.expires_in(), + ActionClass::ReloadLicense, + ); + } + + // Update core + core.core.store(new_core.into()); + + // Increment version counter + core.jmap_inner.increment_config_version(); + } + } + Err(err) => { + tracing::warn!("Failed to reload configuration: {err}",); + } + } + } // SPDX-SnippetEnd } } } diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 5a477b98..c79d78ca 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -26,6 +26,7 @@ managesieve = { path = "../managesieve" } common = { path = "../common" } directory = { path = "../directory" } utils = { path = "../utils" } +se_common = { path = "../se-common" } tokio = { version = "1.23", features = ["full"] } tracing = "0.1" diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 3403620c..ea61dc50 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -14,6 +14,7 @@ use imap::core::{ImapSessionManager, IMAP}; use jmap::{api::JmapSessionManager, services::gossip::spawn::GossiperBuilder, JMAP}; use managesieve::core::ManageSieveSessionManager; use pop3::Pop3SessionManager; +use se_common::EnterpriseCore; use smtp::core::{SmtpSessionManager, SMTP}; use tokio::sync::mpsc; use utils::wait_for_shutdown; @@ -54,6 +55,9 @@ async fn main() -> std::io::Result<()> { config.log_errors(init.guards.is_none()); config.log_warnings(init.guards.is_none()); + // Log licensing information + core.load().as_ref().log_license_details(); + // Spawn servers let (shutdown_tx, shutdown_rx) = init.servers.spawn(|server, acceptor, shutdown_rx| { match &server.protocol { diff --git a/crates/se-common/Cargo.toml b/crates/se-common/Cargo.toml new file mode 100644 index 00000000..f2ebccd8 --- /dev/null +++ b/crates/se-common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "se_common" +version = "0.8.2" +edition = "2021" +license = "LicenseRef-SEL" +resolver = "2" + +[dependencies] +common = { path = "../common" } +tracing = "0.1" + +[features] +test_mode = [] diff --git a/crates/se-common/src/lib.rs b/crates/se-common/src/lib.rs new file mode 100644 index 00000000..ee05adec --- /dev/null +++ b/crates/se-common/src/lib.rs @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: LicenseRef-SEL + * + * This file is subject to the Stalwart Enterprise License Agreement (SEL) and + * is not open source software. It must not be modified or distributed without + * explicit permission from Stalwart Labs Ltd. + * Unauthorized use, modification, or distribution is strictly prohibited. + */ + +use common::Core; + +pub trait EnterpriseCore { + fn is_enterprise_edition(&self) -> bool; + fn log_license_details(&self); + fn licensed_accounts(&self) -> u32; +} + +impl EnterpriseCore for Core { + // WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED + // Any attempt to modify, bypass, or disable this license validation mechanism + // constitutes a severe violation of the Stalwart Enterprise License Agreement. + // Such actions may result in immediate termination of your license, legal action, + // and substantial financial penalties. Stalwart Labs Ltd. actively monitors for + // unauthorized modifications and will pursue all available legal remedies against + // violators to the fullest extent of the law, including but not limited to claims + // for copyright infringement, breach of contract, and fraud. + + fn is_enterprise_edition(&self) -> bool { + self.enterprise + .as_ref() + .map_or(false, |e| !e.license.is_expired()) + } + + fn licensed_accounts(&self) -> u32 { + self.enterprise.as_ref().map_or(0, |e| e.license.accounts) + } + + fn log_license_details(&self) { + if let Some(enterprise) = &self.enterprise { + tracing::info!( + licensed_to = enterprise.license.hostname, + valid_from = enterprise.license.valid_from, + valid_to = enterprise.license.valid_to, + accounts = enterprise.license.accounts, + "Stalwart Enterprise Edition license key is valid", + ); + } + } +} diff --git a/crates/enterprise/Cargo.toml b/crates/se-licensing/Cargo.toml similarity index 88% rename from crates/enterprise/Cargo.toml rename to crates/se-licensing/Cargo.toml index 5aa0dd83..50be1ecd 100644 --- a/crates/enterprise/Cargo.toml +++ b/crates/se-licensing/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "enterprise" +name = "se_licensing" version = "0.8.2" edition = "2021" license = "LicenseRef-SEL" diff --git a/crates/se-licensing/src/lib.rs b/crates/se-licensing/src/lib.rs new file mode 100644 index 00000000..180ec6b5 --- /dev/null +++ b/crates/se-licensing/src/lib.rs @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: LicenseRef-SEL + * + * This file is subject to the Stalwart Enterprise License Agreement (SEL) and + * is not open source software. It must not be modified or distributed without + * explicit permission from Stalwart Labs Ltd. + * Unauthorized use, modification, or distribution is strictly prohibited. + */ + +pub mod license; diff --git a/crates/se-licensing/src/license.rs b/crates/se-licensing/src/license.rs new file mode 100644 index 00000000..f1ff3f2c --- /dev/null +++ b/crates/se-licensing/src/license.rs @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: LicenseRef-SEL + * + * This file is subject to the Stalwart Enterprise License Agreement (SEL) and + * is not open source software. It must not be modified or distributed without + * explicit permission from Stalwart Labs Ltd. + * Unauthorized use, modification, or distribution is strictly prohibited. + */ + +/* + * WARNING: TAMPERING WITH THIS CODE IS STRICTLY PROHIBITED + * Any attempt to modify, bypass, or disable the license validation mechanism + * constitutes a severe violation of the Stalwart Enterprise License Agreement. + * Such actions may result in immediate termination of your license, legal action, + * and substantial financial penalties. Stalwart Labs Ltd. actively monitors for + * unauthorized modifications and will pursue all available legal remedies against + * violators to the fullest extent of the law, including but not limited to claims + * for copyright infringement, breach of contract, and fraud. + */ + +use std::{ + fmt::{Display, Formatter}, + time::{Duration, SystemTime}, +}; + +use ring::signature::{Ed25519KeyPair, UnparsedPublicKey, ED25519}; + +use base64::{engine::general_purpose::STANDARD, Engine}; + +pub struct LicenseValidator { + public_key: UnparsedPublicKey>, +} + +pub struct LicenseGenerator { + key_pair: Ed25519KeyPair, +} + +#[derive(Debug, Clone)] +pub struct LicenseKey { + pub valid_to: u64, + pub valid_from: u64, + pub hostname: String, + pub accounts: u32, +} + +#[derive(Debug)] +pub enum LicenseError { + Expired, + HostnameMismatch { issued_to: String, current: String }, + Parse, + Validation, + Decode, + InvalidParameters, +} + +const U64_LEN: usize = std::mem::size_of::(); +const U32_LEN: usize = std::mem::size_of::(); + +impl LicenseValidator { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + LicenseValidator { + public_key: UnparsedPublicKey::new( + &ED25519, + vec![ + 118, 10, 182, 35, 89, 111, 11, 60, 154, 47, 205, 127, 107, 229, 55, 104, 72, + 54, 141, 14, 97, 219, 2, 4, 119, 143, 156, 10, 152, 216, 32, 194, + ], + ), + } + } + + pub fn try_parse(&self, key: impl AsRef) -> Result { + let key = STANDARD + .decode(key.as_ref()) + .map_err(|_| LicenseError::Decode)?; + let valid_from = u64::from_le_bytes( + key.get(..U64_LEN) + .ok_or(LicenseError::Parse)? + .try_into() + .unwrap(), + ); + let valid_to = u64::from_le_bytes( + key.get(U64_LEN..(U64_LEN * 2)) + .ok_or(LicenseError::Parse)? + .try_into() + .unwrap(), + ); + let accounts = u32::from_le_bytes( + key.get((U64_LEN * 2)..(U64_LEN * 2) + U32_LEN) + .ok_or(LicenseError::Parse)? + .try_into() + .unwrap(), + ); + let hostname_len = u32::from_le_bytes( + key.get((U64_LEN * 2) + U32_LEN..(U64_LEN * 2) + (U32_LEN * 2)) + .ok_or(LicenseError::Parse)? + .try_into() + .unwrap(), + ) as usize; + let hostname = String::from_utf8( + key.get((U64_LEN * 2) + (U32_LEN * 2)..(U64_LEN * 2) + (U32_LEN * 2) + hostname_len) + .ok_or(LicenseError::Parse)? + .to_vec(), + ) + .map_err(|_| LicenseError::Parse)?; + let signature = key + .get((U64_LEN * 2) + (U32_LEN * 2) + hostname_len..) + .ok_or(LicenseError::Parse)?; + + if valid_from == 0 + || valid_to == 0 + || valid_from >= valid_to + || accounts == 0 + || hostname.is_empty() + { + return Err(LicenseError::InvalidParameters); + } + + // Validate signature + self.public_key + .verify( + &key[..(U64_LEN * 2) + (U32_LEN * 2) + hostname_len], + signature, + ) + .map_err(|_| LicenseError::Validation)?; + + let key = LicenseKey { + valid_from, + valid_to, + hostname, + accounts, + }; + + if !key.is_expired() { + Ok(key) + } else { + Err(LicenseError::Expired) + } + } +} + +impl LicenseKey { + pub fn new(hostname: String, accounts: u32, expires_in: u64) -> Self { + let now = SystemTime::UNIX_EPOCH + .elapsed() + .unwrap_or_default() + .as_secs(); + LicenseKey { + valid_from: now - 300, + valid_to: now + expires_in + 300, + hostname, + accounts, + } + } + + pub fn expires_in(&self) -> Duration { + Duration::from_secs( + self.valid_to.saturating_sub( + SystemTime::UNIX_EPOCH + .elapsed() + .unwrap_or_default() + .as_secs(), + ), + ) + } + + pub fn is_expired(&self) -> bool { + let now = SystemTime::UNIX_EPOCH + .elapsed() + .unwrap_or_default() + .as_secs(); + now >= self.valid_to || now < self.valid_from + } + + pub fn into_validated_key(self, hostname: impl AsRef) -> Result { + if self.hostname != hostname.as_ref() { + Err(LicenseError::HostnameMismatch { + issued_to: self.hostname.clone(), + current: hostname.as_ref().to_string(), + }) + } else { + Ok(self) + } + } +} + +impl LicenseGenerator { + pub fn new(pkcs8_der: impl AsRef<[u8]>) -> Self { + Self { + key_pair: Ed25519KeyPair::from_pkcs8(pkcs8_der.as_ref()).unwrap(), + } + } + + pub fn generate(&self, key: LicenseKey) -> String { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&key.valid_from.to_le_bytes()); + bytes.extend_from_slice(&key.valid_to.to_le_bytes()); + bytes.extend_from_slice(&key.accounts.to_le_bytes()); + bytes.extend_from_slice(&(key.hostname.len() as u32).to_le_bytes()); + bytes.extend_from_slice(key.hostname.as_bytes()); + bytes.extend_from_slice(self.key_pair.sign(&bytes).as_ref()); + STANDARD.encode(&bytes) + } +} + +impl Display for LicenseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + LicenseError::Expired => write!(f, "License is expired"), + LicenseError::Parse => write!(f, "Failed to parse license key"), + LicenseError::Validation => write!(f, "Failed to validate license key"), + LicenseError::Decode => write!(f, "Failed to decode license key"), + LicenseError::InvalidParameters => write!(f, "Invalid license key parameters"), + LicenseError::HostnameMismatch { issued_to, current } => { + write!( + f, + "License issued to {} does not match {}", + issued_to, current + ) + } + } + } +} + +/* + +use rustls::sign::CertifiedKey; +use webpki::TrustAnchor; +use x509_parser::{certificate::X509Certificate, prelude::FromDer}; + + +fn validate_certificate(key: &CertifiedKey) -> Result<(), Box> { + let cert_der = key.end_entity_cert()?.as_ref(); + + webpki::EndEntityCert::try_from(cert_der)?.verify_is_valid_tls_server_cert( + &[ + &webpki::ECDSA_P256_SHA256, + &webpki::ECDSA_P256_SHA384, + &webpki::ECDSA_P384_SHA256, + &webpki::ECDSA_P384_SHA384, + &webpki::ED25519, + &webpki::RSA_PKCS1_2048_8192_SHA256, + &webpki::RSA_PKCS1_2048_8192_SHA384, + &webpki::RSA_PKCS1_2048_8192_SHA512, + &webpki::RSA_PKCS1_3072_8192_SHA384, + &webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY, + &webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY, + &webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY, + ], + &webpki::TlsServerTrustAnchors( + webpki_roots::TLS_SERVER_ROOTS + .iter() + .map(|ta| TrustAnchor { + subject: ta.subject.as_ref(), + spki: ta.subject_public_key_info.as_ref(), + name_constraints: ta.name_constraints.as_ref().map(|nc| nc.as_ref()), + }) + .collect::>() + .as_slice(), + ), + &key.cert + .iter() + .skip(1) + .map(|der| der.as_ref()) + .collect::>(), + webpki::Time::try_from(SystemTime::now())?, + )?; + + // Additional checks + let x509 = X509Certificate::from_der(cert_der)?.1; + + // Check if self-signed + if x509.issuer() == x509.subject() { + return Err("Certificate is self-signed".into()); + } + + // Check expiration + let not_before = x509.validity().not_before.timestamp(); + let not_after = x509.validity().not_after.timestamp(); + let now = SystemTime::UNIX_EPOCH + .elapsed() + .unwrap_or_default() + .as_secs() as i64; + + if now < not_before || now > not_after { + Err("Certificate is expired or not yet valid".into()) + } else { + Ok(()) + } +} + + +*/