Skip to Content
Technical Articles
Author's profile photo Rogerio Coimbra

How to create Adapter Framework Module (AFM) to digitally sign the contents of a payload to PKCS#7 pattern.

Summary

Develop an Adapter Framework Module (AFM) capable of signing the contents of a payload in the pattern “PKCS # 7” and encode it in Base64.

 

Use

In my case the module will be used to convert the data generated by a file receiver adapter to the PKCS#7 pattern and then encode them in Base64.

 

Steps

»Step 1: check the version of the development tool

SAP NetWeaver Developer Studio (NWDS) version 7.31 With JDK 6.0 (SAP JVM 6):

 

»Step 2: opening the NWDS perspective for developing Enterprise Java Beans (EJB) projects

Navigate to “NWDS > Window > Open Perspective > Other”:

 

Select option “Java EE” and click in “OK” button:

 

»Step 3: Create EJB project

Navigate to “NWDS > File > New > EJB Project”:

 

Select the options as shown in the print and click the “Next” button:

 

Click the “Next” button:

 

Click the “Finish” button:

 

»Step 4: Configure Build Path

Right click on “CustomSignPKCS7 > Build Path > Configure Build Path”:

 

Navigate to the tab “Libraries” and click on “Add Library” button:

 

Select “XPI Library” and a click the “Next” button:

 

Select the Library Type “XPI Adapter Libraries” and click the “Finish” button:

 

Click the “Ok” button:

 

»Step 5: Create a “Session Bean”

Right click on “CustomSignPKCS7” an navigate to “New > Session Bean (EJB 3.x)”:

 

Click the “Next” button:

 

Click the “Finish” button:

 

Copy the code below and paste it into the “SignaturePKCS7” class of your project:

package com.sap.pi.afm.pkcs7;

/**
 *--------------------------------------------------------------
* @autor: Rogério Coimbra de Oliveira.
* Purpose: To sign data in the PKCS # 7 pattern and Base64 base.
* Version: 001 (create)
 *-------------------------------------------------------------- 
 */

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.rmi.RemoteException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Local;
import javax.ejb.LocalHome;
import javax.ejb.Remote;
import javax.ejb.RemoteHome;
import javax.ejb.Stateless;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import sun.misc.BASE64Encoder;
import com.sap.aii.af.lib.mp.module.Module;
import com.sap.aii.af.lib.mp.module.ModuleContext;
import com.sap.aii.af.lib.mp.module.ModuleData;
import com.sap.aii.af.lib.mp.module.ModuleException;
import com.sap.aii.af.lib.mp.module.ModuleHome;
import com.sap.aii.af.lib.mp.module.ModuleLocal;
import com.sap.aii.af.lib.mp.module.ModuleLocalHome;
import com.sap.aii.af.lib.mp.module.ModuleRemote;
import com.sap.engine.interfaces.keystore.KeystoreManager;
import com.sap.engine.interfaces.messaging.api.Message;
import com.sap.engine.interfaces.messaging.api.MessageKey;
import com.sap.engine.interfaces.messaging.api.PublicAPIAccessFactory;
import com.sap.engine.interfaces.messaging.api.auditlog.AuditAccess;
import com.sap.engine.interfaces.messaging.api.auditlog.AuditLogStatus;
import com.sap.engine.interfaces.messaging.api.exception.InvalidParamException;
import com.sap.security.api.ssf.ISsfData;
import com.sap.security.core.server.ssf.SsfDataPKCS7;
import com.sap.security.core.server.ssf.SsfInvalidAlgException;
import com.sap.security.core.server.ssf.SsfInvalidKeyException;
import com.sap.security.core.server.ssf.SsfProfileKeyStore;

/**
 * Session Bean implementation class SignaturePKCS7
 */
@Stateless(name = "SignaturePKCS7Bean")
@Local(value = { ModuleLocal.class })
@Remote(value = { ModuleRemote.class })
@LocalHome(value = ModuleLocalHome.class)
@RemoteHome(value = ModuleHome.class)
public class SignaturePKCS7 implements Module {

	private AuditAccess audit;
	private byte[] content;
	private Message msg;
	MessageKey msgKey;

	private String aliasPrivateKey;
	private boolean base64Encode;
	private boolean formatPKCS7;
	private boolean applySignature;
	private String pwdPrivateKey;
	private String aliasKeyStore;
	private String mdAlg;
	private int incCerts;
	private boolean detached;
	private boolean paramMandatoryEmpty;

	private static final String PARAM_ALIAS_PRIVATE_KEY = "aliasPrivateKey";
	private static final String PARAM_BASE64_ENCODE = "base64Encode";
	private static final String PARAM_APPLY_SIGNATURE = "applySignature";
	private static final String PARAM_PWD_PRIVATE_KEY = "pwdPrivateKey";
	private static final String PARAM_ALIAS_KEY_STORE = "aliasKeyStore";
	private static final String PARAM_MD_ALG = "mdAlg";
	private static final String PARAM_INC_CERTS = "incCerts";
	private static final String PARAM_DETACHED = "detached";
	private static final String PARAM_FORMAT_PKCS7 = "formatPKCS7";

	@Override
	public ModuleData process(ModuleContext moduleContext,
			ModuleData inputModuleData) throws ModuleException {
		//Load message:
		this.msg = (Message) inputModuleData.getPrincipalData();
		this.msgKey = this.msg.getMessageKey();

		//Loads the contents of the incoming message: 
		this.content = msg.getDocument().getContent();

		//Load configuration parameters:
		this.loadParameters(moduleContext);

		if (this.applySignature && !this.paramMandatoryEmpty) {
			//Sign the file digitally in PKCS # 7 standard:
			content = this.signPKCS7(content);
		}

		if (this.base64Encode) {
			if (this.formatPKCS7) {
				//Encode data in Base64 and non-standard format PKCS7:
				content = this.formatPKCS7(this.encodeBase64(content)).getBytes();				
			} else {
				// Encode data in Base64:
				content = this.encodeBase64(content).getBytes();
			}
		}

		//Update output:
		try {
			msg.getDocument().setContent(content);
		} catch (InvalidParamException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}
		inputModuleData.setPrincipalData(msg);

		return inputModuleData;
	}

	@PostConstruct
	public void initializeResources() {
		try {
			this.audit = PublicAPIAccessFactory.getPublicAPIAccess()
					.getAuditAccess();
		} catch (Exception e) {
			throw new RuntimeException("Error initializing resources: "
					+ e.getMessage());
		}
	}

	@PreDestroy
	public void releaseResources() {
		this.audit.flushAuditLogEntries(this.msgKey);
	}

	private void loadParameters(ModuleContext moduleContext)
			throws ModuleException {

		//Determine the data must be signed:
		if (moduleContext.getContextData(PARAM_APPLY_SIGNATURE) != null
				&& !moduleContext.getContextData(PARAM_APPLY_SIGNATURE)
						.isEmpty()) {
			this.applySignature = Boolean.valueOf(moduleContext
					.getContextData(PARAM_APPLY_SIGNATURE));
		} else {
			this.applySignature = true;
		}

		if (this.applySignature) {
			//Private key or digital certificate name:
			if (moduleContext.getContextData(PARAM_ALIAS_PRIVATE_KEY) == null
					|| moduleContext.getContextData(PARAM_ALIAS_PRIVATE_KEY)
							.isEmpty()) {
				this.paramMandatoryEmpty = true;
				audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR,
						"Parameter " + PARAM_ALIAS_PRIVATE_KEY
								+ " required when the "
								+ PARAM_APPLY_SIGNATURE
								+ " is equal to \"true\"");
			} else {
				this.aliasPrivateKey = moduleContext
						.getContextData(PARAM_ALIAS_PRIVATE_KEY);
			}

			//Private key password:
			if (moduleContext.getContextData(PARAM_PWD_PRIVATE_KEY) != null
					&& !moduleContext.getContextData(PARAM_PWD_PRIVATE_KEY)
							.isEmpty()) {
				this.pwdPrivateKey = moduleContext
						.getContextData(PARAM_PWD_PRIVATE_KEY);
			} else {
				this.pwdPrivateKey = null;
			}

			//Key Store Name:
			if (moduleContext.getContextData(PARAM_ALIAS_KEY_STORE) != null
					&& !moduleContext.getContextData(PARAM_ALIAS_KEY_STORE)
							.isEmpty()) {
				this.aliasKeyStore = moduleContext
						.getContextData(PARAM_ALIAS_KEY_STORE);
			} else {
				this.aliasKeyStore = "DEFAULT";
			}

			//Digital signature algorithm:
			if (moduleContext.getContextData(PARAM_MD_ALG) != null
					&& !moduleContext.getContextData(PARAM_MD_ALG).isEmpty()) {
				this.mdAlg = moduleContext.getContextData(PARAM_MD_ALG);
			} else {
				this.mdAlg = SsfDataPKCS7.ALG_SHA;
			}

			//Determines how the certificate should be included in the content:
			if (moduleContext.getContextData(PARAM_INC_CERTS) != null
					&& !moduleContext.getContextData(PARAM_INC_CERTS).isEmpty()) {
				try {
					this.incCerts = Integer.valueOf(moduleContext
							.getContextData(PARAM_INC_CERTS));
				} catch (NumberFormatException e) {
					this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
							.getMessage());
					throw new ModuleException(e.getMessage(), e);
				}
			} else {
				this.incCerts = 1;
			}

			// Determines whether signed content should be included with signature:
			if (moduleContext.getContextData(PARAM_DETACHED) != null
					&& !moduleContext.getContextData(PARAM_DETACHED).isEmpty()) {
				this.detached = Boolean.valueOf(moduleContext
						.getContextData(PARAM_DETACHED));
			} else {
				this.detached = false;
			}
		}

		//Determines whether content should be formatted in the PKCS7 standard:
		if (moduleContext.getContextData(PARAM_FORMAT_PKCS7) != null
				&& !moduleContext.getContextData(PARAM_FORMAT_PKCS7).isEmpty()) {
			this.formatPKCS7 = Boolean.valueOf(moduleContext
					.getContextData(PARAM_FORMAT_PKCS7));
		} else {
			this.formatPKCS7 = false;
		}

		//Determines whether content should be encoded for Base64:
		if (moduleContext.getContextData(PARAM_BASE64_ENCODE) != null
				&& !moduleContext.getContextData(PARAM_BASE64_ENCODE).isEmpty()) {
			this.base64Encode = Boolean.valueOf(moduleContext
					.getContextData(PARAM_BASE64_ENCODE));
		} else {
			this.base64Encode = true;
		}

		//Generates audit logs:
		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
						+ PARAM_APPLY_SIGNATURE + ") loaded: "
						+ this.applySignature);

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_ALIAS_PRIVATE_KEY + ") loaded: "
				+ this.aliasPrivateKey);

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_PWD_PRIVATE_KEY + ") loaded!" );

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_ALIAS_KEY_STORE + ") loaded: " + this.aliasKeyStore);

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_MD_ALG + ") loaded: " + this.mdAlg);

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_INC_CERTS + ") loaded: " + this.incCerts);

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_DETACHED + ") loaded: " + this.detached);

		audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS, "Parameter ("
				+ PARAM_BASE64_ENCODE + ") loaded: " + this.base64Encode);

	}

	private byte[] signPKCS7(byte[] content) throws ModuleException {

		SsfProfileKeyStore profile;

		//Loads data to be signed:
		InputStream stream = new ByteArrayInputStream(content);
		ISsfData data = null;
		try {
			data = new SsfDataPKCS7(stream);
		} catch (IOException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}

		//Get sign-up profile from Key Storage:
		InitialContext ctx = null;
		try {
			ctx = new InitialContext();
		} catch (NamingException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}

		Object o = null;
		try {
			o = (Object) ctx.lookup("keystore");
			audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS,
					"Context \"Keystore\" loaded!");
		} catch (NamingException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}

		KeystoreManager manager = (KeystoreManager) o;
		KeyStore keyStore = null;
		try {
			keyStore = manager.getKeystore(this.aliasKeyStore);
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS,
					"Private Key Loaded Successfully:" + this.aliasKeyStore
							+ ".");
		} catch (RemoteException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}

		try {
			profile = new SsfProfileKeyStore(keyStore, this.aliasPrivateKey,
					this.pwdPrivateKey);
		} catch (KeyStoreException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}

		try {
			//Sign data in default PKCS # 7:
			boolean res = data.sign(profile, this.mdAlg, this.incCerts,
					this.detached);
			audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS,
					"Data signed successfully: Format \"PKCS#7\".");
			if (!res) {
				// Failed to sign data:
				this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR,
						"Failed to sign file!");
				throw new ModuleException("Failed to sign file!");
			}
		} catch (SsfInvalidKeyException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		} catch (SsfInvalidAlgException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}

		//Get signed data:
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			data.writeTo(baos);
		} catch (IOException e) {
			this.audit.addAuditLogEntry(msgKey, AuditLogStatus.ERROR, e
					.getMessage());
			throw new ModuleException(e.getMessage(), e);
		}
		return baos.toByteArray();
	}

	private String encodeBase64(byte[] content) throws ModuleException {
		String encoded = new BASE64Encoder().encode(content).trim();
		this.audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS,
				"Data encoded successfully: Base64.");
		return encoded;
	}

	private String formatPKCS7(String encoded) throws ModuleException {
		final String bgn_sign = "-----BEGIN PKCS7-----";
		final String end_sign = "-----END PKCS7-----";
		String formated;

		//Format string encoded in BASE64 in the PKCS7 pattern: 
		formated = bgn_sign + "\r\n" + encoded + "\r\n" + end_sign;
		this.audit.addAuditLogEntry(msgKey, AuditLogStatus.SUCCESS,
				"Successfully formatted data: PKCS7 standard.");
		return formated;
	}
}

 

»Step 6: Add External Libraries (JARs…)

  • Libraries required for digital signature in the PKCS # 7 pattern:
    • sap.com~tc~je~keystore_api~API.jar;
    • sap.com~tc~sec~ssf.jar.

Error due to missing required libraries:

 

Right click on “CustomSignPKCS7 > Build Path > Configure Build Path”:

 

Navigate to the tab “Libraries” and click on “Add External JARs…” button:

 

Select the files (.JAR) for the libraries in question:

Note: Click here to learn how to get the libraries.

 

Click the “OK” button:

 

Fixed bug due to missing required libraries:

 

 

»Step 7: Configure “ejb-j2ee-engine.xml”

Right click on “ejb-j2ee-engine.xml” and click on “Open”:

 

Copy the code below and paste it into the “ejb-j2ee-engine.xml” file of your project:

<?xml version="1.0" encoding="UTF-8"?>
<ejb-j2ee-engine xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ejb-j2ee-engine_3_0.xsd">
	<enterprise-beans>
		<enterprise-bean>
			<ejb-name>SignaturePKCS7Bean</ejb-name>
			<jndi-name>SignPKCS7</jndi-name>
		</enterprise-bean>
	</enterprise-beans>
</ejb-j2ee-engine>

 

Navigate to the tab “Source”:

 

»Step 8: Configure “application-j2ee-engine.xml”

Right click on “application-j2ee-engine.xml” and click on “Open”:

 

Copy the code below and paste it into the “application-j2ee-engine.xml” file of your project:

<?xml version="1.0" encoding="UTF-8"?>
<application-j2ee-engine xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="application-j2ee-engine.xsd">
	<reference reference-type="hard">
		<reference-target target-type="service" provider-name="sap.com">engine.security.facade</reference-target>
	</reference>
	<reference reference-type="hard">
		<reference-target target-type="library" provider-name="sap.com">engine.j2ee14.facade</reference-target>
	</reference>
	<reference reference-type="hard">
		<reference-target target-type="service" provider-name="sap.com">com.sap.aii.af.svc.facade</reference-target>
	</reference>
	<reference reference-type="hard">
		<reference-target target-type="interface" provider-name="sap.com">com.sap.aii.af.ifc.facade</reference-target>
	</reference>
	<reference reference-type="hard">
		<reference-target target-type="library" provider-name="sap.com">com.sap.aii.af.lib.facade</reference-target>
	</reference>
	<reference reference-type="hard">
		<reference-target target-type="library" provider-name="sap.com">com.sap.base.technology.facade</reference-target>
	</reference>
	<fail-over-enable xsi:type="fail-over-enableType_disable"
		mode="disable" />
</application-j2ee-engine>

 

Navigate to the tab “Source”:

 

»Step 9: Export SAP EAR

Right click on “CustomSignPKCS7EAR” and navigate to “Export > SAP EAR File”:

 

Click the “Finish” button:

 

»Step 10: Deploy EAR File on SAP PI Server

 

»Step 11: Check if the AFM module (EJB) has been deployed successfully on the SAP PI server

  1. Access SAP PI via Web Browser (http://<host>:<port>/startPage);
  2. Clique the “EJB Explorer”;
  3. Find the module “CustomSignPKCS7_EJB”.

 

»Step 12: Configuration of the AFM in the File Adapter.

  • Configuration parameters:
    • Parameter “aliasKeyStore”: Name of the Key Store where the digital certificate (private key) is stored;
    • Parameter “aliasPrivateKey”: Name of the digital certificate (private key) used to digitally sign the file in PKCS7 pattern;
    • ApplySignature parameter: Determines whether or not the module should digitally sign the file, with the default values ​​being: “true” and “false”.
    • Parameter “base64Encode”: Determines whether or not the module should convert/ encode the file to the Base64 pattern, and the proposed values ​​are:
      • “true” (convert);
      • “false” (does not convert);
    • Parameter “detached”: Determines whether the signed data should be included in the signature, and the values ​​proposed are:
      • “true” (Does not include signature data);
      • “false” (Includes signature data);
    • Parameter “incCerts”: Determines whether the certificates should be included in the signed data, and the values ​​proposed are:
      • “0” (Does not include any certificates when creating a digital signature);
      • “1” (Includes the certificate itself when creating a digital signature);
      • “2” (Includes certificate chain without root when creating a digital signature [default]);
      • “3” (Includes root certificate chain when creating a digital signature);
    • Parameter “mdAlg”: digest message algorithm used for hash generation of the signed data, the proposed values ​​being:
      • “ALG_SHA” (Digest digest algorithm SHA-1);
      • “ALG_SHA256” (digest digest algorithm SHA-256);
      • “ALG_SHA512” (Digest Digest Algorithm SHA-512);
      • “ALG_MD5” (MD5 message digest algorithm (not recommended));
      • “ALG_AES128_CBC” (AES symmetric encryption algorithm (128 bit) in CBC mode);
      • “ALG_AES192_CBC” (AES symmetric encryption algorithm (192 bit) in CBC mode);
      • “ALG_AES256_CBC” (AES symmetric encryption algorithm (256 bit) in CBC mode);
      • “ALG_DES_EDE3_CBC” (Triple DES Symmetric Encryption Algorithm in CBC mode);
      • “ALG_RC2_40_CBC” (Symmetric Encryption Algorithm RC2 (40 bit) in CBC mode);
      • “ALG_RC2_CBC” (Symmetric Encryption Algorithm RC2 (128 bit) in CBC mode);
    • Parameter “pwdPrivateKey”: Password of the private key used to digitally sign the file;
    • Parameter “formatPKCS7”: Determines whether data should be formatted in the PKCS7 pattern.

 

Example of the configuration in the communication channel of type file:

 

»Step 13: Test result

Content of the signed PKCS # 7 file and converted to BASE64:

 

»Step 14: Download code without libraries:

https://github.com/rogcoimbra/SAP/blob/master/CustomSignPKCS7EAR.ear

 

Best regards,

Rogério Coimbra de Oliveira

Assigned Tags

      1 Comment
      You must be Logged on to comment or reply to a post.
      Author's profile photo Miguel Oliveira
      Miguel Oliveira

      Thank you very much.
      Excellent document. Very well detailed.