EN · ORIGINAL REPOSITORY DOCUMENT
Signed client protocol
Original English text. Commands and evidence apply to the revision and environment stated in the document.
product/licensing/README.client.md
On this page
Standalone PHP entitlement client prototype
client.php is original PHP 7.2-compatible code requiring OpenSSL. It is separate
from the WordPress object cache. It gates premium update access only; no cache
operation, Redis connection or runtime correctness depends on licensing.
Create OriginalProduct\Licensing\EntitlementClient with license and installation
secrets, an HTTPS site origin, a configured kid => RSA public PEM map and a private
cache-file path. status() is strictly local. Only an explicit administrative
refresh($transport) invokes your callable, with /entitlement and the request
array. The entitlement transport must use the configured trusted server, verified TLS,
bounded timeouts, POST JSON and a decoded response envelope. The separate WordPress admin adapter supplies
explicit permission/nonce-checked actions and local-only page status. This client
supplies the standalone update helpers described below.
Protocol: {payload, sig, kid}, unpadded canonical base64url. The RSA-SHA256 signature
covers the decoded JSON payload bytes. Claims bind audience
original-product-entitlement, normalized HTTPS origin, SHA256 license digest and
SHA256 installation digest. Accepted signed statuses: active/revoked/inactive/expired.
The timestamps are nonnegative integers with
issued_at <= refresh_after <= expires_at <= grace_until, fresh lifetime at most
3600 seconds, refresh interval at most 300 seconds and grace at most 3600 additional seconds. Future issuance tolerance
is 60 seconds. The server must cap grace at the actual license expiry; that expiry
is not independently present in this envelope. The client cannot reconstruct it.
Local results: fresh/grace permit premium updates; expired/revoked/inactive/unknown do not. Expiry and grace boundaries are exclusive. A signed denial immediately replaces cached active entitlement. Older issuance or a same-time active replay following denial is rejected. Outages, malformed responses and bad signatures never extend verified deadlines. Rotation is explicit: keep old and new public keys in the configured map during transition, then remove retired key IDs.
The persisted JSON contains the signed envelope plus local observed_at high-water
metadata; it stores no raw license or installation key. Atomic same-directory rename
and mode 0600 protect against partial writes. New directories use 0700. Choose an
existing private directory when possible, outside web-served paths. status() can
persist a later observed time locally to prevent a subsequent ordinary clock
rollback from renewing access; inability to retain that mark fails closed. This
metadata is not signed, so a party controlling the client/files/system clock can
alter enforcement. No offline entitlement system is uncrackable. Same-path local writers serialize with flock and re-read the signed envelope
and highest observed time before replacement. Network transport runs outside
the lock. Separate client objects observe a persisted denial on their next local
status call; stale active responses cannot overwrite it. Distributed filesystems
with unreliable advisory locks and externally rewritten clock metadata remain
outside this prototype’s evidence. Lock or persistence failures deny local access.
Tests use generated ephemeral RSA keys; no private key is committed:
docker run --rm --network none -v "$PWD:/opt/plugin:ro" -w /opt/plugin \
php:8.3-apache-bookworm php product/licensing/client-test.php
PHP 8.3 tests do not establish PHP 7.2 runtime coverage; the code intentionally avoids newer syntax. Cross-language compatibility with the Go replacement must pass before adopting it as the actual backend. Owner-confirmed reuse authorization is recorded in ../../PROVENANCE.md; entitlement signatures grant no upstream rights.
Explicit update helpers
checkUpdates($transport) checks the local premium entitlement, then calls
$transport('POST', '/updates', identityArray, []). It requires decoded metadata
with a semantic version string, lowercase SHA256 digest, bounded ASCII bearer token,
active: true and the bound normalized site. download($transport, $metadata)
rechecks local access and metadata, calls
$transport('GET', '/download', [], ['Authorization' => 'Bearer ' . token]), then
returns bytes only if their SHA256 matches. Transport must throw on non-success
HTTP status, including signed-server policy denial, and use the same configured
trusted HTTPS origin. No arbitrary download URL is accepted. Download bytes must
be bounded by the transport to the intended package size. These helpers do not
install/extract packages or modify WordPress.
Transport failures propagate; local grace cannot force a server to approve a revoked license or unavailable download. TLS/server authenticity protects unsigned update metadata; its digest detects corrupted/mismatched bytes, not a malicious issuer. No updater endpoint is configured by default.
Usable explicit HTTP transport
transport.php supplies HttpTransport, an operator-configured fixed HTTPS origin
with verified TLS peer and hostname, redirects disabled, a 5-second default time
budget, 64-KiB JSON and 8-MiB ZIP bounds. Configurable ceilings are 30 seconds,
1 MiB JSON and 64 MiB ZIP. Unsupported methods/paths and arbitrary headers are
rejected. Non-200 responses, wrong content types, invalid JSON, TLS failures and
oversized responses throw generic errors without displaying request secrets.
require_once 'product/licensing/transport.php';
use OriginalProduct\Licensing\EntitlementClient;
use OriginalProduct\Licensing\HttpTransport;
$transport = new HttpTransport(getenv('PRODUCT_LICENSE_ORIGIN'));
$keys = ['fixture-v1' => file_get_contents(getenv('PRODUCT_PUBLIC_KEY_PATH'))];
$client = new EntitlementClient(
getenv('PRODUCT_LICENSE_KEY'), getenv('PRODUCT_INSTALLATION_KEY'),
getenv('PRODUCT_SITE_ORIGIN'), $keys, getenv('PRODUCT_ENTITLEMENT_CACHE_PATH')
);
// Invoke these only through an explicitly authorized administrative action.
$entitlement = $client->refresh([$transport, 'entitlement']);
if ($entitlement['premium_update_access']) {
$metadata = $client->checkUpdates($transport);
$verifiedZipBytes = $client->download($transport, $metadata);
}
// A local status read makes no service request.
$local = $client->status();
For activation/deactivation/check, explicitly call
$transport('POST', '/activate', $identity, []) (or /deactivate, /check) with
license_key, installation_key, site. No lifecycle call happens automatically.
Only HttpTransport::loopbackFixture('http://127.0.0.1:PORT') permits HTTP, for
local fixtures. It rejects localhost names, other IPs, credentials and URL suffixes;
production configuration never falls back to HTTP. Run focused validation with
php product/licensing/transport-test.php; live HTTP behavior is exercised by the
cross-language integration runner. PHP stream connection/header timeout behavior
follows the host runtime; response-body reads also enforce the remaining time budget.