Signing keys

Generating, uploading, rotating and retiring the key that signs your evidence.

A signing key is the Ed25519 key pair your organization's seals are signed with. The public half is what a recipient pins when they verify a locker. The private half is held by Pentr, encrypted at rest.

There is no console screen for keys — the sidebar entry is greyed out — so every operation below is an HTTP call. All of them are authenticated as the organization's user, with a Kratos session token as a bearer token or the ory_kratos_session cookie. The examples use a shell variable:

API="https://<your-console-host>"  # same origin as the console; there is no separate API host
TOKEN=...                         # your Kratos session token
KEY_ID=...                        # a key_id from the list call below

The API is served from the same origin as the console, so API is the host you sign in at. The console's own client calls these paths relative to that origin.

Key identity

A key's key_id is the hex SHA-256 of its raw 32-byte public key. It is derived from the key material, so anyone holding the public key can recompute the id and check it matches the one a seal names. Seals record the key_id as a plain string rather than a foreign key, so a seal stays meaningful even if the key row is looked up separately.

Each organization has at most one active key. The invariant is held by a partial unique index on the table and by the retire-then-activate transaction that writes a new key.

List your keys

curl -H "Authorization: Bearer $TOKEN" "$API/api/keys"

Returns the organization's keys, newest first, with id, key_id, algorithm, the hex-encoded public key, status (active or retired), source (generated or uploaded), created_at and retired_at. The wrapped private key and the identifier of the key that wraps it are not part of this response, or of any other.

Generate a key

curl -X POST -H "Authorization: Bearer $TOKEN" "$API/api/keys"

Returns 201 and the same metadata for the new key. Generating retires the incumbent in the same transaction: generating is rotating, and there is no separate rotate call. Seals already signed by the retired key stay verifiable, because each names the key_id that signed it and retired keys are not deleted.

If your organization has no key when its first run is sealed, one is generated at that moment, so sealing does not fail for want of a key you never set up.

A key generated this way has a private half that exists only inside Pentr. The API returns no private material, so there is no way to retrieve it afterwards. An uploaded key is also wrapped and held by Pentr; uploading lets you keep your own copy of the private half. See Key custody.

Upload your own key

The body is an unencrypted PKCS#8 Ed25519 private key PEM:

openssl genpkey -algorithm ed25519 -out pentr-signing.pem

curl -X POST "$API/api/keys/upload" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -Rs '{private_key_pem: .}' < pentr-signing.pem)"

Returns 201 with source set to uploaded, and retires the incumbent exactly as generating does.

Two rejections. A PEM that will not parse, one that is password-protected, and a key that is not Ed25519 all get 400 with the reason. Key material already registered on the platform gets 409 with a deliberately vague message: the duplicate check runs across all organizations, so a specific answer would tell a caller whether some other organization holds a given key.

Fetch a public key

curl -H "Authorization: Bearer $TOKEN" "$API/api/keys/$KEY_ID/public"

Returns the key_id, the algorithm, the status, the raw public key as hex, and the same key as a PEM. The PEM is what a recipient passes to pentr-verify with --key. A key_id that does not exist and one belonging to another organization both return 404, so the endpoint does not reveal which key ids are registered.

Handing that PEM to the recipient is outside Pentr. It has to reach them through a channel they trust, or pinning it proves nothing — Verifying a locker covers what changes when they pin it and what does not.

Custody

The private half is wrapped with AES-256-GCM under a key-encryption key the server reads from the environment, and only the wrapped blob is stored. Each blob carries its own random nonce, and the key's key_id is authenticated as additional data, so a wrapped blob cannot be moved onto a different key's row and still decrypt. The row records which key-encryption key wrapped it, so a future rotation of that key knows what to re-wrap; no tool for doing that exists. Key custody describes where the key is held and when it is used.

If the key-encryption key is not configured, the endpoints that need it answer 503 and no key operation proceeds. There is no development fallback.

This is software key custody

Unwrapping happens inside the Pentr process at signing time, and the plaintext key lives in memory there. The platform is therefore technically capable of signing on your organization's behalf. The signing interface is written so a KMS or HSM implementation could replace the software one, and neither ships. A locker's README says the same thing to whoever receives it.

What does not exist

  • No endpoint retires a key without activating a replacement, and none deletes one.
  • No endpoint returns private key material, wrapped or otherwise.
  • There is no re-seal, so rotating a key does not re-sign anything. Past seals keep their old key, and a recipient verifying an old locker needs the public key that signed it, not your current one.

Last updated 2026-09-24