Security
All API requests must be made over HTTPS (TLS 1.2).
API authentication
Every request to the API must be authenticated using the JWS detached token
- JSON Web Signature
(RFC7515) - with an
asymmetric signature. The algorithm used for the signature must be
ES256
(EC NIST P-256 DSA key with SHA-256 hashing).
Elliptic-curve cryptography (ECC) is an approach to public-key cryptography based on the algebraic structure of elliptic curves over finite fields. ECC, an alternative technique to RSA, is a powerful cryptography approach.
As the JWS detached token is defined, it does not contain the payload and
the format used is {header}..{signature}
.
The signature is only about the request body in JSON format using UTF-8 encoding. Ensure that the JWT library used to sign the payload does not add any other field to it in the process.
The token header must contain:
alg
: The cypher algorithm ID. Must beES256
.aud
: Target endpoint. It must be specified as{httpMethod} {urlPath}
. The{urlPath}
includes the path with the query string but not the domain. e.g.,POST /v1/payments
.exp
: Expiration timestamp in JSON format (5 minutes max). Make sure your system clock is in sync to avoid unexpected unauthorized responses.kid
: The ID to identify the private key used to sign the request. Thekid
is aKSUID
ID provided by the support team when the public key is registered in the platform.
JWS detached
X-Signature
header using the JSON Web Signature
token:
{header}..{signature}
.
Each API credential can be assigned selected scopes to segregate permissions to different endpoints. In each endpoint specification, it is indicated the scope required.
Example: "X-Signature": "eyJhbGciOiJ..ICjcwYzA5Ny01ZjQzLT"
.
- Security Scheme Type: API Key
- Header parameter name: X-Signature
Generate a private EC key
Generate a private EC key, of size 256, and output it to a file named
private-key.pem
.
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
Extract the public key from the key pair, which can be used in a certificate
openssl ec -in private-key.pem -pubout -out public-key.pem
After running these two commands you end up with two files:
private-key.pem
and public-key.pem
. The private key private-key.pem
must not be shared. Share the public key public-key.pem
with developer
support by emailing support@nomupay.com.
NomuPay´s developer support team will provide the key identifier kid
which must be included in every request, and the shared HMAC key to verify
the response signature.
Revoke/rotate keys
If your private key has been stolen, anyone can impersonate you. If the shared HMAC key has been stolen, you can not ensure the origins of responses or webhook messages.
If you need to regenerate your keys or revoke the existing ones, generate
new keys and contact support@nomupay.com to
indicate the key identifier kid
of the keys to revoke.
It is possible to have multiple keys at the same time. When you generate a new set of keys, the previous keys should be kept active for a period of several days. This ensures you're able to process with two signatures (as NomuPay's APIs operate using an asynchronous system we are not able to able to apply only the new signature to the following responses or sent webhooks notifications). During this time, your endpoint has multiple active secrets and it generates one signature for each secret.
When the old keys are revoked only the current X-Signature
will exist in
the header.
Code snippets
- C#
- Java
- Node.js
- PHP
- Python
For this example, we use a commonly recommended package jose-jwt
which
will help you create and validate JWT tokens with ease. You can find it on
GitHub.
using Jose;
using System.Security.Cryptography;
public async Task<string> GetJwsDetachedToken(string payload, string method, Uri path)
{
const string KeyId = "YOUR_KEY_ID";
const string PrivateKeyFilePath = "private-key.pem";
const int ExpirationInMinutes = 3;
string privateKeyContent = await GetPrivateKeyFromFile(PrivateKeyFilePath);
byte[] blocks = Convert.FromBase64String(privateKeyContent);
var privateKeyEcDsa = ECDsa.Create();
privateKeyEcDsa.ImportECPrivateKey(blocks, out _);
TimeSpan exp = DateTime.UtcNow.AddMinutes(ExpirationInMinutes) - new DateTime(1970, 1, 1);
Dictionary<string, object> headers = new()
{
["aud"] = $"{method} {path}",
["kid"] = KeyId,
["exp"] = (int)exp.TotalSeconds,
};
return JWT.Encode(payload, privateKeyEcDsa, JwsAlgorithm.ES256, extraHeaders: headers, options: new JwtOptions { DetachPayload = true});
}
private static async Task<string> GetPrivateKeyFromFile(string fileName)
{
string privateKey = await File.ReadAllTextAsync(fileName, System.Text.Encoding.UTF8);
return privateKey
.Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
.Replace("-----BEGIN EC PRIVATE KEY-----", string.Empty)
.Replace("-----END PRIVATE KEY-----", string.Empty)
.Replace("-----END EC PRIVATE KEY-----", string.Empty);
}
To handle the private key in Java it needs to be transformed to PKCS8 format.
openssl pkcs8 -topk8 -nocrypt -in private-key.pem -out private-pk8key.pem
For this example, we use a commonly recommended package jose4j
which will
help you create and validate JWT tokens with ease. You can find it on
Bitbucket.
private static String getJwsDetachedToken(String payload, String method, String path) throws NoSuchAlgorithmException, InvalidKeySpecException, JoseException {
final String keyId = "YOUR_KEY_ID";
final String privateKeyFilePath = "private-pk8key.pem";
final int expirationInMinutes = 3;
String privateKeyContent = getPrivateKeyFromFile(privateKeyFilePath);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyContent));
PrivateKey privateKey = keyFactory.generatePrivate(keySpecPKCS8);
long exp = LocalDateTime.now(ZoneOffset.UTC).plusMinutes(expirationInMinutes).toEpochSecond(ZoneOffset.UTC);
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
jws.setKey(privateKey);
jws.setPayload(payload);
jws.setHeader("aud", method + " " + path);
jws.setHeader("exp", (int)exp);
jws.setKeyIdHeaderValue(keyId);
String jwsToken = jws.getCompactSerialization();
String[] jwsBlocks = jwsToken.split("[.]");
return jwsBlocks[0] + ".." + jwsBlocks[2];
}
private static String getPrivateKeyFromFile(String filename)
{
String privateKey = "";
try {
URL url = RequestExample.class.getClassLoader().getResource(filename);
File file = new File(url.getFile());
privateKey = new String(Files.readAllBytes(file.toPath()));
privateKey = privateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----BEGIN EC PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----END EC PRIVATE KEY-----", "")
.replace("\n", "")
.replace("\r", "");
}
catch (IOException e) {
e.printStackTrace();
}
return privateKey;
}
For this example, we use a commonly recommended package jsonwebtoken
which will help you to create and validate JWT tokens easily. You can find
it on GitHub.
const jwt = require('jsonwebtoken'),
fs = require('fs');
const getJwsDetachedToken = (payload, method, path) => {
const keyId = 'YOUR_KID',
privateKeyFilePath = 'private-key.pem',
expirationInMinutes = 3;
const SECS_PER_MIN = 60,
MILLIS = 1000;
const now = new Date(),
exp = new Date(now.getTime() + expirationInMinutes * SECS_PER_MIN * MILLIS),
expiresIn = parseInt(exp.getTime() / 1000),
privateKey = fs.readFileSync(privateKeyFilePath, 'utf8');
const options = {
algorithm: 'ES256',
header: {
aud: `${method} ${path}`,
kid: keyId,
exp: expiresIn,
},
noTimestamp: true,
};
const jws = jwt.sign(payload, privateKey, options),
jwsBlocks = jws.split('.'),
jwsDetached = `${jwsBlocks[0]}..${jwsBlocks[2]}`;
return jwsDetached;
};
For this example, we use a commonly recommended package firebase/php-jwt
which will help you create and validate JWT tokens with ease. You can find
it on GitHub.
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function getJwsDetachedToken($payload, $method, $path)
{
$keyId = "YOUR_KEY_ID";
$privateKeyFilePath = "private-key.pem";
$expirationInMinutes = 3;
$privateKey = file_get_contents($privateKeyFilePath);
$expireIn = time() + ($expirationInMinutes * 60);
$header = array(
"aud" => "$method $path",
"exp" => $expireIn
);
$jws = JWT::encode($payload, $privateKey, "ES256", $keyId, $header);
$jwsBlocks = explode(".", $jws);
return $jwsBlocks[0] . ".." . $jwsBlocks[2];
}
For this example, we use a commonly recommended package python-jose
which
will help you create and validate JWT tokens with ease. You can find it on
PyPI.
import base64
import json
import time
from jose import jwt
import requests
from cryptography.hazmat.primitives import serialization
def get_jws_detached_token(payload, method, path):
"""Get JWS detached token function."""
kid = "YOUR_KEY_ID"
private_key_file_path = "private-key.pem"
expiration_in_minutes = 3
with open(private_key_file_path, "rb") as private_key_file:
private_key_data = private_key_file.read()
private_key = serialization.load_pem_private_key(
private_key_data, password=None)
exp_seconds = int(time.time() + expiration_in_minutes * 60)
headers = {
"alg": "ES256",
"aud": f"{method} {path}",
"kid": kid,
"exp": exp_seconds,
}
# Create a JWS token with the payload and headers.
token = jwt.encode(payload, private_key,
algorithm="ES256", headers=headers)
encoded_parts = token.split(".")
detached_token = encoded_parts[0]+".." + encoded_parts[2]
return detached_token
def json_encode(json_payload):
"""Encode json payload function."""
# Serialize the JSON payload to a string. The separators parameter
# is used to remove all spaces and new lines from the JSON string.
json_data = json.dumps(json_payload, separators=(
',', ':')).encode('utf-8')
request_token = get_jws_detached_token(payload, method, path)
# All spaces and new lines must be removed from the request body.
request_body = json_encode(payload)
response = requests.post(url + path, data=request_body, headers={
'X-Signature': request_token,
'Content-type': 'application/json',
'Accept': 'application/json', }, timeout=10
)
Verifying signed responses
To allow our clients to verify the origin of public API responses and webhook messages, a JWS is included in each API response and webhook message as depicted below:
- JWS token is passed in header
X-Signature: {jwsToken}
. There will be as manyX-Signature
headers as HMAC keys coexist. Several HMAC keys are only valid during a rotation period. - The algorithm used for signature is
HS256
with a HMAC shared key provided by NomuPay´s support team. - Header parameters (strict validation):
alg
: The cypher algorithm ID. Must beHS256
.aud
: Only for webhook messages, containing the target URL.exp
: Expiration timestamp in JSON format (5 minutes max). It must be checked to avoid a timing attack.kid
: The ID to identify the shared key used to sign the token, provided by NomuPay´s support team. It is in KSUID format. The format must be checked to avoid attempts of injection.
- Payload: Contains the response body or webhook message. This should be used as is received, without any transformation.
- Format:
{headers}..{signature}
.
Code snippets
- C#
- Java
- Node.js
- PHP
- Python
For this example, we use a commonly recommended package jose-jwt
which
will help you create and validate JWT tokens with ease. You can find it on
GitHub.
using Jose;
public bool VerifyResponseSignature(string jwsDetached, string payload, string sharedKey)
{
byte[] secretKey = Encoding.UTF8.GetBytes(sharedKey);
try
{
JWT.Decode(jwsDetached, secretKey, JwsAlgorithm.HS256, payload: payload);
return true;
}
catch (IntegrityException)
{
return false;
}
}
For this example, we use a commonly recommended package jose4j
which will
help you create and validate JWT tokens with ease. You can find it on
Bitucket.
private static boolean verifyResponseSignature(String jwsDetached, String body, String sharedKey)
throws JoseException
{
String payload = Base64.getUrlEncoder().withoutPadding().encodeToString(body.getBytes(StandardCharsets.UTF_8));
String[] jwsBlocks = jwsDetached.split("[..]");
String jwsToken = jwsBlocks[0] + "." + payload + "." + jwsBlocks[2];
Key key = new HmacKey(sharedKey.getBytes(StandardCharsets.UTF_8));
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmConstraints(new AlgorithmConstraints(ConstraintType.PERMIT, AlgorithmIdentifiers.HMAC_SHA256));
jws.setCompactSerialization(jwsToken);
jws.setKey(key);
jws.setDoKeyValidation(false);
return jws.verifySignature();
}
For this example, we use a commonly recommended package jsonwebtoken
which will help you create and validate JWT tokens with ease. You can find
it on GitHub.
const jwt = require('jsonwebtoken');
const verifyResponseSignature = (jwsDetached, body, sharedKey) => {
return new Promise((resolve, reject) => {
const jwsBlocks = jwsDetached.split('..'),
payload = Buffer.from(body, 'utf8').toString('base64url'),
jws = `${jwsBlocks[0]}.${payload}.${jwsBlocks[1]}`;
jwt.verify(jws, sharedKey, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
For this example, we use a commonly recommended package firebase/php-jwt
which will help you create and validate JWT tokens with ease. You can find
it on GitHub.
function verifyResponseSignature($jwsDetached, $body, $sharedKey)
{
try {
$jwsBlocks = explode("..", $jwsDetached);
$payload = base64url_encode($body);
$jws = "$jwsBlocks[0].$payload.$jwsBlocks[1]";
JWT::decode($jws, new Key($sharedKey, "HS256"));
} catch (Firebase\JWT\SignatureInvalidException $e) {
return false;
}
return true;
}
For this example, we use a commonly recommended package python-jose
which
will help you create and validate JWT tokens with ease. You can find it on
PyPI.
import base64
from jose import jwt
def base64url_encode(data):
"""Base64url encode function.
The base64url encoding is the same as base64 encoding, except
that the padding character "=" is replaced with "" and the
characters "+" and "/" are respectively replaced with "-" and "_".
"""
encoded_bytes = base64.b64encode(data)
encoded_string = encoded_bytes.decode(
'utf-8').replace("+", "-").replace("/", "_").rstrip("=")
return encoded_string
def verify_response_signature(detached_token, body, shared_key):
"""Before verifying the signature, you need to re-create the JWS
token from the response headers and body, and then verify it
using the shared key.
"""
encoded_string = base64url_encode(body)
jws_blocks = detached_token.split("..")
jws = jws_blocks[0]+"."+encoded_string+"." + jws_blocks[1]
try:
jwt.decode(jws, shared_key, algorithms=["HS256"])
except:
return False
return True