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}