Licensing: generation and verification
Cabinee includes functions for verifying the digital signature and integrity of a license using a standard algorithm (ECDSA-256) that is part of the Java Security API. These functions are used only to confirm license authenticity and are not intended to protect information that is subject to regulation. Cabinee software is not a cryptographic information protection facility, is not subject to mandatory certification, and does not require a license from regulatory authorities (FSB of the Russian Federation, KNB of the Republic of Kazakhstan) for the development or distribution of cryptographic protection tools.
1) Overview
The platform supports two modes:
- COMMUNITY — functional limitations (in the base distribution: 1 Workflow is allowed for the entire instance).
- ENTERPRISE — without the stated limitations. Enabled by a valid license.
A license is a JWS token (signed JSON) that is stored on the server and verified on every start and in online mode.
2) Cryptography and formats
- Signature algorithm:
ES256(ECDSA). - Private key (at the licensor):
PKCS#8 PEM.
This is a standard container for a private key (one key — one file), convenient for libraries and storage.
- Public key (on the server for verification):
X.509 SubjectPublicKeyInfo PEM.
This is a standard representation of the public key only, without a certificate. It is suitable for fast signature verification.
- Key identifier (
kid) is the public key file name without the extension, for example2025-08-e1.pem->kid = 2025-08-e1.
JWS structure
Header
{
"alg": "ES256",
"kid": "2025-08-e1",
"typ": "JWT"
}
Payload
{
"edition": "Enterprise",
"issuedTo": "nazym.kazizmurat@reunico.com",
"dueDate": "2025-08-20T15:30:59Z",
"licenseId": "UUID",
"licenseVersion": 1
}
The
dueDatefield is ISO-8601 UTC. After this date the license becomes invalid.
3) Placing public keys
Public keys are shipped with the application and placed in resources:
src/main/resources/licenses.keys/<kid>.pem
On start, the PublicKeyProvider component loads all *.pem files from:
classpath:/licenses/keys/*.pem
(our path is mapped to licenses.keys in resources).
4) Key generation (at the licensor)
# 1) Generate an EC key (secp256r1)
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
# 2) Convert to PKCS#8 (no password)
openssl pkcs8 -topk8 -nocrypt -in ec-private.pem -out ec-private-pkcs8.pem
# 3) Extract the public key
openssl ec -in ec-private.pem -pubout -out ec-public.pem
# 4) Save the contents of ec-public.pem to the resource:
# src/main/resources/licenses.keys/2025-08-e1.pem
5) Issuing a license (CLI)
The project includes the GenLicenseCli utility.
Run from the IDE — arguments:
--due 2025-08-20T15:30:59Z \
--to "nazym.kazizmurat@reunico.com " \
--kid 2025-08-e1 \
--priv /path/to/ec-private-pkcs8.pem \
--alg ES256
The output is a JWS string that is used to upload the license through the Admin UI.
6) Uploading and managing a license through the Admin UI
Uploading and managing a license in Cabinee is performed through the Admin UI.
6.1 Upload through the Admin UI
Path in the interface:
Admin → License management
The following actions are available in the UI:
- Upload — upload a license
- View — view license information
- Delete — delete a license
When uploading a license:
- The administrator pastes the license JWS token into the UI.
- The UI sends the JWS token to the server through an internal backend mechanism.
- The server validates the license and applies it without restarting the application.
6.2 What the server does when a license is uploaded
Data transferred:
License JWS token
What the server does when a license is uploaded:
Validates the
JWS(ES256, a validkid, a non-expireddueDate,edition = ENTERPRISE, and so on).Stores the
JWSitself in the database, in the bd_attachment table, as a record with:
entity_type=LICENSEfile_name="license.key"content_type="text/plain"content=<JWS bytes, UTF-8>version_tag=licenseId(UUID)
The version_tag field is used to store licenseId and to prevent the same license from being uploaded again.
- Calls LicenseManager.reload() — the mode switches without a restart.
6.3 Deleting a license
A license is deleted through the Admin UI (Delete button).
On deletion:
- The license record is deleted from the
bd_attachmenttable. LicenseManager.reload()is executed.- If there is no longer an active license, the system automatically switches to COMMUNITY mode.
An application restart is not required.
7) Where the license is stored
The license source is the LicenseSource component, which reads in priority order:
- Database: the latest record from
bd_attachmentwithentity_type=LICENSEis taken. - Optional config fallback: if there is no license in the database, the string from
application.yamlis used:
app:
license:
initial: "${LICENSE_KEY:}"
This fallback is read-only at startup and is not saved to the database automatically. The persistent license store is the database. License upload is performed exclusively through the Admin UI.
8) How verification works on the server
The LicenseVerifier component does the following:
- Parses the JWS (JJWT).
- Takes the public key from
PublicKeyProviderbykid. - Checks
alg == ES256(others are rejected). - Requires
edition == "Enterprise". - Compares
dueDatewith the current time (UTC). - Returns
LicenseClaims(issuedTo,dueDate,kid, and so on).
If verification fails, the platform runs as COMMUNITY.
9) License policy (limitations)
- Space (Workflow) — always the very first one (the earliest by creation date).
- On a downgrade from ENTERPRISE to COMMUNITY, if the database already contains several spaces (Workflow), the system will automatically work only with the very first space; the others are treated as “invisible/unavailable” for operations.
- GET list / GET by id: in COMMUNITY we show and allow operations only for the very first space (Workflow); other ids are ignored (or return 404/403 — depending on the operation).
- UPDATE / DELETE: allowed only for the very first space (Workflow).
- CREATE:
- if the system has 0 spaces — the first space (Workflow) can be created;
- if the system has > 1 spaces (legacy after a downgrade) — creating new ones is forbidden until the database is reduced to 1 space (Workflow) (so that the discrepancy is not made worse).
10) Protection against license overwrite
The application includes a protection mechanism against changing a license after it has been uploaded.
app:
license:
immutable: true # false by default
When immutable = false, uploading and deleting a license through the Admin UI is allowed.
When immutable = true, after the first upload:
re-uploading a license is forbidden;
deleting a license is forbidden;
all attempts to change the license return 403 Forbidden.
The immutable flag does not affect license status checks or the /info endpoint.
The mechanism is used to protect the license in production environments.
11) Test checklist
- Keys are located in
src/main/resources/licenses.keys/*.pem, names =kid. - Upload a valid license through the Admin UI → the system switches to ENTERPRISE mode.
- A record with
entity_type=LICENSEappears inbd_attachmentin the database; the content is the original JWS (UTF-8). - The license was deleted from the database /
dueDateexpired → the mode falls back toCOMMUNITY. - If there is no license in the database and
app.license.initialis set inapplication.yaml, the server can start inENTERPRISEin memory, but a database record is created only after the license is uploaded through the Admin UI.
12) Determining the current licensing mode
The current platform licensing mode (ENTERPRISE or COMMUNITY) is determined through the service endpoint:
GET /info
{
"license": {
"status": "ENTERPRISE"
}
}
The endpoint is used to:
determine the current licensing mode in the UI;
diagnose system state;
verify the license without access to the administrative interface.