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

## What you will learn
- Build and run the complete Java 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 JDK 17+; Gson 2.14.0; Bouncy Castle 1.86.
- 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
mkdir -p dependencies build/java
curl -fSL https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar -o dependencies/gson.jar
curl -fSL https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.86/bcprov-jdk18on-1.86.jar -o dependencies/bcprov.jar
javac -cp 'dependencies/gson.jar:dependencies/bcprov.jar' -d build/java java/Canonical.java java/Verify.java
```
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 commands use the macOS/Linux classpath separator, a colon. On Windows replace classpath separators with semicolons and quote the complete classpath. Source code is unchanged. JDK21 was used in the recorded test; the source targets Java17.

## Run the positive and negative cases
```bash
java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify vectors/positive.key.json vectors/positive.envelope.json
java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify vectors/tampered.key.json vectors/tampered.envelope.json
java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify
```
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
```java
// Lossless KDPika JSON tree. Java 17+, Gson 2.14.0. Shared with Kotlin verifier.
// JsonReader retains numeric tokens; duplicate properties and bad Unicode fail.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.*;

public final class Canonical {
    private record NumberToken(String raw) {}
    private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
    public static String quote(String value) {
        for (int i=0;i<value.length();i++) {
            char c=value.charAt(i);
            if (Character.isHighSurrogate(c)) {
                if (++i>=value.length() || !Character.isLowSurrogate(value.charAt(i))) throw new IllegalArgumentException("Invalid Unicode");
            } else if (Character.isLowSurrogate(c)) throw new IllegalArgumentException("Invalid Unicode");
        }
        return GSON.toJson(value);
    }
    private static Object value(JsonReader reader,int depth) throws Exception {
        if (depth>64) throw new IllegalArgumentException("JSON exceeds 64 levels");
        return switch(reader.peek()) {
            case BEGIN_OBJECT -> {
                reader.beginObject(); var map=new HashMap<String,Object>();
                while(reader.hasNext()) {
                    var key=reader.nextName(); quote(key);
                    if(map.containsKey(key)) throw new IllegalArgumentException("Duplicate JSON property");
                    map.put(key,value(reader,depth+1));
                }
                reader.endObject(); yield map;
            }
            case BEGIN_ARRAY -> {
                reader.beginArray(); var list=new ArrayList<Object>();
                while(reader.hasNext()) list.add(value(reader,depth+1)); reader.endArray(); yield list;
            }
            case STRING -> { var text=reader.nextString(); quote(text); yield text; }
            case NUMBER -> new NumberToken(reader.nextString());
            case BOOLEAN -> reader.nextBoolean();
            case NULL -> { reader.nextNull(); yield null; }
            default -> throw new IllegalArgumentException("Invalid JSON value");
        };
    }
    public static Object parse(byte[] raw) throws Exception {
        if(raw.length>1048576) throw new IllegalArgumentException("JSON exceeds 1 MiB");
        var decoder=StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
        try(var reader=new JsonReader(new StringReader(decoder.decode(ByteBuffer.wrap(raw)).toString()))) {
            reader.setStrictness(com.google.gson.Strictness.STRICT);
            var result=value(reader,0);
            if(reader.peek()!=JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing JSON");
            return result;
        }
    }
    public static String encode(Object value) {
        if(value instanceof NumberToken n) return n.raw();
        if(value instanceof Map<?,?> m) {
            var keys=new ArrayList<String>(); for(var k:m.keySet()) keys.add((String)k);
            keys.sort((a,b)->Arrays.compareUnsigned(a.getBytes(StandardCharsets.UTF_8),b.getBytes(StandardCharsets.UTF_8)));
            var parts=new ArrayList<String>(); for(var k:keys) parts.add(quote(k)+":"+encode(m.get(k)));
            return "{"+String.join(",",parts)+"}";
        }
        if(value instanceof List<?> l) { var parts=new ArrayList<String>(); for(var child:l) parts.add(encode(child)); return "["+String.join(",",parts)+"]"; }
        if(value instanceof String s) return quote(s);
        if(value==null) return "null";
        if(value instanceof Boolean b) return b?"true":"false";
        throw new IllegalArgumentException("Unsupported value");
    }
    public static byte[] decode(Object input,int size) {
        if(!(input instanceof String s) || !s.matches("[A-Za-z0-9_-]+")) throw new IllegalArgumentException("Invalid base64url");
        var raw=Base64.getUrlDecoder().decode(s);
        if(raw.length!=size || !Base64.getUrlEncoder().withoutPadding().encodeToString(raw).equals(s)) throw new IllegalArgumentException("Invalid base64url length");
        return raw;
    }
}
```

```java
// KDPika signature authenticity only. Java 17+, Gson 2.14.0, BC 1.86.
// Run: java -cp classes:gson.jar:bcprov.jar Verify key.json envelope.json
// Output signature_valid=true/false. Trust the key separately; enforce policy next.
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
public final class Verify {
    public static boolean verifyEnvelope(byte[] trustedKeyRaw,byte[] envelopeRaw) {
        try {
            var key=(Map<?,?>)Canonical.parse(trustedKeyRaw); var env=(Map<?,?>)Canonical.parse(envelopeRaw);
            var publicKey=Canonical.decode(key.get("public_key"),32);
            var id="ed25519_"+HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(publicKey)).substring(0,16);
            if(!"Ed25519".equals(key.get("algorithm")) || !"base64url".equals(key.get("encoding")) || !id.equals(key.get("key_id")) ||
               !"Ed25519".equals(env.get("algorithm")) || !id.equals(env.get("key_id")) || !(env.get("payload") instanceof Map)) return false;
            var message=Canonical.encode(env.get("payload")).getBytes(StandardCharsets.UTF_8);
            var verifier=new Ed25519Signer(); verifier.init(false,new Ed25519PublicKeyParameters(publicKey,0));
            verifier.update(message,0,message.length); return verifier.verifySignature(Canonical.decode(env.get("signature"),64));
        } catch(Exception failure) { return false; }
    }
    public static void main(String[] args) {
        boolean valid=false;
        try { if(args.length==2) valid=verifyEnvelope(Files.readAllBytes(Path.of(args[0])),Files.readAllBytes(Path.of(args[1]))); }
        catch(Exception failure) { valid=false; }
        System.out.println("signature_valid="+valid); if(!valid) System.exit(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)
