/* X509CertSelector.java -- selects X.509 certificates by criteria. Copyright (C) 2004 Free Software Foundation, Inc. This file is part of GNU Classpath. GNU Classpath is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. GNU Classpath is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Classpath; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ package java.security.cert; import java.io.IOException; import java.math.BigInteger; import java.security.AccessController; import java.security.KeyFactory; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.security.auth.x500.X500Principal; import gnu.java.security.OID; import gnu.java.security.action.GetPropertyAction; /** * A concrete implementation of {@link CertSelector} for X.509 certificates, * which allows a number of criteria to be set when accepting certificates, * from validity dates, to issuer and subject distinguished names, to some * of the various X.509 extensions. * *

Use of this class requires extensive knowledge of the Internet * Engineering Task Force's Public Key Infrastructure (X.509). The primary * document describing this standard is RFC 3280: Internet X.509 * Public Key Infrastructure Certificate and Certificate Revocation List * (CRL) Profile. * *

Note that this class is not thread-safe. If multiple threads will * use or modify this class then they need to synchronize on the object. * * @author Casey Marshall (csm@gnu.org) */ public class X509CertSelector implements CertSelector, Cloneable { // Constants and fields. // ------------------------------------------------------------------------- private static final String AUTH_KEY_ID = "2.5.29.35"; private static final String SUBJECT_KEY_ID = "2.5.29.14"; private static final String NAME_CONSTRAINTS_ID = "2.5.29.30"; private int basicConstraints; private X509Certificate cert; private BigInteger serialNo; private X500Principal issuer; private X500Principal subject; private byte[] subjectKeyId; private byte[] authKeyId; private boolean[] keyUsage; private Date certValid; private OID sigId; private PublicKey subjectKey; private X509EncodedKeySpec subjectKeySpec; private Set keyPurposeSet; private List altNames; private boolean matchAllNames; private byte[] nameConstraints; private Set policy; // Constructors. // ------------------------------------------------------------------------ /** * Creates a new X.509 certificate selector. The new selector will be * empty, and will accept any certificate (provided that it is an * {@link X509Certificate}). */ public X509CertSelector() { basicConstraints = -1; } // Instance methods. // ------------------------------------------------------------------------ /** * Returns the certificate criterion, or null if this value * was not set. * * @return The certificate. */ public X509Certificate getCertificate() { return cert; } /** * Sets the certificate criterion. If set, only certificates that are * equal to the certificate passed here will be accepted. * * @param cert The certificate. */ public void setCertificate(X509Certificate cert) { this.cert = cert; } /** * Returns the serial number criterion, or null if this * value was not set. * * @return The serial number. */ public BigInteger getSerialNumber() { return serialNo; } /** * Sets the serial number of the desired certificate. Only certificates that * contain this serial number are accepted. * * @param serialNo The serial number. */ public void setSerialNumber(BigInteger serialNo) { this.serialNo = serialNo; } /** * Returns the issuer criterion as a string, or null if this * value was not set. * * @return The issuer. */ public String getIssuerAsString() { if (issuer != null) return issuer.getName(); else return null; } /** * Returns the issuer criterion as a sequence of DER bytes, or * null if this value was not set. * * @return The issuer. */ public byte[] getIssuerAsBytes() throws IOException { if (issuer != null) return issuer.getEncoded(); else return null; } /** * Sets the issuer, specified as a string representation of the issuer's * distinguished name. Only certificates issued by this issuer will * be accepted. * * @param name The string representation of the issuer's distinguished name. * @throws IOException If the given name is incorrectly formatted. */ public void setIssuer(String name) throws IOException { if (name != null) { try { issuer = new X500Principal(name); } catch (IllegalArgumentException iae) { throw new IOException(iae.getMessage()); } } else issuer = null; } /** * Sets the issuer, specified as the DER encoding of the issuer's * distinguished name. Only certificates issued by this issuer will * be accepted. * * @param name The DER encoding of the issuer's distinguished name. * @throws IOException If the given name is incorrectly formatted. */ public void setIssuer(byte[] name) throws IOException { if (name != null) { try { issuer = new X500Principal(name); } catch (IllegalArgumentException iae) { throw new IOException(iae.getMessage()); } } else issuer = null; } /** * Returns the subject criterion as a string, of null if * this value was not set. * * @return The subject. */ public String getSubjectAsString() { if (subject != null) return subject.getName(); else return null; } /** * Returns the subject criterion as a sequence of DER bytes, or * null if this value is not set. * * @return The subject. */ public byte[] getSubjectAsBytes() throws IOException { if (subject != null) return subject.getEncoded(); else return null; } /** * Sets the subject, specified as a string representation of the * subject's distinguished name. Only certificates with the given * subject will be accepted. * * @param name The string representation of the subject's distinguished name. * @throws IOException If the given name is incorrectly formatted. */ public void setSubject(String name) throws IOException { if (name != null) { try { subject = new X500Principal(name); } catch (IllegalArgumentException iae) { throw new IOException(iae.getMessage()); } } else subject = null; } /** * Sets the subject, specified as the DER encoding of the subject's * distinguished name. Only certificates with the given subject will * be accepted. * * @param name The DER encoding of the subject's distinguished name. * @throws IOException If the given name is incorrectly formatted. */ public void setSubject(byte[] name) throws IOException { if (name != null) { try { subject = new X500Principal(name); } catch (IllegalArgumentException iae) { throw new IOException(iae.getMessage()); } } else subject = null; } /** * Returns the subject key identifier criterion, or null if * this value was not set. Note that the byte array is cloned to prevent * modification. * * @return The subject key identifier. */ public byte[] getSubjectKeyIdentifier() { if (subjectKeyId != null) return (byte[]) subjectKeyId.clone(); else return null; } /** * Sets the subject key identifier criterion, or null to clear * this criterion. Note that the byte array is cloned to prevent modification. * * @param subjectKeyId The subject key identifier. */ public void setSubjectKeyIdentifier(byte[] subjectKeyId) { this.subjectKeyId = subjectKeyId != null ? (byte[]) subjectKeyId.clone() : null; } /** * Returns the authority key identifier criterion, or null if * this value was not set. Note that the byte array is cloned to prevent * modification. * * @return The authority key identifier. */ public byte[] getAuthorityKeyIdentifier() { if (authKeyId != null) return (byte[]) authKeyId.clone(); else return null; } /** * Sets the authority key identifier criterion, or null to clear * this criterion. Note that the byte array is cloned to prevent modification. * * @param subjectKeyId The subject key identifier. */ public void setAuthorityKeyIdentifier(byte[] authKeyId) { this.authKeyId = authKeyId != null ? (byte[]) authKeyId.clone() : null; } /** * Returns the date at which certificates must be valid, or null * if this criterion was not set. * * @return The target certificate valitity date. */ public Date getCertificateValid() { if (certValid != null) return (Date) certValid.clone(); else return null; } /** * Sets the date at which certificates must be valid. Specify * null to clear this criterion. * * @param certValid The certificate validity date. */ public void setCertificateValid(Date certValid) { this.certValid = certValid != null ? (Date) certValid.clone() : null; } /** * This method, and its related X.509 certificate extension — the * private key usage period — is not supported under the Internet * PKI for X.509 certificates (PKIX), described in RFC 3280. As such, this * method is not supported either. * *

Do not use this method. It is not deprecated, as it is not deprecated * in the Java standard, but it is basically a no-operation and simply * returns null. * * @return Null. */ public Date getPrivateKeyValid() { return null; } /** * This method, and its related X.509 certificate extension — the * private key usage period — is not supported under the Internet * PKI for X.509 certificates (PKIX), described in RFC 3280. As such, this * method is not supported either. * *

Do not use this method. It is not deprecated, as it is not deprecated * in the Java standard, but it is basically a no-operation. * * @param UNUSED Is silently ignored. */ public void setPrivateKeyValid(Date UNUSED) { } /** * Returns the public key algorithm ID that matching certificates must have, * or null if this criterion was not set. * * @return The public key algorithm ID. */ public String getSubjectPublicKeyAlgID() { return String.valueOf(sigId); } /** * Sets the public key algorithm ID that matching certificates must have. * Specify null to clear this criterion. * * @param sigId The public key ID. * @throws IOException If the specified ID is not a valid object identifier. */ public void setSubjectPublicKeyAlgID(String sigId) throws IOException { if (sigId != null) { try { OID oid = new OID(sigId); int[] comp = oid.getIDs(); if (!checkOid(comp)) throw new IOException("malformed OID: " + sigId); this.sigId = oid; } catch (IllegalArgumentException iae) { IOException ioe = new IOException("malformed OID: " + sigId); ioe.initCause(iae); throw ioe; } } else this.sigId = null; } /** * Returns the subject public key criterion, or null if this * value is not set. * * @return The subject public key. */ public PublicKey getSubjectPublicKey() { return subjectKey; } /** * Sets the subject public key criterion as an opaque representation. * Specify null to clear this criterion. * * @param key The public key. */ public void setSubjectPublicKey(PublicKey key) { this.subjectKey = key; if (key == null) { subjectKeySpec = null; return; } try { KeyFactory enc = KeyFactory.getInstance("X.509"); subjectKeySpec = (X509EncodedKeySpec) enc.getKeySpec(key, X509EncodedKeySpec.class); } catch (Exception x) { subjectKey = null; subjectKeySpec = null; } } /** * Sets the subject public key criterion as a DER-encoded key. Specify * null to clear this value. * * @param key The DER-encoded key bytes. * @throws IOException If the argument is not a valid DER-encoded key. */ public void setSubjectPublicKey(byte[] key) throws IOException { if (key == null) { subjectKey = null; subjectKeySpec = null; return; } try { subjectKeySpec = new X509EncodedKeySpec(key); KeyFactory enc = KeyFactory.getInstance("X.509"); subjectKey = enc.generatePublic(subjectKeySpec); } catch (Exception x) { subjectKey = null; subjectKeySpec = null; IOException ioe = new IOException(x.getMessage()); ioe.initCause(x); throw ioe; } } /** * Returns the public key usage criterion, or null if this * value is not set. Note that the array is cloned to prevent modification. * * @return The public key usage. */ public boolean[] getKeyUsage() { if (keyUsage != null) return (boolean[]) keyUsage.clone(); else return null; } /** * Sets the public key usage criterion. Specify null to clear * this value. * * @param keyUsage The public key usage. */ public void setKeyUsage(boolean[] keyUsage) { this.keyUsage = keyUsage != null ? (boolean[]) keyUsage.clone() : null; } /** * Returns the set of extended key purpose IDs, as an unmodifiable set * of OID strings. Returns null if this criterion is not * set. * * @return The set of key purpose OIDs (strings). */ public Set getExtendedKeyUsage() { if (keyPurposeSet != null) return Collections.unmodifiableSet(keyPurposeSet); else return null; } /** * Sets the extended key usage criterion, as a set of OID strings. Specify * null to clear this value. * * @param keyPurposeSet The set of key purpose OIDs. * @throws IOException If any element of the set is not a valid OID string. */ public void setExtendedKeyUsage(Set keyPurposeSet) throws IOException { if (keyPurposeSet == null) { this.keyPurposeSet = null; return; } Set s = new HashSet(); for (Iterator it = keyPurposeSet.iterator(); it.hasNext(); ) { Object o = it.next(); if (!(o instanceof String)) throw new IOException("not a string: " + o); try { OID oid = new OID((String) o); int[] comp = oid.getIDs(); if (!checkOid(comp)) throw new IOException("malformed OID: " + o); } catch (IllegalArgumentException iae) { IOException ioe = new IOException("malformed OID: " + o); ioe.initCause(iae); throw ioe; } } this.keyPurposeSet = s; } /** * Returns whether or not all specified alternative names must match. * If false, a certificate is considered a match if one of the * specified alternative names matches. * * @return true if all names must match. */ public boolean getMatchAllSubjectAltNames() { return matchAllNames; } /** * Sets whether or not all subject alternative names must be matched. * If false, then a certificate will be considered a match if one * alternative name matches. * * @param matchAllNames Whether or not all alternative names must be * matched. */ public void setMatchAllSubjectAltNames(boolean matchAllNames) { this.matchAllNames = matchAllNames; } /** * Sets the subject alternative names critertion. Each element of the * argument must be a {@link java.util.List} that contains exactly two * elements: the first an {@link Integer}, representing the type of * name, and the second either a {@link String} or a byte array, * representing the name itself. * * @param altNames The alternative names. * @throws IOException If any element of the argument is invalid. */ public void setSubjectAlternativeNames(Collection altNames) throws IOException { if (altNames == null) { this.altNames = null; return; } List l = new ArrayList(altNames.size()); for (Iterator it = altNames.iterator(); it.hasNext(); ) { Object o = it.next(); if (!(o instanceof List) || ((List) o).size() != 2 || !(((List) o).get(0) instanceof Integer) || !(((List) o).get(1) instanceof String) || !(((List) o).get(1) instanceof byte[])) throw new IOException("illegal alternative name: " + o); Integer i = (Integer) ((List) o).get(0); if (i.intValue() < 0 || i.intValue() > 8) throw new IOException("illegal alternative name: " + o + ", bad id: " + i); l.add(new ArrayList((List) o)); } this.altNames = l; } /** * Add a name to the subject alternative names criterion. * * @param id The type of name this is. Must be in the range [0,8]. * @param name The name. * @throws IOException If the id is out of range, or if the name * is null. */ public void addSubjectAlternativeName(int id, String name) throws IOException { if (id < 0 || id > 8 || name == null) throw new IOException("illegal alternative name"); if (altNames == null) altNames = new LinkedList(); ArrayList l = new ArrayList(2); l.add(new Integer(id)); l.add(name); altNames.add(l); } /** * Add a name, as DER-encoded bytes, to the subject alternative names * criterion. * * @param id The type of name this is. */ public void addSubjectAlternativeName(int id, byte[] name) throws IOException { if (id < 0 || id > 8 || name == null) throw new IOException("illegal alternative name"); if (altNames == null) altNames = new LinkedList(); ArrayList l = new ArrayList(2); l.add(new Integer(id)); l.add(name); altNames.add(l); } /** * Returns the name constraints criterion, or null if this * value is not set. Note that the byte array is cloned to prevent * modification. * * @return The name constraints. */ public byte[] getNameConstraints() { if (nameConstraints != null) return (byte[]) nameConstraints.clone(); else return null; } /** * Sets the name constraints criterion; specify null to * clear this criterion. Note that if non-null, the argument will be * cloned to prevent modification. * * @param nameConstraints The new name constraints. * @throws IOException If the argument is not a valid DER-encoded * name constraints. */ public void setNameConstraints(byte[] nameConstraints) throws IOException { // FIXME check if the argument is valid. this.nameConstraints = nameConstraints != null ? (byte[]) nameConstraints.clone() : null; } /** * Returns the basic constraints criterion, or -1 if this value is not set. * * @return The basic constraints. */ public int getBasicConstraints() { return basicConstraints; } /** * Sets the basic constraints criterion. Specify -1 to clear this parameter. * * @param basicConstraints The new basic constraints value. */ public void setBasicConstraints(int basicConstraints) { if (basicConstraints < -1) basicConstraints = -1; this.basicConstraints = basicConstraints; } // The last two criteria not yet implemented are certificate policies // and path-to-names. Both of these are somewhat advanced extensions // (you could probably count the applications that actually use them // on one hand), and they both have no support in the X509Certificate // class. // // Not having support in X509Certificate is not always a problem; for // example, we can compare DER-encoded values as byte arrays for some // extensions. We can't, however, compare them if they are specified // in a set (as policies are). We need to parse the actual value in the // certificate, and check it against the specified set. // FIXME // public void setPolicy(Set policy) throws IOException // { // if (policy != null) // { // for (Iterator it = policy.iterator(); it.hasNext(); ) // try // { // OID oid = new OID((String) it.next()); // int[] i = oid.getIDs(); // if (!checkOid(i)) // throw new IOException("invalid OID"); // } // catch (Exception x) // { // throw new IOException("invalid OID"); // } // } // this.policy = policy != null ? new HashSet(policy) : null; // } // FIXME // public void setPathToNames(Collection names) throws IOException // { // if (names == null) // { // this.names = null; // return; // } // for (Iterator it = names.iterator(); it.hasNext(); ) // { // try // { // List l = (List) it.next(); // if (l.get(1) instanceof String) // addPathToName(((Integer)l.get(0)).intValue(), (String)l.get(1)); // else // addPathToName(((Integer)l.get(0)).intValue(), (byte[])l.get(1)); // } // catch (Exception x) // { // this.names = null; // throw new IOException("invalid names"); // } // } // } // FIXME // public void addPathToName(int id, String name) throws IOException // { // } // FIXME // public void addPathToName(int id, byte[] name) throws IOException // { // } // FIXME // public Collection getSubjectAlternativeNames() // { // return null; // } // FIXME // public Set getPolicy() // { // return null; // } // FIXME // public Collection getPathToNames() // { // return null; // } /** * Match a certificate. This method will check the given certificate * against all the enabled criteria of this selector, and will return * true if the given certificate matches. * * @param certificate The certificate to check. * @return true if the certificate matches all criteria. */ public boolean match(Certificate certificate) { if (!(certificate instanceof X509Certificate)) return false; X509Certificate cert = (X509Certificate) certificate; if (this.cert != null) { try { byte[] e1 = this.cert.getEncoded(); byte[] e2 = cert.getEncoded(); if (!Arrays.equals(e1, e2)) return false; } catch (CertificateEncodingException cee) { return false; } } if (serialNo != null) { if (!serialNo.equals(cert.getSerialNumber())) return false; } if (certValid != null) { try { cert.checkValidity(certValid); } catch (CertificateException ce) { return false; } } if (issuer != null) { if (!issuer.equals(cert.getIssuerX500Principal())) return false; } if (subject != null) { if (!subject.equals(cert.getSubjectX500Principal())) return false; } if (sigId != null) { if (!sigId.equals(cert.getSigAlgOID())) return false; } if (subjectKeyId != null) { byte[] b = cert.getExtensionValue(SUBJECT_KEY_ID); if (!Arrays.equals(b, subjectKeyId)) return false; } if (authKeyId != null) { byte[] b = cert.getExtensionValue(AUTH_KEY_ID); if (!Arrays.equals(b, authKeyId)) return false; } if (keyUsage != null) { boolean[] b = cert.getKeyUsage(); if (!Arrays.equals(b, keyUsage)) return false; } if (basicConstraints >= 0) { if (cert.getBasicConstraints() != basicConstraints) return false; } if (keyPurposeSet != null) { List kp = null; try { kp = cert.getExtendedKeyUsage(); } catch (CertificateParsingException cpe) { return false; } if (kp == null) return false; for (Iterator it = keyPurposeSet.iterator(); it.hasNext(); ) { if (!kp.contains(it.next())) return false; } } if (altNames != null) { Collection an = null; try { an = cert.getSubjectAlternativeNames(); } catch (CertificateParsingException cpe) { return false; } if (an == null) return false; int match = 0; for (Iterator it = altNames.iterator(); it.hasNext(); ) { List l = (List) it.next(); Integer id = (Integer) l.get(0); String s = null; byte[] b = null; if (l.get(1) instanceof String) s = (String) l.get(1); else if (l.get(1) instanceof byte[]) b = (byte[]) l.get(1); else return false; for (Iterator it2 = an.iterator(); it2.hasNext(); ) { Object o = it2.next(); if (!(o instanceof List)) continue; List l2 = (List) o; if (l2.size() != 2) continue; if (!id.equals(l2.get(0))) continue; if (s != null && (l2.get(1) instanceof String) && s.equals(l2.get(1))) match++; else if (b != null && (l2.get(1) instanceof byte[]) && Arrays.equals(b, (byte[]) l2.get(1))) match++; } if (match == 0 || (matchAllNames && match != altNames.size())) return false; } } if (nameConstraints != null) { byte[] nc = cert.getExtensionValue(NAME_CONSTRAINTS_ID); if (!Arrays.equals(nameConstraints, nc)) return false; } // FIXME check policies. // FIXME check path-to-names. return true; } public String toString() { StringBuffer str = new StringBuffer(X509CertSelector.class.getName()); GetPropertyAction getProp = new GetPropertyAction("line.separator"); String nl = (String) AccessController.doPrivileged(getProp); String eol = ";" + nl; str.append(" {").append(nl); if (cert != null) str.append(" certificate = ").append(cert).append(eol); if (basicConstraints >= 0) str.append(" basic constraints = ").append(basicConstraints).append(eol); if (serialNo != null) str.append(" serial number = ").append(serialNo).append(eol); if (certValid != null) str.append(" valid date = ").append(certValid).append(eol); if (issuer != null) str.append(" issuer = ").append(issuer).append(eol); if (subject != null) str.append(" subject = ").append(subject).append(eol); if (sigId != null) str.append(" signature OID = ").append(sigId).append(eol); if (subjectKey != null) str.append(" subject public key = ").append(subjectKey).append(eol); if (subjectKeyId != null) { str.append(" subject key ID = "); for (int i = 0; i < subjectKeyId.length; i++) { str.append(Character.forDigit((subjectKeyId[i] & 0xF0) >>> 8, 16)); str.append(Character.forDigit((subjectKeyId[i] & 0x0F), 16)); if (i < subjectKeyId.length - 1) str.append(':'); } str.append(eol); } if (authKeyId != null) { str.append(" authority key ID = "); for (int i = 0; i < authKeyId.length; i++) { str.append(Character.forDigit((authKeyId[i] & 0xF0) >>> 8, 16)); str.append(Character.forDigit((authKeyId[i] & 0x0F), 16)); if (i < authKeyId.length - 1) str.append(':'); } str.append(eol); } if (keyUsage != null) { str.append(" key usage = "); for (int i = 0; i < keyUsage.length; i++) str.append(keyUsage[i] ? '1' : '0'); str.append(eol); } if (keyPurposeSet != null) str.append(" key purpose = ").append(keyPurposeSet).append(eol); if (altNames != null) str.append(" alternative names = ").append(altNames).append(eol); if (nameConstraints != null) str.append(" name constraints = ").append(eol); str.append("}").append(nl); return str.toString(); } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException shouldNotHappen) { throw new Error(shouldNotHappen); } } // Own methods. // ------------------------------------------------------------------------- private static boolean checkOid(int[] oid) { return (oid != null && oid.length > 2 && (oid[0] >= 0 && oid[0] <= 2) && (oid[1] >= 0 && oid[1] <= 39)); } }