org.cesecore.certificates.certificate.certextensions.standard.NameConstraint.java Source code

Java tutorial

Introduction

Here is the source code for org.cesecore.certificates.certificate.certextensions.standard.NameConstraint.java

Source

/*************************************************************************
 *                                                                       *
 *  CESeCore: CE Security Core                                           *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/
package org.cesecore.certificates.certificate.certextensions.standard;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralSubtree;
import org.bouncycastle.asn1.x509.NameConstraints;
import org.cesecore.certificates.ca.CA;
import org.cesecore.certificates.ca.X509CA;
import org.cesecore.certificates.ca.internal.CertificateValidity;
import org.cesecore.certificates.certificate.certextensions.CertificateExtensionException;
import org.cesecore.certificates.certificateprofile.CertificateProfile;
import org.cesecore.certificates.endentity.EndEntityInformation;
import org.cesecore.certificates.endentity.ExtendedInformation;
import org.cesecore.util.CeSecoreNameStyle;

/**
 * Extension for Name Constraints.
 * <a href="https://tools.ietf.org/html/rfc5280#section-4.2.1.10">RFC 5280</a>
 * 
 * For storing Name Constraints, an internal encoded form is used. The format is "type-id:data"
 * where data is either a regular string or hex-encoded data, depending on the type.
 * Use parseNameConstraintList to convert human-readable strings into encoded strings.
 * 
 * @version $Id: NameConstraint.java 20474 2014-12-17 10:45:46Z samuellb $
 */
public class NameConstraint extends StandardCertificateExtension {

    @Override
    public void init(CertificateProfile certProf) {
        super.setOID(Extension.nameConstraints.getId());
        super.setCriticalFlag(certProf.getNameConstraintsCritical());
    }

    @Override
    public ASN1Encodable getValue(EndEntityInformation userData, CA ca, CertificateProfile certProfile,
            PublicKey userPublicKey, PublicKey caPublicKey, CertificateValidity val)
            throws CertificateExtensionException {
        NameConstraints nc = null;

        if (!(ca instanceof X509CA)) {
            throw new CertificateExtensionException("Can't issue non-X509 certificate with Name Constraint");
        }

        final ExtendedInformation ei = userData.getExtendedinformation();
        if (ei != null) {
            final List<String> permittedNames = ei.getNameConstraintsPermitted();
            final List<String> excludedNames = ei.getNameConstraintsExcluded();

            if (permittedNames != null || excludedNames != null) {
                final GeneralSubtree[] permitted = toGeneralSubtrees(permittedNames);
                final GeneralSubtree[] excluded = toGeneralSubtrees(excludedNames);

                // Do not include an empty name constraints extension
                if (permitted.length != 0 || excluded.length != 0) {
                    nc = new NameConstraints(permitted, excluded);
                }
            }
        }

        return nc;
    }

    /**
     * Converts a list of encoded strings of Name Constraints into ASN1 GeneralSubtree objects.
     * This is needed when creating an BouncyCastle ASN1 NameConstraint object for inclusion
     * in a certificate.
     */
    public static GeneralSubtree[] toGeneralSubtrees(List<String> list) {
        if (list == null) {
            return new GeneralSubtree[0];
        }

        GeneralSubtree[] ret = new GeneralSubtree[list.size()];
        int i = 0;
        for (String entry : list) {
            int type = getNameConstraintType(entry);
            Object data = getNameConstraintData(entry);
            GeneralName genname;
            switch (type) {
            case GeneralName.dNSName:
            case GeneralName.rfc822Name:
                genname = new GeneralName(type, (String) data);
                break;
            case GeneralName.directoryName:
                genname = new GeneralName(new X500Name(CeSecoreNameStyle.INSTANCE, (String) data));
                break;
            case GeneralName.iPAddress:
                genname = new GeneralName(type, new DEROctetString((byte[]) data));
                break;
            default:
                throw new UnsupportedOperationException(
                        "Encoding of name constraint type " + type + " is not implemented.");
            }
            ret[i++] = new GeneralSubtree(genname);
        }
        return ret;
    }

    /**
     * Returns the GeneralName type code for an encoded Name Constraint.
     */
    private static int getNameConstraintType(String encoded) {
        String typeString = encoded.split(":", 2)[0];
        if ("iPAddress".equals(typeString))
            return GeneralName.iPAddress;
        if ("dNSName".equals(typeString))
            return GeneralName.dNSName;
        if ("directoryName".equals(typeString))
            return GeneralName.directoryName;
        if ("rfc822Name".equals(typeString))
            return GeneralName.rfc822Name;
        throw new UnsupportedOperationException("Unsupported name constraint type " + typeString);
    }

    /**
     * Returns the GeneralName data (as a byte array or String) from an encoded string.
     */
    private static Object getNameConstraintData(String encoded) {
        int type = getNameConstraintType(encoded);
        String data = encoded.split(":", 2)[1];

        switch (type) {
        case GeneralName.dNSName:
        case GeneralName.directoryName:
        case GeneralName.rfc822Name:
            return data;
        case GeneralName.iPAddress:
            try {
                return Hex.decodeHex(data.toCharArray());
            } catch (DecoderException e) {
                throw new IllegalStateException("internal name constraint data could not be decoded as hex", e);
            }
        default:
            throw new UnsupportedOperationException("Unsupported name constraint type " + type);
        }
    }

    /**
     * Parses a single name constraint entry in human-readable form into
     * an encoded string for database storage etc. The intention is to make it possible
     * to change the human readable form at a later point.
     * 
     * This format is essentially a hex string representation of a RFC 5280 GeneralName,
     * but only DNS Names and IP Addresses are supported so far.
     * 
     * @throws CertificateExtensionException if the string can not be parsed.
     */
    private static String parseNameConstraintEntry(String str) throws CertificateExtensionException {
        if (str.matches("^([0-9]+\\.){3,3}([0-9]+)/[0-9]+$")
                || str.matches("^[0-9a-fA-F]{0,4}:[0-9a-fA-F]{0,4}:[0-9a-fA-F:]*/[0-9]+$")) {
            // IPv4 or IPv6 address
            try {
                String[] pieces = str.split("/", 2);
                byte[] addr = InetAddress.getByName(pieces[0]).getAddress();
                byte[] encoded = new byte[2 * addr.length]; // will hold address and netmask
                System.arraycopy(addr, 0, encoded, 0, addr.length);

                // The second half in the encoded form is the netmask
                int netmask = Integer.parseInt(pieces[1]);
                if (netmask > 8 * addr.length) {
                    throw new CertificateExtensionException("Netmask is too large: " + str);
                }
                for (int i = 0; i < netmask; i++) {
                    encoded[addr.length + i / 8] |= 1 << (7 - i % 8);
                }
                // Clear host part from IP address
                for (int i = netmask; i < 8 * addr.length; i++) {
                    encoded[i / 8] &= ~(1 << (7 - i % 8));
                }
                return "iPAddress:" + Hex.encodeHexString(encoded);
            } catch (UnknownHostException e) {
                throw new CertificateExtensionException("Failed to parse IP address in name constraint: " + str, e);
            }
        } else if (str.matches("^([0-9]+\\.){3,3}([0-9]+)$")) {
            // IP address without netmask. This is not a valid DNS name, so catch it here.
            throw new CertificateExtensionException("Name constraint entry with IP address is missing a netmask: "
                    + str + ". Use /32 to match only this address.");
        } else if (str.matches("^\\.?([a-zA-Z0-9_-]+\\.)*[a-zA-Z0-9_-]+$")) {
            // DNS name (it can start with a ".", this means "all subdomains")
            return "dNSName:" + str;
        } else if (str.matches("^[^=,]*@[a-zA-Z0-9_.\\[\\]:-]+$")) {
            // RFC 822 Name (i.e. e-mail)
            if (str.startsWith("@")) {
                // In EJBCA, rfc822Names without a user part start with @ to distinguish them from domain names.
                // This is not the case in the encoded form.
                str = str.substring(1);
            }
            return "rfc822Name:" + str;
        } else if (str.contains("=")) {
            // Directory name
            return "directoryName:" + new X500Name(CeSecoreNameStyle.INSTANCE, str).toString();
        } else {
            throw new CertificateExtensionException(
                    "Cannot parse name constraint entry (only DNS Name, RFC 822 Name, Directory Name, IPv4/Netmask and IPv6/Netmask are supported): "
                            + str);
        }
    }

    /**
     * Parses human readable name constraints, one entry per line, into a list of encoded name constraints.
     * @see parseNameConstraintEntry
     */
    public static List<String> parseNameConstraintsList(String input) throws CertificateExtensionException {
        List<String> encodedNames = new ArrayList<String>();
        if (input != null) {
            String[] pieces = input.split("\n");
            for (String piece : pieces) {
                piece = piece.trim();
                if (!piece.isEmpty()) {
                    encodedNames.add(NameConstraint.parseNameConstraintEntry(piece));
                }
            }
        }
        return encodedNames;
    }

    /**
     * Formats an encoded name constraint from parseNameConstraintEntry into human-readable form.
     */
    private static String formatNameConstraintEntry(String encoded) {
        if (encoded == null) {
            return "";
        }

        int type = getNameConstraintType(encoded);
        Object data = getNameConstraintData(encoded);

        switch (type) {
        case GeneralName.dNSName:
        case GeneralName.directoryName:
            return (String) data; // not changed during encoding
        case GeneralName.iPAddress:
            byte[] bytes = (byte[]) data;
            byte[] ip = new byte[bytes.length / 2];
            byte[] netmaskBytes = new byte[bytes.length / 2];
            System.arraycopy(bytes, 0, ip, 0, ip.length);
            System.arraycopy(bytes, ip.length, netmaskBytes, 0, netmaskBytes.length);

            int netmask = 0;
            for (int i = 0; i < 8 * netmaskBytes.length; i++) {
                final boolean one = (netmaskBytes[i / 8] >> (7 - i % 8) & 1) == 1;
                if (one && netmask == i) {
                    netmask++; // leading ones
                } else if (one) {
                    // trailings ones = error!
                    throw new IllegalArgumentException("Unsupported netmask with mixed ones/zeros");
                }
            }

            try {
                return InetAddress.getByAddress(ip).getHostAddress() + "/" + netmask;
            } catch (UnknownHostException e) {
                throw new IllegalArgumentException(e);
            }
        case GeneralName.rfc822Name:
            // Prepend @ is it's only the domain part to distinguish from DNS names
            String str = (String) data;
            return (str.contains("@") ? str : "@" + str);
        default:
            throw new UnsupportedOperationException("Unsupported name constraint type " + type);
        }
    }

    /**
     * Formats an encoded list of name constraints into a human-readable list, with one entry per line
     */
    public static String formatNameConstraintsList(List<String> encodedList) {
        StringBuilder sb = new StringBuilder();
        if (encodedList != null) {
            boolean first = true;
            for (String encodedName : encodedList) {
                if (!first) {
                    sb.append('\n');
                }
                first = false;
                sb.append(formatNameConstraintEntry(encodedName));
            }
        }
        return sb.toString();
    }

}