Guru: A Faster Way To Sign A JWT
July 21, 2025 Chris Ringer
In my prior article, which you can read here, I discussed using the OpenSSL tool to asymmetrically sign a JWT. I like OpenSSL because it’s an open source solution available on many platforms and easy to use. But the decision to use OpenSSL should be coupled with the performance level expectations (the PLA) for your project.
For instance, how many tokens will you need to create in a given interval? Will your newly minted tokens expire 24 hours from now or in 5 minutes? In my case currently, only about 5 JWTs are needed per day so OpenSSL works great. But know that using OpenSSL through QShell may be slower than calling an equivalent IBM API directly. If you’re coding for performance to asymmetrically sign dozens of JWTs per second, you should take a look at the IBM API Qc3CalculateSignature.
A Little Privacy Please
Firstly, for my example code, we need to generate a private encryption key using the universal RSA algorithm. From a green screen, run these commands.
01 strqsh 02 cd /tmp 03 openssl genrsa -out ringer-private-key.pem 2048 04 ls -lS ringer-private-key.pem 05 cat ringer-private-key.pem
Example 1
Lines 01 and 02: Start a QShell session and switch to the /tmp directory.
Line 03: Create a private RSA key file with key length 2048 bits (256 bytes).
Line 04: View some attributes of the new private key file. Notice the CCSID is 1208 (UTF-8 encoded) and the file size is 1704 bytes.

Figure 1
Line 05: View the private key file. Here is a snippet of my file:

Figure 2
The Definitions
Let’s jump right into a coding example starting with the definitions.
01 **Free 02 ctl-opt DftActGrp(*No) Copyright('(C) Chris Ringer'); 03 dcl-pr CalculateSignature ExtProc('Qc3CalculateSignature'); 04 iInputData pointer value; 05 iInputDataLen int(10) const; 06 iInputDataFmt char(8) const; 07 iAlgorithm pointer value; 08 iAlgorithmFmt char(8) const; 09 iKey pointer value; 10 iKeyFmt char(8) const; 11 iCryptoService char(1) const; 12 iCryptoDevName char(10) const; 13 oSignature pointer value; 14 iSignatureMaxLen int(10) const; 15 oSignatureRetLen int(10); 16 ioErrorCode likeds(ioStdError); 17 end-pr; 18 dcl-ds iAlgd0400_ds qualified; 19 cipher int(10) inz(50); 20 blockFmt char(1) inz('1'); 21 reserved char(3) inz(*ALLX'00'); 22 hashAlg int(10) inz(3); 23 end-ds; 24 dcl-ds iKeyd0200_ds qualified; 25 keyType int(10) inz(51); 26 keyLen int(10); 27 keyFmt char(1) inz('1'); 28 reserved char(3) inz(*ALLX'00'); 29 key char(5000) ccsid(*hex); 30 end-ds; 31 dcl-s PrivateKeyFile varChar(200); 32 dcl-s PrivateKey varChar(5000) ccsid(*hex); 33 dcl-s InputData varChar(1000) ccsid(*utf8); 34 dcl-s signature varchar(512) ccsid(*utf8); 35 dcl-s signatureLen int(10); 36 dcl-s signatureB64 varchar(1000) ccsid(*utf8); 37 dcl-s signatureEbc varchar(1000); 38 /Include QSYSINC/QRPGLESRC,QUSEC 39 dcl-ds ioStdError qualified; 40 QUSEC likeDs(QUSEC); 41 end-ds;
Example 2
Lines 01 and 02: This coding example is fully free-form code and includes some compile options.
Line 03: The IBM Qc3CalculateSignature API prototype. And I like to prefix my prototype parameters with i=input, o=output, io=both input and output to make the code more readable. Open that page in a new tab and compare the parameter options there to the definitions here.
Line 04: This is the input string we want to sign. The encoding will be ASCII (UTF-8). I will be signing ‘Hello World’ but this normally would be your JWT header.payload string.
Line 05: The length of the input string.
Line 06: The input string format. The literal “DATA0100” just means to sign the input string.
Line 07: The algorithm literal “ALGD0400” is used to sign a string.
Line 08: Tells the API that we are passing in the ALGD0400 data structure (see lines 18 to 23).
Line 09: The private key. We will read the private key (figure 1) from the IFS.
Line 10: The KEYD0200 data structure defines our private key format (see lines 24 to 30).
Line 11: The cryptographic Service provider. A value 0 means the system will pick one for us.
Line 12: The cryptographic device name will be blank.
Line 13: The binary signature returned by the API.
Line 14: The maximum length of the signature variable. The API won’t fill a return value beyond this length.
Line 15: The actual returned length of the signature generated.
Line 16: The standard API error data structure.
Line 17: End of the prototype.
Line 18: Here we define the algorithm data structure, in our case ALGD0400.
Line 19: Our cipher RSA is indicated with value 50. Remember earlier we generated an RSA private key (example
1 line 03).
Line 20: The block format is ‘1’. ‘0’ (the only other choice) is less secure and deprecated.
Line 21: Reserved space in the algorithm.
Line 22: The hash algorithm (AKA message digest) is SHA-256 (256 bits which is 32 bytes) indicated with value 3.
Line 23: Ends the ALGD0400 data structure definition.
Line 24: Here we define the key data structure for the KEYD0200 format.
Line 25: The key type is RSA so value 51.
Line 26: The key length (the bytes size of our private key file – see figure 1).
Line 27: The only key format choice is ‘1’ which is the mysterious “BER encoded PKCS #8” format. We will discuss this more below.
Line 28: Reserved space in the data structure.
Line 29: Our raw unconverted private key.
Line 30: Ends the KEYD0200 data structure definition.
Line 31: The full path and name of our private key file.
Line 32: A variable to hold the contents of the private key file. Notice the CCSID *hex means do not translate the characters. So our private key will be read in from the IFS stream file unconverted and remain ASCII (CCSID 1208 from figure 1).
Line 33: Our ASCII input string to be signed by the API.
Line 34: The signed signature returned by the API.
Line 35: The length of the signed signature returned by the API.
Line 36: The signature encoded as base64, still ASCII.
Line 37: The base64 signature as EBCDIC.
Lines 38 to 41: The standard error data structure used in IBM API calls.
The Mainline Code
Now we’ll call the Qc3CalculateSignature API to sign our string.
01 Exec SQL Set Option Commit=*NONE, Naming=*SYS; 02 PrivateKeyFile = '/tmp/ringer-private-key.pem'; 03 Exec SQL Select LINE Into :PrivateKey From Table(QSYS2.IFS_READ_BINARY( PATH_NAME => :PrivateKeyFile, END_OF_LINE => 'NONE', MAXIMUM_LINE_LENGTH => 5000)); 04 keyd0200_ds.key = PrivateKey; 05 keyd0200_ds.keyLen = %Len(PrivateKey); 06 InputData = 'Hello World'; 07 %len(signature) = %len(signature:*MAX); 08 ioStdError.QUSEC.QUSBPRV = %size(ioStdError); 09 CalculateSignature( %addr(InputData : *data) : %len(InputData) : 'DATA0100' : %addr(algd0400_ds) :'ALGD0400' : %addr(keyd0200_ds) :'KEYD0200' : '0' : ' ' : %addr(signature : *data) : %len(signature) : signatureLen : // 256 bytes for 2048 bit key ioStdError); 10 signatureEbc = ''; 11 If (ioStdError.QUSEC.QUSBAVL = 0); // no errors 12 signatureB64 = %subst(signature:1:signatureLen); 13 exec sql set :signatureB64 = BASE64_ENCODE(:signatureB64); 14 signatureEbc = signatureB64; 15 signatureEbc = %xlate('+/':'-_':%trimR(signatureEbc:'=')); 16 EndIf; 17 *InLR = *On; 18 Return;
Example 3
Line 01: Add your favorite SQL pre-compiler options here.
Line 02: I just hardcoded the name of the private key stream file.
Line 03: Reads in the private key stream file into a binary variable.
Lines 04 and 05: Sets the key and key length in the KEYD0200 data structure. The key length will be 1704 (figure 1).
Line 06: Our simple input string to sign with the private key. Remember, this is ASCII not EBCDIC.
Line 07: Reserves memory for the maximum length of our signature variable (512 bytes). I am only expecting 256 bytes (2048 bits). Someday the private key size might increase to 4096 bits.
Line 08: Sets the length of our error data structure.
Line 09: Calls the API. Review the prototype to see which parameters are input/output/both.
Line 10: Initializes the EBCDIC base64 signature to an empty string.
Line 11: Check the returned error data structure for an error, hopefully none.
Line 12: Extracts the signature from the returned value, as binary.
Line 13: Converts the binary signature to ASCII base64.
Line 14: Converts the ASCII base64 signature to EBCDIC. Why even bother with EBCDIC? An HTTP client tool running on the IBM i may expect the HTTP request payload to be EBCDIC and will convert it to ASCII for us.
Line 15: Trims any trailing = sign padding and makes the base64 URL safe.
Line 16: End the If statement.
Lines 17 and 18: Exit the program.
“I don’t think so, Tim”
When you compile this code and debug it, you will realize something rather unfortunate. It doesn’t work! I searched the web for the message ID CPF9DDB (“The key string or Diffie-Hellman parameter string is not valid”) returned by the API and didn’t find much (but you will now). If you look at the description of the key format parameter (example 2 line 27), it states “BER encoded PKCS #8 format”. A quick web search reveals that “BER encoded” means binary, not base64. I found a way to encode our base64 private key as DER but not as BER. A little further research reveals that DER is actually a subset of BER so I decided to try that. Run these commands to encode the private key from the PEM to the DER format.
01 strqsh 02 cd /tmp 03 openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in ringer-private-key.pem -out ringer-private-key.der 04 ls -lS ringer-private-key.* 05 F3 to exit QShell 06 dspf '/tmp/ringer-private-key.der'
Example 4
Lines 01 and 02: Start QShell and change to the tmp directory.
Line 03: Creates a private key encoded as DER from our PEM base64 format.
Line 04: View some attributes of the private key files. Notice the DER binary file size is smaller than the PEM base64 version.

Figure 3
Line 05: Exit QShell back to a command line.
Line 06: “View” the binary private key file. Here is a snippet of the file. This gibberish means binary.

Figure 4
Now replace the private key file name in example 3 line 02, recompile and debug. Eureka! Now the API returns a signature.
HdkxXksUVPt6ul6RJU1mwnyjGJcOG7ZNdaP2QvobAjRZfGborIC6f8ogPjOM 1-owtjhcomACok7oEleouMoegcckRnHBDjZadDhN5pZ1ej8mcKRmH8o_2xQR jBT8FPnFWbaoIoZRjvkWxoDOVt1V3uQgvtuWeQQ5Tw9vfm-XwNsW9LxZtXZ2 N6nbz2eHyE3xHfPdNHUiLUB5XtjqoEPYyzk4XnHu2rZ361P_3yf3SiaJ9Kcu 1Hem98vikJKDQMjY40rr89QNaM8LDKcXMqM3pXIv8ZBfjP-N3wNs5wrmXMWd eC91sTF8kVdL0wB5VsVGCPRXboZoN7aM1xVGvbkVAA
Figure 5
Trust But Verify
To verify our coding is in fact working correctly, let’s generate a signature with OpenSSL using the same private key and input string. Copy this command and run it.
STRQSH CMD('cd ''/tmp'' && printf "%s" "Hello World" | openssl dgst -sha256 -binary -sign "ringer-private-key.der" -out "ringer-HelloWorld-signed.bin" && openssl base64 -e -A -in ringer-HelloWorld-signed.bin | tr ''+/'' ''-_'' | tr -d ''=''')
Example 5
And you will see the generated OpenSSL signature matches the API signature (figure 5 vs figure 6). By the way, if you run that same command but with the PEM file, you’ll also get the same signature. It’s too bad the API isn’t intelligent enough to automagically handle the various private key formats.
HdkxXksUVPt6ul6RJU1mwnyjGJcOG7ZNdaP2QvobAjRZfGborIC6f8ogPjOM 1-owtjhcomACok7oEleouMoegcckRnHBDjZadDhN5pZ1ej8mcKRmH8o_2xQR jBT8FPnFWbaoIoZRjvkWxoDOVt1V3uQgvtuWeQQ5Tw9vfm-XwNsW9LxZtXZ2 N6nbz2eHyE3xHfPdNHUiLUB5XtjqoEPYyzk4XnHu2rZ361P_3yf3SiaJ9Kcu 1Hem98vikJKDQMjY40rr89QNaM8LDKcXMqM3pXIv8ZBfjP-N3wNs5wrmXMWd eC91sTF8kVdL0wB5VsVGCPRXboZoN7aM1xVGvbkVAA Press ENTER to end terminal session.
Figure 6
Performance
As expected, the API is faster than OpenSSL but how much? For my test results, it depends if we’re talking about the first call in a given job or subsequent calls in that same job. Look at figure 7. For OpenSSL, the 1st -vs- next call average run times were about the same, around 0.3 seconds. But for the API, the performance difference was significant, .05 vs .0025 seconds.

Figure 7
This means If you submit a job to batch and it signs one JWT and ends, you won’t see a noticeable performance gain for OpenSSL vs the API. But if that batch job spins in a loop to sign several JWTs, the performance gain could be considerable.
Outro
Out of curiosity I asked an AI tool to generate sample RPG code and I was not impressed. The code had several issues and was completely void of ASCII variables. I iteratively instructed the AI tool to fix the bugs but I abandoned it after a few tries. I think we’re still a few years away from an AI tool generating code as cleanly a developer in a chair
Lastly, I’m the first to admit that I’m not a cryptography expert. When such a ticket is assigned to me, I try to dig in and figure it out, especially when a due date is looming. For this particular API, over 200 parameter combinations are possible but only a few combinations are valid. I hope this API coding example is helpful for you.
I hope this tip is helpful for you. Until next time, keep coding!
Chris Ringer began coding RPG programs in 1989, and after a recent unexpected but valuable detour to C# is happy to be back in the IBM world. In his spare time he enjoys cycling and running – and taking the family dog Eddie for walks.
RELATED STORIES
Guru: Web Concepts For The RPG Developer, Part 4
Guru: Web Concepts For The RPG Developer, Part 3
Guru: Web Concepts For The RPG Developer, Part 2
Guru: Web Concepts For The RPG Developer, Part 1
Guru: The PHP Path To Victory, Part 1