================================================================================
  RockyGuard Library - Documentation
  Version 1.3.0
  Copyright (c) 2025-2026 Rocky Software Inc. All rights reserved.
================================================================================

About this manual:

This document describes RockyGuard v1.3.0. Two inline markers are used: "(v1.3+)" for APIs, CLI flags, and behaviour added in v1.3.0, and "(v1.2.1+)" for what arrived in v1.2.1; anything unmarked has been present since v1.2.0. Everything marked "(v1.3+)" is a real, shipping part of v1.3.0 -- not a preview, and not a draft note copied ahead of itself.

Version applicability (READ THIS IF YOU ARE NOT SURE WHICH RELEASE YOU HAVE):

This manual documents the v1.3.0 surface of the library. If your installed library is older, some of what is described here DOES NOT EXIST in your version -- calling it will fail to compile, or fail at the CLI argument parser. Read the inline markers against the version you actually have: passages marked "(v1.3+)" require v1.3.0, and passages marked "(v1.2.1+)" require v1.2.1. To check what you have linked, read ROCKYGUARD_VERSION_STRING from <rockyguard/version.h>.

If you hold a v1.2.1 release, this is not your manual. The v1.2.1 manuals are frozen under docs/archive/v1.2.1/, and the copy packaged inside your customer zip matches your binary exactly -- prefer that one. The website (rockyguard.dev) publishes only the latest release, so a search result can easily land you on a newer manual than your build.

To check the version you actually have linked, read the ROCKYGUARD_VERSION_STRING macro from <rockyguard/version.h> in your application, or run any shipped CLI tool with --help -- the banner that prints carries the version. The library version, the CLI tool versions, and the manual you are reading should all match.

The general rule, whatever version you are on: the manual to trust is the one that came inside your customer zip under docs/Customer_Documentation.pdf, because it was packaged at the same time as the binary you have. Reading a newer manual against an older binary produces "function does not exist" or "unrecognized argument" errors that are easy to misread as bugs.

v1.2.1+ also renames the floating-license CLI binaries from `floating_server_example` / `floating_client_example` to `rg_floating_server` / `rg_floating_client`; references throughout this manual use the new names.

The version-marker convention -- "(v1.3+)" and "(v1.2.1+)" -- is repeated below for completeness.

Reading conventions:

  - Where a feature is new since v1.2.0, the manual marks it
    explicitly with "(v1.2.1+)" inline. Examples: the per-machine
    floating-license cap (§5), Debug-CRT linking variant (§3.1),
    floating-server log rotation (§5.2), --metadata / --show-count
    flags on license_create (§7.3), the LicenseVerifier::check_version()
    API (Customer_API_Reference §6.7), SignatureAlgorithm::AutoDetect.

  - Everything NOT marked "(v1.3+)" or "(v1.2.1+)" describes behavior
    present since v1.2.0. A reader fresh to v1.3.0 can ignore the
    markers entirely and read the manual as a unified description of
    the current library. A reader upgrading from an earlier release
    can scan only the marked sections to see what is new, matching
    the marker to the version they are coming from, without
    re-reading the rest.

  - NOT ALL CROSS-REFERENCED DOCUMENTS SHIP IN YOUR PACKAGE. Your
    docs/ directory contains exactly three files: this manual, the
    Customer_API_Reference, and the License Agreement. The following
    are cited in places and are VENDOR-INTERNAL -- do not go looking
    for them in the zip, and ask support if you need what they cover:

        RockyGuard_Future_Roadmap.txt   deferred / post-v1.3 items
        RockyGuard_Versioning_Policy.txt  SemVer + ABI policy
        RockyGuard_v1_3_Plan.txt        internal release planning
        docs/archive/, docs/archive/v1.2.1/  prior-release doc snapshots

    Citations to them are kept because they name where a decision is
    recorded, which is useful when raising a support question. A
    citation is not a promise that the file is in your hands. The
    substance you actually need is in the three shipped documents; if
    you hit a case where it is not, that is a documentation bug and
    worth reporting.

  - "Customer_API_Reference §X.Y" cross-references point at the
    companion docs/Customer_API_Reference.txt / .pdf -- the C++
    API reference shipped alongside this manual.

--------------------------------------------------------------------------------
TABLE OF CONTENTS
--------------------------------------------------------------------------------
1.  Introduction
2.  Getting Started
3.  Library Integration
4.  End-User License Management
5.  Floating Licensing (Premium Tier)
6.  Hardware Fingerprinting
7.  CLI Tools Reference
8.  License File Format
9.  Anti-Tampering Features
10. Troubleshooting
11. Glossary
12. Production Checklist

<!-- PAGEBREAK -->
================================================================================
1. INTRODUCTION
================================================================================

RockyGuard is a C++17 license checking library that lets you add license
management to your software. It provides two licensing models for your
end users:

  - Node-Locked Licensing: Ties a license to a specific machine via
    hardware fingerprinting (MAC address, CPU ID, disk serial, motherboard
    ID). The license can only be used on the machine it was generated for.

  - Floating Licensing (Premium tier): A central license server manages a
    pool of licenses. End-user applications check out a license when they
    start and return it when they stop.

Key concepts:

Three actors. Three meanings of the word "license". Get this upfront and the rest of the manual reads cleanly; the full glossary lives at §11 for lookup.

The actors:

  YOU (the manual reader; the "library customer"). A software vendor who has bought or is evaluating RockyGuard. You receive a vendor license from Rocky Software Inc., generate end-user licenses on your build machine, and ship a RockyGuard-protected application to your end users.

  YOUR END USER. The person whose machine runs your application. They never see RockyGuard directly. They receive an end-user license file plus your RockyGuard-protected executable. Their machine's hardware fingerprint is what node-locked licenses bind to.

  ROCKY SOFTWARE INC. The library vendor (us). We sign your vendor license. After delivery we are not in the runtime path -- the RockyGuard library never phones home.

The three "licenses":

  Vendor license (vendor_license.json) -- what YOU receive from Rocky Software Inc. Authorizes you to issue end-user licenses. Required by license_create and rg_floating_server. Stays on your build machine; never ships to your end users. See §2.2.

  End-user license (license.json) -- what YOU issue to YOUR end user. Carries their machine's fingerprint (node-locked) or floating settings, plus expiry, features, and your signature. Your application's RockyGuard verifier reads this on their machine. See §4.

  Floating license (a seat in a pool) -- a Premium-tier model in which one end-user license has type=floating and represents N concurrent seats served by a floating server. Individual end-user processes check out a seat at startup and release it on exit. See §5.

Whenever this manual says "the license" without a qualifier, the qualifier is usually clear from context (the §4 chapter is about end-user licenses; §2.2-§2.3 are about the vendor license; §5 is about the floating-license model). When ambiguity could matter, the docs spell out which one explicitly.

Key capabilities:
  - Ed25519 and RSA-SHA256 digital signature algorithms
  - Tamper-proof license files (signed JSON format)
  - Cross-platform hardware fingerprinting (Windows, Linux)
  - Virtual network adapter filtering (VMware, Docker, VPN, etc.)
  - Fuzzy hardware matching with configurable threshold
  - License expiry with configurable grace periods
  - Per-feature license gating
  - Floating license server with heartbeat and automatic lease expiry
  - Clock manipulation detection
  - Binary integrity self-check (shared library builds)
  - Security hardening (ASLR, DEP, Control Flow Guard)

Dependencies:
  - OpenSSL 3.x (linked automatically)

Supported platforms:
  - Windows 10 / 11 (x64), Windows Server 2019+
  - Linux x64 on glibc 2.34 or newer. The shipped Linux binary
    statically links libstdc++, libgcc, and OpenSSL, so glibc is
    the only runtime ABI dependency. It runs on Ubuntu 22.04 LTS
    (glibc 2.35), Ubuntu 24.04 LTS (glibc 2.39), Debian 12
    "Bookworm" (glibc 2.36), RHEL 9 / CentOS Stream 9 / Rocky
    Linux 9 (glibc 2.34), Amazon Linux 2023 (glibc 2.34), and
    any other x86_64 Linux distribution with glibc 2.34 or newer.
    It does NOT run on Debian 11, Ubuntu 20.04, RHEL 8, or
    Amazon Linux 2 (all have glibc 2.31 or older). A glibc-2.28
    build extending coverage to those older distributions is
    planned for a subsequent release.
  - macOS (x64 and Apple Silicon): available upon request -- the
    library's macOS support code (IOKit hardware fingerprint, Mach-O
    integrity check) is present but a shipping macOS build is not
    included in v1.3 either. Contact the vendor if you need one; it is
    built on request rather than shipped in the standard package, and
    remains so as of this release. (Earlier revisions of this bullet
    said "not included in v1.2.x ... ahead of the v1.3 release target",
    which read, in the v1.3 manual, as though macOS had since shipped.
    It has not.)

To confirm your Linux distribution's glibc version, run:
    ldd --version | head -1

<!-- PAGEBREAK -->
================================================================================
2. GETTING STARTED
================================================================================

2.1 Package Contents
----------------------

The Windows customer zip (rockyguard-v1.3.0-windows-x64-customer.zip) extracts to a folder of the same name and contains:

| Path                | File                          | Description                             |
|---------------------|-------------------------------|-----------------------------------------|
| include/rockyguard/ |                               | Public headers (verification only).     |
|                     | rockyguard.h                  | Single-include convenience header.      |
|                     | version.h                     | Library version macros (auto-generated).|
|                     | types.h                       | Enums and result types.                 |
|                     | license.h                     | License data model.                     |
|                     | license_verifier.h            | License verification.                   |
|                     | hardware_fingerprint.h        | Machine hardware fingerprinting.        |
|                     | floating_client.h             | Floating license client (Premium).      |
|                     | export.h                      | DLL export macros.                      |
| lib/static/         | rockyguard.lib                | Static library, /MD (Release-CRT) -- link from a Release consumer build. |
|                     | rockyguard_mdd.lib            | Static library, /MDd (Debug-CRT) -- link from a Debug consumer build (v1.2.1+). See §3.1 for $<CONFIG>-driven selection. |
| lib/shared/         | rockyguard.dll                | Shared library (DLL).                   |
|                     | rockyguard.lib                | Import library for the DLL.             |
|                     | rockyguard.sig                | Integrity signature (ship with DLL).    |
|                     | libcrypto-3-x64.dll           | OpenSSL runtime.                        |
|                     | libssl-3-x64.dll              | OpenSSL TLS runtime.                    |
| tools/              | license_keygen.exe            | Generate your Ed25519 or RSA keypair.   |
|                     | license_create.exe            | Create end-user licenses.               |
|                     | license_verify.exe            | Verify and inspect license files.       |
|                     | rg_fingerprint.exe            | Print machine hardware fingerprint (CLI). |
|                     | rg_fingerprint_gui.exe        | Same fingerprint output, in a GUI window with a Copy button + auto-clipboard. Ship this to non-technical end users who would double-click an .exe rather than open a terminal. Windows only. v1.3+. |
|                     | rg_floating_server.exe        | Floating license server (Premium).      |
|                     | floating_server_config.yaml   | Sample server config file.              |
|                     | rg_floating_client.exe        | Floating license client smoke-test binary (Premium). |
|                     | libcrypto-3-x64.dll           | OpenSSL runtime for the tools above. A second copy of the same DLL that appears under lib/shared/; the tools load it from their own directory, so keep both next to the .exe files if you relocate them. |
|                     | libssl-3-x64.dll              | As above. Both are required -- the tools fail to start with STATUS_DLL_NOT_FOUND (0xC0000135) and no message if either is missing. |
| portal/             | start.bat / start.ps1 / start.sh | Launchers for the self-hosted licensing web app described in §4.8. OPTIONAL -- everything the portal does is `license_create` underneath (§4.3), so you can delete this directory outright if your team uses the CLI. |
|                     | run.py                        | Entry point the launchers call; run it directly if you prefer to manage the Python environment yourself. |
|                     | app/                          | FastAPI application (routes, templates, static assets). |
|                     | wheels/                       | Pre-downloaded Python dependency wheels, so first run works on a machine with no PyPI access. Platform-specific: the Windows zip carries win_amd64 wheels, the Linux zip manylinux2014_x86_64. |
|                     | requirements.txt              | Dependency pin list matching wheels/. |
|                     | README.md                     | Portal setup and daily workflow. Start here rather than with §4.8 if the portal is all you need. |
| deps/               | include/openssl/              | Bundled OpenSSL headers. The exact version is stated in deps/THIRD-PARTY-NOTICES.txt and in the OPENSSL_FULL_VERSION_STR macro in include/openssl/opensslv.h. As of v1.3.0 the bundled version is OpenSSL 3.6.1. |
|                     | lib/libcrypto.lib             | OpenSSL crypto import library (needed when static-linking against rockyguard.lib; see §3.1). |
|                     | lib/libssl.lib                | OpenSSL TLS import library (needed when static-linking against rockyguard.lib; see §3.1). |
|                     | OPENSSL-LICENSE.txt           | The Apache License 2.0, under which the bundled OpenSSL is distributed. **You must carry this file, or its text, with any application you ship that redistributes OpenSSL** -- which the shared-link path in §3.4 requires. See §3.4 "Third-party attribution". |
|                     | THIRD-PARTY-NOTICES.txt       | Attribution for every bundled third-party component, with the exact version and what the licence asks of you as a redistributor. Generated at packaging time from the headers actually in this zip, so it cannot disagree with them. |
| examples/           | CMakeLists.txt                | Build script for the examples.          |
|                     | node_locked_example.cpp       | Node-locked license verification example. |
|                     | rg_floating_client.cpp        | Floating license client example source. |
|                     | banner.h                      | Shared header that both .cpp examples #include for their startup banner; copied into the package so the examples build standalone. |
|                     | README.txt                    | How to configure and build the two examples on their own, separately from your application. |
| docs/               | Customer_Documentation.txt/pdf | This document.                         |
|                     | Customer_API_Reference.txt/pdf | API reference.                         |
|                     | RockyGuard_License_Agreement.txt/pdf | The licence agreement governing your use of the library. Binding regardless of whether the portal or the CLI issues your end-user licences. |
| (zip root)          | AI_INTEGRATION_GUIDE.md       | AI-agent integration recipe. Hand this file to Claude Code, Cursor, Copilot, etc. to have them perform the integration end-to-end (see §2.6). |
| (zip root)          | README.txt                    | One-page orientation: top-level package contents, quick start, support contact. Read this first if you're new. |

Per-platform differences (Linux):

The Linux customer zip (rockyguard-v1.3.0-linux-x64-customer.zip) is identical in structure to the Windows zip above with the following file-level substitutions:

| Path        | Linux file                      | Notes                                                      |
|-------------|---------------------------------|------------------------------------------------------------|
| lib/shared/ | librockyguard.so                | In place of rockyguard.dll.                                |
|             | librockyguard.sig               | In place of rockyguard.sig. Same format; the name matches the .so. |
|             | (no libcrypto / libssl files)   | OpenSSL is statically linked into librockyguard.so on Linux, so there is no separate runtime dependency. |
| lib/static/ | librockyguard.a                 | In place of rockyguard.lib.                                |
| tools/      | (binaries with no .exe suffix)  | Same tools as Windows MINUS rg_fingerprint_gui (which is Windows-only in v1.3): license_keygen, license_create, license_verify, rg_fingerprint, rg_floating_server, rg_floating_client. No OpenSSL runtime files here either -- they are statically linked. |
| portal/     | start.sh                        | In place of start.bat / start.ps1. Same application; wheels/ carries manylinux2014_x86_64 wheels rather than win_amd64. |
| deps/lib/   | libcrypto.a                     | In place of libcrypto.lib. Needed when static-linking librockyguard.a. |
|             | libssl.a                        | In place of libssl.lib. Needed when static-linking librockyguard.a.   |
| deps/       | OPENSSL-LICENSE.txt             | Same file as the Windows zip. Note that it still applies on Linux even though no OpenSSL file ships beside your binary: OpenSSL is linked INTO librockyguard.so and into anything you static-link against librockyguard.a, so you are distributing OpenSSL code either way. See §3.4 "Third-party attribution". |
|             | THIRD-PARTY-NOTICES.txt         | Same file, generated per platform; its shipping note is worded for the Linux linkage described above. |

No .lib import library is needed on Linux; the shared library is linked directly via `-lrockyguard` once LD_LIBRARY_PATH or RUNPATH points at lib/shared/. macOS customers: available upon request (see §1, Supported platforms).

2.2 How You Receive Your Vendor License
-----------------------------------------

The vendor license is the file that authorizes your use of RockyGuard for issuing end-user licenses. You receive it from the library vendor (Rocky Software Inc.) once your purchase or trial agreement is in place; it is not bundled inside the customer zip you downloaded, and the library will not generate end-user licenses without it.

How to request it:

Use the contact form at https://rockyguard.dev/contact (preferred -- the form routes to the same inbox as the email below and pre-fills a few fields), or email hello@rockyguard.dev directly. Include the legal name of the licensee organisation, the product name the license is for, and the desired tier (Basic or Premium); see §2.3 for tier details.

Delivery format:

The vendor license arrives as an email attachment: a single signed JSON file, typically named after your organisation, e.g. rocky_software_license.json or acme_inc_license.json. RENAME IT TO vendor_license.json, or substitute your filename in every command that follows. Every example in 2.4, 3.3, 4.3, 5.1, 7.3, 9.7 and the portal walkthrough hardcodes vendor_license.json; renaming once is less error-prone than editing each command. Save it to a stable location on your build machine alongside your existing private signing key. Do NOT commit it to a public repository; treat it the same way you treat your private key.

Expected receipt time:

One business day from a complete request. If your request is received outside business hours (Eastern Time), expect delivery the next business day. The contact form's auto-reply confirms receipt; the actual license arrives in a follow-up email.

If your license is missing or has not arrived:

Email hello@rockyguard.dev with your original request thread (subject line, sender, or approximate date is enough to locate the original request on our side) and we will resend or investigate. Same SLA as the initial request: one business day. If license_create or rg_floating_server reports that the vendor license file cannot be parsed when you point them at it (e.g., a "Vendor license missing" or "MalformedFile" error), do not modify the file -- ask for a fresh copy.

What the vendor license contains and how the library uses it:

The vendor license is a signed JSON document carrying your organisation name, license tier, generation-limit counter (Basic: unlimited; Premium: configurable), product binding, and a signature from the library vendor. license_create (called with --vendor-license) and rg_floating_server (vendor_license: in its YAML config) consume it at startup: the file's signature is verified against the vendor public key embedded in the library, the tier and limits are loaded into the running tool, and the matching code paths are unlocked. Every subsequent license-generation call then cross-checks the vendor license to enforce the tier and counter limits. license_keygen does NOT consume the vendor license -- it is a standalone tool that generates a fresh Ed25519 or RSA keypair on its own, run as the very first step of the workflow before you have anything from Rocky Software Inc. (see §2.4 Quick Start, step 1). The verifier-side library used by your end-user application does NOT need the vendor license either -- see §2.3.

Once it arrives, 2.3 explains where it is consumed and what your tier
grants -- you do not need the vendor license in your shipped
application, only in the tools that GENERATE licenses.

2.3 What Your Vendor License Does
-----------------------------------

2.2 covered how the file reaches you. This section covers what it is
FOR: which of your programs need it, which do not, and what your tier
changes. If you already have vendor_license.json in hand and only want
to know where it plugs in, start here.

(These two sections used to overlap: 2.3 opened by re-announcing that
you should have received the file, which reads as a restart rather than
a continuation. The procurement detail lives in 2.2 and is not repeated
here.)

  IMPORTANT:
  - The vendor license is needed ONLY in your license generation tools
    and floating license server.
  - Your end-user application does NOT need the vendor license file.
    It only verifies licenses using LicenseVerifier and FloatingLicenseClient.

  Your license tier:
  - Basic:   Node-locked licensing, unlimited license generation
  - Premium: Node-locked + floating licensing, configurable generation limit

  Yes, Basic is the LESS restricted of the two on that middle column,
  and it is not a typo. The generation counter is a vendor-side control
  on how many end-user licenses you may mint, and it is applied where
  the commercial agreement calls for it -- typically Premium seat-pool
  deals, which are priced per concurrent seat. Basic is priced without
  such a cap, so none is enforced. Tier governs which FEATURES you get
  (floating licensing is Premium-only); it is not a single ranked axis
  where Premium is uniformly more permissive. See EULA 2.2(c).

2.4 Quick Start
-----------------

PATH SEPARATORS, AND ONE SHELL THAT WILL NOT ACCEPT THEM. Every
command in this manual is written with forward slashes
(tools/license_create). That form works in PowerShell, in Git Bash, and
on Linux, which is why it is used here -- this manual serves both
platforms and a backslash would break the Linux reader.

It does NOT work in cmd.exe. Pasting

    tools/license_create --key private.pem ...

into a classic Command Prompt fails, because cmd.exe does not accept
forward slashes in the program path. If you use cmd.exe, either switch
the separators:

    tools\license_create.exe --key private.pem ...

or run the commands from PowerShell instead, which is the default
terminal in Windows 11 and needs no change. This affects only the path
to the tool; the flags are identical in every shell.

WHERE THESE COMMANDS RUN, AND WHERE THEIR OUTPUT LANDS. Every path
below is RELATIVE TO YOUR CURRENT WORKING DIRECTORY, and none of these
tools writes to the package directory. Pick a working directory you
control -- not inside the extracted zip -- and stay in it for all three
steps:

    cd C:\licensing\myproduct        (any directory you own)

SET ONE VARIABLE FIRST, AND EVERY COMMAND IN THIS MANUAL WILL WORK FROM
THERE. Because your working directory is deliberately NOT the extracted
package, a bare `tools/license_keygen` cannot resolve -- there is no
tools/ directory where you are standing. Point a variable at the
package once:

    PowerShell:   $RG = "C:\rockyguard-v1.3.0-windows-x64-customer"
    cmd.exe:      set RG=C:\rockyguard-v1.3.0-windows-x64-customer
    bash / Linux: RG=$HOME/rockyguard-v1.3.0-linux-x64-customer

substituting wherever you actually unzipped it. Then every command that
this manual writes as `tools/<something>` is run as:

    PowerShell:   & "$RG\tools\license_keygen" --private private.pem ...
    cmd.exe:      "%RG%\tools\license_keygen.exe" --private private.pem ...
    bash / Linux: "$RG/tools/license_keygen" --private private.pem ...

THIS CONVENTION APPLIES THROUGHOUT THE MANUAL, not only to the Quick
Start: sections 3.3, 4.1, 4.3, 4.4, 5.1, 8.2 and 9.7 all print
`tools/...` in the same shorthand, and all of them mean "the tools
directory inside your extracted package". The Quick Start below spells
the prefix out in full; later sections use the shorthand to keep the
commands readable. If a command fails with "not recognized as the name
of a cmdlet" on Windows or "No such file or directory" on Linux, this
is why -- you are missing the prefix, not the tool.

(Until 2026-08-14 this paragraph said "invoke the tools by path into
the package, as the examples do" and then printed the bare form, which
does not do that. Two independent first-time readers pasted step 1
verbatim from the directory this section tells you to create and got
"not recognized" / exit 127; one of them then ran it from the package
root instead, which works but drops your private signing key inside the
extracted zip -- the thing the paragraph above warns against. The
convention is now stated rather than implied.)

With that cwd, step 1 writes private.pem and public.pem there, step 2
writes license.json there (license_create's default --output; see 7.3),
and the anti-rollback state files described in 9.7 are written under
your per-user application-data directory rather than the cwd as of
v1.3. Keeping one stable working directory matters because private.pem
from step 1 is the input to step 2, and losing track of which directory
it landed in is the most common way this sequence goes wrong.

If you would rather have an AI agent (Claude Code, Cursor, GitHub Copilot, ChatGPT, Gemini, Cody, or similar) perform the integration for you end-to-end instead of walking through the steps below by hand, see §2.6 "Integrating with an AI assistant" and skip to it now. The two paths produce equivalent results; choose whichever matches how you usually work.

Step 1: Generate your keypair (run once):

    "$RG/tools/license_keygen" --private private.pem --public public.pem

(PowerShell: prefix with `& `. cmd.exe: "%RG%\tools\license_keygen.exe".
See the paragraph above if you have not set $RG yet.)

Keep private.pem SECRET. You will embed public.pem in your application.
Note that both keys land in your CURRENT directory -- the one you cd'd
to above -- and not in the package.

Step 2: Create an end-user license. Requires the vendor_license.json
your library vendor provided (see sections 2.2 and 2.3). Replace
"<end user's fingerprint>" with the hash your end user obtained by
running rg_fingerprint on THEIR machine (see §4.2 for how to
ask for it):

    "$RG/tools/license_create" --key private.pem \
      --vendor-license vendor_license.json \
      --id "LIC-001" \
      --licensee "Customer Name" \
      --product "YourApp" \
      --expires "2027-12-31T23:59:59Z" \
      --feature "pro_feature" \
      --fingerprint-value "<end user's fingerprint>"

Quick self-test alternative: use --fingerprint (no value) instead of --fingerprint-value to bind the license to YOUR OWN build machine. This is convenient for verifying the toolchain end-to-end without a real end user, but the resulting license will not run on any other machine. Do not ship a self-fingerprinted license to an actual customer.

Step 3: In your end-user application (no vendor license needed):

    #include <rockyguard/rockyguard.h>

    static constexpr char PUBLIC_KEY[] = R"(-----BEGIN PUBLIC KEY-----
    MCowBQYDK2VwAyEA...your public key here...
    -----END PUBLIC KEY-----)";

    int main() {
        // CONSOLE-APP SHAPE -- read this before pasting it into a GUI.
        // load() and check_node_locked() below are SYNCHRONOUS, and both
        // can block for roughly 9 seconds or more (three HTTPS
        // time-check hosts at 3s each, plus OS DNS resolution) when the
        // machine is offline or sitting behind a captive portal. See
        // section 9.2. Blocking main() in a console tool is harmless. In
        // a desktop application, do this on a background thread and show
        // your window first -- otherwise the app looks hung on first
        // launch, which is exactly when a new customer is deciding
        // whether your software works.
        //
        // The constructor is the only verifier call that throws
        // (std::runtime_error on an unparseable PEM or an unsupported
        // key type). Uncaught, that is std::terminate() -- a hard crash
        // rather than a message. A mis-pasted PUBLIC_KEY literal is the
        // usual cause, so guard it even when the key is embedded.
        // LicenseVerifier is not copyable or movable, hence optional +
        // emplace(); this also keeps the body below un-indented.
        // See Customer_API_Reference section 6.1.
        std::optional<rockyguard::LicenseVerifier> maybe_verifier;
        try {
            maybe_verifier.emplace(PUBLIC_KEY);
        } catch (const std::runtime_error& e) {
            std::cerr << "License system error: " << e.what() << std::endl;
            return 1;
        }
        rockyguard::LicenseVerifier& verifier = *maybe_verifier;

        auto result = verifier.load("license.json");
        if (!result) {
            std::cerr << result.message << std::endl;
            return 1;
        }

        result = verifier.check_node_locked();
        if (!result) {
            std::cerr << result.message << std::endl;
            return 1;
        }

        // Optional: enforce version_range at runtime (v1.2.1+).
        // Skip if you don't issue licenses with --version, or if you
        // want the field to remain informational only. See §3.3 point B
        // for the uncommented version and Customer_API_Reference §6.7
        // for the full syntax.
        //   result = verifier.check_version("3.1.0");
        //   if (!result) return 1;

        // Application runs with valid license
        return 0;
    }

2.5 Building and Running the Examples
----------------------------------------

The examples/ folder contains working example source code and a
CMakeLists.txt to build them. Use these to learn how the library works
before integrating into your own application.

To build the examples (OpenSSL is bundled, no separate install needed):

    cd examples
    cmake -B build
    cmake --build build --config Release

The CMakeLists.txt automatically finds the library in the parent
directory (the package root that contains include/, lib/, deps/,
etc. after you unzipped the customer zip). To point to a different
location:

    cmake -B build -DROCKYGUARD_DIR=/path/to/extracted/package

Available examples:

node_locked_example:
Demonstrates license loading, signature verification, hardware fingerprint checking, grace period handling, and feature gating. Run as:

    node_locked_example <public_key.pem> <license.json>

rg_floating_client:
Demonstrates floating license checkout, heartbeat, and checkin. Premium tier only -- requires a running floating server (see §5). Run as:

    rg_floating_client [server_host] [port]

2.6 Integrating with an AI Assistant
--------------------------------------

The customer zip ships an AI-agent-readable integration recipe at the package root: AI_INTEGRATION_GUIDE.md. The same file is also published online at https://rockyguard.dev/AI_INTEGRATION_GUIDE.md so an agent can fetch it by URL. It is a parallel path to the human-walked Quick Start (§2.4) and the build-and-run examples (§2.5); the end state is the same -- a working RockyGuard integration in your application -- produced by a different actor.

What it is:

A how-to written in AI-imperative voice ("you are an AI assistant... do X, ask the user Y") and structured for agentic execution: 11 numbered sections covering the full integration (CMake wiring, public-key embedding, startup verification, feature gating, optional floating-license setup), explicit input-collection prompts the agent must ask the user before touching code, and explicit "stop and ask the user" gates for situations the agent should not unilaterally resolve. The same content is readable to a human; the imperative voice exists so an AI agent executes it deterministically.

How to use it:

Open your AI agent of choice (Claude Code, Cursor, GitHub Copilot, ChatGPT, Gemini, Cody, etc.) and give it one of these prompts. After extracting the customer zip you will have a folder named after the zip itself -- e.g. rockyguard-v1.3.0-windows-x64-customer/ on Windows or rockyguard-v1.3.0-linux-x64-customer/ on Linux (the version and platform parts match whichever zip you downloaded). That extracted folder is the package root the AI agent should be pointed at; you do not need to rename it, move it into a vendor/ or third_party/ subdirectory, or otherwise reorganize it before running the prompts below.

- If you have already extracted the zip (recommended; the agent reads the local copy of the guide directly):

    Please integrate the RockyGuard license library into my project.
    The package is at rockyguard-v1.3.0-windows-x64-customer/.
    Read rockyguard-v1.3.0-windows-x64-customer/AI_INTEGRATION_GUIDE.md
    and follow it.

  Replace the folder name with whatever your extracted zip actually produced. If you extracted into a subdirectory of your repo (downloads/, third_party/, or anywhere else), prepend that path so the agent knows where to look -- e.g. third_party/rockyguard-v1.3.0-windows-x64-customer/.

- If you have not yet downloaded the zip, or prefer the agent to fetch the guide directly from the website:

    Please integrate the RockyGuard license library into my project.
    Fetch https://rockyguard.dev/AI_INTEGRATION_GUIDE.md and follow it.

  The agent will then ask you for the location of the extracted package (or instruct you on downloading and extracting it) before it touches your build files. The guide's §2 covers placement: it tells the agent to match your project's existing convention for vendored dependencies rather than invent one.

The agent will then ask you, in order, for the four inputs the guide requires before it modifies any code:

- Your public key (PEM string, from public.pem produced by license_keygen).
- Which binary in your project should carry the license check (if you have more than one).
- Where license.json should be loaded from at runtime (default: next to the executable). This is the license file your application will read on the end-user's computer to verify the user is licensed; it is not the vendor license and it does not stay on your build machine.
- Your tier (Basic = node-locked only; Premium = node-locked + floating) and which features in your application should be paid-tier gated.

Once those are answered, the agent edits your CMakeLists.txt to link the library, embeds your public key as a string constant in the source file containing main(), inserts the verification block at the top of main(), and adds check_feature() calls for each feature you named. It then runs your normal build to confirm nothing breaks.

What the agent will and will NOT do automatically:

The guide instructs the agent to STOP and ask you, rather than guess, in any of the following situations:

- It cannot unambiguously locate int main(...).
- Your project has multiple binaries and it is unclear which one to protect.
- Your build system is not vanilla CMake (Bazel, Meson, MSBuild, plain Make).
- You already have a different license-checking system in place (Sentinel, FlexLM, custom) -- whether to replace it or run side-by-side is your call.
- public.pem is not in the workspace and you have not pasted its contents.
- You have not told the agent which features should be paid-tier gated.

The guide also enforces a set of hard-no rules the agent will refuse to violate: never commit private.pem to git, never ship private.pem, never catch and ignore license verification errors, never embed license.json into the binary, never generate a fake public key, never silently switch your build system. These mirror the security and operational rules in §3, §9, and §12 of this manual.

Why we ship this:

C++ integration tasks -- adding a third-party library to a CMakeLists.txt, embedding a key, wiring a verification call into main(), defining feature gates -- are increasingly handed to AI agents. Most license-management libraries ship documentation that assumes a human reads a PDF and translates it into code by hand: a half-day to one-day onboarding even for an experienced engineer. AI_INTEGRATION_GUIDE.md exists to compress that to a single prompt by giving the agent a deterministic, opinionated recipe written for the way agents actually work (asking the user for ambiguities up front, refusing to guess, naming the failure modes that produce silent runtime bugs). It is a deliberate design choice, not a marketing layer over the human-readable docs; the same recipe produces a clean integration whether followed by a human or an agent.

Cross-references:

- The full guide is in your customer zip at AI_INTEGRATION_GUIDE.md and at https://rockyguard.dev/AI_INTEGRATION_GUIDE.md.
- The human-walked equivalent is §2.4 (Quick Start) plus §3 (Library Integration).
- For feature gating (the half of the integration the agent will ask you to define), see §4.6 of this manual.
- For the floating-license additions the agent applies on Premium tier when explicitly requested, see §5.

<!-- PAGEBREAK -->
================================================================================
3. LIBRARY INTEGRATION
================================================================================

REQUIREMENT: C++17. The shipped library is compiled as C++17, and your
consumer project must be built with a C++17 (or later) standard so that
your translation units agree with it on the standard library types
crossing the boundary. In CMake:

    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

With a direct compiler flag: /std:c++17 (MSVC) or -std=c++17 (gcc /
clang). If this is missed, the first rockyguard header you include
fires a static_assert naming the exact fix -- no template error
cascade. That guard lives in include/rockyguard/export.h, which every
other public header reaches, so it fires whichever header you include
first and whether or not you go through the rockyguard.h convenience
header. The one deliberate exception is version.h, which includes
nothing and carries only macros, so build scripts and
standard-agnostic code can read your library version without tripping
the guard.

(Correcting an earlier version of this paragraph: it said the public
headers "use std::optional, std::variant, std::string_view, structured
bindings, and inline variables". They do not -- those features are used
in the library's own implementation, not in its interface, and a reader
who checked the headers for them found nothing and could reasonably
conclude the requirement was stale. The requirement is real; the reason
given for it was wrong. Separately, until v1.3.0 the guard sat only in
rockyguard.h, so including an individual header under a pre-C++17
standard compiled silently. Both were found by the Phase 4.1 v1.3.0
onboarding dry-run.)

3.1 Linking (Static - Recommended)
-------------------------------------

The static rockyguard.lib pulls in both libcrypto (license signing,
hashing, HMAC, ed25519/RSA) AND libssl (TLS for the online time-anchor
verification and the optional floating-license HTTPS). Your consumer
must link BOTH, plus the Windows system libs those transitively need.

Static builds do not need a .sig file. The integrity-check signature
shipped as rockyguard.sig in lib/shared/ is paired with the rockyguard
DLL only -- a static link compiles the library code directly into your
executable, so there is no separate binary for the integrity check to
verify against. §9.3 has the full picture of what the .sig file
does and why static builds skip it.

CRT VARIANTS on Windows: the customer zip ships TWO static
library variants so you can build your own executable in either
Release or Debug without hitting LNK2038 / LNK1319 errors about
mismatched _ITERATOR_DEBUG_LEVEL or RuntimeLibrary:

    rockyguard.lib       built with /MD  -- link from a Release
                         (/MD) consumer.
    rockyguard_mdd.lib   built with /MDd -- link from a Debug
                         (/MDd) consumer.

The bundled OpenSSL import libs (libssl.lib, libcrypto.lib) are
CRT-agnostic: vcpkg's Release and Debug import stubs are byte-
identical, and OpenSSL's API does not pass C-runtime objects across
the DLL boundary, so a single pair serves both consumer variants.

If your CMake selects per-config, the natural pattern is to feed
the right variant via a generator expression:

    target_link_libraries(your_app PRIVATE
        $<IF:$<CONFIG:Debug>,
            path/to/lib/static/rockyguard_mdd.lib,
            path/to/lib/static/rockyguard.lib>
        path/to/deps/lib/libssl.lib
        path/to/deps/lib/libcrypto.lib
        ws2_32 crypt32 iphlpapi ole32 oleaut32 wbemuuid advapi32 shell32)

This restriction does not apply on Linux: glibc / libstdc++ ABI
does not split per-config, so a single librockyguard.a serves both
Release and Debug consumers.

In your CMakeLists.txt (Windows, Release-only example):

    # Point to the library's include and lib directories. The
    # bundled deps/ directory contains the exact OpenSSL build we
    # linked against -- use it for the most predictable result.
    target_include_directories(your_app PRIVATE
        path/to/include
        path/to/deps/include)
    target_link_libraries(your_app PRIVATE
        path/to/lib/static/rockyguard.lib
        path/to/deps/lib/libssl.lib
        path/to/deps/lib/libcrypto.lib
        ws2_32 crypt32 iphlpapi ole32 oleaut32 wbemuuid advapi32 shell32)

On Linux, the static path is structurally the same as Windows: the
shipped librockyguard.a contains the RockyGuard code only, and you
link the bundled OpenSSL archives from deps/lib/ (both libssl.a and
libcrypto.a are needed; libssl supplies the TLS symbols used by the
online time-anchor verification, libcrypto supplies the hashing /
signature / HMAC symbols). pthread + dl are required by OpenSSL on
Linux:

    target_include_directories(your_app PRIVATE
        path/to/include
        path/to/deps/include)
    target_link_libraries(your_app PRIVATE
        path/to/lib/static/librockyguard.a
        path/to/deps/lib/libssl.a
        path/to/deps/lib/libcrypto.a
        pthread dl)

NOT AVAILABLE TO YOU: add_subdirectory(). Earlier revisions of this
section offered a third variant --

    add_subdirectory(path/to/rockyguard)      # DOES NOT WORK HERE
    target_link_libraries(your_app PRIVATE rockyguard)

-- described as letting the library's own CMake target export its
transitive dependencies so you need not list them. That is true of the
library's SOURCE tree, which is not what you have. Your package ships
headers and prebuilt binaries; there is no CMakeLists.txt and no source
to add_subdirectory() into, so this variant cannot resolve and there is
no directory in the zip to point it at. It is documented here only
because it appeared in prior revisions and is the variant a newcomer
naturally reaches for -- it is the one that promises to spare you the
manual link line.

If you want that ergonomics without a source tree, copy
examples/CMakeLists.txt: it defines an IMPORTED target named
`rockyguard` that carries the include directories, both OpenSSL
libraries and the platform libraries as INTERFACE properties, so your
own targets get them transitively from a single
target_link_libraries(your_app PRIVATE rockyguard). That file is the
supported equivalent, and it is the same file that builds the shipped
examples, so it is known to work.

Static builds do NOT require rockyguard.sig at runtime. The
integrity signature is only relevant to shared (DLL / .so) builds,
where the library image is a separate file that can be tampered
with after install. When the library is statically linked into
your_app.exe, it is part of the executable image and any tamper
invalidates the executable's own signature.

3.2 Linking (Shared / DLL)
-----------------------------

Windows:

    # Define ROCKYGUARD_SHARED_LIB before including headers so the
    # public symbols are marked __declspec(dllimport).
    target_compile_definitions(your_app PRIVATE ROCKYGUARD_SHARED_LIB)
    target_include_directories(your_app PRIVATE path/to/include)
    target_link_libraries(your_app PRIVATE
        path/to/lib/shared/rockyguard.lib)

At build time you only link rockyguard.lib (an import library that routes calls to rockyguard.dll at load time). You do NOT have to link any OpenSSL .lib files yourself; rockyguard.dll already imports the OpenSSL symbols it needs.

At runtime, rockyguard.dll loads two OpenSSL DLLs from the same directory: libcrypto-3-x64.dll and libssl-3-x64.dll. These are vcpkg-built dynamic-linkage OpenSSL binaries; rockyguard.dll does NOT statically embed them. Without them in your application's load path, rockyguard.dll fails to load with an OS-level "missing dependency" error before any of your code runs.

Ship with your application:
  - rockyguard.dll
  - rockyguard.sig           (integrity signature - must be next to the DLL)
  - libcrypto-3-x64.dll      (OpenSSL crypto runtime)
  - libssl-3-x64.dll         (OpenSSL TLS runtime)

Place all four files in the same directory as your_app.exe (or anywhere on the OS DLL search path). The Linux shared-build behaves differently -- see the Linux subsection below -- so do not assume the Windows shipping list applies to the .so.

Linux:

    target_compile_definitions(your_app PRIVATE ROCKYGUARD_SHARED_LIB)
    target_include_directories(your_app PRIVATE path/to/include)
    target_link_libraries(your_app PRIVATE
        path/to/lib/shared/librockyguard.so)

    # Either set RPATH at build time so your_app finds the .so at
    # runtime without LD_LIBRARY_PATH:
    set_target_properties(your_app PROPERTIES
        INSTALL_RPATH "$ORIGIN/../lib/shared"
        BUILD_WITH_INSTALL_RPATH TRUE)
    # ...or ship your_app and librockyguard.so in the same directory
    # and set INSTALL_RPATH "$ORIGIN" instead, with the same
    # BUILD_WITH_INSTALL_RPATH TRUE line.

BUILD_WITH_INSTALL_RPATH TRUE IS REQUIRED, NOT OPTIONAL, AND ITS ABSENCE FAILS IN A WAY THAT PASSES YOUR OWN TESTING. CMake applies INSTALL_RPATH only to the binary produced by `cmake --install`. Without BUILD_WITH_INSTALL_RPATH TRUE, the binary in your build tree instead receives an automatic absolute RPATH pointing at wherever librockyguard.so sat when you linked -- typically the directory you unpacked this package into.

That binary then works on your machine and only on your machine. Copying it next to librockyguard.so in a deployment directory does not help and, worse, appears to: the absolute RPATH still resolves, because your extracted package is still where it was, so `ldd` shows the library found and the program prints its licence status normally. On an end user's machine, where that path does not exist, the dynamic loader fails before main() runs:

    ./your_app: error while loading shared libraries: librockyguard.so:
    cannot open shared object file: No such file or directory

Exit status 127, no output of your own, and no RockyGuard diagnostic -- the library never gets control, so nothing in Section 10 applies. This is the Linux counterpart of the Windows STATUS_DLL_NOT_FOUND (0xC0000135) failure described above, and it is harder to catch because the Windows case fails on the developer's machine too.

Verify before you ship, from your build tree:

    objdump -x build/your_app | grep -E 'RPATH|RUNPATH'

You want to see $ORIGIN. An absolute path containing your own home or build directory means BUILD_WITH_INSTALL_RPATH is missing, and the binary is not portable.

Ship with your application:
  - librockyguard.so
  - librockyguard.sig  (integrity signature - must be next to the .so)

OpenSSL is statically linked into librockyguard.so on Linux, so there is no separate libcrypto / libssl runtime to ship -- the Linux .so is self-contained for OpenSSL. (The Windows shared-build packages OpenSSL as separate DLLs because that is vcpkg's standard x64-windows convention; the Linux build configures OpenSSL for static archiving instead. Either choice is equivalent at the call-site level; the difference is only in which files ship next to your_app.)

3.3 Two Separate Integration Points
--------------------------------------

Your software will have TWO places where the library is used:

A) YOUR LICENSE GENERATION WORKFLOW (vendor license required):
   This is the workflow you run when issuing a new end-user license.
   The package ships license_create.exe (Windows) / license_create
   (Linux) for this; you do NOT need to write or compile any C++ for
   license generation. Programmatic license generation requires
   headers that are not shipped in this package.

   Typical invocation (shown in bash continuation style; Windows
   cmd.exe users substitute `^` for `\`, or PowerShell users
   substitute a backtick, or simply put the whole thing on one
   line):

     tools/license_create --key private.pem \
       --vendor-license vendor_license.json \
       --id "LIC-001" \
       --licensee "End User Corp" \
       --product "YourApp" \
       --version "3.*" \
       --expires "2027-12-31T23:59:59Z" \
       --grace-days 7 \
       --fingerprint-value "<end user's fingerprint>" \
       --threshold 2 \
       --feature "feature_a" \
       --feature "feature_b"

   See §4.3 below for full flag documentation, including the
   floating-license and permanent-license forms. The end user's
   fingerprint is the value they get from running rg_fingerprint on
   their machine (§4.2).

B) YOUR END-USER APPLICATION (no vendor license needed):
   This is the application you ship to your end users.

    #include <rockyguard/rockyguard.h>

    static constexpr char PUBLIC_KEY[] = R"(-----BEGIN PUBLIC KEY-----
    ...your public key...
    -----END PUBLIC KEY-----)";

    int main() {
        // Guard construction: it is the only verifier call that throws.
        // See §2.4 and Customer_API_Reference §6.1 for why this matters
        // even when PUBLIC_KEY is an embedded literal.
        std::optional<rockyguard::LicenseVerifier> maybe_verifier;
        try {
            maybe_verifier.emplace(PUBLIC_KEY);
        } catch (const std::runtime_error& e) {
            std::cerr << "License system error: " << e.what() << std::endl;
            return 1;
        }
        rockyguard::LicenseVerifier& verifier = *maybe_verifier;

        auto result = verifier.load("license.json");
        if (!result) {
            std::cerr << result.message << std::endl;
            return 1;
        }

        result = verifier.check_node_locked();
        if (result.status == rockyguard::LicenseStatus::InGracePeriod) {
            std::cerr << "License expiring soon: "
                      << result.grace_days_remaining << " days left\n";
        } else if (!result) {
            std::cerr << result.message << std::endl;
            return 1;
        }

        // Optional version gate (v1.2.1+). Include this block if you
        // issue licenses with --version "3.*" (or similar) and want
        // the library to ENFORCE the range at runtime; omit it if
        // version_range should remain informational only.
        // check_version() is opt-in: the library never consults
        // version_range automatically. See Customer_API_Reference §6.7
        // for the supported syntaxes (glob and comparator).
        static constexpr char APP_VERSION[] = "3.1.0";
        result = verifier.check_version(APP_VERSION);
        if (!result) {
            std::cerr << "Version not allowed by license: "
                      << result.message << std::endl;
            return 1;
        }

        // Feature gating
        if (verifier.check_feature("feature_a")) {
            enable_feature_a();
        }

        return 0;
    }

The recommended startup sequence is therefore: load() -> check_node_locked() -> (optional) check_version() -> per-feature check_feature() calls. check_node_locked() does the heavy lifting (signature, hardware match, clock-manipulation detection, integrity self-check, expiry / grace evaluation); check_version() and check_feature() are cheap data lookups that run after. See Customer_API_Reference §6 "What each check method re-evaluates" for the full per-method coverage matrix.

3.4 What to Ship to Your End User
-----------------------------------

The vendor license, your private signing key, and the license_create / license_keygen tools from your customer zip NEVER ship to end users. Those stay on your build machine. What goes in the customer-facing distribution depends on the link variant you chose:

Static link (§3.1, the recommended default):

  - your_application.exe (or unsuffixed binary on Linux): your own
    executable with the RockyGuard library compiled in. No external
    RockyGuard files needed alongside it.
  - license.json: the end-user license you generated for this
    customer. Conventional location is next to the executable; if
    you load from a different path in your code, document it for the
    end user. One file per end user / per machine.

  That is the entire shipping list for the static path. Specifically NOT shipped: rockyguard.lib, rockyguard.dll, rockyguard.sig, public.pem (it is embedded into your executable as a string constant, not shipped as a file), the deps/ folder, or the rest of the extracted package directory.

Shared (DLL) link (§3.2):

  - your_application.exe: your own executable.
  - rockyguard.dll: the shared library. Same directory as the .exe.
  - rockyguard.sig: integrity signature for the DLL. Same directory
    as the DLL. The library refuses to load without it (see
    §9.3).
  - libcrypto-3-x64.dll, libssl-3-x64.dll: bundled OpenSSL runtime.
    Same directory as the .exe (Windows DLL search rules).
  - license.json: the end-user license, as in the static path.

  NOT shipped: rockyguard.lib (it is the import library, link-time
  only), the deps/ folder, anything from the package's lib/static/
  or tools/ directories.

Linux equivalents (same logical files, different extensions):

Linux static link:

  - your_application (unsuffixed binary): your own executable with the
    RockyGuard library compiled in.
  - license.json: the end-user license, as in the Windows static path.

Linux shared link:

  - your_application: your own executable.
  - librockyguard.so: the shared library. Same directory as the
    executable (or somewhere reachable via RUNPATH / LD_LIBRARY_PATH if
    you ship it elsewhere).
  - librockyguard.sig: integrity signature for the .so. Same directory
    as the .so. The library refuses to load without it.
  - license.json: the end-user license.

NO separate OpenSSL .so files are shipped on Linux: the Linux shared build statically links libssl and libcrypto into librockyguard.so itself (vcpkg's x64-linux triplet uses static OpenSSL), so the .so is self-contained for the crypto runtime. This is the inverse of the Windows shared build, which DOES ship libcrypto-3-x64.dll and libssl-3-x64.dll alongside rockyguard.dll. See §3.2 for the full discussion of the Windows-vs-Linux asymmetry.

End-user license file location is up to you. The library loads from whatever path you pass to LicenseVerifier::load(). Common conventions: alongside the executable (single-tenant desktop apps), under %APPDATA%\YourCompany\YourProduct\license.json on Windows, or $XDG_CONFIG_HOME/YourCompany/license.json on Linux.

THE DIRECTORY HOLDING license.json MUST BE WRITABLE BY THE USER RUNNING YOUR APPLICATION. This constrains the choice above and is easy to miss. The anti-rollback machinery in §9.2 keeps a local cache file named `.lck_cache_<tag>` NEXT TO the licence, and it is written during verification -- `check_expiry()` and `check_node_locked()` both touch it, on every run, not only the first. One file appears per licence id; they are not cleaned up.

The consequence for the "alongside the executable" convention is the one that bites: a Windows application installed to `%ProgramFiles%\YourApp\` runs read-only for a normal non-elevated user, so the cache write fails there every time. The same applies to `/usr/local/bin` or `/opt` on Linux, and to any read-only container image or signed app bundle.

What happens when the write fails: verification still SUCCEEDS. The cache is one of three anti-rollback witnesses (§9.2) and the other two live in per-user locations that remain writable, so losing it degrades tamper detection rather than breaking licensing -- your end user sees no error and your application keeps running. That is deliberate, and it is also why the problem is invisible in testing: nothing surfaces until someone audits why clock-rollback detection is weaker on deployed machines than on the developer's.

Recommendation: for an installed application, do NOT put license.json next to the executable. Use `%APPDATA%\YourCompany\YourProduct\` on Windows or `$XDG_CONFIG_HOME/YourCompany/` on Linux -- both are per-user and writable by construction. Reserve the alongside-the-executable convention for portable or single-user builds that live in a directory the user owns.

For a floating-licensing deployment (Premium tier, §5), the same per-customer "ship list" applies on the end-user side. The floating server itself is a SEPARATE deployment to a host inside your network, and it gets its own copy of the vendor license -- treat that machine with the same care as your build machine.

Third-party attribution -- one more file, on every ship list above
-------------------------------------------------------------------

This package bundles OpenSSL (Apache License 2.0). Every ship list in this section puts OpenSSL code into what you distribute, so every one of them makes you a redistributor of OpenSSL and Apache-2.0's attribution terms apply to your distribution, not only to ours.

On the Windows shared-link path this is visible: libcrypto-3-x64.dll and libssl-3-x64.dll are named on the list. On the two static paths and on the Linux shared path it is NOT visible, and that is the case people get wrong -- no OpenSSL file appears next to your binary, but OpenSSL is compiled or linked into it. `ldd`, `dumpbin /dependents` and the absence of a DLL beside your executable will all fail to remind you.

So add to every deployment:

  - deps/OPENSSL-LICENSE.txt: the Apache License 2.0 text, verbatim, as shipped in this package.

Where you put it is your choice -- next to your executable, inside an installer's licence folder, or folded into your own THIRD-PARTY-NOTICES file with the other components you ship. What matters is that the recipient of your application can obtain the licence text. deps/THIRD-PARTY-NOTICES.txt in this package states the exact OpenSSL version this zip carries, restates these obligations, and records that OpenSSL 3.x ships no NOTICE file (so Apache-2.0 section 4(d) adds nothing here). If your legal review needs the version in writing, cite that file rather than this manual -- it is generated from the headers in the zip and cannot drift from them.

RockyGuard's own licence is a separate matter and is unaffected by any of the above; see docs/RockyGuard_License_Agreement.txt and §12.4.

<!-- PAGEBREAK -->
================================================================================
4. END-USER LICENSE MANAGEMENT
================================================================================

The end-to-end flow has three actors: YOU (the library customer running license_create), YOUR end user (the person whose machine the license will run on), and YOUR application (which embeds the verifier and reads license.json at startup). Two flags on license_create control which machine the license binds to and they are easy to confuse:

- --fingerprint-value "<hash>"   The PRODUCTION path. Bind the license to the hash your end user sent you (they obtained it by running rg_fingerprint on their own machine; see §4.2). The license verifies on THEIR machine and only theirs.

- --fingerprint                   The SELF-TEST / TOOLCHAIN-CHECK path. Auto-detect THIS machine's fingerprint and bind the license to it. Convenient when you want to verify your keypair, vendor license, and license_create / verify pipeline end-to-end on your own build host without involving a real customer. Do NOT ship a self-fingerprinted license to an actual end user; it will fail verification on their machine because their hardware fingerprint is different.

If both flags are provided, --fingerprint-value wins. The Quick Start in §2.4 uses --fingerprint-value because that is what new customers actually want; the testing alternative is mentioned there as a one-liner.

4.1 Generating Your Keypair
------------------------------

Run once. Keep the private key secret; embed the public key in your app.

    tools/license_keygen --private private.pem --public public.pem

For RSA instead of Ed25519:

    tools/license_keygen --algo rsa --rsa-bits 2048 \
      --private private.pem --public public.pem

Choosing an RSA key size:

Ed25519 is the recommended default for new deployments -- it provides a 128-bit security level with smaller keys and signatures and faster verification, and there are no key-size choices to second-guess. The remainder of this sub-section applies only if you have a specific reason to use RSA (e.g., a corporate cryptographic-policy mandate, FIPS-validated module requirements, or interoperability with an existing RSA toolchain).

| --rsa-bits | Security level | Recommendation |
|------------|----------------|----------------|
| 1024 | ~80 bits | NOT supported by OpenSSL 3 at default security level; rejected at key generation. Do not use. |
| 2048 | ~112 bits | The library default. Adequate for license-signing workloads in 2026 and remains acceptable through the late 2020s per common guidance (NIST SP 800-57 Part 1, BSI TR-02102). Adequate for most products through the standard product lifecycle. |
| 3072 | ~128 bits | Recommended for keys you expect to use beyond ~2030. Same security level as Ed25519. Verification is roughly 2-3x slower than 2048 (still sub-millisecond on modern hardware -- no practical impact at the license-verify call site, which fires once at application startup). |
| 4096 | ~152 bits | Maximum margin against future cryptanalysis. Verification is roughly 5-7x slower than 2048; signing is several times slower than that. The cost is paid by YOUR license_create runs (signing) and your end users' application startup (verification). For a desktop application's once-at-launch verify the absolute latency is still in the low single-digit milliseconds; choose 4096 freely if your security policy mandates it. |

The library accepts any --rsa-bits value OpenSSL accepts (multiples of 8, subject to OpenSSL's per-version minimum at the configured security level). 2048, 3072, and 4096 are the values to consider; everything else is either too weak or non-standard. The library does not impose its own ceiling.

A note on key longevity: license-signing keys are typically long-lived because rotating the signing key requires shipping a new application binary with the new public key embedded AND re-issuing every customer's existing license under the new key (Customer_Documentation §9.1). Pick a key size strong enough to comfortably outlive the longest support window you expect to commit to for the products it signs.

Performance numbers above are approximate and intended only for rough sizing -- the actual throughput depends on your CPU, OpenSSL build, and whether hardware acceleration is available. Verification (the customer-facing path) is far cheaper than signing in all cases.

4.2 Getting an End User's Machine Fingerprint
------------------------------------------------

Ask your end user to run:

    rg_fingerprint -v

Or to save to a file they can send you:

    rg_fingerprint -v -o fingerprint.txt

For non-technical end users on Windows: ship them `rg_fingerprint_gui.exe` from the `tools/` directory of the customer zip instead of the CLI. It's a small GUI window (no terminal involved) that shows the component breakdown, displays the joined fingerprint hash in a copy-friendly box, auto-copies the hash to the clipboard on launch, and has a "Copy" button for manual recopying. Same fingerprint output as the CLI -- the two tools share their hardware-collection code in lockstep with the library, so a license issued against the CLI's output verifies correctly against `rg_fingerprint_gui`'s, and vice versa. See §7.1 for the full reference.

The output looks like:

    MAC address:    6c:2f:80:5e:e8:45
    CPU ID:         GenuineIntel-...
    Disk serial:    ...
    Motherboard ID: ...
    311867...|ff9f0f...|877f34...|f3b059...

The unlabeled line at the bottom is the composed hardware
fingerprint, four SHA-256 component hashes joined with pipe ('|')
in fixed order: MAC | CPU | Disk | Motherboard. Use that line
verbatim when creating the end user's license (pass it to
license_create --fingerprint-value). If a component cannot be
read on the target machine (for example, the disk serial is
unavailable inside a VM), the corresponding slot is the SHA-256
of the empty string and the library's match_count skips such
slots when computing hardware-match scores.

What if every component is empty? On a stripped Docker container
(no MAC adapter exposed, no /sys/class/dmi access, no SMART data),
on a CI runner deliberately scrubbed of host identifiers, or on
some sandboxed VM configurations, all four slots can come back
empty. In that case the fingerprint is four copies of
SHA-256("") joined by pipes, and on the verifier side
match_count returns 0 -- which fails the default
fingerprint_match_threshold of 2, so a node-locked license
issued for any other host is correctly rejected on this machine
(LicenseStatus::HardwareMismatch).

Two practical implications:

- Do NOT issue a node-locked license against an all-empty fingerprint expecting it to be accepted on the same machine later. The verifier still returns HardwareMismatch even against a matching all-empty fingerprint, because match_count of 0 is below the threshold of 2 by design.

- If you genuinely want a license to verify on machines with no readable hardware identifiers (server-room appliances, fully sandboxed end users), issue the license with --threshold 0 -- this is the explicit "not hardware-locked" setting; the verifier then accepts any fingerprint, including the all-empty one. See Customer_API_Reference §7.5 for the underlying match_count semantics.

4.3 Creating End-User Licenses
---------------------------------

All license_create invocations require --vendor-license pointing at
the vendor_license.json you received from your library vendor. The
tool will refuse to run without it.

Node-locked license:

    tools/license_create --key private.pem \
      --vendor-license vendor_license.json \
      --id "LIC-001" \
      --licensee "End User Corp" \
      --product "YourApp" \
      --version "3.*" \
      --expires "2027-12-31T23:59:59Z" \
      --grace-days 7 \
      --fingerprint-value "<end user's fingerprint>" \
      --threshold 2 \
      --feature "export_pdf" \
      --feature "advanced_reports"

Floating license (Premium tier):

    tools/license_create --key private.pem \
      --vendor-license vendor_license.json \
      --id "FLOAT-001" \
      --licensee "End User Corp" \
      --product "YourApp" \
      --type floating \
      --expires "2027-12-31T23:59:59Z" \
      --max-users 10

Permanent license:

    tools/license_create --key private.pem \
      --vendor-license vendor_license.json \
      --id "PERM-001" \
      --licensee "End User Corp" \
      --product "YourApp" \
      --expires permanent \
      --fingerprint-value "<fingerprint>"

License with metadata (v1.2.1+; pass --metadata key=value, repeatable):

    tools/license_create --key private.pem \
      --vendor-license vendor_license.json \
      --id "LIC-2026-0042" \
      --licensee "Acme Inc." \
      --product "YourApp" \
      --expires "2027-12-31T23:59:59Z" \
      --fingerprint \
      --metadata "customer_po=PO-2026-0042" \
      --metadata "region=NA" \
      --metadata "channel=direct"

See §8.2 for the canonical description of the metadata field (semantics, recommended use cases, read-side examples, size guidance) and §7.3 for the --metadata flag's parse rules (delimiter handling, empty-value behavior, repeated-key behavior, shell-quoting tips).

4.4 Verifying a License File
-------------------------------

Quick check (signature + payload only):

    tools/license_verify --key public.pem --license license.json

Full check (also re-runs the hardware fingerprint match and confirms a specific feature is licensed):

    tools/license_verify --key public.pem --license license.json \
      --check-hardware --check-feature "export_pdf"

4.4.1 Activation-Key Form (v1.3+)
------------------------------------

In addition to the `.lic` file form your library has used since
v1.2.0, the verifier accepts an "activation key" -- the same signed
envelope encoded as a single Base32 string with a recognizable
`RGAK-` prefix and dash-grouped 5-character bodies, e.g.:

    RGAK-A4DGB-2P67Q-RXKMN-4HEYS-...-Z9V77

This is a different transport for the same license -- it carries the
same payload, the same Ed25519/RSA signature, and produces the same
verification result as the `.lic` file. The advantage is delivery
ergonomics: an activation key can be pasted into a settings dialog,
sent inline in an email, or stored in a password manager, without
file-attachment workflows. Choose whichever form your customer
support flow prefers; both are equally valid.

Generating an activation key with the CLI:

    tools/license_create --key private.pem \
      --vendor-license vendor_license.json \
      --id LIC-A4D2-2026 --licensee "Acme Corp" \
      --product "MyApp" --expires "2027-12-31" \
      --output       license.lic        \
      --output-key   license.key

Both `--output` and `--output-key` may be passed in the same run --
they emit the same minted license in two formats; the generation
counter increments exactly once.

Generating an activation key from the portal: the license-issue page
shows both the `.lic file` download and a `Key` link side-by-side
after the license is minted; pick whichever channel fits the
customer.

Loading an activation key in your application:

    // Construction is guarded for the reason given in §12.2 and
    // Customer_API_Reference §6.1: the constructor throws on a key it
    // cannot parse, and an uncaught throw is std::terminate() on your
    // end user's machine rather than a message. The rest of this
    // snippet is about load_from_string(); the guard is not optional
    // just because the surrounding example is about something else.
    std::optional<rockyguard::LicenseVerifier> maybe_verifier;
    try {
        maybe_verifier.emplace(public_key_pem);
    } catch (const std::runtime_error& e) {
        std::cerr << "License system error: " << e.what() << "\n";
        return 1;
    }
    rockyguard::LicenseVerifier& verifier = *maybe_verifier;

    auto result = verifier.load_from_string(activation_key_text);
    // Same LicenseResult as load(path); same .status / .message
    // semantics; the verifier sees the same License object.

`load_from_string()` accepts EITHER a raw JSON envelope (v1.2.x
behavior, unchanged) OR a `RGAK-` activation key; the format is
auto-detected by prefix. Whitespace and case are tolerated on
activation keys -- a key pasted from email with stray line breaks or
typed in lowercase decodes the same as the canonical form.

If the key arrives as a FILE on disk rather than as text, use load()
and nothing else -- there is no need to read it into a string first:

    verifier.load("license.lic");   // JSON envelope        - works
    verifier.load("license.key");   // RGAK- activation key - also works

load() reads the file and hands the bytes to exactly the same detection
logic as load_from_string(), so both forms work through either entry
point. Detection is by CONTENT, not by file extension: the deciding
factor is whether the first non-whitespace characters are "RGAK". The
trailing newline that `license_create --output-key` leaves at the end of
a .key file is stripped along with the dash separators, so such a file
loads unmodified. In practice this means an application can accept
whatever the end user hands it -- a .lic, a .key, or a pasted string --
and route all three through the same two lines of code.

What the verifier reports on input failures:

    KeyMalformed          The string starts with RGAK but the body
                          contains chars outside the Base32 alphabet,
                          is truncated, or uses an unknown format
                          version. Detail message names the precise
                          failure mode.
    KeyChecksumMismatch   The Base32 body decoded but the trailing
                          CRC32 checksum does not match. Almost
                          always means a single-character typo
                          during transcription; recopy the full key
                          from the source.
    SignatureInvalid      The body decoded and checksums match, but
                          the Ed25519/RSA signature on the inner
                          envelope did not verify. The key has been
                          tampered with, OR it was generated for a
                          different vendor public key, OR the
                          customer is loading it into the wrong
                          product.
    MalformedFile         The string is neither a valid activation
                          key (no RGAK prefix) nor valid JSON.

The CRC32 is a customer-support aid, NOT a security primitive --
only the Ed25519/RSA signature on the inner envelope authenticates
the license. The CRC exists so that "you mistyped a character" and
"this key was tampered with" surface as distinguishable error states
instead of both reporting SignatureInvalid.

4.5 Fuzzy Hardware Matching
------------------------------

The fingerprint contains 4 hardware component hashes separated by '|'.
When verifying, each component is compared independently. The
fingerprint_match_threshold (default: 2) controls how many must match.

If an end user replaces one component (e.g. new network adapter), the
license still validates as long as enough other components match. Set
--threshold 2 for a good balance between security and convenience.

Unavailable hardware components (e.g., no disk serial on a virtual
machine) are automatically skipped during matching - they don't count
as a match or a mismatch. The library warns when a component is
unavailable during fingerprint generation.

4.6 Feature Gating
---------------------

Add features to a license with --feature (can repeat). In your app:

    if (verifier.check_feature("export_pdf")) {
        enable_pdf_export();
    }
    if (verifier.check_feature("advanced_reports")) {
        enable_advanced_reports();
    }

Create different license tiers by varying the feature list:

  Basic:      --feature "basic"
  Pro:        --feature "basic" --feature "export_pdf" --feature "reports"
  Enterprise: --feature "basic" --feature "export_pdf" --feature "reports"
              --feature "api_access" --feature "sso"

4.7 Grace Periods
-------------------

Set --grace-days N when creating a license. After expiry, the license
continues to work for N days with status InGracePeriod. Your app can
show a renewal warning:

    if (result.status == rockyguard::LicenseStatus::InGracePeriod) {
        show_warning("License expired. " +
                     std::to_string(result.grace_days_remaining) +
                     " days remaining before deactivation.");
    }

4.8 Web Portal (alternative to the CLI)
------------------------------------------

The customer zip ships a self-hosted FastAPI web portal under `portal/` that wraps `license_create` in a browser UI. The portal is the recommended workflow for non-technical licensing-admin staff who would otherwise have to learn the `license_create` flag set. Everything the portal does is just `license_create` underneath, so CLI users and portal users can coexist on the same vendor private key without coordination.

What the portal does, at a glance:

  - **Products page** -- define each product you license once (name, current version, default version range, the set of available features, the set of metadata keys you attach to licenses). Subsequent license generation pivots off the selected product so you never re-type product names or feature lists.
  - **Users page** -- manage the end-user customers you issue licenses to (name, email, organization, trial / paid status).
  - **License generation** -- pick a product, tick the feature checkboxes (auto-pre-filled from the most recently issued license for that product, so repeat-customer workflow is one click), fill metadata values, pick an expiry date from a calendar, click Generate. The user-detail page exposes each minted license in two delivery formats side-by-side: a `.lic file` download (the signed JSON file your library has used since v1.2.0) and a `Key` link (v1.3+) that opens the activation-key form (RGAK-prefixed Base32 string) in a plain-text browser tab for select-all + copy + paste-into-email delivery. Both forms are the same minted license -- the generation counter increments exactly once -- so pick whichever channel fits the customer.
  - **Batch generation** -- issue many node-locked licenses in one submission. Shared product / features / metadata / expiry at the top of the form; a per-row table for (name, organization, email, fingerprint) at the bottom; "Paste CSV..." for large batches. Submit produces all licenses + a `_manifest.json` + a ZIP download.
  - **Dashboard generation-counter widget** -- shows current end-user license count vs your vendor license's cap, with a progress bar that turns red below 20% remaining. Avoids the situation where minting hits the cap mid-workday because nobody was watching the counter.
  - **Vendor license tier auto-detection** -- the Settings page reads your vendor license JSON, surfaces tier (Basic vs Premium), licensee, expiry, model, and max licenses. Basic tier vendor licenses automatically hide the Floating option from product and license forms.
  - **File-picker buttons** in Settings for locating `license_create.exe`, your vendor license JSON, and your vendor private key -- no typing absolute paths.
  - **Login security** -- bcrypt-hashed admin password, per-IP brute-force lockout with exponential backoff (see §4.8.6).

The portal is OPTIONAL. Everything it does can be done from the CLI as described in §4.3. Customers who prefer the CLI for scripting, audit logging, or CI integration can ignore the portal entirely.

4.8.1 Launching the portal + first-run setup
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Launch (from the customer zip's `portal/` directory):

    Windows:    start.bat
    Linux:      ./start.sh
    PowerShell: ./start.ps1

The start scripts create a Python virtualenv on first launch and install dependencies from a bundled offline `wheels/` directory (no internet required), then prompt for an admin password. Subsequent launches are instant. The browser opens at `http://127.0.0.1:8765/`.

First-run Settings tasks (the portal will guide you):

  1. Click **Browse...** next to "license_create executable" and pick it from the customer zip: `tools/license_create.exe` on Windows, `tools/license_create` on Linux.
  2. Click **Browse...** next to "Vendor license file" and pick your `vendor_license.json` (provided separately by the library vendor).
  3. Click **Browse...** next to "Vendor private key" and pick the private signing key you generated with `license_keygen` -- the file this manual calls `private.pem` everywhere else. The portal UI and `portal/README.md` label it "vendor private key", which is the same file under a different name; it is NOT something the library vendor sends you. (The vendor sends the vendor LICENSE, `vendor_license.json`. The signing KEY never leaves your machine.)
  4. Leave **Signing algorithm** on "Auto-detect" unless you have a specific reason to force Ed25519 / RSA.
  5. Optionally upload a logo to brand the sidebar.

The portal reads your vendor license and displays the tier (Basic / Premium), licensee, expiry, and max-end-user-licenses cap below the path fields. Green ✓ / red ✗ pills next to each path indicate whether the file exists on disk.

Default network exposure is localhost-only (`host: 127.0.0.1` in `portal/config.json` -- that file does not exist in the zip; it is generated on first run, so look for it only after you have started the portal once). The customer can flip to `0.0.0.0` for intranet access behind their own VPN if multiple licensing-admin staff need to share one portal instance; HTTPS via reverse proxy is recommended for any non-localhost deployment.

4.8.2 Defining your products
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Open **Products** in the sidebar, click **+ New product**, and fill the form:

  - **Product name** (e.g. `FooApp`) -- used as the `--product` argument when minting licenses.
  - **Current version** (e.g. `2.3.1`) -- bump this each time you ship a release.
  - **Default version range** (e.g. `2.*`) -- pre-fills the license form. Use `2.*` for major-compat, `2.3.*` for minor-compat, `*` for any.
  - **Available features** -- one per line. These become checkboxes on the license form. Names should match the strings your application checks against `License::has_feature()`.
  - **Metadata keys** -- one per line (e.g. `region`, `channel`, `tier`). Just the schema; actual values are entered at license-generation time. Stored on the license as `License::metadata[key]` and not interpreted by the library (see §8.2).
  - **Default license type** (Node-locked / Floating). Floating is hidden when your vendor license is Basic tier.
  - **Default grace days**.

You can have any number of products. Each license is bound to one product. Deleting a product is safe -- existing issued licenses keep their snapshot of the product name and continue to verify; they just lose their link back to the product row.

4.8.3 Single license generation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Open **Users**, pick a user (or click **+ New user**), then **+ Generate license**. Select the product from the dropdown. The form populates:

  - Version range -- pre-filled from the product (or the most recent license for that product if any).
  - License type -- Node-locked / Floating (Floating only available on Premium-tier vendor licenses).
  - Expiry date -- pick from a calendar widget OR check "Permanent". Past dates are rejected with a pop-up warning.
  - Grace days -- pre-filled from the product default.
  - Fingerprint match threshold -- 0-4 (default 2).
  - Hardware fingerprint -- paste the value the end user got from `rg_fingerprint` / `rg_fingerprint_gui`.
  - Features -- checkboxes generated from the product's available_features. Pre-checked from the most recent license issued for this product, so issuing the same license profile to another end user is one click.
  - Metadata -- one row per metadata key declared on the product. Leave a value blank to omit that key from the license.

Submit. The `.lic` file lands at `portal/licenses/<sanitized-email>/<timestamp>.lic` and a download link appears on the user-detail page.

4.8.4 Batch license generation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For issuing many node-locked licenses in one submission (e.g., a 50-seat company rollout): sidebar **Batch generate** or **+ Batch generate** on the Licenses page.

Shared fields (single product / features / metadata / expiry / grace / threshold) at the top. Per-row table at the bottom: **Name**, **Organization**, **Email**, **Fingerprint**. Click **+ Add row** to extend or **× ** on a row to remove. For large batches click **Paste CSV...** and paste:

    Alice Lee,Acme Corp,alice@acme.com,ABC123-MAC-CPU-DISK-MOBO
    Bob Singh,Acme Corp,bob@acme.com,DEF456-MAC-CPU-DISK-MOBO

Header row optional (auto-detected if the first cell is `name`). Click **Replace rows with CSV** -- the rows in the table get replaced and you can still edit any individual row before submitting.

Submit produces:

  - One `.lic` file per row at `portal/licenses/_batch/<batch_id>/<email>.lic`.
  - A `_manifest.json` capturing the shared-field snapshot and the per-row outcome (success / failure with reason).
  - A result page with **Download all as ZIP** + per-row status pills.

Per-row license-create failures (e.g., bad fingerprint format, vendor-license error) don't abort the batch -- they're reported on the result page with the error message, and the rest of the batch proceeds.

Validation: name / email / fingerprint required per row; duplicate emails within one batch are rejected.

Batch is node-locked only (each row carries a unique fingerprint). For floating licenses, use single-license generation -- there is no batch-floating mode.

4.8.5 Generation-counter dashboard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The Dashboard shows a "Vendor license & generation counter" card with:

  - Tier pill (Basic / Premium)
  - Licensee + model + expiry
  - **Progress bar**: current end-user licenses generated vs your vendor license's cap (`max_end_user_licenses` in the vendor JSON). Turns red when remaining drops below 20%.

The counter comes from shelling out `license_create --show-count --vendor-license <path>`. If your vendor license has `max_end_user_licenses: 0`, the cap is unlimited and the card shows the running count without a progress bar.

If the card says **"Generation counter unavailable: ..."** there's a Settings misconfiguration (most commonly `license_create.exe` or the vendor license JSON pointing at a wrong path). Open Settings and verify the green ✓ next to each path.

4.8.6 Login security (brute-force protection, v1.3+)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The portal's admin login is protected against brute-force attacks. After 5 consecutive failed password attempts from the same source IP, that source is locked out:

    1st lockout: 1 minute
    2nd lockout: 5 minutes
    3rd lockout: 15 minutes
    4th+ lockout: 60 minutes (capped)

Lockout state is persisted to `portal/lockout.json` and survives a portal restart -- killing the server does NOT reset the counter. Each failed attempt also incurs an artificial sleep (2 / 4 / 8 / 10 seconds, capped at 10) before the response is returned, which slows brute-force tools and removes the response-time side channel between "wrong password, bcrypt ran" and any internal short-circuit path.

A successful login from a previously failing IP clears that IP's full failure / lockout history; one proof-of-knowledge resets the penalty.

**Recovery if you lock yourself out and don't want to wait**: delete `portal/lockout.json` and refresh the login page. The file is regenerated automatically on the next failed attempt; no other state is lost.

X-Forwarded-For is honored when the portal sits behind a reverse proxy -- the lockout counter keys on the first entry of XFF (the original client IP) rather than the proxy's IP, so a single shared proxy in front of a multi-user deployment doesn't get all-users-locked-out by one bad actor.

**Operational visibility**: when a lockout fires, the portal logs a structured line to stderr in the form `[portal] lockout: ip=<source> lockout_count=N locked_for=Ns`. Pipe the portal's stderr to a log file if you want a persistent record of lockout events for audit / compliance review.

4.8.7 Backing up the portal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Two paths are worth backing up:

  - **`portal/portal.db`** -- your customer + license history database (SQLite).
  - **`portal/licenses/`** -- the `.lic` files themselves. Your end users may also have their own copies, but this is the master.

`portal/config.json` contains the hashed admin password and session secret; it can be regenerated by deleting it and re-running the launch script for your platform (`start.bat` on Windows, `./start.sh` on Linux), which prompts for a new admin password. Other regenerable files (`lockout.json`, `.venv/`) don't need backup.

For a fuller treatment of every portal feature including click-by-click walkthroughs, see `portal/README.md` shipped in the customer zip's `portal/` directory.

<!-- PAGEBREAK -->
================================================================================
5. FLOATING LICENSING (PREMIUM TIER)
================================================================================

Floating licensing requires a Premium tier library license. If you have
a Basic tier, contact the library vendor to upgrade.

5.1 Creating a Floating License
----------------------------------

    tools/license_create --key private.pem \
      --vendor-license vendor_license.json \
      --id "FLOAT-001" --licensee "Corp" --product "App" \
      --type floating --max-users 10 \
      --expires "2027-12-31T23:59:59Z"

--vendor-license is required by every license_create invocation (see §4.3); the tool refuses to run without it. The example above shows only the floating-specific flags; for the full flag set see §4.3.

5.2 Running the License Server
---------------------------------

The floating license server requires a vendor license (Premium tier). Deploy it on a machine accessible to all end users. The supported customer path is the shipped rg_floating_server binary configured via a YAML file (described in this section); ship the binary plus its YAML config to the host you want to run the server on. The programmatic FloatingLicenseServer C++ class is NOT part of the customer-distributed header set, in v1.3 or any earlier release. Your customer zip contains floating_client.h but no floating_server.h, so there is nothing to include and nothing to link against: the server is a process you run, not a class you construct. If you have a strong need to embed the server inside your own service rather than running it as a separate process, contact the vendor to discuss a custom build that exposes the header; this is a v1.4+ scope question (see Future_Roadmap).

ON THE C++ FIELD NAMES IN THIS SECTION. YAML is the only configuration surface available to you, and every setting below is reachable from it. Where this manual also names a C++ field -- "YAML key lease_timeout; C++ struct field FloatingServerConfig::lease_timeout_sec" -- that is a CROSS-REFERENCE, not a second way to configure the server. The names are given because the server's own log lines, error messages, and this manual's glossary use the C++ spelling, which differs from the YAML spelling often enough (lease_timeout vs lease_timeout_sec, private_key vs signing_private_key_pem) to be worth mapping. Read them as a translation table for interpreting output, not as an API you are expected to call. If you find yourself wanting to set one of these fields from C++, you want the YAML key instead.

Configuration file:

Create a config file (e.g. server_config.yaml). The example below shows every recognised key. Required keys are marked; everything else is optional with the default value shown. As shipped it is configured for LOCAL EVALUATION -- it enables allow_insecure_http rather than TLS, because a first-run config cannot assume you already have a certificate. Before putting the server on any network you do not fully control, set tls_cert / tls_key and delete the allow_insecure_http line.

    # Vendor license file (REQUIRED, Premium tier).
    vendor_license: vendor_license.json

    # License pool size (REQUIRED).
    max_users: 10

    # Per-machine seat cap (v1.2.1+; defends against ghost-checkout
    # exhaustion). 0 (default) = uncapped, preserving v1.2.0 behavior.
    # Set above the expected steady-state concurrency on one workstation
    # to leave headroom for crash-recovery (ghost leases count toward
    # this cap until the reaper TTL fires).
    max_leases_per_machine_id: 0

    # Server settings.
    port: 8080
    heartbeat_interval: 60
    lease_timeout: 120                    # Seconds. The YAML key is
                                          # lease_timeout (no _sec suffix);
                                          # the corresponding C++ struct
                                          # field is FloatingServerConfig::
                                          # lease_timeout_sec. Both names
                                          # appear in this document depending
                                          # on whether the discussion is
                                          # about operator tuning (YAML) or
                                          # the underlying API field.

    # Thread pool and client timeout.
    thread_pool_size: 4
    client_timeout: 5                     # Seconds. Same YAML/struct split
                                          # as lease_timeout above: the C++
                                          # field is FloatingServerConfig::
                                          # client_timeout_sec.

    # Response signing (recommended): server signs every response.
    private_key: private.pem              # Path to PEM file. The C++
                                          # struct field that holds the
                                          # loaded PEM contents is
                                          # FloatingServerConfig::
                                          # signing_private_key_pem.

    # TLS encryption: uncomment BOTH to enable HTTPS.
    # tls_cert: server.crt
    # tls_key: server.key

    # Cleartext opt-in (v1.3+, default false). REQUIRED IF YOU LEAVE
    # tls_cert / tls_key COMMENTED OUT. As of v1.3 the server REFUSES
    # TO START with neither a cert/key pair nor this flag; pre-v1.3 it
    # fell through to plain HTTP with only a log warning. Set it true
    # only for local evaluation or an already-encrypted transport
    # (loopback, service mesh, WireGuard). See the discussion below.
    allow_insecure_http: true

    # Belt-and-braces (v1.3+, default false). When true, startup
    # requires tls_cert/tls_key even if allow_insecure_http is set, so
    # an operator cannot launch a plaintext server by mistake in an
    # environment where encryption is mandatory.
    # require_tls: true

    # Per-IP rate limiting (v1.3+). Token bucket per client IP; one
    # token is consumed per incoming connection BEFORE the TLS
    # handshake, so floods are rejected cheaply. rate_limit_rps is the
    # steady refill rate, rate_limit_burst the bucket capacity.
    # Defaults shown are generous -- normal traffic is one heartbeat
    # per client per heartbeat_interval. Set rate_limit_rps <= 0 to
    # disable the limiter entirely.
    # rate_limit_rps: 50
    # rate_limit_burst: 250

    # Server mode: release or debug.
    mode: release

    # Log to file (uncomment to enable).
    # log_file: server.log

    # Log rotation (v1.2.1+; only applies when log_file is set).
    # Defaults shown below: rotate at 100 MiB, keep 5 archives. Set
    # log_max_bytes <= 0 to disable rotation (v1.2.0 unbounded growth).
    # log_max_bytes: 104857600
    # log_keep_count: 5

Run the server:

    rg_floating_server server_config.yaml

A sample config file (floating_server_config.yaml) is included in the tools/ directory.

Server modes:

The mode key controls log verbosity. Two values are accepted.

In release mode (the default), the server logs basic events only: server start and stop, license checkouts and checkins, lease expirations, and errors. This is suitable for production use.

In debug mode, the server logs everything from release mode plus full request and response details, client connection info (IP and port), heartbeat events, periodic lease-table dumps with heartbeat ages, worker thread activity, and client timeouts. Use for troubleshooting; the volume is high.

Log output:

By default, all log output goes to stderr. To log to a file, set the log_file field in the config; logs are appended, not overwritten. The format is:

    2026-04-09 20:25:23.533 [INFO] Server started on port 8080
    2026-04-09 20:25:24.100 [DEBUG] Connection from 192.168.1.5:52341
    2026-04-09 20:25:24.102 [INFO] Checkout: client abc-123 - license granted (1/10)

Log rotation (v1.2.1+, file-backed loggers only):

When log_file is set, the active log file rotates as soon as it reaches log_max_bytes (default 100 MiB). The rename cascade is:

    server.log    -> server.log.1
    server.log.1  -> server.log.2  (prior .2 promoted to .3, and so on)
    server.log.N  -> dropped       (where N = log_keep_count)

A fresh server.log is opened immediately after the rename. Worst-case disk usage per log_file path is bounded by log_max_bytes * (log_keep_count + 1) -- 600 MiB at the defaults. Set log_max_bytes <= 0 to disable rotation entirely (preserves the v1.2.0 unbounded-growth behavior). log_keep_count <= 0 means "rotate but keep no archives" -- each rotation truncates the log. log_keep_count is silently clamped to the range [0, 100] -- values above 100 are pinned to 100 so a misconfigured "keep a million archives" cannot turn each rotation into an O(N) filesystem walk. Rotation runs under the writer mutex, so log lines are never lost or torn across the rename cascade. Stderr-backed loggers do not rotate; the host's logging system handles that path.

Per-machine seat cap (v1.2.1+):

max_leases_per_machine_id bounds the number of concurrent leases any single workstation can hold. Without it, a flaky client that crashes and restarts before sending checkin can multiply its lease hold (the ghost lease persists until lease_timeout fires). With it set, the (cap+1)th checkout from the same machine is denied with the dedicated error "machine seat limit reached" (LicenseStatus::MachineSeatLimitReached on the client side), even if the global pool still has seats free for other machines.

Sizing guideline: set the cap ABOVE the steady-state concurrency you expect on one workstation. Ghost leases count toward this cap until the reaper TTL (lease_timeout) fires, so a tight cap means a crashed client temporarily blocks one slot for the reaper TTL after restart. A reasonable default for a CAD-style product where one user usually runs at most three concurrent sessions is cap=4 or cap=5: legitimate concurrency works, but a single machine still cannot drain a 50-seat pool by crashing in a tight loop.

Thread pool and connection handling:

The server uses a thread pool. The main thread accepts connections and dispatches them to a pool of worker threads. Each worker sets a receive timeout on the client socket to protect against slow or malicious clients.

thread_pool_size (default 4) sets the number of worker threads. Each handles one connection at a time; raise it for high-concurrency environments.

client_timeout (default 5 seconds) drops a connection if the client connects but does not send data within this time. Protects against denial-of-service attacks where a client connects and holds the connection open.

Per-IP rate limiting (v1.3): rate_limit_rps (default 50) and rate_limit_burst (default 250) apply a token bucket per client IP address, checked before the TLS handshake so a flood is rejected cheaply. rate_limit_rps is the steady refill rate in requests/second; rate_limit_burst is the largest instantaneous spike tolerated from one IP. Over-limit connections receive HTTP 429 (plain HTTP) or are dropped (TLS). The defaults are generous -- normal traffic is one heartbeat per client per heartbeat_interval, so even a large NAT'd site sits far below the steady rate and the burst absorbs a mass cold-start. Set rate_limit_rps to 0 to disable the limiter entirely (for example if you front the server with a reverse proxy that already does rate limiting). Note that the unauthenticated /status endpoint is throttled by the same limiter; it exposes only pool counts.

The server and client both handle TCP fragmentation correctly: HTTP requests and responses that arrive in multiple packets are reassembled using Content-Length before parsing.

Response signing and TLS:

The server can sign every response with a private key; the client verifies the signature using the corresponding public key (FloatingClientConfig::server_public_key_pem). This prevents server spoofing (e.g., someone redirecting traffic to a fake server that always returns "checked_out"). Set private_key in the server config to the path of an Ed25519 or RSA private key, and ship the corresponding public key with the client.

Recommended: use a SEPARATE keypair for server response signing, not the keypair used for license signing.

The server's signing key and the license-signing key are independent fields in the library: the server takes its key via private_key in the YAML config; the license-signing key is the one you pass to license_create --key. Nothing enforces they be the same. Using a dedicated server keypair is the recommended configuration because it separates two distinct trust roles:

- The license-signing key authorizes minting of new end-user licenses. Compromise mints unauthorized licenses but does not let an attacker impersonate your live floating server.
- The server-signing key authenticates live server responses (checkout, heartbeat, checkin). Compromise lets an attacker run a fake server but does not let them mint licenses.

If you reuse one keypair for both roles, a single compromise gives the attacker both capabilities at once. Generating a second keypair is a one-time cost (tools/license_keygen --private server_signing_priv.pem --public server_signing_pub.pem); the operational handling is otherwise identical to the license keypair. Embed the server signing PUBLIC key in your end-user binary as FloatingClientConfig::server_public_key_pem (alongside, but distinct from, the license-verification public key embedded in LicenseVerifier).

Acceptable for very small or pre-production deployments: reusing the license-signing keypair is a supported path -- the field is independent precisely so you can choose. If you go that route, do it with the explicit understanding that the two compromise scenarios above collapse into one. For any production deployment exposed to a network you do not fully control, generate a separate server keypair.

Rotation: you can rotate the server key independently of the license-signing key as long as you ship a new application binary carrying the new server public key (clients pin server identity via the embedded key). Rotating the license-signing key has the heavier cost called out in §9.1 (re-issuing every existing customer's license under the new key). Separating the two keypairs lets you respond to a server-side incident without that license-re-issuance cost.

For additional traffic encryption, enable TLS by providing a certificate and key. Generate a self-signed certificate:

    openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
      -days 3650 -nodes -keyout server.key -out server.crt \
      -subj "/CN=127.0.0.1" \
      -addext "subjectAltName=IP:127.0.0.1,DNS:localhost"

THE CERTIFICATE IDENTITY MUST MATCH THE HOST YOUR CLIENTS DIAL. The -addext line above is not optional decoration. OpenSSL 3 verifies the server's hostname against the certificate's subjectAltName extension, and a certificate with no SAN is rejected no matter what its CN says. Replace the SAN values with the host your clients actually pass as FloatingClientConfig::server_host -- add DNS:license.example.com for a DNS name, IP:10.0.0.5 for an address, or list several comma-separated as above. The command as written is for a client dialling 127.0.0.1 or localhost, which is what the shipped floating_server_config.yaml and the rg_floating_client examples use.

Getting this wrong produces a confusing failure, so it is worth recognising: the client reports "Cannot reach license server" (LicenseStatus::ServerUnreachable), which reads like the server is down or firewalled. The TCP connection actually succeeded and only certificate verification failed. If you see that message while the server log says the process started and is listening, check the certificate's SAN before you check the network:

    openssl x509 -in server.crt -noout -ext subjectAltName

An empty result means the certificate is the problem. Section 10.5.2 lists the other causes.

PREREQUISITE: this command needs the OpenSSL command-line tool, which is NOT included in your customer package -- the package bundles the OpenSSL libraries the library links against, not the CLI. On Linux install it from your distribution (openssl on Debian/Ubuntu/RHEL). On Windows it ships with Git for Windows at C:\Program Files\Git\mingw64\bin\openssl.exe, or install it separately. Any tool that can emit a PEM certificate with a subjectAltName works; openssl is simply the one this command uses.

Then set tls_cert and tls_key in the server config file. On the client, set use_tls = true and tls_ca_cert_path = "server.crt" to enable certificate verification.

As of v1.3, a client with use_tls = true and an empty tls_ca_cert_path fails closed: the FloatingLicenseClient constructor throws, because TLS without a CA certificate encrypts traffic but cannot verify the server identity (it is vulnerable to man-in-the-middle attacks). If you genuinely want to accept any certificate -- for example in a controlled pre-production test -- set allow_insecure_tls = true; the client then proceeds but prints a one-shot MITM warning to stderr. Prefer setting tls_ca_cert_path for any real deployment.

Two related server-side knobs (v1.3):
- require_tls (config key require_tls): when true the server refuses to start unless both tls_cert and tls_key are set. Use it to guarantee an operator cannot accidentally launch a plaintext server.

- allow_insecure_http (config key allow_insecure_http): BREAKING CHANGE IN v1.3. The server now refuses to start when tls_cert/tls_key are not both configured unless you set allow_insecure_http: true. Up to v1.2.x, omitting the certificate silently produced a working cleartext server with only a log warning. If you upgrade a plaintext deployment, the server will not start until you either configure TLS or add this key; the startup error names the key so the fix is one line. The client half is symmetric: FloatingClientConfig::use_tls = false now throws from the FloatingLicenseClient constructor unless FloatingClientConfig::allow_insecure_http = true.

  Why this fails closed rather than warning: the checkout response carries session_secret, the HMAC key that signs every subsequent request. An eavesdropper who captures one plaintext checkout can forge any later request, so cleartext does not merely weaken client authentication, it nullifies it. Response signing does not compensate, because it provides authenticity, not confidentiality.

  Note that allow_insecure_http and allow_insecure_tls are different settings on different axes, and confusing them is easy. allow_insecure_tls relaxes certificate VERIFICATION once TLS is already enabled; allow_insecure_http governs whether there is any encryption at all. Leaving allow_insecure_tls false does not protect you from cleartext.

  Set allow_insecure_http only for local development, or where the transport is already encrypted by other means (loopback, a service mesh, a WireGuard tunnel).
- When TLS is disabled the client and server each emit a plaintext warning at startup, because the session secret and license traffic travel in the clear. Enable TLS for production.

5.3 Client Integration (No Vendor License Needed)
----------------------------------------------------

In your end-user application (see examples/rg_floating_client.cpp
for a complete working example):

    rockyguard::FloatingClientConfig config;
    config.server_host = "license-server.internal";
    config.server_port = 8080;
    config.heartbeat_interval_sec = 30;

    // The SERVER SIGNING public key -- NOT the license-verification
    // public key you pass to LicenseVerifier. These are two different
    // keypairs serving two different trust roles; see 5.2 above. Naming
    // them distinctly in your own code is worth the small effort, since
    // pasting the wrong one here silently collapses that separation.
    config.server_public_key_pem = SERVER_SIGNING_PUBLIC_KEY;

    // TLS is required as of v1.3: constructing a client with
    // use_tls = false throws unless allow_insecure_http is set. See 5.2.
    config.use_tls = true;
    config.tls_ca_cert_path = "server.crt";     // Verify server certificate

    // Local development against a plaintext server only -- never ship
    // this. Replaces the two lines above rather than joining them.
    // config.allow_insecure_http = true;

    rockyguard::FloatingLicenseClient client(config);

    auto result = client.checkout();
    if (!result) {
        std::cerr << result.message << std::endl;
        return 1;
    }

    // Application runs... heartbeat is automatic
    // License is returned on client.checkin() or destructor

v1.3+: the client also offers four heartbeat-lifecycle callbacks
(on_connection_lost, on_connection_restored, on_license_revoked,
on_give_up) plus a retry-budget knob (max_heartbeat_failures). Wire
these on FloatingClientConfig::callbacks to drive UI state from lease
events instead of polling is_checked_out(). See Customer_API_Reference
§8.7 for the full semantics, threading model, and re-entrancy contract.

5.4 Server HTTP Endpoints
----------------------------

    POST /checkout    { "client_id": "uuid", "machine_id": "..." }   -> 200 or 403
    POST /checkin     { "client_id": "uuid", "machine_id": "..." }   -> 200
    POST /heartbeat   { "client_id": "uuid", "machine_id": "..." }   -> 200
    GET  /status                                                      -> { total, active, available }

The machine_id field carries the host's hardware fingerprint -- the same |-joined
SHA-256 hashes (MAC, CPU ID, disk serial, motherboard ID) used for node-locked
verification. The server stores the hashes only; raw hardware values never leave
the host. v1.2.1+ servers use machine_id to enforce max_leases_per_machine_id;
v1.2.0 servers logged the field but did not act on it. Three further fields are
sent on every POST but elided here for clarity: a fresh nonce, a monotonic
sequence counter, and a session-secret-keyed HMAC (replay protection + client
authentication). See Customer_API_Reference §8.3 for the full wire-data
inventory.

Leases are automatically released if a heartbeat is not received within
the lease_timeout window (YAML key lease_timeout, default 120 seconds;
the matching C++ struct field is FloatingServerConfig::lease_timeout_sec).

<!-- PAGEBREAK -->
================================================================================
6. HARDWARE FINGERPRINTING
================================================================================

6.1 Collected Components
--------------------------

  Component        Source
  ---------------- -----------------------------------------------
  MAC address      Physical NIC (virtual adapters filtered out)
  CPU ID           Vendor + brand string
  Disk serial      Boot disk serial number
  Motherboard ID   Serial number or platform UUID

On Windows, Ethernet NICs are preferred over Wi-Fi. Virtual adapters
(VMware, VirtualBox, Hyper-V, Docker, VPN, WireGuard, Bluetooth, etc.)
are automatically filtered out.

6.2 Using the Fingerprint API
--------------------------------

    auto hw = rockyguard::HardwareFingerprint::collect();
    std::cout << "MAC:  " << hw.mac_address << "\n";
    std::cout << "CPU:  " << hw.cpu_id << "\n";

    // Get full fingerprint string
    std::string fp = rockyguard::HardwareFingerprint::fingerprint();

    // Compare fingerprints (returns 0-4 matching components)
    int matches = rockyguard::HardwareFingerprint::match_count(fp1, fp2);

<!-- PAGEBREAK -->
================================================================================
7. CLI TOOLS REFERENCE
================================================================================

7.1 rg_fingerprint (CLI)
--------------------------

    Usage: rg_fingerprint [options]

    Print the machine's hardware fingerprint.

    -v, --verbose         Show individual hardware components
    -o, --output <file>   Write fingerprint to file (default: stdout)

    Examples:

      # Print fingerprint to screen
      rg_fingerprint

      # Show detailed components + fingerprint
      rg_fingerprint -v

      # Save to file (to send to vendor or use with license_create)
      rg_fingerprint -o fingerprint.txt

      # Capture it in a script (v1.3+)
      rg_fingerprint > fingerprint.txt
      FP=$(rg_fingerprint)

Output streams. The fingerprint is the tool's only data output and goes to STDOUT, alone and unadorned. Everything else -- the startup banner, the `-v` component dump, error text, the double-click pause prompt -- is diagnostic and goes to STDERR. So `rg_fingerprint > fp.txt` and `FP=$(rg_fingerprint)` both capture the bare fingerprint and nothing else, and `rg_fingerprint -v > fp.txt` writes the hash to the file while showing you the component breakdown on screen. (The banner additionally suppresses itself when stderr is not a terminal, and honours ROCKYGUARD_NO_BANNER.)

Before v1.3 the fingerprint went to stderr along with everything else, so both redirect idioms produced an empty file. If you have a script that worked around this by capturing stderr, it will now capture the banner and any `-v` output instead of the hash; read stdout instead.

When launched by double-click from Windows Explorer (rather than from an open command prompt), `rg_fingerprint.exe` detects this -- via the GetConsoleProcessList API -- and pauses with `Press Enter to close...` before exiting, so the user has a chance to read the output before the console window closes. The pause is suppressed when run from an existing terminal session so dev workflows are unchanged.

7.1.1 rg_fingerprint_gui (Windows GUI; v1.3+)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`rg_fingerprint_gui.exe` is a Windows-native GUI counterpart to the CLI tool above, shipped alongside it in `tools/`. Same fingerprint output (the two tools share their hardware-collection / virtual-adapter-filter / LAA-bit-check code via the `rg_fingerprint_shared.h` header, so output is identical), different audience: end users who would double-click an .exe from Explorer rather than open a terminal.

What the window shows:

  - "Your hardware fingerprint" page title.
  - A short instruction line: "Send this to your software vendor so they can issue a license bound to this machine."
  - The four component values (MAC address, CPU ID, Disk serial, Motherboard ID) in a labeled grid.
  - The joined fingerprint hash in a boxed read-only edit field (selectable text, monospace font).
  - A "Copy" button (copies the fingerprint hash to the clipboard; flashes "Copied!" for visual feedback) and a "Close" button.
  - A "✓ Copied to clipboard" status message after Copy is clicked.
  - The Rocky Software brand logo in the top-right corner.

On launch the GUI **auto-copies** the fingerprint hash to the clipboard, so a user who immediately Alt-Tabs to their email client and hits Ctrl+V doesn't need to click the Copy button first.

Implementation notes useful to downstream support:

  - Compiled with `/SUBSYSTEM:WINDOWS` so no console window flashes on launch.
  - Embeds a Common-Controls v6 manifest so buttons / edit fields render with the modern Windows 10/11 themed look rather than the Win9x 3D bezel.
  - PerMonitor-V2 DPI aware -- crisp on 4K laptops without bitmap stretching. The window resizes after creation based on the actual monitor's DPI.
  - Source compiled with /utf-8 so the U+2713 CHECK MARK glyph in "✓ Copied" renders correctly regardless of the build machine's system code page.

Customer support tip: when a license issued to a customer's fingerprint suddenly fails to verify after a hardware change, ask the customer to re-run `rg_fingerprint_gui.exe` and send the new fingerprint. The match-count threshold (default 2 of 4) is forgiving for a single component change (one new MAC, one new disk), but two simultaneous changes (motherboard + disk replacement, or VM migration) will trip below the threshold and require a re-issued license.

There is currently no Linux / macOS GUI build of this tool; Linux / macOS end users use the CLI `rg_fingerprint`. The GUI is on the v1.4+ roadmap for cross-platform expansion when a customer asks.

7.2 license_keygen
--------------------

    Usage: license_keygen [options]

    --algo <ed25519|rsa>   Algorithm (default: ed25519). Ed25519 is
                           recommended; RSA is provided for policy /
                           interoperability reasons only. There is no
                           "auto" choice here -- auto-detection is not
                           applicable to key generation (you are
                           creating a key, not loading one).
    --rsa-bits <bits>      RSA key size when --algo rsa (default: 2048).
                           Ignored when --algo ed25519. Recommended
                           values: 2048 for typical deployments through
                           the late 2020s; 3072 for keys expected to
                           live past ~2030; 4096 for maximum margin /
                           policy mandates. Smaller values are rejected
                           by OpenSSL at default security level. See
                           §4.1 "Choosing an RSA key size" for the full
                           recommendation table including security
                           levels and verification-cost notes.
    --private <file>       Private key output (default: private.pem)
    --public <file>        Public key output (default: public.pem)

7.3 license_create
--------------------

    Usage: license_create [options]

    --key <file>                Private key PEM file (required)
    --vendor-license <file>     Vendor license file (required for every
                                invocation that generates or inspects an
                                end-user license, including --show-count;
                                see §4.3). Same vendor_license.json the
                                library vendor sent you (§2.2).
    --algo <auto|ed25519|rsa>   Signature algorithm (default: auto;
                                v1.2.1+). "auto" inspects the private
                                key and picks ed25519 or rsa
                                automatically. Pass an explicit value
                                only if you want to ASSERT one
                                algorithm (e.g., a security policy
                                that rejects RSA after migrating to
                                Ed25519). Unknown values are
                                rejected; in v1.2.0 a typo silently
                                fell back to ed25519.
    --output <file>             Output file (default: license.json)
    --output-key <file>         ALSO write a short activation key to
                                <file> (v1.3+). This is the flag that
                                produces the Base32 activation key
                                described in §4.4.1 -- the headline v1.3
                                feature -- and it is easy to miss because
                                a run without it silently produces only
                                the .json. Both --output and --output-key
                                may be passed in one invocation; see
                                §4.4.1 for when to hand an end user a key
                                rather than a file, and §9.8 for the
                                trailing-newline note when transcribing.
    --id <id>                   License ID (required, must be unique)
    --licensee <name>           Licensee name
    --product <name>            Product name (required)
    --version <range>           Version range. Glob form ("3.*",
                                "3.1.*", "3.1.5") or comparator form
                                (">=3.0,<4.0", "!=3.5.0"). Empty
                                or "*" matches any version. Enforced
                                by LicenseVerifier::check_version()
                                (v1.2.1+); see Customer_API_Reference §6.7 for the full syntax.
    --type <node_locked|floating>  License type (default: node_locked)
    --expires <date>            Expiry (ISO 8601) or "permanent"
    --grace-days <n>            Grace period in days (default: 0)
    --max-users <n>             Max concurrent users (floating only)
    --feature <name>            Add a feature (repeatable)
    --metadata <key=value>      Add a metadata pair (repeatable; v1.2.1+).
                                Round-tripped to verifier-side as
                                License::metadata[key]; useful for
                                customer-internal IDs (PO, account ID),
                                region tags, channel labels, anything
                                you want signed alongside the license
                                without changing the schema. The library
                                does not interpret these.

                                Parse rules:
                                  - The FIRST '=' is the delimiter. Key
                                    is everything before it; value is
                                    everything after it. A value may
                                    therefore contain '=' freely:
                                    --metadata "formula=a=b+c" sets
                                    key="formula", value="a=b+c".
                                  - An argument with no '=' is rejected
                                    with "Error: --metadata requires
                                    key=value form" and the tool exits
                                    non-zero. Pass key= (with a trailing
                                    '=' and nothing after) for a
                                    presence-flag entry.
                                  - An empty key (the argument starts
                                    with '=', e.g. --metadata "=foo")
                                    is rejected with "Error: --metadata
                                    key cannot be empty".
                                  - An empty value is ALLOWED:
                                    --metadata "is_trial=" sets the
                                    key with an empty string value.
                                    Use this when the presence of the
                                    key is the signal and the value
                                    does not matter.
                                  - The same --metadata key passed
                                    twice on one command line is
                                    last-write-wins (License::metadata
                                    is a std::map<string,string>).
                                  - No character restrictions on keys
                                    or values: any byte sequence your
                                    shell can deliver is accepted.
                                    Quote arguments that contain
                                    spaces, '=' (in the value), '$',
                                    '|', '&', or any other shell
                                    meta-character so the WHOLE
                                    key=value pair reaches the tool
                                    as one argv entry.

                                Quoting tips:
                                  bash / zsh:
                                    --metadata 'po_number=PO-12345'
                                    --metadata "region=North America"
                                  Windows cmd.exe:
                                    --metadata "po_number=PO-12345"
                                    --metadata "region=North America"
                                  PowerShell:
                                    --metadata "po_number=PO-12345"
                                    Use single quotes around values
                                    that contain '$' to suppress
                                    PowerShell variable expansion:
                                    --metadata 'tag=$revision'
    --fingerprint               Auto-detect THIS MACHINE's fingerprint and
                                bind the license to it. Useful for
                                self-licensing or end-to-end testing of
                                the toolchain on your own build host.
                                NOT what you want when issuing to a
                                remote end user -- their machine has a
                                different hardware fingerprint, so the
                                license will not verify on their host.
                                Use --fingerprint-value for that case.
    --fingerprint-value <v>     Bind the license to a specific hardware
                                fingerprint string. This is the standard
                                production path: the end user runs
                                rg_fingerprint on their own machine and
                                sends you the resulting hash; you pass
                                that hash here.
    --threshold <n>             Fingerprint match threshold (default: 2)
    --show-fingerprint          Print machine fingerprint and exit
    --show-count                Print the current end-user license
                                generation count and exit (read-only;
                                requires --vendor-license; v1.2.1+).
                                Output includes the count, the vendor-
                                license id, and the cap with remaining
                                budget. Use to monitor approach to the
                                cap before hitting "End-user license
                                generation limit reached" (§10.3.2).
                                See §9.7 for storage details.

7.4 license_verify
--------------------

    Usage: license_verify [options]

    --key <file>                Public key PEM file (required)
    --license <file>            License file (required)
    --algo <auto|ed25519|rsa>   Signature algorithm (default: auto;
                                v1.2.1+). "auto" inspects the public
                                key and picks ed25519 or rsa
                                automatically; this matches the
                                LicenseVerifier C++ default
                                (Customer_API_Reference §6.1).
                                Pass an explicit value only if you
                                want to ASSERT one algorithm.
                                Unknown values are rejected; in
                                v1.2.0 a typo silently fell back
                                to ed25519.
    --check-hardware            Verify hardware fingerprint
    --check-feature <name>      Check if feature is licensed
    --check-version <ver>       Check that <ver> satisfies the license's
                                version_range (v1.2.1+; corresponds to
                                LicenseVerifier::check_version() at
                                Customer_API_Reference §6.7). Prints
                                "Version '<ver>' against range '<range>':
                                PASS|FAIL - <message>" and exits non-zero
                                on FAIL. Empty version_range matches any
                                <ver>. Glob form ('3.*', '3.1.5') and
                                comparator form ('>=3.0,<4.0,!=3.5.0')
                                are both accepted; full syntax in
                                Customer_API_Reference §6.7. Useful for validating an
                                issued license against a target build's
                                version before shipping.

7.5 rg_floating_server (Premium tier)
---------------------------------------

    Usage: rg_floating_server <config.yaml>

Takes exactly one argument: the path to a YAML config file. It has no
other flags -- every setting is a YAML key, and the full key list with
defaults is in 5.2 rather than repeated here. Run with no argument and
it prints a sample config, which is the quickest way to see the
recognised keys on a machine where you do not have this manual. That
output goes to STDERR, not stdout, and the process exits 1 -- so
`rg_floating_server > server_config.yaml` produces an EMPTY file.
Redirect stderr instead:

    rg_floating_server 2> server_config.yaml

(This paragraph said "to stdout" until 2026-08-14, which made the
obvious way to capture it silently produce nothing.)

Refuses to start if neither tls_cert/tls_key nor allow_insecure_http
is configured (v1.3+). See 5.2.

7.6 rg_floating_client (Premium tier; example binary)
-------------------------------------------------------

    Usage: rg_floating_client [host] [port] [public_key.pem]
                              [--tls] [--ca-cert <file>]
                              [--insecure-http] [--help]

This is an EXAMPLE and a smoke-test tool, not something to ship. Your
application embeds FloatingLicenseClient directly (Customer_API_Reference
section 8); this binary exists so you can confirm a server is reachable
and issuing leases before you write any client code. Source is in
examples/rg_floating_client.cpp.

Defaults are 127.0.0.1 and port 8080. Without --tls you must pass
--insecure-http, or the client refuses to construct (v1.3+); the same
fail-closed rule the library applies to your own code.

Note on the two tools above: section 7 is titled "CLI Tools Reference"
and until v1.3 documented only the five node-locked tools, leaving the
two floating executables discoverable only from 5.2 and the API
reference. Both are in your tools/ directory, so both are listed here.

<!-- PAGEBREAK -->
================================================================================
8. LICENSE FILE FORMAT
================================================================================

8.1 Structure
---------------

    {
      "payload": "<JSON string>",
      "signature": "<base64-encoded Ed25519/RSA signature>"
    }

The signature covers the exact payload bytes. Any modification to the
payload invalidates the signature.

8.2 Payload Fields
--------------------

    Field                       Type      Description
    -------------------------   -------   ---------------------------
    license_id                  string    Unique license identifier (REQUIRED)
    licensee                    string    Licensed entity name
    product                     string    Product name (REQUIRED)
    version_range               string    Version pattern: "3.*" / "3.1.*"
                                          / "3.1.5" (glob form) or
                                          ">=3.0,<4.0" / "!=3.5.0"
                                          (comparator form). Empty or
                                          "*" = match any. Enforced by
                                          LicenseVerifier::check_version()
                                          v1.2.1+; informational only
                                          in v1.2.0.
    type                        string    "node_locked" or "floating"
    hardware_fingerprint        string    Component hashes (pipe-separated)
    fingerprint_match_threshold int       Min matching components (default: 2)
    issued_at                   string    ISO 8601 issue date
    expires_at                  string    ISO 8601 expiry or "permanent"
    grace_period_days           int       Days after expiry before block
    max_concurrent_users        int       Pool size (floating only)
    features                    string[]  Feature flag names
    metadata                    object    Arbitrary string->string pairs

The metadata field is a customer-defined string -> string map that the library round-trips through the signed license file without inspecting it. Set values from license_create with --metadata key=value (repeatable; v1.2.1+); read them on the verifier side via verifier.license().metadata, which returns a std::map<std::string, std::string>. The library does not enforce or interpret any key; the JSON object is stored exactly as you supplied it.

As of v1.3, verifier.license() returns a License by VALUE rather than a const reference, so that a snapshot cannot be torn by another thread reloading the verifier (Customer_API_Reference section 6.11 has the full rationale). The examples below are unaffected and remain correct: binding "const auto& meta = verifier.license().metadata;" to a member of the returned temporary extends that temporary's lifetime for as long as the reference lives, and using verifier.license() directly inside a larger expression keeps it alive to the end of that expression. The only form that stops compiling is a non-const reference binding such as "auto& lic = verifier.license();" - deliberately, because a compile error is a clearer signal than a silent change in behaviour. If you hold license data for a long time, prefer copying it outright: "rockyguard::License snap = verifier.license();".

Why use metadata instead of a sidecar config file:

- It is signed: any byte change invalidates the license signature, so an end user cannot edit metadata after the fact without breaking license verification entirely.
- It is bound to the license: you cannot pair Customer A's license with Customer B's metadata, since both travel in the same signed payload.
- It is one file to deliver and one file to deploy.

Common use cases:

- Audit and accounting trail. Bind business data to the license so finance and legal can verify it later. Example keys: customer_po (the purchase order the license was issued against), contract (an MSA / quote / SO reference), account_id (your CRM identifier for the licensee), channel (reseller, region, or salesperson code).

  Example invocation (set at issuance):

      tools/license_create --key private.pem \
        --vendor-license vendor_license.json \
        --id "LIC-2026-0042" --product "YourApp" \
        --licensee "Acme Inc." \
        --expires "2027-12-31T23:59:59Z" \
        --fingerprint \
        --metadata "customer_po=PO-2026-0042" \
        --metadata "contract=MSA-2025-Acme-v3" \
        --metadata "account_id=ACC-91827"

  Read on the support side (e.g., from a debug-info dump in your application):

      const auto& meta = verifier.license().metadata;
      auto it = meta.find("customer_po");
      if (it != meta.end()) std::cout << "PO: " << it->second << "\n";

- Pass-through data your application needs to know about the licensee. Many products have an internal taxonomy independent of the license itself: tier or plan names, regions for data-residency compliance, edition / SKU identifiers. Metadata is the supported channel for carrying that data signed.

  Example keys: plan (your product's tier name -- "free", "pro", "enterprise"), region (where the customer is allowed to operate -- "EU", "NA", "APAC-no-CN"), edition (which SKU this license was issued for), end_user_id (your customer's own identifier for their end user).

  Example application-side use, after verifier.load() succeeds:

      const auto& meta = verifier.license().metadata;
      std::string plan = meta.count("plan") ? meta.at("plan") : "free";
      if (plan == "enterprise") enable_enterprise_features();

- Anti-leak forensics. If a license file ends up on a public file-share or in a leaked archive, metadata can identify which cohort it was issued to. Example keys: issued_by (an internal staff member ID), cohort (a quarterly batch label), watermark (a unique-per-license tag for tracing).

  Example:

      --metadata "issued_by=staff-91" \
      --metadata "cohort=2026-Q1-acme"

  Because the license is signed, an attacker who copies the file cannot strip the watermark without invalidating the signature. The leak is tamper-evidence.

- Display strings your application can trust. The licensee field is the legal entity ("Acme Inc."); metadata can carry a friendlier display string for user-visible UI ("Acme Manufacturing, Munich"). Both are signed and both come from your issuance pipeline.

  Example: --metadata "display_name=Acme Manufacturing, Munich".

- Numeric limits beyond binary feature flags. The features array is binary (a flag is either listed or not). When you need quantitative caps -- max_seats specific to your product, an API rate limit, a storage quota -- metadata carries them as strings; your application parses and enforces. RockyGuard guarantees the value came from a signed license, but does not enforce it itself.

  Example:

      --metadata "max_seats=50" \
      --metadata "api_rate_limit_qps=100" \
      --metadata "storage_gb=500"

  Application side:

      int seats = std::stoi(verifier.license().metadata.at("max_seats"));

  (Wrap stoi in try/catch and pick a safe default if the key is absent or unparseable; metadata is a free-form string map, not a typed config.)

A note on size: the entire license is signed and re-parsed on every verification call. Keep individual values small (kilobytes, not megabytes); a multi-megabyte metadata payload makes every license check slow and has no good reason to be there. If you need to ship a large config blob to your end user, ship it next to the license as a separate file, but reference it from metadata (e.g., a SHA-256 of the config you trust).

<!-- PAGEBREAK -->
================================================================================
9. ANTI-TAMPERING FEATURES
================================================================================

The library includes several anti-tampering mechanisms that protect
end-user licenses from bypass attempts. These work automatically and
require no configuration.

9.1 Digital Signatures
------------------------

KEY ROTATION (cited from 4.1 and the 12.5 checklist). Two places in
this manual point here for the rotation procedure, and until v1.3 this
section contained none -- it is about signature verification. The
procedure, in full:

  1. Generate a new keypair with license_keygen. Keep the old private
     key; you will need it only if you must re-sign anything under the
     old identity, and you should destroy it once step 4 completes.
  2. Ship a new application build with the NEW public key embedded.
     Until a customer installs that build, their binary trusts only the
     old key -- this is the step that sets your rotation timeline, not
     the key generation.
  3. Re-issue every outstanding end-user license, signed with the new
     private key. Licenses signed with the old key will NOT verify
     against the new public key; there is no dual-key grace period in
     v1.3, by design (accepting two signing identities at once would
     weaken the property the signature exists to provide).
  4. Set an end-of-life date for the old application version, after
     which you stop issuing old-key licenses.

  Because step 2 requires a shipped binary and step 3 touches every
  customer, rotation is a release-scale operation, not a maintenance
  task. That is why 4.1 advises choosing a key strong enough to outlive
  your longest support commitment. If the private key is COMPROMISED
  rather than merely aging, the same four steps apply but on an
  emergency timeline, and you should assume any license mintable with
  that key already exists.


Every license file is digitally signed with Ed25519 or RSA-SHA256. The
signature is verified BEFORE the payload is parsed. Modifying any byte
in the payload invalidates the signature.

The library is safe against malformed license files: invalid JSON, wrong
value types, or missing fields all return a clean error (MalformedFile)
without crashing the host application.

9.2 Clock Manipulation Detection
-----------------------------------

The library detects system clock rollback to prevent end users from
setting their clock backward to extend an expired license.

  - UTC timestamps are stored in multiple hidden locations on the machine
    (see "Storage locations" below)
  - Anchor files are unique per license (derived from license_id, product,
    and licensee), so different products don't interfere with each other.
    The per-license tag is SHA-256(license_id + "|" + product + "|" +
    licensee), abbreviated to 12 hex characters; you'll see it as the
    suffix on every storage location below. The "|" separators matter --
    they prevent collisions between fields that would otherwise concatenate
    ambiguously (e.g., id="AB"+product="CD" vs id="ABC"+product="D")
  - On each license check, all locations are read and cross-verified
  - If the current time is more than 1 hour behind the maximum stored
    timestamp, clock manipulation is detected
  - The stored timestamps are integrity-protected; editing them is detected
  - Deleting some storage locations doesn't help - surviving ones still
    catch the rollback
  - Online time verification: the library verifies the system clock
    via HTTPS (port 443) against a pool of 12 trusted internet servers,
    randomly selected. THE POOL, so you can write the firewall rule
    (all port 443, outbound only, no inbound and no other port):

        www.google.com       www.microsoft.com    www.cloudflare.com
        www.apple.com        www.amazon.com       www.github.com
        www.wikipedia.org    www.mozilla.org      www.w3.org
        www.ietf.org         www.debian.org       www.kernel.org

    One reachable host is enough -- a single successful check
    re-anchors the clock -- so an allow-list covering any subset still
    works, with fewer hosts meaning more retries when some are blocked.
    The list is compiled in; treat it as fixed for v1.3 and re-check
    this section on upgrade rather than assuming it is stable forever.
    Uses TLS certificate timestamps (CA-signed, can't
    be forged) and HTTP Date headers. Hosts file redirects fail because
    the TLS handshake rejects invalid certificates. Mandatory when
    anchors are missing, 10% random during normal operation. Silently
    skipped if offline.

Deploying on intranet-only or air-gapped networks:

There is currently NO configuration flag or API setting to disable the outbound time check, and no way to redirect it at an internal NTP or HTTPS host. This is a real gap for locked-down deployments and we would rather say so than let you search for a switch that does not exist. If you need one, tell us -- an opt-out, or a configurable host pool, is a v1.4 candidate.

What actually happens on a network with no route out matters more than it sounds, because the check is SYNCHRONOUS and blocks the calling thread. The library tries up to 3 hosts at 3 seconds each, so the worst case inside RockyGuard is roughly 9 seconds added to the call that triggered it -- typically load() at startup. That is the library's own budget and does not include OS-level DNS resolution, which on some configurations adds several seconds of its own before the connection attempt even begins. A network that DROPS packets silently produces the worst case; one that actively REFUSES the connection, or has no DNS entry at all, usually fails in milliseconds. If your environment can be made to refuse rather than blackhole traffic to port 443, startup latency largely disappears.

The check fires on 100% of first runs (or any run where the anchor files are missing) and on about 10% of subsequent runs, so a stalled startup is intermittent rather than constant -- which makes it easy to misdiagnose as a hang somewhere else. If you see occasional multi-second startup delays on an isolated network, this is the first thing to rule out.

Practical mitigations available today: allow outbound HTTPS to the time-check pool if policy permits, since one successful check re-anchors the clock; call load() off the UI thread so the stall is not user-visible; or accept the latency knowing it is bounded and intermittent.

One security consequence is specific to air-gapped machines and is a documented, accepted limitation rather than an oversight. The online check is what catches a rollback when the local anchors are gone. On a permanently isolated machine, a user who deletes ALL anchor locations AND sets the clock back gets one false pass, because the library cannot distinguish that from a legitimate first run on a new offline machine -- refusing would break every genuine air-gapped install. See the threat-model table in section 11.2 and Customer_API_Reference section 6.6. For air-gapped fleets the practical countermeasure is shorter licence durations, so re-issuance forces clock truth on a regular cadence.

Storage locations:

The library writes the anchor timestamp to two backends per platform plus a local cache file that is shared across platforms. Every path or key carries the per-license <tag> suffix described in the bullet list above.

On Windows:

    HKCU\Software\RockyGuard\ta_<tag>             (registry value, REG_SZ)
    %LOCALAPPDATA%\RockyGuard\.timeanchor_<tag>   (per-user file)

On Linux:

    $HOME/.local/share/rockyguard/.timeanchor_<tag>   (per-user file)
    $HOME/.lck_svc_<tag>                              (hidden in $HOME)

On both platforms, in addition to the above:

    <directory of license.json>/.lck_cache_<tag>      (local cache, next
                                                       to the license file)

The dual-backend design means a savvy end user who deletes one location does not defeat the rollback check -- the surviving backend still catches the manipulation. The local cache (.lck_cache_<tag>) is the third witness and travels with the license file.

Anchor integrity does not depend on where the licence lives. Each witness carries an HMAC computed over its timestamp plus a stable identifier for the LOCATION, not over its filesystem path. That distinction matters whenever an installation moves. Before v1.3.1 the path was part of the integrity tag, so renaming an install directory, spelling the licence path relatively on one run and absolutely on the next, or (on Windows) reaching one directory through a different drive-letter case, a mapped drive, or a junction turned an intact anchor into a failed integrity check -- reported as tampering, which the library deliberately never self-repairs. Anchors written by v1.3.0 and earlier still verify and are rewritten in the current form on the next successful run, so upgrading needs no action and loses no rollback history.

Because that third witness is written NEXT TO license.json, the directory you choose for the licence must be writable by the user running your application, on every run and not only the first. If it is not -- the common case being an application installed under %ProgramFiles% or /opt and run non-elevated -- verification still succeeds and your end user sees nothing, but that witness is permanently absent and rollback detection falls back to the two per-user backends. §3.4 covers the deployment choice this implies; the short version is to put license.json under a per-user directory rather than beside the executable.

If clock manipulation is detected, the check returns LicenseStatus::ClockManipulated.

IMPORTANT: license_id and product are required fields. They are used to
generate unique anti-tampering anchor identifiers. The library will print
a warning if these fields are empty, and the CLI tools enforce them as
required parameters.

9.3 Binary Integrity Self-Check (Shared Library / DLL)
--------------------------------------------------------

For DLL builds, the library verifies its own binary on every validation:

  - The library's SHA-256 hash is computed and compared against a signed
    hash file (rockyguard.sig) shipped alongside the DLL
  - If the DLL has been patched or modified, the check fails
  - The .sig file cannot be forged (signed with vendor's private key)

If detected, the check returns LicenseStatus::IntegrityCheckFailed.

Ship rockyguard.sig next to rockyguard.dll in your distribution.

Static builds do not have or need a .sig file. The library's code is
compiled directly into your executable, so there is no separate binary
for the integrity check to verify and the package's lib/static/ directory ships
no .sig. Customers shipping their applications statically linked
against rockyguard.lib (or rockyguard_mdd.lib on Windows Debug) get
the integrity properties of their own executable instead -- whatever
code-signing or binary-attestation flow they apply to that .exe / ELF
covers the RockyGuard code transitively.

9.4 Floating Server Authentication
-------------------------------------

The floating license protocol includes multiple security layers:

  - Response signing: every server response is signed with the server's
    private key. The client verifies using the public key. Prevents
    server spoofing via /etc/hosts or DNS manipulation.

  - Client request authentication (non-TLS mode): the server issues a
    cryptographic session secret at checkout time (delivered inside the
    signed response). The client includes an HMAC of each subsequent
    request using this secret. This prevents attackers from forging
    checkin/heartbeat requests to evict other users, even if they
    sniff the client_id from network traffic.

  - Nonce-based replay protection: each request and response includes
    a cryptographically random nonce. Prevents replay attacks.

  - Cryptographic random: all client IDs and nonces are generated using
    OpenSSL RAND_bytes (hardware entropy), not predictable PRNGs.

  - Optional TLS: encrypts all traffic between client and server.
    HMAC authentication is ALWAYS required, even under TLS, because
    TLS authenticates the transport but not the application-level
    client identity. Without HMAC binding each request to a
    session-specific secret, a peer who learns another client's
    client_id (which is not secret) could forge checkin / heartbeat
    requests over the same TLS channel.

WARNING - Without TLS, session secrets are sniffable on the wire.

Without TLS, session authentication tokens flow as plaintext over the network. Response signing still prevents server spoofing (the client verifies every response), but a PASSIVE listener on the same LAN segment can capture a client's session secret as it leaves the server, then forge checkin or heartbeat requests against that client's lease -- evicting the legitimate user, holding the seat indefinitely, or both.

For any deployment reachable from a network you do not fully control (Wi-Fi, cross-VLAN, public internet, anything not a wired LAN with controlled access), enable TLS by setting tls_cert and tls_key in the server config and tls_ca_cert_path on the client. As of v1.3 this is enforced rather than advised: a server with no tls_cert/tls_key refuses to start unless allow_insecure_http: true is set, and a client with use_tls = false throws from its constructor unless allow_insecure_http = true is set. See the allow_insecure_http entry above for the upgrade impact and the reasoning.

9.5 Security Hardening
------------------------

The library is compiled with OS-level security protections:

  - ASLR: DLL base address randomized on each load
  - DEP: Non-executable stack and heap
  - Control Flow Guard (Windows) / CET (Linux): Validates all indirect
    function calls at runtime, preventing control flow hijacking

These are enabled automatically and require no configuration.

9.6 Threat Model
------------------

The table below summarizes the concrete attacks RockyGuard is designed to defend against, the mechanism each defense relies on, and the residual risk that remains after the defense is applied. The "Residual risk" column is what an attacker can still attempt; the column "Mitigation" describes what your application or operations team should add on top of the library to close that gap.

| Attack | RockyGuard defense | Residual risk and mitigation |
|--------|--------------------|------------------------------|
| License file tampering (editing dates, machine fingerprint, features inside the JSON) | Ed25519 / RSA-SHA256 signature over the payload, verified at load() | None for the file itself: any byte change invalidates the signature and load() returns SignatureInvalid. Residual: an attacker who steals your private key can sign anything; mitigation = keep private.pem on a secure machine; rotate keys per the versioning policy if compromise is suspected. |
| Keygen / counterfeit license generator | Ed25519 / RSA private key required to sign a valid payload; public-key-only verifier on the customer side | None: the verifier cannot produce a valid signature without the private key. Same private-key custody mitigation as above. |
| Patching or cracking the shipped binary (NOP-out license checks) | Binary integrity self-check (DLL/SO builds): rockyguard.sig is verified at load against the binary's SHA-256 | Residual: an attacker who modifies BOTH the binary AND the .sig in coordinated fashion bypasses the integrity check. Mitigation: ship the application's own integrity check (code signing, executable signature) so the OS / loader detects pre-launch modification before RockyGuard gets a chance to run. Static-library builds embed the library code directly into your executable, raising the bar further. |
| Clock rollback (set the system clock back to before expiry) | Multi-location time anchors (file + Windows registry where applicable) cross-checked against current clock; HTTPS time-anchor pool consulted on first run and 10% of subsequent runs | Residual: a determined attacker on a fully air-gapped machine who deletes ALL anchor locations AND rolls the clock back gets one false-pass at first run (documented in Customer_API_Reference §6.6). Mitigation: ship to non-air-gapped customers when possible; for air-gapped fleets, issue shorter-duration licenses so re-issuance forces clock truth on a regular cadence. |
| MITM on the floating-server connection (intercept and forge checkout / heartbeat / checkin responses) | TLS encryption, REQUIRED as of v1.3 (use_tls + tls_ca_cert_path; running without it needs an explicit allow_insecure_http opt-in on both client and server); REQUIRED Ed25519 payload signature on every server response (server_public_key_pem) | Residual only if the opt-in is used: a passive sniffer captures the session secret issued at checkout and can forge subsequent HMAC-authenticated requests, so response signing does not help -- it gives authenticity, not confidentiality. Mitigation: do not set allow_insecure_http outside local development; pin the server cert via tls_ca_cert_path. With TLS + server_public_key_pem set, the residual is essentially zero. |
| Server spoofing (rogue floating server pretending to be the real one) | Client verifies every server response against server_public_key_pem | Residual: if server_public_key_pem is left empty (Development-only configuration), the client trusts any server. Mitigation: never ship Development-only configuration to end users; embed the production server's public key in the client at build time. |
| Eviction / heartbeat forgery on the floating server (peer who knows another client's client_id evicts them) | Per-checkout session secret issued by the server; HMAC-authenticated heartbeat / checkin requests bound to that secret | Residual without TLS: session secret travels in cleartext during checkout response and a sniffer can capture it. Mitigation: enable TLS; the captured-secret window closes. |
| Hardware fingerprint forgery (running a license bound to one machine on another machine that mimics the original's MAC / CPU / disk / motherboard IDs) | Four independent component hashes; configurable threshold (default: 2 of 4 must match) | Residual: a sufficiently determined attacker can spoof MAC and CPU id; spoofing all four including motherboard serial is much harder. Mitigation: keep the default threshold of 2 unless customer hardware constraints force you to lower it; consider raising threshold to 3 for high-value licenses. |
| Memory dumping / runtime extraction of the loaded license object | Out of scope: a library cannot defend against attackers with kernel-level access to the host process | None: the License struct lives in process memory after load() succeeds. Mitigation: this is an operating-system / hosting-environment problem, not a library problem. RockyGuard accepts this; if the threat model includes attackers with kernel-level access, license enforcement at the application layer is the wrong defense. |

9.7 Generation Counter Storage
--------------------------------

Every successful license_create call increments a persistent counter so the library can enforce the end-user-license generation cap on your vendor license (the "End-user license generation limit reached" error in §10.3.2). The counter is HMAC-protected (tamper attempts force the value to 999999999, blocking further generation) and stored in three locations per platform; the library reads the maximum across all locations, so deleting some but not all does not reset the counter.

Scope:

The counter is per-vendor-license and per-user-on-this-machine. The storage tag is derived from your vendor_license_id, so different vendor licenses on the same machine have independent counters; copying counter files between vendor licenses does not work because the HMAC binds to the vlid. Counters do NOT sync across machines or users -- each generation host counts independently.

Storage locations:

The library writes the counter to three backends per platform. The <tag> suffix is derived from your vendor_license_id (a SHA-256 hash, abbreviated to 12 hex characters); you'll see it on every storage path below.

On Windows:

    HKCU\Software\RockyGuard\gc_<tag>          (registry value, REG_SZ)
    %LOCALAPPDATA%\RockyGuard\.gencount_<tag>  (per-user file)
    <cwd>\.rg_gencount_<tag>                   (file in license_create's
                                                current working directory)

On Linux:

    $HOME/.local/share/rockyguard/.gencount_<tag>   (per-user file)
    $HOME/.rg_gc_<tag>                              (hidden file in $HOME)
    <cwd>/.rg_gencount_<tag>                        (file in license_create's
                                                     current working directory)

Storage format (file-based locations):

    <count>:<vlid>:<hmac>

A single text line. <count> is a non-negative integer. <vlid> is the vendor_license_id. <hmac> is HMAC-SHA-256 over <count> + <vlid> with a per-path salt, base-16-encoded. Editing the count or removing the HMAC invalidates the file: the library treats it as a tamper attempt and forces the counter to 999999999.

How to inspect:

The supported way is to run the v1.2.1+ CLI flag:

    tools/license_create --vendor-license vendor_license.json --show-count

This prints the current count, the vendor-license id, and the cap (with remaining budget if a cap is set, "unlimited" otherwise). The flag is read-only -- no side effects, no counter increment.

If you need to read a specific storage location directly (e.g., as part of an audit script), the count is the substring before the first ':' on the single-line file format. Target the per-user application-data location (%LOCALAPPDATA% on Windows, $HOME/.local/share/rockyguard on POSIX), which needs no admin or registry access to read. Do not target the working-directory file: as of v1.3 it is no longer written, so an audit script reading it will report a stale count or find nothing at all.

Local-cache file note:

CHANGED IN v1.3: the counter is no longer written to the current working directory. Through v1.2.x a third copy was written to <cwd>/.rg_gencount_<tag>, so a file appeared in whatever directory you happened to run license_create from -- including version-controlled trees, where it became untracked noise you had to remember not to commit.

It was dropped because it was the weakest of the three locations, not merely the most annoying. Being a RELATIVE path, its contents depended on where the process was invoked: run license_create from a different directory and the file was simply absent, contributing nothing. Anyone deliberately shedding it needed only to change directory. The two remaining locations are absolute, and each has to be found and deleted individually, which is where the redundancy actually comes from.

Existing files are still READ, so upgrading cannot lower your count: if the highest value happens to sit in an old <cwd>/.rg_gencount_<tag> -- for example because %LOCALAPPDATA% was unwritable on the run that incremented it -- load() still picks it up. Nothing writes those paths any more, so no new ones appear.

Housekeeping after upgrading: any .rg_gencount_* files already sitting in your working trees are now inert once the count is reflected in the two permanent locations, which happens on the next license_create run. You can delete them at that point. If you added .rg_gencount_* to .gitignore on the previous advice, it is harmless to leave in place.

<!-- PAGEBREAK -->
================================================================================
10. TROUBLESHOOTING
================================================================================

10.1 Error Reference Table
----------------------------

The table below is the canonical lookup: every LicenseStatus enum value and every CLI tool error message your customers might see, mapped to the human-readable message text and the sub-section in this document where the cause and resolution are explained in full.

| LicenseStatus / message | Where it surfaces | Detailed entry |
|-------------------------|-------------------|----------------|
| SignatureInvalid | LicenseVerifier::load() / load_from_string() | 10.2.1 |
| HardwareMismatch | LicenseVerifier::check_node_locked() | 10.2.2 |
| Expired | LicenseVerifier::load() / check_expiry() / check_node_locked() | 10.2.3 |
| FeatureNotLicensed | LicenseVerifier::check_feature() | 10.2.4 |
| VersionMismatch (v1.2.1+) | LicenseVerifier::check_version() | 10.2.5 |
| MalformedFile | LicenseVerifier::load() (invalid JSON or missing fields) | 10.2.1 (related) |
| KeyMalformed (v1.3+) | LicenseVerifier::load_from_string() given a RGAK- activation key whose body is unrecoverable (non-Base32 char, truncation, unsupported format version) | 10.2.6 |
| KeyChecksumMismatch (v1.3+) | LicenseVerifier::load_from_string() given a RGAK- activation key whose CRC32 trailer disagrees with the body (almost always a single-char typo) | 10.2.7 |
| LibraryNotInitialized | license_create / rg_floating_server CLI | 10.3.1 |
| GenerationLimitReached | license_create CLI | 10.3.2 |
| MachineNotAuthorized | license_create / rg_floating_server CLI | 10.3.3 |
| TierNotAuthorized | rg_floating_server CLI | 10.3.4 |
| ClockManipulated | LicenseVerifier::check_expiry() / load() / check_node_locked() | 10.4.1 |
| IntegrityCheckFailed | LicenseVerifier::load() / check_expiry() / check_node_locked() (DLL builds) | 10.4.2 |
| NoLicensesAvailable | FloatingLicenseClient::checkout() | 10.5.1 |
| ServerUnreachable | FloatingLicenseClient::checkout() / checkin() | 10.5.2 |
| MachineSeatLimitReached (v1.2.1+) | FloatingLicenseClient::checkout() | 10.5.3 |
| InGracePeriod | LicenseVerifier::load() / check_expiry() / check_node_locked() | Not an error: license is expired but still usable; check grace_days_remaining and warn the user. |
| Valid | Any verification call | Not an error: success. |
| NotYetValid | (never returned) | Reserved enum value, unreachable by design rather than by oversight. No shipped code path returns it in any release to date. The license's issued_at field is recorded and signed but deliberately NOT compared against the local clock: doing so would reject a legitimate license on any machine whose clock runs a few minutes slow, and it would defend against no attacker, since only the holder of the vendor private key can mint a future-dated license. Enforcing it is an activation-date feature ("valid from 1 March") needing its own design for clock-skew tolerance; the enumerator is held open so that can land without renumbering. It will not be deleted for tidiness -- it sits mid-enum, so removing it would renumber every later value. If your code switches on every LicenseStatus, include this case (route it as you would MalformedFile) to silence "unhandled enum" warnings. |

The full enum definition and operator bool() semantics are in Customer_API_Reference §4.2 (enum class LicenseStatus) and §4.4 (struct LicenseResult).

Each entry below is structured as:

  - Error: the literal message text the library or CLI tool prints.
  - Cause: why that message was produced (the underlying condition).
  - Resolution: the action you take to fix it.

10.2 License Errors
---------------------

10.2.1 "License signature verification failed"
------------------------------------------------

Cause: One of three things has gone wrong.

- The license file or its signature was modified after signing.
- The public key embedded in your application does not correspond to the private key that signed the license.
- The SignatureAlgorithm passed explicitly to LicenseVerifier differs from the one used at signing time. (v1.2.1+: the constructor's default is now SignatureAlgorithm::AutoDetect, which inspects the loaded key and picks the right algorithm automatically; this case only fires if you pass an explicit value that disagrees with the key.)

Resolution: Address the matching cause from the list above.

- Replace the license with the genuine signed copy from your license_create run.
- Verify that PUBLIC_KEY in your application is the exact PEM string from public.pem produced by license_keygen alongside the private key used at signing.
- Drop the explicit SignatureAlgorithm argument and let v1.2.1+'s AutoDetect default pick it from the key. If you have a deliberate reason to pass an explicit value, make sure it matches the algorithm used at signing time.

10.2.2 "Hardware mismatch: N of M required components matched"
----------------------------------------------------------------

Cause: The license was issued against a different machine's fingerprint than the one currently running it. N (matched components) is below the license's fingerprint_match_threshold (M, default 2).

Resolution: Re-issue the license bound to the correct machine: have the end user run rg_fingerprint on their machine and send you the resulting hash, then call license_create --fingerprint-value "<that hash>". Alternatively, if the same machine had only a partial hardware change (e.g., a NIC swap or disk replacement), lower fingerprint_match_threshold via license_create --threshold so fewer components are required to match.

10.2.3 "License has expired"
------------------------------

Cause: The current system time is past the license's expires_at, and the grace period (if any) has also elapsed.

Resolution: Issue a new license with a later --expires date and ship it to the end user. If the end user's system clock is wrong (e.g., dead CMOS battery resetting the date), correcting the clock is sufficient; no re-issue is needed.

10.2.4 "Feature not licensed: <name>"
---------------------------------------

Cause: Your application called check_feature("<name>") but that name is not in the license's features array.

Resolution: Re-issue the license with --feature "<name>" added (one --feature flag per feature). If the feature should not have required a license, remove the corresponding check_feature() call from your application instead.

10.2.5 "Version <X> not allowed by license version range <range>"  (v1.2.1+)
-----------------------------------------------------------------------------

Cause: Your application called LicenseVerifier::check_version("<X>") (Customer_API_Reference §6.7), and the version string you passed does not satisfy the license's version_range field. The range is set at issuance time via license_create --version, and supports two notations: glob form ("3.*", "3.1.*", "3.1.5", "*") and comparator form (">=3.0,<4.0,!=3.5.0", with comma-AND'd clauses). An empty version_range matches anything (the v1.2.0 default; see Customer_API_Reference §5.1).

The most common ways this surfaces:

- The end user upgraded your application to a major version newer than what their existing license allows (e.g., license issued for "3.*", application now reports "4.0.0"). This is the intended use of version_range -- gate paid major-version upgrades.
- The application passed the wrong current_version string (e.g., a build-tag suffix slipped through: "3.1.0-rc1" against a license that accepts "3.*"). Glob and comparator matching are strict; "3.1.0-rc1" is NOT "3.1.0".
- The license was issued with an overly narrow range by mistake (e.g., "3.1.5" exact when the intent was "3.1.*").

Resolution: Pick the matching cause from the list above.

- For a legitimate version-gated upgrade, either re-issue the license with a wider --version range (license_create --version "3.*" -> "3.* || 4.*" via two clauses, or simply "*" if you want to drop the gate entirely) or instruct the end user to stay on a supported version.
- If the current_version string is wrong, normalize it before passing to check_version() (strip build suffixes, take only the major.minor.patch).
- For a too-narrow license, re-issue with the corrected range. The version_range field is part of the signed license payload, so editing the JSON by hand will trip SignatureInvalid (§10.2.1) -- you must regenerate.

If you do not want runtime version enforcement at all, simply do not call check_version() in your application and leave version_range empty at issuance. The library does not consult version_range automatically; check_version() is opt-in.

10.2.6 "Activation key is malformed: <reason>"  (v1.3+)
---------------------------------------------------------

Cause: LicenseVerifier::load_from_string() was given a string that begins with the "RGAK-" activation-key prefix (or its case-insensitive equivalent) but whose body is not a recoverable activation key. The detail message names the exact failure:

  - "Activation key contains characters outside the Base32 alphabet ..." -- one or more characters in the body are not in A-Z / 2-7 / '-' / whitespace. The most common cause is a character substitution during transcription (e.g., '0' for 'O', '1' for 'I'). Note that the Base32 alphabet OMITS '0', '1', 'I', and 'O' precisely to avoid this confusion.
  - "Activation key decodes to fewer than 5 bytes -- key is truncated." -- the body was too short to contain even the version byte + CRC32 trailer. Almost always means the user copied only part of the key.
  - "Activation key uses format version <N> which this version of RockyGuard does not understand." -- the key was generated by a newer release of RockyGuard with a future format extension. Upgrade your application's RockyGuard library to a matching version, or have the vendor reissue the key in the v1.3 format.

Resolution: Recopy the full key from the source. If the source is an email, expand any auto-shortened "view full message" lines; some email clients truncate long strings at the visual boundary. If recopying fails repeatedly with the same error, ask the vendor to reissue the activation key.

10.2.7 "Activation key checksum (CRC32) does not match the key body" (v1.3+)
---------------------------------------------------------------------------------

Cause: LicenseVerifier::load_from_string() decoded the Base32 body successfully, but the trailing 4-byte CRC32 inside the decoded payload does not match the bytes preceding it. A single-character flip in transcription is the overwhelmingly most common cause -- the CRC32 was added in v1.3 specifically to catch this case BEFORE the signature verification step runs, so the customer-support diagnostic is unambiguous.

Distinguish from §10.2.1 SignatureInvalid: SignatureInvalid means the cryptographic signature did not verify (the key was tampered with, was generated against a different vendor public key, or you are loading it into the wrong product). KeyChecksumMismatch means the customer mistyped or partially-pasted the key; the cryptographic check did not even run.

Resolution: Recopy the full key from the original source verbatim. If the key was transcribed from a phone call or printed page, paste from a digital source instead. If recopying produces the same error every time, ask the vendor to reissue.

10.3 Vendor License Errors
-----------------------------

These errors appear when running the vendor-side CLI tools shipped in this package: license_create (issues end-user licenses) and rg_floating_server (Premium tier; runs the floating license server).

10.3.1 "Vendor license missing. Re-run with --vendor-license vendor_license.json"
-----------------------------------------------------------------------------------

Cause: The CLI tool was invoked without the --vendor-license flag, so it has no vendor license to authenticate license generation against. (In v1.2.0 this error read "Library not initialized. Call rockyguard::init()..."; renamed in v1.2.1 because the original wording named an internal API the customer does not call.)

Resolution: Re-run the tool with --vendor-license vendor_license.json (pointing at the vendor_license.json file your library vendor sent you). For the rg_floating_server variant of this error, set vendor_license: <path> in the YAML config file instead.

10.3.2 "End-user license generation limit reached"
-----------------------------------------------------

Cause: Your vendor license carries a maximum number of end-user licenses you may issue, and the persistent generation counter (preserved across CLI tool runs and reboots) has hit that cap. The counter is HMAC-protected: deleting state files does not reset it. See §9.7 for the storage layout and `tools/license_create --show-count` (v1.2.1+) for a read-only way to inspect the count and remaining budget before issuance.

Resolution: Contact the library vendor to renew the cap, raise the limit, or upgrade your vendor license to a higher tier.

10.3.3 "This machine is not authorized for license generation"
----------------------------------------------------------------

Cause: Your vendor license is bound to one or more specific generation machines (by hardware fingerprint), and the machine you are running the CLI tool on is not in that set.

Resolution: Run rg_fingerprint on the new generation machine, send the hash to your library vendor, and request a re-issued vendor license that includes the new machine.

10.3.4 "Floating licensing requires a Premium tier library license"
---------------------------------------------------------------------

Cause: rg_floating_server was invoked under a Basic-tier vendor license. Floating licensing is gated behind the Premium tier.

Resolution: Contact the library vendor to upgrade your vendor license from Basic to Premium.

10.4 Anti-Tampering Errors
-----------------------------

10.4.1 "System clock manipulation detected" / "Local time-anchor state reports a timestamp N seconds ahead of the current system clock"
-----------------------------------------------------------------------------------------------------------------------------------------

In v1.3+ the library produces three distinct customer-facing messages here depending on the underlying detection path, replacing the single generic "System clock manipulation detected" wording v1.2.x emitted for all three cases (v1.2.1 Phase 4.1 Report finding DR3-1):

- **"System clock is behind real time by N seconds (verified against the online time pool). Set the system clock to the correct time and retry."** -- the online check found the clock significantly behind real time. Confident clock-rollback finding. Resolution: set the system clock correctly.

- **"System clock is ahead of real time by N seconds (verified against the online time pool). Set the system clock to the correct time and retry."** -- the online check found the clock significantly ahead. Same resolution.

- **"Local time-anchor state reports a timestamp N seconds ahead of the current system clock..."** -- the local anchor files report a timestamp ahead of "now". Ambiguous: this is consistent with genuine clock rollback BUT it is ALSO what happens when stale anchor files from a prior install (or from a session where the clock was set forward and later corrected) are still on disk. The message includes the recovery procedure below.

Cause: The library compared the current system clock to multiple stored time anchors and detected a backward jump beyond the 1-hour tolerance. Three common ways this happens:

- The end user genuinely set the clock back to extend an expired license.
- The clock was set FORWARD at some earlier point (deliberately, or because of a dead CMOS battery showing a wrong year) while the application ran -- the library wrote anchors at that future time -- and then the clock was corrected. The current correct time is now "behind" the future-dated anchor, even though the clock is right.
- A timezone change wrapped a date boundary, or anchors were corrupted on disk.
- Stale .lck_cache_* / .licensecheck_anchor_* / .rg_gencount_* files from a prior install of a DIFFERENT product (sharing the same RockyGuard library) are still on disk and now look "ahead" relative to the current run. Common when uninstalling/reinstalling without cleaning the per-user state directory.

Resolution: Set the system clock to the correct current time first. If that alone resolves it, the rollback was straightforward.

If the system clock is correct but the error persists, the anchor was likely written at a future time during a previous forward-clock excursion. The recovery path is to delete the per-license anchor files at the storage locations listed in §9.2 and re-run the application while connected to the internet. With every anchor location empty, the library treats the situation as a first run: the mandatory online time-anchor verification fires (HTTPS to a rotating pool of trusted servers), the corrected clock is validated against that real time, and fresh anchors are written at the correct time. The license then verifies cleanly.

  Concrete steps:
    Windows  -- delete HKCU\Software\RockyGuard\ta_<tag> (registry value),
                %LOCALAPPDATA%\RockyGuard\.timeanchor_<tag>, and
                <directory of license.json>\.lck_cache_<tag>.
    Linux    -- delete $HOME/.local/share/rockyguard/.timeanchor_<tag>,
                $HOME/.lck_svc_<tag>, and
                <directory of license.json>/.lck_cache_<tag>.
  The <tag> is the 12-hex-character abbreviation of SHA-256(license_id + "|" + product + "|" + licensee); §9.2 explains the derivation. If you do not know the tag, deleting every .timeanchor_* / .lck_svc_* / .lck_cache_* entry under those locations is safe -- only the per-license entries the customer's installation has touched will be regenerated, and the next run rewrites them at the correct time.
  Pre-condition: the host must be online for at least the first run after deletion, OR the user can defer until they next have network connectivity. The mandatory online check that fires when all anchors are missing is what authoritatively validates "the clock is now correct" before fresh anchors are written.

As of v1.3 the library repairs this state on its own whenever it can prove the current clock is trustworthy. If the online time pool confirms the system clock agrees with real time in both directions, then the clock is demonstrably not rolled back, the anchor is simply stale, and the library re-baselines every anchor location and continues -- no manual intervention, no support call. A genuine rollback is unaffected: it fails the online drift check before reaching the repair path, and a tampered anchor (one whose integrity check fails) is never repaired. You will see a one-line WARN naming the drift that was healed; if it recurs, the system clock is being set forward, and a failing CMOS battery is the usual cause.

A different message -- the time-anchor state "failed its integrity check (HMAC mismatch)" -- is a different diagnosis, and the self-repair just described does not apply to it: an anchor's contents no longer match its integrity tag, and that verdict is never healed automatically. As of v1.3.1 the message lists every anchor location for that licence by full path, so the remedy is to delete exactly those, plus any .rg_gencount* / .licensecheck_anchor* state beside the licence, and re-run once with network access. Setting ROCKYGUARD_DEBUG_TIME_ANCHOR=1 in the environment prints one line per witness -- path, stored timestamp, and verdict -- so you can see WHICH location failed rather than deleting all of them on spec. Note this diagnostic covers the anchor reads themselves and therefore prints even when verification is denied before any network access is attempted.

If the host cannot reach the internet at all (air-gapped fleets), there is no evidence either way, so the library keeps the conservative behavior and refuses -- delete the state files listed above to re-baseline, or contact your support so you can re-issue a license out of band.

10.4.2 "Library integrity check failed" (and the v1.3 precise variants)
-----------------------------------------------------------------------

In v1.3+ the library produces a distinct trailing customer-facing message per failure mode, replacing the single generic "Library integrity check failed. The library binary appears to have been modified." wording v1.2.x used for all of them (v1.2.1 Phase 4.1 Report finding V121-7). The variants:

- **"Integrity signature file not found (<path>). The library DLL ships with a .sig file alongside it; verify both are present after copying / installing."** -- the .sig file is missing. The library binary itself may be perfectly intact; this is a packaging / deployment issue, NOT actual tampering. Resolution: copy the rockyguard.sig file from your customer zip to the directory holding rockyguard.dll. (The v1.2.x generic "binary appears to have been modified" wording for THIS case was the source of finding V121-7.)

- **"Library binary has been modified since signing (<path>). Re-install the library from the original distribution."** -- the binary's SHA-256 does not match the hash signed in the .sig file. Genuine tampering OR an incomplete copy / partial file-write. Resolution: re-install from the original distribution.

- **"Library integrity signature did not verify (<path>). The .sig file may have been tampered with; re-install both the library and its .sig from the original distribution."** -- the binary hash matched what's stored in .sig, but the .sig's vendor signature is invalid. Someone replaced the .sig file with a forgery. Resolution: re-install both files from the original distribution.

- **"Library binary could not be read for integrity check (<path>). The file may be locked by another process or missing on disk."** -- the library file exists on disk but cannot be opened for hashing. On Windows this typically means another process opened the file with exclusive share mode (an anti-tampering tool the customer also installed, or an antivirus mid-scan). Resolution: identify the locking process; on persistent failures, allowlist the rockyguard.dll path in any host security tool.

- **"Could not determine the library's own DLL path for integrity check."** -- the runtime could not find its own module path. Rare; typically indicates the library was loaded in an unusual way (e.g., via a manual VirtualAlloc + memcpy rather than LoadLibrary).

Cause: The shipped library binary (rockyguard.dll on Windows, librockyguard.so on Linux) was modified, OR the integrity .sig file next to it is missing, stale, or does not match the binary's current bytes.

Resolution: Restore BOTH the library binary AND its .sig file from the original shipped copies (they must come from the same build). The .sig file is paired with the binary at build time; mixing a binary from one build with a .sig from another build will continue to fail this check.

10.5 Floating License Errors
-------------------------------

10.5.1 "No floating licenses available"
-----------------------------------------

Cause: All seats in the floating server's pool are currently held by other clients (max_users reached).

Resolution: Wait for one or more existing clients to release their leases. The server's heartbeat-timeout path will reclaim leases from clients that have stopped sending heartbeats automatically. If all seats are legitimately in use, increase max_users in the floating server's configuration and restart the server.

10.5.2 "Cannot reach license server"
--------------------------------------

Cause: The FloatingLicenseClient could not establish a TCP connection (or, with use_tls, a TLS handshake) to the configured FloatingClientConfig.server_host : server_port.

Resolution: Verify the server is running and listening on the expected port. Check network connectivity (ping, traceroute) and firewall rules between client and server. Confirm server_host and server_port in FloatingClientConfig point at the right host. With use_tls = true, also confirm tls_ca_cert_path is correct and the server's certificate is valid.

10.5.3 "machine seat limit reached"  (LicenseStatus::MachineSeatLimitReached, v1.2.1+)
----------------------------------------------------------------------------------------

Cause: The floating server's max_leases_per_machine_id cap is set to N, and this workstation already holds N concurrent leases (from a mix of currently-running clients and any ghost leases still waiting to be reclaimed by the reaper). Distinct from "no licenses available": the global pool may still have seats free for OTHER machines, but this machine has hit its per-host ceiling.

Resolution: Close another running session on the SAME workstation and retry the checkout. If you know the prior client was force-killed (no graceful checkin) and you do not want to wait for the lease_timeout reaper, restarting the server clears all leases. If legitimate concurrency on one machine genuinely exceeds the cap, ask the operator to raise max_leases_per_machine_id. Operator sizing guideline: set the cap above expected steady-state concurrency to absorb crash-recovery; a tight cap means a crashed client temporarily blocks one slot until the reaper fires.

<!-- PAGEBREAK -->
================================================================================
11. GLOSSARY
================================================================================

A consolidated reference for terms used throughout this manual and the API reference. Defined inline elsewhere in the docs; consolidated here for readers who reach a section in isolation.

Anchor file:
A small file that stores a UTC timestamp plus an HMAC over that timestamp, used by the clock-manipulation detector. Anchor files are unique per license (per-license tag derived from license_id, product, and licensee). See §9.2 for storage locations.

Basic tier:
The lower of the two RockyGuard library tiers. Allows node-locked license generation and verification. Does not allow floating-license server / client. Configured by the vendor license file you receive from Rocky Software Inc.

Client ID:
A cryptographically random UUID generated at construction time by FloatingLicenseClient. Identifies one in-process client to the floating server. Distinct from machine ID (one process: distinct client_id; one host running N processes: same machine_id, N different client_ids).

End-user license:
The signed license file you (the library customer) issue to YOUR end users. Contains licensee name, product name, expiry, hardware fingerprint (for node-locked) or floating settings, feature list, and signature. Filename conventionally license.json. Generated with license_create.

Feature flag:
A string identifier in the end-user license's features list. Your application calls verifier.check_feature("export_pdf") (or similar) to gate code paths. Names are arbitrary; you choose the taxonomy.

Floating license:
A licensing model where N concurrent seats are managed by a server on the customer's LAN. Clients check out a seat at startup, send heartbeats while running, and release on exit. Premium tier only.

Ghost lease:
A floating-server lease still held by the server because the client that owned it crashed without sending a checkin. Reclaimed by the reaper thread after the lease_timeout window (YAML key lease_timeout; C++ struct field FloatingServerConfig::lease_timeout_sec). Counts toward max_leases_per_machine_id until reclaimed.

Grace period:
A configurable number of days after expires_at during which the license still verifies but with status InGracePeriod (not Valid). Lets your application warn the user without blocking access at the moment of expiry.

Hardware fingerprint:
A "|"-joined string of four SHA-256 hashes -- MAC address, CPU id, disk serial, motherboard serial -- used to bind a node-locked license to a specific machine. Computed by HardwareFingerprint::fingerprint(). Hashes only; no raw hardware identifiers leave the host.

Heartbeat:
A periodic POST sent by FloatingLicenseClient to the floating server to keep its lease alive. Default interval 60 seconds. If the server stops receiving heartbeats for the lease_timeout window (YAML key lease_timeout; C++ struct field FloatingServerConfig::lease_timeout_sec), the reaper reclaims the lease (it becomes a "ghost" briefly, then is dropped).

Integrity self-check:
A SHA-256 hash over the rockyguard.dll / librockyguard.so binary, signed by the vendor private key and shipped as rockyguard.sig. Verified at runtime by the library to detect post-build modification of the DLL. Static builds have no .sig file (§3.1, §9.3).

Machine ID:
The hardware fingerprint string sent by FloatingLicenseClient to the floating server on every checkout / heartbeat / checkin. Used by v1.2.1+ servers to enforce max_leases_per_machine_id. Multiple clients on one host share the same machine_id but each has its own client_id.

Node-locked license:
A licensing model where the license binds to one machine via hardware fingerprint. Available on both Basic and Premium tiers. Verified offline; no server contact.

Per-license tag:
The 12-hex-character abbreviation of SHA-256(license_id + "|" + product + "|" + licensee). The "|" separators are part of the tag formula; omitting them yields the wrong hash. Used as the suffix on every anchor-file storage location so different licenses on one machine do not interfere. See §9.2.

Premium tier:
The higher of the two RockyGuard library tiers. Allows everything Basic does, plus floating-license server / client. Configured by the vendor license file.

Public key (verification):
The Ed25519 or RSA public key embedded in your end-user application as a string constant. Pairs with the private signing key used by license_create. Safe to commit and ship; does not leak the ability to forge licenses.

Private key (signing):
The Ed25519 or RSA private key used by license_create to sign end-user licenses. Lives ONLY on your build machine. Never ships to end users. Never committed to public source control.

Reaper / reaper TTL:
The floating server's background thread that drops leases whose last heartbeat is older than the lease_timeout window. Runs on an interval of lease_timeout / 4, minimum 1 second. (The YAML key is lease_timeout; the matching C++ struct field is FloatingServerConfig::lease_timeout_sec. The "TTL" in informal usage refers to this same window.)

Session secret:
A short random string the floating server returns on a successful checkout. Used by FloatingLicenseClient to HMAC subsequent heartbeat and checkin requests so a third party who learns a client_id cannot forge eviction or heartbeat requests. Required regardless of TLS, because TLS does not authenticate application-level client identity.

Time anchor:
Any of the persistent storage locations the clock-manipulation detector writes timestamps to. See "anchor file" and §9.2. Multiple anchors are written per license; surviving anchors detect rollback even if some are deleted.

Vendor license:
The signed JSON file YOU (library customer) receive from Rocky Software Inc. that authorizes your use of RockyGuard for issuing end-user licenses. Required by license_create (every end-user license issued must be authorized) and by rg_floating_server / FloatingLicenseServer (Premium-tier gating). NOT required by license_keygen, which is a standalone tool that generates a fresh Ed25519 / RSA keypair before you receive any vendor license. NOT shipped to your end users; not needed by LicenseVerifier or FloatingLicenseClient. See §2.2.

Vendor public key:
A public key embedded in the RockyGuard library that verifies the vendor license signature at vendor-tool startup. You do not control this key; it is set by Rocky Software Inc. and ships inside the compiled library.

Worker thread / thread pool:
The floating server uses a fixed pool of worker threads to handle incoming client connections. Each worker handles one connection at a time. Configured by thread_pool_size (default 4).

<!-- PAGEBREAK -->
================================================================================
12. PRODUCTION CHECKLIST
================================================================================

A copy-paste preflight list before you ship a RockyGuard-protected build to a customer. Each item links to the section that has the full discussion; this list exists so you do not miss any one of them. Run through every item once per release cycle, not just on the first deployment.

12.1 Build configuration
--------------------------

- [ ] Consumer compiled with the matching CRT variant: link rockyguard.lib for Release, rockyguard_mdd.lib for Debug. Use the per-config $<IF:$<CONFIG:Debug>,...> generator expression in your CMakeLists, or rely on the IMPORTED_LOCATION_RELEASE / IMPORTED_LOCATION_DEBUG mechanism the shipped examples/CMakeLists.txt template uses. (§3.1; v1.2.1+ feature.)

- [ ] Public key embedded as a const char* string literal in your binary. NOT loaded from a file at runtime; NOT fetched over the network. (See §3.3, end-user application path / point B.)

- [ ] license_id and product set on every issued end-user license. Empty values weaken clock-manipulation defense and emit a stderr warning at load time. (§9.2 paragraph after §9.2 anchor table.)

- [ ] Decision made on runtime version enforcement (v1.2.1+). If you issue licenses with --version "3.*" (or similar) AND you want the library to BLOCK applications outside that range, your end-user code calls LicenseVerifier::check_version(YOUR_APP_VERSION) after check_node_locked(). If version_range is purely informational, you do nothing -- the library does not consult version_range automatically. Pick one and document it in your release notes so you do not silently change behavior between releases. (See §3.3 end-user application path / point B; Customer_API_Reference §6.7; troubleshooting at §10.2.5.)

12.2 Runtime / threading
-------------------------

- [ ] LicenseVerifier::load() is called off the UI thread on a desktop GUI app. The online time-anchor verification can take tens of seconds worst case if the network is flaky. Wrap in std::async or your application's job queue. A non-blocking load_async() API is on the v1.4 roadmap (deferred from v1.3). Note that load() holds an internal writer lock for the duration, so other calls on the same verifier block until it returns -- keep status polling off the UI's critical path rather than assuming a background load frees the object up. (Customer_API_Reference §6.2.)

- [ ] If the verifier is shared across threads, you have read the thread-safety contract (Customer_API_Reference §6 "Thread safety"): publish after load(), call the truly read-only methods (is_loaded, license, check_feature, check_version) freely from many threads, but treat check_expiry() AND check_node_locked() as single-writer (both perform filesystem / registry writes for clock-manipulation anchors -- check_node_locked() tail-calls check_expiry() on the hardware-pass path).

- [ ] LicenseVerifier construction is wrapped in try/catch. This applies to the STANDARD pattern too -- a PEM compiled in as a string literal. What the compiler guarantees is that the literal is present and unmodified at runtime, not that its contents parse as a key: a literal that was truncated, wrapped wrong, or pasted without its BEGIN/END armor compiles cleanly and throws at construction. Pasting public.pem into a source file is the most common way the standard pattern goes wrong, and an AI assistant doing the paste makes it more likely, not less. Uncaught, the throw is std::terminate() on the end user's machine -- a hard crash with no actionable message. LicenseVerifier is neither copyable nor movable, so hold it in a std::optional and emplace() if it must outlive the try block; §2.4's Quick Start shows the pattern, Customer_API_Reference §6.1 the rationale. (Earlier revisions of this checklist said the guard was unnecessary for the embedded-literal case. That was wrong and contradicted both §2.4 and API §6.1.)

- [ ] If you use FloatingLicenseClient (Premium tier only), you have read its threading contract (Customer_API_Reference §8 "Thread safety"). The client is internally synchronized: checkout() and checkin() serialize on an internal mutex and are safe to call from any thread; is_checked_out() is an atomic read with no lock and is advisory. The destructor is NOT safe to run concurrently with in-flight checkout() / checkin() / is_checked_out() from another thread (standard C++ object-lifetime rule). Construct once, publish to whatever threads need it, stop using it before destroying it. For genuinely-concurrent checkouts from many threads on one host, prefer one client instance per thread.

12.3 Floating-server deployment (Premium tier only)
-----------------------------------------------------

- [ ] Server has a vendor license file at the path named by vendor_license: in the YAML config. Same file you use for issuing end-user licenses; treat it with the same care as your private signing key. (§5.2.)

- [ ] private_key: set in the server config so every response is signed and the client can verify the server identity. Without it, a peer who reaches the server's port can mint fake responses (e.g., always "checked_out") to a client. Required regardless of whether TLS is enabled. (§5.2 "Response signing and TLS".)

- [ ] The server's private_key is a SEPARATE keypair from the license-signing key (the one you pass to license_create --key). Reusing the license-signing key for server response signing is supported but collapses two distinct trust roles into one: a single compromise gives the attacker both license-minting and server-impersonation capabilities. Generate a dedicated server keypair with tools/license_keygen and embed its PUBLIC key in your end-user binary as FloatingClientConfig::server_public_key_pem. (§5.2 "Response signing and TLS".)

- [ ] tls_cert: and tls_key: set if the server is reachable over a network a third party could intercept (Wi-Fi, the public internet, anything not a wired LAN you control). On the client, set tls_ca_cert_path to pin the server certificate. As of v1.3, use_tls = true with no tls_ca_cert_path is a hard error unless you also set allow_insecure_tls = true (which accepts any certificate and warns). Consider setting require_tls: true on the server so it refuses to start without a cert/key. Also as of v1.3, a server with no cert/key will not start at all unless allow_insecure_http: true is set, and a client with use_tls = false will not construct unless allow_insecure_http = true is set - so a plaintext v1.2.x deployment needs one of those two edits before it will run on v1.3. (§5.2 "Response signing and TLS".)

- [ ] rate_limit_rps / rate_limit_burst reviewed for the deployment. Defaults (50 rps / 250 burst per IP) suit typical LAN use. If many end-user machines share one egress IP (large NAT) and cold-start simultaneously, raise rate_limit_burst; set rate_limit_rps to 0 only if a fronting proxy already rate-limits. (§5.2, "Per-IP rate limiting".)

- [ ] max_leases_per_machine_id set ABOVE the steady-state concurrency you expect on one workstation. Ghost leases count toward the cap until the reaper TTL fires; a tight cap means a crashed client temporarily blocks one slot for the reaper window. (§5.2 "Per-machine seat cap".)

- [ ] log_max_bytes / log_keep_count appropriate for the deployment's disk budget. Defaults are 100 MiB / 5 archives = ~600 MiB worst case per log_file path. Set log_max_bytes <= 0 only if your host has its own log-rotation pipeline. (§5.2 "Log rotation".)

12.4 Deployment artifacts (what ships to your end users)
----------------------------------------------------------

- [ ] Static-link path: just your_application.exe + license.json. NO rockyguard.lib, NO rockyguard.dll, NO rockyguard.sig, NO public.pem file (it's embedded in your executable). (§3.4.)

- [ ] Shared-link path: your_application.exe + rockyguard.dll + rockyguard.sig + libcrypto-3-x64.dll + libssl-3-x64.dll + license.json, all in the same directory. The library refuses to load without the matching .sig. (§3.4 + §9.3.)

- [ ] Vendor license, private signing key, and the license_create / license_keygen tools from your customer zip NEVER ship to end users. Stay on your build machine. (§3.4.)

- [ ] deps/OPENSSL-LICENSE.txt (or its text, folded into your own third-party-notices document) ships with your application. This applies to EVERY path above, including the static ones and the Linux shared one where no OpenSSL file appears beside your binary -- OpenSSL is linked into it, so you are redistributing OpenSSL and Apache-2.0's attribution terms bind you. deps/THIRD-PARTY-NOTICES.txt names the exact version this zip carries. (§3.4 "Third-party attribution".)

- [ ] Linux artifacts, if you ship Linux: static path is your_application + license.json; shared path adds librockyguard.so and librockyguard.sig, and NO separate OpenSSL files (they are inside the .so). The three checkboxes above name Windows filenames because Windows is this manual's worked example; §3.4 carries the Linux ship lists in full. Note that item 1 of §12.1's build checklist -- the Debug/Release CRT split -- does not apply on Linux at all (§3.1).

12.5 Operational and security
-------------------------------

- [ ] You have read the threat model and accepted the residual risks called out there (notably: an air-gapped attacker who deletes ALL anchor locations and rolls the clock back gets one false-pass at first run; memory-dump attacks are out of scope). (§9.6.)

- [ ] You have a key-rotation plan if your private signing key is ever compromised. The plan is generally: generate new keypair, ship a new application version with the new public key embedded, re-issue end-user licenses signed with the new private key, set an EOL date on the old version's license-acceptance window. (§9.1; Versioning Policy doc.)

- [ ] You know which build of RockyGuard your release ships against (track ROCKYGUARD_VERSION_STRING from version.h in your release notes). Helps when correlating customer-side reports with library changelogs. (Customer_API_Reference §2.)

================================================================================
  END OF DOCUMENT
================================================================================
