# CertificateUtils.java

    /*
    ** Module   : CertificateUtils.java
    ** Abstract : Place for various certificate utility methods
    **
    ** Copyright (c) 2009-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------Description----------------------
    ** 001 CA  20090512   @42162  Created the first version of the file.
    ** 002 CA  20090519   @42244  Added compareNames() method, to compare two
    **                            Distinctive Names. Modified the validation error
    **                            when signature doesn't match certificate.
    ** 003 CA  20090519   @42325  Fixed broken certificate error handling.
    ** 004 NVS 20090811   @43596  Added extended diagnostics for the invalid
    **                            certificates.
    ** 005 SBI 20170320           Moved the GWT shared code out of CertificateUtils
    **                            to be used on the client side.
    ** 006 CA  20200122           Javadoc fixes.
    ** 007 TJD 20240123           Java 17 compatibility updates
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.admin;

    import java.io.*;
    import java.security.cert.*;
    import java.text.*;
    import java.util.*;

    /**
     * Provides utility methods to work with X509 certificates.
     *
     * @author CA
     */
    public class CertificateUtils
    {
       /** Key to identify map with Owner properties. */
       public static final String CERT_OWNER  = "OWNER";

       /** Key to identify map with Issuer properties. */
       public static final String CERT_ISSUER = "ISSUER";

       /** Key to identify Validity Not Before property. */
       public static final String CERT_VALIDITY_NOT_BEFORE = "NOT_BEFORE";

       /** Key to identify Validity Not After property. */
       public static final String CERT_VALIDITY_NOT_AFTER = "NOT_AFTER";

       /** Key to identify certificate Serial Number. */
       public static final String CERT_SERIAL_NUMBER = "SERIAL_NUMBER";

       /** Indicates the certificate is valid. */
       public static final int CERT_VALID = 0;

       /** Indicates the PEM data is not valid. */
       public static final int CERT_INVALID_BROKEN  = 1;

       /** Indicates the certificate is not yet valid. */
       public static final int CERT_INVALID_NOT_YET_VALID = 2;

       /** Indicates the certificate is expired. */
       public static final int CERT_INVALID_EXPIRED = 3;

       /** Indicates the certificate is Peer and self-signed. */
       public static final int CERT_INVALID_PEER_SELF_SIGNED = 4;

       /** Indicates the certificate is Peer and not signed by a CA certificate. */
       public static final int CERT_INVALID_PEER_NOT_SIGNED_BY_CA = 5;

       /** Indicates the signer certificate was not found. */
       public static final int CERT_INVALID_SIGNER_NOT_FOUND = 6;

       /** Indicates the certificate is CA and not signed by a CA certificate. */
       public static final int CERT_INVALID_CA_NOT_SIGNED_BY_CA = 7;

       /**
        * Indicates the certificate is corrupted.
        * Invalid signer's public key.
        */
       public static final int CERT_INVALID_CORRUPTED1 = 8;

       /**
        * Indicates the certificate is corrupted.
        * No such security provider.
        */
       public static final int CERT_INVALID_CORRUPTED2 = 9;

       /**
        * Indicates the certificate is corrupted.
        * No such digital signature algorithm.
        */
       public static final int CERT_INVALID_CORRUPTED3 = 10;

       /**
        * Indicates the certificate is corrupted.
        * Signature does not match contents.
        */
       public static final int CERT_INVALID_CORRUPTED4 = 11;

       /**
        * Indicates the certificate is corrupted.
        * Invalid certificate encoding.
        */
       public static final int CERT_INVALID_CORRUPTED5 = 12;

       /** certificate encoding start mark */
       static final String CERT_BROKEN = "*** broken certificate ***";

       /** certificate encoding start mark */
       static final String CERT_BEG = "-----BEGIN CERTIFICATE-----";

       /** certificate encoding end mark */
       static final String CERT_END = "-----END CERTIFICATE-----";

       /** certificate encoding I/O chunk (as in *.PEM files) */
       static final int SPLIT_CHUNK = 64;

       /**
        * Check if the given certificate is self-signed.
        *
        * @param    cert
        *           The certificate which should be checked.
        *
        * @return   <code>true</code> if this certificate is self-signed
        */
       public static boolean isSelfSigned(CertDef cert)
       {
          X509Certificate x509 = loadCertificate(cert.certificate);
          return x509 != null && isSelfSigned(x509);
       }

       /**
        * Check if the given certificate is self-signed (issuer and owner DN are
        * equal).
        *
        * @param    x509
        *           The certificate which should be checked.
        *
        * @return   <code>true</code> if this certificate is self-signed
        */
       public static boolean isSelfSigned(X509Certificate x509)
       {
          String issuerDN = x509.getIssuerX500Principal().getName();
          String ownerDN = x509.getSubjectX500Principal().getName();

          return compareNames(issuerDN, ownerDN);
       }

       /**
        * Compare the two given Distinctive Names and check if they are identical.
        * Comparison is made in the following way:
        * <ul>
        *    <li>Property names are compared case-insensitive.</li>
        *    <li>Values are compared case-sensitive.</li>
        * </ul>
        *
        * @param   dn1
        *          First Distinctive Name
        * @param   dn2
        *          Second Distinctive Name
        *
        * @return  <code>true</code> if the two Distinctive Names are identical.
        */
       public static boolean compareNames(String dn1, String dn2)
       {
          Map<String, String> m1 = AdminHelper.parseDistinctiveName(dn1);
          Map<String, String> m2 = AdminHelper.parseDistinctiveName(dn2);

          for (String key : m1.keySet())
          {
             String v1 = m1.get(key);

             // m1 and m2 have the keys converted to lowercase, so its safe to
             // just check if m2 has the key
             if (m2.containsKey(key))
             {
                String v2 = m2.get(key);
                if (!v1.equals(v2))
                {
                   return false;
                }

                // remove property, it is identical
                m2.remove(key);
             }
          }

          // if no more properties are in the second name, than they are identical
          return m2.size() == 0;
       }

       /**
        * Get a string describing the given certificate state.
        *
        * @param   state
        *          The state to be described.
        *
        * @return  a text representation of this state.
        */
       public static String getValiditityMessage(int state)
       {
          String msg = "";
          switch (state)
          {
             case CERT_VALID:
             {
                msg = "valid";
                break;
             }
             case CERT_INVALID_BROKEN:
             {
                msg = "invalid: PEM definition broken";
                break;
             }
             case CERT_INVALID_NOT_YET_VALID:
             {
                msg = "not yet valid";
                break;
             }
             case CERT_INVALID_EXPIRED:
             {
                msg = "expired";
                break;
             }
             case CERT_INVALID_PEER_SELF_SIGNED:
             {
                msg = "invalid: peer certificate is self-signed";
                break;
             }
             case CERT_INVALID_PEER_NOT_SIGNED_BY_CA:
             {
                msg = "invalid: peer certificate not signed by a CA certificate";
                break;
             }
             case CERT_INVALID_SIGNER_NOT_FOUND:
             {
                msg = "invalid: signer certificate not found";
                break;
             }
             case CERT_INVALID_CA_NOT_SIGNED_BY_CA:
             {
                msg = "invalid: CA certificate is signed by Peer certificate";
                break;
             }
             case CERT_INVALID_CORRUPTED1:
             {
                msg = "invalid: invalid signer's public key";
                break;
             }
             case CERT_INVALID_CORRUPTED2:
             {
                msg = "invalid: no such security provider";
                break;
             }
             case CERT_INVALID_CORRUPTED3:
             {
                msg = "invalid: no such digital signature algorithm";
                break;
             }
             case CERT_INVALID_CORRUPTED4:
             {
                msg = "invalid: signature does not match contents";
                break;
             }
             case CERT_INVALID_CORRUPTED5:
             {
                msg = "invalid: nnvalid certificate encoding";
                break;
             }
             default:
             {
                msg = "**unknown state**";
                break;
             }
          }

          return msg;
       }

       /**
        * Loads the X509 certificate from the specified definition and returns
        * a map with properties for:
        * <ul>
        *    <li>
        *       {@link #CERT_OWNER} - this property will keep a Map of Distinctive
        *       Name properties for the owner
        *    </li>
        *    <li>
        *       {@link #CERT_ISSUER} - this property will keep a Map of Distinctive
        *       Name properties for the issuer
        *    </li>
        *    <li>
        *       {@link #CERT_VALIDITY_NOT_BEFORE} - the date from which this
        *       certificate starts to be valid.
        *    </li>
        *    <li>
        *       {@link #CERT_VALIDITY_NOT_AFTER} - the date from which this
        *       certificate's validity period ends.
        *    </li>
        * </ul>
        *
        * @param   cd
        *          The certificate definition from which the info will be
        *          extracted.
        *
        * @return  A map with certificate properties or <code>null</code> if the
        *          certificate PEM data could not be loaded.
        */
       public static Map<String, Object> getCertificateProperties(CertDef cd)
       {
          X509Certificate cert = loadCertificate(cd.certificate);

          if (cert == null)
          {
             return null;
          }

          Map<String, String> ownerDetails =
                   AdminHelper.parseDistinctiveName(cert.getSubjectX500Principal().getName());
          if (ownerDetails == null)
          {
             return null;
          }

          Map<String, String> issuerDetails =
                   AdminHelper.parseDistinctiveName(cert.getIssuerX500Principal().getName());
          if (issuerDetails == null)
          {
             return null;
          }

          String notBefore = DateFormat.getInstance().format(cert.getNotBefore());
          String notAfter = DateFormat.getInstance().format(cert.getNotAfter());

          // get owner serial number
          String ssn = cert.getSerialNumber().toString(16).toUpperCase();
          // pretty-print the serial number
          String sn = "";
          for (int i = 0; i < ssn.length(); i++)
          {
             sn = sn + ssn.charAt(i);
             if (i < ssn.length() - 1 && (i + 1) % 2 == 0)
             {
                sn = sn + ":";
             }
          }
          // add serial number to owner's details
          ownerDetails.put(CERT_SERIAL_NUMBER, sn);

          LinkedHashMap details = new LinkedHashMap();
          details.put(CERT_OWNER, ownerDetails);
          details.put(CERT_ISSUER, issuerDetails);
          details.put(CERT_VALIDITY_NOT_BEFORE, notBefore);
          details.put(CERT_VALIDITY_NOT_AFTER, notAfter);

          return details;
       }

       /**
        * Get a map of the owner or issuer Distinctive Name properties.
        *
        * @param   cd
        *          The certificate definition from which the info will be
        *          extracted.
        * @param   owner
        *          <code>true</code> if the owner property should be retrieved;
        *          <code>false</code> for the issuer property.
        *
        * @return  A map with certificate properties or <code>null</code> if the
        *          certificate PEM data could not be loaded.
        */
       public static Map<String, String> getCertificateDNProperties(CertDef cd,
                                                                  boolean owner)
       {
          X509Certificate cert = loadCertificate(cd.certificate);
          if (cert == null)
          {
             return null;
          }

          String DN = owner ? cert.getSubjectX500Principal().getName()
                            : cert.getIssuerX500Principal().getName();

          return AdminHelper.parseDistinctiveName(DN);
       }

       /**
        * Get the value of the specified property, from the Distinctive Name entry
        * associated with the owner or issuer.
        *
        * @param   cd
        *          The certificate definition from which the info will be extracted.
        * @param   prop
        *          The property name.
        * @param   owner
        *          <code>true</code> if the owner property should be retrieved;
        *          <code>false</code> for the issuer property.
        *
        * @return  The value for the specified property.
        */
       public static String getCertificateDNProperty(CertDef cd,
                                                     String prop,
                                                     boolean owner)
       {
          prop = prop.toLowerCase().trim();

          Map<String, String> details = getCertificateDNProperties(cd, owner);
          String val = details == null ? CERT_BROKEN : details.get(prop);

          return val == null ? "" : val;
       }

       /**
        * Parse the given PEM certificate representation and return its X509
        * instance.
        *
        * @param   PEM
        *          The PEM representation of this certificate.
        *
        * @return  The X509 instance for this certificate or <code>null</code> if
        *          the certificate could not be instantiated.
        */
       static X509Certificate loadCertificate(String[] PEM)
       {
          X509Certificate cert = null;

          try
          {
             java.security.cert.CertificateFactory cf =
                CertificateFactory.getInstance("X.509");

             // create a byte-array from the PEM definition
             ByteArrayOutputStream bos =
                new ByteArrayOutputStream(PEM.length);

             for (int i = 0; i < PEM.length; i++)
             {
                bos.write((PEM[i] + "\n").getBytes());
             }

             byte[] bytes = bos.toByteArray();
             bos.close();

             // load the bytes into an input stream and generate the certificate
             ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
             cert = (X509Certificate) cf.generateCertificate(bis);
             bis.close();
          }
          catch (Exception kse)
          {
          }

          return cert;
       }
    }