Verify KDPika payload authenticity in JavaScript without losing the server's signed numeric tokens.
What you will learn
- Build and run the complete JavaScript verifier.
- Reject altered, ambiguous and untrusted envelopes.
- Keep signature checking separate from permission to use a license.
Prerequisites
- Read signed responses.
- Install Node.js 22+; built-in crypto; no npm 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
node --version
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
node javascript/verify.mjs vectors/positive.key.json vectors/positive.envelope.json
node javascript/verify.mjs vectors/tampered.key.json vectors/tampered.envelope.json
node javascript/verify.mjs vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command node javascript/verify.mjs
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
// Lossless KDPika JSON canonicalization; Node 22+. Used by JS and TS samples.
// Import parseRaw and canonical. Numeric tokens remain in their server form.
// Throws on malformed JSON, duplicate properties, invalid Unicode or depth >64.
export class NumberToken { constructor(raw) { this.raw = raw; } }
export function quote(value) {
for (const character of value) {
const cp = character.codePointAt(0);
if (cp >= 0xd800 && cp <= 0xdfff) throw new Error('Invalid Unicode');
}
return JSON.stringify(value).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
}
export function parseRaw(bytes) {
if (bytes.length > 1048576) throw new Error('JSON exceeds 1 MiB');
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
let pos = 0;
const ws = () => { while (/[\x20\t\r\n]/.test(text[pos] || 'x')) pos++; };
function string() {
const match = /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/.exec(text.slice(pos));
if (!match) throw new Error('Invalid JSON string');
pos += match[0].length;
const value = JSON.parse(match[0]); quote(value); return value;
}
function value(depth) {
if (depth > 64) throw new Error('JSON exceeds 64 levels');
ws(); const ch = text[pos];
if (ch === '"') return string();
if (ch === '{') {
pos++; ws(); const result = Object.create(null);
if (text[pos] === '}') { pos++; return result; }
while (true) {
ws(); const key = string(); ws();
if (text[pos++] !== ':' || Object.hasOwn(result, key)) throw new Error('Duplicate or invalid property');
result[key] = value(depth + 1); ws(); const next = text[pos++];
if (next === '}') return result;
if (next !== ',') throw new Error('Invalid object');
}
}
if (ch === '[') {
pos++; ws(); const result = [];
if (text[pos] === ']') { pos++; return result; }
while (true) {
result.push(value(depth + 1)); ws(); const next = text[pos++];
if (next === ']') return result;
if (next !== ',') throw new Error('Invalid array');
}
}
for (const [token, result] of [['true', true], ['false', false], ['null', null]]) {
if (text.startsWith(token, pos)) { pos += token.length; return result; }
}
const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(text.slice(pos));
if (!number) throw new Error('Invalid JSON value');
pos += number[0].length; return new NumberToken(number[0]);
}
const result = value(0); ws();
if (pos !== text.length) throw new Error('Trailing JSON content');
return result;
}
export function canonical(value) {
if (value instanceof NumberToken) return value.raw;
if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
if (value !== null && typeof value === 'object') {
const keys = Object.keys(value).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
return '{' + keys.map(key => quote(key) + ':' + canonical(value[key])).join(',') + '}';
}
return typeof value === 'string' ? quote(value) : JSON.stringify(value);
}
// KDPika signature authenticity only. Node 22+, built-in crypto, no npm package.
// Run: node verify.mjs key.json envelope.json. Output signature_valid=true/false.
// Pin key.json independently. Enforce license policy separately before access.
import { createHash, createPublicKey, verify } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { parseRaw, canonical } from './canonical.mjs';
function decode(value, size) {
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Invalid base64url');
const bytes = Buffer.from(value, 'base64url');
if (bytes.length !== size || bytes.toString('base64url') !== value) throw new Error('Invalid base64url length');
return bytes;
}
export function verifyEnvelope(trustedKeyRaw, envelopeRaw) {
try {
const key = parseRaw(trustedKeyRaw), env = parseRaw(envelopeRaw);
const publicBytes = decode(key.public_key, 32);
const id = 'ed25519_' + createHash('sha256').update(publicBytes).digest('hex').slice(0, 16);
if (key.algorithm !== 'Ed25519' || key.encoding !== 'base64url' || key.key_id !== id ||
env.algorithm !== 'Ed25519' || env.key_id !== id || !env.payload ||
Array.isArray(env.payload) || typeof env.payload !== 'object') return false;
const publicKey = createPublicKey({ key: Buffer.concat([
Buffer.from('302a300506032b6570032100', 'hex'), publicBytes
]), format: 'der', type: 'spki' });
return verify(null, Buffer.from(canonical(env.payload)), publicKey, decode(env.signature, 64));
} catch { return false; }
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
let valid = false;
try { if (process.argv.length === 4) valid = verifyEnvelope(readFileSync(process.argv[2]), readFileSync(process.argv[3])); }
catch { valid = false; }
console.log('signature_valid=' + valid); process.exitCode = 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
| Symptom | Cause | Action |
|---|---|---|
| Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build |
| Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification |
| Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer |
| Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths |
Next steps
Run all test vectors and apply the signed-response policy checks.