IBM Security Verify

 View Only
Expand all | Collapse all

User Mapping for LMI Certificate authentication

  • 1.  User Mapping for LMI Certificate authentication

    Posted Fri March 10, 2023 07:47 AM

    Hello,

    To address one of the STIG findings, we need to enable PIV authentication for Security Verify Access LMI. We need to map one of the attributes from the certificate to the user id in LDAP. Has anyone written a user map function for this? The documentation gives a simple example of extracting 'cn' attribute and combining it with the baseDN. I want to list all available attributes and then determine which one can be used to generate the resultant LDAP dn. Also, I tried to place a System.out.println in the code,  but that did not work-any thoughts on what functions I can use to log some messages?

    Thanks,

    Rakesh 



    ------------------------------
    Rakesh Vohra
    Great Falls VA
    2405683495
    ------------------------------


  • 2.  RE: User Mapping for LMI Certificate authentication

    Posted Mon March 13, 2023 04:57 AM
    Edited by Peter Volckaert Mon March 13, 2023 04:57 AM

    Hi Rakesh,

    In Infomaps you can use the traceString() function. In this 'vintage' article traceString() and its use is explained: https://philipnye.com/2014/04/03/isam-for-mobile-trace-statements-in-mapping-rules/

    traceString() is a method of the Java class IDMappingExtUtils. Detailed documentation on Java is contained in the ISAM-javadoc.zip that can be downloaded from the appliance; see for example this link on how to download that .zip file: https://www.ibm.com/docs/en/sva/9.0.5?topic=policies-access-policy-development

    You can find lots of examples using Google or searching in the ISVA Documentation

    Cheers,

      - Peter.



    ------------------------------
    Peter Volckaert
    Senior Sales Engineer
    Authentication and Access
    IBM Security
    ------------------------------



  • 3.  RE: User Mapping for LMI Certificate authentication

    Posted Tue March 14, 2023 02:19 AM

    I have done this several times using an InfoMap AAC mechanism as the "certificate EAI" implementation, including for some very bespoke ASN.1 encoded extensions, using Javascript libraries to parse the x509 certificate data. The KJUR (actually jsrsasign - https://github.com/kjur/jsrsasign) library is your friend here - it's very easy to import that into your infomap and parse standard extensions with that. 

    Here's a sample InfoMap that acts as a certificate EAI where the username to login as can be found in a very specific OID encoded as an otherName part of the SAN extension. You can most likely dumb this down a lot for your own use case....

    importClass(Packages.com.tivoli.am.fim.trustserver.sts.utilities.IDMappingExtUtils);
    
    // must use jsrsasign 10.5.3 or later for otherName supoprt (see https://kjur.github.io/jsrsasign/api/symbols/KJUR.asn1.x509.GeneralName.html)
    importMappingRule("jsrsasign")
    
    function getRequestHeader(h) {
    	return context.get(Scope.REQUEST, "urn:ibm:security:asf:request:header", h);
    }
    
    function addOptionalResponseAttribute(attrName, attrValue) {
    	if (attrValue != null) {
    		context.set(Scope.SESSION, "urn:ibm:security:asf:response:token:attributes", attrName, attrValue);
    	}
    }
    
    function getPrincipalNameFromSAN(san) {
    	let result = null;
    	if (san != null && 
    		san["array"] != null &&
    		san["array"].length > 0) {
    		for (let i = 0; i < san["array"].length && result == null; i++) {
    			let aObj = san["array"][i];
    			if (aObj["other"] != null && 
    				aObj["other"]["oid"] == "1.3.6.1.4.1.311.20.2.3" &&
    				aObj["other"]["value"] != null &&
    				aObj["other"]["value"]["utf8str"] != null &&
    				aObj["other"]["value"]["utf8str"]["str"] != null) {
    				result = aObj["other"]["value"]["utf8str"]["str"];		
    			}
    		}		
    	}
    	return result;
    }
    
    let headerToAttribute = {
    	"cert": "cert",
    	"subjectcn": "SubjectCN",
    	"fingerprint": "fingerprint",
    	"subjectdn": "subjectDN",
    	"issuerdn": "issuerDN",
    	"subjectorganizationalunit": "subjectOU",
    	"alternativednsname": "alternativeDNSName",
    	"alternativeipaddress": "alternativeIPAddress",
    	"alternativeuri": "alternativeURI",
    	"alternativeemail": "alternativeEmail"
    };
    
    let headerMap = {};
    
    Object.keys(headerToAttribute).forEach((x) => {
    	let val = getRequestHeader(x);
    	if (val != null) {
    		headerMap[x] = ''+val;
    	}
    });
    
    IDMappingExtUtils.traceString("Entering CertEAI Infomap");
    
    // useful trace
    IDMappingExtUtils.traceString("CertEAI headers: " + JSON.stringify(headerMap));
    let eaiResult = false;
    
    if (headerMap["cert"] != null) {
    
    	// use the jsrsasign library to parse the x509 cert and extract SAN details
    	let mycert = new X509();
    	mycert.readCertHex(b64tohex(headerMap["cert"]));
    
    	let san = mycert.getExtSubjectAltName();
    	if (san != null) {
    		IDMappingExtUtils.traceString("SAN: " + JSON.stringify(san));
    	
    		let sanUPN = getPrincipalNameFromSAN(san);
    		if (sanUPN != null) {
    			// login as the username from UPN
    			IDMappingExtUtils.traceString("logging in as: " + sanUPN);
    			addOptionalResponseAttribute("username", sanUPN);
    			addOptionalResponseAttribute("AUTHENTICATION_LEVEL", "1");	
    			addOptionalResponseAttribute("san", JSON.stringify(san));
    			eaiResult = true;	
    		} else {
    			IDMappingExtUtils.traceString("Certificate SAN did not contain valid UPN");
    		}
    	}
    } else {
    	IDMappingExtUtils.traceString("Certificate information unavailable");
    }
    success.setValue(eaiResult);
    

    The configuration of it in the web reverse proxy is something like this:

    [certificate]
    accept-client-certs = required 
    eai-uri = /mga/sps/authsvc/policy/certeai
    eai-data = Base64Certificate:cert
    

    That should be plenty to get you started.

    Regards,
    Shane.



    ------------------------------
    Shane Weeden
    IBM
    ------------------------------



  • 4.  RE: User Mapping for LMI Certificate authentication

    Posted Tue March 14, 2023 07:22 AM
    Hello, Shane,

    Thanks for your reply and the information.

    In my case, it is the certificate authentication for the ISVA LMI. I am not sure if I could use a similar code there. The management authentication configuration portion of the LMI setup provides an option to use javascript to do user mapping. I am not sure what all functions and libraries are available when writing code for that user mapping. 

    Thanks,

    Rakesh





  • 5.  RE: User Mapping for LMI Certificate authentication

    Posted Tue March 14, 2023 10:46 PM
    Sorry about that - I missed "LMI" :)

    Anyway, this is not an area of ISVA I am intimately familiar with, but I have experiemented a bit and here's something to get started.

    First, I believe you will need to have System -> Administrator Settings -> Validate Client Certificate Identity turned ON before the user mapping function can work. 

    In my experimentation with the user mapping function, it appears it can only contain just one function "mapUser" (documentation here: https://www.ibm.com/docs/en/sva/10.0.5?topic=settings-configuring-management-authentication) and it should return the *username* (not the DN) of the user to authenticate into the LMI.

    You can access the X509Certificate as its *Java class* (java.security.cert.X509Certificate) via:
    var cert = props.get("cert");

    You can also use java.lang.System.out.println("some string") to print to the LMI message and trace logs (which you can monitor from the command line using isam logs monitor).
    Finally you can also turn on LMI trace (System -> Administrator Settings -> LMI Tracing) for the component com.ibm.mesa.security.authentication.modules=all to get trace of the authentication process (then monitor the LMI trace.log).

    Here's a more elaborate example that will print out the PEM version of the client certificate, then just maps everything to "admin". You should be able to use other standard Java APIs available from X509Certificate to parse other parts of the data.
    function mapUser(props) {
        // dump all available properties for debugging
        java.lang.System.out.println("props: " + props.toString());
    
        // this is java.security.cert.X509Certificate
        var cert = props.get("cert");
    
        // build the PEM version
        var b64encoder = java.util.Base64.getMimeEncoder(64, [ 0x0A ]);
        var pemcert = "-----BEGIN CERTIFICATE-----"
            + java.lang.System.lineSeparator();
            + b64encoder.encodeToString(cert.getEncoded());
            + java.lang.System.lineSeparator();
            + "-----END CERTIFICATE-----"
            + java.lang.System.lineSeparator();
    
        // and log it
        java.lang.System.out.println(pemcert);
    
        return "admin";
    }
    



    ------------------------------
    Shane Weeden
    IBM
    ------------------------------



  • 6.  RE: User Mapping for LMI Certificate authentication

    Posted Wed November 22, 2023 09:53 AM

    Hi Shane and Rakesh,

    We have a similar use case so I would like ask a follow up question here.

    We also need to map the certificate to the user in Active directory but our organization does certificate pinning. We would need to do a ldap callout from to get the user object with that particular certificate. This is supported in the reverse proxy using xsl certificate mapping and in mapping rules, but I have not found a way to do this for authenticating to the LMI. 

    Is there a way to make an ldap callout from this User Mapping Script for LMI authentication?

    /Peter




    ------------------------------
    Peter Lindqvist
    ------------------------------



  • 7.  RE: User Mapping for LMI Certificate authentication

    Posted Thu November 23, 2023 06:12 PM

    I think you can, using the native Java JNDI interface to lookup ldap. This is a very rough example (not much error handling, no LDAP context cleanup, etc) but should give you the idea of how to do it. In my case I just searched for "testuser" in the embedded LDAP on my appliance, but you can change this around as needed. Also my example always just maps to "admin@local" because I was only trying to prove you could do an LDAP search.

    
    function mapUser(props) {
        // dump all available properties for debugging
        java.lang.System.out.println("props: " + props.toString());
    
        // this is java.security.cert.X509Certificate
        var cert = props.get("cert");
    
        // build the PEM version
        var b64encoder = java.util.Base64.getMimeEncoder(64, [ 0x0A ]);
        var pemcert = "-----BEGIN CERTIFICATE-----"
            + java.lang.System.lineSeparator();
            + b64encoder.encodeToString(cert.getEncoded());
            + java.lang.System.lineSeparator();
            + "-----END CERTIFICATE-----"
            + java.lang.System.lineSeparator();
    
        // and log it
        java.lang.System.out.println(pemcert);
    
        // LDAP search example
    
        var ldapEnv = new java.util.Hashtable();
        ldapEnv.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        ldapEnv.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
        ldapEnv.put(javax.naming.Context.SECURITY_PRINCIPAL, "cn=root,secauthority=default");
        ldapEnv.put(javax.naming.Context.SECURITY_CREDENTIALS, "passw0rd");
        ldapEnv.put(javax.naming.Context.PROVIDER_URL, "ldaps://localhost:636");
        var ldapCtx = new javax.naming.directory.InitialDirContext(ldapEnv);
    
        var baseDN = "dc=iswga";
        var searchForUser = "testuser";
        var searchFilter = "(uid="+searchForUser+")";
        var searchControls = new javax.naming.directory.SearchControls();
        searchControls.setSearchScope(javax.naming.directory.SearchControls.SUBTREE_SCOPE);
    
        // NamingEnumeration<SearchResult> 
        java.lang.System.out.println("About to perform search for: " + searchForUser);
        var searchResults = ldapCtx.search("dc=iswga", searchFilter, searchControls);
        if (searchResults != null && searchResults.hasMoreElements()) {
            var searchResult = searchResults.nextElement();
    
            // make sure there is not another item available, there should be only 1 match
            if(searchResults.hasMoreElements()) {
                java.lang.System.out.println("Matched multiple users for: " + searchForUser);
            } else {
                // print contents of searchResult
                java.lang.System.out.println("Found user searchResult: " + searchResult.toString());
            }
        } else {
            java.lang.System.out.println("No search results for user: " + searchForUser);
        }
    
    
        return "admin@local";
    }
    


    ------------------------------
    Shane Weeden
    IBM
    ------------------------------



  • 8.  RE: User Mapping for LMI Certificate authentication

    Posted Mon November 27, 2023 09:38 AM

    Thank you, that is exactly what I needed.



    ------------------------------
    Peter Lindqvist
    ------------------------------



  • 9.  RE: User Mapping for LMI Certificate authentication

    Posted Thu March 16, 2023 10:48 AM

    Hello Rakesh,

    Maybe this example can help you out, depending on what x509 attribute you want to extract the username from.

    The script below was written by isva support team, to seperate the userID from the otheName attribute, which was ASN encoded in my case.

    function mapUser(props) {
    // example script to extract otherName attribute from san data
    // this code expects it to be an email address
    // it extracts the part before the @ and uses it to return the mapped user
    // if certificate doesn't contain san data or other name attribute
    // it uses the cn to return the mapped user
    //start ASN1 code
    // minimal asn 1 parser - extracts first string it finds - doesnt decode oids - skips them
    var ID   = new Array();
    var NAME = new Array();
    // ASN1Decode will return the first occurrence of the following types of string
    ID['UTF8String']       = 0x0c;
    ID['IA5String']        = 0x16;
    for ( var i in ID ){ NAME[ID[i]] = i; }
    var first_string = "";
    var asn1_error = "";
     function ASN1Decode(data)
    { 
      var idx = 0;
      first_string = "";
      asn1_error = "";
      while ( idx < data.length ){
        var tagType = data[idx++];
        var isSeq = tagType & 32; 
        var isContext = tagType & 128;
        var tag = tagType & 31;
        var len = 0;
        if ( tag != 0x5){  // Ignore NULL
          if ( data[idx] & 128 ){
            var lenLength = data[idx++] & 127;
            if ( lenLength > 2 ) {
              asn1_error="too long";
              return;
            }
            for(var il = 0;il < lenLength; il++) {
            len = (len << 8) + data[idx++];
            }
          }
          else {// 1 byte length
            len = data[idx++];
          }
          if ( len > data.length - idx ) {
            esn1_error="length error";
            return ;
          }
        }
        else {
          idx++;
        }
        // process value
        if ( len ){
          val = data.slice( idx, idx+len);
          idx += len;
        }
        if(isSeq) {
            ASN1Decode(val); 
            if(asn1_error != "") break;
        } else if( !isContext) {
            ASN1getValue( tag , val);
        }
        if(first_string != "") break;
      }
    }
    function ASN1getValue(tag, data)
    {
      if ( NAME[tag] != undefined && NAME[tag].match(/(String)$/) ) {
        for (var k = 0; k < data.length; k++ ){
          first_string += String.fromCharCode(data[k]);
        }
        if (NAME[tag] == "UTF8String") {
          first_string  = decodeURIComponent( escape( first_string ) );
        }
      }
    }
    // end ASN1 code
     var user = props.get("cn");
     var sans = props.get("san");
     if(sans != null) {
    // sans is a Collection  - we need to iterate thru to get the element we want
    // each element is a list of 2 items - key and value
    // there may be more than 1 list in the collection with the same key
    // this script only uses the first list containing a particular  key
    // key is in range 0..8
    // value will have different type depending on key - doc says rfc822name (1) dnsName (2) and uri (6) are strings
    // otherName (0) x400Address (3) and ediPartyName (5) are byte arrays ASN.1 DER encoded
    // ASN1Decode finds the first string in the byte array and puts it in the variable first_string
    // if there was a parsing error asn1_error will not be empty
        for (var iterator = sans.iterator(); iterator.hasNext();) {
            var list = iterator.next();
            var key = list.get(0);
            if(key == 0) { // otherName
              var val = list.get(1);
              if(val != null) { 
                 // val is an ASN.1 encoded  byte array
                 ASN1Decode(val);
                 var index = first_string.indexOf("@");
                 if(index != -1) { //contained '@'
                    return props.get("userAttribute") + "=" + first_string.substring(0,index)+  ",cn=Users," + props.get("baseDN");
                 }
            }// val not null
          }// key
        } // iterator
    } // sans not null
    // fall thru to default
      return props.get("userAttribute") + "=" + user + "," + props.get("baseDN");
    }
    


    ------------------------------
    Jan Weiberg
    ------------------------------



  • 10.  RE: User Mapping for LMI Certificate authentication

    Posted Thu March 16, 2023 11:03 AM

    Thank you so much, Jan! Though this is much more than I needed, but you never know when I may come across more complex requirements. I will keep this for my future reference.



    ------------------------------
    Rakesh Vohra
    Great Falls VA
    2405683495
    ------------------------------



  • 11.  RE: User Mapping for LMI Certificate authentication

    Posted Thu November 02, 2023 12:13 PM

    Hi Shane,

    I am trying to make this work, but I am bit confused about which "jsrsasign" to download en use in the mapping rule.

    When I use the latest version "jsrsasign(all) 10.8.6 (2023-04-26)" I get an error message when the mapping rule is hit:

    {
        "exceptionMsg": "ReferenceError: \"navigator\" is not defined. (jsrsasign#197)",
        "state": "",
        "message": "",
        "mechanism": "urn:ibm:security:authentication:asf:mechanism:certcheck"
    }

    Apparently the code expects to run in a browser.

    Do you have a suggestion about this?

    Regards,
    Paul van den Brink



    ------------------------------
    Paul van den Brink
    ------------------------------



  • 12.  RE: User Mapping for LMI Certificate authentication

    Posted Thu November 02, 2023 04:16 PM
    Edited by Shane Weeden Thu November 02, 2023 04:16 PM

    Edit the file and try putting this at the start:

    /*
     * The following two variables are declared to work around assumptions in included libraries that
     * think we are running in a browser
     */
    if (typeof navigator == "undefined" || !navigator) {var navigator = { "appName": null, "appVersion": null }};
    if (typeof navigator == "window" || !window) {var window = { }};
    



    ------------------------------
    Shane Weeden
    IBM
    ------------------------------