TrustManager.java

// Copyright (c) ZeroC, Inc.

package com.zeroc.Ice.SSL;

import com.zeroc.Ice.Communicator;
import com.zeroc.Ice.InitializationException;
import com.zeroc.Ice.ParseException;
import com.zeroc.Ice.Properties;

import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.security.auth.x500.X500Principal;

class TrustManager {
    TrustManager(Communicator communicator) {
        assert communicator != null;
        _communicator = communicator;
        Properties properties = communicator.getProperties();
        _traceLevel = properties.getIcePropertyAsInt("IceSSL.Trace.Security");
        String key = null;
        try {
            key = "IceSSL.TrustOnly";
            parse(properties.getProperty(key), _rejectAll, _acceptAll);
            key = "IceSSL.TrustOnly.Client";
            parse(properties.getProperty(key), _rejectClient, _acceptClient);
            key = "IceSSL.TrustOnly.Server";
            parse(properties.getProperty(key), _rejectAllServer, _acceptAllServer);
            Map<String, String> dict =
                properties.getPropertiesForPrefix("IceSSL.TrustOnly.Server.");
            for (Map.Entry<String, String> p : dict.entrySet()) {
                key = p.getKey();
                String name = key.substring("IceSSL.TrustOnly.Server.".length());
                List<List<RFC2253.RDNPair>> reject =
                    new LinkedList<List<RFC2253.RDNPair>>();
                List<List<RFC2253.RDNPair>> accept =
                    new LinkedList<List<RFC2253.RDNPair>>();
                parse(p.getValue(), reject, accept);
                if (!reject.isEmpty()) {
                    _rejectServer.put(name, reject);
                }
                if (!accept.isEmpty()) {
                    _acceptServer.put(name, accept);
                }
            }
        } catch (ParseException ex) {
            throw new InitializationException("Ice.SSL: invalid property " + key, ex);
        }
    }

    boolean verify(ConnectionInfo info, String desc) {
        List<List<List<RFC2253.RDNPair>>>
            reject =
                new LinkedList<List<List<RFC2253.RDNPair>>>(),
            accept =
                new LinkedList<List<List<RFC2253.RDNPair>>>();

        if (!_rejectAll.isEmpty()) {
            reject.add(_rejectAll);
        }
        if (info.incoming) {
            if (!_rejectAllServer.isEmpty()) {
                reject.add(_rejectAllServer);
            }
            if (!info.adapterName.isEmpty()) {
                List<List<RFC2253.RDNPair>> p =
                    _rejectServer.get(info.adapterName);
                if (p != null) {
                    reject.add(p);
                }
            }
        } else {
            if (!_rejectClient.isEmpty()) {
                reject.add(_rejectClient);
            }
        }

        if (!_acceptAll.isEmpty()) {
            accept.add(_acceptAll);
        }
        if (info.incoming) {
            if (!_acceptAllServer.isEmpty()) {
                accept.add(_acceptAllServer);
            }
            if (!info.adapterName.isEmpty()) {
                List<List<RFC2253.RDNPair>> p =
                    _acceptServer.get(info.adapterName);
                if (p != null) {
                    accept.add(p);
                }
            }
        } else {
            if (!_acceptClient.isEmpty()) {
                accept.add(_acceptClient);
            }
        }

        //
        // If there is nothing to match against, then we accept the cert.
        //
        if (reject.isEmpty() && accept.isEmpty()) {
            return true;
        }

        //
        // If there is no certificate then we match false.
        //
        if (info.certs != null && info.certs.length > 0) {
            X500Principal subjectDN =
                ((X509Certificate) info.certs[0]).getSubjectX500Principal();
            String subjectName = subjectDN.getName(X500Principal.RFC2253);
            assert subjectName != null;
            try {
                //
                // Decompose the subject DN into the RDNs.
                //
                if (_traceLevel > 0) {
                    if (info.incoming) {
                        _communicator
                            .getLogger()
                            .trace(
                                "Security",
                                "trust manager evaluating client:\n"
                                    + "subject = "
                                    + subjectName
                                    + "\n"
                                    + "adapter = "
                                    + info.adapterName
                                    + "\n"
                                    + desc);
                    } else {
                        _communicator
                            .getLogger()
                            .trace(
                                "Security",
                                "trust manager evaluating server:\n"
                                    + "subject = "
                                    + subjectName
                                    + "\n"
                                    + desc);
                    }
                }
                List<RFC2253.RDNPair> dn = RFC2253.parseStrict(subjectName);

                //
                // Fail if we match anything in the reject set.
                //
                for (List<List<RFC2253.RDNPair>> matchSet : reject) {
                    if (_traceLevel > 1) {
                        StringBuilder s = new StringBuilder("trust manager rejecting PDNs:\n");
                        stringify(matchSet, s);
                        _communicator.getLogger().trace("Security", s.toString());
                    }
                    if (match(matchSet, dn)) {
                        return false;
                    }
                }

                //
                // Succeed if we match anything in the accept set.
                //
                for (List<List<RFC2253.RDNPair>> matchSet : accept) {
                    if (_traceLevel > 1) {
                        StringBuilder s = new StringBuilder("trust manager accepting PDNs:\n");
                        stringify(matchSet, s);
                        _communicator.getLogger().trace("Security", s.toString());
                    }
                    if (match(matchSet, dn)) {
                        return true;
                    }
                }
            } catch (ParseException ex) {
                _communicator
                    .getLogger()
                    .warning(
                        "Ice.SSL: unable to parse certificate DN `"
                            + subjectName
                            + "'\nreason: "
                            + ex.getMessage());
            }

            //
            // At this point we accept the connection if there are no explicit accept rules.
            //
            return accept.isEmpty();
        }

        return false;
    }

    private boolean match(
            List<List<RFC2253.RDNPair>> matchSet,
            List<RFC2253.RDNPair> subject) {
        for (List<RFC2253.RDNPair> r : matchSet) {
            if (matchRDNs(r, subject)) {
                return true;
            }
        }
        return false;
    }

    private boolean matchRDNs(
            List<RFC2253.RDNPair> match, List<RFC2253.RDNPair> subject) {
        for (RFC2253.RDNPair matchRDN : match) {
            boolean found = false;
            for (RFC2253.RDNPair subjectRDN : subject) {
                if (matchRDN.key.equals(subjectRDN.key)) {
                    found = true;
                    if (!matchRDN.value.equals(subjectRDN.value)) {
                        return false;
                    }
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }

    void parse(
            String value,
            List<List<RFC2253.RDNPair>> reject,
            List<List<RFC2253.RDNPair>> accept)
        throws ParseException {
        //
        // Java X500Principal.getName says:
        //
        // If "RFC2253" is specified as the format, this method emits
        // the attribute type keywords defined in RFC 2253 (CN, L, ST,
        // O, OU, C, STREET, DC, UID). Any other attribute type is
        // emitted as an OID. Under a strict reading, RFC 2253 only
        // specifies a UTF-8 string representation. The String
        // returned by this method is the Unicode string achieved by
        // decoding this UTF-8 representation.
        //
        // This means that things like emailAddress and such will be turned into
        // something like:
        //
        // 1.2.840.113549.1.9.1=#160e696e666f407a65726f632e636f6d
        //
        // The left hand side is the OID (see
        // http://www.columbia.edu/~ariel/ssleay/asn1-oids.html) for a
        // list. The right hand side is a BER encoding of the value.
        //
        // This means that the user input, unless it uses the
        // unfriendly OID format, will not directly match the
        // principal.
        //
        // Two possible solutions:
        //
        // Have the RFC2253 parser convert anything that is not CN, L,
        // ST, O, OU, C, STREET, DC, UID into OID format, and have it
        // convert the values into a BER encoding.
        //
        // Send the user data through X500Principal to string form and
        // then through the RFC2253 encoder. This uses the
        // X500Principal to do the encoding for us.
        //
        // The latter is much simpler, however, it means we need to
        // send the data through the parser twice because we split the
        // DNs on ';' which cannot be blindly split because of quotes,
        // \ and such.
        //
        List<RFC2253.RDNEntry> l = RFC2253.parse(value);
        for (RFC2253.RDNEntry e : l) {
            StringBuilder v = new StringBuilder();
            boolean first = true;
            for (RFC2253.RDNPair pair : e.rdn) {
                if (!first) {
                    v.append(',');
                }
                first = false;
                v.append(pair.key);
                v.append('=');
                v.append(pair.value);
            }
            var principal = new X500Principal(v.toString());
            String subjectName = principal.getName(X500Principal.RFC2253);
            if (e.negate) {
                reject.add(RFC2253.parseStrict(subjectName));
            } else {
                accept.add(RFC2253.parseStrict(subjectName));
            }
        }
    }

    private static void stringify(
            List<List<RFC2253.RDNPair>> matchSet, StringBuilder s) {
        boolean addSemi = false;
        for (List<RFC2253.RDNPair> rdnSet : matchSet) {
            if (addSemi) {
                s.append(';');
            }
            addSemi = true;
            boolean addComma = false;
            for (RFC2253.RDNPair rdn : rdnSet) {
                if (addComma) {
                    s.append(',');
                }
                addComma = true;
                s.append(rdn.key);
                s.append('=');
                s.append(rdn.value);
            }
        }
    }

    private final Communicator _communicator;
    private final int _traceLevel;

    private final List<List<RFC2253.RDNPair>> _rejectAll =
        new LinkedList<List<RFC2253.RDNPair>>();
    private final List<List<RFC2253.RDNPair>> _rejectClient =
        new LinkedList<List<RFC2253.RDNPair>>();
    private final List<List<RFC2253.RDNPair>> _rejectAllServer =
        new LinkedList<List<RFC2253.RDNPair>>();
    private final Map<String, List<List<RFC2253.RDNPair>>> _rejectServer =
        new HashMap<String, List<List<RFC2253.RDNPair>>>();

    private final List<List<RFC2253.RDNPair>> _acceptAll =
        new LinkedList<List<RFC2253.RDNPair>>();
    private final List<List<RFC2253.RDNPair>> _acceptClient =
        new LinkedList<List<RFC2253.RDNPair>>();
    private final List<List<RFC2253.RDNPair>> _acceptAllServer =
        new LinkedList<List<RFC2253.RDNPair>>();
    private final Map<String, List<List<RFC2253.RDNPair>>> _acceptServer =
        new HashMap<String, List<List<RFC2253.RDNPair>>>();
}