//! Offline signature verification. //! //! This is all you need if your app is happy to trust a key forever once it //! was issued by the right server. For live revocation checking, combine //! with the [`crate::online`] module. use crate::error::{Error, Result}; use crate::key::{LicenseKey, LicensePayload}; use crate::pubkey::PublicKeyPem; use ed25519_dalek::{Signature, Verifier as _}; use sha2::{Digest, Sha256}; /// Verifies license keys against a single issuing server's public key. /// /// Cheap to construct and cheap to call; shareable across threads /// (`Clone` + `Send` + `Sync`). #[derive(Debug, Clone)] pub struct Verifier { pubkey: PublicKeyPem, } /// Successful verification result. #[derive(Debug, Clone)] pub struct VerifyOk { /// Parsed payload fields. pub payload: LicensePayload, /// License id as a lowercase UUID string. pub license_id: String, /// Product id as a lowercase UUID string. pub product_id: String, } impl VerifyOk { /// Convenience: true if the key's `expires_at` is at or before `now`. /// Always false for perpetual keys. pub fn is_expired_at(&self, now_unix: i64) -> bool { self.payload.is_expired_at(now_unix) } /// Convenience: true if the key grants the given entitlement slug. pub fn has_entitlement(&self, slug: &str) -> bool { self.payload.has_entitlement(slug) } /// Convenience: true if the key is flagged as a trial. pub fn is_trial(&self) -> bool { self.payload.is_trial() } } impl Verifier { /// Construct a verifier bound to a single issuing server's public key. pub fn new(pubkey: PublicKeyPem) -> Self { Self { pubkey } } /// Verify a license key string. Returns either a [`VerifyOk`] on /// success or an [`Error`] explaining the failure. /// /// This checks only the cryptographic signature and format. It does /// **not** check expiry — call [`VerifyOk::is_expired_at`] if you want /// that, or use [`Self::verify_with_time`] to get an error on expired /// keys directly. pub fn verify(&self, key: &str) -> Result { let parsed = LicenseKey::parse(key)?; self.verify_parsed(&parsed) } /// Verify an already-parsed key. pub fn verify_parsed(&self, key: &LicenseKey) -> Result { let sig = Signature::from_bytes(&key.signature); self.pubkey .verifying .verify(&key.signed_bytes, &sig) .map_err(|_| Error::BadSignature)?; Ok(VerifyOk { license_id: key.payload.license_uuid(), product_id: key.payload.product_uuid(), payload: key.payload.clone(), }) } /// Verify a key AND enforce that, if the key is fingerprint-bound, the /// provided fingerprint matches. If the key is *not* fingerprint-bound, /// the fingerprint is ignored (call [`Self::verify`] instead if that's /// what you want). pub fn verify_with_fingerprint(&self, key: &str, fingerprint: &str) -> Result { let parsed = LicenseKey::parse(key)?; let ok = self.verify_parsed(&parsed)?; if ok.payload.is_fingerprint_bound() { let expected = hash_fingerprint(fingerprint); if expected != ok.payload.fingerprint_hash { return Err(Error::BadSignature); } } Ok(ok) } /// Verify a key and additionally reject it if `now_unix` is past its /// `expires_at` (with no grace — for grace-window logic, use an online /// check against `/v1/validate`). Perpetual keys (`expires_at = 0`) are /// accepted regardless of `now_unix`. pub fn verify_with_time(&self, key: &str, now_unix: i64) -> Result { let ok = self.verify(key)?; if ok.payload.is_expired_at(now_unix) { return Err(Error::Expired); } Ok(ok) } } /// Hash a raw fingerprint string to the 32-byte form embedded in keys. /// Matches the service-side implementation. pub fn hash_fingerprint(raw: &str) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(raw.as_bytes()); let digest = hasher.finalize(); let mut out = [0u8; 32]; out.copy_from_slice(&digest); out }