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.
Prerequisites
- Read signed responses.
- Install JDK 17+; Gson 2.14.0; Bouncy Castle 1.86.
- 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
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
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.
Complete source
// 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;
}
}
// 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.
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.