Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
CarlosRoggan
Product and Topic Expert
Product and Topic Expert

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().

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