rust_license_key/
validator.rs

1//! License validation logic.
2//!
3//! This module provides comprehensive validation of license payloads against
4//! a given runtime context. It checks temporal constraints, feature restrictions,
5//! host limitations, version compatibility, and custom constraints.
6//!
7//! # Validation Philosophy
8//!
9//! Validation is strict and deterministic:
10//! - Any constraint violation results in validation failure.
11//! - All failures are explicitly reported with detailed information.
12//! - The validation result provides complete status information.
13
14use chrono::{DateTime, Utc};
15
16use crate::crypto::PublicKey;
17use crate::error::{LicenseError, Result, ValidationFailure, ValidationFailureType};
18use crate::models::{LicensePayload, ValidationContext, ValidationResult};
19use crate::parser::LicenseParser;
20
21// =============================================================================
22// License Validator
23// =============================================================================
24
25/// Validator for checking license constraints against runtime context.
26///
27/// The validator performs comprehensive checking of all license constraints
28/// and produces detailed validation results suitable for logging and
29/// application logic.
30///
31/// # Example
32///
33/// ```
34/// use rust_license_key::validator::LicenseValidator;
35/// use rust_license_key::models::ValidationContext;
36/// use rust_license_key::crypto::PublicKey;
37/// use semver::Version;
38///
39/// // Create validator with the embedded public key
40/// // let public_key = PublicKey::from_base64("...").unwrap();
41/// // let validator = LicenseValidator::new(public_key);
42///
43/// // Set up the validation context
44/// // let context = ValidationContext::new()
45/// //     .with_hostname("myserver.example.com")
46/// //     .with_software_version(Version::new(1, 2, 3))
47/// //     .with_feature("premium");
48///
49/// // Validate the license
50/// // let result = validator.validate_json(&license_json, &context).unwrap();
51/// // if result.is_valid {
52/// //     println!("License is valid for {} more days", result.days_remaining().unwrap_or(i64::MAX));
53/// // }
54/// ```
55#[derive(Debug, Clone)]
56pub struct LicenseValidator {
57    /// The parser used to load and verify licenses.
58    parser: LicenseParser,
59}
60
61impl LicenseValidator {
62    /// Creates a new validator with the given public key.
63    ///
64    /// # Arguments
65    ///
66    /// * `public_key` - The publisher's public key for signature verification.
67    pub fn new(public_key: PublicKey) -> Self {
68        Self {
69            parser: LicenseParser::new(public_key),
70        }
71    }
72
73    /// Creates a new validator from a base64-encoded public key.
74    ///
75    /// # Arguments
76    ///
77    /// * `public_key_base64` - The base64-encoded public key string.
78    pub fn from_public_key_base64(public_key_base64: &str) -> Result<Self> {
79        let parser = LicenseParser::from_public_key_base64(public_key_base64)?;
80        Ok(Self { parser })
81    }
82
83    /// Validates a license from a JSON string.
84    ///
85    /// This is the primary validation method. It:
86    /// 1. Parses and verifies the license signature.
87    /// 2. Checks all constraints against the provided context.
88    /// 3. Returns a comprehensive validation result.
89    ///
90    /// # Arguments
91    ///
92    /// * `license_json` - The JSON string containing the signed license.
93    /// * `context` - The runtime context to validate against.
94    ///
95    /// # Returns
96    ///
97    /// A `ValidationResult` indicating whether the license is valid and
98    /// providing detailed information about any failures.
99    pub fn validate_json(
100        &self,
101        license_json: &str,
102        context: &ValidationContext,
103    ) -> Result<ValidationResult> {
104        // First, parse and verify the license
105        let payload = match self.parser.parse_json(license_json) {
106            Ok(p) => p,
107            Err(LicenseError::InvalidSignature) => {
108                return Ok(ValidationResult::failure(vec![ValidationFailure::new(
109                    ValidationFailureType::InvalidSignature,
110                    "License signature is invalid or has been tampered with",
111                )]));
112            }
113            Err(LicenseError::UnsupportedLicenseVersion { found, supported }) => {
114                return Ok(ValidationResult::failure(vec![ValidationFailure::new(
115                    ValidationFailureType::UnsupportedVersion,
116                    format!("License version {} is not supported ({})", found, supported),
117                )]));
118            }
119            Err(e) => return Err(e),
120        };
121
122        // Validate the payload against the context
123        Ok(self.validate_payload(&payload, context))
124    }
125
126    /// Validates a license payload directly.
127    ///
128    /// Use this when you already have a parsed and verified payload.
129    /// This method assumes the signature has already been verified.
130    ///
131    /// # Arguments
132    ///
133    /// * `payload` - The license payload to validate.
134    /// * `context` - The runtime context to validate against.
135    ///
136    /// # Returns
137    ///
138    /// A `ValidationResult` with detailed status information.
139    pub fn validate_payload(
140        &self,
141        payload: &LicensePayload,
142        context: &ValidationContext,
143    ) -> ValidationResult {
144        let mut failures = Vec::new();
145
146        // Determine the current time for temporal checks
147        let current_time = context.current_time.unwrap_or_else(Utc::now);
148
149        // Check expiration
150        self.check_expiration(payload, current_time, &mut failures);
151
152        // Check valid_from
153        self.check_valid_from(payload, current_time, &mut failures);
154
155        // Check hostname constraint
156        self.check_hostname(payload, context, &mut failures);
157
158        // Check machine ID constraint
159        self.check_machine_id(payload, context, &mut failures);
160
161        // Check version constraint
162        self.check_version(payload, context, &mut failures);
163
164        // Check connection limit
165        self.check_connection_limit(payload, context, &mut failures);
166
167        // Check requested features
168        self.check_features(payload, context, &mut failures);
169
170        // Build the result
171        if failures.is_empty() {
172            ValidationResult::success(payload.clone())
173        } else {
174            // Create a partial result with the payload for informational purposes
175            let mut result = ValidationResult::success(payload.clone());
176            result.is_valid = false;
177            result.failures = failures;
178            result
179        }
180    }
181
182    /// Checks the license expiration constraint.
183    fn check_expiration(
184        &self,
185        payload: &LicensePayload,
186        current_time: DateTime<Utc>,
187        failures: &mut Vec<ValidationFailure>,
188    ) {
189        if let Some(expiration) = payload.constraints.expiration_date {
190            if current_time > expiration {
191                failures.push(
192                    ValidationFailure::new(
193                        ValidationFailureType::Expired,
194                        format!(
195                            "License expired on {}",
196                            expiration.format("%Y-%m-%d %H:%M:%S UTC")
197                        ),
198                    )
199                    .with_context(format!(
200                        "Current time: {}",
201                        current_time.format("%Y-%m-%d %H:%M:%S UTC")
202                    )),
203                );
204            }
205        }
206    }
207
208    /// Checks the valid_from constraint (license activation date).
209    fn check_valid_from(
210        &self,
211        payload: &LicensePayload,
212        current_time: DateTime<Utc>,
213        failures: &mut Vec<ValidationFailure>,
214    ) {
215        if let Some(valid_from) = payload.constraints.valid_from {
216            if current_time < valid_from {
217                failures.push(
218                    ValidationFailure::new(
219                        ValidationFailureType::NotYetValid,
220                        format!(
221                            "License becomes valid on {}",
222                            valid_from.format("%Y-%m-%d %H:%M:%S UTC")
223                        ),
224                    )
225                    .with_context(format!(
226                        "Current time: {}",
227                        current_time.format("%Y-%m-%d %H:%M:%S UTC")
228                    )),
229                );
230            }
231        }
232    }
233
234    /// Checks the hostname constraint.
235    fn check_hostname(
236        &self,
237        payload: &LicensePayload,
238        context: &ValidationContext,
239        failures: &mut Vec<ValidationFailure>,
240    ) {
241        if let Some(ref hostname) = context.current_hostname {
242            if !payload.constraints.is_hostname_allowed(hostname) {
243                let allowed = payload
244                    .constraints
245                    .allowed_hostnames
246                    .as_ref()
247                    .map(|h| h.iter().cloned().collect::<Vec<_>>().join(", "))
248                    .unwrap_or_else(|| "(none specified)".to_string());
249
250                failures.push(
251                    ValidationFailure::new(
252                        ValidationFailureType::HostnameConstraint,
253                        format!("Hostname '{}' is not allowed by this license", hostname),
254                    )
255                    .with_context(format!("Allowed hostnames: {}", allowed)),
256                );
257            }
258        }
259    }
260
261    /// Checks the machine ID constraint.
262    fn check_machine_id(
263        &self,
264        payload: &LicensePayload,
265        context: &ValidationContext,
266        failures: &mut Vec<ValidationFailure>,
267    ) {
268        if let Some(ref machine_id) = context.current_machine_id {
269            if !payload.constraints.is_machine_id_allowed(machine_id) {
270                failures.push(ValidationFailure::new(
271                    ValidationFailureType::MachineIdConstraint,
272                    format!(
273                        "Machine identifier '{}' is not allowed by this license",
274                        machine_id
275                    ),
276                ));
277            }
278        }
279    }
280
281    /// Checks the software version constraint.
282    fn check_version(
283        &self,
284        payload: &LicensePayload,
285        context: &ValidationContext,
286        failures: &mut Vec<ValidationFailure>,
287    ) {
288        if let Some(ref version) = context.current_software_version {
289            if let Err(reason) = payload.constraints.check_version_compatibility(version) {
290                failures.push(
291                    ValidationFailure::new(
292                        ValidationFailureType::VersionConstraint,
293                        format!("Software version {} is not compatible", version),
294                    )
295                    .with_context(reason),
296                );
297            }
298        }
299    }
300
301    /// Checks the connection limit constraint.
302    fn check_connection_limit(
303        &self,
304        payload: &LicensePayload,
305        context: &ValidationContext,
306        failures: &mut Vec<ValidationFailure>,
307    ) {
308        if let (Some(max_allowed), Some(current_count)) = (
309            payload.constraints.max_connections,
310            context.current_connection_count,
311        ) {
312            if current_count >= max_allowed {
313                failures.push(
314                    ValidationFailure::new(
315                        ValidationFailureType::ConnectionLimit,
316                        format!(
317                            "Connection limit exceeded: {} connections in use, maximum {} allowed",
318                            current_count, max_allowed
319                        ),
320                    )
321                    .with_context(format!(
322                        "Attempting to use connection {} of {} allowed",
323                        current_count + 1,
324                        max_allowed
325                    )),
326                );
327            }
328        }
329    }
330
331    /// Checks that all requested features are allowed.
332    fn check_features(
333        &self,
334        payload: &LicensePayload,
335        context: &ValidationContext,
336        failures: &mut Vec<ValidationFailure>,
337    ) {
338        for feature in &context.requested_features {
339            if !payload.constraints.is_feature_allowed(feature) {
340                // Determine why the feature is not allowed
341                let reason = if payload
342                    .constraints
343                    .denied_features
344                    .as_ref()
345                    .map(|d| d.contains(feature))
346                    .unwrap_or(false)
347                {
348                    "feature is explicitly denied"
349                } else {
350                    "feature is not in the allowed list"
351                };
352
353                failures.push(
354                    ValidationFailure::new(
355                        ValidationFailureType::FeatureConstraint,
356                        format!("Feature '{}' is not allowed", feature),
357                    )
358                    .with_context(reason.to_string()),
359                );
360            }
361        }
362    }
363
364    /// Returns a reference to the underlying parser.
365    pub fn parser(&self) -> &LicenseParser {
366        &self.parser
367    }
368}
369
370// =============================================================================
371// Convenience Functions
372// =============================================================================
373
374/// Validates a license using a base64-encoded public key.
375///
376/// This is a convenience function for one-shot license validation.
377/// For multiple validations, create a `LicenseValidator` instance.
378///
379/// # Arguments
380///
381/// * `license_json` - The JSON string containing the signed license.
382/// * `public_key_base64` - The base64-encoded public key.
383/// * `context` - The runtime context to validate against.
384///
385/// # Returns
386///
387/// A `ValidationResult` with detailed status information.
388pub fn validate_license(
389    license_json: &str,
390    public_key_base64: &str,
391    context: &ValidationContext,
392) -> Result<ValidationResult> {
393    let validator = LicenseValidator::from_public_key_base64(public_key_base64)?;
394    validator.validate_json(license_json, context)
395}
396
397/// Performs a quick check to see if a license is currently valid.
398///
399/// This function only checks signature validity and expiration.
400/// For full validation, use `validate_license` or `LicenseValidator`.
401///
402/// # Arguments
403///
404/// * `license_json` - The JSON string containing the signed license.
405/// * `public_key_base64` - The base64-encoded public key.
406///
407/// # Returns
408///
409/// `true` if the license is valid and not expired, `false` otherwise.
410pub fn is_license_valid(license_json: &str, public_key_base64: &str) -> bool {
411    let context = ValidationContext::new();
412    validate_license(license_json, public_key_base64, &context)
413        .map(|r| r.is_valid)
414        .unwrap_or(false)
415}
416
417/// Checks if a specific feature is allowed by a license.
418///
419/// # Arguments
420///
421/// * `license_json` - The JSON string containing the signed license.
422/// * `public_key_base64` - The base64-encoded public key.
423/// * `feature` - The feature to check.
424///
425/// # Returns
426///
427/// `true` if the license is valid and the feature is allowed.
428pub fn is_feature_allowed(license_json: &str, public_key_base64: &str, feature: &str) -> bool {
429    let context = ValidationContext::new().with_feature(feature);
430    validate_license(license_json, public_key_base64, &context)
431        .map(|r| r.is_valid)
432        .unwrap_or(false)
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::builder::LicenseBuilder;
439    use crate::crypto::KeyPair;
440    use chrono::Duration;
441    use semver::Version;
442
443    fn create_key_pair() -> KeyPair {
444        KeyPair::generate().expect("Key generation should succeed")
445    }
446
447    fn create_basic_license(key_pair: &KeyPair) -> String {
448        LicenseBuilder::new()
449            .license_id("TEST-001")
450            .customer_id("CUST-001")
451            .expires_in(Duration::days(30))
452            .build_and_sign_to_json(key_pair)
453            .expect("Should create license")
454    }
455
456    #[test]
457    fn test_validate_valid_license() {
458        let key_pair = create_key_pair();
459        let license_json = create_basic_license(&key_pair);
460
461        let validator = LicenseValidator::new(key_pair.public_key());
462        let context = ValidationContext::new();
463
464        let result = validator
465            .validate_json(&license_json, &context)
466            .expect("Should validate");
467
468        assert!(result.is_valid);
469        assert!(result.failures.is_empty());
470        assert!(result.payload.is_some());
471    }
472
473    #[test]
474    fn test_validate_expired_license() {
475        let key_pair = create_key_pair();
476
477        // Create an already-expired license
478        let license_json = LicenseBuilder::new()
479            .license_id("TEST-001")
480            .customer_id("CUST-001")
481            .expires_in(Duration::days(-1)) // Expired yesterday
482            .build_and_sign_to_json(&key_pair)
483            .expect("Should create license");
484
485        let validator = LicenseValidator::new(key_pair.public_key());
486        let context = ValidationContext::new();
487
488        let result = validator
489            .validate_json(&license_json, &context)
490            .expect("Should validate");
491
492        assert!(!result.is_valid);
493        assert_eq!(result.failures.len(), 1);
494        assert_eq!(
495            result.failures[0].failure_type,
496            ValidationFailureType::Expired
497        );
498    }
499
500    #[test]
501    fn test_validate_not_yet_valid_license() {
502        let key_pair = create_key_pair();
503
504        let license_json = LicenseBuilder::new()
505            .license_id("TEST-001")
506            .customer_id("CUST-001")
507            .valid_after(Duration::days(7)) // Valid in 7 days
508            .build_and_sign_to_json(&key_pair)
509            .expect("Should create license");
510
511        let validator = LicenseValidator::new(key_pair.public_key());
512        let context = ValidationContext::new();
513
514        let result = validator
515            .validate_json(&license_json, &context)
516            .expect("Should validate");
517
518        assert!(!result.is_valid);
519        assert_eq!(
520            result.failures[0].failure_type,
521            ValidationFailureType::NotYetValid
522        );
523    }
524
525    #[test]
526    fn test_validate_hostname_restriction() {
527        let key_pair = create_key_pair();
528
529        let license_json = LicenseBuilder::new()
530            .license_id("TEST-001")
531            .customer_id("CUST-001")
532            .allowed_hostname("allowed.example.com")
533            .build_and_sign_to_json(&key_pair)
534            .expect("Should create license");
535
536        let validator = LicenseValidator::new(key_pair.public_key());
537
538        // Valid hostname
539        let context = ValidationContext::new().with_hostname("allowed.example.com");
540        let result = validator
541            .validate_json(&license_json, &context)
542            .expect("Should validate");
543        assert!(result.is_valid);
544
545        // Invalid hostname
546        let context = ValidationContext::new().with_hostname("other.example.com");
547        let result = validator
548            .validate_json(&license_json, &context)
549            .expect("Should validate");
550        assert!(!result.is_valid);
551        assert_eq!(
552            result.failures[0].failure_type,
553            ValidationFailureType::HostnameConstraint
554        );
555    }
556
557    #[test]
558    fn test_validate_version_constraints() {
559        let key_pair = create_key_pair();
560
561        let license_json = LicenseBuilder::new()
562            .license_id("TEST-001")
563            .customer_id("CUST-001")
564            .minimum_version(Version::new(1, 0, 0))
565            .maximum_version(Version::new(2, 0, 0))
566            .build_and_sign_to_json(&key_pair)
567            .expect("Should create license");
568
569        let validator = LicenseValidator::new(key_pair.public_key());
570
571        // Valid version
572        let context = ValidationContext::new().with_software_version(Version::new(1, 5, 0));
573        let result = validator
574            .validate_json(&license_json, &context)
575            .expect("Should validate");
576        assert!(result.is_valid);
577
578        // Version too low
579        let context = ValidationContext::new().with_software_version(Version::new(0, 9, 0));
580        let result = validator
581            .validate_json(&license_json, &context)
582            .expect("Should validate");
583        assert!(!result.is_valid);
584        assert_eq!(
585            result.failures[0].failure_type,
586            ValidationFailureType::VersionConstraint
587        );
588
589        // Version too high
590        let context = ValidationContext::new().with_software_version(Version::new(2, 1, 0));
591        let result = validator
592            .validate_json(&license_json, &context)
593            .expect("Should validate");
594        assert!(!result.is_valid);
595    }
596
597    #[test]
598    fn test_validate_feature_constraints() {
599        let key_pair = create_key_pair();
600
601        let license_json = LicenseBuilder::new()
602            .license_id("TEST-001")
603            .customer_id("CUST-001")
604            .allowed_features(vec!["basic", "premium"])
605            .denied_feature("admin")
606            .build_and_sign_to_json(&key_pair)
607            .expect("Should create license");
608
609        let validator = LicenseValidator::new(key_pair.public_key());
610
611        // Allowed feature
612        let context = ValidationContext::new().with_feature("premium");
613        let result = validator
614            .validate_json(&license_json, &context)
615            .expect("Should validate");
616        assert!(result.is_valid);
617
618        // Unlisted feature
619        let context = ValidationContext::new().with_feature("enterprise");
620        let result = validator
621            .validate_json(&license_json, &context)
622            .expect("Should validate");
623        assert!(!result.is_valid);
624
625        // Denied feature
626        let context = ValidationContext::new().with_feature("admin");
627        let result = validator
628            .validate_json(&license_json, &context)
629            .expect("Should validate");
630        assert!(!result.is_valid);
631    }
632
633    #[test]
634    fn test_validate_connection_limit() {
635        let key_pair = create_key_pair();
636
637        let license_json = LicenseBuilder::new()
638            .license_id("TEST-001")
639            .customer_id("CUST-001")
640            .max_connections(10)
641            .build_and_sign_to_json(&key_pair)
642            .expect("Should create license");
643
644        let validator = LicenseValidator::new(key_pair.public_key());
645
646        // Under limit
647        let context = ValidationContext::new().with_connection_count(5);
648        let result = validator
649            .validate_json(&license_json, &context)
650            .expect("Should validate");
651        assert!(result.is_valid);
652
653        // At limit (trying to add one more)
654        let context = ValidationContext::new().with_connection_count(10);
655        let result = validator
656            .validate_json(&license_json, &context)
657            .expect("Should validate");
658        assert!(!result.is_valid);
659        assert_eq!(
660            result.failures[0].failure_type,
661            ValidationFailureType::ConnectionLimit
662        );
663    }
664
665    #[test]
666    fn test_validate_invalid_signature() {
667        let key_pair_1 = create_key_pair();
668        let key_pair_2 = create_key_pair();
669
670        let license_json = create_basic_license(&key_pair_1);
671
672        // Try to validate with wrong key
673        let validator = LicenseValidator::new(key_pair_2.public_key());
674        let context = ValidationContext::new();
675
676        let result = validator
677            .validate_json(&license_json, &context)
678            .expect("Should return result");
679
680        assert!(!result.is_valid);
681        assert_eq!(
682            result.failures[0].failure_type,
683            ValidationFailureType::InvalidSignature
684        );
685    }
686
687    #[test]
688    fn test_validate_multiple_failures() {
689        let key_pair = create_key_pair();
690
691        let license_json = LicenseBuilder::new()
692            .license_id("TEST-001")
693            .customer_id("CUST-001")
694            .expires_in(Duration::days(-1)) // Expired
695            .allowed_hostname("allowed.example.com")
696            .build_and_sign_to_json(&key_pair)
697            .expect("Should create license");
698
699        let validator = LicenseValidator::new(key_pair.public_key());
700        let context = ValidationContext::new().with_hostname("other.example.com");
701
702        let result = validator
703            .validate_json(&license_json, &context)
704            .expect("Should validate");
705
706        assert!(!result.is_valid);
707        // Should have both expiration and hostname failures
708        assert!(result.failures.len() >= 2);
709    }
710
711    #[test]
712    fn test_is_license_valid_convenience() {
713        let key_pair = create_key_pair();
714        let public_key_base64 = key_pair.public_key_base64();
715        let license_json = create_basic_license(&key_pair);
716
717        assert!(is_license_valid(&license_json, &public_key_base64));
718    }
719
720    #[test]
721    fn test_is_feature_allowed_convenience() {
722        let key_pair = create_key_pair();
723        let public_key_base64 = key_pair.public_key_base64();
724
725        let license_json = LicenseBuilder::new()
726            .license_id("TEST-001")
727            .customer_id("CUST-001")
728            .allowed_feature("premium")
729            .build_and_sign_to_json(&key_pair)
730            .expect("Should create license");
731
732        assert!(is_feature_allowed(
733            &license_json,
734            &public_key_base64,
735            "premium"
736        ));
737        assert!(!is_feature_allowed(
738            &license_json,
739            &public_key_base64,
740            "enterprise"
741        ));
742    }
743
744    #[test]
745    fn test_validation_result_days_remaining() {
746        let key_pair = create_key_pair();
747
748        let license_json = LicenseBuilder::new()
749            .license_id("TEST-001")
750            .customer_id("CUST-001")
751            .expires_in(Duration::days(30))
752            .build_and_sign_to_json(&key_pair)
753            .expect("Should create license");
754
755        let validator = LicenseValidator::new(key_pair.public_key());
756        let context = ValidationContext::new();
757
758        let result = validator
759            .validate_json(&license_json, &context)
760            .expect("Should validate");
761
762        assert!(result.is_valid);
763        let days = result.days_remaining().expect("Should have days remaining");
764        assert!(days >= 29 && days <= 30);
765    }
766
767    #[test]
768    fn test_validation_with_custom_time() {
769        let key_pair = create_key_pair();
770
771        let license_json = LicenseBuilder::new()
772            .license_id("TEST-001")
773            .customer_id("CUST-001")
774            .expires_at(Utc::now() + Duration::days(30))
775            .build_and_sign_to_json(&key_pair)
776            .expect("Should create license");
777
778        let validator = LicenseValidator::new(key_pair.public_key());
779
780        // Validate at a future time (60 days from now)
781        let future_time = Utc::now() + Duration::days(60);
782        let context = ValidationContext::new().with_time(future_time);
783
784        let result = validator
785            .validate_json(&license_json, &context)
786            .expect("Should validate");
787
788        assert!(!result.is_valid);
789        assert_eq!(
790            result.failures[0].failure_type,
791            ValidationFailureType::Expired
792        );
793    }
794}