Salesforce integrations can be tricky every so often. For example, one of our customers had to connect a system that required the use of JSON Web Tokens with an RSA-SHA512 signature.

As of writing, Crypto doesn’t support RSA-SHA512 signatures.
(Now supported thanks to Summer ’20 release! Stronger hashing algorithms in Apex)

One workaround is 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.


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 using electronic mail 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:

HexTypeHexLength
30Sequence82025C﹡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:

HexTypeHexLengthHexValue
02Integer011 byte00Version: V
02Integer8181﹡129 bytes00A92302…RSA modulus: N
02Integer033 bytes010001RSA public exponent: E
02Integer8181﹡129 bytes00A2CC60…RSA private exponent: D
02Integer4165 bytes00DFD036…Prime1: P
02Integer4165 bytes00C175DA…Prime2: Q
02Integer4064 bytes7EC1D0A9…Dp Exponent1: D mod (P-1)
02Integer4064 bytes4FF87AE9…Dq Exponent2: D mod (Q-1)
02Integer4064 bytes1BA12159…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)

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:

Related Posts

Last modified: 29th September 2020

Comments

Write a Reply or Comment

Your email address will not be published.