Verifying the Signed Token
Every 200 response carries a compact JWS. Verifying it is what turns a JSON body you were handed into proof that Ulises Licenses produced it.
Why verify at all
HTTPS is not the end of the story. It protects the connection from the network, not from the machine on the other end, and a server owner has every incentive to patch your plugin if patching it is easy.
- A proxy can sit in the middle: HTTPS protects you from the network, not from the machine in front of it. A corporate proxy, an antivirus TLS inspector or a managed device configuration can install its own root certificate and terminate the connection. Once it does, it can rewrite the response body and the token inside it. A signature check catches that.
- Your own client is modifiable: Anyone running a server can patch the jar, stub out the check, or point your plugin at a local server. That is what obfuscation and native compilation are for. A signature check means a patch has to also forge a valid token, which requires our private key.
- A captured response can be replayed: A signed response is a real document. Sending a nonce makes it single use, and the exp claim bounds how long it stays useful. Together they close the window where a captured 200 body is enough to start your plugin.
The token
Three base64url segments separated by dots: the protected header, the payload, and the signature. The signed input is the ASCII bytes of header.payload, exactly as they appear in the token.
{ "alg": "ES256", "typ": "ULS-LIC", "kid": "2026-10"}{ "v": 2, "iss": "https://www.uliseslicenses.xyz", "aud": "65f1c2a4b9d84e37a0c5119f", "jti": "b3f1c8d2a9e04756", "iat": 1767225600, "nbf": 1767225300, "exp": 1767240000, "hwid": "9A2B7C1D-44E5-4F60-8B9A-0C1D2E3F4A5B", "st": "active", "data": { "product": { "id": "...", "name": "...", "description": "..." }, "license": { "discordId": "...", "status": "active", "expiresAt": "..." }, "updateInfo": { "available": true, "version": "1.2.3", "jarUrl": null } }}| Claim | Meaning |
|---|---|
| v | Token format version. Currently 2. Reject anything you do not recognise. |
| iss | Issuer. Always https://www.uliseslicenses.xyz. |
| aud | The product id this token was issued for. Must equal the product id you sent. |
| jti | The nonce for this request. Equal to the nonce in the response body. |
| iat | Issued at, seconds since the Unix epoch. |
| nbf | Not valid before. Set to iat minus 300 seconds of clock skew allowance. |
| exp | Expires at, 4 hours after iat. Re-check well before this. |
| hwid | The normalized hardware id the license was checked against, or null when none was sent. |
| st | License status at issue time, normally active. |
| data | The same data tree the response body carries. |
The signing key
Ship the public key with your plugin and key it by kid. Never fetch a key from a URL the response told you to fetch it from, because anything that can rewrite the body can rewrite that URL.
{ "2026-10": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5v07+GZ7jOdgk0ImQ7ESxjClbo+ZjuVHBfbwGxLr1IIPZpfYsSnzw4owYQOnoubRkezeiDz1XN1kGhPl7lcKXA=="}Java
A JWS ES256 signature is the raw 64 byte R||S form, while Signature.getInstance("SHA256withECDSA") expects a DER encoded SEQUENCE of two INTEGERs, so the example wraps the raw bytes in DER before handing them to the JDK.
import java.io.ByteArrayOutputStream;import java.nio.charset.StandardCharsets;import java.security.KeyFactory;import java.security.PublicKey;import java.security.Signature;import java.security.spec.X509EncodedKeySpec;import java.util.Base64;import java.util.Map;import java.util.regex.Matcher;import java.util.regex.Pattern; public final class LicenseTokenVerifier { private static final String JWS_TYP = "ULS-LIC"; private static final long CLOCK_SKEW_SECONDS = 300L; private static final Map<String, String> KEYS = Map.of( "2026-10", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5v07+GZ7jOdgk0ImQ7ESxjClbo+ZjuVHBfbwGxLr1IIPZpfYsSnzw4owYQOnoubRkezeiDz1XN1kGhPl7lcKXA==" ); private static final Pattern TEXT = Pattern.compile("\"([a-zA-Z]+)\"\\s*:\\s*(?:\"([^\"]*)\"|null)"); private static final Pattern NUMBER = Pattern.compile("\"([a-zA-Z]+)\"\\s*:\\s*(-?\\d+)"); public static boolean verify(String token, String productId, String hwid) { try { String[] parts = token.split("\\.", -1); if (parts.length != 3) { return reject("malformed compact JWS"); } String header = decode(parts[0]); if (!"ES256".equals(text(header, "alg"))) { return reject("alg is not ES256"); } if (!JWS_TYP.equals(text(header, "typ"))) { return reject("unexpected typ"); } String spki = KEYS.get(text(header, "kid")); if (spki == null) { return reject("unknown kid"); } byte[] raw = Base64.getUrlDecoder().decode(parts[2]); if (raw.length != 64) { return reject("signature is not 64 bytes"); } Signature verifier = Signature.getInstance("SHA256withECDSA"); verifier.initVerify(publicKey(spki)); verifier.update((parts[0] + "." + parts[1]).getBytes(StandardCharsets.US_ASCII)); if (!verifier.verify(toDer(raw))) { return reject("signature mismatch"); } String payload = decode(parts[1]); long now = System.currentTimeMillis() / 1000L; if (now - CLOCK_SKEW_SECONDS > number(payload, "exp")) { return reject("token expired"); } if (now + CLOCK_SKEW_SECONDS < number(payload, "nbf")) { return reject("token not active yet"); } if (!productId.equals(text(payload, "aud"))) { return reject("aud mismatch"); } String claimHwid = text(payload, "hwid"); if (hwid != null && (claimHwid == null || !hwid.equalsIgnoreCase(claimHwid))) { return reject("hwid mismatch"); } return true; } catch (Exception error) { return reject(String.valueOf(error.getMessage())); } } private static PublicKey publicKey(String spkiBase64) throws Exception { byte[] der = Base64.getDecoder().decode(spkiBase64); return KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(der)); } private static byte[] toDer(byte[] raw) throws Exception { byte[] r = derInteger(raw, 0); byte[] s = derInteger(raw, 32); int bodyLength = r.length + s.length; ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write(0x30); out.write(bodyLength); out.write(r); out.write(s); return out.toByteArray(); } private static byte[] derInteger(byte[] raw, int offset) { int start = offset; while (start < offset + 31 && raw[start] == 0x00) { start++; } int length = offset + 32 - start; boolean pad = (raw[start] & 0x80) != 0; byte[] out = new byte[2 + length + (pad ? 1 : 0)]; out[0] = 0x02; out[1] = (byte) (length + (pad ? 1 : 0)); if (pad) { out[2] = 0x00; } System.arraycopy(raw, start, out, pad ? 3 : 2, length); return out; } private static String decode(String segment) { return new String(Base64.getUrlDecoder().decode(segment), StandardCharsets.UTF_8); } private static String text(String json, String name) { Matcher matcher = TEXT.matcher(json); while (matcher.find()) { if (name.equals(matcher.group(1))) { return matcher.group(2); } } return null; } private static long number(String json, String name) { Matcher matcher = NUMBER.matcher(json); while (matcher.find()) { if (name.equals(matcher.group(1))) { return Long.parseLong(matcher.group(2)); } } throw new IllegalArgumentException("missing numeric claim " + name); } private static boolean reject(String reason) { System.out.println("[license] token rejected: " + reason); return false; }}Node.js
Node expects a DER signature by default too, so pass dsaEncoding: "ieee-p1363" to tell it the bytes are the raw R||S form instead of converting them yourself.
import { createPublicKey, verify } from "node:crypto" const JWS_ALG = "ES256"const JWS_TYP = "ULS-LIC"const CLOCK_SKEW_SECONDS = 300 const KEYS = { "2026-10": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5v07+GZ7jOdgk0ImQ7ESxjClbo+ZjuVHBfbwGxLr1IIPZpfYsSnzw4owYQOnoubRkezeiDz1XN1kGhPl7lcKXA==",} const keyCache = new Map() function publicKeyFor(kid) { if (!keyCache.has(kid)) { keyCache.set( kid, createPublicKey({ key: Buffer.from(KEYS[kid], "base64"), format: "der", type: "spki" }), ) } return keyCache.get(kid)} export function verifyLicenseToken(token, productId, hwid = null) { const parts = token.split(".") if (parts.length !== 3) throw new Error("Malformed compact JWS") const header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8")) if (header.alg !== JWS_ALG) throw new Error("Unsupported alg " + header.alg) if (header.typ !== JWS_TYP) throw new Error("Unexpected typ " + header.typ) const spki = KEYS[header.kid] if (!spki) throw new Error("Unknown kid " + header.kid) const ok = verify( "sha256", Buffer.from(parts[0] + "." + parts[1], "ascii"), { key: publicKeyFor(header.kid), dsaEncoding: "ieee-p1363" }, Buffer.from(parts[2], "base64url"), ) if (!ok) throw new Error("Signature mismatch") const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) const now = Math.floor(Date.now() / 1000) if (now - CLOCK_SKEW_SECONDS > payload.exp) throw new Error("Token expired") if (now + CLOCK_SKEW_SECONDS < payload.nbf) throw new Error("Token not active yet") if (payload.aud !== productId) throw new Error("aud mismatch") if (hwid !== null && String(payload.hwid ?? "").toUpperCase() !== hwid.toUpperCase()) { throw new Error("hwid mismatch") } return payload}Python
The cryptography library takes DER as well, so the two 32 byte halves are packed back into an ASN.1 signature with encode_dss_signature before verifying.
import base64import jsonimport time from cryptography.exceptions import InvalidSignaturefrom cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import ecfrom cryptography.hazmat.primitives.serialization import load_der_public_keyfrom cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature JWS_ALG = "ES256"JWS_TYP = "ULS-LIC"CLOCK_SKEW_SECONDS = 300 KEYS = { "2026-10": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5v07+GZ7jOdgk0ImQ7ESxjClbo+ZjuVHBfbwGxLr1IIPZpfYsSnzw4owYQOnoubRkezeiDz1XN1kGhPl7lcKXA==",} def b64url(value): return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) def verify_license_token(token, product_id, hwid=None): header_b64, payload_b64, signature_b64 = token.split(".") header = json.loads(b64url(header_b64)) if header.get("alg") != JWS_ALG: raise ValueError("unsupported alg") if header.get("typ") != JWS_TYP: raise ValueError("unexpected typ") spki = KEYS.get(header.get("kid")) if spki is None: raise ValueError("unknown kid") public_key = load_der_public_key(base64.b64decode(spki)) signing_input = (header_b64 + "." + payload_b64).encode("ascii") raw = b64url(signature_b64) der = encode_dss_signature( int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big"), ) try: public_key.verify(der, signing_input, ec.ECDSA(hashes.SHA256())) except InvalidSignature as error: raise ValueError("signature mismatch") from error payload = json.loads(b64url(payload_b64)) now = int(time.time()) if now - CLOCK_SKEW_SECONDS > payload["exp"]: raise ValueError("token expired") if now + CLOCK_SKEW_SECONDS < payload["nbf"]: raise ValueError("token not active yet") if payload["aud"] != product_id: raise ValueError("aud mismatch") if hwid is not None and str(payload.get("hwid") or "").upper() != hwid.upper(): raise ValueError("hwid mismatch") return payloadBefore you ship
- The token is a compact JWS with exactly three dot separated segments.
- The protected header alg is exactly ES256. Never accept alg none and never follow the header to pick an algorithm.
- The protected header typ is ULS-LIC.
- The protected header kid selects a key from the keys you ship with, not from anything the response told you to fetch.
- The signature verifies against that key over the ASCII bytes of header.payload.
- exp is in the future, allowing at most a small clock skew.
- nbf is in the past, allowing at most a small clock skew.
- aud equals your product id.
- hwid equals the hardware id you sent, compared case insensitively.
- The data tree you act on comes from the verified payload, not from the parsed response body.
UlisesLib.jar does the same validation for you, with retries, HWID collection and the auto-updater built in. It is a dependency you shade into your jar rather than a contract you implement.
Using the SDKBuilt by Ulises - Citymoon Dynamics