Get started
Quickstart for Rust
Use the keyrail crate in any Rust desktop app or CLI: the License helper, or verification only.
The keyrail crate (Rust 1.85 or later) has two layers:
License, a helper that activates, caches, verifies and refreshes the certificate for you. It needs theclientfeature (on by default), a small blocking HTTPS transport (ureqwith rustls).verify_certificate, verification only: no network, no storage, no clock (you pass the current time). Use it if you already have your own HTTP stack, or to verify certificates on a server.
1. Add the crate#
[dependencies]
keyrail = "0.1"
# Verification only, no HTTP client:
# keyrail = { version = "0.1", default-features = false }2. The License helper#
use std::path::Path;
use keyrail::{Certificate, License, LicenseConfig, LicenseError};
const PRODUCT_ID: &str = "prd_7k2m9q4x1c8d3f6h0j5n";
const PUBLIC_KEYS: [(&str, &str); 1] = [("k1", "pZ3vC0x6hYjQ8m1K4tR9wN2bF7sL5dA0eUqTgHiOcVk")];
pub fn open_license(data_dir: &Path) -> Result<License, LicenseError> {
let config = LicenseConfig::new(
PRODUCT_ID,
PUBLIC_KEYS,
env!("CARGO_PKG_VERSION"), // its major is compared with maxMajor
data_dir.join("license.json"), // cache file, in the per-user app data directory
);
// Debug builds only: point the SDK at a local service (see "Test locally" below).
#[cfg(debug_assertions)]
let config = match std::env::var("KEYRAIL_API") {
Ok(api_base) => config.with_api_base(api_base),
Err(_) => config,
};
License::new(config)
}
/// At every start. Blocks while a refresh is due (one HTTP call, 10 s timeout at most).
pub fn startup(license: &License) -> bool {
match license.check() {
Ok(certificate) => {
println!("Licensed ({} plan)", certificate.plan);
true
}
// Never activated on this machine: show the activation screen.
Err(LicenseError::NoLicense) => false,
// Expired, revoked, version_not_covered, clock_tampered...
Err(e) if e.is_license_state() => {
eprintln!("License not usable: {}", e.code());
false
}
// Environment problem: unreadable cache file or machine ID.
Err(e) => {
eprintln!("License check failed: {e}");
false
}
}
}
pub fn enter_key(license: &License, key: &str) -> Result<Certificate, LicenseError> {
// One online call. On error, e.code() is e.g. "activation_limit", "unknown_key",
// "invalid_request" (bad key format), "network_error" or "invalid_certificate".
let certificate = license.activate(key)?;
println!("Licensed to {}", certificate.licensee.as_deref().unwrap_or("you"));
Ok(certificate)
}check() returns the valid Certificate, or a LicenseError whose code() is stable across SDKs: no_license, a verification outcome (expired, wrong_machine, version_not_covered…) or a definitive server refusal (license_revoked, not_activated…). certificate.has_entitlement("export") checks the signed entitlements.
check() is blocking while a refresh is due. License is Send + Sync: share it in an Arc and call check() from a background thread, or call status() first for an instant, offline-only answer. Other methods:
| Method | What it does |
|---|---|
check_state() | Like check(), but returns a LicenseState (valid, reason, certificate, offline, maskedKey, serializable for a UI). Fails only for environment errors. |
refresh() | Like check(), forcing the online revalidation even before refreshAfter. |
status() | Offline-only LicenseState, no network call. |
deactivate() | Frees the seat for this machine (for a “Move to another computer” button), then clears the cache. Fails with network_error and keeps the cache when the server cannot be reached. |
Refresh rules#
- Valid certificate before
refreshAfter(one day after issue): no network call. - After
refreshAfter, or when the offline check failed for a reason other thanclock_tampered: one call to/v1/licenses/validate. - Network failure,
rate_limited, server error or unexpected answer: the cached certificate is kept and used while it is valid (LicenseState::offlineistrue). - Definitive refusal (
403 license_*, or404 unknown_key,unknown_product,not_activated): the cache is cleared (only the highestissuedAtis kept, for clock rollback detection) and the error is returned. - A new authentic certificate is stored even when it does not grant access; an answer that does not verify is ignored.
The cache file is JSON (version, key, certificate, highestIssuedAt), shared format with the Node SDK. It holds the license key, needed for silent revalidation, so keep it in the per-user app data directory. It is written atomically, with mode 0600 on Unix.
3. Verification only#
use keyrail::{machine_fingerprint, verify_certificate, VerifyConfig, VerifyError};
let product_id = "prd_7k2m9q4x1c8d3f6h0j5n";
// SHA-256 of the machine ID with the product ID: the raw ID never leaves the machine.
let fingerprint = machine_fingerprint(product_id)?;
let config = VerifyConfig::new(
product_id,
[("k1", "pZ3vC0x6hYjQ8m1K4tR9wN2bF7sL5dA0eUqTgHiOcVk")],
fingerprint,
)?;
match verify_certificate(&certificate, &config, now_unix, app_major, highest_issued_at_seen) {
Ok(cert) => println!("valid until {}", cert.expires_at),
Err(VerifyError::Expired) => println!("needs an online validation"),
Err(e) => println!("rejected: {}", e.code()),
}now_unix is the current time in Unix seconds, app_major the major version of your app (keyrail::verify::parse_app_major("1.4.2") gives 1) and highest_issued_at_seen an Option<i64>. verify_certificate_detailed also returns the authentic payload when a later rule fails, for messages such as “your subscription ended on …”.
With verification alone, you call POST /v1/licenses/activate and /validate yourself (or through keyrail::ApiClient with your own Transport), store the certificate string as received, and keep the highest issuedAt you have seen for the clock rollback check.
Test locally#
From a checkout of the Keyrail repository, cd e2e && npm run demo-server starts a throwaway local service on 127.0.0.1:8787, creates a product and a test license, and prints KEYRAIL_API, KEYRAIL_PRODUCT, KEYRAIL_PUBLIC_KEY (kid:key) and the test license key. With the snippet above, a debug build run with KEYRAIL_API=http://127.0.0.1:8787/v1 talks to it (LicenseConfig::with_api_base); use the printed product ID and public key. The crate’s own example reads the same variables:
# from sdk/rust/ in the repository
cargo run -p keyrail --example activate -- activate <test license key>
cargo run -p keyrail --example activate -- checkPlatforms#
The machine identifier comes from MachineGuid in the registry on Windows (64-bit view), IOPlatformUUID on macOS, and /etc/machine-id (then /var/lib/dbus/machine-id) on Linux. Only its hash leaves the machine: see fingerprints.
Next: concepts, then connect a payment provider.
Need help? See Troubleshooting or write to support@keyrail.tech.