rymga ← Back to Licenses

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.

Fail closed. Missing header, wrong key, malformed body, verification error — all mean invalid. Only a response whose signature verifies is allowed through.

The headers

X-SignatureBase64 RSA signature (SHA256withRSA) over the exact bytes of the response body.
X-KidId of the signing key, so you can support rotation (see below).

Steps

  1. Read the raw response body bytes. Do not re-serialize the JSON — re-encoding changes the bytes and breaks the signature.
  2. Base64-decode X-Signature.
  3. Verify it with your tenant public key (X.509 SubjectPublicKeyInfo, Base64) using SHA256withRSA over those raw bytes.
  4. 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/pubkey

Embed 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.