Xentitlement.Entitlements (Xentitlement v0.1.0)

Copy Markdown View Source

Generic entitlement validation using RSA signatures and canonical JSON.

Entitlements are JSON documents signed with RSA-SHA256. The entire payload (all fields except 'signature') is canonically serialized before signing. This design is reusable for any operation type that requires signed metadata.

The validation ensures:

  1. The token is valid JSON
  2. The RSA signature matches the payload
  3. The entitlement_id in the token matches the expected ID
  4. No field was tampered with after signing

Signature Format

Signatures are computed as:

signature = Base64.encode(RSA_SIGN(SHA256(canonical_json), private_key))

where canonical_json is the JSON representation with:

  • All fields except "signature"
  • Keys sorted alphabetically
  • Whitespace normalized

This allows any application holding the RSA public key to verify the token without needing to know how it was generated.

Note: This module uses Erlang :public_key built-in module which is not known at compile time but available at runtime. Compile warnings are suppressed with @compile {:nowarn_undefined}.

Summary

Functions

Extracts entitlement token from HTTP header.

Signs canonical JSON with RSA private key.

Signs an entitlement claims map and returns a JSON string with signature.

Validates an entitlement token (JSON with RSA signature).

Functions

extract_from_header(conn, header_name \\ "x-entitlement")

@spec extract_from_header(Plug.Conn.t(), binary()) ::
  {:ok, binary()} | {:error, :missing | :multiple}

Extracts entitlement token from HTTP header.

By default, looks for the X-Entitlement header, but a custom header name can be specified.

Parameters

  • conn: Plug.Conn connection
  • header_name: Name of the header to extract (default: "x-entitlement")

Returns {:ok, token} if exactly one header present, {:error, :missing} if absent, or {:error, :multiple} if multiple headers present.

Example

iex> extract_from_header(conn)
{:ok, "{\"entitlement_id\":\"...\", ...}"}

iex> extract_from_header(conn, "x-custom-auth")
{:ok, token}

sign_canonical_json(canonical_json, private_key_pem)

@spec sign_canonical_json(binary(), binary()) ::
  {:ok, binary()} | {:error, :invalid_key}

Signs canonical JSON with RSA private key.

Parameters

  • canonical_json: JSON string (should be canonically formatted)
  • private_key_pem: RSA private key in PEM format

Returns

  • {:ok, signature}: Base64-encoded signature
  • {:error, :invalid_key}: Key could not be decoded

Example

iex> canonical_json = "{\"key\":\"value\"}"
iex> sign_canonical_json(canonical_json, private_key_pem)
{:ok, "base64-encoded-signature"}

sign_token(claims, private_key_pem)

@spec sign_token(map(), binary()) ::
  {:ok, binary()} | {:error, :invalid_key | :signing_failed}

Signs an entitlement claims map and returns a JSON string with signature.

The signature is computed over the canonical JSON representation (all fields sorted by key), then encoded as Base64 and added to the token.

Parameters

  • claims: Map with entitlement data (keys should be strings)
  • private_key_pem: RSA private key in PEM format

Returns

  • {:ok, token_json}: JSON string with signature field added
  • {:error, :invalid_key}: Private key could not be decoded
  • {:error, :signing_failed}: Signature operation failed

Example

iex> claims = %{
  "entitlement_id" => "967376a7-6a33-4bc8-846c-ca443c5c56eb",
  "user_id" => "user-123",
  "operation" => "file_upload"
}
iex> sign_token(claims, private_key_pem)
{:ok, "{\"entitlement_id\":\"...\"...}"}

validate(token_json, public_key_pem, entitlement_id)

@spec validate(binary(), binary(), binary()) ::
  {:ok, map()} | {:error, :invalid_json | :invalid_signature | :invalid_claims}

Validates an entitlement token (JSON with RSA signature).

The token is a JSON string containing all authorization data and a Base64-encoded RSA signature. The entire JSON (minus the signature field) must be valid under the public key.

Parameters

  • token_json: JSON string from X-Entitlement header (must be valid JSON)
  • public_key_pem: RSA public key in PEM format
  • entitlement_id: UUID to match against token's entitlement_id field

Returns

  • {:ok, claims}: All fields from the token JSON (including signature field)
  • {:error, :invalid_json}: Token is not valid JSON
  • {:error, :invalid_signature}: RSA signature verification failed
  • {:error, :invalid_claims}: Signature field missing, invalid format, or entitlement_id mismatch

Example

iex> validate(token_json, pem_key, "967376a7-6a33-4bc8-846c-ca443c5c56eb")
{:ok, %{"entitlement_id" => "...", "user_id" => "...", "operation" => "...", ...}}