//: "The contents of this file are subject to the Mozilla Public License //: Version 1.1 (the "License"); you may not use this file except in //: compliance with the License. You may obtain a copy of the License at //: http://www.mozilla.org/MPL/ //: //: Software distributed under the License is distributed on an "AS IS" //: basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the //: License for the specific language governing rights and limitations //: under the License. //: //: The Original Code is Guanxi (http://www.guanxi.uhi.ac.uk). //: //: The Initial Developer of the Original Code is Alistair Young alistair@codebrane.com //: All Rights Reserved. //: package org.guanxi.common.trust; import org.apache.xml.security.signature.ObjectContainer; import org.guanxi.xal.saml_2_0.metadata.*; import org.guanxi.xal.w3.xmldsig.X509DataType; import org.guanxi.xal.w3.xmldsig.KeyInfoType; import org.guanxi.xal.saml_1_0.protocol.ResponseDocument; import org.guanxi.common.GuanxiException; import org.apache.log4j.Logger; import org.apache.xml.security.signature.XMLSignature; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.utils.Constants; import org.apache.xmlbeans.XmlObject; import org.w3c.dom.*; import org.bouncycastle.openssl.PEMReader; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.*; import java.security.cert.*; import java.security.*; import java.security.interfaces.RSAPublicKey; import java.security.interfaces.DSAPublicKey; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.*; import java.net.URL; import java.net.MalformedURLException; /** * Shibboleth and SAML trust functionality * * @author alistair * @author matthew */ public class TrustUtils { public static final int ENTITY_TYPE_SSO = 1; public static final int ENTITY_TYPE_AA = 2; public static final int ENTITY_TYPE_SP = 3; /** Our logger */ private static final Logger logger = Logger.getLogger(TrustUtils.class.getName()); /** * This holds the path to the Reference node of a signed SAML Response. * This is used to validate the signature in the SAML Response. */ private static XPathExpression referencePath; // Setup the XPath stuff and precompile any expressions static { XPathFactory factory = XPathFactory.newInstance(); XPath xPath = factory.newXPath(); xPath.setNamespaceContext(new SAMLNamespaceContext()); try { referencePath = xPath.compile("//ds:Signature/ds:SignedInfo/ds:Reference"); } catch (XPathExpressionException e) { throw new ExceptionInInitializerError(e); } } /** * Performs trust validation via X509 certificates. The trust is in the context * of a secure connection to an AA as seen by the IdP. * * @param saml2Metadata The metadata for the SP * @param clientCerts The SP's client certificates from the secure connection * @param caCerts The list of CA root certs as trust anchors * @param hostName The hostname for the validation context * @return true if validation succeeds otherwise false * @throws GuanxiException if an error occurs */ public static boolean validateClientCert(EntityDescriptorType saml2Metadata, X509Certificate[] clientCerts, Vector<X509Certificate> caCerts, String hostName) throws GuanxiException { if (validateEmbeddedCert(saml2Metadata, clientCerts, TrustUtils.ENTITY_TYPE_SP)) { return true; } // Try validation via PKIX path validation ArrayList<String> allKeyNames = new ArrayList<String>(); String[] spKeyNames = getKeyNamesFromSPMetadata(saml2Metadata); for (String spKeyName : spKeyNames) { allKeyNames.add(spKeyName); } if (hostName != null) { allKeyNames.add(hostName); } for (String keyName : allKeyNames) { for (X509Certificate clientCert : clientCerts) { if (compareX509SubjectWithKeyName(clientCert, keyName)) { if (validateCertPath(clientCert, caCerts)) { return true; } } } } return false; } /** * Performs explicit key validation to check an entity's message or TLS public keys against those * embedded in SAML2 metadata for the entity. * * @param saml2Metadata The SAML2 metadata for the entity * @param clientCerts The X509 certificates from the message signature or secure connection * @param entityType ENTITY_TYPE_SSO to validate an AuthenticationStatement * ENTITY_TYPE_AA to validate a back channel secure connection to an Attribute Authority * ENTITY_TYPE_SP to validate a back channel secure connection from a Service Provider * @return true if explicit key validation passes, otherwise false * @throws GuanxiException if an error occurs */ public static boolean validateEmbeddedCert(EntityDescriptorType saml2Metadata, X509Certificate[] clientCerts, int entityType) throws GuanxiException { try { CertificateFactory certFactory = CertificateFactory.getInstance("x.509"); KeyDescriptorType[] keyDescriptors = null; if (entityType == ENTITY_TYPE_SSO) { keyDescriptors = saml2Metadata.getIDPSSODescriptorArray(0).getKeyDescriptorArray(); } if (entityType == ENTITY_TYPE_AA) { keyDescriptors = saml2Metadata.getAttributeAuthorityDescriptorArray(0).getKeyDescriptorArray(); } if (entityType == ENTITY_TYPE_SP) { keyDescriptors = saml2Metadata.getSPSSODescriptorArray(0).getKeyDescriptorArray(); } for (KeyDescriptorType keyDescriptor : keyDescriptors) { X509DataType[] x509Datas = keyDescriptor.getKeyInfo().getX509DataArray(); for (X509DataType x509Data : x509Datas) { byte[][] x509CertsBytes = x509Data.getX509CertificateArray(); for (byte[] x509CertBytes : x509CertsBytes) { ByteArrayInputStream certByteStream = new ByteArrayInputStream(x509CertBytes); X509Certificate metadataCert = (X509Certificate)certFactory.generateCertificate(certByteStream); certByteStream.close(); if ((metadataCert.getPublicKey() instanceof DSAPublicKey) && (clientCerts[0].getPublicKey() instanceof DSAPublicKey)) { DSAPublicKey metadataDSA = (DSAPublicKey)metadataCert.getPublicKey(); DSAPublicKey clientDSA = (DSAPublicKey)clientCerts[0].getPublicKey(); if (metadataDSA.getY().equals(clientDSA.getY()) && metadataDSA.getParams().getG().equals(clientDSA.getParams().getG()) && metadataDSA.getParams().getP().equals(clientDSA.getParams().getP()) && metadataDSA.getParams().getQ().equals(clientDSA.getParams().getQ())) { return true; } } else if ((metadataCert.getPublicKey() instanceof RSAPublicKey) && (clientCerts[0].getPublicKey() instanceof RSAPublicKey)) { RSAPublicKey metadataRSA = (RSAPublicKey)metadataCert.getPublicKey(); RSAPublicKey clientRSA = (RSAPublicKey)clientCerts[0].getPublicKey(); if (metadataRSA.getPublicExponent().equals(clientRSA.getPublicExponent()) && metadataRSA.getModulus().equals(clientRSA.getModulus())) { return true; } } } } } return false; } catch(Exception e) { throw new GuanxiException(e); } } /** * Performs PKIX path validation based on certificates from metadata * * @param samlResponse The SAML Response from an IdP containing an AuthenticationStatement * @param saml2Metadata The metadata for the IdP * @param caCerts The list of CA root certs as trust anchors * @param hostName The hostname for the validation context * @return true if validation succeeds otherwise false * @throws GuanxiException if an error occurs */ public static boolean validatePKIX(ResponseDocument samlResponse, EntityDescriptorType saml2Metadata, Vector<X509Certificate> caCerts, String hostName) throws GuanxiException { /* PKIX Path Validation * quickie summary: * - Match X509 in SAML Response signature to KeyName in IdP metadata * - Match issuer of X509 in SAML Response to one of the X509s in shibmeta:keyauthority * in global metadata */ X509Certificate x509CertFromSig = getX509CertFromSignature(samlResponse); // First find a match between the X509 in the signature and a KeyName in the metadata... if (matchCertToKeyName(x509CertFromSig, saml2Metadata, hostName)) { // ...then follow the chain from the X509 in the signature back to a supported CA in the metadata if (validateCertPath(x509CertFromSig, caCerts)) { return true; } } return false; } /** * Performs PKIX path validation based on certificates from a back channel connection * * @param x509CertFromConnection The certificate from the connection * @param saml2Metadata The metadata for the IdP * @param caCerts The list of CA root certs as trust anchors * @param hostName The hostname for the validation context * @return true if validation succeeds otherwise false * @throws GuanxiException if an error occurs */ public static boolean validatePKIXBC(X509Certificate x509CertFromConnection, EntityDescriptorType saml2Metadata, Vector<X509Certificate> caCerts, String hostName) throws GuanxiException { /* PKIX Path Validation * quickie summary: * - Match X509 from connection to KeyName in IdP metadata * - Match issuer of X509 from connection to one of the X509s in shibmeta:keyauthority * in global metadata */ // First find a match between the X509 from the connection and a KeyName in the metadata... if (matchAACertToKeyName(x509CertFromConnection, saml2Metadata, hostName)) { // ...then follow the chain from the X509 in the signature back to a supported CA in the metadata if (validateCertPath(x509CertFromConnection, caCerts)) { return true; } } return false; } /** * Retrieves the X509Certificate from a digital signature * * @param samlResponse The SAML Response containing the signature * @return X509Certificate from the signature * @throws GuanxiException if an error occurs */ public static X509Certificate getX509CertFromSignature(XmlObject samlResponse) throws GuanxiException { KeyInfoType keyInfo = null; if (samlResponse instanceof org.guanxi.xal.saml_1_0.protocol.ResponseDocument) { keyInfo = ((org.guanxi.xal.saml_1_0.protocol.ResponseDocument)(samlResponse)).getResponse().getSignature().getKeyInfo(); } else if (samlResponse instanceof org.guanxi.xal.saml_2_0.protocol.ResponseDocument) { keyInfo = ((org.guanxi.xal.saml_2_0.protocol.ResponseDocument)(samlResponse)).getResponse().getSignature().getKeyInfo(); } try { byte[] x509CertBytes = keyInfo.getX509DataArray(0).getX509CertificateArray(0); CertificateFactory certFactory = CertificateFactory.getInstance("x.509"); ByteArrayInputStream certByteStream = new ByteArrayInputStream(x509CertBytes); X509Certificate cert = (X509Certificate)certFactory.generateCertificate(certByteStream); certByteStream.close(); return cert; } catch(CertificateException ce) { logger.error("Error obtaining certificate factory", ce); throw new GuanxiException(ce); } catch(IOException ioe) { logger.error("Error closing certificate byte stream", ioe); throw new GuanxiException(ioe); } } /** * Retrieves the X509Certificate from a digital signature * * @param keyInfo The KeyInfo within the SAML message * @return X509Certificate from the signature * @throws GuanxiException if an error occurs */ public static X509Certificate getX509CertFromSignature(KeyInfoType keyInfo) throws GuanxiException { try { byte[] x509CertBytes = keyInfo.getX509DataArray(0).getX509CertificateArray(0); CertificateFactory certFactory = CertificateFactory.getInstance("x.509"); ByteArrayInputStream certByteStream = new ByteArrayInputStream(x509CertBytes); X509Certificate cert = (X509Certificate)certFactory.generateCertificate(certByteStream); certByteStream.close(); return cert; } catch(CertificateException ce) { logger.error("Error obtaining certificate factory", ce); throw new GuanxiException(ce); } catch(IOException ioe) { logger.error("Error closing certificate byte stream", ioe); throw new GuanxiException(ioe); } } /** * Tries to match an X509 certificate subject to a KeyName in metadata * * @param x509 The X509 to match with a KeyName * @param saml2Metadata The metadata which contains the KeyName * @param hostName The hostname for the validation context * @return true if a match was made, otherwise false */ public static boolean matchCertToKeyName(X509Certificate x509, EntityDescriptorType saml2Metadata, String hostName) { IDPSSODescriptorType[] idpSSOs = saml2Metadata.getIDPSSODescriptorArray(); // EntityDescriptor/IDPSSODescriptor for (IDPSSODescriptorType idpSSO : idpSSOs) { // EntityDescriptor/IDPSSODescriptor/KeyDescriptor if (validateX509WithKeyName(x509, idpSSO.getKeyDescriptorArray(), hostName)) { return true; } } return false; } /** * Tries to match an X509 certificate subject to a KeyName in metadata * * @param x509 The X509 to match with a KeyName * @param saml2Metadata The metadata which contains the KeyName * @param hostName The hostname for the validation context * @return true if a match was made, otherwise false */ public static boolean matchAACertToKeyName(X509Certificate x509, EntityDescriptorType saml2Metadata, String hostName) { AttributeAuthorityDescriptorType[] aaList = saml2Metadata.getAttributeAuthorityDescriptorArray(); // EntityDescriptor/AttributeAuthorityDescriptor for (AttributeAuthorityDescriptorType aa : aaList) { // EntityDescriptor/IDPSSODescriptor/KeyDescriptor if (validateX509WithKeyName(x509, aa.getKeyDescriptorArray(), hostName)) { return true; } } return false; } /** * Validates an X509 certificate based on key names in metadata * * @param x509 The X509 to match with a KeyName * @param keyDescriptors pointer to the list of key descriptors from the metadata * @param hostName The hostname for the validation context * @return if a match was found */ public static boolean validateX509WithKeyName(X509Certificate x509, KeyDescriptorType[] keyDescriptors, String hostName) { for (KeyDescriptorType keyDescriptor : keyDescriptors) { // EntityDescriptor/IDPSSODescriptor/KeyDescriptor/KeyInfo if (keyDescriptor.getKeyInfo() != null) { // EntityDescriptor/IDPSSODescriptor/KeyDescriptor/KeyInfo/KeyName if (keyDescriptor.getKeyInfo().getKeyNameArray() != null) { ArrayList<String> allKeyNames = new ArrayList<String>(); String[] metadataKeyNames = keyDescriptor.getKeyInfo().getKeyNameArray(); for (String metadataKeyName : metadataKeyNames) { allKeyNames.add(metadataKeyName); } // Shibboleth spec says the hostname is also a KeyName if (hostName != null) { allKeyNames.add(hostName); } for (String keyName : allKeyNames) { String metadataKeyName = new String(keyName.getBytes()); // Do the hard work of comparison if (compareX509SubjectWithKeyName(x509, metadataKeyName)) { return true; } } } } } return false; } /** * Compares an X509 subject to a KeyName string using various techniques. * We can hit a problem with certs when the IdP's providerId is, e.g. urn:uni:ac:uk:idp * but it's cert DN is CN=urn:uni:ac:uk:idp, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown * We didn't see this in the beginning as the certs generated by BouncyCastle in the IdP don't have * the extra OU etc. Most commandline tools do put the extra OU in if you don't specify them. * * @param x509 The X509 to use * @param keyName The KeyName string to use * @return if they match, otherwise false */ public static boolean compareX509SubjectWithKeyName(X509Certificate x509, String keyName) { logger.debug("subject DN : " + x509.getSubjectDN().getName() + ", KeyName : " + keyName); // Try the full DN logger.debug("trying DN : " + x509.getSubjectDN().getName() + " KeyName : " + keyName); if (x509.getSubjectDN().getName().equals(keyName)) { logger.debug("matched DN"); return true; } // Try the CN String[] split; for (String currentEntry : x509.getSubjectDN().getName().split(",\\s*")) { split = currentEntry.split("="); if (split[0].equals("CN")) { if (split[1].equals(keyName)) { logger.debug("matched CN"); return true; } // Try getting down to the hostname String[] nameParts = split[1].split("/"); for (String namePart : nameParts) { if (namePart.equalsIgnoreCase(keyName)) { logger.debug("matched hostname : " + keyName); return true; } } } } return false; } /** * Validates a certificate path starting with the mystery cert and working * back to a trust anchor, using the CA certs in the trust engine. * * @param x509ToVerify the mystery cert, should we trust it? * @param caCerts the list of CA root certs to trust * @return true if we trust the cert, otherwise false */ public static boolean validateCertPath(X509Certificate x509ToVerify, Vector<X509Certificate> caCerts) { for (X509Certificate caX509 : caCerts) { if (caX509.getSubjectDN().getName().equals(x509ToVerify.getIssuerDN().getName())) { return validatePKIXPath(x509ToVerify, caX509); } } return false; } /** * Performs PKIX path validation on a set of certificates * * @param x509ToVerify The X509Certificate to validate * @param caX509 The root trust anchor X509Certificate * @return true if successful otherwise false */ public static boolean validatePKIXPath(X509Certificate x509ToVerify, X509Certificate caX509) { try { ArrayList<X509Certificate> certsList = new ArrayList<X509Certificate>(); certsList.add(caX509); certsList.add(x509ToVerify); CollectionCertStoreParameters certStoreParams = new CollectionCertStoreParameters(certsList); CertStore certStore = CertStore.getInstance("Collection", certStoreParams, "BC"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); ArrayList<X509Certificate> certChain = new ArrayList<X509Certificate>(); certChain.add(x509ToVerify); CertPath certPath = certFactory.generateCertPath(certChain); Set<TrustAnchor> trust = Collections.singleton(new TrustAnchor(caX509, null)); CertPathValidator validator = CertPathValidator.getInstance("PKIX", "BC"); PKIXParameters pkixParams = new PKIXParameters(trust); pkixParams.addCertStore(certStore); //pkixParams.setDate(new Date()); /* In case we get here via a "virtual" KeyName, we're not interested * in the validity of the cert per se. */ pkixParams.setRevocationEnabled(false); // Do the path validation validator.validate(certPath, pkixParams); return true; } catch(Exception e) { return false; } } /** * Verifies the digital signature on a SAML Response * * @param samlMessage The SAML Response document containing the signature * @return true if the signature verifies otherwise false * @throws GuanxiException if an error occurs */ public static boolean verifySignature(XmlObject samlMessage) throws GuanxiException { try { /* We need to check for ID attributes, which requires DOM Level 3, which XMLBeans * does not support. So we need to jump into DOM land. */ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); dbf.setAttribute("http://xml.org/sax/features/namespaces", Boolean.TRUE); DocumentBuilder db = dbf.newDocumentBuilder(); db.setErrorHandler(new org.apache.xml.security.utils.IgnoreAllErrorHandler()); Document doc = db.parse(samlMessage.newInputStream()); setIdNode(doc); NodeList nodes = doc.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature"); Element sigElement = (Element)nodes.item(0); XMLSignature xmlSignature = new XMLSignature(sigElement, ""); /* Stop the Signature/Object attack where the original Response is copied * into Signature/Object and the signature removed from the copied Response. * The ID of the real Response is then changed which means the * Signature/SignedInfo/Reference now points to the original Response that * was copied to Signature/Object. * So the signature is verified on Signature/Object/Response and the root * Response is then modified to anything required to break into the system * with different attributes. */ if (xmlSignature.getObjectLength() > 1) { for (int c=0; c < xmlSignature.getObjectLength(); c++) { ObjectContainer object = xmlSignature.getObjectItem(c); if (object.getBaseLocalName() != null) { if (object.getBaseLocalName().equals("Response")) { throw new GuanxiException("There is a Response in signature/object#" + c); } } } } // Make sure the signature reference is not suspicious String rootResponseID = null; if (doc.getFirstChild().getAttributes().getNamedItem("ID") != null) { // SAML2 rootResponseID = doc.getFirstChild().getAttributes().getNamedItem("ID").getTextContent(); } else if (doc.getFirstChild().getAttributes().getNamedItem("ResponseID") != null) { // Shibboleth rootResponseID = doc.getFirstChild().getAttributes().getNamedItem("ResponseID").getTextContent(); } if (rootResponseID == null) { throw new GuanxiException("No Response ID"); } String signatureReference = xmlSignature.getSignedInfo().getReferencedContentBeforeTransformsItem(0).getSourceURI(); if ((!signatureReference.equals("")) && (!(("#" + rootResponseID).equals(signatureReference)))) { throw new GuanxiException("The signature reference is not for the Response"); } X509Certificate cert = xmlSignature.getKeyInfo().getX509Certificate(); return xmlSignature.checkSignatureValue(cert); } catch(ParserConfigurationException pce) { throw new GuanxiException(pce); } catch(SAXException se) { throw new GuanxiException(se); } catch(IOException ioe) { throw new GuanxiException(ioe); } catch(XMLSecurityException xse) { throw new GuanxiException(xse); } } /** * This passes through the nodes of the document looking for the Reference Id and * then assigns that Id to the root node of the document. * * @param doc SAML Response document * @throws GuanxiException if an error occurs */ private static void setIdNode(Document doc) throws GuanxiException { try { // Look for the Reference node in the Signature... NodeList sigReference = (NodeList)referencePath.evaluate(doc, XPathConstants.NODESET); // ...to see if it has a value... if (sigReference.getLength() > 0 && sigReference.item(0).getAttributes() != null && sigReference.item(0).getAttributes().getNamedItem("URI") != null && sigReference.item(0).getAttributes().getNamedItem("URI").getTextContent() != "") { // ...and mark the attribute with that value as an ID attribute // Shibboleth if ((doc.getDocumentElement().getAttribute("ResponseID") != null) && (!doc.getDocumentElement().getAttribute("ResponseID").equals(""))) { doc.getDocumentElement().setIdAttribute("ResponseID", true); } // SAML2 else if ((doc.getDocumentElement().getAttribute("ID") != null) && (!doc.getDocumentElement().getAttribute("ID").equals(""))) { doc.getDocumentElement().setIdAttribute("ID", true); } } } catch(XPathExpressionException xee) { throw new GuanxiException(xee); } } /** * Extracts X509 certificates from a SAML2 IdP EntityDescriptor * * @param saml2Metadata The SAML2 metadata which may contain the certificates * @return array of X509Certificate objects created from the metadata * @throws GuanxiException if an error occurs */ public static X509Certificate[] getX509CertsFromIdPMetadata(EntityDescriptorType saml2Metadata) throws GuanxiException { return getX509CertsFromMetadata(saml2Metadata.getIDPSSODescriptorArray()); } /** * Extracts X509 certificates from a SAML2 SP EntityDescriptor * * @param saml2Metadata The SAML2 metadata which may contain the certificates * @return array of X509Certificate objects created from the metadata * @throws GuanxiException if an error occurs */ public static X509Certificate[] getX509CertsFromSPMetadata(EntityDescriptorType saml2Metadata) throws GuanxiException { return getX509CertsFromMetadata(saml2Metadata.getSPSSODescriptorArray()); } /** * Extracts X509 certificates from a SAML2 EntityDescriptor * * @param entityDescriptors The SAML2 metadata which may contain the certificates. This can either be * an IDPSSODescriptor or an SPSSODescriptor * @return array of X509Certificate objects created from the metadata * @throws GuanxiException if an error occurs */ public static X509Certificate[] getX509CertsFromMetadata(SSODescriptorType[] entityDescriptors) throws GuanxiException { if (entityDescriptors == null) { return null; } Vector<X509Certificate> x509Certs = new Vector<X509Certificate>(); for (SSODescriptorType entityDescriptor : entityDescriptors) { KeyDescriptorType[] keys = entityDescriptor.getKeyDescriptorArray(); // SSODescriptor/KeyDescriptor for (KeyDescriptorType key : keys) { if (key.getKeyInfo() != null) { // SSODescriptor/KeyDescriptor/KeyInfo if (key.getKeyInfo().getX509DataArray() != null) { X509DataType[] x509s = key.getKeyInfo().getX509DataArray(); // SSODescriptor/KeyDescriptor/KeyInfo/X509Data for (X509DataType x509 : x509s) { if (x509.getX509CertificateArray() != null) { byte[][] x509bytesArray = x509.getX509CertificateArray(); // SSODescriptor/KeyDescriptor/KeyInfo/X509Data/X509Certificate try { CertificateFactory certFactory = CertificateFactory.getInstance("x.509"); for (byte[] x509bytes : x509bytesArray) { ByteArrayInputStream certByteStream = new ByteArrayInputStream(x509bytes); X509Certificate x509CertFromMetadata = (X509Certificate)certFactory.generateCertificate(certByteStream); certByteStream.close(); x509Certs.add(x509CertFromMetadata); } // for (byte[] x509bytes : x509bytesArray) } catch(CertificateException ce) { logger.error("Error obtaining certificate factory", ce); throw new GuanxiException(ce); } catch(IOException ioe) { logger.error("Error closing certificate byte stream", ioe); throw new GuanxiException(ioe); } } // if (x509.getX509CertificateArray() != null) } } } } } // for (SSODescriptorType idpInfo : idpInfos) X509Certificate[] x509sFromMetadata = new X509Certificate[x509Certs.size()]; x509Certs.copyInto(x509sFromMetadata); return x509sFromMetadata; } /** * Extracts KeyNames from a SAML2 IdP EntityDescriptor * * @param saml2Metadata The SAML2 metadata which may contain the certificates * @return array of String objects created from the metadata * @throws GuanxiException if an error occurs */ public static String[] getKeyNamesFromIdPMetadata(EntityDescriptorType saml2Metadata) throws GuanxiException { return getKeyNamesFromMetadata(saml2Metadata.getIDPSSODescriptorArray()); } /** * Extracts KeyNames from a SAML2 SP EntityDescriptor * * @param saml2Metadata The SAML2 metadata which may contain the certificates * @return array of String objects created from the metadata * @throws GuanxiException if an error occurs */ public static String[] getKeyNamesFromSPMetadata(EntityDescriptorType saml2Metadata) throws GuanxiException { return getKeyNamesFromMetadata(saml2Metadata.getSPSSODescriptorArray()); } /** * Extracts KeyNames from a SAML2 EntityDescriptor * * @param entityDescriptors The SAML2 metadata which may contain the key names. This can either be * an IDPSSODescriptor or an SPSSODescriptor * @return array of String objects created from the metadata * @throws GuanxiException if an error occurs */ public static String[] getKeyNamesFromMetadata(SSODescriptorType[] entityDescriptors) throws GuanxiException { if (entityDescriptors == null) { return null; } Vector<String> keyNames = new Vector<String>(); for (SSODescriptorType entityDescriptor : entityDescriptors) { KeyDescriptorType[] keys = entityDescriptor.getKeyDescriptorArray(); // SSODescriptor/KeyDescriptor for (KeyDescriptorType key : keys) { if (key.getKeyInfo() != null) { // SSODescriptor/KeyDescriptor/KeyInfo if (key.getKeyInfo().getKeyNameArray() != null) { keyNames.addAll(Arrays.asList(key.getKeyInfo().getKeyNameArray())); } } } } // for (SSODescriptorType idpInfo : idpInfos) String[] keyNamesFromMetadata = new String[keyNames.size()]; keyNames.copyInto(keyNamesFromMetadata); return keyNamesFromMetadata; } /** * Compares the fingerprints of two X509 certificates. * * @param cert1 X509Certificate * @param cert2 X509Certificate * @return true if the fingerprints are equal, otherwise false * @throws GuanxiException if an error occurred */ public static boolean checkCertfingerprints(X509Certificate cert1, X509Certificate cert2) throws GuanxiException { try { MessageDigest md = MessageDigest.getInstance("SHA1"); md.update(cert1.getEncoded()); byte[] cert1Fingerprint = md.digest(); md.update(cert2.getEncoded()); byte[] cert2Fingerprint = md.digest(); return byteArrayToHexString(cert1Fingerprint).equals(byteArrayToHexString(cert2Fingerprint)); } catch(NoSuchAlgorithmException nsae) { throw new GuanxiException(nsae); } catch(CertificateEncodingException cee) { throw new GuanxiException(cee); } } /** * Returns the hex representation of a byte array. * * @param bytes byte array to be converted to hex * @return hex representation of bytes */ public static String byteArrayToHexString(byte bytes[]) { byte ch = 0x00; int count = 0; if (bytes == null || bytes.length <= 0) return null; String pseudo[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; StringBuffer out = new StringBuffer(bytes.length * 2); while (count < bytes.length) { ch = (byte) (bytes[count] & 0xF0); ch = (byte) (ch >>> 4); ch = (byte) (ch & 0x0F); out.append(pseudo[ (int) ch]); ch = (byte) (bytes[count] & 0x0F); out.append(pseudo[ (int) ch]); if (count < (bytes.length -1)) { out.append(":"); } count++; } return new String(out); } /** * Converts a PEM to an X509Certificate. Requires the Bouncy Castle provider * to be installed. * * @param pemURL URL of the PEM file * @return X509Certificate * @throws GuanxiException if an error occurs */ public static X509Certificate pem2x509(String pemURL) throws GuanxiException { try { URL pem = new URL(pemURL); PEMReader pemReader = new PEMReader(new InputStreamReader(pem.openStream())); return (X509Certificate)pemReader.readObject(); } catch(MalformedURLException mue) { throw new GuanxiException(mue); } catch(IOException ioe) { throw new GuanxiException(ioe); } } /** * Compares two PublicKeys to see if they are the same * * @param keyOne PublicKey to compare * @param keyTwo PublicKey to compare * @return true if the keys are the same * @throws GuanxiException if an error occurs */ public static boolean compareKeys(PublicKey keyOne, PublicKey keyTwo) throws GuanxiException { if ((keyOne instanceof DSAPublicKey) && (keyTwo instanceof DSAPublicKey)) { DSAPublicKey keyOneDSA = (DSAPublicKey)keyOne; DSAPublicKey keyTwoDSA = (DSAPublicKey)keyTwo; if (keyOneDSA.getY().equals(keyTwoDSA.getY()) && keyOneDSA.getParams().getG().equals(keyTwoDSA.getParams().getG()) && keyOneDSA.getParams().getP().equals(keyTwoDSA.getParams().getP()) && keyOneDSA.getParams().getQ().equals(keyTwoDSA.getParams().getQ())) { return true; } } else if ((keyOne instanceof RSAPublicKey) && (keyTwo instanceof RSAPublicKey)) { RSAPublicKey keyOneRSA = (RSAPublicKey)keyOne; RSAPublicKey keyTwoRSA = (RSAPublicKey)keyTwo; if (keyOneRSA.getPublicExponent().equals(keyTwoRSA.getPublicExponent()) && keyOneRSA.getModulus().equals(keyTwoRSA.getModulus())) { return true; } } throw new GuanxiException("Unsupported PublicKey type"); } }