Salesforce integrations can be tricky. One of our customers had to connect a system that required the use of JSON Web Tokens with an RSA-SHA512 signature.

One workaround was to implement the RSA cryptography in Apex. Let's see how the Apex language enables us to express a wide variety of solutions, even at the bare metal if needed.

Update: the platform Crypto class now supports RSA-SHA512 signatures
You should use native methods as of the Summer '20 release - Stronger hashing algorithms in Apex

At its core, RSA (invented by Rivest / Shamir / Adleman) is a cryptosystem based on a mathematical trick and involves several moving parts. Apex source code on GitHub.

  1. A private key file holds secret prime numbers
  2. An ASN key reader handles the binary file format
  3. The message to sign consists of a JSON Web Token
  4. Then a SHA512 hash reduces the message to a big integer
  5. Finally the RSA signer executes the modPow signature math

To succeed, we must implement each logical part in Apex then make it performant. A similar look and feel to the platform Crypto class can be achieved starting with pseudocode like this:

Blob sign(Blob inputMessage, String privateKeyFile)
{
    ASN reader = new ASN(privateKeyFile);
    RSA signer = new RSA(reader.values());
    return signer.sign(SHA512(inputMessage));
}

Step 1) Private Key File

The signing process always starts with an RSA Private Key. For example, this RSA tool generates a binary representation of a private key wrapped in a header / footer. This is called a PEM file and it is base64 encoded to simplify transport by email or your clipboard:

-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCpIwKH7fYUgtCtOCtnvIlAXcFvzLPkLWWHNZoT+hOIzT
WR+PrOQiQrat7wsVD+161g3CUUcdpTR2HMSThWxEjLldsZq0BA3TqEecXm
V6rZzy0ZTJb2tgzjoX+eNiLvIHpgAaq5x2g2J9mB/b/9PFWspvQgMPAXl6
rpAM5ZNIEwuQIDAQABAoGBAKLMYP5HbMo3U/a3Dwhtr8p1s+ARr8FcdNId
JO4/khfmNb8IYRixDzF/T5FriyOQo4CMxWAVamkoVxkUDRdvHQTO17hWMK
ysLIn46y3bHxxRb6g5tpSwK61L25Hdiwi9GjqGrn7Z5rMFIqOqaCIX9gUQ
ICmW31Y7XyekZ9RPA+8VAkEA39A2eqystpFTtkvfrAgG7uOL4IrckA5kuC
4xPHeehvlI8bDZU4kmLFNnduNOVtfKSxzfT4l+Qvl8IRxUycRodwJBAMF1
2s9NvILcJ1i2Ah2oTf91gkv82XoNOr0DsdLMqRkNSODKWiqxwZTVq8OeiW
wF58FzdBmNJegVfoE2eO0zrE8CQH7B0KkHtMWtZwjeze4DmdGgQ+9HFgXs
cPSzDKWfZcQx2TMxItSh32HJVtbJg+vBSUvjLUJBr6XE4J1sC0U+nJ8CQE
/4eunk1X82qGEoY7mEwDFQjvsAW5nzbAuEQnbEOUZs0mpx21H4xu/SX71u
hJoN2t6B7kU9rqTAddnN/bD4AksCQBuhIVld31iTzgyrc4R9e+KnLuW0rP
/01SWYPYE0oeYCViZ/r19XsHQicPFLjKtKoOLNhVlHGsrW0yCnpcb2ahs=
-----END RSA PRIVATE KEY-----

To work with individual bytes, convert the data into hexadecimals:

String b64 = 'MIICXAIB...';
Blob binary = EncodingUtil.base64decode(b64);
String hex = EncodingUtil.convertToHex(binary);

Now with each byte seen as a hex pair, structure emerges:

3082025C02010002818100A9230287EDF61482D0AD382B67BC89405DC16FCCB3E42D6587359A
13FA1388CD3591F8FACE42242B6ADEF0B150FED7AD60DC251471DA534761CC493856C448CB95
DB19AB4040DD3A8479C5E657AAD9CF2D194C96F6B60CE3A17F9E3622EF207A6001AAB9C76836
27D981FDBFFD3C55ACA6F42030F01797AAE900CE59348130B9020301000102818100A2CC60FE
476CCA3753F6B70F086DAFCA75B3E011AFC15C74D21D24EE3F9217E635BF086118B10F317F4F
916B8B2390A3808CC560156A69285719140D176F1D04CED7B85630ACAC2C89F8EB2DDB1F1C51
6FA839B694B02BAD4BDB91DD8B08BD1A3A86AE7ED9E6B30522A3AA682217F60510202996DF56
3B5F27A467D44F03EF15024100DFD0367AACACB69153B64BDFAC0806EEE38BE08ADC900E64B8
2E313C779E86F948F1B0D95389262C536776E34E56D7CA4B1CDF4F897E42F97C211C54C9C468
77024100C175DACF4DBC82DC2758B6021DA84DFF75824BFCD97A0D3ABD03B1D2CCA9190D48E0
CA5A2AB1C194D5ABC39E896C05E7C17374198D25E8157E813678ED33AC4F02407EC1D0A907B4
C5AD6708DECDEE0399D1A043EF471605EC70F4B30CA59F65C431D9333122D4A1DF61C956D6C9
83EBC1494BE32D4241AFA5C4E09D6C0B453E9C9F02404FF87AE9E4D57F36A8612863B984C031
508EFB005B99F36C0B844276C439466CD26A71DB51F8C6EFD25FBD6E849A0DDADE81EE453DAE
A4C075D9CDFDB0F8024B02401BA121595DDF5893CE0CAB73847D7BE2A72EE5B4ACFFF4D52598
3D8134A1E60256267FAF5F57B0742270F14B8CAB4AA0E2CD8559471ACAD6D320A7A5C6F66A1B

Step 2) Abstract Syntax Notation - ASN.cls

The private key file is not just a random seed. It holds 9 pieces of content in Tag-Length-Value encoding per Abstract Syntax Notation One. Reading each hex pair (or byte) as a string, Apex will extract the critical prime numbers (P, Q, etc) described in the RSA specification.

  • Tag byte
  • Length byte
  • Contents bytes

The first tag tells us the data holds a list of values called a sequence:

Hex Type Hex Length
30 Sequence 82025C﹡ 604 bytes
﹡When content length exceeds 128 bytes (hex 80) the length of the length is also given.
Here, hex 82 means a 2-byte length, then 025C means the sequence is 604 bytes long.

Let's tabulate all the subsequent values contained inside the sequence:

Hex Type Hex Length Hex Value
02 Integer 01 1 byte 00 Version: V
02 Integer 8181﹡ 129 bytes 00A92302… RSA modulus: N
02 Integer 03 3 bytes 010001 RSA public exponent: E
02 Integer 8181﹡ 129 bytes 00A2CC60… RSA private exponent: D
02 Integer 41 65 bytes 00DFD036… Prime1: P
02 Integer 41 65 bytes 00C175DA… Prime2: Q
02 Integer 40 64 bytes 7EC1D0A9… Dp Exponent1: D mod (P-1)
02 Integer 40 64 bytes 4FF87AE9… Dq Exponent2: D mod (Q-1)
02 Integer 40 64 bytes 1BA12159… Coefficient: Qinv mod P

﹡When content length exceeds 128 bytes (hex 80) the length of the length is also given.
Here, hex 81 means a 1-byte length, then 81 means the integer is 129 bytes long. Not bits.


Step 3) JWT Message - Tutorial

JSON Web Tokens exist to prove the integrity of an API request: only the private key holder can sign tokens and issue valid requests. Each token consists of three parts: Header / Payload / Signature. The special values are covered in depth in the tutorial.

Combine the Header and Payload to prepare the message:

String header = '{"alg":"RS512","typ":"JWT"}';
String payload = '{"iat":1581009850,"exp":1581011650}';
String message = base64url(header) + '.' + base64url(payload);

Note the base64 URL variant strips trailing = padding, swaps + for -, and / for _ according to the JWT spec. This avoids issues if intermediate systems use tokens as filenames.


Step 4) Hash Function - SHA512.cls

Hashing the message ensures the input to the signature math is a predictable length. Else the signature math would become more and more expensive with each byte of data in the token.

Blob hashedMessage = Crypto.generateDigest('SHA-512', Blob.valueOf(message));

Step 5) Signature - RSA.cls

The original concept of modular exponentiation underlying RSA was described in 1977 and (assuming small prime numbers) can be executed with pen and paper by hand in 10 minutes:

Signature = CD mod P×Q (for message C, private exponent D, primes P and Q)

Eddie Woo - The RSA Encryption Algorithm

In real situations, this modular exponentiation gets computationally expensive and exceeds the Apex CPU limit without using a specific optimization: the Chinese Remainder Theorem.

It isn't the end of the road. Most crypto libraries use the CRT optimization. In fact, its use is so common that ALL private keys hold 5 extra values, precomputed to help implement CRT. The implementation in Apex can be seen as a direct parallel of the algorithm from Wikipedia:

Chinese remainder theorem (Wikipedia) Apex implementation in RSA.cls
m1 = CDp mod P
m2 = CDq mod Q
h = Qinv (m1 - m2) mod P
m = m2 + HQ mod PQ
M1 = C.powMod(Dp, P);
M2 = C.powMod(Dq, Q);
H = Qinv.multiply(M1.subtract(M2)).mod(P);
M =  M2.add(H.multiply(Q).mod(P.multiply(Q)));

Big Integer math class - BigInt.cls

All the aforementioned math must work with big integers. The calculations must be exact and this is where the real challenge lies. Back in Step 2 you probably spotted the 129-byte value in the private key. That number has 300+ digits while the maximum length of any number in Apex is 19 digits. This isn't a shortcoming of Apex - most languages have the same constraint. An extra class handles the big integers, representing them as lists of smaller integer primitives.


Acknowledgements

This solution stands of the shoulders of a number of people who provided reference implementations and ideas. We wish to express appreciation for their published work: