rust_license_key/
lib.rs

1//! # rust-license-key
2//!
3//! A production-grade Rust library for creating and validating offline software
4//! licenses using Ed25519 cryptography.
5//!
6//! ## Overview
7//!
8//! `rust-license-key` provides a secure, offline licensing system for software applications.
9//! It uses Ed25519 digital signatures to create tamper-proof licenses that can be
10//! verified without any network access.
11//!
12//! ### Key Features
13//!
14//! - **Asymmetric Cryptography**: Licenses are signed with a private key and verified
15//!   with a public key. The client never has access to the signing key.
16//! - **Offline Verification**: No network calls required for license validation.
17//! - **Rich Constraints**: Support for expiration dates, feature flags, hostname
18//!   restrictions, version limits, and custom constraints.
19//! - **Tamper-Proof**: Any modification to the license invalidates the signature.
20//! - **Human-Readable**: License payloads are JSON, making debugging easy.
21//! - **Versioned Format**: Built-in version checking for forward compatibility.
22//!
23//! ## Quick Start
24//!
25//! ### Publisher Side: Creating Licenses
26//!
27//! ```rust
28//! use rust_license_key::prelude::*;
29//! use chrono::Duration;
30//!
31//! // Generate a key pair (do this once and store securely)
32//! let key_pair = KeyPair::generate().expect("Key generation failed");
33//!
34//! // Save these keys:
35//! // - Private key (keep secret!): key_pair.private_key_base64()
36//! // - Public key (embed in app): key_pair.public_key_base64()
37//!
38//! // Create a license
39//! let license_json = LicenseBuilder::new()
40//!     .license_id("LIC-2024-001")
41//!     .customer_id("ACME-CORP")
42//!     .customer_name("Acme Corporation")
43//!     .expires_in(Duration::days(365))
44//!     .allowed_features(vec!["basic", "premium", "analytics"])
45//!     .max_connections(100)
46//!     .build_and_sign_to_json(&key_pair)
47//!     .expect("License creation failed");
48//!
49//! // Send license_json to the customer
50//! println!("{}", license_json);
51//! ```
52//!
53//! ### Client Side: Validating Licenses
54//!
55//! ```rust
56//! use rust_license_key::prelude::*;
57//! use semver::Version;
58//!
59//! // The public key embedded in your application
60//! let public_key_base64 = "..."; // Your public key here
61//!
62//! // The license file content
63//! let license_json = "..."; // Customer's license file
64//!
65//! // Create a validator
66//! // let validator = LicenseValidator::from_public_key_base64(public_key_base64)
67//! //     .expect("Invalid public key");
68//!
69//! // Set up validation context
70//! // let context = ValidationContext::new()
71//! //     .with_hostname("myserver.example.com")
72//! //     .with_software_version(Version::new(1, 2, 3))
73//! //     .with_feature("premium");
74//!
75//! // Validate the license
76//! // let result = validator.validate_json(&license_json, &context)
77//! //     .expect("Validation error");
78//!
79//! // if result.is_valid {
80//! //     println!("License valid! Days remaining: {:?}", result.days_remaining());
81//! //     if result.is_feature_allowed("premium") {
82//! //         println!("Premium features enabled!");
83//! //     }
84//! // } else {
85//! //     for failure in &result.failures {
86//! //         println!("Validation failed: {}", failure.message);
87//! //     }
88//! // }
89//! ```
90//!
91//! ## Module Organization
92//!
93//! - [`crypto`] - Ed25519 key generation, signing, and verification.
94//! - [`builder`] - Fluent API for creating and signing licenses.
95//! - [`parser`] - Loading and decoding signed licenses.
96//! - [`validator`] - Comprehensive license validation.
97//! - [`models`] - Data structures for licenses, constraints, and results.
98//! - [`error`] - Error types and validation failure information.
99//!
100//! ## Security Considerations
101//!
102//! - **Private Key Security**: The private key must be kept secret and should only
103//!   exist on the license generation server. Never include it in client applications.
104//! - **Public Key Distribution**: The public key can be safely embedded in client
105//!   applications. It can only verify signatures, not create them.
106//! - **No Encryption**: License payloads are signed but not encrypted. Do not store
107//!   sensitive information in license metadata.
108//! - **Offline Only**: This library does not provide license revocation or online
109//!   validation. For these features, implement a separate online check.
110
111#![warn(missing_docs)]
112#![warn(rustdoc::missing_crate_level_docs)]
113#![deny(unsafe_code)]
114
115// =============================================================================
116// Module Declarations
117// =============================================================================
118
119pub mod builder;
120pub mod crypto;
121pub mod error;
122pub mod models;
123pub mod parser;
124pub mod validator;
125
126// =============================================================================
127// Prelude - Common Imports
128// =============================================================================
129
130/// Convenient re-exports of the most commonly used types.
131///
132/// Import this module to get quick access to the main API:
133///
134/// ```rust
135/// use rust_license_key::prelude::*;
136/// ```
137pub mod prelude {
138    // Crypto types
139    pub use crate::crypto::{generate_key_pair_base64, KeyPair, PublicKey};
140
141    // Builder
142    pub use crate::builder::LicenseBuilder;
143
144    // Parser
145    pub use crate::parser::{parse_license, LicenseParser};
146
147    // Validator
148    pub use crate::validator::{
149        is_feature_allowed, is_license_valid, validate_license, LicenseValidator,
150    };
151
152    // Models
153    pub use crate::models::{
154        LicenseConstraints, LicensePayload, SignedLicense, ValidationContext, ValidationResult,
155        LICENSE_FORMAT_VERSION,
156    };
157
158    // Errors
159    pub use crate::error::{LicenseError, Result, ValidationFailure, ValidationFailureType};
160}
161
162// =============================================================================
163// Top-Level Re-exports for Convenience
164// =============================================================================
165
166// Re-export key types at the crate root for convenience
167pub use builder::LicenseBuilder;
168pub use crypto::{generate_key_pair_base64, KeyPair, PublicKey};
169pub use error::{LicenseError, Result};
170pub use models::{
171    LicenseConstraints, LicensePayload, SignedLicense, ValidationContext, ValidationResult,
172};
173pub use parser::{parse_license, LicenseParser};
174pub use validator::{is_feature_allowed, is_license_valid, validate_license, LicenseValidator};
175
176// =============================================================================
177// Integration Tests as Doctests
178// =============================================================================
179
180#[cfg(test)]
181mod integration_tests {
182    use super::*;
183    use chrono::Duration;
184    use semver::Version;
185
186    /// Complete end-to-end workflow test.
187    #[test]
188    fn test_complete_workflow() {
189        // === PUBLISHER SIDE ===
190
191        // 1. Generate key pair
192        let key_pair = KeyPair::generate().expect("Key generation should succeed");
193        let public_key_base64 = key_pair.public_key_base64();
194
195        // 2. Create a license with various constraints
196        let license_json = LicenseBuilder::new()
197            .license_id("E2E-TEST-001")
198            .customer_id("INTEGRATION-TEST")
199            .customer_name("Integration Test Customer")
200            .expires_in(Duration::days(365))
201            .allowed_features(vec!["basic", "premium", "analytics"])
202            .denied_feature("experimental")
203            .max_connections(50)
204            .allowed_hostname("test.example.com")
205            .minimum_version(Version::new(1, 0, 0))
206            .maximum_version(Version::new(3, 0, 0))
207            .metadata("department", serde_json::json!("Engineering"))
208            .custom_constraint("max_users", serde_json::json!(100))
209            .build_and_sign_to_json(&key_pair)
210            .expect("License creation should succeed");
211
212        // === CLIENT SIDE ===
213
214        // 3. Create validator with public key
215        let validator = LicenseValidator::from_public_key_base64(&public_key_base64)
216            .expect("Validator creation should succeed");
217
218        // 4. Create validation context
219        let context = ValidationContext::new()
220            .with_hostname("test.example.com")
221            .with_software_version(Version::new(2, 0, 0))
222            .with_connection_count(25)
223            .with_feature("premium")
224            .with_feature("analytics");
225
226        // 5. Validate the license
227        let result = validator
228            .validate_json(&license_json, &context)
229            .expect("Validation should not error");
230
231        // 6. Verify the results
232        assert!(result.is_valid, "License should be valid");
233        assert!(result.is_active(), "License should be active");
234        assert!(result.failures.is_empty(), "Should have no failures");
235
236        // Check remaining time
237        let days_remaining = result.days_remaining().expect("Should have days remaining");
238        assert!(
239            days_remaining >= 364,
240            "Should have approximately 365 days remaining"
241        );
242
243        // Check feature access
244        assert!(result.is_feature_allowed("premium"));
245        assert!(result.is_feature_allowed("analytics"));
246        assert!(!result.is_feature_allowed("experimental")); // Denied
247
248        // Check payload contents
249        let payload = result.payload.expect("Should have payload");
250        assert_eq!(payload.license_id, "E2E-TEST-001");
251        assert_eq!(payload.customer_id, "INTEGRATION-TEST");
252        assert_eq!(
253            payload.customer_name.as_deref(),
254            Some("Integration Test Customer")
255        );
256
257        // Check metadata
258        let metadata = payload.metadata.expect("Should have metadata");
259        assert_eq!(metadata["department"], serde_json::json!("Engineering"));
260    }
261
262    /// Test that tampering with the license is detected.
263    #[test]
264    fn test_tampering_detection() {
265        let key_pair = KeyPair::generate().expect("Key generation should succeed");
266
267        let license_json = LicenseBuilder::new()
268            .license_id("TAMPER-TEST")
269            .customer_id("TAMPER-CUST")
270            .build_and_sign_to_json(&key_pair)
271            .expect("License creation should succeed");
272
273        // Parse the license JSON
274        let mut signed: SignedLicense =
275            serde_json::from_str(&license_json).expect("Should parse JSON");
276
277        // Tamper with the payload
278        signed.encoded_payload = signed.encoded_payload.replace('A', "B");
279
280        let tampered_json = serde_json::to_string(&signed).expect("Should serialize");
281
282        // Try to validate
283        let validator = LicenseValidator::new(key_pair.public_key());
284        let result = validator
285            .validate_json(&tampered_json, &ValidationContext::new())
286            .expect("Should return result");
287
288        assert!(!result.is_valid, "Tampered license should be invalid");
289    }
290
291    /// Test convenience functions.
292    #[test]
293    fn test_convenience_functions() {
294        let key_pair = KeyPair::generate().expect("Key generation should succeed");
295        let public_key_base64 = key_pair.public_key_base64();
296
297        let license_json = LicenseBuilder::new()
298            .license_id("CONVENIENCE-TEST")
299            .customer_id("CONVENIENCE-CUST")
300            .allowed_feature("premium")
301            .build_and_sign_to_json(&key_pair)
302            .expect("License creation should succeed");
303
304        // Test is_license_valid
305        assert!(is_license_valid(&license_json, &public_key_base64));
306
307        // Test is_feature_allowed
308        assert!(is_feature_allowed(
309            &license_json,
310            &public_key_base64,
311            "premium"
312        ));
313        assert!(!is_feature_allowed(
314            &license_json,
315            &public_key_base64,
316            "enterprise"
317        ));
318
319        // Test parse_license
320        let payload =
321            parse_license(&license_json, &public_key_base64).expect("Should parse license");
322        assert_eq!(payload.license_id, "CONVENIENCE-TEST");
323    }
324}