HTTP Message Signing
All HTTP messages (both requests and responses) shall contain a signed JWT token placed in the x-jwt-signature header. This token is the primary authentication method, and in addition to authentication of the client also ensures message contents is tamper-resistant and offers a high level of confidence that a message was authentically sent by the claimed sender. This protects both the corporate client and the bank.
Generating a Private Key
During onboarding, you will be asked to generate an Elliptic Curve Digital Signature Algorithm (ECDSA) P-256 keypair, keeping the private key safe and exchanging the public key with us. Likewise, we will sign messages originating from the bank with our private signing key and share our public verification key with you. You may generate an ES256 keypair using openssl, as depicted below. ES256 is the only currently supported algorithm, since it has wide library support.
Generate an ES256 keypair:
❯ openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
❯ openssl pkcs8 -topk8 -inform pem -in private-key.pem -outform pem -nocrypt -out private-key-pkcs8.pem
❯ cat private-key-pkcs8.pem
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRdJ0ile3l4JLyTnL
iRhuQwN3PR3BNUviPEebDYMCFXWhRANCAASeKOVbubNj5Z7Uw5QIciUAS3ORosM1
y5yUBZSSoGsBqaIeXANVoU5gp2ioEiLfdRMerLhjb1YRYrcd1ZoSHbre
-----END PRIVATE KEY-----
❯ openssl ec -in private-key-pkcs8.pem -pubout > public-key.pem
read EC key
writing EC key
❯ cat public-key.pem
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnijlW7mzY+We1MOUCHIlAEtzkaLD
NcuclAWUkqBrAamiHlwDVaFOYKdoqBIi33UTHqy4Y29WEWK3HdWaEh263g==
-----END PUBLIC KEY-----Message Content Digest
Any HTTP messages (request and responses) that contain a message body must include an SHA-256 cryptographic hash of the binary data of the message body in the electrum.co.za/content-digest JWT claim. This is included in the JWT payload section and is then signed along with the other header and payload contents of the JWT.
The digest should be calculated using the binary representation of the HTTP message body as it will be sent in the HTTP session, but before any transfer encoding such as chunking or compression is applied. The HEX encoded digest should be placed in the electrum.co.za/content-digest claim.
HTTP messages (requests and responses) that do not contain a message body should not include the electrum.co.za/content-digest claim.
In order to verify a JWT correctly care should be taken to verify the electrum.co.za/content-digest digest claim to ensure it matches the HTTP message content to assert that the message content was not altered from when it was signed by the sender.
JWT Construction
A JWT should be constructed in accordance with RFC7519 with the following fields and values:
JOSE Header
| Parameter | Name | Value |
|---|---|---|
| alg | Algorithm | Must be set to ES256. Take note that when verifying a JWT you must not rely on this field to determine the algorithm. See JWT Best Practices section 2.1 for more detail. |
| typ | Type | This is an optional parameter which, if set, must be set to JWT. |
| kid | Key ID | Set to the relevant key id value that was shared during key exchange. |
We recommend that your implementation supports the configuration of multiple keys at once (selected by the kid header) in order to simplify operational effort during key rotation.
JWT Claims
| Claim | Name | Value |
|---|---|---|
| iss | Issuer | Will either be set to electrum.co.za for messages originating from the bank or should be set to the value communicated during onboarding for messages originating from you. |
| iat | Issued At | Set to the time (seconds from 1970-01-01T00:00:00Z) at which the JWT was issued. This value should be used when verifying the JWT to protect against replay attacks. Typically no more than 5 minutes before or after this time should be considered valid. |
| electrum.co.za/content-digest | Content Digest | Set to the HEX encoded SHA-256 message digest of the HTTP message body as described above. Should be omitted if the HTTP message does not contain a message body. |
OpenSSL JWT Generation Example
The following example is provided for descriptive purposes. In a real implementation it is expected that a suitable library is used to do most of the below processing. See JWT.IO for a list of JWT libraries.
Let's say you are trying to send the following HTTP message:
POST /say-hello HTTP/1.1
Host: service.internal.example
Date: Thu, 04 May 2023 08:54:09 GMT
Content-Type: application/json
Content-Length: 18
x-api-key: 7538f15b-475b-439c-a652-ca55725df933
{"hello": "world"}Step 1: Calculate the Content Digest
❯ echo -n '{"hello": "world"}' | openssl dgst -sha256
5f8f04f6a3a892aaabbddb6cf273894493773960d4a325b105fee46eef4304f1Step 2: Construct the JOSE Header
{
"kid": "6dcbaaa9-e66a-46a0-b6f0-44b166423995",
"typ": "JWT",
"alg": "ES256"
}Note that the kid value identifies the key and would have been previously shared during key exchange.
Step 3: Compact and Base64 URL Encode the JOSE Header
❯ echo -n '{"kid":"6dcbaaa9-e66a-46a0-b6f0-44b166423995","typ":"JWT","alg":"ES256"}' | base64 | sed s/\+/-/ | sed -E s/=+$//
eyJraWQiOiI2ZGNiYWFhOS1lNjZhLTQ2YTAtYjZmMC00NGIxNjY0MjM5OTUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9Step 4: Construct the JWT Claims
{
"iss": "electrum.co.za",
"iat": 1683190449,
"electrum.co.za/content-digest": "5f8f04f6a3a892aaabbddb6cf273894493773960d4a325b105fee46eef4304f1"
}Step 5: Compact and Base64 URL Encode the JWT Claims
❯ echo -n '{"iss":"electrum.co.za","iat":1683190449,"electrum.co.za/content-digest":"5f8f04f6a3a892aaabbddb6cf273894493773960d4a325b105fee46eef4304f1"}' | base64 | sed s/\+/-/ | sed -E s/=+$//
eyJpc3MiOiJlbGVjdHJ1bS5jby56YSIsImlhdCI6MTY4MzE5MDQ0OSwiZWxlY3RydW0uY28uemEvY29udGVudC1kaWdlc3QiOiI1ZjhmMDRmNmEzYTg5MmFhYWJiZGRiNmNmMjczODk0NDkzNzczOTYwZDRhMzI1YjEwNWZlZTQ2ZWVmNDMwNGYxIn0Step 6: Generate the ECDSA Signature
Concatenate Base64URL(header).Base64URL(payload) and save this in a file named signing-payload.txt:
❯ echo -n "eyJraWQiOiI2ZGNiYWFhOS1lNjZhLTQ2YTAtYjZmMC00NGIxNjY0MjM5OTUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlbGVjdHJ1bS5jby56YSIsImlhdCI6MTY4MzE5MDQ0OSwiZWxlY3RydW0uY28uemEvY29udGVudC1kaWdlc3QiOiI1ZjhmMDRmNmEzYTg5MmFhYWJiZGRiNmNmMjczODk0NDkzNzczOTYwZDRhMzI1YjEwNWZlZTQ2ZWVmNDMwNGYxIn0" > signing-payload.txtGenerate the signature across the signing payload and store the result in a file named signature.bin:
❯ openssl dgst -sha256 -sign private-key-pkcs8.pem -out signature.bin signing-payload.txtUnfortunately openssl calculates the signature in DER format and JWS requires it in r || s format, necessitating the following conversion:
❯ openssl asn1parse -in signature.bin -inform DER > asn1
cat asn1 | perl -n -e'/INTEGER :([0-9A-Z]*)$/ && print $1' > signature.hex
cat signature.hex | xxd -p -r | base64 | tr -d '\n=' | tr -- '+/' '-_' > signature.sigThe signature is now stored in signature.sig:
❯ cat signature.sig
y2eyfivi8eSN-X215HgJh1HaJXk3A8WciMjgHGXeCVxEGi39HKahV1KLiQDJ1wNf8O-9RUDbp56PUyMHQtOkwAStep 7: Form the Signed JWT
The full JWT then is: Base64URL(header).Base64URL(payload).Base64URL(signature):
eyJraWQiOiI2ZGNiYWFhOS1lNjZhLTQ2YTAtYjZmMC00NGIxNjY0MjM5OTUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlbGVjdHJ1bS5jby56YSIsImlhdCI6MTY4MzE5MDQ0OSwiZWxlY3RydW0uY28uemEvY29udGVudC1kaWdlc3QiOiI1ZjhmMDRmNmEzYTg5MmFhYWJiZGRiNmNmMjczODk0NDkzNzczOTYwZDRhMzI1YjEwNWZlZTQ2ZWVmNDMwNGYxIn0.y2eyfivi8eSN-X215HgJh1HaJXk3A8WciMjgHGXeCVxEGi39HKahV1KLiQDJ1wNf8O-9RUDbp56PUyMHQtOkwAStep 8: Send the Message
Finally, we can send our message with the x-jwt-signature header included:
POST /say-hello HTTP/1.1
Host: service.internal.example
Date: Thu, 04 May 2023 08:54:09 GMT
Content-Type: application/json
Content-Length: 18
x-api-key: 7538f15b-475b-439c-a652-ca55725df933
x-jwt-signature: eyJraWQiOiI2ZGNiYWFhOS1lNjZhLTQ2YTAtYjZmMC00NGIxNjY0MjM5OTUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlbGVjdHJ1bS5jby56YSIsImlhdCI6MTY4MzE5MDQ0OSwiZWxlY3RydW0uY28uemEvY29udGVudC1kaWdlc3QiOiI1ZjhmMDRmNmEzYTg5MmFhYWJiZGRiNmNmMjczODk0NDkzNzczOTYwZDRhMzI1YjEwNWZlZTQ2ZWVmNDMwNGYxIn0.y2eyfivi8eSN-X215HgJh1HaJXk3A8WciMjgHGXeCVxEGi39HKahV1KLiQDJ1wNf8O-9RUDbp56PUyMHQtOkwA
{"hello": "world"}Auth0 Java library Verification Example
Let's verify the JWT we created above with the Auth0 Java JWT library. Note that this example is purely for descriptive value and is not suitable for production use as it omits any error handling.
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.interfaces.ECPublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Verification;
public class SignatureVerify {
private static final String publicKeyData =
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnijlW7mzY+We1MOUCHIlAEtzkaLDNcuclAWUkqBrAamiHlwDVaFOYKdoqBIi33UTHqy4Y29WEWK3HdWaEh263g==";
public static void main(String[] args) throws Exception {
String jwtHeader =
"eyJraWQiOiI2ZGNiYWFhOS1lNjZhLTQ2YTAtYjZmMC00NGIxNjY0MjM5OTUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlbGVjdHJ1bS5jby56YSIsImlhdCI6MTY4MzE5MDQ0OSwiZWxlY3RydW0uY28uemEvY29udGVudC1kaWdlc3QiOiI1ZjhmMDRmNmEzYTg5MmFhYWJiZGRiNmNmMjczODk0NDkzNzczOTYwZDRhMzI1YjEwNWZlZTQ2ZWVmNDMwNGYxIn0.y2eyfivi8eSN-X215HgJh1HaJXk3A8WciMjgHGXeCVxEGi39HKahV1KLiQDJ1wNf8O-9RUDbp56PUyMHQtOkwA";
String httpBody = "{\"hello\": \"world\"}";
System.out.printf("JWT is valid: %b", verifyJwt(jwtHeader, httpBody));
}
private static boolean verifyJwt(String jwtHeader, String httpBody) throws Exception {
Algorithm algorithm = Algorithm.ECDSA256(getPublicKey(), null);
Verification verification = JWT.require(algorithm);
DecodedJWT jwt = verification.build().verify(jwtHeader);
return calculateDigest(httpBody).equals(jwt.getClaim("electrum.co.za/content-digest").asString());
}
private static ECPublicKey getPublicKey() throws Exception {
KeyFactory kf = KeyFactory.getInstance("EC");
byte[] publicBytes = Base64.decodeBase64(publicKeyData);
EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicBytes);
return (ECPublicKey) kf.generatePublic(publicSpec);
}
private static String calculateDigest(String value) throws Exception {
MessageDigest digester = MessageDigest.getInstance("SHA-256");
byte[] digestBytes = digester.digest(value.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(digestBytes);
}
}