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
entitlementsfor local feature gating, not hardcoded tiers in the product.