RockyGuard Integration Guide for AI Assistants
You are an AI assistant (Claude, ChatGPT, Cursor, Copilot, Gemini, Cody, or similar) helping a software developer integrate the RockyGuard license library into their C++17 application.
Read this entire file before making any changes. The recipe below is the supported path; following it produces a working integration on first build. Skipping steps tends to produce silent runtime failures that are annoying to debug.
If you are a human reader: this file is a how-to. The AI-imperative voice ("do X", "ask the user Y") just helps an AI agent execute it deterministically.
Where to start. If you were pointed at
https://rockyguard.devand the user does not yet have the library on disk, start at Section 0.1 — it resolves the right build for their platform, verifies it, and extracts it. If you are reading this from the root of an already-extracted package (include/,lib/,tools/anddocs/are its siblings), start at Section 1.Canonical copy of this file: https://rockyguard.dev/AI_INTEGRATION_GUIDE.md. It is the same text that ships at the root of every package, and it is refreshed on every release — prefer it over a copy you were handed if the two disagree.
0. What you are integrating
RockyGuard is a C++17 static library that verifies signed license files at
program startup. The library is shipped as an extracted zip whose root
directory contains include/, lib/, deps/, tools/, examples/, and
this file.
The integration adds four things to the user's project:
- A small block of CMake that links against
<package>/lib/static/rockyguard.{lib,a}. - A C++17 string constant containing the user's RSA/Ed25519 public key.
- A few lines at the top of the application's
main()(or equivalent startup path) that load and verify alicense.jsonfile. - For each paid feature: a load-bearing binding (§6.5) — the feature's own mandatory data, decrypted with a key the license yields, so a patched-out check produces garbage instead of a working unlock.
There is no daemon, no SaaS, no online activation. Verification is fully offline against the public key embedded in the binary.
Read this before choosing where to spend effort. An offline license
check is, on its own, one branch on the attacker's CPU — flip
if (!result) refuse; and the app runs. The startup verify in §5 is
necessary but it is not where the protection lives; it drives user
messaging, expiry, and hardware binding. The enforcement that actually
resists patching is §6.5: binding each paid feature to a key derived from
the license. Treat §6.5 as part of the required recipe wherever a feature
has a bindable asset — not as optional hardening. Sections 9.1–9.4 and
9.6 are the genuinely optional extras on top.
Optionally -- and only if the user asks -- a fourth: a Help → Diagnose License dialog that reports what licensing is actually doing on an end user's machine. See Section 8. It is not part of the required recipe.
0.1 Get the package (start here if you do not already have it)
Skip this section if the user already has an extracted RockyGuard
package. You will know: this file sits at its root, beside include/,
lib/, tools/, deps/, cmake/ and docs/.
Otherwise work through the seven steps below, in order.
Do not improvise a download URL. The zip filenames carry the version number and rotate on every release, so any literal zip URL -- including one written in this sentence -- is wrong by the next one. Resolve the download from the manifest in step 2. Its URL is the only thing here that never moves.
Step 1 — Identify platform and architecture
Map the host to one of exactly three slugs:
| Host | Slug |
|---|---|
| Windows 10 / 11, x86-64 | windows-x64 |
| Linux, x86-64, glibc ≥ 2.34 (Ubuntu 22.04+, RHEL 9+, Debian 12+) | linux-x64 |
| macOS 13+, x86-64 | macos-x64 |
The bundles are x86-64 only. There is no arm64 build.
- Apple Silicon (arm64 macOS):
macos-x64runs under Rosetta 2, but the user's application must then be built for x86_64 too — you cannot link an x86_64 static library into an arm64 target. Stop and tell the user this before downloading, so they can decide whether an x86_64 toolchain is acceptable for their product. - arm64 Linux: unsupported. Stop and tell the user to contact https://rockyguard.dev/contact. Do not download the x64 bundle "to see if it works"; it cannot.
Determine glibc with ldd --version on Linux. On an older glibc, stop:
the prebuilt librockyguard.a will link but fail at load time with
unresolved versioned symbols, which is a confusing failure to hand a user.
Step 2 — Fetch the manifest
curl -fsSL https://rockyguard.dev/downloads/latest.json
It returns the current release. The fields you need:
| Field | Meaning |
|---|---|
version |
e.g. 1.4.0. Quote it to the user. |
platforms.<slug>.url |
The zip to download. |
platforms.<slug>.sha256 |
Digest you must verify in step 5. |
platforms.<slug>.bytes |
Exact size. Quote it when asking permission. |
platforms.<slug>.root |
The directory name the zip extracts to. |
license_request |
Where the 30-day Premium trial request goes (Section 10.5). |
If the manifest is unreachable, or the slug you need is absent from
platforms, stop and ask the user. Do not fall back to guessing a
filename, and do not scrape https://rockyguard.dev/download for a link
— that page is rendered from this same manifest, so a failure to fetch
one is not fixed by fetching the other.
Step 3 — Ask the user before downloading
This is a hard rule (Section 11), not a courtesy. Downloading and then executing binaries from the network is the user's decision to make, and they are entitled to make it with the facts in front of them. State, in one message: the product and version, the URL, the size in bytes, the SHA-256 you are going to verify against, and where on disk you intend to put it.
Wait for an explicit yes. If the user declines, stop — do not offer a workaround, and do not proceed with a package from some other source.
Step 4 — Download
# Linux / macOS
curl -fL -o rockyguard.zip "<platforms.<slug>.url>"
# Windows PowerShell
Invoke-WebRequest -Uri "<platforms.<slug>.url>" -OutFile rockyguard.zip
Download to a scratch directory, not into the user's repository. Nothing enters their tree until step 6, after the digest has been checked.
Step 5 — Verify the SHA-256, and abort on mismatch
# Linux
sha256sum rockyguard.zip
# macOS
shasum -a 256 rockyguard.zip
# Windows PowerShell
(Get-FileHash -Algorithm SHA256 rockyguard.zip).Hash.ToLower()
Compare against platforms.<slug>.sha256 from the manifest.
On mismatch: delete the file, tell the user the digest did not match, and stop. Do not retry, do not extract it "to check", and do not continue with a note in the summary. A mismatch means the bytes are not the bytes we published, and there is no benign reading of that which justifies running the tools inside. The user can re-run later; a corrupted or substituted package integrated into their build is not something they can undo by re-running anything.
Step 6 — Extract into the repository
Confirm the destination with the user first — Section 2 covers the
layout and the cases where you must ask (an existing third_party/
convention, submodules, vcpkg, Conan). The default is
<their-repo>/vendor/rockyguard/.
Extract, then move the contents of the root directory (the manifest
told you its name) up so the package root is vendor/rockyguard/.
You want vendor/rockyguard/include/, not
vendor/rockyguard/rockyguard-v1.4.0-linux-x64-demo/include/ — every
CMake path in Section 3 assumes the former.
Then make the tools runnable:
- Linux / macOS: the zips carry Unix mode bits, but some extractors
drop them. Check
ls -l vendor/rockyguard/tools/andchmod +x vendor/rockyguard/tools/*if the exec bit is missing. - macOS additionally: the binaries are not notarized, so Gatekeeper
will refuse to run them after a browser or
curldownload sets the quarantine attribute — the symptom is a "cannot be opened because the developer cannot be verified" dialog, or a barekilled: 9. Clearing it requires the user's informed consent, so tell them what the command does and why before running it:
If the user would rather clear it per-binary through System Settings → Privacy & Security, that works too and is entirely reasonable.xattr -dr com.apple.quarantine vendor/rockyguard
Step 7 — Smoke-test, and surface the licence agreement
vendor/rockyguard/tools/rg_fingerprint --help
If that prints usage, the package is intact and the toolchain can run it. A missing-shared-library error on Linux usually means the glibc check in step 1 was skipped.
Then tell the user, in one sentence, that the package ships the
RockyGuard Library Software License Agreement at
vendor/rockyguard/docs/RockyGuard_License_Agreement.pdf (and .txt),
and that the 30-day Premium trial does not grant the right to sell
licences to paying end users — that needs Basic or Premium. Do not
paraphrase the agreement's terms beyond that, and do not accept it on the
user's behalf.
You now have everything Sections 1–10 need. Continue at Section 1.
1. Inputs the user must provide
Before writing any code, settle the following. Ask explicitly; do not guess. Items 2–4 are hard gates — halt until you have answers.
Public key (PEM format, ~100 bytes for Ed25519, ~400 bytes for RSA).
- If the user already has a keypair, this is a hard gate too: read
the actual
public.pemcontent from disk, or ask them to paste it. Do not invent or generate one to stand in for theirs. - If they do not have one yet, do not halt — generate it, after
telling them you are about to:
This produces<package>/tools/license_keygen --algo ed25519private.pem(stays on the user's build machine — never commit, never ship; add it to.gitignoreper Section 11) andpublic.pem(embedded in the compiled binary in Section 4). Ed25519 is the right default: smaller signatures, smaller embedded key, and the verifier auto-detects the algorithm either way (Section 5). Choose RSA only if the user says they need it. - Confirm with the user that the key you are embedding is the public
one. Embedding
private.pemby mistake ships their signing key to every customer, and nothing downstream will warn you.
- If the user already has a keypair, this is a hard gate too: read
the actual
Where the application's
main()lives. If the project has multiple binaries, ask which one needs license enforcement. Default to the end-user-facing executable.Where they want
license.jsonto be loaded from at runtime. The default is a per-user, user-writable location:%APPDATA%\<Company>\<Product>\license.jsonon Windows,$XDG_CONFIG_HOME/<Company>/license.json(falling back to~/.config) on Linux. "Next to the executable" is acceptable only when the executable itself lives somewhere the running user can write (a portable or per-user install) -- never under%ProgramFiles%or/opt. Reason: the anti-rollback machinery writes a witness file (.lck_cache_<tag>) beside the license on every verification; when that directory is read-only the write fails silently, verification still passes, and clock-rollback protection is weaker on that machine for good (manual §3.4, §9.2). Confirm before deviating from the default, and say why when you do.Tier they have purchased, or are trialling.
- Not sure: treat as Basic for the purposes of this guide, and skip Section 7. The library is not crippled — the tier is a licensing decision, not a build — so the whole recipe except floating licensing applies unchanged.
- Basic: node-locked licensing only. Skip Section 7.
- Premium: node-locked + floating-license server/client. Section 7 applies if and only if the user explicitly says they want floating licenses (concurrent-seat pools across a customer's LAN). Most Premium customers ship products that use only node-locked.
- 30-day Premium trial: the trial vendor licence is Premium tier, so treat it as Premium above — Section 7 only if they explicitly ask for floating. If they do, say that keeping floating licensing after the trial needs Premium; Basic is node-locked only.
Whether they already have a vendor licence. Ask, but do not halt on it — this is the one input that is allowed to arrive last.
A vendor licence (
vendor_license.json, issued by Rocky Software and bound to the user's build machine) is whatlicense_createrequires in order to mint an end-userlicense.json. A first-time user will not have one, and cannot get one until they have runrg_fingerprint— for which they need the package, which they now have.So the ordering is: integrate first, request the vendor licence at the end (Section 10.5), mint and run the end-to-end test when it arrives. Everything in Sections 2–9 — CMake, the embedded key, the startup verify, feature gates, asset binding — is written and compiled without it. Say this to the user up front so the gap is expected rather than read as a broken integration, and do not go looking for a vendor licence to "unblock" yourself.
Never invent, fabricate or self-sign a vendor licence, and never reuse the
sample_demo_*credentials from the bundle'sdemo/directory for anything but the bundled demo. They are ours, published, and identical for every downloader; a build wired to them verifies against a keypair the whole internet has.
2. Place the package inside the user's repo
If you came through Section 0.1 you have already done this, with the destination confirmed at step 6. Read this section anyway to check the result against the layout below, then move on.
Recommended layout (verify with the user before moving files):
<their-repo>/
src/ # their existing source
CMakeLists.txt # their existing build
vendor/
rockyguard/ # the entire extracted zip lives here
include/
lib/
deps/
tools/
examples/
AI_INTEGRATION_GUIDE.md # this file
README.txt
If their repo already has a third_party/ or external/ convention,
match it. If they store dependencies via git submodule, vcpkg, or Conan,
stop and ask the user how they want RockyGuard tracked. Do not
unilaterally invent a new dependency-management scheme.
3. Add the library to CMake
Add this block to the project's top-level CMakeLists.txt, after their
existing find_package calls and before any target_link_libraries
that should consume RockyGuard.
# --- RockyGuard license library ---
if(NOT DEFINED ROCKYGUARD_DIR)
set(ROCKYGUARD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/rockyguard")
endif()
if(NOT EXISTS "${ROCKYGUARD_DIR}/include/rockyguard/rockyguard.h")
message(FATAL_ERROR "RockyGuard not found at ${ROCKYGUARD_DIR}")
endif()
set(_RG_DEPS "${ROCKYGUARD_DIR}/deps")
add_library(rg_openssl_crypto STATIC IMPORTED)
add_library(rg_openssl_ssl STATIC IMPORTED)
if(WIN32)
set_target_properties(rg_openssl_crypto PROPERTIES
IMPORTED_LOCATION "${_RG_DEPS}/lib/libcrypto.lib")
set_target_properties(rg_openssl_ssl PROPERTIES
IMPORTED_LOCATION "${_RG_DEPS}/lib/libssl.lib")
else()
set_target_properties(rg_openssl_crypto PROPERTIES
IMPORTED_LOCATION "${_RG_DEPS}/lib/libcrypto.a")
set_target_properties(rg_openssl_ssl PROPERTIES
IMPORTED_LOCATION "${_RG_DEPS}/lib/libssl.a")
endif()
target_include_directories(rg_openssl_crypto INTERFACE "${_RG_DEPS}/include")
target_include_directories(rg_openssl_ssl INTERFACE "${_RG_DEPS}/include")
add_library(rockyguard STATIC IMPORTED)
if(WIN32)
# Per-config CRT variants. rockyguard.lib is built with /MD
# (Release-CRT) and rockyguard_mdd.lib with /MDd (Debug-CRT).
# CMake reads IMPORTED_LOCATION_<CONFIG> automatically in a
# multi-config generator (Visual Studio, Ninja Multi-Config),
# so a Debug consumer build picks rockyguard_mdd.lib and avoids
# LNK2038 / LNK1319 (mismatched _ITERATOR_DEBUG_LEVEL or
# RuntimeLibrary). Other configs (RelWithDebInfo, MinSizeRel)
# map to the Release variant.
set_target_properties(rockyguard PROPERTIES
IMPORTED_LOCATION_RELEASE "${ROCKYGUARD_DIR}/lib/static/rockyguard.lib"
IMPORTED_LOCATION_DEBUG "${ROCKYGUARD_DIR}/lib/static/rockyguard_mdd.lib"
IMPORTED_LOCATION "${ROCKYGUARD_DIR}/lib/static/rockyguard.lib"
MAP_IMPORTED_CONFIG_MINSIZEREL Release
MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release)
else()
set_target_properties(rockyguard PROPERTIES
IMPORTED_LOCATION "${ROCKYGUARD_DIR}/lib/static/librockyguard.a")
endif()
target_include_directories(rockyguard INTERFACE "${ROCKYGUARD_DIR}/include")
# Link order matters on Linux: ssl before crypto. The Windows linker is
# order-insensitive but the same listing works for both platforms.
target_link_libraries(rockyguard INTERFACE rg_openssl_ssl rg_openssl_crypto)
if(WIN32)
target_link_libraries(rockyguard INTERFACE
ws2_32 crypt32 iphlpapi ole32 oleaut32 wbemuuid advapi32 shell32)
elseif(NOT APPLE)
target_link_libraries(rockyguard INTERFACE pthread dl)
endif()
# This template links the STATIC library, which is the default and the
# only path with a Debug-CRT variant. If the user insists on the DLL
# (lib/shared/rockyguard.dll, import lib rockyguard.lib, plus
# rockyguard.sig and the two OpenSSL DLLs beside the exe), note that
# the DLL is built /MD ONLY: a Debug (/MDd) consumer links its import
# library without error and then corrupts memory, because std::string /
# vector / map have a different layout under the two CRTs and
# LicenseResult / License cross the boundary by value. Keep the Debug
# configuration on the static rockyguard_mdd.lib, or compile it with
# the Release CRT. Manual 3.1 / 3.2.
#
# You will not get far wrong by accident: since v1.4.0 export.h emits a
# hard #error for a Debug-CRT translation unit that has
# ROCKYGUARD_SHARED_LIB defined, naming both remedies. Do NOT reach for
# the escape hatch (ROCKYGUARD_ALLOW_DEBUG_CRT_WITH_DLL) to make that
# error go away -- it exists only for someone who has built their own
# Debug rockyguard.dll from source, and defining it against the SHIPPED
# DLL restores exactly the silent corruption the guard prevents.
# C++17 is REQUIRED. Putting it on the interface means every target that
# links rockyguard inherits it, so you do not have to remember to set
# CMAKE_CXX_STANDARD in the consuming project -- and a project that pins
# an older standard gets a clear CMake-level conflict rather than a
# compile error deep in a header.
target_compile_features(rockyguard INTERFACE cxx_std_17)
# --- end RockyGuard ---
Do not omit the target_compile_features line. The shipped library
is built as C++17 and enforces it with a static_assert in
include/rockyguard/export.h, which every public header reaches. MSVC
still defaults to C++14, so on Windows a project that has not set the
standard itself fails to build with:
error C2338: static_assert failed: 'RockyGuard requires C++17 or later.
In CMake: set(CMAKE_CXX_STANDARD 17); set(CMAKE_CXX_STANDARD_REQUIRED ON).
Direct compiler flag: /std:c++17 (MSVC) or -std=c++17 (gcc/clang).'
Recent g++ and clang default to C++17 or later, so the omission is
invisible there — which is exactly why it is easy to ship a
CMakeLists.txt that only works on the machine it was written on. If you
would rather set it on the consuming project instead of the interface,
set(CMAKE_CXX_STANDARD 17) plus
set(CMAKE_CXX_STANDARD_REQUIRED ON) is equivalent.
Then, on the target you decided in Section 1.2, add:
target_link_libraries(<their_main_target> PRIVATE rockyguard)
Replace <their_main_target> with the actual add_executable() name in
their CMakeLists.
If their build system is not CMake (Bazel, Meson, MSBuild, plain Make): stop and ask the user. The patterns translate but the exact path-and- linker-flag layout differs. Don't guess.
4. Embed the public key
Find the source file containing int main(...). At the top, after the
existing #includes, add:
#include <rockyguard/rockyguard.h>
namespace {
// RockyGuard public key. Pairs with the private key on the build machine.
// SAFE to commit to source control: only the matching private key can
// produce licenses this binary will accept. The key below is a placeholder;
// replace it with the contents of public.pem from `tools/license_keygen`.
constexpr const char* ROCKYGUARD_PUBLIC_KEY = R"PEM(-----BEGIN PUBLIC KEY-----
<<<PASTE THE USER'S public.pem CONTENT HERE>>>
-----END PUBLIC KEY-----
)PEM";
} // namespace
Read the user's public.pem file and substitute its content into the
placeholder above. Do not generate, fake, or shorten the key. If you
cannot read the file, ask the user to paste it.
5. Add verification at startup
Insert these lines as the first thing main() does, before any
substantive work. The pattern:
int main(int argc, char* argv[]) {
// The constructor is the only verifier call that throws. It raises
// std::runtime_error if ROCKYGUARD_PUBLIC_KEY is not a parseable
// PEM public key -- which is exactly what a truncated or mangled
// paste in Section 4 produces. Uncaught, that is std::terminate():
// the user sees a hard crash instead of a message. Guard it.
//
// std::optional keeps construction inside the try while `verifier`
// stays in scope, so the existing main() body below does NOT need
// to be re-indented into a try block. LicenseVerifier is neither
// copyable nor movable, so emplace() constructs it in place --
// plain assignment will not compile.
std::optional<rockyguard::LicenseVerifier> maybe_verifier;
try {
maybe_verifier.emplace(ROCKYGUARD_PUBLIC_KEY);
} catch (const std::runtime_error& e) {
std::cerr << "License system error: " << e.what() << "\n";
return 1;
}
rockyguard::LicenseVerifier& verifier = *maybe_verifier;
auto load_result = verifier.load("license.json");
log_line(load_result.to_json()); // see "Report by code" below
if (!load_result) {
std::cerr << "License error: " << load_result.message << "\n";
return 1;
}
// Capture the result. Do NOT write
// `if (!verifier.check_node_locked())` and throw it away: a result
// that PASSES can still carry a warning worth telling the user
// about, and a result that fails carries the code you log and the
// remedy you show. Nor is a hard-coded "not valid for this machine"
// line right here -- this call also reports clock manipulation,
// integrity and expiry failures, so most of what it refuses has
// nothing to do with the hardware.
const auto hw_result = verifier.check_node_locked();
log_line(hw_result.to_json());
if (!hw_result) {
std::cerr << "License error: " << hw_result.message << "\n";
return 1;
}
if (rockyguard::error_info(hw_result.code).severity ==
rockyguard::DiagnosticSeverity::Warning) {
// Passing, but say something -- expired-but-in-grace, or a
// clock anomaly that was repaired and will probably recur.
// Never gate startup on this branch.
warn_the_user(hw_result.message);
}
// ... existing main() body continues here ...
}
Add #include <optional> alongside the other includes if it is not
already present, and #include <rockyguard/license_error.h> for
error_info() / DiagnosticSeverity (the umbrella
<rockyguard/rockyguard.h> already pulls it in, but name it if the
project includes the individual headers). Replace log_line() and
warn_the_user() with whatever the project actually uses; do not
introduce a logging framework it does not already have.
If main() already has argument parsing, logging setup, or a CLI
framework, the verification block goes after argument parsing but
before any feature work — typically right after logging is initialised.
The "license.json" path is relative to the working directory. Substitute
the per-user path chosen in Section 1.3 (resolve %APPDATA% /
$XDG_CONFIG_HOME at runtime; do not hard-code a user's home). Whatever
the path, its directory must be writable by the running user -- the
library writes an anti-rollback witness beside the license and fails
silently when it cannot.
Do not catch and swallow license errors. If verification fails, the application must not proceed into licensed functionality. Treating a failed license as success is the most common integration mistake and silently disables protection.
"Must not proceed" is not the same as "must call exit()". How to
refuse depends on what you are integrating into, and return 1 from
main() is only correct for the first case:
| Host type | Correct response on failure |
|---|---|
| Console app / CLI | Print the message to stderr and return non-zero from main(). This is the snippet above. |
| GUI app | Show a dialog with result.message -- or, if the dialog has a separate action line, your own headline plus result.suggested_action -- then quit the event loop. Exiting silently before the main window appears is indistinguishable from a crash to the end user. |
| Service / daemon | Log the failure and either stay up in a disabled state or exit deliberately. A bare exit() in a supervised service usually triggers a restart loop that hides the real cause. |
| Plugin / shared library | Return a failure code to the host and disable your features. Never call exit() or abort() here — there is no main() of yours to return from, and terminating takes down the host application (the CAD package, IDE, or DAW that loaded you) along with any unsaved user work. |
Ask the user which of these their application is if it is not obvious from the codebase, and pick the matching pattern.
Report by code, never by message text. (v1.4+) Every LicenseResult
carries a fine-grained LicenseErrorCode in result.code, alongside the
coarse result.status it has always had. The two are orthogonal: each
code maps onto exactly one status, and the mapping reproduces what
pre-v1.4 releases returned, so status-based code keeps working unchanged.
When you generate error handling, follow these rules -- this is the part
integrations get wrong in a way that only shows up years later, when a
release rewords a sentence and someone's if (msg.find("expired"))
quietly stops firing.
- Branch on
result.statusfor control flow. It is the coarse outcome and it is frozen -- v1.4 added no enumerator toLicenseStatus, precisely so that a customer's exhaustiveswitchkeeps compiling without new-Wswitchwarnings. - Use
result.codewhen one status covers conditions you want to treat differently.HardwareMismatchcovers bothHWID_MISMATCH(this really is a different machine) andHWID_MISSING_IN_LICENSE(the licence was minted without a fingerprint -- an issuance bug to route back at the vendor).ExpiredcoversLICENSE_EXPIREDandGRACE_PERIOD_EXPIRED, which are two different conversations to have with a paying customer.ServerUnreachablecovers sixNETWORK_*transport causes that used to be one opaque "cannot reach license server". - Never substring-match, regex, or compare
result.message. The code and itsSCREAMING_SNAKE_CASEname are documented stable contracts; the wording ofmessageis documented as explicitly NOT one, and it is reworded between releases. If you catch yourself writing a string comparison againstmessage, the code you want already exists -- look it up in Customer_API_Reference §4.5. - Log
result.to_json(). One line, machine-parseable, carrying code, status, severity, retryable, message, detail, suggested_action and grace days. It is the single thing worth capturing for support and the thing to ask the user to paste into a ticket. Log it on the passing path too -- a clean run's line is what makes a later failure line meaningful.result.to_string()is the shorter form if a JSON line is more than the project's log format wants. - Show
result.suggested_actionas the user-facing "what to do". Do not invent remedy wording; these sentences are written per code and are maintained with the library, so they cannot go stale the way a hard-coded string in the host does.result.messageis that same text pre-joined (summary + detail + suggested_action), so printmessageor the parts -- printing both says the remedy twice. - Treat
DiagnosticSeverity::Warningas "passing, but say something".error_info(result.code).severityisWarningonly on results that convert totrue: currentlyOK_GRACE_PERIOD_ACTIVEandOK_CLOCK_ANCHOR_REPAIRED. Surface it and let the application run. Blocking startup on a licence that passed is a worse bug than not reporting the warning at all. - For a floating checkout, retry only on
error_info(result.code).retryable, with backoff, and stop as soon as it is false. See Customer_API_Reference §4.6 for the shape.
Two notes on upgrading an existing integration. LicenseStatus is
unchanged, so nothing that switches on it needs editing. LicenseResult
gained three appended members and therefore changed size, so a project
moving to v1.4 must recompile against the new headers -- dropping a
v1.4 shared library under a binary built against v1.3 is not supported
(RockyGuard_Versioning_Policy section 6).
Full catalog of codes with meanings and remedies: §4.5 of
docs/Customer_API_Reference.pdf. The catalog lookups
(error_info(), error_code_name(), error_code_from_name(),
status_name(), error_catalog(), DiagnosticSeverity) and a worked
handling example: §4.6. Read both before writing error handling by
hand; do not guess code names, they are listed there.
6. Feature gating (call this out to the user)
This is the part most integrations miss. Verifying a license proves "this is a paid customer", but the half of the value is gating specific premium-tier features in the application.
Before writing any feature checks, ask the user:
"Looking at your product, here are the features I can identify that might be paid-tier: [list 3-7 candidates derived from the codebase — e.g. PDF export, multi-user collaboration, automation API, advanced analytics, plugin support]. Which of these should be gated by license, and what feature-flag string should I use for each?"
Wait for their answer. Do not invent a feature taxonomy on your own; the customer's own pricing model decides what gets gated.
Once they answer, gate each feature with:
if (!verifier.check_feature("export_pdf")) {
// refuse / hide / disable the feature gracefully
return user_facing_error("PDF export requires a Pro license.");
}
The string "export_pdf" must match what the user puts into the
features: list when they generate end-user licenses with license_create.
Record the chosen feature names in a FEATURES.md at the repo root: copy
<package>/examples/FEATURES.template.md there and fill it in. It is the one
place code, the build (rockyguard_bind_asset), and license minting
(license_create --feature <name>, repeated per feature) must agree on every flag string, and it also
records which features are bound (§6.5) versus boolean-only (§6.5.7). Keep it
current — a flag typo in any of the three fails silently.
check_feature() returns a bool, and a bool is one branch an attacker
flips. It is the right tool for greying out a menu or showing "Pro only",
but it is not, by itself, enforcement. For each gated feature that loads a
mandatory asset, the enforcement is §6.5 — bind that asset to the license.
Do §6.5 for those features in the same pass as §6; do not defer it to a
later "hardening" phase.
It is also a lookup, not a re-validation: it reads the License cached by
load() and never re-checks expiry, the clock or integrity, so a process that
stays up for days keeps getting true after the license has expired. The
periodic background timer in §9.4 (re-verify with check_node_locked() or
check_expiry() and gate on that result) is what closes this for long-running
hosts. It is not optional for a service or a desktop application.
6.5 Make each paid feature load-bearing (REQUIRED where a bindable asset exists)
Sections 5 and 6 make licensing work and drive the UI. This section makes it hold. Instead of branching on a verify result — a decision made on the attacker's CPU, so a branch they can flip — you derive a key from the validated license and use it to decrypt data the feature genuinely needs (a lookup table, a resource blob, a model, a ruleset). Patch the check out and the key never appears, so the bytes stay encrypted and the feature produces garbage rather than a working unlock. A bool has two values; a 32-byte key has 2²⁵⁶ — you cannot patch a function into emitting a key it does not possess.
The library ships this as an API with no verdict in it:
rockyguard::LicenseAsset (<rockyguard/license_asset.h>) captures the
32-byte key the vendor sealed into the license — the product data key
(license_create --wrap-data-key) or one feature's key
(--wrap-feature-key <name>=<hex>) — and only when a genuine signature
is present, the one field an attacker cannot forge. Its open() returns the
decrypted asset or an empty vector; there is no ok(), no operator bool,
no std::optional, and no way to ask whether the key arrived. The package
ships the build side too: tools/rg_bind_asset seals assets, the
rockyguard_bind_asset() CMake helper wires it into the build, and
tools/rg_bind_lint catches regressions. (The lower-level
LicenseVerifier::unwrap_data_key() / unwrap_feature_key() return
std::optional; prefer LicenseAsset, which exists precisely so nobody
writes if (!key) around them.)
6.5.1 The rule
For every feature you gated in §6: if the feature loads an asset it
cannot function without, bind that asset (steps below) instead of relying on
the check_feature() bool. A feature with no bindable asset falls back to
§6.5.7 — and you tell the user which features got only bool protection.
6.5.2 Discover bindable assets — run this procedure, then confirm (do not invent)
Do not ask the user an open-ended "what could I encrypt?". Run this and present the result:
- For each gated feature, find its entry point and list every file or blob
it loads at runtime and cannot run without: resource bundles
(
.rcc/.qrc/.pak/.dat/.bin), model weights, lookup / rule / pricing tables, shader or parameter blobs, embedded scripting bytecode, schemas. - Score each candidate 0–2 on each axis:
- Mandatory — the feature is dead or produces garbage without it.
- Choke-loadable — it enters through one narrow call you can route through a single decrypt.
- Opaque — wrong bytes are visibly broken, not silently subtly wrong.
- Small — under a few MB (it is decrypted into memory).
- Present a ranked table to the user. Propose binding the top candidate per feature. Ask them only to confirm or correct — not to invent.
- Any feature with no candidate scoring ≥1 on both Mandatory and Choke-loadable is boolean-only: handle it per §6.5.7 and say so.
6.5.3 Seal the asset at build time (one tool, one CMake call)
Do not hand-roll AES-GCM. Use the shipped binder; the plaintext never enters the repo or the binary.
Generate the keys (once each) and keep them in the build machine's / CI's secret store, never in the tree:
<package>/tools/license_create --gen-data-key # product key -> RG_DATA_KEY <package>/tools/license_create --gen-data-key # key for export_pdf -> RG_KEY_EXPORT_PDFOne product key for assets every licensee gets, plus one separate key per bound feature for anything tier-gated (§6.5.5 explains why the product key alone gives no tier separation).
Wire the encrypt step into the build with the CMake helper (
<package>/cmake/RockyGuardBindAsset.cmake):list(APPEND CMAKE_MODULE_PATH "${ROCKYGUARD_DIR}/cmake") include(RockyGuardBindAsset) rockyguard_bind_asset(TARGET <their_main_target> FEATURE export_pdf # must match the feature name in §6 ASSET ${CMAKE_SOURCE_DIR}/assets/export_tables.bin HEADER ${CMAKE_BINARY_DIR}/generated/export_tables.enc.h SYMBOL kExportTables DATA_KEY_ENV RG_KEY_EXPORT_PDF) # that feature's key, from the environmentThe build reads the key from
$RG_KEY_EXPORT_PDFat build time and generates a ciphertext-only header (kExportTables/kExportTables_len). Provide the key to the build environment (CI secret), e.g.RG_KEY_EXPORT_PDF=<hex> cmake --build build. For a product-wide asset pointDATA_KEY_ENVatRG_DATA_KEYinstead. Under the hood this runstools/rg_bind_asset(prebuilt in the package, source beside it; the examples CMake also builds it as targetrg_bind_asset). The blob it writes is the library's ownnonce(12) || ciphertext || tag(16)layout — exactly whatLicenseAsset::open()reads; nothing is derived in between.Wrap the same keys into each end-user license at mint time — the product key into every license, each feature key only into licenses entitled to that feature:
<package>/tools/license_create ... --feature export_pdf \ --wrap-data-key <product hex> --wrap-feature-key export_pdf=<feature hex> \ --output license.json--featuretakes one name and is repeated per feature (--feature export_pdf --feature batch_mode). There is no--features, and a comma-separated value is not split — it becomes one feature whose name contains a comma.license_createrefuses to wrap keys into a short evaluation license unless you pass--allow-eval-keys. Do not: a trial carrying production keys hands them to anyone who downloads it.If you are testing this integration, do not test it with a trial license. A license expiring within 7 days is an evaluation mint and carries no wrapped key —
license_createrefuses to put one there, and the licensing portal silently omits it. The integration then behaves as if it were broken: every check passes, butunwrap_data_key()andLicenseAsset::open()return nothing, with no error code to explain it because nothing actually failed. Use a license that expires more than 7 days out for any development or test work on this path.
6.5.4 Open and USE it at runtime — without branching
Nothing to copy but two lines. At the feature's load site — where it used
to call check_feature() and branch — construct a LicenseAsset for that
feature and feed open()'s result straight into the work:
#include <rockyguard/license_asset.h>
#include "export_tables.enc.h" // generated in 6.5.3: kExportTables / kExportTables_len
// ...
rockyguard::LicenseAsset asset(verifier, "export_pdf"); // AFTER verifier.load(); cannot fail
load_export_tables(asset.open(kExportTables, kExportTables_len)); // real work; empty/garbage without a genuine, entitled license
For a product-wide asset use rockyguard::LicenseAsset asset(verifier);
(no feature name). open() returns the plaintext, or an empty vector when
there is no key, the blob is malformed, or authentication fails — and it
deliberately does not say which. Do not write if (table.empty()) refuse; around it: that is the branch this section exists to remove. Let
the loader fail on nothing. Construct the LicenseAsset after load(); one
built earlier captures nothing, silently, by design.
The feature is now incapable of working without the key. There is no
verdict to patch — only the plaintext, which never materialises for an
unlicensed run. (examples/bound_asset.h is the build-time sealer behind the
binder; the application does not need it.)
6.5.5 Per-feature keys are wrapped by the vendor, not derived
The product data key answers "was this license genuinely signed". It does
not answer "is this license entitled to this feature": every license
carries the product key, so an asset sealed under it opens for every
licensee, and entitlement would be back to the check_feature() bool. So for
each tier-gated feature you generate a separate key (§6.5.3 step 1) and
license_create --wrap-feature-key puts it only into licenses entitled to
that feature. A Basic license physically carries no export_pdf key;
LicenseAsset(verifier, "export_pdf") captures nothing; a patched
check_feature() cannot insert a key the envelope never carried.
Do not "simplify" this into subkeys derived from the product key
(HKDF(product_key, feature) or similar). It was evaluated and rejected:
every input to that derivation is public or comes from the one product-wide
key, so a license of any tier yields every tier's key offline — the
appearance of tier separation with none of the substance. The binder seals
under whichever key you hand it and derives nothing; the separation comes
from what the vendor wraps into each license.
Bind eight features → eight independent decryptions under eight keys, no shared verdict anywhere; patch any one site and only that feature breaks (into garbage), while the others never consulted a check to patch. One leaked plaintext does not compromise other features' assets either, since they never shared a key.
6.5.6 The negative test is MANDATORY (§10 enforces it)
A fake binding passes the normal "it works with a good license" test. Only the negative test distinguishes real binding: with a wrong/foreign license (valid signature, different key), the bound feature must be visibly broken/garbage — not a clean "license invalid" that still lets the rest of the app run. If the feature still works with the wrong key, the binding is fake; do not ship it. See §10.
6.5.7 When a feature has no bindable asset (honest fallback)
Not every feature loads opaque mandatory data. In order:
- Manufacture a dependency: find a constant or table the feature's real
code already uses (a coefficient set, a state table, embedded defaults),
move it into an encrypted asset, and open it with
LicenseAsset. Most "pure logic" features have at least one such table. - If there is genuinely nothing to bind, fall back to code-diversity re-checks (§9.4 item 3) and tell the user plainly: "feature X is protected only by a flippable check, because it has no bindable asset." Never leave it looking hardened when it is not.
6.5.8 State the ceiling — do not oversell it
This defeats the free branch-flip / keygen-less crack and forces an attacker
to possess a genuine license and reverse your derive/decrypt path — so
compile that translation unit under ROCKYGUARD_OBFUSCATE. It does not,
for an offline scheme, force signature forgery: one leaked genuine license
carries the signature that yields the key (break-once-run-everywhere). It is
signature-bound, not machine-bound — keep check_node_locked() (§5) for
hardware/expiry enforcement; the key derivation deliberately does not fold in
the live fingerprint, so ordinary hardware drift does not break decryption,
and it does not fold in time either: offline, the only clock is the end
user's, so a key that depended on "not yet expired" would be a branch on the
local clock wearing a KDF. Expiry stays a verdict from check_expiry();
treat it as one (§9.4).
For leak traceability, use a per-customer data key and record which key
went to whom. Full worked example: examples/loadbearing_asset_example.cpp.
7. Floating licensing (Premium tier, only if requested)
Skip this section unless the user has explicitly said:
- They have a Premium-tier vendor license, AND
- Their product is multi-seat / multi-user, AND
- They want concurrent-seat licensing across their customer's LAN.
If yes, see examples/rg_floating_client.cpp in this package. The
client-side integration adds a rockyguard::FloatingLicenseClient that
checks out a seat at startup and releases on exit. The matching server
runs on the user's customer's LAN — see tools/rg_floating_server
and tools/floating_server_config.yaml. Walk the user through running
the server before adding client-side checkout calls; otherwise the
first checkout() returns ServerUnreachable -- as of v1.4 with a code
saying which transport failure it was, usually
NETWORK_CONNECTION_REFUSED for "nothing is listening" as against
NETWORK_HOST_UNRESOLVED for a wrong hostname. Log
result.to_json() around a failing checkout and the answer is in the
"code" field rather than in a guess.
Heartbeat callbacks run on the client's own thread. If you wire
FloatingHeartbeatCallbacks (on_connection_lost, on_connection_restored,
on_license_revoked, on_give_up), they fire on the client's internal
heartbeat thread. From inside one, never call checkout() or checkin()
on that client (rejected with FLOATING_REENTRANT_CALL since v1.4; a crash
or deadlock before), and never destroy the client (delete,
unique_ptr::reset(), scope exit): the destructor joins the thread the
callback is running on, and that self-join terminates the process with no
guard possible. Set an atomic flag or post to your UI / work queue, and act
from the thread that owns the client. is_checked_out() is safe from a
callback. Reference §8.7.2; manual §10.5.4.
Transport security is mandatory-by-default as of v1.3. Both sides fail closed at startup rather than falling back to cleartext, so a configuration that worked against v1.2.x can now refuse to start:
- Client —
FloatingLicenseClient's constructor throwsstd::runtime_error(before any network call) ifuse_tlsis false andallow_insecure_httpis not true, or ifuse_tlsis true with an emptytls_ca_cert_pathandallow_insecure_tlsnot true. Setuse_tls = trueplustls_ca_cert_path; use the opt-out flags only for local testing. - Server —
rg_floating_serverexits at startup if neithertls_cert/tls_keynorallow_insecure_http: trueis present in the YAML. The shippedfloating_server_config.yamlsetsallow_insecure_http: trueso local evaluation works out of the box; tell the user to replace it with a real cert before deployment.
Note allow_insecure_tls and allow_insecure_http are different axes —
the first relaxes certificate verification once TLS is on, the second
permits no encryption at all. Setting the first does not enable the
second. Both fail closed because the checkout response carries the
session secret that HMACs every later request.
8. "Diagnose License" menu item (ask first -- opt-in)
This section is opt-in. Do not add it unless the user asks for it, or you asked and they said yes. Sections 3-6 are the required recipe; this is a support feature on top.
Ask the user:
"Do you want a Help → Diagnose License menu item? It collects a full licensing status report on the end user's machine -- fingerprint, licence verdicts, clock, network, cache state -- and shows it in a dialog, so your support desk can be told what is actually wrong instead of guessing. Optionally it also writes one encrypted file the end user can e-mail you."
If they have no GUI (a CLI tool, a service, a library), say so and skip this section. Do not invent a menu.
8.1 What to add
Two functions, both over <rockyguard/diagnostics.h>:
#include <rockyguard/diagnostics.h>
namespace rgd = rockyguard::diagnostics;
// public_key_pem takes PEM CONTENT, not a path -- your end user has no
// key file and must not need one.
static rgd::Options diagnostic_options() {
rgd::Options o;
o.license_path = "license.json"; // whatever Section 5 uses
o.public_key_pem = ROCKYGUARD_PUBLIC_KEY;
o.tool_version = "<AppName> <version>";
return o;
}
First, make the key reachable. Section 4 puts
ROCKYGUARD_PUBLIC_KEY in an anonymous namespace in the file containing
main(), which gives it internal linkage -- it is invisible from any other
translation unit. A diagnostics dialog almost always lives in a different
.cpp (a UI file), so that constant will not compile there.
Do not paste a second copy of the key. Two copies drift, and the one you did not update becomes a licence that verifies in one code path and fails in the other. Instead move the declaration into a small header both files include:
// rockyguard_key.h -- new file, next to the file that had main()
#pragma once
inline constexpr const char* ROCKYGUARD_PUBLIC_KEY =
R"PEM(-----BEGIN PUBLIC KEY-----
... the user's public.pem content, moved verbatim from Section 4 ...
-----END PUBLIC KEY-----
)PEM";
inline constexpr gives it external linkage with no ODR problem, so both
translation units share one definition. Delete the anonymous-namespace
copy from Section 4's file and #include "rockyguard_key.h" in both.
The menu item -- runs off the UI thread, because collection performs network I/O and can take seconds:
// Kick it off when the menu item is clicked. Returns immediately.
auto job = std::make_shared<rgd::AsyncHealthCheck>(diagnostic_options());
// Then, on a timer / idle callback in your UI loop:
if (job->ready()) {
const rgd::HealthReport h = job->take();
show_dialog(h); // see 8.2
} else {
status_label.set_text(job->progress()); // safe from any thread
}
AsyncHealthCheck deliberately calls nothing back. You poll it. That
is why there is no callback to marshal into the UI thread -- which is the
single easiest thing to get wrong here, and it crashes rather than
glitching when you do.
8.2 Rendering the dialog
HealthReport is data, not text. Render it with the user's own widgets:
void show_dialog(const rgd::HealthReport& h) {
set_title(h.headline); // one sentence
set_colour(h.overall); // Info / Warning / Error
for (const rgd::HealthItem& item : h.items) {
add_row(icon_for(item.severity), // your icon set
item.label, // "Licence verification"
item.value, // "HWID_MISMATCH (HardwareMismatch)"
item.detail); // what to do about it, may be empty
}
}
Do not call summarize() for a GUI. It returns fixed-width terminal
text; in a proportional font the columns come out ragged. summarize()
is for consoles and log files.
8.3 Acting on findings
Branch on item.code. It is the same stable error-code contract as
Section 5. The wording of value and detail is not stable.
for (const rgd::HealthItem& item : h.items) {
if (item.code == "HWID_MISMATCH") offer_reactivation();
else if (item.code == "LICENSE_EXPIRED" ||
item.code == "GRACE_PERIOD_EXPIRED") open_renewal_page();
}
One finding is worth surfacing prominently because nothing else reports
it: a "Hardware drift" item with severity Warning. It means the
licence still verifies, but a fingerprint component has changed -- so the
machine is one hardware change away from failing, with no error anywhere
to warn the user. If the user wants a proactive "your licence may stop
working soon" prompt, this is the hook.
Do not wire diagnose() into this dialog. There is a root-cause
engine in the same header (rgd::diagnose(), API reference 11.8) and it
is tempting here, because it turns LICENSE_EXPIRED into "your clock is
three days fast, do not buy a renewal". It is written for the SUPPORT
DESK, not for the end user's screen: some causes name the vendor's own
issuance pipeline, and the anti-rollback remedies double as a tamper
recipe for whoever caused the problem. Causes carry a support_only flag
for exactly this reason, but nothing filters on it automatically, and the
rg_inspect tool RockyGuard ships deliberately does not diagnose either.
Its home is the vendor's side, on the report that comes back: either
rg_report_read --diagnose, or diagnose() inside the vendor's own
support tooling. If the user asks for root causes in the end-user dialog
anyway, that is their call to make -- say what it discloses, filter
support_only, and let them decide.
8.4 Offering "send to support" (optional, ask)
If the user wants the end user to be able to mail them the report, ask for
an RSA public key and seal h.report_json:
rgd::SealRequest req;
req.recipient_public_key_pem = SUPPORT_RECIPIENT_PUBLIC_KEY; // RSA, theirs
req.tool_version = "<AppName> <version>";
const rgd::SealOutcome sealed = rgd::seal(h.report_json, req);
if (sealed.ok) write_file("diagnostic.rgdr", sealed.container);
Tell the user plainly:
- This is a second, separate keypair from their licence-signing key,
and it must be RSA: an Ed25519 signing key has no encryption
operation and will be rejected. Generate it with
tools/license_keygen --algo rsa. - The private half stays on their support desk and opens the reports.
They read them with
rg_report_read(source inexamples/inspector/). Never shiprg_report_reador that private key to an end user. - If they do not want this, skip it. The dialog alone is useful; often the user reads the verdict aloud on the phone and that ends the ticket.
8.5 Do NOT put this at startup
Health collection is not a startup gate. It costs roughly 100 ms with
probe_network = false and several hundred with network on -- worst case
far more, because DNS resolution cannot be bounded. It also is not
side-effect free: verifying creates the anti-rollback witness files if
it finds none.
Keep the Section 5 LicenseVerifier check as the startup path. That is
what it is for and it is much cheaper. If the user specifically wants
richer context at launch, use health_check() with
o.probe_network = false and do it once, off the critical path -- never
in a loop, and never on every window open.
9. Hardening the integration against tampering (optional extras)
Sections 3–6 make licensing work; §6.5 is where the enforcement lives and is required wherever a feature has a bindable asset. This section is the optional extras on top: symbol hygiene, anti-substitution for the embedded key, avoiding a central verdict choke point, and folding the integrity check into the key. Do §6.5 first; reach here only if the threat model justifies more.
State the ceiling first, and do not oversell it. Everything here raises
attacker cost; none of it is unbreakable. A license verified offline on
hardware the attacker controls can always, eventually, be patched out. The one
primitive that genuinely resists patching is RockyGuard's own integrity
self-check (INTEGRITY_HASH_MISMATCH = patched on-disk binary,
INTEGRITY_RUNTIME_MODIFIED = runtime hook / attached debugger), which runs on
every check_node_locked(). The techniques below exist to make that self-check
hard to locate and bypass, and to remove the cheap wins (a greppable key, a
single flippable branch). Tell the user this plainly.
9.1 Measure the binary first — do not guess
The right hardening depends on what is actually exposed, and that differs by target type. Inspect the real artifact before changing anything:
nm -D --defined-only <artifact> | grep -ci rockyguard # DYNAMIC export table (.dynsym)
nm --defined-only <artifact> | grep -ci rockyguard # STATIC symbol table (.symtab)
strings -a <artifact> | grep -c 'BEGIN PUBLIC KEY' # greppable key blob?
file <artifact> | grep -o 'not stripped\|stripped' # symbols still present?
.dynsymis the export table — what-fvisibilityandvisibility("default")govern. It matters for a shared library; for an executable it usually holds only imports and a few runtime symbols..symtabis what plainnmreads. Stripping removes it; visibility flags do not touch it.
9.2 Symbol exposure — the actions differ by target type
Statically-linked executable (the app links librockyguard.a):
- RockyGuard's functions are internal to the executable, so they are already
absent from
.dynsym(nm -Dreturns 0).-fvisibility=hiddentherefore changes little on its own here — do not expect it to "hidecheck_license", because there is no export entry to hide. - What leaks the names to
nmis.symtab. Stripping is the high-value action —-Wl,-sat link (orstrip, orQMAKE/CMake install-strip). It also cannot change the prebuilt.a's baked-in symbol attributes, which is another reason visibility flags do nothing to those symbols. - Keep
-fvisibility=hidden -fvisibility-inlines-hiddenanyway as hygiene: it shrinks the app's own tiny surface and pays off the day the project links the shared RockyGuard lib or builds plugins. - Ensure release has no
-g(or strip removes it). Keep debuggability with a detached debug file:objcopy --only-keep-debug+--add-gnu-debuglink.
Shared library / plugin (you ship a .so / .dll, or wrap RockyGuard in
one):
- Here the export table is the ABI surface, and
-fvisibility=hiddendoes real work. Default to hidden, then export only the handful the host genuinely calls with__attribute__((visibility("default"))). The license functions (check_*, key accessors, the verifier glue) must not be exported. - ELF: for precise control add a linker version script that whitelists exports
and makes everything else local:
# exports.map { global: my_plugin_entry; my_plugin_shutdown; local: *; };-Wl,--version-script=exports.map. Verify withnm -D. - Windows DLL: symbols are hidden by default and exported opt-in. The mistake
to avoid is the reverse of ELF — do not put
__declspec(dllexport)(or a.defentry) on any license symbol. Ship without the.pdb. - Strip still applies. Note that if RockyGuard is consumed as its shared
librockyguard.so, the vendor already export-controls its symbols; your job is your own wrapper's exports plus shippinglibrockyguard.sigbeside the.sofor the integrity check.
Build-system snippets:
- CMake:
set_target_properties(t PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON); strip release viatarget_link_options(t PRIVATE $<$<CONFIG:Release>:-s>)or install-strip. - qmake: in a
CONFIG(release, debug|release)block:QMAKE_CXXFLAGS += -fvisibility=hidden -fvisibility-inlines-hiddenandQMAKE_LFLAGS += -Wl,-s.
9.3 Do not embed the public key as a greppable blob
Applies to both target types. The threat is substitution, not
extraction: a public key is not secret, but a contiguous PEM in .rodata is
a clean overwrite target — replace it with the attacker's key and the app
verifies licenses they signed.
Use examples/embed_public_key.py to emit the key as two independently
XOR-masked byte tables plus a reconstruct-and-cross-check accessor
(public_key_pem() returns "" if the tables disagree). Verify against the
embedded reconstruction (..._intact()) and fail closed on empty. After
this, strings | grep 'BEGIN PUBLIC KEY' returns nothing and a substitution
must patch both tables consistently. Rotate the key by regenerating the header;
its output is deterministic, so an unchanged key yields an empty diff.
This is masking, not encryption (the mask is derivable from the binary). It becomes a real cost only when the reconstruct/compare TU is compiled under an obfuscator so it cannot be trivially located and NOP'd.
9.4 Kill the central choke point (don't funnel gates through one verdict)
A single startup if (!result) refuse; is a single branch to flip. The
instinct is "check in more places" — but the choke point is the verdict,
not the number of call sites. Eight gates that all call one predicate —
check_node_locked(), or worse a bool is_licensed() wrapper you write
around it — are still one patch: flip that function's return once and all
eight gates see "valid." Nor does copying the same if into eight inlined
sites help: the compiler's common-subexpression elimination and the linker's
identical-code-folding (/OPT:ICF, --icf=all) merge identical checks back
into one blob, and even when they survive, byte-identical checks are exactly
what a pattern-based cracker search-and-replaces in one pass.
So, in order of how much they actually buy you:
Do NOT wrap the check in a single shared boolean predicate of your own. A
bool is_licensed()that everything calls adds a customer-side choke point on top of the library's. If you must produce a verdict, produce it at the site, not through a shared function.Strongest — give each gate its own data dependency, not a shared bool. This is Section 6.5 applied per-gate: each gated feature opens its own required asset through
LicenseAssetunder its own vendor-wrapped feature key (--wrap-feature-key; §6.5.5 — not subkeys you derive from the product key), woven into that feature's real work. Eight features → eight independent decryptions, no shared verdict anywhere. Patch any one site and only that feature breaks — into garbage, not a bypass — and the others never consulted a verdict to patch. Crucially, routing many gates through the singleunwrap_data_key()function is fine even though it is one function: you cannot patch a function to emit a 32-byte key you do not possess (a bool has two values; a key has 2²⁵⁶). That is the whole difference between returning a verdict and returning key material.For paths that genuinely must yield a verdict (startup, greying out a menu item), still verify at several independent sites — each calling
check_node_locked()so each re-runs the integrity self-check — and make the sites do distinct real work so CSE/ICF cannot fold them together (avolatilesprinkle is not enough). Treat this code-diversity layer as secondary to (2): it raises the cost against scripted/pattern crackers but a determined reverser defeats it, and it cannot touch the library's own single verdict function — that is whatROCKYGUARD_OBFUSCATEand the integrity self-check defend.- Feature entry points — re-verify before performing a gated action, throttled (e.g. once per 30 s) so per-click cost is bounded.
- A periodic background timer — re-verify every few minutes so a session whose startup check was patched out does not run indefinitely.
Which real computations can carry the dependency in (2) is app-specific — ask the user. Do not invent it: ask which of the gated features consume a lookup table, resource blob, model, ruleset, or other data you can encrypt under the license key, and wire the dependency into those. Where a feature has no natural data to bind, fall back to (3) for that one.
Choose the failure response by host type (this mirrors the Section 5 table):
- Executable: feature-site failure should refuse the action (do not
exit()mid-click — you would destroy unsaved work); the periodic timer is the site that may tear the session down. - Plugin / shared library: never
exit()/abort()at any site — that kills the host. Return failure to the host and disable your features.
9.5 Strongest: make the license load-bearing (consume the result, don't branch on it)
MOVED — this is now §6.5 and is part of the required recipe. As of this guide revision the load-bearing pattern ships as a tool (
tools/rg_bind_asset), a CMake helper (rockyguard_bind_asset()), theLicenseAssetclass (<rockyguard/license_asset.h>), a worked example (examples/loadbearing_asset_example.cpp), and a linter (tools/rg_bind_lint). Do it from §6.5, which supersedes the prose below. The text here is retained as the conceptual explanation and is referenced by §9.6.
Everything above raises the cost of patching the license decision. This step removes the decision as the thing worth patching. Instead of branching on the verify result — a choice made on the attacker's CPU, so a branch they can flip — derive a key from the validated license and use it to decrypt data or code the program genuinely needs (a required lookup table, a resource blob, a critical routine). Patch the check out and the key never appears, so the bytes stay encrypted and the app produces garbage rather than a working unlock. A branch can be flipped; a missing decryption key cannot be wished away.
As of v1.4 the library supports this directly, so you do not hand-roll the key derivation:
- Make one data key per product and keep it on your build machine:
<package>/tools/license_create --gen-data-key # 64 hex chars = 32 bytes - Encrypt the asset your program needs, under that key, at build time.
examples/loadbearing_asset_example.cpp --encrypt <dk_hex> <in> <out.enc>does it with AES-256-GCM in the exact format the runtime expects. Embed the ciphertext in your binary (a byte array in a header); the plaintext never ships. - Wrap the key into every end-user license at mint time:
This seals the key into the license, bound to that license's signature.<package>/tools/license_create ... --wrap-data-key <dk_hex> --output license.json - At runtime, recover the key and decrypt — without branching on it:
// Do NOT write `if (!key) refuse;` -- that reintroduces the flippable // branch. Feed the key straight into your decryption and USE the result; // if the license is absent/foreign/tampered, unwrap_data_key() returns // nullopt, the decrypt fails, and the program has no usable data. std::vector<uint8_t> key; if (auto k = verifier.unwrap_data_key()) key = *k; // empty on any failure auto table = my_aes_gcm_open(key, embedded_asset_blob); // your decrypt use_the_table(table); // real work; garbage if empty
unwrap_data_key() returns the exact 32 bytes you passed to
--wrap-data-key, and only when a genuine signature is present: the
wrap key is HKDF-SHA256(license signature), the one field an attacker
cannot forge for a payload of their choosing. A tampered signature, a
corrupted blob, a foreign license, or an absent one all yield nullopt
(GCM authentication failure), never a forged key.
State the ceiling to the user, do not oversell it. This defeats the
free branch-flip / keygen-less crack and forces an attacker to possess a
genuine license and reverse your derive/decrypt path — so compile that
translation unit under ROCKYGUARD_OBFUSCATE. It does not, for an
offline scheme, force signature forgery: one leaked genuine license
carries the signature that yields the key. It is signature-bound, not
machine-bound — keep check_node_locked() for hardware binding; the key
derivation deliberately does not fold in the live fingerprint, because
that would break decryption on the ordinary hardware drift the 2-of-4
match threshold is designed to tolerate. Pick a small, mandatory asset
so wrong-key decryption is visibly broken, not a quietly disabled feature.
Full worked example end to end: examples/loadbearing_asset_example.cpp.
9.6 Folding a code hash into the key — evaluated, and NOT recommended
An earlier revision of this guide recommended, as an advanced step, folding a
hash of the binary's own code into the key derivation — K = KDF(DK, hash(R)),
so a patched .text derives the wrong key and the asset decrypts to garbage
with no integrity branch to NOP. We have since red-teamed it against the same
attacker model as the rest of this section, and we now advise against it.
It does not reliably raise the bar, and it can lower it. This subsection keeps
the reasoning on record in place of the construction.
Why it looked good. An isolated integrity check (if (hash != expected) refuse;) is just another patchable branch. Feeding the hash into the key
instead of into a branch removes that branch — the same "no verdict to flip"
property that makes 9.5 load-bearing.
Why it is not load-bearing. 9.5 works because the value it folds in — the
signature — is a secret: an attacker cannot produce the key without
possessing a genuinely signed license. hash(R) has no such property. The
expected code hash is not secret — it is a deterministic function of the
legitimate binary the attacker already holds. So the attacker never has to
touch the crypto. They keep a pristine copy of R (or the known-good
constant) and make the running image hash that instead of itself: a one-line
redirect of the pointer or length handed to the hasher.
And this is what makes it worse than doing nothing: the self-hash routine is
a more distinctive, more greppable target than the branch it replaced. A
lone if (status != Valid) is one of thousands of comparisons; a routine that
reads its own code section and feeds the result into a KDF is a recognizable
shape with recognizable call sites. Adding it can hand the reverser an
easier landmark than the jne you were trying to eliminate. A mechanism
whose only net effect is to move the patch target somewhere more obvious has
negative value.
What to do instead — the steps that fold in a real secret or genuinely raise cost:
- 9.5 (signature-bound key) is the load-bearing step. Keep it: the signature is the secret the attacker must possess, and no code patch substitutes for it.
ROCKYGUARD_OBFUSCATEraises the cost of locating any target in the library's own code, a would-be self-hash included. It is friction, not a secret — the right tool for "make the code hard to navigate," and the wrong one to mistake for a lock.- The shipped
.sigand the runtime integrity check are the detection layer: they tell an honestly-deployed process that its binary was altered. They are not a substitute for the load-bearing key, and — being trust-on-first-use in a static build — they do not catch a binary that was patched on disk before first run. Treat them as detection and hygiene, not as the thing that stops this attack.
The permanent ceiling is the one stated in 9.5: an offline check on hardware the attacker controls raises cost, it does not prevent. Do not present a code-hash-in-the-key layer, to yourself or to a reviewer, as the step that closes that gap — it does not.
9.7 Worked example (static Qt executable)
For a static-linked Qt/qmake app the effective set was: strip release
(-Wl,-s) + -fvisibility=hidden (hygiene); masked key via
embed_public_key.py with a fail-closed intact check at startup; and three
check sites — startup, throttled feature-gate re-checks, and a 5-minute
periodic re-verify that quits on hard failure. Result, measured: .dynsym
rockyguard symbols 0 (already), .symtab 454 → 0 after strip,
grep 'BEGIN PUBLIC KEY' 1 → 0, tamper of one masked byte → key fails to
reconstruct → app refuses to start.
10. Verification
After making the changes above:
Build the project with the user's normal build command. The build should succeed with no new warnings or errors. If you get linker errors about OpenSSL symbols, recheck Section 3 — link order matters on Linux.
Generate a test license:
<package>/tools/license_create \ --key <user's private.pem> \ --vendor-license <user's vendor_license.json> \ --id TEST-001 --licensee "Dev Test" \ --product "<their product>" \ --type node_locked --threshold 2 --fingerprint \ --expires permanent --output license.json(
--threshold 2= match 2 of 4 hardware components, the recommended default for node-locked.--threshold 0= ignore hardware, useful only for in-CI testing, never ship.)--fingerprintis REQUIRED here and you must not drop it. It binds the test licence to the machine you are running on, which is what makes step 3 pass. Anode_lockedlicence with--threshold 2and NO fingerprint flag is rejected at verification time with statusHardwareMismatchand codeHWID_MISSING_IN_LICENSE-- deliberately, as a safety net against issuance bugs (Customer_API_Reference §6.4 and §4.5). Match on the code, not on the sentence: v1.4 reworded this one, so a check written against the old text ("Node-locked license has no hardware fingerprint") no longer fires. Note thatlicense_createstill exits 0 and prints a success line when you omit it, so the mint gives you no warning; the failure appears only at step 3, and it looks exactly like a broken integration. Earlier revisions of this guide omitted the flag and told you to expect success, which could not happen. If you are minting for a real end user, use--fingerprint-value "<their hash>"instead;--fingerprintmeans THIS machine and must never ship.Run the binary with
license.jsonin the working directory. Expected: program runs normally. Then rename license.json and run again. Expected: program prints the license error and exits 1 (with the logging from Section 5 in place, the failing run also emits oneto_json()line whose"code"isLICENSE_FILE_UNREADABLE-- that line is the fastest way to confirm the error path is wired up). If both behaviours match, the integration is verified. If instead the first run fails with codeHWID_MISSING_IN_LICENSE, the licence in step 2 was minted without--fingerprint. Re-mint it; nothing is wrong with the integration.Print the license info on first run so the user can sanity-check:
// license() returns a snapshot by value, copied under the verifier's // internal lock, so it stays valid even if another thread reloads. const rockyguard::License lic = verifier.license(); std::cerr << "Licensed to: " << lic.licensee << " (license ID " << lic.license_id << ")\n";Wrap in
#ifdef DEBUGif the user prefers.The negative test — MANDATORY for every feature you bound in §6.5. A binding that only passes the happy path may be fake. Mint a second license from different keys (or reuse a foreign license), run the app with it, and exercise each bound feature. Expected: the feature is visibly broken / produces garbage / is inert — NOT a clean "license invalid" while the rest of the app runs. If a bound feature still works under the wrong key, the plaintext is reachable without the key: the binding is fake and must be fixed before shipping. The shipped example demonstrates both runs:
# genuine license -> asset opens, feature works loadbearing_asset_example --pubkey public.pem --license good.json \ --feature export_pdf --asset export_tables.enc # foreign/absent license -> feature INERT (exit 3), asset never decrypts loadbearing_asset_example --pubkey public.pem --license foreign.json \ --feature export_pdf --asset export_tables.encRun the binding linter and treat ERRORs as blocking:
python <package>/tools/rg_bind_lint <their-source-dir>It catches the ways this pattern silently regresses to a flippable boolean: branching on
unwrap_data_key()/unwrap_feature_key(), anif (asset.empty()) refuse;guard, a sharedbool is_licensed()predicate, a key that is recovered but never opens an asset, message-text branching, and a missing negative test. "Done" is: the build is clean, step 3 passes, step 5's negative test shows a broken feature, andrg_bind_lintreports zero errors — not merely "it compiles."
10.5 Hardware fingerprint, and the 30-day Premium trial
Run this once the integration builds. It is the last step of the recipe,
and for a first-time user it is the step that makes the integration
testable at all: without a vendor licence they cannot mint a
license.json, and without a fingerprint we cannot issue one.
10.5.1 Read the fingerprint
# Linux / macOS
FP=$(<package>/tools/rg_fingerprint)
echo "$FP"
# Windows PowerShell
$FP = & <package>\tools\rg_fingerprint.exe
$FP
The fingerprint — and only the fingerprint — goes to stdout, as a
single line. The banner, any error text and the -v component dump go to
stderr, precisely so $(...) and > capture the bare value. Do not
scrape it out of a combined transcript; you will capture the banner too.
Add -v if the user wants to see which hardware components feed the
hash. Do not pass -v on the run you capture from — capture and display
are separate concerns, and mixing them is how a banner ends up pasted
into a licence request.
10.5.2 Show it to the user and explain what it is
Print the value and say, in substance:
- It is a hash of stable hardware identifiers of this machine — their build machine. Not their customers' machines.
- It binds the vendor licence, which authorises them to mint end-user licences. It is not the fingerprint that ends up in any licence they ship.
- Rocky Software needs it to issue the licence, and it will travel with the request.
- If they later change build machines, the licence needs reissuing. That is normal; they email and ask.
Do not describe it as anonymous. It identifies a machine — that is its entire job — and a user deciding whether to send it is entitled to an accurate description rather than a reassuring one.
10.5.3 Ask — then do exactly what they said
Ask one direct question: may you send a 30-day Premium trial request to Rocky Software on their behalf? Then follow the branch. If they ask what the trial covers, the terms are in Section 13.
If the user says no, or does not answer clearly: print the fingerprint and the URL https://rockyguard.dev/contact?intent=trial and stop. Say the form pre-selects the right reason and has a field for the fingerprint. This is a perfectly good outcome — say so, rather than re-asking.
If the user does not want you to send it but wants the form filled: same URL, and hand them the exact values to paste. Do not navigate a browser to submit it for them; that is the same action wearing different clothes.
If the user says yes: collect name, email and company (company optional) — ask, do not guess from git config or the environment, and do not reuse an address you happened to see in the repository. Then submit:
curl -fsS -X POST https://rockyguard.dev/api/contact \
--data-urlencode "reason=trial" \
--data-urlencode "name=<their name>" \
--data-urlencode "company=<their company, or omit>" \
--data-urlencode "email=<their email>" \
--data-urlencode "fingerprint=$FP" \
--data-urlencode "message=<one line: what they are integrating into>"
Notes on that call, so you can read a failure instead of retrying blindly:
reason=trialis the slug for the 30-day Premium trial. The other valid slugs are listed at https://rockyguard.dev/contact; usetrialunless the user asked for something else by name. (free-dev, the slug of the retired free Developer tier, is still accepted as an alias fortrial; do not send it.)- Send no
websitefield. It is a honeypot: any value at all makes the endpoint silently discard the submission and still answer200. name,emailandmessageare required.400 name length/400 message lengthmean an empty field, not a long one.- The endpoint checks that the email domain resolves in DNS.
400 email domain does not existmeans a typo in the address — show the user, do not "fix" it yourself. 503means the contact endpoint is misconfigured on our side. Fall back to the form URL and tell the user it is our fault, not theirs.
On success, tell the user the request went to Rocky Software and that the vendor licence arrives by email. Do not promise a turnaround time.
10.5.4 What happens when the licence arrives
Leave the user with the three steps they will need, so the gap between today and the email is a wait rather than a second integration session:
- Mint a test licence with the
license_createinvocation in Section 10, step 2 — copy it verbatim, including--fingerprint. Dropping that one flag produces a licence that mints successfully, exits 0, and then fails verification withHWID_MISSING_IN_LICENSE, which reads exactly like a broken integration. Section 10 explains why. - Check it outside the application, so a failure at step 3 is
unambiguously the integration and not the licence:
<package>/tools/license_verify \ --key public.pem --license license.json --check-hardware - Run the application against it, then run the rest of Section 10 —
including the negative test in step 5 and
rg_bind_lintin step 6. The integration is not finished until those pass.
One trap worth stating in the same breath, because it presents as a
broken integration: do not test with a short-dated licence. A licence
expiring within 7 days is treated as an evaluation mint and carries no
wrapped key — license_create refuses to put one there. Every check
still passes, but unwrap_data_key() and LicenseAsset::open() return
nothing, with no error code to explain it, because nothing failed. Use an
expiry more than 7 days out for any development or test licence. (The
same rule is the reason short-dated licences do not spend the trial's
50-licence budget — see Section 13.)
11. Hard-no rules
You MUST NOT do any of the following:
- Do not download the package without asking first (Section 0.1, step 3). State the product and version, the URL, the size in bytes, the SHA-256 you will verify against, and where on disk it is going — then wait for an explicit yes. Fetching and running binaries from the network is the user's call, not yours.
- Do not continue past a SHA-256 mismatch (Section 0.1, step 5). Delete the file, say the digest did not match, stop. Not "retry", not "extract it to check", not "proceed with a note in the summary."
- Do not hard-code a download URL. Resolve it from https://rockyguard.dev/downloads/latest.json. The zip filenames carry the version and rotate on every release.
- Do not send a licence request without an explicit yes (Section 10.5.3). The hardware fingerprint identifies the user's machine and the submission is made in their name and from their email address. No pre-filling-and-submitting, no "I'll send it and you can retract it", and no driving a browser to the form on their behalf. If they have not said yes in this conversation, print the fingerprint and the URL and leave it with them.
- Do not invent, fabricate or self-sign a vendor licence, and do not
wire the build to the
sample_demo_*keys from the bundle'sdemo/directory. Those are ours, published, and identical for every downloader. A first-time user simply has no vendor licence yet — that is expected, and Section 10.5 is how it gets resolved. - Do not commit the user's
private.pemto git. Add it to.gitignoreif it lives anywhere inside the repo. - Do not ship
private.pemin the application binary, package, or installer. It only ever exists on the user's build machine. - Do not catch and ignore license verification errors. The program must exit on failure.
- Do not branch on
result.messagetext. Nofind(), no regex, no equality test against a message string, and no parsing it apart. Branch onresult.statusandresult.code, which are stable contracts; the wording is not one and does change between releases (Section 5, and Customer_API_Reference §4.5). - Do not embed
license.jsonin the binary. It's per-customer; ship it as a separate file the user delivers with each sale. - Do not generate or fake a public key. Always read the user's
actual
public.pemcontent; ask if you can't find it. - Do not skip Section 6 (feature gating). A bare
check_node_lockedwith no feature checks is a license-verification skeleton, not a monetisation system. The user is paying for the feature-gating layer. - Do not silently switch the user's build system (e.g. CMake → Bazel) to make integration easier. Match what they have.
- Do not call
health_check()orcollect()on a UI thread. UseAsyncHealthCheck(Section 8.1). Collection does network I/O and can take seconds; blocking the UI thread freezes the application. - Do not use the diagnostic collection as the startup licence check. Section 5 is the startup path. Section 8.5 explains why.
- Do not ship
rg_report_read, or the report private key, to an end user. That binary and that key belong on the user's support desk.rg_inspect/ the in-app dialog is what goes out. - Do not reuse the licence-signing key as the report recipient key. It is Ed25519 in the default setup and cannot encrypt at all. Mint a separate RSA pair (Section 8.4).
- Do not branch on
HealthItem::valueor::detail. Same rule asresult.message: branch onHealthItem::code. - Do not deliver a paid feature whose only enforcement is a branch on a verdict when that feature loads a bindable asset. Bind it (§6.5). If it has no bindable asset, say so explicitly (§6.5.7) — do not leave it looking hardened.
- Do not write
if (!unwrap_data_key()) refuse;(orif (key.empty()) refuse;, orif (table.empty()) refuse;afterLicenseAsset::open()). Each reintroduces the exact flippable branch §6.5 exists to remove. UseLicenseAsset, which has no optional to test, and consumeopen()'s result. - Do not hand-roll the asset encryption. Seal with
tools/rg_bind_asset/rockyguard_bind_asset()and open withLicenseAsset; a bespoke AES-GCM with the wrong nonce handling or a leaked plaintext buffer is worse than no binding. - Do not derive per-feature keys from the product key. Generate a
separate key per bound feature and wrap it with
--wrap-feature-key(§6.5.5).HKDF(product_key, feature)gives every licensee every tier. - Do not commit the plaintext asset or the data key. The binder reads the
key from the environment and emits ciphertext only; keep the plaintext and
the
--gen-data-keyvalue out of the repo (same rule asprivate.pem). - Do not declare the integration done on a clean build alone. It is done
only when the §10 negative test shows a broken feature under a wrong
license and
rg_bind_lintreports zero errors.
12. When to stop and ask the user
Stop and ask, do not guess, when:
- https://rockyguard.dev/downloads/latest.json is unreachable, or does not list the platform slug you need (Section 0.1, step 2). Do not guess a filename and do not scrape the download page for a link.
- The host is arm64 Linux (unsupported) or Apple Silicon (the x86_64 bundle runs under Rosetta, but their application must then be built x86_64 too). Section 0.1, step 1.
- The user's glibc is older than 2.34 on Linux.
- You would need to clear the macOS quarantine attribute — say what the command does and why, and let the user decide (Section 0.1, step 6).
- You can't unambiguously locate
int main(...). - The project has multiple binaries and it's unclear which to protect.
- Their build system is not vanilla CMake.
- They already have a different license-checking system in place (Sentinel, FlexLM, custom). Ask whether to replace it or run side-by-side.
- Their
public.pemis not in the workspace and they haven't pasted its content. - They haven't told you which features should be gated.
- You're about to modify a file you don't recognise.
- The user asked for a "Diagnose License" menu (Section 8) but the application has no GUI, or you cannot tell where its menus are built.
- The user wants the "send to support" flow (Section 8.4) but has not provided an RSA recipient public key. Do not substitute their licence key; it cannot encrypt.
13. References
API surface:
docs/Customer_API_Reference.pdf(every public class, struct, enum, and CLI flag; §4.4-4.6 areLicenseResult, theLicenseErrorCodecatalog with a remedy per code, and the<rockyguard/license_error.h>lookups)Full integration manual:
docs/Customer_Documentation.pdf(deeper coverage of fingerprinting, anti-tampering, floating server, troubleshooting)Working example:
examples/node_locked_example.cpp(this is what Section 5's pattern is distilled from)Floating example:
examples/rg_floating_client.cppHealth-check example:
examples/health_check_example.cpp(Section 8; shows both the async menu pattern and the cheap startup variant)Hardening (Section 9):
examples/embed_public_key.py(the masked-key generator, Section 9.3), plusexamples/hidden_key_example.cppand its generatedexamples/hidden_public_key.h(node-locked verification with a masked, cross-checked embedded key — the Section 9.3 anti-substitution pattern end to end). Regeneratehidden_public_key.hfrom your own key; the shipped one is a placeholder.
Load-bearing binding (Section 6.5, required where a feature has a bindable asset) — this is a different threat from 9.3: 9.3 stops the embedded key being substituted; 6.5 makes a patched-out check yield garbage. The pieces:
<rockyguard/license_asset.h>—LicenseAsset, the runtime side: the class with no verdict in it (Customer_API_Reference §12).examples/loadbearing_asset_example.cpp— end to end:--encryptat build time and verify →LicenseAsset→ open → use at runtime, including the negative (wrong-license) behaviour (exit 3).tools/rg_bind_asset(sourcetools/rg_bind_asset.cpp) — build-time binder: seals an asset under a key into the library's blob layout and emits a ciphertext header.examples/bound_asset.h— the header-only sealer behind the binder and the example's--encrypt. Build-time only; the application does not need it.cmake/RockyGuardBindAsset.cmake—rockyguard_bind_asset(), wires the encrypt step into the build (plaintext never enters the tree).tools/rg_bind_lint— catches regressions to a flippable boolean; run it in Section 10 and treat ERRORs as blocking.- Mint side:
license_create --gen-data-key,--wrap-data-key(product key, every license) and--wrap-feature-key <name>=<hex>(per feature, entitled licenses only). examples/FEATURES.template.md— copy to the repo root asFEATURES.md; the shared register of flag strings, per-feature enforcement (bound vs boolean-only), bindings, and the mint command (Section 6).
Diagnostics API:
docs/Customer_API_Reference.pdf§11 -- §11.6 is the structuredHealthReport, §11.7 isAsyncHealthCheck, §11.8 isdiagnose()(support-desk side only -- see the warning in Section 8.3)Root-cause catalog:
docs/Customer_Documentation.pdf§10.1.2, the list of cause ids and what each assertsBuilding the standalone Inspector tools:
examples/inspector/README.txt(only if the user wants a separate executable to hand out rather than an in-app dialog)Download manifest (Section 0.1): https://rockyguard.dev/downloads/latest.json — the stable, machine- readable index of the current release: per platform, the zip URL, its SHA-256, its size in bytes, and the directory it extracts to. Resolve every download from here. Digests are also published as https://rockyguard.dev/downloads/SHA256SUMS.txt.
Agent entry point: https://rockyguard.dev/llms.txt — the short index an agent lands on when given only the domain. Names this guide, the manifest, and the two consent rules above.
This guide, canonical: https://rockyguard.dev/AI_INTEGRATION_GUIDE.md (raw markdown) and https://rockyguard.dev/docs/ai-integration (rendered, for humans). Refreshed every release; prefer it over a copy you were handed.
Bundle contents: the download is the full SDK, not a toy. It carries
include/, bothlib/static/andlib/shared/,deps/,docs/,cmake/RockyGuardBindAsset.cmake, and atools/directory withlicense_keygen,license_create,license_verify,rg_fingerprint,rg_bind_asset,rg_bind_lintandrg_inspect— everything Sections 2 through 10 call for. It also adds ademo/directory holding a self-contained 30-line program that exercises Section 5; that directory is the only thing in the bundle the word "demo" describes. The floating-licence tooling (Section 7) and the admin portal are the only parts held back for the full customer zip, which Rocky Software sends along with a trial or paid vendor licence.(Through v1.4.0 the artifact itself was named
...-demo.zip. It is now...-sdk.zip. If you are holding an older link or an older copy of this guide, re-resolve it from the manifest rather than reconstructing the filename.)30-day Premium trial (Section 10.5): https://rockyguard.dev/contact?intent=trial — the request form, with the reason pre-selected and a field for the
rg_fingerprintoutput. No credit card. The vendor licence arrives by email, bound to the build machine's fingerprint, together with the full customer zip. The trial is full Premium, floating licensing included, on one build machine, good for up to 50 end-user licences that expire more than 7 days out; licences expiring within 7 days (already-expired test licences included) are metered separately and do not count against the 50. It is for evaluation and pilots and does not grant the right to sell licences to paying end users — that needs Basic or Premium. Extension on request. When the user buys, the vendor licence is re-issued against the same fingerprint. Terms: https://rockyguard.dev/pricing.Basic or Premium (paid): https://rockyguard.dev/contact — direct the user here to buy, or to ask about anything the trial does not cover.