# 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 the `client` feature (on by default), a small blocking HTTPS transport (`ureq` with 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


```toml title="Cargo.toml"
[dependencies]
keyrail = "0.1"

# Verification only, no HTTP client:
# keyrail = { version = "0.1", default-features = false }
```

## 2. The License helper


```rust title="src/license.rs"
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](/docs/offline-certificates/#outcomes) (`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 than `clock_tampered`: one call to [`/v1/licenses/validate`](/docs/client-api/#validate).
- Network failure, `rate_limited`, server error or unexpected answer: the cached certificate is kept and used while it is valid (`LicenseState::offline` is `true`).
- Definitive refusal (`403 license_*`, or `404 unknown_key`, `unknown_product`, `not_activated`): the cache is cleared (only the highest `issuedAt` is 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


```rust
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`](/docs/client-api/#activate) and [`/validate`](/docs/client-api/#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:

```bash
# from sdk/rust/ in the repository
cargo run -p keyrail --example activate -- activate <test license key>
cargo run -p keyrail --example activate -- check
```

## Platforms

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](/docs/concepts/#machines-and-fingerprints).

Next: [concepts](/docs/concepts/), then [connect a payment provider](/docs/paddle/).
