---
title: "Dart signature verification"
description: "Run the complete Dart Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases."
slug: "/docs/v1/security/verify-ed25519/dart"
section: "Signature examples"
order: 212
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 Dart without losing the server's signed numeric tokens.

## What you will learn
- Build and run the complete Dart 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 Dart 3.7+; cryptography 2.9.0.
- 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
cd dart
dart pub get
cd ..
```
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
dart run dart/verify.dart vectors/positive.key.json vectors/positive.envelope.json
dart run dart/verify.dart vectors/tampered.key.json vectors/tampered.envelope.json
dart run dart/verify.dart vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command dart run dart/verify.dart
```
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
```yaml
name: kdpika_verifier_sample
environment:
  sdk: '>=3.7.0 <4.0.0'
dependencies:
  cryptography: 2.9.0
```

```dart
// KDPika signature authenticity only. Dart 3.7+, cryptography 2.9.0.
// Run: dart run verify.dart key.json envelope.json. Output signature_valid=true/false.
// Trust key.json separately; enforce license policy after successful verification.
import 'dart:convert';
import 'dart:io';
import 'package:cryptography/cryptography.dart';

class NumberToken { final String raw; NumberToken(this.raw); }
String quote(String value) {
  for (final cp in value.runes) { if (cp >= 0xd800 && cp <= 0xdfff) throw FormatException('Invalid Unicode'); }
  return jsonEncode(value).replaceAll('\u2028', r'\u2028').replaceAll('\u2029', r'\u2029');
}
class Parser {
  final String text; int i = 0;
  Parser(List<int> raw): text = utf8.decode(raw, allowMalformed: false) {
    if (raw.length > 1048576) throw FormatException('JSON exceeds 1 MiB');
  }
  void ws() { while (i < text.length && ' \t\r\n'.contains(text[i])) { i++; } }
  String string() {
    final m = RegExp(r'^"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"').firstMatch(text.substring(i));
    if (m == null) throw FormatException('Invalid string');
    i += m[0]!.length; final value = jsonDecode(m[0]!) as String; quote(value); return value;
  }
  dynamic value(int depth) {
    if (depth > 64) throw FormatException('JSON exceeds 64 levels');
    ws(); if (i >= text.length) throw FormatException('Missing JSON value');
    final ch = text[i]; if (ch == '"') return string();
    if (ch == '{' || ch == '[') {
      final object = ch == '{'; final end = object ? '}' : ']'; i++; ws();
      final map = <String,dynamic>{}; final list = <dynamic>[];
      if (i < text.length && text[i] == end) { i++; return object ? map : list; }
      while (true) {
        if (object) {
          ws(); final key = string(); ws();
          if (i >= text.length || text[i++] != ':' || map.containsKey(key)) throw FormatException('Duplicate or invalid property');
          map[key] = value(depth + 1);
        } else { list.add(value(depth + 1)); }
        ws(); if (i >= text.length) throw FormatException('Incomplete collection');
        final next = text[i++]; if (next == end) return object ? map : list;
        if (next != ',') throw FormatException('Invalid collection');
      }
    }
    for (final entry in {'true':true, 'false':false, 'null':null}.entries) {
      if (text.startsWith(entry.key,i)) { i += entry.key.length; return entry.value; }
    }
    final m = RegExp(r'^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?').firstMatch(text.substring(i));
    if (m == null) throw FormatException('Invalid JSON value');
    i += m[0]!.length; return NumberToken(m[0]!);
  }
  dynamic parse() { final result = value(0); ws(); if (i != text.length) throw FormatException('Trailing JSON'); return result; }
}
int compareUtf8(String a, String b) {
  final x = utf8.encode(a), y = utf8.encode(b);
  for (var i = 0; i < x.length && i < y.length; i++) { if (x[i] != y[i]) return x[i].compareTo(y[i]); }
  return x.length.compareTo(y.length);
}
String canonical(dynamic value) {
  if (value is NumberToken) return value.raw;
  if (value is Map<String,dynamic>) {
    final keys = value.keys.toList()..sort(compareUtf8);
    return '{${keys.map((k) => '${quote(k)}:${canonical(value[k])}').join(',')}}';
  }
  if (value is List) return '[${value.map(canonical).join(',')}]';
  return value is String ? quote(value) : jsonEncode(value);
}
List<int> decode(dynamic value, int size) {
  if (value is! String || !RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(value)) throw FormatException('Invalid base64url');
  final raw = base64Url.decode(base64Url.normalize(value));
  if (raw.length != size || base64Url.encode(raw).replaceAll('=', '') != value) throw FormatException('Invalid base64url length');
  return raw;
}
Future<bool> verifyEnvelope(List<int> trustedKeyRaw, List<int> envRaw) async {
  try {
    final key = Parser(trustedKeyRaw).parse() as Map<String,dynamic>, env = Parser(envRaw).parse() as Map<String,dynamic>;
    final public = decode(key['public_key'],32); final hash = await Sha256().hash(public);
    final id = 'ed25519_${hash.bytes.map((b) => b.toRadixString(16).padLeft(2,'0')).join().substring(0,16)}';
    if (key['algorithm'] != 'Ed25519' || key['encoding'] != 'base64url' || key['key_id'] != id || env['algorithm'] != 'Ed25519' || env['key_id'] != id || env['payload'] is! Map<String,dynamic>) return false;
    final signature = Signature(decode(env['signature'],64), publicKey: SimplePublicKey(public,type:KeyPairType.ed25519));
    return await Ed25519().verify(utf8.encode(canonical(env['payload'])), signature:signature);
  } catch (_) { return false; }
}
Future<void> main(List<String> args) async {
  var valid = false;
  try { if (args.length == 2) valid = await verifyEnvelope(await File(args[0]).readAsBytes(), await File(args[1]).readAsBytes()); }
  catch (_) { valid = false; }
  print('signature_valid=$valid'); if (!valid) exitCode = 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)
