Skip to content

Verifying the entitlement signature

The /v1/activate and /v1/validate responses include an entitlement envelope and an entitlement_signature (Ed25519). This lets a product build confirm the response was not forged or modified by a proxy / man-in-the- middle — without trusting the transport.

What you need

  • The matching Ed25519 public key, embedded in your product release. The product's publisher provides it (the private key stays on the Berlanggan side).
  • The ability to reproduce the exact same JSON canonicalization.

Canonicalization

The signature is over the UTF-8 bytes of entitlement serialized with:

  • keys sorted (sort_keys),
  • no whitespace,
  • separators only , and :,
  • non-ASCII characters not escaped (ensure_ascii=false).

Equivalent Python:

import json
canonical = json.dumps(
    entitlement, sort_keys=True, separators=(",", ":"), ensure_ascii=False
).encode("utf-8")

Verification must run against your own canonicalization of the entitlement object, not against a raw slice of the HTTP body.

Verification examples

import base64, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

PUBLIC_KEY_B64 = "…"  # embedded in the product build

def verify(entitlement: dict, signature_b64: str) -> bool:
    pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIC_KEY_B64))
    msg = json.dumps(entitlement, sort_keys=True,
                     separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    try:
        pub.verify(base64.b64decode(signature_b64), msg)
        return True
    except InvalidSignature:
        return False
const nacl = require("tweetnacl");
// canon(): sort object keys recursively, JSON.stringify (default separators = no spaces)
function verify(entitlement, sigB64, pubB64) {
  const msg = Buffer.from(canon(entitlement), "utf8");
  return nacl.sign.detached.verify(
    msg, Buffer.from(sigB64, "base64"), Buffer.from(pubB64, "base64"));
}

Warning

Test that your canonicalization is byte-identical to Berlanggan's before release — any escaping/ordering difference makes verification always fail.

Trust rules

  • An empty entitlement_signature → the server is not configured to sign (dev mode). A production build that has a public key must treat an unsigned response as untrusted, not as "still active".
  • Signature mismatch → don't use the entitlement from that response; keep the last trusted state and schedule a retry.
  • Use the values in entitlements for local feature gating, not hardcoded tiers in the product.