RSA Keys User Guide

Generate an RSA key pair, publish the public key and mint signed JWTs for server-to-server calls.

This guide shows how to authenticate server-to-server calls with an RSA-signed JWT: generate a key pair, publish the public key in the backoffice, and mint tokens — either with the bundled helper script scripts/build_keys.py, or from your own code in any language.

Golden rule: the private key signs tokens and never leaves your backend. Only the public key is uploaded to Verbatim. See Security best practices.

⚠️ The GitHub TokenBuilder project documents the RSA key generation process.
You will also find code there to help you generate your keys.

https://github.com/verbatim-ai/python-token-builder


How it works

  1. You generate an RSA key pair.
  2. You register the public key in the backoffice. Verbatim stores it and gives it a key id (a UUID).
  3. Your backend signs a JWT (RS512) with the private key and puts the key id in the JWT's kid header.
  4. On each request the server reads kid, looks up your public key, verifies the signature, and trusts the claims (oid, uid, …).

Two identifiers — don't mix them up

NameWhat it isWhere it lives
Key file name (--key-name)The local filename of the key pair on disk. Used to sign.Your backend only — never sent to the server.
Key id (--key-id, the JWT kid)The UUID Verbatim assigns when you register the public key. Used by the server to look up your key.JWT kid header.

They are deliberately independent: the local file can be named anything; the kid must be the id Verbatim returned. Keep a note of which file name maps to which key id.


Part A — Using the key-builder/main.py script

key-builder/main.py is a small, dependency-free helper that generates key pairs and mints tokens. It's the fastest way to get a working token for testing.

Requirements

  • Python 3.10+
  • openssl available on your PATH (used for key generation and RS512 signing)

The script uses only the Python standard library — nothing to pip install.

Install

⚠️ The GitHub TokenBuilder project documents the RSA key generation process.
You will also find code there to help you generate your keys.

https://github.com/verbatim-ai/python-token-builder

uv sync

Step 1 — Generate a key pair

By default keys are read from / written to the ../keys/ directory at the repo root ; override with --keys-dir.

python key-builder/main.py --gen-keys --key-name <your-key-name>

Example with a key named staging

python key-builder/main.py --gen-keys --key-name staging

This creates two files (RSA-4096) and prints the public key to your terminal:

FilePurpose
keys/<your-key-name>Private key (chmod 600) — keep secret
keys/<your-key-name>.pubPublic key — register this in the backoffice

Only --key-name is required here; no key id exists yet.

Step 2 — Publish the public key in the backoffice

Register the contents of keys/staging.pub in the backoffice — follow Managing API keys. The lifecycle is:

By convention, use the same name value (<your-key-name>, like staging), in the backoffice.

Step 3 — Mint a JWT

Sign with the local private key (--key-name) and stamp the server key id into kid (--key-id):

# Basic token (org only)
python key-builder/main.py \
  --key-name staging \
  --key-id <key-uuid-from-backoffice> \
  --org-id <your-org-uuid>
# Basic token (org only)
python key-builder/main.py \
  --key-name staging \
  --key-id  <key-uuid-from-backoffice> \
  --org-id  <your-org-uuid-from-backoffice>
# Token that also carries an end-user id (uid claim)
python key-builder/main.py \
  --key-name staging \
  --key-id <your-key-uuid-from-backoffice> \
  --org-id <your-org-uuid-from-backoffice> \
  --user-id user_42
# Copy straight to the clipboard (macOS)
python key-builder/main.py \
  --key-name staging \
  --key-id <your-key-uuid-from-backoffice> \
  --org-id <your-org-uuid-from-backoffice> | pbcopy

The JWT is printed to stdout; all diagnostics go to stderr, so piping is clean.

You can verify your token with jwt.io, and get

as Header

{
  "alg": "RS512",
  "typ": "JWT",
  "kid": "<your-key-uuid-from-backoffice>"
}

as Payload

{
  "iss": "verbatim-ai.com",
  "iat": 1786020246,
  "exp": 1786023846,
  "oid": "<your-org-uuid-from-backoffice>",
  "uid": "user_42"
}

Step 4 — Test your setup

Test your setup immediately with the v1/auth/whoami endpoint.

JWT=$(python key-builder/main.py --key-name staging --key-id <key-uuid> --org-id <org-uuid>)
curl -H "Authorization: Bearer $JWT" \
     https://api.verbatim-ai.com/v1/auth/whoami

You should get the json body

{
  "organizationId": "<your-org-uuid-from-backoffice>",
  "userId": "user_42"
}

In this example, you get a typical curl command authenticated by a JWT token, issued by your private RSA key.

You can use the JWT token to play with the APIs in your Swagger playground

Parameters

ParameterRequiredDefaultDescription
--key-nameyes—Local key-pair filename (keys/<name> + keys/<name>.pub). Signs; never sent to the server.
--key-idyes (minting)—UUID placed in the JWT kid header — the server key id from the backoffice.
--org-idyes (minting)—Organization UUID → JWT oid claim.
--user-idno—End-user id → JWT uid claim.
--gen-keysnofalseGenerate a new key pair instead of minting (needs only --key-name).
--keys-dirno../keysDirectory for key files.
--issuernoverbatim-ai.comJWT iss claim. Must stay verbatim-ai.com for the server to accept it.
--ttlno3600Token lifetime in seconds.

Running the tests

The script needs nothing installed, but the test suite uses uv:

uv sync          # creates .venv with pytest + pytest-cov
uv run pytest    # runs the suite; coverage must stay at or above 90%

Coverage is printed to the terminal and written as HTML to htmlcov/index.html.


Part B — Minting a JWT from your own code

The script is convenient, but in production you'll sign tokens inside your backend. A Verbatim JWT is a standard RS512 JWT — any mature JWT library produces one. You only need to get three things right: the algorithm, the kid header, and the claims.

The exact token shape

Header

{ "alg": "RS512", "typ": "JWT", "kid": "<your-key-id>" }

Payload

{
  "iss": "verbatim-ai.com",
  "iat": 1700000000,
  "exp": 1700003600,
  "oid": "<your-org-uuid>",
  "uid": "user_42"
}
  • iss must be verbatim-ai.com.
  • oid is required; uid is optional.
  • iat/exp are Unix epoch seconds. Keep the window short (minutes to an hour).

Language-agnostic recipe

If you ever hand-roll it, a JWT is three base64url segments joined by dots:

  1. header_b64 = base64url(json(header))
  2. payload_b64 = base64url(json(payload))
  3. signing_input = header_b64 + "." + payload_b64
  4. signature = RSA_SHA512_sign(private_key, signing_input)
  5. jwt = signing_input + "." + base64url(signature)

Use base64url without padding (drop trailing =). "RS512" = RSASSA-PKCS1-v1_5 with SHA-512. Prefer a library over hand-rolling — the examples below are one call each.

Node.js (jsonwebtoken)

import fs from "node:fs";
import jwt from "jsonwebtoken";

const privateKey = fs.readFileSync("keys/staging", "utf8");

const token = jwt.sign(
  { oid: process.env.ORG_ID, uid: "user_42" },
  privateKey,
  {
    algorithm: "RS512",
    issuer: "verbatim-ai.com",
    expiresIn: "1h",
    keyid: process.env.KEY_ID, // -> kid header
  }
);

Python (PyJWT)

import jwt  # pip install "pyjwt[crypto]"

with open("keys/staging") as f:
    private_key = f.read()

token = jwt.encode(
    {"iss": "verbatim-ai.com", "oid": ORG_ID, "uid": "user_42"},
    private_key,
    algorithm="RS512",
    headers={"kid": KEY_ID},
)
# NB: PyJWT sets iat automatically; add "exp" for a bounded lifetime.

Java (auth0 java-jwt)

Algorithm alg = Algorithm.RSA512(null, privateKey); // RSAPrivateKey
String token = JWT.create()
    .withKeyId(keyId)              // kid header
    .withIssuer("verbatim-ai.com")
    .withClaim("oid", orgId)
    .withClaim("uid", "user_42")
    .withIssuedAt(Instant.now())
    .withExpiresAt(Instant.now().plusSeconds(3600))
    .sign(alg);

Did this page help you?