KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Advanced

PHP signature verification

Run the complete PHP Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases.

On this page
Updated 2026-10-17 UTCDownload Markdown

Verify KDPika payload authenticity in PHP without losing the server's signed numeric tokens.

What you will learn

  • Build and run the complete PHP verifier.
  • Reject altered, ambiguous and untrusted envelopes.
  • Keep signature checking separate from permission to use a license.

Prerequisites

  • Read signed responses.
  • Install PHP 8.2+ with ext-sodium; no Composer dependency.
  • Download the complete verification source archive and extract it.
  • Open a terminal in the extracted security directory; all paths below are relative to it.
  • Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier.

Install and build

bashbash
php -r 'exit(extension_loaded("sodium") ? 0 : 1);'

Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support.

The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting.

Run the positive and negative cases

bashbash
php php/verify.php vectors/positive.key.json vectors/positive.envelope.json
php php/verify.php vectors/tampered.key.json vectors/tampered.envelope.json
php php/verify.php vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command php php/verify.php

Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches.

Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the official SDK.

Complete source

verify.phpphp
<?php
// KDPika signature authenticity only. PHP 8.2+, ext-sodium, no Composer package.
// Run: php verify.php key.json envelope.json. Output signature_valid=true/false.
// Keep key.json trusted independently. A valid signature does not grant access.
declare(strict_types=1);
final class NumberToken { public function __construct(public string $raw) {} }
final class RawJson {
    private int $i = 0;
    public function __construct(private string $text) {
        if (strlen($text) > 1048576) throw new RuntimeException('JSON exceeds 1 MiB');
    }
    private function ws(): void { $this->i += strspn($this->text, " \t\r\n", $this->i); }
    private function string(): string {
        if (!preg_match('/\G"(?:[^"\\\\\x00-\x1f]|\\\\(?:["\\\\\/bfnrt]|u[0-9a-fA-F]{4}))*"/s', $this->text, $m, 0, $this->i)) throw new RuntimeException('Invalid string');
        $this->i += strlen($m[0]);
        return json_decode($m[0], false, 64, JSON_THROW_ON_ERROR);
    }
    private function value(int $depth): mixed {
        if ($depth > 64) throw new RuntimeException('JSON exceeds 64 levels');
        $this->ws(); $ch = $this->text[$this->i] ?? '';
        if ($ch === '"') return $this->string();
        if ($ch === '{' || $ch === '[') {
            $object = $ch === '{'; $end = $object ? '}' : ']'; $this->i++; $this->ws();
            $result = $object ? new stdClass() : [];
            if (($this->text[$this->i] ?? '') === $end) { $this->i++; return $result; }
            while (true) {
                if ($object) {
                    $this->ws(); $key = $this->string(); $this->ws();
                    if (($this->text[$this->i++] ?? '') !== ':' || property_exists($result, $key)) throw new RuntimeException('Duplicate or invalid property');
                    $result->{$key} = $this->value($depth + 1);
                } else { $result[] = $this->value($depth + 1); }
                $this->ws(); $next = $this->text[$this->i++] ?? '';
                if ($next === $end) return $result;
                if ($next !== ',') throw new RuntimeException('Invalid collection');
            }
        }
        foreach (['true' => true, 'false' => false, 'null' => null] as $token => $value) {
            if (substr($this->text, $this->i, strlen($token)) === $token) { $this->i += strlen($token); return $value; }
        }
        if (!preg_match('/\G-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/', $this->text, $m, 0, $this->i)) throw new RuntimeException('Invalid value');
        $this->i += strlen($m[0]); return new NumberToken($m[0]);
    }
    public function parse(): mixed {
        $value = $this->value(0); $this->ws();
        if ($this->i !== strlen($this->text)) throw new RuntimeException('Trailing content');
        return $value;
    }
}
function canonical(mixed $value): string {
    if ($value instanceof NumberToken) return $value->raw;
    if ($value instanceof stdClass) {
        $props = get_object_vars($value); ksort($props, SORT_STRING); $parts = [];
        foreach ($props as $key => $child) $parts[] = canonical((string)$key) . ':' . canonical($child);
        return '{' . implode(',', $parts) . '}';
    }
    if (is_array($value)) return '[' . implode(',', array_map('canonical', $value)) . ']';
    return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
function decodeKey(string $value, int $size): string {
    if (!preg_match('/^[A-Za-z0-9_-]+$/D', $value)) throw new RuntimeException('Invalid base64url');
    $raw = base64_decode(strtr($value, '-_', '+/'), true);
    if ($raw === false || strlen($raw) !== $size || rtrim(strtr(base64_encode($raw), '+/', '-_'), '=') !== $value) throw new RuntimeException('Invalid base64url length');
    return $raw;
}
function verifyEnvelope(string $trustedKeyRaw, string $envelopeRaw): bool {
    try {
        $key = (new RawJson($trustedKeyRaw))->parse(); $env = (new RawJson($envelopeRaw))->parse();
        if (!($key instanceof stdClass) || !($env instanceof stdClass)) return false;
        foreach (['public_key','algorithm','encoding','key_id'] as $field) if (!isset($key->{$field}) || !is_string($key->{$field})) return false;
        foreach (['algorithm','key_id','signature','payload'] as $field) if (!isset($env->{$field})) return false;
        $public = decodeKey($key->public_key, 32); $id = 'ed25519_' . substr(hash('sha256', $public), 0, 16);
        if ($key->algorithm !== 'Ed25519' || $key->encoding !== 'base64url' || $key->key_id !== $id || $env->algorithm !== 'Ed25519' || $env->key_id !== $id || !($env->payload instanceof stdClass)) return false;
        return sodium_crypto_sign_verify_detached(decodeKey($env->signature, 64), canonical($env->payload), $public);
    } catch (Throwable) { return false; }
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
    $valid = false;
    try { if ($argc === 3) $valid = verifyEnvelope((string)file_get_contents($argv[1]), (string)file_get_contents($argv[2])); }
    catch (Throwable) { $valid = false; }
    echo 'signature_valid=' . ($valid ? 'true' : 'false') . PHP_EOL; exit($valid ? 0 : 1);
}

Expected result

All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values.

Common mistakes

  • Taking the verification key from the same untrusted message being checked.
  • Treating a successful signature as proof of active access, correct installation or recent payment.
  • Parsing and reserializing numbers before passing the envelope into this sample.

Security notes

The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified features.

After signature success, check product, environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See offline mode. The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs.

Troubleshooting

SymptomCauseAction
Positive case failsWrong trusted key, changed numeric tokens or missing dependencyRestore the exact archive/vector files and rerun the build
Tampered case returns falseExpected rejectionKeep the refusal; do not bypass verification
Expired case returns trueAuthenticity and expiry are separateReject access in the license policy layer
Cannot open a fileTerminal is outside the extracted security directoryChange to that directory and retry using quoted paths

Next steps

Run all test vectors and apply the signed-response policy checks.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings