1use 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#[derive(Debug, Clone)]
56pub struct LicenseValidator {
57 parser: LicenseParser,
59}
60
61impl LicenseValidator {
62 pub fn new(public_key: PublicKey) -> Self {
68 Self {
69 parser: LicenseParser::new(public_key),
70 }
71 }
72
73 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 pub fn validate_json(
100 &self,
101 license_json: &str,
102 context: &ValidationContext,
103 ) -> Result<ValidationResult> {
104 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 Ok(self.validate_payload(&payload, context))
124 }
125
126 pub fn validate_payload(
140 &self,
141 payload: &LicensePayload,
142 context: &ValidationContext,
143 ) -> ValidationResult {
144 let mut failures = Vec::new();
145
146 let current_time = context.current_time.unwrap_or_else(Utc::now);
148
149 self.check_expiration(payload, current_time, &mut failures);
151
152 self.check_valid_from(payload, current_time, &mut failures);
154
155 self.check_hostname(payload, context, &mut failures);
157
158 self.check_machine_id(payload, context, &mut failures);
160
161 self.check_version(payload, context, &mut failures);
163
164 self.check_connection_limit(payload, context, &mut failures);
166
167 self.check_features(payload, context, &mut failures);
169
170 if failures.is_empty() {
172 ValidationResult::success(payload.clone())
173 } else {
174 let mut result = ValidationResult::success(payload.clone());
176 result.is_valid = false;
177 result.failures = failures;
178 result
179 }
180 }
181
182 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 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 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 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 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 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 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 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 pub fn parser(&self) -> &LicenseParser {
366 &self.parser
367 }
368}
369
370pub 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
397pub 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
417pub 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 let license_json = LicenseBuilder::new()
479 .license_id("TEST-001")
480 .customer_id("CUST-001")
481 .expires_in(Duration::days(-1)) .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)) .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 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 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 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 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 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 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 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 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 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 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 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)) .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 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 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}