With other words:
How to
verify a digital signature
in Node.js
SAP Cloud Integration (CPI) provides functionality to automatically sign a message with a digital signature using the
Simple Signer.
This blog post explains how to verify such signature with Node.js in an HTTP receiver.
In a tutorial, we use an iFlow that calls a Node.js server app which does the signature verification.
Quicklinks:
Quick Guide
Sample Project
Content
0. Prerequisites
1. Introduction
2. Tutorial
2.1. Create Node.js App
2.2. Create Key Pair
2.3. Create iFlow
2.4. Run
3. Optional: How to find supported Algorithms
Appendix 1: Sample Code
Appendix 2: Algorithms supported by Node.js
0. Prerequisites
To follow this tutorial, access to a Cloud Integration tenant is required, as well as basic knowledge about creating iFlows.
You should be familiar with Node.js, even though the tutorial can be followed without local Node.js
installation.
The
Simple Signer Blog Post explains the basics about digital signatures and how to create them in CPI.
The previous blog post showed how to verify a signature with Groovy and it contains a
little recap about digital signatures.
For remaining open questions I recommend the
Security Glossary.
1. Introduction
In the previous blog post we introduced the
Simple Signer which is a little tool that allows to easily create a digital signature.
Little recap:
It is an iFlow step which can be placed in an iFlow, in order to create a digital signature of the message content.
The created signature is placed in a header.
This header has a default name “SAPSimpleSignatureValue” which can be configured in the property sheet.
To create the signature, a private key is required. As such, we need to create a Key Pair and upload it to the CPI Keystore dashboard.
Alternatively, the key pair can be generated in the dashboard itself.
In the configuration of the “Simple Signer” we choose the combination of 2 algorithms to be used for the digital signature, the default is “SHA512/RSA”.
Finally, in the documentation (and in my previous blog) we read that the signature is base64-encoded before storing it in the header.
So these are the components that are used to
create a digital signature:
- the actual content to be signed
- the algorithm to create the hash code
- the algorithm of they key pair, used to encrypt the hash code
- the private key to encrypt the hash code
- the Base64 encoding to avoid sending the signature in binary format.
- the header name for transporting the signature.
All this info and these artifacts are required for
verifying the signature.
As mentioned, in CPI we don’t have a tool to verify such signature.
That’s why I
posted a Groovy script to show how to verify manually in an iFlow.
In the present blog post, we showcase a scenario where an iFlow message is sent via HTTPS adapter to a receiver that is a Node.js application.
Required steps:
First, we need to prepare the POST request payload, to make sure that all required information – as listed above – is sent to the receiver app.
Afterwards, this node app has to verify the received message, before it can be trusted and processed further.
To verify the received message in the node.js code, a convenient
crypto package can be used.
To verify the received digital signature, the app needs:
- the actual content
- the hashing algorithm name
- the encryption algorithm name
- the public key for decryption
- the digital signature itself, base64-encoded
- the header name which contains the signature
Regarding algorithm names and public key, we have 2 possible approaches:
Implicit:
- hard-code the algorithm names in the code
- copy the public key into the app folder, deploy together
Outside-in:
- send the algorithm names in request headers
- send the public key from iFlow in a header, base64-encoded
In our tutorial, we’re using the second approach. It requires more steps, but is more realistic, thus more fun.
Note:
This approach is not a good idea for productive environment, because not safe.
2. Tutorial
In our tutorial, we create a Node.js server application that offers a REST endpoint for HTTP POST requests.
The implementation of this endpoint verifies the incoming digital signature.
It expects that the other required information including the public key are sent via request headers.
The simple endpoint does nothing else than the verification.
The app is deployed to Cloud Foundry
As next step we’re going to create an iFlow that uses the
Simple Signer to create a digital signature of a dummy message content.
In a Groovy script, we grab the public key and store it in a header, in order to send it to the receiver.
We use the HTTP adapter to send the message to our Node.js application.
After running the iFlow in CPI, we check the Cloud Foundry log and CPI log, to view the result of our little scenario.
2.1. Create Node.js Application
Our node app is meant to be simple and only show how to implement the verification of the signature.
The app itself is not secured, it doesn’t require authentication.
2.2.1. Create Project
To follow this tutorial, we create a project
digisigi which contains 3 application files:
C:\digisigi
manifest.yml
package.json
server.js
The full code can be copied from the
Appendix 1.
2.2.2. Create Application
Our app is a very simple server app based on
express like all other demo apps.
We don't require external libraries for the signature handling, as we can use the built-in
crypto package.
const crypto = require('crypto')
Our simple application offers just one REST endpoint
/process that can be called with an HTTP POST request.
app.post('/process', (req, res)=>{
The endpoint implementation expects that
- the request body contains the content to be verified and
- the headers contain all other information like signature, public key and algorithm infos.
const headers = req.headers
const content = req.body
const signature = headers.digisigi
const algorithmCombi = headers.digialgi
const publicKeyB64 = headers.publickey
As such, when calling this app endpoint from an iFlow, we need to make sure to send all required info accordingly.
The app uses the info to verify the digital signature and writes the result to the console:
const verificationResult = doVerification(content, signature, publicKeyB64, algorithmCombi)
console.log(`===> Result of digital signature verification : ${verificationResult}`);
If the verification is successful, the actual code of the REST endpoint could be executed.
In our dummy app, we just write a comment and end the process with status code 204, which means success and no response body:
if (verificationResult) {
// TODO : after verification of the signature, the app can continue processing the content
res.status(204).end()
} else {
res.status(404).send("Invalid content: digital signature verification failed.")
}
If the verification fails, we send an error message and status
404 bad request, because the request is invalid.
Now let’s have a look at the verification function:
function doVerification(content, signature, publicKeyB64, algorithmCombi){
const publicKey = `-----BEGIN PUBLIC KEY-----\n${publicKeyB64}\n-----END PUBLIC KEY-----`
const verifier = crypto.createVerify(algorithmCombi)
verifier.write(content)
verifier.end()
const result = verifier.verify(publicKey, signature, 'base64')
return result
}
The first line looks a bit odd:
const publicKey = `-----BEGIN PUBLIC KEY-----\n${publicKeyB64}\n-----END PUBLIC KEY-----`
What hack is going on here?
We have to anticipate that in CPI we’re getting hold of the public key by querying the CPI Keystore (via API).
We receive the public key in binary DER format.
We encode it with Base64 in order to safely transfer it over the internet in a header.
Now, in our Node.js app, we’re using the crypto library in order to do the verification.
The verification method requires a public key in PEM format.
PEM format basically means that the binary content is Base64-encoded and surrounded with hyphens and
BEGIN / END statements.
As we already have the key in Base64 encoding, we just need to surround it, to make it PEM-conform.
Obviously, it looks like a hack and not stable.
In productive environment, you would rather look out for a library (e.g.
node-forge) to do the job.
But I always try to not depend on third-party libraries in the demo code, as they tend to become obsolete or replaced as time goes by.
OK.
Afterwards, we initialize the verifier with:
algorithmCombi:
e.g.
RSA-SHA512
content:
The original content from the iFlow message, received in the request body of our app.
publicKey:
As mentioned, the public key of the sender who signed the content.
The key in PEM format.
const verifier = crypto.createVerify(algorithmCombi)
verifier.write(content)
verifier.end()
const result = verifier.verify(publicKey, signature, 'base64')
What is it about that third parameter: ‘base64’ ?
This parameter specifies the encoding of the signature, according to the
node docu.
This is required because the
Simple Signer in CPI will always base64-encode the signature.
The
verify() method returns a boolean, which we use to calculate the response of our REST endpoint, as mentioned above.
The full code can be found in the
Appendix.
My apologies for simple code.
2.2.3. Deploy
For deploying the app to Cloud Foundry, we can use a very simple manifest as no services are required (see
Appendix).
After deployment with
cf push, we need to get a hold of the app URL, as we will need it in the next chapter, to configure the iFlow HTTP adapter.
cf app digisigiapp
The output gives us the app URL to which we have to append the name of our endpoint.
In my example:
https://digisigiapp.cfapps.eu12.hana.ondemand.com/process
2.2. Create Key Pair
We let CPI generate a key pair for us.
This is done in the Keystore of
Cloud Integration.
Go to your CPI -> "Operations & Monitoring" -> "Manage Security" -> "Keystore"
Direct link:
<cpi>/itspaces/shell/monitoring/Keystore
Choose "Create" -> "Key Pair"
Enter some values of your choice, e.g. "iflowtonodekeys" and press “Create”.
Alias:
Used to refer to this key. Is required later, we can take a note of it, or try to remember (or follow my description).
Key Type:
The default is RSA, which is most widely used.
DSA is rather common in the context of digital signatures, because faster.
See my blog post
here for some info about algorithms.
Note that the signer and the verifier (groovy) need to be configured according to the choice that’s being taken here.
In our example, let's stick to RSA.
Key Size:
Larger Key Size increases the security. For our tutorial we can leave the default
2.3. Create iFlow
We create a simple iFlow that has the purpose of creating a digital signature with the Simple Signer and send it to our Node.js endpoint, which will do the verification.
We create a simple iFlow that does the following:
defines some hardcoded dummy text
signs it with Simple Signer
handles public key with groovy script
snds the message via HTTP adapter to our Node.js app
It looks like this:
We create an iFlow with the following elements:
Start Timer
set to “Run Once”.
Content Modifier
Message Body with some arbitrary text.
Create 2 new Headers:
Name: 'content-type' – Value: 'text/plain'
Name: 'digialgi' – Value: 'RSA-SHA512'
The 'digialgi' header will transport the signature algorithms to the node app.
We must write a string that exactly matches one of the algorithms supported by the 'crypto' library of Node.js.
See
here for the list of supported algorithms.
How can we find out which algorithms are supported by the Node.js lib?
See
here.
Furthermore, the algorithm that we specify in the header must match the algorithm which we choose in the next step.
Simple Signer
Private Key Alias name is "iflowtonodekeys".
Signature: "SHA512/RSA"
Note that the algorithm must match the key type chosen when creating the key pair (RSA).
Signature Header Name: digisigi
Groovy Script
Content copied from Appendix.
The script reads the key pair with alias "iflowtonodekeys" which we created in previous step and which we’ve configured in the Simple Signer step.
The key object is binary, so we encode it with Base64, to make it suitable for transmission in a header.
Finally, we store it in a message header with name
publickey.
Make sure to correctly type the name, as it must match the header name which we use in the Node.js app, to read the header.
HTTP Adapter
Address: here we enter the URL which we composed in the deploy chapter of our Node.js app.
In my example:
https://digisigiapp.cfapps.eu12.hana.ondemand.com/process
Request Headers: we can enter an asterisk (*), to send all headers to our REST endpoint.
In a productive environment, we would enter only the required header names.
Note:
Make sure to not forget this important setting, as otherwise our scenario would fail.
2.4. Run
Now we can deploy the iFlow, it will start and execute right after deployment.
As the server app is already deployed, the iFlow should complete successfully and we can go ahead and view the results.
2.4.1. Happy Path
All above configuration was made to get the happy result of the scenario.
We can also try some other possible combinations of algorithms.
Below table shows the names to be entered in the header of Content Modifier and to choose in the Simple Signer:
Header (node name) |
Signer (Java name) |
---|
ripemd160WithRSA |
RIPEMD160/RSA |
RSA-MD5 |
MD5/RSA |
These are just examples, to see the difference in the Java and Node notation.
2.4.1.1. Result in Cloud Foundry
To read the log statement that we write in our node app, we need to view the Cloud Foundry log:
cf logs digisigiapp --recent
The result is the expected success response of the verifier: “true”
2.4.1.2. Result in CPI
To view the response of our service in the CPI Monitor, we need to increase the log level.
We go to “Manage Integration Content” or
.../shell/monitoring/Artifacts
Change the “Log Configuration” to “Trace”, which is required to see the Message Content in the log.
Then redeploy the iFlow, to let the timer start again.
Afterwards, we can go to “Monitor Message Processing”
...shell/monitoring/Messages
Click on the "Trace" link
We select the last step, on top, and the “Message Content” tab and see the expected success status code:
2.4.2. Negative Test
To let the verification fail, we have several possibilities:
Modify the message content after signing it, e.g. additional Content Modifier .
Use different public key (requires create a new key pair)
Use different algorithm in signer and verifier.
So let’s try the last option, which is easy to implement
In our iFlow configuration, we specify
header: RSA-SHA512
signer: MD5/RSA
Result:
After deploying the iFlow with wrong configuration, we see the verification result as “false” in the Cloud Foundry log.
In CPI, the message has failed and the Error Details give the reason: HTTP-statusCode: 404
In error case, we don’t need to increase the log level, we can click on “Info” to see the response of our service endpoint:
Alternatively, click on "HTTP_Receiver_Adapter_Response_Body" in the “Attachments” section of the Message Monitor.
3. Optional: Find supported Algorithms
When using Node.js to sign/verify we need to specify the combination of algorithms (hashing/encrypting).
This needs to be done in the exact notation that is supported by node.
As such, in order to use the library, we need to find out how to exactly write the algorithm names.
To do so, we can just use the existing method of the crypto package:
getHashes()
For your convenience, I’ve created a little script that prints all names.
For even more convenience, I’ve copied the current list to appendix 2
The Node.js script
Create a file with name .e.g
script.js
The file contains just one line:
require('crypto').getHashes().forEach(s => { console.log(s) })
Note:
The library also has a method for the ciphers:
crypto.getCiphers(...)
List of supported Algorithms
To execute the above script, we run the file like this:
node script.js
The result is a long list that can be viewed in
Appendix 2
Summary
Today we’ve learned how to do verification of a digital signature created with Simple Signer.
Basically, it is a standard signature which is base64-encoded, so we can use native Node.js.
Furthermore, we’ve created a little scenario to send an iFlow message via HTTP adapter to a node app in Cloud Foundry.
We learned a possible way to prepare and configure the iFlow in order to achieve it.
Note:
Please have a look at the
next blog post discussing the weak aspects of this scenario.
Quick Guide
Summary of noteworthy settings:
IFlow
Simple Signer: note that algorithm name used here cannot be copy&pasted to Node.js code.
Simple Signer: define header name to lower case (otherwise lowercased during HTTP request).
Simple Signer: signature will be stored in a header, base64-encoded.
HTTP adapter: Make sure to “send headers” (e.g. via * ).
Content Modifier: set header: content-type with value: text/plain
Node.js app
Use some body parser for message content in POST request.
In verifier use algorithm combi name as found via
getHashes().
Links
SAP Help Portal
Docu for Groovy API, e.g.
KeyStore
Docu for
Message-Level Security
Node.js
Official documentation of
crypto package
Blogs
Understanding
Simple Signer
Signature verification in Groovy
Script
Know the weak aspects of this node-scnenario in
next blog post
Security Glossary Blog
Appendix 1: Sample Code
Note:
You might need to adapt the app names in manifest and the domain of the routes.
Also, if you changed names of headers, make sure to adapt them in the code below
App
manifest.yml
---
applications:
- name: digisigiapp
path: .
memory: 64M
routes:
- route: digisigiapp.cfapps.eu12.hana.ondemand.com
package.json
{
"dependencies": {
"express": "^4.16.2"
}
}
server.js
const crypto = require('crypto');
const express = require('express')
const app = express()
app.use(express.text())
/* App server */
app.listen(process.env.PORT)
/* App endpoint */
app.post('/process', (req, res)=>{
const headers = req.headers
// collect the data required for verification
const content = req.body
const signature = headers.digisigi
const algorithmCombi = headers.digialgi
const publicKeyB64 = headers.publickey
// do verification
const verificationResult = doVerification(content, signature, publicKeyB64, algorithmCombi)
console.log(`===> Result of digital signature verification : ${verificationResult}`);
if (verificationResult) {
// TODO : after verification of the signature, the app can continue processing the content
res.status(204).end()
} else {
res.status(404).send("Invalid content: digital signature verification failed.")
}
})
/* Helper */
function doVerification(content, signature, publicKeyB64, algorithmCombi){
// the crypto lib requires PEM format, so manually adapt
const publicKey = `-----BEGIN PUBLIC KEY-----\n${publicKeyB64}\n-----END PUBLIC KEY-----`
// use native crypto lib
const verifier = crypto.createVerify(algorithmCombi)
verifier.write(content)
verifier.end()
const result = verifier.verify(publicKey, signature, 'base64') // use param 'base64' for an incoming signature that is base64 encoded (by CPI)
return result
}
iFlow
Groovy script
import com.sap.gateway.ip.core.customdev.util.Message;
import com.sap.it.api.ITApiFactory;
import com.sap.it.api.keystore.KeystoreService;
import java.security.KeyPair;
import java.security.PublicKey;
def Message processData(Message message) {
// Public Key
KeystoreService keystoreService = ITApiFactory.getService(KeystoreService.class, null)
KeyPair keyPair = keystoreService.getKeyPair("iflowtonodekeys");
PublicKey publicKey = keyPair.getPublic();
// base64-encode the public key
byte[] pubKeyBytes = publicKey.getEncoded();
String pubKeyBase64 = Base64.getEncoder().encodeToString(pubKeyBytes);
// store the key in a header
message.setHeader("publickey", pubKeyBase64 );
return message;
}
Appendix 2: Algorithms supported by Node.js Crypto package
Printed using Node.js version 16
RSA-MD4
RSA-MD5
RSA-MDC2
RSA-RIPEMD160
RSA-SHA1
RSA-SHA1-2
RSA-SHA224
RSA-SHA256
RSA-SHA3-224
RSA-SHA3-256
RSA-SHA3-384
RSA-SHA3-512
RSA-SHA384
RSA-SHA512
RSA-SHA512/224
RSA-SHA512/256
RSA-SM3
blake2b512
blake2s256
id-rsassa-pkcs1-v1_5-with-sha3-224
id-rsassa-pkcs1-v1_5-with-sha3-256
id-rsassa-pkcs1-v1_5-with-sha3-384
id-rsassa-pkcs1-v1_5-with-sha3-512
md4
md4WithRSAEncryption
md5
md5-sha1
md5WithRSAEncryption
mdc2
mdc2WithRSA
ripemd
ripemd160
ripemd160WithRSA
rmd160
sha1
sha1WithRSAEncryption
sha224
sha224WithRSAEncryption
sha256
sha256WithRSAEncryption
sha3-224
sha3-256
sha3-384
sha3-512
sha384
sha384WithRSAEncryption
sha512
sha512-224
sha512-224WithRSAEncryption
sha512-256
sha512-256WithRSAEncryption
sha512WithRSAEncryption
shake128
shake256
sm3
sm3WithRSAEncryption
ssl3-md5
ssl3-sha1
whirlpool