Request Signatures
Requests to the payment gateway are signed using public-key cryptography. This allows the payment gateway to verify that an incoming request is really from your application, using a key that you never need to share with CAB or any third party.
To sign requests, first you'll need to generate keys. You'll generate two keys, a "private key" (which you store securely and never send to anyone else) and a "public key" (which you upload to the payment gateway using the key management interface).
Then you'll sign each request with your private key.
If your application accepts callback requests from the payment gateway, you'll use the payment gateway's public key (see Payment Gateway Public Key below) and use it to verify that callback requests are legitimate.
Generating Keys
Keys can be generated using OpenSSL or using cryptography libraries in a variety of languages. Keys must meet a few requirements:
- Use the EC algorithm, not RSA or DSA.
- Be at least 256 bits in length.
- Use a named curve. We recommend using
secp256k1
.
Public keys must be encoded in PEM format when uploading to the payment gateway's key management interface.
Using OpenSSL
OpenSSL's command line tools may be used to generate keys.
openssl ecparam -genkey -name secp256k1 -out private-key.pem
openssl ec -in private-key.pem -out public-key.pem -pubout
This will create two PEM-encoded files private-key.pem
(which you should use to sign your outgoing requests; do not send this file to CAB) and public-key.pem
which you should upload to CAB's payment platform using the key management interface.
If your application is in Java and you're using the default java.security
provider, your private key will need to be in PKCS#8 DER encoding.
openssl pkcs8 -in private-key.pem -out private-key.p8 -outform der -nocrypt -topk8
Using Python
This script uses the ECDSA implementation in the community-maintained ecdsa package. You will need to install it (for example, using pip install ecdsa
) first. Before using this or any other cryptography library, make sure it has been audited for security weaknesses to your satisfaction.
from ecdsa import SigningKey, NIST256p
signing_key = SigningKey.generate(curve=NIST256p)
verifying_key = signing_key.get_verifying_key()
with open('private-key.pem', 'wb') as private_key_file:
private_key_file.write(signing_key.to_pem())
with open('public-key.pem', 'wb') as public_key_file:
public_key_file.write(verifying_key.to_pem())
This will create two PEM-encoded files private-key.pem
(which you should use to sign your outgoing requests; do not send this file to CAB) and public-key.pem
which you should upload to CAB's payment platform using the key management interface.
Key Rotation
The payment gateway supports multiple public keys per client ID. If you would like to switch to a new private key, e.g., because you suspect your existing one was compromised, you can do so without interruption of service as follows:
-
Generate a new key pair.
-
Upload the new public key to the payment gateway's key management interface.
-
Change your application configuration to use the new private key to sign requests.
-
Delete the old public key using the payment gateway's key management interface.
Once the old public key is deleted, any requests signed with the old private key will be rejected by the payment gateway.
Signing API Requests
Every API request must contain two header lines, Request-Signature
and Key-ID
, which should have the format
Request-Signature: ecdsa=(signature value)
Key-ID: (key ID)
The key ID indicates which key is being used to sign a request. It is assigned when the public key is uploaded to the payment gateway's key management interface.
The signature value is generated as follows:
-
Render the request body as JSON using UTF-8 encoding.
-
Compute the ECDSA signature of the SHA-256 hash of the rendered request body using your private key.
-
Encode the signature using DER. Many cryptography libraries will do this automatically.
-
Render the encoded signature using Base64 encoding.
Do not include your private key in the request body! It should only be supplied as an input to the signature generation code.
Verifying Callback Request Signatures
Every callback request from the gateway will contain Request-Signature
and Key-ID
HTTP header lines in the same format as described in the previous section. Your application should verify that it is correct.
-
Read the payment gateway's public key. The payment gateway's public key rotates frequently, so use the value of the
Key-ID
header line to determine which key to read. If the key ID is one your application hasn't seen before, request the contents of the key using the/api/callback/key
API request. You may cache the key contents for each key ID indefinitely. -
Decode the signature value from the callback request's
Request-Signature
header line by extracting the value after theecdsa=
prefix and running it through a base64 decoder. -
Feed the raw request payload and the decoded signature to your cryptography library's signature verifier. Note that the payload must be the actual bytes sent by the payment gateway; signature verification is typically done before the payload is passed to a JSON parser.
Example Code
The following code snippets are intended as illustrations and shouldn't be used verbatim. In particular, for clarity's sake they assume that the private key is stored in an unencrypted file. Private keys should be stored and accessed securely.
Java
This example uses Jackson JSON serialization and the JDK HTTP client.
Signing Requests
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
public class SigningExample {
/**
* Sends a request to the payment gateway. In real code, you would probably want to do this
* asynchronously with a more flexible HTTP client library.
*
* @param apiRequestUrl For example, https://payment-api.thesegovia.com/api/pay
* @param requestParams Payload to send. You'll probably want to define classes to represent the
* payloads of the payment gateway API requests you send.
* @param keyId The ID of your keypair.
* @param keyFile Location of your private key in PKCS8 DER encoding. In real code, you'll
* probably want to load your key from a secure data store.
* @return The gateway's response. In real code, you'll probably want to define classes to
* represent the response payloads of the API requests you send; this example returns a
* generic Map object.
*/
public Map<String, Object> sendGatewayRequest(
URL apiRequestUrl, Object requestParams, String keyId, File keyFile)
throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException,
SignatureException {
KeyFactory keyFactory = KeyFactory.getInstance("EC");
Signature ecdsa = Signature.getInstance("SHA256withECDSA");
// Load your private key. (Java requires the key to be in PKCS8 DER encoding.)
byte[] keyData = Files.readAllBytes(keyFile.toPath());
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyData);
PrivateKey privateKey = keyFactory.generatePrivate(spec);
// Serialize the request parameters to JSON in a byte array.
ObjectMapper mapper = new ObjectMapper();
byte[] requestBody = mapper.writeValueAsBytes(requestParams);
// Sign the request body with the private key.
ecdsa.initSign(privateKey);
ecdsa.update(requestBody);
byte[] derSignature = ecdsa.sign();
// Render the Request-Signature header's value.
String signature = Base64.getEncoder().encodeToString(derSignature);
String headerValue = "ecdsa=" + signature;
// Open a connection to the payment gateway and set the required HTTP headers.
HttpURLConnection connection = (HttpURLConnection) apiRequestUrl.openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("API-Version", "1.0");
connection.setRequestProperty("Content-Length", String.valueOf(requestBody.length));
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Key-ID", keyId);
connection.setRequestProperty("Request-Signature", headerValue);
// Send the request body (in JSON form) to the payment gateway.
OutputStream out = connection.getOutputStream();
out.write(requestBody);
out.close();
if (connection.getResponseCode() >= 200 && connection.getResponseCode() <= 299) {
// Read the response. You may prefer to deserialize to a class that has the fields
// you expect to get back; this example deserializes to a generic Map object.
try (InputStream in = connection.getInputStream()) {
return mapper.readValue(in, new TypeReference<Map<String, Object>>() {});
}
} else {
throw new IOException(
String.format(
"Payment gateway request failed with status %d: %s",
connection.getResponseCode(), connection.getResponseMessage()));
}
}
}
Verifying Callback Signatures
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class VerifyingExample {
/** Returns true if the signature in the headers is valid for the payload. */
public boolean verifySignature(byte[] payload, Map<String, String> headers)
throws InvalidKeySpecException, IOException, NoSuchAlgorithmException, SignatureException,
InvalidKeyException {
Signature ecdsa = Signature.getInstance("SHA256withECDSA");
String keyId = headers.get("Key-ID");
String requestSignature = headers.get("Request-Signature");
PublicKey publicKey = getPublicKey(keyId);
// Decode the value from the header line.
Pattern signaturePattern = Pattern.compile("ecdsa=(\\S+)");
Matcher matcher = signaturePattern.matcher(requestSignature);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid request signature");
}
byte[] derSignature = Base64.getDecoder().decode(matcher.group(1));
// Check whether the signature is valid for the payload.
ecdsa.initVerify(publicKey);
ecdsa.update(payload);
return ecdsa.verify(derSignature);
}
/** ID of most recently seen key. */
private String mostRecentKeyId;
/** Most recently seen key. */
private PublicKey mostRecentPublicKey;
/** Address of API endpoint to retrieve the current public key. */
URI GATEWAY_KEY_URI = URI.create("https://payment-api.thesegovia.com/api/callback/key");
/**
* Retrieves a public key from the payment gateway. In real code, you would probably want to do
* this with a different HTTP client library, and you might also choose to cache the most recent
* key so it's already available when your application starts. There is no need to cache older
* keys; once the gateway stops using a particular key, that key should be discarded.
*/
private PublicKey getPublicKey(String keyId)
throws IOException, InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException {
if (keyId.equals(mostRecentKeyId)) {
return mostRecentPublicKey;
}
HttpURLConnection connection = (HttpURLConnection) GATEWAY_KEY_URI.toURL().openConnection();
connection.connect();
if (connection.getResponseCode() == 200) {
try (InputStream in = connection.getInputStream()) {
KeyFactory keyFactory = KeyFactory.getInstance("EC");
ObjectMapper mapper = new ObjectMapper();
Map<String, String> result =
mapper.readValue(in, new TypeReference<Map<String, String>>() {});
if (!keyId.equals(result.get("keyId"))) {
throw new InvalidKeyException("Gateway returned a different key ID");
}
// Extract the base64-encoded body of the key, discarding the PEM header and footer.
// If you are using a cryptography library such as BouncyCastle that can directly accept
// PEM-encoded keys, this is unnecessary. (For clarity, this example omits sanity
// checking of the payment gateway's response.)
String pem = result.get("pem");
String base64DerKey = pem.split("-----")[2];
byte[] derKey = Base64.getMimeDecoder().decode(base64DerKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derKey);
mostRecentPublicKey = keyFactory.generatePublic(keySpec);
mostRecentKeyId = keyId;
return mostRecentPublicKey;
}
} else {
throw new IOException("Unable to fetch public key from payment gateway");
}
}
}
PHP
This example uses the ECDSA implementation in the community-maintained mdanter/ecc package. Before using this or any other cryptography library, make sure it has been audited for security weaknesses to your satisfaction.
Signing Requests
use Mdanter\Ecc\Crypto\Signature\Signer;
use Mdanter\Ecc\Crypto\Signature\SignHasher;
use Mdanter\Ecc\EccFactory;
use Mdanter\Ecc\Random\RandomGeneratorFactory;
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
$adapter = EccFactory::getAdapter();
$generator = EccFactory::getNistCurves()->generator256();
$serializer = new PemPrivateKeySerializer(new DerPrivateKeySerializer($adapter));
$signatureSerializer = new DerSignatureSerializer();
$hasher = new SignHasher('sha256', $adapter);
$signer = new Signer($adapter);
$randomGenerator = RandomGeneratorFactory::getRandomGenerator();
$randomBits = $randomGenerator->generate($generator->getOrder());
// Load your private key.
$privateKey = $serializer->parse(file_get_contents('/path/to/your/key.pem'));
$keyId = 'YourKeyID';
// Encode the request parameters as a JSON string.
$postBody = json_encode($requestParameters);
// Sign the SHA-256 hash of the body using a random value.
$hash = $hasher->makeHash($postBody, $generator);
$signature = $signer->sign($privateKey, $hash, $randomBits);
$serializedSignature = $signatureSerializer->serialize($signature);
// Render the Request-Signature header's value.
$base64Signature = base64_encode($serializedSignature);
// Set up the URL and header lines.
$url = 'https://payment-api.thesegovia.com/api/pay';
$headers = array(
'API-Version: 1.0',
'Content-Type: application/json',
'Key-ID: ' . $keyId,
'Request-Signature: ecdsa=' . $base64Signature
);
$curl_handle = curl_init($url);
curl_setopt_array($curl_handle, array(
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $post_body,
CURLOPT_RETURNTRANSFER => true
));
// Send the request and read the response from the payment gateway.
$response = curl_exec($curl_handle);
if ($response === false) {
print('Error sending request: ' . curl_error($curl_handle) . '\n');
} else {
$result = json_decode($response);
}
// Close the cURL handle to release system resources.
curl_close($curl_handle);
// Examine $result and do whatever your application needs to do.
Verifying Callback Signatures
This example uses the APCu module to cache the payment gateway public key between requests.
use Mdanter\Ecc\Crypto\Signature\Signer;
use Mdanter\Ecc\Crypto\Signature\SignHasher;
use Mdanter\Ecc\EccFactory;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
$adapter = EccFactory::getAdapter();
$generator = EccFactory::getNistCurves()->generator256();
$serializer = new PemPublicKeySerializer(new DerPublicKeySerializer($adapter));
$signatureSerializer = new DerSignatureSerializer();
$hasher = new SignHasher('sha256', $adapter);
$signer = new Signer($adapter);
// Use a previously-fetched key if its ID is correct.
$gatewayKey = apcu_fetch('gatewayKey');
if (!$gatewayKey || $gatewayKey['keyId'] != $_SERVER['HTTP_KEY_ID']) {
// Fetch the current key from the gateway.
$keyJson = file_get_contents('https://payment-api.thesegovia.com/api/callback/key');
$gatewayKey = json_decode($keyJson);
if ($gatewayKey['keyId'] != $_SERVER['HTTP_KEY_ID']) {
throw new Exception('Received invalid key ID');
}
apcu_store('gatewayKey', $gatewayKey);
}
$publicKey = $serializer->parse($gatewayKey['pem']);
// Pull the signature from the Request-Signature HTTP header.
$headerParts = explode('=', $_SERVER['HTTP_REQUEST_SIGNATURE'], 2);
if (count($headerParts) != 2 || $headerParts[0] != 'ecdsa') {
throw new Exception('Invalid Request-Signature value');
}
$derSignature = base64_decode($headerParts[1]);
$signature = $signatureSerializer->parse($derSignature);
// Read the body of the payment gateway's POST request.
$requestPayload = file_get_contents('php://input');
$hash = $hasher->makeHash($requestPayload, $generator);
if ($signer->verify($publicKey, $signature, $hash)) {
// The callback request signature is valid.
} else {
// The callback request signature is invalid and the request should be rejected.
}
Python 3
This example uses the ECDSA implementation in the community-maintained ecdsa package. Before using this or any other cryptography library, make sure it has been audited for security weaknesses to your satisfaction.
Signing Requests
from base64 import b64encode
from ecdsa import SigningKey
from ecdsa.util import sigencode_der
import hashlib
import json
import requests
def send_request_to_gateway(url, params, key_id, private_key_filename):
"""Send a signed request to the payment gateway and return the response.
url: For example, https://payment-api.thesegovia.com/api/pay
payload: Payload to send to gateway, typically a dict or object
key_id: Segovia-assigned ID of the private key to use for signing
private_key_filename: Location of PEM-encoded private key. In real code, you
might put the private key somewhere other than the local filesystem."""
# Load your private key.
with open(private_key_filename, "r") as keyfile:
signing_key = SigningKey.from_pem(keyfile.read())
# Encode the request parameters as a JSON string.
request_body = json.dumps(request_params).encode("UTF-8")
# Generate the binary DER encoding of the signature.
binary_signature = signing_key.sign(
request_body, hashfunc=hashlib.sha256, sigencode=sigencode_der
)
# Render the Request-Signature header's value.
base64_signature = b64encode(binary_signature).decode("ascii")
header_value = "ecdsa={0}".format(base64_signature)
# Set the required headers.
headers = {
"API-Version": "1.0",
"Content-Type": "application/json",
"Key-ID": key_id,
"Request-Signature": header_value,
}
# Send the request and read the response from the payment gateway.
response = requests.post(url, data=request_body, headers=headers)
result = response.json()
# Examine result and do whatever your application needs to do.
return result
Verifying Callback Signatures
from base64 import b64decode
from ecdsa import VerifyingKey
from ecdsa.util import sigdecode_der
import hashlib
import json
import os
import re
import requests
request_signature_header_regex = re.compile(r"ecdsa=([0-9A-Za-z+/=]+)")
gateway_key_file = "gateway-public-key.json"
def get_gateway_key_pem(key_id):
"""Load the gateway's public key, first looking for a previously-retrieved copy.
In real code, you'd probably cache the key somewhere other than a local file."""
if os.path.exists(gateway_key_file):
with open(gateway_key_file, "r") as key_fp:
cached_key = json.load(key_fp)
if cached_key["keyId"] == key_id:
return cached_key["pem"]
# Either we didn't have a previously-cached key or it had an old ID.
response = requests.get("https://payment-api.thesegovia.com/api/callback/key")
response.raise_for_status()
key_json = response.json()
if key_json["keyId"] != key_id:
raise Exception("Invalid key ID received")
with open(gateway_key_file, "w") as key_fp:
json.dump(key_json, key_fp)
return key_json["pem"]
def verify_signature(request_body, headers):
"""Verify a payment gateway callback signature.
request_body: Byte string containing the payload of the callback request.
headers: Dict containing the HTTP headers from the callback request.
Return True if the signature matches the request body."""
verifying_key = VerifyingKey.from_pem(get_gateway_key_pem(headers["Key-ID"]))
matches = request_signature_header_regex.match(headers["Request-Signature"])
if not matches:
raise Exception("Request-Signature header value invalid")
der_signature = b64decode(matches.group(1))
return verifying_key.verify(
der_signature, request_body, hashfunc=hashlib.sha256, sigdecode=sigdecode_der
)
Payment Gateway Public Key
The gateway's current key ID is ...loading...
.