/* * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved. * See LICENCE file for licensing information. * * Derived from the code copyrighted and licensed as follows: * * Copyright (c) Members of the EGEE Collaboration. 2004. * See http://www.eu-egee.org/partners/ for details on the copyright * holders. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package eu.emi.security.authn.x509.proxy; import java.io.IOException; import java.math.BigInteger; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.bouncycastle.asn1.x509.AttributeCertificate; import eu.emi.security.authn.x509.helpers.proxy.ExtendedProxyType; import eu.emi.security.authn.x509.helpers.proxy.IPAddressHelper; import eu.emi.security.authn.x509.helpers.proxy.ProxyACExtension; import eu.emi.security.authn.x509.helpers.proxy.ProxyAddressRestrictionData; import eu.emi.security.authn.x509.helpers.proxy.ProxyCertInfoExtension; import eu.emi.security.authn.x509.helpers.proxy.ProxyHelper; import eu.emi.security.authn.x509.helpers.proxy.ProxySAMLExtension; import eu.emi.security.authn.x509.helpers.proxy.ProxyTracingExtension; import eu.emi.security.authn.x509.impl.CertificateUtils; /** * A class to get an information from a proxy certificate chain. * * @author J. Hahkala * @author K. Benedyczak */ public class ProxyChainInfo { static { CertificateUtils.configureSecProvider(); } private X509Certificate[] chain; private int firstProxy; private ProxyChainType type; private ProxyPolicy[] policy; private Boolean limited; /** * Generates new instance of this class using the certificate chain as the source * of the data. * @param chain chain with at least one proxy certificate * @throws CertificateException if there is no proxy certificate in the chain or * if the chain is inconsistent, i.e. after proxy there is a non-proxy certificate. */ public ProxyChainInfo(X509Certificate[] chain) throws CertificateException { if (chain == null || chain.length == 0) throw new IllegalArgumentException("Certificate chain passed may not be null or empty"); int i; for (i=chain.length-1; i>=0; i--) if (ProxyUtils.isProxy(chain[i])) { firstProxy = i; this.chain = chain; break; } if (i == -1) throw new CertificateException("There is no proxy certificate in the chain"); } /** * * @return array with serial numbers of the certificates in the chain */ public BigInteger[] getSerialNumbers() { BigInteger[] ret = new BigInteger[chain.length]; for (int i=0; i<chain.length; i++) ret[i] = chain[i].getSerialNumber(); return ret; } /** * The type of the proxy chain chain is returned. If chain contains * different types then MIXED type is returned. * @return the type of the chain * @throws CertificateException certificate exception */ public ProxyChainType getProxyType() throws CertificateException { if (type != null) return type; for (int i=0; i<=firstProxy; i++) { ExtendedProxyType ptype = ProxyHelper.getProxyType(chain[i]); switch (ptype) { case NOT_A_PROXY: break; case DRAFT_RFC: if (type == null) type = ProxyChainType.DRAFT_RFC; else if (type != ProxyChainType.DRAFT_RFC) type = ProxyChainType.MIXED; break; case RFC3820: if (type == null) type = ProxyChainType.RFC3820; else if (type != ProxyChainType.RFC3820) type = ProxyChainType.MIXED; break; case LEGACY: if (type == null) type = ProxyChainType.LEGACY; else if (type != ProxyChainType.LEGACY) type = ProxyChainType.MIXED; } } return type; } /** * @return the index of the first proxy in the chain (issued by the EEC). */ public int getFirstProxyPosition() { return firstProxy; } /** * Used to check whether the proxy chain is limited or not. * The method returns 'true' if and only if there is at least one limited * proxy in the chain. * @return true if the chain is limited, i.e. owner of the certificate * may not submit jobs * @throws CertificateException certificate exception * @throws IOException IO exception */ public boolean isLimited() throws CertificateException, IOException { if (limited != null) return limited; for (int i=0; i<=firstProxy; i++) if (ProxyHelper.isLimited(chain[i])) { limited = true; return true; } limited = false; return false; } /** * Gets the array of RFC proxy extension policy OID and octets of the * policy. See RFC3820. Policy octets can be null in case the OID in itself * defines the behavior, like with "inherit all" policy or * "independent" policy. The array contains entries from all certificates * in chain. * @return array with policy information * @throws IOException Thrown in case the parsing of the information failed. */ public ProxyPolicy[] getPolicy() throws IOException { if (policy != null) return policy; List<ProxyPolicy> policies = new ArrayList<ProxyPolicy>(); for (int i=firstProxy; i>=0; i--) { ExtendedProxyType type = ProxyHelper.getProxyType(chain[i]); if (type == ExtendedProxyType.DRAFT_RFC || type == ExtendedProxyType.RFC3820) { ProxyCertInfoExtension ext = ProxyCertInfoExtension.getInstance(chain[i]); if (ext != null) policies.add(ext.getPolicy()); } } policy = policies.toArray(new ProxyPolicy[policies.size()]); return policy; } /** * Returns an array of URLs of the proxy tracing issuers in * the chain. Non-traced proxies will have null in the array. * * @return The proxy tracing issuer URLs in String format, or null in the * array if an extension was not found or it was empty. * @throws IOException Thrown in case the parsing of the information failed. */ public String[] getProxyTracingIssuers() throws IOException { String ret[] = new String[chain.length]; for (int i=0; i<chain.length; i++) { ProxyTracingExtension extension = ProxyTracingExtension.getInstance(chain[i], true); ret[i] = extension == null ? null : extension.getURL(); } return ret; } /** * Returns an array of URLs of the proxy tracing subjects in the chain. * Non-traced proxies will have null in the array. * @return The proxy tracing subject URLs in String format, or null in the * array if an extension was not found or it was empty. * @throws IOException Thrown in case the parsing of the information failed. */ public String[] getProxyTracingSubjects() throws IOException { String ret[] = new String[chain.length]; for (int i=0; i<chain.length; i++) { ProxyTracingExtension extension = ProxyTracingExtension.getInstance(chain[i], false); ret[i] = extension == null ? null : extension.getURL(); } return ret; } /** * Returns the SAML extensions from the certificate chain. * @return The SAML assertions in String format. A null in the array * means that no SAML extensions were found at the given position. * @throws IOException Thrown in case the parsing of the information failed. */ public String[] getSAMLExtensions() throws IOException { String ret[] = new String[chain.length]; for (int i=0; i<chain.length; i++) { ProxySAMLExtension extension = ProxySAMLExtension.getInstance(chain[i]); if (extension != null) ret[i] = extension.getSAML(); } return ret; } /** * Returns the Attribute Certificate extensions from the certificate chain. * @return The Attribute Certificates array. The first index corresponds to the * first certificate in the chain. A null in the array * means that no AC extension was found at the given position. * @throws IOException Thrown in case the parsing of the information failed. */ public AttributeCertificate[][] getAttributeCertificateExtensions() throws IOException { AttributeCertificate ret[][] = new AttributeCertificate[chain.length][]; for (int i=0; i<chain.length; i++) { ProxyACExtension extension = ProxyACExtension.getInstance(chain[i]); if (extension != null) ret[i] = extension.getAttributeCertificates(); } return ret; } /** * Returns the remaining path length of this chain. Will * search for both the RFC 3820 and the draft proxy path limit extensions. * Legacy proxies are treated as unlimited. * <p> * Notice: negative value means that the chain is invalid as * it has passed the limit of delegations. Integer.MAX_INT is returned * if there is no path length limit set on the chain. * * @return remaining proxy path limit * @throws IOException Thrown in case the parsing of the information failed. */ public int getRemainingPathLimit() throws IOException { int remainingLen = Integer.MAX_VALUE; for (int i=firstProxy; i>=0; i--) { int lenRestriction = ProxyHelper.getProxyPathLimit(chain[i]); if (lenRestriction < remainingLen) remainingLen = lenRestriction; else remainingLen--; } return remainingLen; } /** * Gets the proxy source restriction data from the chain. * The allowed namespaces in different certificates in the * chain will be intersected and the excluded namespaces will be summed. * The returned array has as the first item the array of allowed * namespaces and as the second item the array of excluded namespaces. * If extensions exist, but in the end no allowed or excluded namespaces are left, * the array is empty. * * @return array with proxy source restrictions. Null is returned when there is no restriction defined * for any of the proxies in the chain. * @throws IOException Thrown in case the parsing of the information failed. */ public byte[][][] getProxySourceRestrictions() throws IOException { return getProxyRestrictions(true); } /** * Gets the proxy target restriction data from the chain. The allowed * namespaces in different certificates in the * chain will be intersected and the union of the excluded namespaces will be computed. * The returned array has as the first item the array of allowed namespaces * and as the second item the array of excluded namespaces. If extensions exist, but in the end * no allowed or excluded namespaces are left, the array is empty. * * @return array with proxy target restrictions. Null is returned when there is no restriction defined * for any of the proxies in the chain. * @throws IOException Thrown in case the parsing of the information failed. */ public byte[][][] getProxyTargetRestrictions() throws IOException { return getProxyRestrictions(false); } /** * Checks if the given IP address is allowed as this proxy chain source. * * @param ipAddress host IPv4 address in 4 elements array * @return true if and only if the ipAddress is OK w.r.t. this proxy * chain's source restrictions. * @throws IOException Thrown in case the parsing of the information failed. */ public boolean isHostAllowedAsSource(byte[] ipAddress) throws IOException { return isHostAllowed(ipAddress, getProxySourceRestrictions()); } /** * Checks if the given IP address is allowed as this proxy chain target. * * @param ipAddress host IPv4 address in 4 elements array * @return true if and only if the ipAddress is OK w.r.t. this proxy * chain's source restrictions. * @throws IOException Thrown in case the parsing of the information failed. */ public boolean isHostAllowedAsTarget(byte[] ipAddress) throws IOException { return isHostAllowed(ipAddress, getProxyTargetRestrictions()); } /** * Calculates the union of the newSpaces and the given vectors of IPv4 * and IPv6 namespaces. * * @param newSpaces * The namespaces to add. * @param ipV4Spaces * The old IPv4 spaces. * @param ipV6Spaces * The old IPv6 spaces. * @return the two resulting vectors, IPv4 vector first and the IPv6 * vector second. */ private List<List<byte[]>> union(byte[][] newSpaces, List<byte[]> ipV4Spaces, List<byte[]> ipV6Spaces) { List<List<byte[]>> ret = new ArrayList<List<byte[]>>(); if (newSpaces == null) { ret.add(ipV4Spaces); ret.add(ipV6Spaces); return ret; } List<byte[]> newIPv4 = new ArrayList<byte[]>(); List<byte[]> newIPv6 = new ArrayList<byte[]>(); if (ipV4Spaces != null) newIPv4.addAll(ipV4Spaces); if (ipV6Spaces != null) newIPv6.addAll(ipV6Spaces); for (int i = 0; i < newSpaces.length; i++) { if (newSpaces[i].length == 8) { newIPv4.add(newSpaces[i]); } else { if (newSpaces[i].length == 32) { newIPv6.add(newSpaces[i]); } else { throw new IllegalArgumentException( "IP space definition has to be either 8 bytes or 32 bytes, length was: " + newSpaces.length); } } } ret.add(newIPv4); ret.add(newIPv6); return ret; } /** * Calculates the intersection of the newSpaces and the given lists of * IPv4 and IPv6 namespaces. * * @param newSpaces * The namespaces to intersect with. * @param ipV4Spaces * The old IPv4 spaces. * @param ipV6Spaces * The old IPv6 spaces. * @return the two resulting lists, IPv4 list first and the IPv6 * list second. */ private List<List<byte[]>> intersection(byte[][] newSpaces, List<byte[]> ipV4Spaces, List<byte[]> ipV6Spaces) { List<List<byte[]>> ret = new ArrayList<List<byte[]>>(); if (newSpaces == null) { ret.add(ipV4Spaces); ret.add(ipV6Spaces); return ret; } List<byte[]> newIPv4 = new ArrayList<byte[]>(); List<byte[]> newIPv6 = new ArrayList<byte[]>(); for (int i = 0; i < newSpaces.length; i++) { List<byte[]> newIPs; int len; if (newSpaces[i].length == 8) { newIPs = newIPv4; len = 8; } else { if (newSpaces[i].length == 32) { newIPs = newIPv6; len = 32; } else { throw new IllegalArgumentException( "Invalid namespace definition, length should be 8 or 32 bytes. It was: " + newSpaces[i].length + " bytes."); } } if (ipV4Spaces != null && ipV6Spaces != null) { byte[] ip = Arrays.copyOfRange(newSpaces[i], 0, len / 2); Iterator<byte[]> iter = newIPs.iterator(); while (iter.hasNext()) { byte[] oldSpace = iter.next(); if (IPAddressHelper.isWithinAddressSpace(ip, oldSpace)) { boolean newTighter = true; for (int n = 0; n < len / 2; n++) { if ((oldSpace[n + len / 2] & 0xFF) < (newSpaces[i][n + len / 2] & 0xFF)) { newTighter = false; break; } } if (newTighter) { newIPs.add(newSpaces[i]); } else { newIPs.add(oldSpace); } } } } else { newIPs.add(newSpaces[i]); } } ret.add(newIPv4); ret.add(newIPv6); return ret; } /** * Goes through the whole proxy chain and collects and combines either * the source restrictions or target restrictions. * * @param source true if source extensions are to be collected. False * if target extensions are to be collected. * @return The collected and combined restriction data. * @throws IOException Thrown in case a certificate parsing fails. */ private byte[][][] getProxyRestrictions(boolean source) throws IOException { List<byte[]> allowedIPv4Spaces = null; List<byte[]> allowedIPv6Spaces = null; List<byte[]> excludedIPv4Spaces = null; List<byte[]> excludedIPv6Spaces = null; boolean found = false; for (int i = chain.length - 1; i >= 0; i--) { ProxyAddressRestrictionData restrictions = ProxyAddressRestrictionData.getInstance( chain[i], source); if (restrictions != null) { found = true; byte[][][] spaces = restrictions.getIPSpaces(); List<List<byte[]>> newSpaces = intersection(spaces[0], allowedIPv4Spaces, allowedIPv6Spaces); allowedIPv4Spaces = newSpaces.get(0); allowedIPv6Spaces = newSpaces.get(1); newSpaces = union(spaces[1], excludedIPv4Spaces, excludedIPv6Spaces); excludedIPv4Spaces = newSpaces.get(0); excludedIPv6Spaces = newSpaces.get(1); } } if (!found) return null; byte[][][] newSpaces = new byte[2][][]; if (allowedIPv4Spaces != null && allowedIPv6Spaces != null) { newSpaces[0] = concatArrays( allowedIPv4Spaces.toArray(new byte[0][0]), allowedIPv6Spaces.toArray(new byte[0][0])); } else newSpaces[0] = new byte[0][]; if (excludedIPv4Spaces != null && excludedIPv6Spaces != null) { newSpaces[1] = concatArrays( excludedIPv4Spaces.toArray(new byte[0][0]), excludedIPv6Spaces.toArray(new byte[0][0])); } else newSpaces[1] = new byte[0][]; return newSpaces; } private boolean isHostAllowed(byte[] ipAddress, byte[][][] restrictions) throws IOException { if (restrictions == null) return true; for (int i=0; i<restrictions[1].length; i++) if (IPAddressHelper.isWithinAddressSpace(ipAddress, restrictions[1][i])) return false; for (int i=0; i<restrictions[0].length; i++) if (IPAddressHelper.isWithinAddressSpace(ipAddress, restrictions[0][i])) return true; return false; } /** * Concatenates two arrays of arrays bytes. * * @param first * The array of arrays to begin with. * @param second * The array of arrays to end with. * @return the array of arrays that contains the arrays from both * argument arrays. */ public static byte[][] concatArrays(byte[][] first, byte[][] second) { int firstLen = first.length; int secondLen = second.length; byte[][] newByteArrays = new byte[firstLen + secondLen][]; for (int i = 0; i < firstLen; i++) newByteArrays[i] = first[i]; for (int i = 0; i < secondLen; i++) newByteArrays[i + firstLen] = second[i]; return newByteArrays; } }