Technical Articles
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
- Options:
»Step 11: Check if the AFM module (EJB) has been deployed successfully on the SAP PI server
- Access SAP PI via Web Browser (http://<host>:<port>/startPage);
- Clique the “EJB Explorer”;
- 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
Thank you very much.
Excellent document. Very well detailed.