Minka Ledger Docs
Explanations

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:

  1. Serialize the data
  2. Hash the serialized data
  3. 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:

  1. Serialize the data
  2. Hash the serialized data
  3. Compare the received hash with the hash from the payload
  4. 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.

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 Token

Supported algorithms at the present moment:

  • EdDSA (ed25519 schema)

JWT payload claims definition:

Definition
ississuer of the token, represents a client identifier, for example cli, studio, etc.
subsubject of the token, represents a user identifier, either public key or handle of the signer or an arbitrary string for external tokens.
audaudience of the token, represents a recipient for which a token is intended
iatissued at time, time at which a token was issued, seconds since epoch
expexpiration 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.
  }
}
  1. 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" 
    }
  2. Hash the serialized request object with sha256 algorithm.

  3. Format the hsh field 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.

On this page