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