Skip to content

Activation and deactivation ids are UUIDs, not integers - #28

Merged
rameerez merged 7 commits into
mainfrom
fix/activation-id-is-a-string
Sep 5, 2026
Merged

rameerez merged 7 commits into
mainfrom
fix/activation-id-is-a-string

Conversation

@rameerez

@rameerez rameerez commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

activate() and deactivate() have been broken against production for every C++ consumer since the SDK was written. Reported independently by two customers: Dave Carkeet (Aug 24, with a debugger trace) and Michael Rieger / FreeD Printing GmbH (Sep 3), both seeing Failed to parse response: Activation response identity is inconsistent on an activation the dashboard shows as successful.

Root cause. The server issues UUID primary keys, so id and activation_id arrive as strings. parse_activation and parse_deactivation only read them when the JSON value was a number, the field stayed 0, and the identity/completeness checks in client.cpp rejected every real response. The API's own OpenAPI annotations declared these fields Integer (fixed in rameerez/license_seat#27), which is where the assumption came from. The tables were created with UUID keys on day one; there was never an integer to parse.

Three commits:

  1. fc0d088 Activation::id() becomes std::string; integers still accepted and normalised.
  2. 83f8afb The same fix for Deactivation::activation_id. This one was worse: the throw happened before the local cache was cleared, so the server released the seat while the client kept its license, offline token and machine file. Also fixes the Unreal integration, which parses both responses on its own, and two test files that still built an Activation from an integer (the branch did not compile with -Werror before this).
  3. d6d5859 Docs: signing_public_key is never fetched on the offline path (verify_machine_file() and the cold-start restore), so pin it; the README's Manual Storage example showed exactly the unsupported flow the second customer followed, and its pinning example showed a PEM/DER key that is_valid_ed25519_public_key silently rejects. The server returns the raw 32-byte key base64-encoded.

Verification.

  • ctest: 382/382 pass (main: the test target did not build).
  • Integration test against production with a throwaway license: on main, Activate license fails with the customers' exact error; on this branch, activate, validate-with-activation, deactivate, and both async paths pass with UUID ids.
  • Machine-file probe against production: fresh client + certificate only reproduces Public key required for machine file verification; fetch_signing_key(kid) + explicit key verifies; pinned signing_public_key in a fresh client verifies with the license payload present; a wrong fingerprint fails decryption_failed.

API change. Activation::id() and Deactivation::activation_id are std::string. Anything storing them as integers changes one line. Suggest tagging v0.7.0.

Not fixed here (filed separately): cold-start offline restore fails unless signing_public_key is pinned, because the fetched key is only cached in memory; MachineFile has no key_id field so the explicit-key path needs crypto::internal::extract_machine_file_key_id.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BTtmNBLK1ivBapaS3sXysu

rameerez and others added 7 commits August 25, 2026 10:09
parse_activation() only read `id` when the JSON value was a number, but the
API issues UUID primary keys, so the field always arrives as a string. The id
stayed 0, and client.cpp's `activation.id() <= 0` identity check then rejected
every successful activation with "Activation response identity is inconsistent".

activate() was therefore broken against production for every C++ consumer. The
tests missed it because they only ever fed parse_activation an integer id, which
is a shape the server does not send. Reported by a customer who read the source
and diagnosed it correctly himself.

Activation::id() is now a std::string, matching the Swift SDK, which already
types this field as String. Integer ids are still accepted and normalised so
older servers keep working.
fc0d088 fixed parse_activation but left three things behind:

1. tests/test_license.cpp and tests/scenario_test.cpp still built an Activation
   from an integer id and called std::to_string on id(). With -Werror the test
   target did not compile, so CI on the branch would have been red and the fix
   could never have merged as-is.

2. parse_deactivation had the identical bug: it read activation_id only when the
   JSON value was a number, the server sends the activation's UUID, so the field
   stayed 0 and client.cpp's  check rejected every successful
   deactivation with "Deactivation response is incomplete". Worse than activate:
   the throw happens before the local cache is cleared, so the seat was released
   on the server while the client kept its license, offline token and machine
   file. Deactivation::activation_id is now a std::string; integers are still
   accepted and normalised.

3. The Unreal integration parses both responses on its own and required a
   numeric id in both places. It now accepts a string or a legacy integer.

Tests updated to feed the UUID shape the server actually sends. 382/382 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTtmNBLK1ivBapaS3sXysu
…ned key is raw base64

The signing_public_key docblock promised the key would be fetched from the API on
first use. That is true for activate() and checkout_machine_file() and false for
verify_machine_file() and the offline restore path, which are the offline entry
points and deliberately never touch the network. A customer read the promise,
fetched his machine file with curl, called verify_machine_file() and got
"Public key required for machine file verification" (licenseseat-cpp#27).

The README's Manual Storage example showed that exact flow with no key and no
prior checkout, and the pinning example showed a PEM/DER-looking key that
is_valid_ed25519_public_key silently rejects. The server returns the raw 32-byte
key base64-encoded; the docs now say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTtmNBLK1ivBapaS3sXysu
…e project version to 0.6.0

fc0d088 was cut before the v0.6.1 tag, so merging it as-is would have set
CMakeLists VERSION back from 0.6.1 to 0.6.0. Activation::id() and
Deactivation::activation_id changing type is a public API change, so this
is 0.7.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTtmNBLK1ivBapaS3sXysu
@rameerez
rameerez merged commit 3571ba9 into main Sep 5, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant