Verifying the signature
This is the part you must get right. The backend signs every response with your tenant's private key; your software verifies it with the public key you embed. A verdict you can't verify is not a verdict — treat it as invalid, no matter what the body says.
Why it matters
Without signature checking, anyone who can sit between your app and the network — a proxy, a patched
DNS entry, a local hosts file, a tampered build — can hand your app a fake
{ "valid": true } and unlock everything. The signature closes that hole: the
response is only trustworthy if it was produced by your private key, which never leaves the
backend.
The headers
| X-Signature | Base64 RSA signature (SHA256withRSA) over the exact bytes of the response body. |
| X-Kid | Id of the signing key, so you can support rotation (see below). |
Steps
- Read the raw response body bytes. Do not re-serialize the JSON — re-encoding changes the bytes and breaks the signature.
- Base64-decode
X-Signature. - Verify it with your tenant public key (X.509 SubjectPublicKeyInfo, Base64) using
SHA256withRSAover those raw bytes. - Only if verification succeeds, parse the JSON and read the verdict.
Examples
Verify the Base64 signature over the exact response bytes with your embedded public key. Java, Node,
C# and Go verify RSA in their standard library — no extra dependency. Python has no
RSA verifier in its stdlib, so the example uses the widely-used cryptography package
(pip install cryptography).
// PUBLIC_KEY_B64 = your tenant public key (X.509, Base64), embedded as a constant.
byte[] der = Base64.getDecoder().decode(PUBLIC_KEY_B64);
PublicKey pub = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(der));
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(pub);
sig.update(rawBody); // exact response bytes
boolean ok = sig.verify(Base64.getDecoder().decode(xSignature));
if (!ok) throw new SecurityException("SIGNATURE_INVALID");
import { createVerify } from "node:crypto";
// PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\n...";
const ok = createVerify("RSA-SHA256")
.update(rawBody) // Buffer of the exact response bytes
.verify(PUBLIC_KEY_PEM, xSignature, "base64");
if (!ok) throw new Error("SIGNATURE_INVALID");
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64
pub = serialization.load_pem_public_key(PUBLIC_KEY_PEM)
try:
pub.verify(base64.b64decode(x_signature),
raw_body, # exact response bytes
padding.PKCS1v15(), hashes.SHA256())
except Exception:
raise ValueError("SIGNATURE_INVALID")
using System.Security.Cryptography;
using var rsa = RSA.Create();
rsa.ImportFromPem(PUBLIC_KEY_PEM); // or ImportSubjectPublicKeyInfo(der)
bool ok = rsa.VerifyData(
rawBody, // exact response bytes
Convert.FromBase64String(xSignature),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
if (!ok) throw new CryptographicException("SIGNATURE_INVALID");
block, _ := pem.Decode([]byte(publicKeyPEM))
pub, _ := x509.ParsePKIXPublicKey(block.Bytes)
sig, _ := base64.StdEncoding.DecodeString(xSignature)
sum := sha256.Sum256(rawBody) // exact response bytes
if err := rsa.VerifyPKCS1v15(
pub.(*rsa.PublicKey), crypto.SHA256, sum[:], sig); err != nil {
return errors.New("SIGNATURE_INVALID")
}
Getting the key
Download your tenant public key from the panel, or fetch it once and pin it:
GET https://licenses.rymga.com/t/{slug}/api/v1/pubkeyEmbed it as a constant in your build. Do not fetch it at runtime over plain HTTP — an attacker who can swap the key can swap the verdict. Pinning the key in the binary is the whole point.
Key rotation
Each response carries X-Kid, the id of the key that signed it. If you ever rotate
your signing keypair, ship a build that knows both the old and new public keys, keyed by X-Kid,
so in-field installs keep verifying during the transition.