Initial public commit

This commit is contained in:
Keysat
2026-05-07 10:40:53 -05:00
commit 50952b631a
12 changed files with 1157 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
//! 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<VerifyOk> {
let parsed = LicenseKey::parse(key)?;
self.verify_parsed(&parsed)
}
/// Verify an already-parsed key.
pub fn verify_parsed(&self, key: &LicenseKey) -> Result<VerifyOk> {
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<VerifyOk> {
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<VerifyOk> {
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
}