---
title: "JavaScript signature verification"
description: "Run the complete JavaScript Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases."
slug: "/docs/v1/security/verify-ed25519/javascript"
section: "Signature examples"
order: 203
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"]
draft: false
---
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](/docs/v1/start/glossary#license).

## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Install Node.js 22+; built-in crypto; no npm dependency.
- Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) 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
```bash
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
```bash
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](/docs/v1/start/glossary#official-sdk).

## Complete source
```javascript
// 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);
}
```

```javascript
// 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](/docs/v1/start/glossary#feature).

After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/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](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses).

## Related
- [Verification language index](/docs/v1/security/verify-ed25519)
- [Secure token storage](/docs/v1/security/secure-token-storage)
