Decode Base64-encoded DER header inside a sharedflow

Hello,

I have a load balancer in front of my Apigee instance who does northbound mtls authentication. I still need to do some further verification inside my proxy with for example the issuer DN so I made the load balancer pass in the header client_cert_issuer_dn as specified in here: https://cloud.google.com/load-balancing/docs/https/custom-headers-global#mtls-variables

I'm getting that header as expected but the issue I'm facing is that I cannot properly parse it to get the uid for example: MHAxEDAOBgNVBAoMB1JlbmF1bHQxGDAWBgNVBAMMD1ZBVUxUIElybi02ODkyNDEpMCcGCSqGSIb3DQEJARYabGlzdC52bHQtYWRtaW5AcmVuYXVsdC5jb20xFzAVBgoJkiaJk/IsZAEBDAdhd3ZsdDAy

  • Using an openssl command on the certificate enables me to get all needed information using this command: openssl x509 -inform DER -in {input} -text -noout
  • On Edge, we used to get that information correctly formatted with virtual host having mutual ssl enabled on the variable tls.client.i.dn as explained here: 

Any idea on how I can make my sf policy correctly get that issuer dn from that LB header client_cert_issuer_dn?

Thanks

Mouad

Solved Solved
2 4 199
1 ACCEPTED SOLUTION

Hello Mouad,

I had the same struggle to get this values decoded, the only way I found (it was also google's recommendation) to do it was using a Java Callout.

Unfortunately, I still haven't got the time to document it properly but I can share the code I have used to parse it as JSON and then set an environment variable with the decoded values.

 

package com.agite.apigee.callouts;

import java.io.IOException;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.util.ASN1Dump;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;

import com.apigee.flow.execution.ExecutionContext;
import com.apigee.flow.execution.ExecutionResult;
import com.apigee.flow.message.MessageContext;
import com.apigee.flow.execution.spi.Execution;

public class DecodeHeadersMTLS implements Execution {
    
    // Properties map for configuration
    private Map<String, String> properties; // read-only
    
    // OID mappings
    private static Map<String, String> oidMap = new HashMap<>();

    // Constructor
    public DecodeHeadersMTLS(Map<String, String> properties) {
        this.properties = properties;
    }

    // Static initializer block to populate OID map
    static {
        oidMap.put("2.5.4.3", "commonName");
        oidMap.put("2.5.4.6", "country");
        oidMap.put("2.5.4.7", "locality");
        oidMap.put("2.5.4.8", "state");
        oidMap.put("2.5.4.10", "organization");
        oidMap.put("2.5.4.11", "organizationalUnit");
        oidMap.put("1.2.840.113549.1.9.1", "emailAddress");
    }

    // Function to get the value corresponding to an OID
    public static String getValueFromOID(String oid) {
        if (oidMap.containsKey(oid)) {
            return oidMap.get(oid);
        } 
        
        return oid;
    }

    // Utility method to get stack trace as string
    protected static String getStackTrace(Throwable e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        return sw.toString();
    }

    // Decode Base64 encoded string into ASN1Sequence
    public static ASN1Sequence decodeBase64(String encodedString) throws IOException {
        byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
        ASN1InputStream ais = new ASN1InputStream(decodedBytes);
        return (ASN1Sequence) ais.readObject();
    }

    // Parse ASN1 sequence to JSON array
    public static JSONArray parseASN1ToJSON(ASN1Sequence sequence) throws JSONException {
        JSONArray jsonArray = new JSONArray();

        for (int i = 0; i < sequence.size(); i++) {
            ASN1Set set = (ASN1Set) sequence.getObjectAt(i);
            JSONObject setJsonObject = parseSetToJSON(set);
            if (setJsonObject instanceof JSONObject && setJsonObject.length() > 0) {
                jsonArray.put(setJsonObject);
            }
        }
        return jsonArray;
    }

    // Parse ASN1 set to JSON object
    public static JSONObject parseSetToJSON(ASN1Set set) throws JSONException {
        JSONObject jsonObject = new JSONObject();
        for (int i = 0; i < set.size(); i++) {
            ASN1Sequence innerSequence = (ASN1Sequence) set.getObjectAt(i);
            ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier) innerSequence.getObjectAt(0);
            String oidString = getValueFromOID(oid.getId());
            ASN1Primitive value = (ASN1Primitive) innerSequence.getObjectAt(1);

            String valueString;
            if (value instanceof ASN1String || value instanceof DERPrintableString) {
                valueString = ASN1Dump.dumpAsString(value)
                    .replace("PrintableString", "")
                    .replace("IA5String", "")
                    .replace("(", "")
                    .replace(")", "")                    
                    .replace("\n", "")
                    .replace("\r", "")
                    .trim();
            } 
            else {
                valueString = "Unsupported Value Type";
            }

            jsonObject.put(oidString, valueString);
        }
        return jsonObject;
    }

    // Merge array of JSON into a single JSON object
    public static JSONObject mergeJSONObjects(JSONArray jsonArray) throws JSONException {
        JSONObject mergedJSON = new JSONObject();

        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            mergeJSONObject(mergedJSON, jsonObject);
        }
        return mergedJSON;
    }

    // Merge JSON objects recursively
    private static void mergeJSONObject(JSONObject mergedJSON, JSONObject jsonObject) throws JSONException {
        for (String key : jsonObject.keySet()) {
            Object value = jsonObject.get(key);
            if (mergedJSON.has(key)) {
                Object existingValue = mergedJSON.get(key);
                if (value instanceof JSONObject && existingValue instanceof JSONObject) {
                    mergeJSONObject((JSONObject) existingValue, (JSONObject) value);
                } else if (value instanceof JSONArray && existingValue instanceof JSONArray) {
                    JSONArray mergedArray = new JSONArray(existingValue.toString());
                    JSONArray newValueArray = (JSONArray) value;
                    for (int j = 0; j < newValueArray.length(); j++) {
                        mergedArray.put(newValueArray.get(j));
                    }
                    mergedJSON.put(key, mergedArray);
                } else {
                    mergedJSON.put(key, value);
                }
            } else {
                mergedJSON.put(key, value);
            }
        }
    }

    // Java Callout entry point
    public ExecutionResult execute(MessageContext messageContext, ExecutionContext executionContext) {

        try {

            // Get env variables names from Java Callout properties
            String subjectDnVar = this.properties.get("subjectDn");
            String issuerDnVar = this.properties.get("issuerDn");
            String dnsnameSansVar = this.properties.get("dnsnameSans");

            // Get env variables
            String encodedSubjectDn = messageContext.getVariable(subjectDnVar);
            String encodedIssuerDn = messageContext.getVariable(issuerDnVar);
            String encodedDnsNameSans = messageContext.getVariable(dnsnameSansVar);

            if (encodedSubjectDn != null && encodedSubjectDn.length() > 0) {
                ASN1Sequence sequence = decodeBase64(encodedSubjectDn);
                JSONArray jsonData = parseASN1ToJSON(sequence);
                JSONObject flatJSON = mergeJSONObjects(jsonData);

                // This variables can be renamed as you wish
                messageContext.setVariable("clientCert.decoded.subjectDn", flatJSON.toString(4));
            } 
            if (encodedIssuerDn != null && encodedIssuerDn.length() > 0) {
                ASN1Sequence sequence = decodeBase64(encodedIssuerDn);
                JSONArray jsonData = parseASN1ToJSON(sequence);
                JSONObject flatJSON = mergeJSONObjects(jsonData);

                // This variables can be renamed as you wish
                messageContext.setVariable("clientCert.decoded.issuerDn", flatJSON.toString(4));
            }
            if (encodedDnsNameSans != null && encodedDnsNameSans.length() > 0) { 
                // Split SANS, decode each DNS Name SANS from Base64 and rejoin the decoded values
                StringBuilder builder = new StringBuilder();
                String[] parts = encodedDnsNameSans
                    .replace("[", "").replace("]", "").replace(" ", "")
                    .split(",");


                for (int i = 0; i < parts.length; i++) {
                    byte[] decodedBytes = Base64.getDecoder().decode(parts[i]);
                    String decodedString = new String(decodedBytes);
                    builder.append(decodedString);
    
                    if (i < parts.length - 1) {
                        builder.append(",");
                    }
                }
                messageContext.setVariable("clientCert.decoded.dnsnameSans", builder.toString());
            }

            return ExecutionResult.SUCCESS;

        } catch (Exception e) {
            messageContext.setVariable("jc-exception", getStackTrace(e));
            return ExecutionResult.ABORT;
        }
    }   
}

 

Note: You can find the Apigee specific JARs from this link

I will update this response once I have it well documented.

View solution in original post

4 REPLIES 4

Hello Mouad,

I had the same struggle to get this values decoded, the only way I found (it was also google's recommendation) to do it was using a Java Callout.

Unfortunately, I still haven't got the time to document it properly but I can share the code I have used to parse it as JSON and then set an environment variable with the decoded values.

 

package com.agite.apigee.callouts;

import java.io.IOException;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.util.ASN1Dump;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;

import com.apigee.flow.execution.ExecutionContext;
import com.apigee.flow.execution.ExecutionResult;
import com.apigee.flow.message.MessageContext;
import com.apigee.flow.execution.spi.Execution;

public class DecodeHeadersMTLS implements Execution {
    
    // Properties map for configuration
    private Map<String, String> properties; // read-only
    
    // OID mappings
    private static Map<String, String> oidMap = new HashMap<>();

    // Constructor
    public DecodeHeadersMTLS(Map<String, String> properties) {
        this.properties = properties;
    }

    // Static initializer block to populate OID map
    static {
        oidMap.put("2.5.4.3", "commonName");
        oidMap.put("2.5.4.6", "country");
        oidMap.put("2.5.4.7", "locality");
        oidMap.put("2.5.4.8", "state");
        oidMap.put("2.5.4.10", "organization");
        oidMap.put("2.5.4.11", "organizationalUnit");
        oidMap.put("1.2.840.113549.1.9.1", "emailAddress");
    }

    // Function to get the value corresponding to an OID
    public static String getValueFromOID(String oid) {
        if (oidMap.containsKey(oid)) {
            return oidMap.get(oid);
        } 
        
        return oid;
    }

    // Utility method to get stack trace as string
    protected static String getStackTrace(Throwable e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        return sw.toString();
    }

    // Decode Base64 encoded string into ASN1Sequence
    public static ASN1Sequence decodeBase64(String encodedString) throws IOException {
        byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
        ASN1InputStream ais = new ASN1InputStream(decodedBytes);
        return (ASN1Sequence) ais.readObject();
    }

    // Parse ASN1 sequence to JSON array
    public static JSONArray parseASN1ToJSON(ASN1Sequence sequence) throws JSONException {
        JSONArray jsonArray = new JSONArray();

        for (int i = 0; i < sequence.size(); i++) {
            ASN1Set set = (ASN1Set) sequence.getObjectAt(i);
            JSONObject setJsonObject = parseSetToJSON(set);
            if (setJsonObject instanceof JSONObject && setJsonObject.length() > 0) {
                jsonArray.put(setJsonObject);
            }
        }
        return jsonArray;
    }

    // Parse ASN1 set to JSON object
    public static JSONObject parseSetToJSON(ASN1Set set) throws JSONException {
        JSONObject jsonObject = new JSONObject();
        for (int i = 0; i < set.size(); i++) {
            ASN1Sequence innerSequence = (ASN1Sequence) set.getObjectAt(i);
            ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier) innerSequence.getObjectAt(0);
            String oidString = getValueFromOID(oid.getId());
            ASN1Primitive value = (ASN1Primitive) innerSequence.getObjectAt(1);

            String valueString;
            if (value instanceof ASN1String || value instanceof DERPrintableString) {
                valueString = ASN1Dump.dumpAsString(value)
                    .replace("PrintableString", "")
                    .replace("IA5String", "")
                    .replace("(", "")
                    .replace(")", "")                    
                    .replace("\n", "")
                    .replace("\r", "")
                    .trim();
            } 
            else {
                valueString = "Unsupported Value Type";
            }

            jsonObject.put(oidString, valueString);
        }
        return jsonObject;
    }

    // Merge array of JSON into a single JSON object
    public static JSONObject mergeJSONObjects(JSONArray jsonArray) throws JSONException {
        JSONObject mergedJSON = new JSONObject();

        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            mergeJSONObject(mergedJSON, jsonObject);
        }
        return mergedJSON;
    }

    // Merge JSON objects recursively
    private static void mergeJSONObject(JSONObject mergedJSON, JSONObject jsonObject) throws JSONException {
        for (String key : jsonObject.keySet()) {
            Object value = jsonObject.get(key);
            if (mergedJSON.has(key)) {
                Object existingValue = mergedJSON.get(key);
                if (value instanceof JSONObject && existingValue instanceof JSONObject) {
                    mergeJSONObject((JSONObject) existingValue, (JSONObject) value);
                } else if (value instanceof JSONArray && existingValue instanceof JSONArray) {
                    JSONArray mergedArray = new JSONArray(existingValue.toString());
                    JSONArray newValueArray = (JSONArray) value;
                    for (int j = 0; j < newValueArray.length(); j++) {
                        mergedArray.put(newValueArray.get(j));
                    }
                    mergedJSON.put(key, mergedArray);
                } else {
                    mergedJSON.put(key, value);
                }
            } else {
                mergedJSON.put(key, value);
            }
        }
    }

    // Java Callout entry point
    public ExecutionResult execute(MessageContext messageContext, ExecutionContext executionContext) {

        try {

            // Get env variables names from Java Callout properties
            String subjectDnVar = this.properties.get("subjectDn");
            String issuerDnVar = this.properties.get("issuerDn");
            String dnsnameSansVar = this.properties.get("dnsnameSans");

            // Get env variables
            String encodedSubjectDn = messageContext.getVariable(subjectDnVar);
            String encodedIssuerDn = messageContext.getVariable(issuerDnVar);
            String encodedDnsNameSans = messageContext.getVariable(dnsnameSansVar);

            if (encodedSubjectDn != null && encodedSubjectDn.length() > 0) {
                ASN1Sequence sequence = decodeBase64(encodedSubjectDn);
                JSONArray jsonData = parseASN1ToJSON(sequence);
                JSONObject flatJSON = mergeJSONObjects(jsonData);

                // This variables can be renamed as you wish
                messageContext.setVariable("clientCert.decoded.subjectDn", flatJSON.toString(4));
            } 
            if (encodedIssuerDn != null && encodedIssuerDn.length() > 0) {
                ASN1Sequence sequence = decodeBase64(encodedIssuerDn);
                JSONArray jsonData = parseASN1ToJSON(sequence);
                JSONObject flatJSON = mergeJSONObjects(jsonData);

                // This variables can be renamed as you wish
                messageContext.setVariable("clientCert.decoded.issuerDn", flatJSON.toString(4));
            }
            if (encodedDnsNameSans != null && encodedDnsNameSans.length() > 0) { 
                // Split SANS, decode each DNS Name SANS from Base64 and rejoin the decoded values
                StringBuilder builder = new StringBuilder();
                String[] parts = encodedDnsNameSans
                    .replace("[", "").replace("]", "").replace(" ", "")
                    .split(",");


                for (int i = 0; i < parts.length; i++) {
                    byte[] decodedBytes = Base64.getDecoder().decode(parts[i]);
                    String decodedString = new String(decodedBytes);
                    builder.append(decodedString);
    
                    if (i < parts.length - 1) {
                        builder.append(",");
                    }
                }
                messageContext.setVariable("clientCert.decoded.dnsnameSans", builder.toString());
            }

            return ExecutionResult.SUCCESS;

        } catch (Exception e) {
            messageContext.setVariable("jc-exception", getStackTrace(e));
            return ExecutionResult.ABORT;
        }
    }   
}

 

Note: You can find the Apigee specific JARs from this link

I will update this response once I have it well documented.

Thanks for your answer @bselistre-dvt 
Can you please share the pom.xml used for the compilation of the code above? My java knowledge is very limited..

This might be a working solution for you: https://github.com/yuriylesyuk/eidas-x509-for-psd2

The cert format is incompatible with the the output from the LB. We get DER binary base64 encoded and format expected by your callout parser is eIDAS/PSD2.

Can you guide me into properly parse the LB output (whole certificate or specific headers like subject or issuer DN) properly?