API General Principles

This page describes the conventions shared by all Secure Keystore REST endpoints: transport rules, authentication, the encryption envelope, error codes and common types. Terms are explained in the Glossary.

The REST API is the target design. The endpoints are not implemented yet.

Endpoints:

Common Conventions

Rule Detail
Base path Every endpoint is under /v1. All operations are POST except GET /v1/backend/keys/{keyId}.
Media type Requests and responses are application/json; errors are application/problem+json. An unknown route or method, a POST without Content-Type: application/json, or an unsupported Accept is 400 invalid_request.
Request hygiene Host must be in keystore.api.allowed-hosts. An Origin outside keystore.api.allowed-origins, or Sec-Fetch-Site: cross-site, is 400 invalid_request.
No retries The Wallet Integration Server must not retry a user call: each signed request can be used only once (jti).
Lost responses The keystore stores no responses. See If a Response Is Lost.
Strict JSON Unknown members, duplicate members, trailing data and type coercion (such as a number for a string) are 400 invalid_request.
Body limit 96 KB (keystore.api.max-body-size), measured on the encrypted body where there is one. A larger body is refused before it is read (detail request body too large).
Response headers Every response carries Cache-Control: no-store. Every /v1 response, errors included, also carries a random X-Request-Id, which is written to the request log.
Binary encoding base64url without padding (JOSE) for the signed request and the values of the unit and EUDI endpoints. Standard Base64 (PowerAuth) for the user-signing public keys and all backend-key values.

If a Response Is Lost

  • Registration: revoke the unit and start a new activation.
  • Unit revocation: repeat it; 409 unit_revoked means the revocation succeeded.
  • User call: send a new signed request, with a new jti.
  • Backend key creation: the keyId is generated by the keystore and no lookup endpoint exists, so a lost response leaves a resident key that cannot be addressed through the API. Retrying creates another key; ask the operator to remove the orphaned key from the HSM.

Authentication

Signed Request (User Endpoints)

The plaintext of every encrypted user request is a signed request: a JWS in the general JSON serialization (RFC 7515, section 7.2.1) over one payload. It carries one signature per device key stored for the unit: ES384 for a classical activation, and ES384 plus ML-DSA-65 or ML-DSA-87 for a hybrid one. The PowerAuth mobile SDK builds exactly this object in one call, after one vault unlock (PIN or biometry); see Create JSON Web Signature with Device Private Key for Android and iOS:

calculateJwsSignature(context, authentication, claims, "sk-request+jwt", false, PowerAuthSignatureKeyId.DEVICE, listener)

In this call:

  • DEVICE signs with every device key of the activation;
  • false selects the JSON serialization, because the compact form allows only one signature;
  • "sk-request+jwt" becomes the header’s typ.

The SDK writes the header as exactly typ and alg, and an ES384 signature as raw R‖S.

{
  "payload": "<base64url of the claims>",
  "signatures": [
    {"protected": "<base64url of {\"typ\":\"sk-request+jwt\",\"alg\":\"ES384\"}>", "signature": "<base64url>"},
    {"protected": "<base64url of {\"typ\":\"sk-request+jwt\",\"alg\":\"ML-DSA-87\"}>", "signature": "<base64url>"}
  ]
}

Claims:

{
  "aud": "/v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/key-binding-signatures",
  "sub": "2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4",
  "iat": 1793271600,
  "jti": "q3Vb8Kx0TfW2nLc7RgJ1sA",
  "request": {"items": ["…"]}
}
Part Field Rule
protected header typ Exactly sk-request+jwt.
protected header alg ES384 (raw R‖S) for the ECDSA device key; ML-DSA-65 or ML-DSA-87 (pure ML-DSA) for the ML-DSA device key. No other header member is allowed.
signatures   Exactly one per device key stored for the unit, each algorithm once. Each is verified with the unit’s stored device key of that algorithm.
claims aud The called route with the unit id filled in, for example /v1/eudi/units/2a7d9c14-…/key-binding-signatures.
claims sub The unit id. Must equal the path unitId and the activation of the envelope’s temporary key.
claims iat Unix seconds of the signing. Accepted from 5 minutes before to 1 minute after the keystore’s clock (defaults).
claims jti At least 16 random bytes, base64url, 22 to 64 characters. Single use per unit.
claims request The operation’s request members, documented per endpoint.

Notes on the claims:

  • No other claim is allowed, and the JSON is strict (no duplicate members).
  • The wallet sets iat itself, in seconds, because the SDK’s JWS signing does not add or correct it. Take it from the SDK’s synchronized time (getTimeSynchronizationService().getCurrentTime(), milliseconds, so divide by 1000) and synchronize the time before signing; an unsynchronized SDK falls back to the phone clock, and a wrong iat is rejected as 403 invalid_user_authorization.
  • The keystore remembers each jti, shared by all instances, until its iat is no longer accepted, and refuses a repeated one.

A signed request that does not parse, a missing, extra or invalid signature, a claim that does not match, an iat outside the window, or a repeated jti is 403 invalid_user_authorization. A schema violation inside request is 400 invalid_request. After these checks, the keystore asks PowerAuth Server for the activation status; see Request Checks. Unit registration and revocation carry no signed request.

Backend Callers: Bearer Token, Later Mutual TLS

The Wallet Integration Server’s unit calls and the backend components’ key calls carry no user authorization. Instead, the keystore authenticates the backend caller: with a bearer token per client now, and with a mutual TLS client certificate in production later. Each client is configured with its domains and whether it may register and revoke units.

  • A call without valid credentials gets 401 backend_unauthenticated.
  • A call for a domain of another client gets 403 backend_forbidden.
  • A unit call by a client that may not manage units gets 403 backend_forbidden.

End-to-End Encryption Envelope

The envelope encrypts the user requests and their responses: pa/units/{unitId}/signatures, wallet-instance-keys, wallet-instance-attestations, credential-keys and key-binding-signatures. It carries the signed request; the members documented per endpoint are the signed request’s request claim.

Encryption header. Every encrypted request carries PowerAuth’s encryption header. It names the activation whose temporary key encrypted the body. The SDK’s encryptor sets it for activation-scope encryption (requestHeaders):

X-PowerAuth-Encryption: PowerAuth version="4.0", application_key="znZ5XajbFZAq/jv2HCJLoQ==",
    activation_id="2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4"
  • A missing or empty header, or one longer than 1024 characters, is 400 invalid_request.
  • A header that does not parse, or an activation_id other than the path unitId, is 403 invalid_user_authorization.
  • An application key of another application is refused by PowerAuth Server when it decrypts: 400 invalid_request, detail body: encryption.

Encrypted request (EncryptedRequest, exactly these four members):

Member Type Constraints Meaning
temporaryKeyId string 1 to 64 of [0-9A-Za-z-]; a UUID in practice The temporary key the wallet obtained for this activation from the Wallet Integration Server’s /pa/v4/keystore/create (PowerAuth RESTful integration), which calls PowerAuth Server’s POST /rest/v4/keystore/create; unrelated to Secure Keystore.
encryptedData string standard Base64 PowerAuth AEAD output, in this order: the 12-byte nonce, the 32-byte tag, then the ciphertext. Its length is 4 × ceil((44 + plaintext bytes) / 3).
nonce string standard Base64, at most 64 characters The envelope nonce; single use at PowerAuth Server.
timestamp integer (int64) at least 1 Unix milliseconds of the encryption.

Encrypted response (EncryptedResponse, only on 2xx) has two members:

  • encryptedData (string, standard Base64): the response, encrypted under the request’s temporary key and nonce;
  • timestamp (integer, int64): Unix milliseconds of the encryption.

Freshness. A request is fresh while its temporary key is valid (powerauth.service.temporaryKey.validity, default 5 minutes, by PowerAuth Server’s clock) and its signed request’s iat is within the keystore’s window. An expired, unknown or foreign temporary key is refused by PowerAuth Server.

Sample (a key-binding-signatures request with one item):

POST /v1/eudi/units/2a7d9c14-5e0b-4c1e-9d0a-6f3b8e21c7a4/key-binding-signatures HTTP/1.1
X-PowerAuth-Encryption: PowerAuth version="4.0", …(as above)

{"temporaryKeyId":"e8cd1538-ef37-4c8e-b745-0e8e9f326ecc","encryptedData":"GbydIAxxoU8qUFHUhpZQa1BzAJyK2/myJsLJtpJWjz8JUVt+VOdOUZHYmvZ8TDRL…","nonce":"CVBa2DGm1w1gQvM0h6WUXHG8S+cm0tCd","timestamp":1793271600123}

HTTP/1.1 200 OK

{"encryptedData":"r7Kq0VtM3yHc9XwB2nE6LdA4sZpJ8uGf…","timestamp":1793271600187}

Possible Error Codes

Every error response is an application/problem+json body (RFC 9457). The members come in the order of the table below; absent members are omitted.

{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "code": "invalid_user_authorization"
}
Attribute Type Description
type* String Always about:blank.
title* String The HTTP reason phrase of the status, for example Forbidden.
status* Integer The HTTP status code.
detail String Only for some errors: the failing field and rule, never a value.
code* String The error code, which tells the cases apart (see the table below).

503 responses carry Retry-After in whole seconds, at least 1 (keystore.api.retry-after, default 5 seconds). Error responses are never encrypted.

The codes below are generic: an endpoint returns those its page lists. Codes that only some endpoints return (unit_not_found, unit_already_exists, unit_revoked, activation_state_invalid, wrapped_key_invalid, key_not_found, key_revoked) are listed in the error tabs of each endpoint: PowerAuth Unit API, EUDI API, Backend Keys API.

HTTP Error Code Description
400 invalid_request The request breaks a schema, size or transport rule. See the list below.
401 backend_unauthenticated Unit and backend-key calls: a missing, malformed or unknown bearer token. Sent with WWW-Authenticate: Bearer, before the body is read.
403 invalid_user_authorization The user request is not authorized. See the list below.
403 backend_forbidden A backend-key call for a domain of another client, or a unit call by a client that may not manage units.
500 internal_error A token or key configuration fault, or an unexpected error; no detail.
503 hsm_unavailable The HSM is unavailable.
503 authorization_unavailable PowerAuth Server is unreachable, timed out, or returned an error other than a refusal.

400 invalid_request covers:

  • a schema or pattern violation, or an unknown member;
  • a body over the limit;
  • a request refused by the common conventions (route, method, Content-Type, Accept, Host, Origin, Sec-Fetch-Site);
  • a refused envelope;
  • a refused publicJwk, PoP or proof;
  • a keyCount above the cap, or a key type the domain does not accept.

403 invalid_user_authorization covers:

  • an encryption header that names another activation;
  • a signed request that does not parse, a missing, extra or invalid signature, an aud or sub that does not match, an iat outside the window, or a reused jti;
  • device keys of the unit that the device-key policy refuses;
  • on user signing, stored device keys that differ from those sealed in a wrapped key;
  • an activation that PowerAuth Server reports as not ACTIVE, of another application, with other device keys, or that it refuses.

Common Types

Type JSON type Constraints Meaning
UnitId string canonical lowercase UUID (36 characters) The unit id, equal to the PowerAuth activation id.
WrappedKey string 1 to 1366 characters; compact JWE with an empty second segment An EUDI wrapped key (format), P-256, without kty. Store and send it unchanged; never parse it.
UserSigningWrappedKey string 1 to 9605 characters; compact JWE with an empty second segment A user-signing wrapped key, whose kty header is its key type. At most 1383 characters for ECDSA_P384.
UserSigningKeyType string ECDSA_P384, MLDSA_65, MLDSA_87 Key type of a user-signing key, named like PowerAuth’s key types.
Sha256Digest string 43 base64url characters, canonical (padding bits zero) A 32-byte SHA-256 that the wallet computed over content it showed to the user.
Signature string 86 base64url characters ES256 signature, raw R‖S, base64url.
EcPublicJwk object exactly kty (EC), crv (P-256), x and y (43 base64url characters each); a point on the curve A public P-256 key. A private member such as d is refused.
Last updated on Nov 11, 2026 (21:37) Edit on Github Send Feedback

develop

Secure Keystore