SAP Cloud Integration (CPI) provides functionality to automatically sign a message with a digital signature using the Simple Signer.
In the previous blog post we’ve learned how to verify such signature with Node.js in an HTTP receiver.
Today we’re going to show the weakness of that scenario by simulating a hacker exploit.

Quicklinks:
Sample Code

Content

0. Prerequisites
1. Introduction
2. Hacker Scenario
2.1. Create Node.js App
2.2. Create iFlow
2.3. Run
Appendix 1: Hacker Scenario Code

0. Prerequisites

  • Today’s tutorial builds upon the Node.js scenario created in the previous blog post.
    So please follow that blog first to create the sample project and iFlow.
  • The Simple Signer Blog Post explains the basics about digital signatures and how to create them in CPI.
  • One 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.
  • 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.

1. Introduction

Starting from previous tutorial, we created a scenario as follows:
🔹 Create message with some important content.
🔹 Create digital signature with Simple Signer, it stores the signature in a header.
🔹 Create a header with algorithm info.
🔹 In Groovy script, fetch corresponding public key and store it in a header.
🔹 Send the message via HTTP adapter to REST endpoint.
🔹 Create Node.js server app exposing an endpoint that receives the message.
🔹 Read headers to extract public key, algorithm info and signature.
🔹 Verify the digital signature in Node.js code.

As we’ve learned, the digital signature covers the following security aspects:
🔹 The integrity of the message content is guaranteed via hash
– > modifying the content would result in different hash value
🔹 The authenticity of the signature is ensured via encrypting the hash with private key
-> modifying the hash + encrypting with new private key results in failing decryption

Looks nice, comfortable and easy to maintain in case of changing keys.

However, in the previous blog post we already stated that it was a bad idea…
This gives us the opportunity to showcase today why it was a bad idea.
The next blog post will show how it can be improved.

Our scenario has the following weak point:
If a hacker manages to intercept the network traffic, he can:
🔸 Modify the message content
🔸 Create new hash
🔸 Encrypt it with his own private key
🔸 Replace the message and headers with
Modified content
Fake signature
Fake public key

One weak aspect of the scenario is the weakness of “normal” digital signatures in general:
The encryption with key pair only ensures that the signature was created with a private key that corresponds to the given public key.
It cannot prove who actually created the signature.

Below screenshot shows a public key: it doesn’t contain any information about an identity:

The second aspect:
🚨 So our scenario is especially weak, because some weak blog post designed it in a weak way: sending the public key together with the signature in the same HTTP request 🚨

So the security relies on the way how the public key is handed over to the receiver of the message.
Security can be improved by personally handing over the public key to the receiver, or transmitting separately in secure way.

Nice.
However, a public key is meant to be public, and this is also the concept of a digital signature based on key pair:
Everybody in the world should be able to verify that the signature is valid, by using a publicly available public key.

So to make the scenario more secure, we need to go a different approach, as we’ll see in the next blog post

But first, let’s showcase how the vulnerability can be exploited by a hacker.

2. The Hacker Exploit Scenario

We want to simulate how a hacker intercepts the network traffic.
He would change the message content and let the receiver think that everything’s OK.

2.1. Extend Node.js application with hacker interceptor

As mentioned, the receiver will verify the signature and believe that everything’s alright.
As such, the code of the application is not changed, all verification logic has to remain the same.

We want to simulate an interceptor, which gets active after the messages leaves CPI and before the request reaches our service endpoint.
In Node.js, express based server app, this can be nicely achieved by introducing a middleware.

As such, today we only need to adapt our existing digisigi Node.js application.
Those of you who don’t have it yet, can follow this chapter of the previous tutorial.
The full code can be copied from the Appendix 1.

The hacker interceptor middleware

As mentioned above, if a hacker intercepts the network traffic he would access the HTTP request to read the headers and the body.
He would replace the values with his own fake values.
Then forward the request to the receiver.
The receiver’s verification of the (fake) signature should be successful.

So let’s have a look at the middleware:

function intercept(req, res, next) {
   const fakeText = `Send payment to account: 'Joe Cool, 12345678'`
   const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {modulusLength: 2048});

   const sign = crypto.createSign('SHA256');
   sign.update(fakeText);
   sign.end();
   const signatureb64 = sign.sign(privateKey, 'base64'); 

   const pubKeyDer = publicKey.export({
      type: 'spki',
      format: 'der'
   })
   const pubKeyString = Buffer.from(pubKeyDer).toString('base64') 

   // set the new fake values to the request
   req.body = fakeText
   req.headers.publickey =  pubKeyString
   req.headers.digisigi =  signatureb64
   req.headers.digialgi =  'RSA-SHA256'

   next()
}

We can see that just a few lines of code are required.

The sample code shows:

The hacker creates his own fake text, where he e.g. changes the bank account to receive the payment for himself:

const fakeText = `Send payment to account: 'Joe Cool, 12345678'`

This text will become the new request body.

The hacker creates his own fake key pair on the fly:

const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {modulusLength: 2048})

Note that the algorithm chosen here must match the algorithm combit that will be set in the header field below.

Now the hacker can create a new signature on the fake content, using the fake private key.
He even uses a different algorithm (SHA256 instead of 512) to reduce performance loss during message processing:

const sign = crypto.createSign('SHA256')
sign.update(fakeText)
sign.end()
const signatureb64 = sign.sign(privateKey, 'base64')

Same as Simple Signer in CPI, the hacker converts the signature to Base64 encoding.

Then, the hacker needs to get a hold of the public key and base64-encode it, as it must look same as the public key sent from Groovy script:

const pubKeyDer = publicKey.export({
    type: 'spki',
    format: 'der'
})
const pubKeyString = Buffer.from(pubKeyDer).toString('base64') 

Finally, the hacker replaces all relevant headers with his fake artifacts and info:

req.body = fakeText
req.headers.publickey =  pubKeyString
req.headers.digisigi =  signatureb64
req.headers.digialgi =  'RSA-SHA256'

We simulate the forwarding by calling next():

next()

This will invoke our /process endpoint code

In our sample project, we add the interceptor to the existing code.
We must not forget to make use of the middleware.

app.use(intercept)

The full code can be found in the Appendix 1.

Note:
Of course, there can be more weak aspects.
If a hacker managed to steal the private key (e.g. from some unsafe storage), then he can tweak the important message content without the need of intercepting the public key.
Etc.

Deploy

After we’ve added the middleware and the app.use() statement to our existing node app, we can deploy it.
No further modifications required.

2.2. The iFlow

On CPI side there are no changes to be done.
The whole scenario remains untouched, only the hacker slides in between.
The key pair to be used is the same as well.

For those of you who haven’t created the iFlow yet: the description can be found here.

For your convenience, I’ve added the Groovy script to Appendix 1.

2.3. Run

Although we haven’t changed anything, we redeploy the iFlow to trigger message processing.
We check the Cloud Foundry log and see the result:
Verification of digital signature has been successful.

The hacker interceptor has been active, changed the whole message, but nevertheless, the verification was successful.
This means that the hacker has successfully exploited the vulnerability of our scenario and we haven’t even noticed.

Summary

In this blog post we’ve learned how to create a digital signature in Node.js on the fly.

We’ve proven that digital signatures still have some weak aspects.
They don’t ensure non-repudiation.
They cannot ensure that the owner of the public key is the one that we expect.
They only ensure that the signature was created with a private key that corresponds to the public key that was used for (successful) verification.

If a hacker slides into a scenario with weak design, he can replace content, signature and public key, without causing the signature verification to fail.
Although in this hacker scenario we have severe assumptions:
– Hacker must have intercepted the message transmission.
– Hacker must have knowledge of our weak iFlow design.

SAP Help Portal
Docu for Message-Level Security

Node.js
Official documentation of crypto package

Blogs
Understanding Simple Signer
Signature verification in Groovy Script
Previous blog post: signature verification in Node.js
Security Glossary Blog

Appendix 1: Hacker Scenario 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())


/* HACKER CODE START */

/* Middleware to hack the message */
function intercept(req, res, next) {
   console.log("[PSSST] Hacker interceptor activated...")
   const fakeText = `Send payment to account: 'Joe Cool, 12345678'`
   const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {modulusLength: 2048});
   const sign = crypto.createSign('SHA256');
   sign.update(fakeText);
   sign.end();
   const signatureb64 = sign.sign(privateKey, 'base64'); 

   const pubKeyDer = publicKey.export({
      type: 'spki',
      format: 'der'
   })
   const pubKeyString = Buffer.from(pubKeyDer).toString('base64') 

   // set the new fake values to the request
   req.body = fakeText
   req.headers.publickey =  pubKeyString
   req.headers.digisigi =  signatureb64
   req.headers.digialgi =  'RSA-SHA256'

   console.log("[PSSST] Hacker interceptor done.")
   next()
}

// enable the middleware
app.use(intercept)


/* HACKER CODE END */


/* ORIGINAL APP */

/* App server */
app.listen(process.env.PORT)


/* App endpoint */
app.post('/process', (req, res)=>{
   const headers = req.headers   
   const content = req.body
   const signature = headers.digisigi   
   const algorithmCombi = headers.digialgi
   const publicKeyB64 = headers.publickey

   const verificationResult = doVerification(content, signature, publicKeyB64, algorithmCombi)
   console.log(`===> Result of digital signature verification : ${verificationResult}`);

   if (verificationResult) {
      res.status(204).end()      
   } else {
      res.status(404).send("Invalid content: digital signature verification failed.")
   }
})

/* Helper */
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
}

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;
}

 

Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x