com.sun.identity.security.cert.AMCRLStore.java Source code

Java tutorial

Introduction

Here is the source code for com.sun.identity.security.cert.AMCRLStore.java

Source

/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: AMCRLStore.java,v 1.7 2009/01/28 05:35:12 ww203982 Exp $
 *
 */

/**
 * Portions Copyrighted 2013 ForgeRock Inc
 */

package com.sun.identity.security.cert;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import com.sun.identity.shared.ldap.LDAPAttribute;
import com.sun.identity.shared.ldap.LDAPAttributeSet;
import com.sun.identity.shared.ldap.LDAPConnection;
import com.sun.identity.shared.ldap.LDAPEntry;
import com.sun.identity.shared.ldap.LDAPException;
import com.sun.identity.shared.ldap.LDAPModification;
import com.sun.identity.shared.ldap.LDAPSearchResults;
import com.sun.identity.shared.ldap.LDAPUrl;
import sun.security.x509.CertificateExtensions;
import sun.security.x509.GeneralNames;
import sun.security.x509.PKIXExtensions;
import sun.security.x509.X509CertImpl;

import sun.security.x509.CRLDistributionPointsExtension;
import sun.security.x509.DistributionPoint;

import com.iplanet.security.x509.IssuingDistributionPointExtension;
import com.iplanet.security.x509.X500Name;
//import com.iplanet.am.util.AMURLEncDec;
import com.sun.identity.shared.encode.URLEncDec;

import com.sun.identity.common.HttpURLConnectionManager;
import org.apache.commons.lang.ArrayUtils;

/**
* The class is used to manage crl store in LDAP server
* This class does get crl and update crl with CRLDistribution
* PointsExtension in client certificate or IssuingDistribution 
* PointExtension in CRL. This class should be used 
* in order to manage CRL store in LDAP
* id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::=  { id-ce 31 }
*
* RLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
*
* DistributionPoint ::= SEQUENCE {
*        distributionPoint       [0]     DistributionPointName OPTIONAL,
*        reasons                 [1]     ReasonFlags OPTIONAL,
*        cRLIssuer               [2]     GeneralNames OPTIONAL }
*
* DistributionPointName ::= CHOICE {
*        fullName                [0]     GeneralNames,
*        nameRelativeToCRLIssuer [1]     RelativeDistinguishedName }
*
* ReasonFlags ::= BIT STRING {
*        unused                  (0),
*        keyCompromise           (1),
*        cACompromise            (2),
*        affiliationChanged      (3),
*        superseded              (4),
*        cessationOfOperation    (5),
*        certificateHold         (6),
*        privilegeWithdrawn      (7),
*        aACompromise            (8) }
* 
*/

public class AMCRLStore extends AMCertStore {

    // In memory CRL cache
    private static Hashtable cachedcrls = new Hashtable();

    private String mCrlAttrName = null;

    /**
     * Class AMCRLStore is special cased CRL store for LDAP.
     * A AMCRLStore instance has to have all the information for ldap 
     * and all the access information for CRLDistributionPointExtension and
     * CRLIssuingDistributionPoint Extension
     *
     * @param param
     */
    public AMCRLStore(AMLDAPCertStoreParameters param) {
        super(param);
    }

    /**
     * Checks certificate and returns corresponding stored CRL in ldap store
     * @param certificate
     */
    public X509CRL getCRL(X509Certificate certificate) throws IOException {
        LDAPEntry crlEntry = null;
        X509CRL crl = null;

        if (storeParam.isDoCRLCaching()) {
            if (debug.messageEnabled()) {
                debug.message("AMCRLStore.getCRL: Trying to get CRL from cache");
            }
            crl = (X509CRL) getCRLFromCache(certificate);
        }

        LDAPConnection ldc = getConnection();

        try {
            if (crl == null) {
                if (debug.messageEnabled()) {
                    debug.message("AMCRLStore.getCRL: crl is null");
                }
                crlEntry = getLdapEntry(ldc);
                crl = getCRLFromEntry(crlEntry);
            }

            if (storeParam.isDoUpdateCRLs() && needCRLUpdate(crl)) {
                if (debug.messageEnabled()) {
                    debug.message("AMCRLStore.getCRL: need CRL update");
                }

                X509CRL tmpcrl = null;
                IssuingDistributionPointExtension crlIDPExt = null;
                try {
                    if (crl != null) {
                        crlIDPExt = getCRLIDPExt(crl);
                    }
                } catch (Exception e) {
                    debug.message("AMCRLStore.getCRL: crlIDPExt is null");
                }

                CRLDistributionPointsExtension crlDPExt = null;
                try {
                    crlDPExt = getCRLDPExt(certificate);
                } catch (Exception e) {
                    debug.message("AMCRLStore.getCRL: crlDPExt is null");
                }

                if ((tmpcrl == null) && (crlIDPExt != null)) {
                    tmpcrl = getUpdateCRLFromCrlIDP(crlIDPExt);
                }

                if ((tmpcrl == null) && (crlDPExt != null)) {
                    tmpcrl = getUpdateCRLFromCrlDP(crlDPExt);
                }

                if (tmpcrl != null) {
                    if (crlEntry == null) {
                        crlEntry = getLdapEntry(ldc);
                    }

                    if (debug.messageEnabled()) {
                        debug.message("AMCRLStore.getCRL: new crl = " + tmpcrl);
                    }

                    if (crlEntry != null) {
                        updateCRL(ldc, crlEntry.getDN().toString(), tmpcrl.getEncoded());
                    }
                }
                crl = tmpcrl;
            }

            if (storeParam.isDoCRLCaching()) {
                if (debug.messageEnabled()) {
                    debug.message("AMCRLStore.getCRL: Updating CRL cache");
                }
                updateCRLCache(certificate, crl);
            }

        } catch (Exception e) {
            debug.error("AMCRLStore.getCRL: Error in getting CRL : ", e);
        } finally {
            if ((ldc != null) && (ldc.isConnected())) {
                try {
                    ldc.disconnect();
                } catch (LDAPException ex) {
                    //ignored
                }
            }
        }

        return crl;
    }

    /**
     * Checks certificate and returns corresponding stored CRL 
     * in cached CRL store
     * @param certificate
     */
    public X509CRL getCRLFromCache(X509Certificate certificate) throws IOException {
        X500Name issuerDN = getIssuerDN(certificate);

        X509CRL crl = null;

        crl = (X509CRL) cachedcrls.get(issuerDN.toString());

        return crl;
    }

    /**
     * Checks certificate and update CRL in cached CRL store
     * @param certificate
     */
    public void updateCRLCache(X509Certificate certificate, X509CRL crl) throws IOException {
        X500Name issuerDN = getIssuerDN(certificate);

        if (crl == null) {
            cachedcrls.remove(issuerDN.toString());
        } else {
            cachedcrls.put(issuerDN.toString(), crl);
        }
    }

    private X509CRL getCRLFromEntry(LDAPEntry crlEntry) throws Exception {

        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.getCRLFromEntry:");
        }

        if (crlEntry == null) {
            return null;
        }

        LDAPAttributeSet attributeSet = crlEntry.getAttributeSet();
        LDAPAttribute crlAttribute = null;
        X509CRL crl = null;

        try {
            /*
             * Retrieve the certificate revocation list if available.
             */

            if (mCrlAttrName == null) {
                crlAttribute = attributeSet.getAttribute("certificaterevocationlist");
                if (crlAttribute == null) {
                    crlAttribute = attributeSet.getAttribute("certificaterevocationlist;binary");
                    if (crlAttribute == null) {
                        debug.error("No CRL Cache is configured");
                        return null;
                    }
                }

                mCrlAttrName = crlAttribute.getName();
            } else {
                crlAttribute = attributeSet.getAttribute(mCrlAttrName);
            }

            if (crlAttribute.size() > 1) {
                debug.error("More than one CRL entries are configured");
                return null;
            }
        } catch (Exception e) {
            debug.error("Error in getting Cached CRL");
            return null;
        }

        try {
            Enumeration Crls = crlAttribute.getByteValues();
            byte[] bytes = (byte[]) Crls.nextElement();
            if (debug.messageEnabled()) {
                debug.message("AMCRLStore.getCRLFromEntry: crl size = " + bytes.length);
            }
            cf = CertificateFactory.getInstance("X.509");
            crl = (X509CRL) cf.generateCRL(new ByteArrayInputStream(bytes));
        } catch (Exception e) {
            debug.error("Certificate: CertRevoked = ", e);
        }

        return crl;
    }

    /**
     * It checks whether the certificate has CRLDistributionPointsExtension
     * or not. If there is, it returns the extension.
     * @param X509Certificate certificate
     */
    private CRLDistributionPointsExtension getCRLDPExt(X509Certificate certificate) {
        CRLDistributionPointsExtension dpExt = null;
        CertificateExtensions exts = null;

        try {
            X509CertImpl certImpl = new X509CertImpl(certificate.getEncoded());
            dpExt = certImpl.getCRLDistributionPointsExtension();
        } catch (Exception e) {
            debug.error("Error finding CRL distribution Point configured: ", e);
        }

        return dpExt;
    }

    /**
     * It checks whether the crl has IssuingDistributionPointExtension
     * or not. If there is, it returns the extension.
     * @param X509CRL crl
     */
    private IssuingDistributionPointExtension getCRLIDPExt(X509CRL crl) {
        IssuingDistributionPointExtension idpExt = null;

        if (crl == null) {
            return null;
        }

        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.getCRLIDPExt: crl = " + crl);
        }
        try {
            byte[] ext = crl.getExtensionValue(PKIXExtensions.IssuingDistributionPoint_Id.toString());
            if (ext != null) {
                idpExt = new IssuingDistributionPointExtension(ext);
            }
        } catch (Exception e) {
            debug.error("Error finding CRL distribution Point configured: ", e);
        }

        return idpExt;
    }

    /**
     * It updates CRL under the dn in the directory server.
     * It retrieves CRL distribution points from the parameter 
     * CRLDistributionPointsExtension dpExt.
     * @param CRLDistributionPointsExtension dpExt
     */
    private synchronized X509CRL getUpdateCRLFromCrlDP(CRLDistributionPointsExtension dpExt) {
        // Get CRL Distribution points
        if (dpExt == null) {
            return null;
        }

        List dps = null;
        try {
            dps = (List) dpExt.get(CRLDistributionPointsExtension.POINTS);
        } catch (IOException ioex) {
            if (debug.warningEnabled()) {
                debug.warning("AMCRLStore.getUpdateCRLFromCrlDP: ", ioex);
            }
        }

        if (dps == null || dps.isEmpty()) {
            return null;
        }

        for (Iterator iter = dps.iterator(); iter.hasNext();) {
            DistributionPoint dp = (DistributionPoint) iter.next();
            GeneralNames gName = dp.getFullName();
            if (debug.messageEnabled()) {
                debug.message("AMCRLStore.getUpdateCRLFromCrlDP: DP = " + gName);
            }

            byte[] Crls = getCRLsFromGeneralNames(gName);
            if (Crls != null && Crls.length > 0) {
                try {
                    return (X509CRL) cf.generateCRL(new ByteArrayInputStream(Crls));
                } catch (Exception ex) {
                    if (debug.warningEnabled()) {
                        debug.warning("AMCRLStore.getUpdateCRLFromCrlDP: " + "Error in generating X509CRL", ex);
                    }
                }
            }
        }

        return null;
    }

    /**
     * It updates CRL under the dn in the directory server.
     * It retrieves CRL distribution points from the parameter 
     * CRLDistributionPointsExtension dpExt.
     * @param LDAPConnection ldc
     * @param String dn
     * @param CRLDistributionPointsExtension dpExt
     */
    private synchronized X509CRL getUpdateCRLFromCrlIDP(IssuingDistributionPointExtension idpExt) {

        GeneralNames gName = idpExt.getFullName();
        if (gName == null) {
            return null;
        }

        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.getUpdateCRLFromCrlIDP: gName = " + gName);
        }
        byte[] Crls = getCRLsFromGeneralNames(gName);

        X509CRL crl = null;
        if (Crls != null) {
            try {
                crl = (X509CRL) cf.generateCRL(new ByteArrayInputStream(Crls));
            } catch (Exception e) {
                debug.error("Error in generating X509CRL" + e.toString());
            }
        }

        return crl;
    }

    private byte[] getCRLsFromGeneralNames(GeneralNames gName) {
        byte[] Crls = null;
        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.getCRLsFromGeneralNames: gNames.size = " + gName.size());
        }
        int idx = 0;
        do {
            String uri = gName.get(idx++).toString().trim();
            String protocol = uri.toLowerCase();
            int proto_pos;
            if ((proto_pos = protocol.indexOf("http")) == -1) {
                if ((proto_pos = protocol.indexOf("https")) == -1) {
                    if ((proto_pos = protocol.indexOf("ldap")) == -1) {
                        if ((proto_pos = protocol.indexOf("ldaps")) == -1) {
                            continue;
                        }
                    }
                }
            }

            uri = uri.substring(proto_pos, uri.length());
            if (debug.messageEnabled()) {
                debug.message("DP Name : " + uri);
            }
            Crls = getCRLByURI(uri);
        } while ((Crls != null) && (idx < gName.size()));

        return Crls;
    }

    /**
     * It replaces attribute value under the DN.
     * It is used to replace old CRL with new one.
     * @param LDAPConnection ldc
     * @param String dn
     * @param LDAPAttribute attr
     */
    private boolean updateCRL(LDAPConnection ldc, String dn, byte[] crls) {
        LDAPAttribute crlAttribute = new LDAPAttribute(mCrlAttrName, crls);
        LDAPModification mod = new LDAPModification(LDAPModification.REPLACE, crlAttribute);
        try {
            ldc.modify(dn, mod);
        } catch (LDAPException e) {
            debug.error("Error updating CRL Cache : ", e);
            return false;
        }

        return true;
    }

    /**
     * It is checking uri's protocol.
     * Protocol has to be http(s) or ldap.
     * Based on checked protocol, it gets new CRL by invking 
     * getCRLByLdapURI() or getCRLByHttpURI()
     * @param String uri
     */
    private byte[] getCRLByURI(String uri) {
        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.getCRLByURI : uri = " + uri);
        }
        if (uri == null) {
            return null;
        }

        String protocol = uri.trim().toLowerCase();
        if (protocol.startsWith("http") || protocol.startsWith("https")) {
            return getCRLByHttpURI(uri);
        } else if (protocol.startsWith("ldap") || protocol.startsWith("ldaps")) {
            return getCRLByLdapURI(uri);
        }

        return null;
    }

    /**
     * It gets the new CRL from ldap server. 
     * If it is ldap URI, the URI has to be a dn that can be accessed 
     * with ldap anonymous bind. 
     * (example : ldap://server:port/uid=ca,o=company.com) 
     * This dn entry has to have CRL in attribute certificaterevocationlist 
     * or certificaterevocationlist;binary.
     * 
     * @param String uri
     */
    private byte[] getCRLByLdapURI(String uri) {

        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.getCRLByLdapURI: uri = " + uri);
        }

        LDAPUrl url = null;
        LDAPConnection ldc = null;
        byte[] crl = null;

        try {
            url = new LDAPUrl(uri);
            if (debug.messageEnabled()) {
                debug.message("AMCRLStore.getCRLByLdapURI: url.dn = " + url.getDN());
            }
            // Check ldap over SSL
            if (url.isSecure()) {
                ldc = new LDAPConnection(storeParam.getSecureSocketFactory());
            } else { // non-ssl
                ldc = new LDAPConnection();
            }

            ldc.connect(url.getHost(), url.getPort(), "", "");
            LDAPSearchResults results = ldc.search(url.getDN().toString(), LDAPConnection.SCOPE_BASE, null, null,
                    false);

            if (results == null || results.hasMoreElements() == false) {
                debug.error("verifyCertificate - " + "No CRL distribution Point configured");
                return null;
            }

            LDAPEntry entry = results.next();
            LDAPAttributeSet attributeSet = entry.getAttributeSet();

            /* 
            * Retrieve the certificate revocation list if available.
            */
            LDAPAttribute crlAttribute = attributeSet.getAttribute("certificaterevocationlist");
            if (crlAttribute == null) {
                crlAttribute = attributeSet.getAttribute("certificaterevocationlist;binary");
                if (crlAttribute == null) {
                    debug.error("verifyCertificate - " + "No CRL distribution Point configured");
                    return null;
                }
            }

            crl = (byte[]) crlAttribute.getByteValues().nextElement();

        } catch (Exception e) {
            debug.error("getCRLByLdapURI : Error in getting CRL", e);
        } finally {
            if ((ldc != null) && (ldc.isConnected())) {
                try {
                    ldc.disconnect();
                } catch (LDAPException ex) {
                    //ignored
                }
            }
        }

        return crl;
    }

    private byte[] getCRLByHttpURI(String url) {
        String argString = ""; //default
        StringBuffer params = null;
        HttpURLConnection con = null;
        byte[] crl = null;

        String uriParamsCRL = storeParam.getURIParams();

        try {

            if (uriParamsCRL != null) {
                params = new StringBuffer();
                StringTokenizer st1 = new StringTokenizer(uriParamsCRL, ",");
                while (st1.hasMoreTokens()) {
                    String token = st1.nextToken();
                    StringTokenizer st2 = new StringTokenizer(token, "=");
                    if (st2.countTokens() == 2) {
                        String param = st2.nextToken();
                        String value = st2.nextToken();
                        params.append(URLEncDec.encode(param) + "=" + URLEncDec.encode(value));
                    } else {
                        continue;
                    }

                    if (st1.hasMoreTokens()) {
                        params.append("&");
                    }
                }
            }

            URL uri = new URL(url);
            con = HttpURLConnectionManager.getConnection(uri);

            // Prepare for both input and output
            con.setDoInput(true);

            // Turn off Caching
            con.setUseCaches(false);

            if (params != null) {
                byte[] paramsBytes = params.toString().trim().getBytes("UTF-8");
                if (paramsBytes.length > 0) {
                    con.setDoOutput(true);
                    con.setRequestProperty("Content-Length", Integer.toString(paramsBytes.length));

                    // Write the arguments as post data
                    BufferedOutputStream out = new BufferedOutputStream(con.getOutputStream());
                    out.write(paramsBytes, 0, paramsBytes.length);
                    out.flush();
                    out.close();
                }
            }
            // Input ...
            InputStream in = con.getInputStream();

            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            int len;
            byte[] buf = new byte[1024];
            while ((len = in.read(buf, 0, buf.length)) != -1) {
                bos.write(buf, 0, len);
            }
            crl = bos.toByteArray();

            if (debug.messageEnabled()) {
                debug.message("AMCRLStore.getCRLByHttpURI: crl.length = " + crl.length);
            }
        } catch (Exception e) {
            debug.error("getCRLByHttpURI : Error in getting CRL", e);
        }

        return crl;
    }

    // It returns NextCRLUpdate for current cached CRL
    // It gets CRL from crlAttribue member variable
    private boolean needCRLUpdate(X509CRL crl) {
        Date nextCRLUpdate = null;
        if (crl == null) {
            return true;
        }
        // Check CRLNextUpdate in CRL
        nextCRLUpdate = crl.getNextUpdate();
        if (debug.messageEnabled()) {
            debug.message("AMCRLStore.needCRLUpdate: nextCrlUpdate = " + nextCRLUpdate);
        }

        return ((nextCRLUpdate != null) && nextCRLUpdate.before(new Date()));
    }

    /**
     * It gets the new CRL from ldap server. 
     * If it is ldap URI, the URI has to be a dn that can be accessed 
     * with ldap anonymous bind. 
     * (example : ldap://server:port/uid=ca,o=company.com) 
     * This dn entry has to have CRL in attribute certificaterevocationlist 
     * or certificaterevocationlist;binary.
     * 
     * if attrNames does only contain one value the ldap search filter will be
     * (attrName=Value_of_the_corresponding_Attribute_from_SubjectDN)
     * e.g. SubjectDN of issuer cert 'C=BE, CN=Citizen CA, serialNumber=201007'
     * attrNames is 'CN', search filter used will be (CN=Citizen CA)
     * 
     * if attrNames does contain serveral values the ldap search filter value will be
     * a comma separated list of name attribute values, the search attribute will be 'cn'
     * (cn="attrNames[0]=Value_of_the_corresponding_Attribute_from_SubjectDN,
     * attrNames[1]=Value_of_the_corresponding_Attribute_from_SubjectDN")
     * 
     * e.g. SubjectDN of issuer cert 'C=BE, CN=Citizen CA, serialNumber=201007'
     * attrNames is {"CN","serialNumber"}, search filter used will be
     * (cn=CN=Citizen CA,serialNumber=201007)
     * 
     * The order of the values of attrNames matter as they must match the value of the
     * 'cn' attribute of a crlDistributionPoint entry in the directory server
     * 
     * @param ldapParam 
     * @param cert
     * @param attrNames, attributes names from the subjectDN of the issuer cert
     */
    static public X509CRL getCRL(AMLDAPCertStoreParameters ldapParam, X509Certificate cert, String... attrNames) {
        X509CRL crl = null;

        try {

            if (!ArrayUtils.isEmpty(attrNames)) {

                X500Name dn = null;

                try {
                    dn = AMCRLStore.getIssuerDN(cert);
                } catch (Exception ex) {
                    if (debug.messageEnabled()) {
                        debug.message("AMCRLStore:getCRL " + "Certificate - getIssuerDN: ", ex);
                    }
                    return crl;
                }

                String searchFilter = null;

                if (attrNames.length < 2) {
                    /*
                     * Get the CN of the input certificate
                     */
                    String attrValue = null;

                    if (null != dn) {
                        // Retrieve attribute value of the attribute name
                        attrValue = dn.getAttributeValue(attrNames[0]);
                    }

                    if (null == attrValue) {
                        return crl;
                    }

                    searchFilter = setSearchFilter(attrNames[0], attrValue);

                } else {

                    String searchFilterValue = buildSearchFilterValue(attrNames, dn);

                    if (searchFilterValue.isEmpty()) {
                        return crl;
                    }
                    searchFilter = setSearchFilter("cn", searchFilterValue);
                }

                if (debug.messageEnabled()) {
                    debug.message("AMCRLStore:getCRL using searchFilter " + searchFilter);
                }

                /*
                 * Lookup the certificate in the LDAP certificate directory
                 */

                ldapParam.setSearchFilter(searchFilter);

                AMCRLStore store = new AMCRLStore(ldapParam);
                crl = store.getCRL(cert);
            }

        } catch (Exception e) {
            debug.error("AMCRLStore:getCRL ", e);
        }

        return crl;
    }

    private static String buildSearchFilterValue(String[] attrNames, X500Name dn) throws IOException {
        StringBuilder searchFilterBuilder = new StringBuilder();
        for (int i = 0; i < attrNames.length; i++) {
            String attrName = attrNames[i];
            String attrValue = dn.getAttributeValue(attrName);

            if (null != attrValue) {
                searchFilterBuilder.append(attrName);
                searchFilterBuilder.append("=");
                searchFilterBuilder.append(attrValue);
                if (i < attrNames.length - 1) {
                    searchFilterBuilder.append(",");
                }
            }
        }
        return searchFilterBuilder.toString();
    }
}