About Authentication
Ledger security is based on asymmetric cryptography. Each ledger record is secured by a public and private key pair. Public keys are registered in the ledger and each ledger operation is verified by checking the provided signatures.
There are two main types of ledger requests, mutations and reads. A mutation stands for any kind of request to store or modify a record in the ledger.
Authenticating by signing a mutation body
Mutation requests always contain a payload which is designed to convey all information required to perform the mutation.
Because of that mutations are more straightforward and secure. The primary security mechanism for mutations is contained in the proofs array that is provided in the meta object as part of the payload. For example, a body of a create wallet request looks like this:
{
"hash": "<hash of the data>"
"data": {
"handle": "wallet-handle"
},
"meta": {
"proofs": [{
"method": "ed25519-v2",
"public": "<public key>",
"result": "<signature of the hash>",
"digest": "<hash of the data>",
"custom": {
"moment": "2023-02-20T21:42:10.279Z"
}
}]
}
}Steps to sign the object like above:
- Serialize the data
- Hash the serialized data
- Sign the hash with one or more private keys
The keys used for signing are going to be used by the ledger to verify if it is allowed to execute the request in question.
Steps to verify an incoming mutation:
- Serialize the data
- Hash the serialized data
- Compare the received hash with the hash from the payload
- Verify each received signature using public keys from the signatures array and the calculated hash
If the steps above are successful, this means that the received payload is valid and that it was sent by the owners of the provided public keys. We still need to check the permissions of those public keys in order to make sure they are authorized to perform the required operation. See About Authorization for more details about authorization.
Even though mutations are authenticated via body signature, clients can also use JWT Tokens to authenticate and provide a second layer of security. Additionally, clients can skip body signing entirely and authenticate mutations with just a token via token impersonation.
Authenticating with JWT token
The presence of a token - Authorization header - is not mandatory. It becomes required through the configuration of authorization access rules that requires a token to grant access. Once sent, the token is validated for its format, signature and expiration, regardless of the presence of access rules.
The above mechanism doesn't work for read requests, since those are usually GET HTTP requests without any body. For those requests the URL, query parameters and headers define what is going to be returned by the API. To make the ledger API easy to use, but in order to still keep the same security model, the ledger supports JWT tokens for security context exchange between clients and the server. JWT tokens are very flexible and also allow users to transport additional information as part of their payload that can be verified by the ledger. To keep the primary security model compatible with the model for body signatures, clients which hold private keys can issue JWT tokens that can be validated by public keys which ledger has access.
In this model, JWT token replaces the body that is sent in mutation requests, but the whole security remains the same. A client issues a JWT and signs it with its own private key. The ledger can verify that JWT with a public key and enforce security constraints configured for that public key in case the verification is successful. Requests with invalid tokens are rejected regardless of authorization rules set to the ledger.
JWT should be sent to the ledger in a standard way, using the Authorization header:
Authorization: Bearer <JWT token>JWT required headers:
kid - public key which should be used to verify the token signature
alg - algorithm used to sign the JWT TokenSupported algorithms at the present moment:
- EdDSA (ed25519 schema)
JWT payload claims definition:
| Definition | |
|---|---|
| iss | issuer of the token, represents a client identifier, for example cli, studio, etc. |
| sub | subject of the token, represents a user identifier, either public key or handle of the signer or an arbitrary string for external tokens. |
| aud | audience of the token, represents a recipient for which a token is intended |
| iat | issued at time, time at which a token was issued, seconds since epoch |
| exp | expiration time, time after which a token expires, seconds since epoch |
| jti | (optional) unique id of the token, can be used to prevent replay attacks |
| hsh | (optional) request hash (sha256), custom ledger field that enables request content validation |
All claims listed above except for jti and hsh are required. Public keys or handles can be used for all entities that have them as identifiers. Handles are preferable, if a key is registered with the ledger, since they are shorter.
Tokens without hsh are not linked to a specific request and can be used for multiple API requests. Token expiration is controlled by exp claim. If a jti claim is present the ledger needs to store the token id until the token expiration time expires. Clients should create short lived tokens if they provide a jti, max exp allowed is 5 minutes for single use tokens.
A more secure token can also be created by including a request hash. This ties a token to a specific request, so it limits the possible attacks in case a token is leaked. The hashing algorithm used is the same as for the request body described above. The steps to create a request hash:
Create an JSON object representing a request
{
url: "<absolute request URL, including query params>",
method: "<HTTP method, uppercase>", // for example POST
headers: {
// Any protected headers or null if no headers should be protected
// key/value pairs, keys should be lowercased,
// for example "content-type": "application/json"
},
body: {
// Request body or null. This should be an object in case of JSON.
}
}-
Serialize the request JSON object to string by using a deterministic algorithm - two objects with same property names and values should result in the same string. For example: the resulting string should be equal for the following request objects.
const aRequest = { url: "https://minka.io", method: "GET" } const anotherRequest = { method: "GET" method: "https://minka.io" } -
Hash the serialized request object with
sha256algorithm. -
Format the
hshfield by using the following format"hsh": "<hash value>:<protected headers, comma separated>" // example, if Content-Type and X-Api-Key headers are protected "hsh": "3da5df75f03a365b0bc4f53946c77f017aa4fd03ba49977fb7ceb8d75f65cb8f:content-type,x-api-key" // example, if there are no protected headers, only hash should be included "hsh": "3da5df75f03a365b0bc4f53946c77f017aa4fd03ba49977fb7ceb8d75f65cb8f"
We still need to check the permissions of those public keys in order to make sure they are authorized to perform the required operation.
See About Authorization for more details about authorization.
Authenticating mutations with token impersonation
The standard way to authenticate a mutation is to sign the request body with a private key and include the proof in meta.proofs. However, the ledger also supports token impersonation, which allows clients to authenticate mutation requests using only a JWT token in the Authorization header — without having to sign the request body themselves.
This is useful when the client has access to a JWT token but does not have direct access to the private key needed to sign the payload, or when simplifying client-side integration is preferred.
How token impersonation works
When the ledger receives a mutation request (create, update, add proof, or drop) with a valid JWT token and no body signature, it performs the following steps:
-
Token validation: The JWT is extracted from the
Authorizationheader and validated as described in Authenticating with JWT token. The ledger resolves the signer associated with the token'skidheader. -
Impersonation signing: The ledger's built-in
system.authsigner re-signs the request data using its own key pair, on behalf of the authenticated token holder. The generated proof is annotated with:signer— set to the handle of the signer identified by the tokenorigin— set toself-signed-tokento distinguish it from a direct key-pair signatureissuer— set to the handle of the token issuer
-
Proof injection: The system-generated proofs are appended to the request's
meta.proofsarray. The rest of the middleware pipeline (signature validation, authorization) processes them as normal proofs. -
Authorization: The authorization check uses the resolved signer from the token, so the same access rules that apply to key-pair-signed mutations apply to token-impersonated mutations.
Sending a token-only mutation
To create a wallet using only a JWT token, send the request body without hash or meta.proofs:
{
"data": {
"handle": "wallet-handle"
}
}With the Authorization header:
Authorization: Bearer <JWT token>The ledger will hash the data, generate the proof using the system.auth signer on behalf of the token's signer, and validate signatures and authorization as usual.
Partial proofs
Clients can also send partial proofs — proof objects in the meta.proofs array that omit the public key field. Partial proofs act as templates: the impersonation middleware signs on behalf of each partial proof, carrying over any custom data the client included.
This is useful when the client needs to attach custom metadata (such as status or labels) to the proof while letting the ledger handle the cryptographic signing.
{
"data": {
"handle": "wallet-handle"
},
"meta": {
"proofs": [{
"custom": {
"status": "active"
}
}]
}
}The ledger will merge the custom data from each partial proof into the generated signature, producing a fully-formed proof with the appropriate origin.
OAuth2 bearer token impersonation
When the bearer token is an OAuth2 token — either issued by the ledger's /oauth/token endpoint or by an external identity provider (IdP) — impersonation follows the same pattern but with two differences.
Proof origin: The injected proof carries origin: oauth2-token instead of self-signed-token, allowing consumers to distinguish the authentication method.
Signer resolution from sub:
- Ledger-issued tokens —
issmatches the provider signer handle. Thesubclaim must follow thesigner:<handle>format, and the referenced signer must exist on the ledger. - External IdP tokens —
isscontains://(e.g. an Auth0 domain). Thesubclaim is trusted as-is and used directly as the impersonated signer handle. No corresponding signer needs to exist on the ledger.
In both cases issuer on the proof is set to the token's iss claim.
Supported operations
Token impersonation is available for all mutation types:
| Operation | Endpoint pattern | Impersonation middleware |
|---|---|---|
| Create | POST /v2/<records> | Signs and injects proofs into meta.proofs |
| Update | PUT /v2/<records>/:id | Signs and injects proofs into meta.proofs |
| Drop | POST /v2/<records>/:id/drop or DELETE /v2/<records>/:id | Signs and injects proofs into meta.proofs |
| Add proof | POST /v2/<records>/:id/proofs | Signs and replaces the proof payload body |
Token impersonation only activates when a valid JWT token is present. If the request already contains fully-signed proofs (with a public key), those are used as-is and impersonation is skipped for those proofs.
Security considerations
- The
system.authsigner is a ledger-managed internal signer. Its key pairs are never exposed to clients. - Impersonated proofs are marked with
origin: self-signed-token(self-signed JWT) ororigin: oauth2-token(OAuth2 bearer token). This allows the ledger to distinguish between direct key-pair signatures and token-based impersonated signatures during proof resolution. - The same authorization rules apply regardless of whether the proof originated from a direct signature or token impersonation. The resolved signer from the token must have the required permissions.
- Clients cannot forge impersonated proofs. During proof resolution, the ledger verifies that proofs claiming either impersonated origin were actually signed by the
system.authsigner's public key. Proofs with spoofed origin values are stripped and treated as regular key-pair proofs.