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;
   }
}