BCCertFactory.java

/*
** Module   : BCCertFactory.java
** Abstract : Implementation to generate SSL certificates and their private keys using Bouncy 
**            Castle library. 
**
** Copyright (c) 2014-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 CA  20140207 First version.
** 002 CA  20160106 Enforce a RSA key length of at least 2048 bits.
** 003 GES 20170203 Expose a helper routine as a static.
** 004 CA  20170603 Include the subject alternative name (as DNS) in the certificate's extensions;
**                  this solves problems with validating the certificate in recent versions of FF 
**                  and Chrome.
** 005 CA  20170630 Added setRootCA().
** 006 SBI 20171030 Added loadRootCA(certPemFile, privateKeyPemFile, certChainPemFile) to load
*                   external public and private key pair and its trusted certificate chain.
** 007 SBI 20171109 Fixed loadRootCA to load trusted certificate chain correctly, to save trusted
**                  chain into the certificates store.
**     SBI 20171111 Added getFullChainFromRoot and setCertificateChain.
** 008 SBI 20171112 Changed to load certificate suite.
**     SBI 20171116 Changed loadCertificateSuite to support the case of using the full chain file.
** 009 TJD 20240220 Removed compiler warnings related with libraries 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.security;

import java.io.*;
import java.math.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.security.spec.*;
import java.util.*;

import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.oiw.*;
import org.bouncycastle.asn1.pkcs.*;
import org.bouncycastle.asn1.x500.*;
import org.bouncycastle.asn1.x500.style.*;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.*;
import org.bouncycastle.cert.jcajce.*;
import org.bouncycastle.crypto.*;
import org.bouncycastle.crypto.engines.*;
import org.bouncycastle.crypto.generators.*;
import org.bouncycastle.crypto.paddings.*;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.util.*;
import org.bouncycastle.jce.provider.*;
import org.bouncycastle.openssl.*;
import org.bouncycastle.openssl.jcajce.*;
import org.bouncycastle.operator.*;
import org.bouncycastle.operator.bc.*;
import org.bouncycastle.operator.jcajce.*;

// explicit declaration to resolve Base64  
import com.goldencode.p2j.directory.Base64;
import com.goldencode.util.*;

/**
 * Implementation to generate SSL certificates and their private keys, using the Bouncy Castle
 * library.
 * <p>
 * Before starting to generate certificates, a root CA needs to be generated by calling 
 * {@link #generateRootCA}.  This will be saved and will be used for signing all the issued 
 * certificates, using this {@link BCCertFactory} instance.
 */
class BCCertFactory
extends SSLCertFactory
{
   /** Secure random to generate the keys. */
   private final SecureRandom srnd = new SecureRandom();
   
   /** Helper to generate X509 extensions. */
   private final X509ExtensionUtils extUtils;
   
   /** The Bouncy Castle-style root CA certificate. */
   private X509CertificateHolder rootCAHolder = null;
   
   /** The private key for the root CA certificate, used to sign all issued certificates. */
   private AsymmetricKeyParameter rootCAPrivateKey;

   /** The X509-encoded root CA certificate. */
   private X509Certificate rootCAX509Cert;
   
   /** The certificate chain from the root CA up to the well-known authority.*/
   private List<Certificate> certChain = new LinkedList<Certificate>();
   
   static
   {
      // register the Bouncy Castle as a security algorithm provider.
      Security.addProvider(new BouncyCastleProvider());
   }
   
   /**
    * Create a new Bouncy Castle-style SSL certificate factory.
    * 
    * @throws   SSLCertGenException
    *           If the factory could not be initialized.
    */
   public BCCertFactory()
   throws SSLCertGenException
   {
      DigestCalculator digCalc;
      try
      {
         digCalc = new BcDigestCalculatorProvider()
            .get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));

         this.extUtils = new X509ExtensionUtils(digCalc);
      }
      catch (OperatorCreationException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Encrypt or decrypt the given byte array, using AES and the provided password.
    * 
    * @param    encrypt
    *           Flag indicating if we need to encrypt or decrypt.
    * @param    bytes
    *           The bytes to be processed.
    * @param    password
    *           The encrypt/decrypt password.
    *           
    * @return   The processed byte array.
    * 
    * @throws   SSLCertGenException
    *           If the was problems during processing.
    */
   public static byte[] cipherBytes(boolean encrypt, byte[] bytes, String password)
   throws SSLCertGenException
   {
      int bits = password.getBytes().length * 8;
      if (!(bits == 128 || bits == 192 || bits == 256))
      {
         throw new SSLCertGenException("The AES key must have 128, 192 or 256 bits!");
      }
      
      PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(AESEngine.newInstance());
      cipher.init(encrypt, new KeyParameter(password.getBytes()));

      ByteArrayOutputStream out = new ByteArrayOutputStream();
      ByteArrayInputStream in = new ByteArrayInputStream(bytes);
      
      byte[] inBuf = new byte[16];
      byte[] outBuf = new byte[512];
      int bytesRead = -1;
      int bytesProcessed = 0;
      
      try
      {
         while ((bytesRead = in.read(inBuf)) >= 0)
         {
            bytesProcessed = cipher.processBytes(inBuf, 0, bytesRead, outBuf, 0);
            out.write(outBuf, 0, bytesProcessed);
         }
         bytesProcessed = cipher.doFinal(outBuf, 0);
         out.write(outBuf, 0, bytesProcessed);

         out.flush();
      }
      catch (DataLengthException        |
             IllegalStateException      |
             InvalidCipherTextException |
             IOException e)
      {
         throw new SSLCertGenException(e);
      }
      
      return out.toByteArray();
   }
   
   /**
    * Initiatialize this SSL certificate factory.
    * <p>
    * The existing root CA will be removed.
    *  
    * @param    keyStrength
    *           The private key size: {@link SSLCertFactory#MIN_RSA_KEY_STRENGTH} bits or better.  
    *           If {@code null}, defaults to {@link SSLCertFactory#MIN_RSA_KEY_STRENGTH}.
    * @param    exponent
    *           The public key exponent.  If {@code null}, defaults to 65537.
    *           WARNING: a wrong value may result in vulnerable SSL private keys and also 3rd party 
    *           software might not accept them. Use with care.
    * 
    * @throws   SSLCertGenException
    *           If the factory could not be instantiated.
    */
   public void init(Integer keyStrength, BigInteger exponent)
   throws SSLCertGenException
   {
      super.init(keyStrength, exponent);
      
      this.rootCAHolder = null;
      this.rootCAPrivateKey = null;
      this.rootCAX509Cert = null;
   }
   
   /**
    * Decrypt a private key which was previously AES encrypted with the given password.
    * 
    * @param    encrypted
    *           The bytes representing the encrypted private key.
    * @param    password
    *           The encryption password.
    * 
    * @return   The decrypted {@link PrivateKey private key}.
    * 
    * @throws   SSLCertGenException
    *           If the private key could not be decrypted.
    */
   @Override
   public PrivateKey decryptPrivateKey(byte[] encrypted, String password)
   throws SSLCertGenException
   {
      try
      {
         byte[] encoded = cipherBytes(false, encrypted, password);

         PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
         KeyFactory kf = KeyFactory.getInstance("RSA");
         PrivateKey key = kf.generatePrivate(keySpec);
         
         return key;
      }
      catch (NoSuchAlgorithmException | InvalidKeySpecException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Encrypt the given key using AES and the provided password.
    * 
    * @param    key
    *           The key to be encrypted.
    * @param    password
    *           The encryption password.
    *           
    * @return   A byte array with the encrypted key.
    * 
    * @throws   SSLCertGenException
    *           If the key could not be encrypted.
    */
   @Override
   public byte[] encryptPrivateKey(Key key, String password)
   throws SSLCertGenException
   {
      return cipherBytes(true, key.getEncoded(), password);
   }

   /**
    * Generate a certificate and sign it with the already generated root CA.
    * <p>
    * The encrypted private key will be saved in the specified {@code certKeyStore}; the encrypt
    * password will be returned by this API.
    * <p>
    * The public certificate will be saved in the specified {@code certStore} and will be signed
    * using the {@link #rootCAPrivateKey}.
    * 
    * @param    alias
    *           The certificate alias, used to store the private key and certificate.
    * @param    validity
    *           The certificate validity, in years.
    * @param    commonName
    *           The certificate's common name (CN).
    * @param    fieldMap
    *           A map with additional subject attributes.
    * @param    certStore
    *           The store where to save the certificate.
    * @param    certKeyStore
    *           The store where to save the private key.
    *
    * @return   The password used to encrypt the private key in the store.
    * 
    * @throws   SSLCertGenException
    *           If the root CA is not yet generated or the certificate could not be generated.
    */
   @Override
   public String generateCertificate(String              alias,
                                     int                 validity,
                                     String              commonName,
                                     Map<String, String> fieldMap,
                                     KeyStore            certStore,
                                     KeyStore            certKeyStore)
   throws SSLCertGenException
   {
      if (rootCAHolder == null)
      {
         throw new SSLCertGenException("First a root CA must be generated using generateRootCA!");
      }
      
      X500Name subject = buildSubject(fieldMap, commonName);

      AsymmetricCipherKeyPair subjectKeyPair = generateKeyPair();

//      KeyPurposeId[] usages =
//      {
//         KeyPurposeId.anyExtendedKeyUsage,
//         KeyPurposeId.id_kp_serverAuth,
//         KeyPurposeId.id_kp_clientAuth
//      };
      X509CertificateHolder cert = generateCertificate(validity, subject, subjectKeyPair,
                                                       rootCAHolder, rootCAPrivateKey,
                                                       false, null);
      X509Certificate x509 = convertCertificate(cert);
       
      try
      {
         x509.verify(rootCAX509Cert.getPublicKey());
      }
      catch (Exception e)
      {
         throw new SSLCertGenException(e);
      }
      
      try
      {
         certStore.setCertificateEntry(alias, x509);
      }
      catch (KeyStoreException e)
      {
         throw new SSLCertGenException(e);
      }
      
      String keyPassword = RandomWordGenerator.create();

      Certificate[] rootChain = getFullChainFromRoot();
      
      Certificate[] endEntityChain = new Certificate[rootChain.length + 1];
      
      endEntityChain[0] = x509;
      
      System.arraycopy(rootChain, 0, endEntityChain, 1, rootChain.length);
      
      // save the private key to the keystore
      saveKeyEntry(certKeyStore,
                   subjectKeyPair,
                   alias,
                   keyPassword.toCharArray(),
                   endEntityChain);
      
      return keyPassword;
   }

   /**
    * Generate a self-signed root CA certificate, which will be used to sign all the issues 
    * certificates.
    * <p>
    * The encrypted private key will be saved in the specified {@code certKeyStore}; the encrypt
    * password will be returned by this API.
    * <p>
    * The public root CA certificate will be saved in the specified {@code certStore}.
    * 
    * @param    alias
    *           The certificate alias, used to store the private key and certificate.
    * @param    validity
    *           The certificate validity, in years.
    * @param    commonName
    *           The certificate's common name (CN).
    * @param    fieldMap
    *           A map with additional subject attributes.
    * @param    certStore
    *           The store where to save the certificate.
    * @param    certKeyStore
    *           The store where to save the private key.
    *
    * @return   The password used to encrypt the private key in the store.
    * 
    * @throws   SSLCertGenException
    *           If the root CA is not yet generated or the certificate could not be generated.
    */
   @Override
   public String generateRootCA(String              alias,
                                int                 validity,
                                String              commonName,
                                Map<String, String> fieldMap,
                                KeyStore            certStore,
                                KeyStore            certKeyStore)
   throws SSLCertGenException
   {
      return generateSelfSignedCertificate(alias, true, validity, commonName, fieldMap,
                                           certStore, certKeyStore);
   }
   
   /**
    * Set the details for the root CA certificate.
    * 
    * @param    cert
    *           The certificate.
    * @param    pk
    *           The private key.
    */
   @Override
   public void setRootCA(X509Certificate cert, PrivateKey pk)
   throws SSLCertGenException
   {
      try
      {
         this.rootCAX509Cert = cert;
         this.rootCAHolder = new JcaX509CertificateHolder(cert);
         
         String pkPEM = "-----BEGIN PRIVATE KEY-----\n" +
                        Base64.byteArrayToBase64(pk.getEncoded()) +
                        "\n-----END PRIVATE KEY-----\n";
         PEMParser parser = new PEMParser(new StringReader(pkPEM));
         this.rootCAPrivateKey = PrivateKeyFactory.createKey((PrivateKeyInfo) parser.readObject());
         parser.close();
      }
      catch (CertificateEncodingException | IOException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Loads externally generated certificate suite of public and private certificates with
    * trusted certificate chain if this PEM file is provided.
    * 
    * @param    certPemFile
    *           The PEM file that contains the public certificate signed by well-known authorities
    *           or self-signed.
    * @param    privateKeyPemFile
    *           The PEM file that contains the private certificate signed by well-known authorities
    *           or self-signed.
    * @param    certChainPemFile
    *           The PEM file that contains the intermediate certificates up to the well-known
    *           authority CA certificate. 
    * 
    * @return   The certificate suite
    * 
    * @throws   SSLCertGenException
    *           If IO or parse exceptions are thrown
    */
   public CertificateSuite loadCertificateSuite(String certPemFile,
                                                String privateKeyPemFile,
                                                String certChainPemFile)
   throws SSLCertGenException
   {
      CertificateSuite suite = new CertificateSuite();
      
      PrivateKey privateKey = null;
      
      X509Certificate publicKey = null;
      
      X509Certificate[] chain   = null;
      
      try
      {
         try (FileReader certReader       = new FileReader(certPemFile);
              PEMParser  certParser       = new PEMParser(certReader);
              FileReader privateKeyReader = new FileReader(privateKeyPemFile);
              PEMParser  privateKeyParser = new PEMParser(privateKeyReader);
              )
         {
            PrivateKeyInfo privateKeyInfo = null;
            X509CertificateHolder cert = null;
            PEMKeyPair pemKeyPair = null;
            Object obj;
            while((obj = privateKeyParser.readObject()) != null)
            {
               if (obj instanceof PrivateKeyInfo && privateKeyInfo == null)
               {
               // private key found
                  privateKeyInfo = (PrivateKeyInfo) obj;
               }
               else if (obj instanceof PEMKeyPair && privateKeyInfo == null)
               {
                  // it contains a private and public keys pair
                  pemKeyPair = (PEMKeyPair) obj;
                  
                  privateKeyInfo = pemKeyPair.getPrivateKeyInfo();
               }
               else if (obj instanceof X509CertificateHolder && cert == null)
               {// this case ?
                  //it contains a certificate
                  cert = (X509CertificateHolder) obj;
               }
            }
            
            while((obj = certParser.readObject()) != null)
            {
               if (obj instanceof PrivateKeyInfo && privateKeyInfo == null)
               {
                  // private key found
                  privateKeyInfo = (PrivateKeyInfo) obj;
               }
               else if (obj instanceof PEMKeyPair && privateKeyInfo == null)
               {
                  // it contains a private and public keys pair
                  pemKeyPair = (PEMKeyPair) obj;
                  privateKeyInfo = pemKeyPair.getPrivateKeyInfo();
                  
               }
               else if (obj instanceof X509CertificateHolder && cert == null)
               {
                  //it contains a certificate
                  cert = (X509CertificateHolder) obj;
               }
            }
            
            if (privateKeyInfo == null)
            {
               throw new SSLCertGenException("Private key was not found!");
            }
            
            if (cert == null)
            {
               throw new SSLCertGenException("Certificate was not found!");
            }
            
            privateKey = new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
            
            publicKey = convertCertificate(cert);
            
            List<X509Certificate> chainList = new LinkedList<>();
            
            if (certChainPemFile != null && !certChainPemFile.isEmpty())
            {
               // chain pem file provided
               try(FileReader certChainReader = new FileReader(certChainPemFile);
                   PEMParser  certChainParser = new PEMParser(certChainReader);)
               {
                  while((obj = certChainParser.readObject()) != null)
                  {
                     if (obj instanceof X509CertificateHolder)
                     {
                        chainList.add(convertCertificate((X509CertificateHolder) obj));
                     }
                  }
               }
            }
            
            // chainList can be a full chain
            if (!chainList.isEmpty() && chainList.get(0).equals(publicKey))
            {
               chainList.remove(0);
            }
            
            chain = chainList.toArray(new X509Certificate[chainList.size()]);
         }
         
         suite.publicKey  = publicKey;
         suite.privateKey = privateKey;
         suite.chain      = chain;
         
         return suite;
      }
      catch (IOException ex)
      {
         throw new SSLCertGenException(ex);
      }
   }

   /**
    * Generate a self-signed certificate. This may be used as a root CA.
    * <p>
    * The encrypted private key will be saved in the specified {@code certKeyStore}; the encrypt
    * password will be returned by this API.
    * <p>
    * The public certificate will be saved in the specified {@code certStore}.
    * 
    * @param    alias
    *           The certificate alias, used to store the private key and certificate.
    * @param    certificateAuthority
    *           Flag indicating if the generated self-signed certificate will be used as the root
    *           CA.
    * @param    validity
    *           The certificate validity, in years.
    * @param    commonName
    *           The certificate's common name (CN).
    * @param    fieldMap
    *           A map with additional subject attributes.
    * @param    certStore
    *           The store where to save the certificate.
    * @param    certKeyStore
    *           The store where to save the private key.
    *
    * @return   The password used to encrypt the private key in the store.
    * 
    * @throws   SSLCertGenException
    *           If the root CA is not yet generated or the certificate could not be generated.
    */
   @Override
   public String generateSelfSignedCertificate(String              alias,
                                               boolean             certificateAuthority,
                                               int                 validity,
                                               String              commonName,
                                               Map<String, String> fieldMap,
                                               KeyStore            certStore,
                                               KeyStore            certKeyStore)
   throws SSLCertGenException
   {
      X500Name subject = buildSubject(fieldMap, commonName);

      AsymmetricCipherKeyPair subjectKeyPair = generateKeyPair();
      
      AsymmetricCipherKeyPair issuerKeyPair = subjectKeyPair;
      
      X509CertificateHolder certHolder = generateCertificate(validity,
                                                             subject, subjectKeyPair,
                                                             null, issuerKeyPair.getPrivate(),
                                                             certificateAuthority, null);

      X509Certificate x509 = convertCertificate(certHolder);
      try
      {
         x509.verify(x509.getPublicKey());
      }
      catch (Exception e)
      {
         throw new SSLCertGenException(e);
      }
      
      if (certificateAuthority)
      {
         // save these, for later usage
         this.rootCAHolder     = certHolder;
         this.rootCAPrivateKey = subjectKeyPair.getPrivate();
         this.rootCAX509Cert   = x509;
      }
      
      // save the certificate to the store
      return saveRootCA(alias, certStore, certKeyStore);
   }

   /**
    * Save the root CA into the provided certificate and private key stores to be accessed by the
    * given alias.
    * 
    * @param    alias
    *           The certificate alias
    * @param    certStore
    *           The certificate store
    * @param    certKeyStore
    *           The certificate private key store
    * 
    * @return   The protected private key password.
    * 
    * @throws   SSLCertGenException
    * 
    */
   public String saveRootCA(String                  alias,
                            KeyStore                certStore,
                            KeyStore                certKeyStore)
   throws SSLCertGenException
   {
      // save the certificate to the store
      try
      {
         certStore.setCertificateEntry(alias, this.rootCAX509Cert);
      }
      catch (KeyStoreException e)
      {
         throw new SSLCertGenException(e);
      }
      
      // save the private key to the keystore
      try
      {
         String keyPassword = RandomWordGenerator.create();
         
         Certificate[] chain = getFullChainFromRoot();
         
         saveKeyEntry(certKeyStore,
                      new AsymmetricCipherKeyPair(
                         PublicKeyFactory.createKey(this.rootCAHolder.getSubjectPublicKeyInfo()),
                         this.rootCAPrivateKey),
                      alias,
                      keyPassword.toCharArray(),
                      chain);
         
         return keyPassword;
      }
      catch (IOException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Build a full chain from the root certificate up to the well-known authority.
    * 
    * @return   Certificate chain
    */
   public Certificate[] getFullChainFromRoot()
   {
      Certificate[] chain;
      
      if (this.certChain.isEmpty())
      {
         chain = new Certificate[] { this.rootCAX509Cert };
      }
      else
      {
         List<Certificate> list;
         // check the first certificate in this chain (full chain?)
         if (certChain.get(0).equals(this.rootCAX509Cert))
         {
            list = this.certChain;
         }
         else
         {
            list = new LinkedList<Certificate>();
            list.add(this.rootCAX509Cert);
            list.addAll(this.certChain);
         }
         chain = list.toArray(new Certificate[list.size()]);
      }
      
      return chain;
   }
   
   /**
    * Get the map with the mandatory subject attributes.  The key is the string representation of
    * this attribute, while the value will be the associated description. 
    * <p>
    * This includes the "Organization Unit (OU)", "Organization (O)", "Locality (city, etc) (L)", 
    * "State or Province Name (ST)" and "Country Code (C)" fields.

    * @return   See above.
    */
   @Override
   public Map<String, String> getMandatorySubjectFields()
   {
      ASN1ObjectIdentifier[] keys =
      {
         BCStyle.OU,
         BCStyle.O,
         BCStyle.L,
         BCStyle.ST,
         BCStyle.C,
      };
      String[] descr =
      {
         "Organization Unit",
         "Organization",
         "Locality (city, etc)",
         "State or Province Name",
         "Country Code"
      };
      Map<String, String> mandatoryKeys = new LinkedHashMap<>();
      for (int i = 0; i < keys.length; i++)
      {
         String nm = BCStyle.INSTANCE.oidToDisplayName(keys[i]).toUpperCase();
         mandatoryKeys.put(nm, descr[i]);
      }
      
      return mandatoryKeys;
   }

   /**
    * Generate a certificate and sign it with the private key for the specified issuer.
    * 
    * @param    validity
    *           The certificate validity, in years.
    * @param    subject
    *           X500-style subject name.
    * @param    subjectKeyPair
    *           The subject's asymmetric private-public key pair.
    * @param    issuer
    *           The issuer's Bouncy Castle-style certificate. If <code>null</code>, it will be
    *           self-signed.
    * @param    issuerPrivateKey
    *           The issuer's private key, to sign the new certificate.
    * @param    certificateAuthority
    *           Flag indicating if this certificate should be marked as a CA.
    * @param    usages
    *           Extended usages for this certificate.
    *
    * @return   The generated Bouncy Castle-style certificate.
    * 
    * @throws   SSLCertGenException
    *           In case of problems during the certificate generation.
    */
   private X509CertificateHolder generateCertificate(int                     validity,
                                                     X500Name                subject,
                                                     AsymmetricCipherKeyPair subjectKeyPair,
                                                     X509CertificateHolder   issuer,
                                                     AsymmetricKeyParameter  issuerPrivateKey,
                                                     boolean                 certificateAuthority,
                                                     KeyPurposeId[]          usages) 
   throws SSLCertGenException
   {
      Calendar cnotBefore = new GregorianCalendar();
      Calendar cnotAfter = new GregorianCalendar();
      cnotAfter.add(Calendar.YEAR, validity);
      
      Date notBefore = new Date(cnotBefore.getTimeInMillis());
      Date notAfter = new Date(cnotAfter.getTimeInMillis());
      
      X500Name issuerSubject = (issuer == null) ? subject : issuer.getIssuer();
     
      BigInteger subjectSerial = new BigInteger(srnd.nextInt(100), srnd);

      X509v3CertificateBuilder builder;
      ContentSigner signer;
      try
      {
         SubjectPublicKeyInfo publicInfo =
            SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(subjectKeyPair.getPublic());
         PrivateKeyInfo privateInfo =
            PrivateKeyInfoFactory.createPrivateKeyInfo(issuerPrivateKey);
         
         builder = new X509v3CertificateBuilder(issuerSubject, subjectSerial,
                                                notBefore, notAfter,
                                                subject, publicInfo);
         JcaContentSignerBuilder jcaSigner = new JcaContentSignerBuilder("SHA256WithRSA");
         jcaSigner.setProvider(Security.getProvider(BouncyCastleProvider.PROVIDER_NAME));
         jcaSigner.setSecureRandom(srnd);
         signer = jcaSigner.build(BouncyCastleProvider.getPrivateKey(privateInfo));

         AuthorityKeyIdentifier ext1 = 
            (issuer == null) ? extUtils.createAuthorityKeyIdentifier(publicInfo)
                             : extUtils.createAuthorityKeyIdentifier(issuer);
         SubjectKeyIdentifier ext2 = extUtils.createSubjectKeyIdentifier(publicInfo);
         BasicConstraints ext3 = new BasicConstraints(certificateAuthority);
         String cn = IETFUtils.valueToString(subject.getRDNs(BCStyle.CN)[0].getFirst().getValue());
         GeneralNames ext4 = new GeneralNames(new GeneralName(GeneralName.dNSName, cn));
         
         builder.addExtension(Extension.authorityKeyIdentifier, false, ext1);
         builder.addExtension(Extension.subjectKeyIdentifier, false, ext2);
         builder.addExtension(Extension.basicConstraints, true, ext3);
         builder.addExtension(Extension.subjectAlternativeName, false, ext4);
         if (usages != null && usages.length > 0)
         {
            builder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(usages));
         }

         return builder.build(signer);
      }
      catch (OperatorCreationException | IOException e)
      {
         throw new SSLCertGenException(e);
      }
   }
   
   /**
    * Generate an asymmetric key pair using the RSA algorithm.
    * 
    * @return   See above.
    */
   private AsymmetricCipherKeyPair generateKeyPair()
   {
      // certainty (last parameter) is passed to the BigInteger.isProbablePrime(certainty)
      // and represents the number of Miller-Rabin iterations. A large value works OK for small 
      // numbers, but with large numbers (>1024 bits) BigInteger.isProbablePrime defaults to a 
      // 2-round Miller-Rabin primality test.
      RSAKeyGenerationParameters params = new RSAKeyGenerationParameters(exponent, srnd, 
                                                                         keyStrength, 80);
      RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
      gen.init(params);

      return gen.generateKeyPair();
   }

   /**
    * Encrypt and save the private key in the specified store.
    * 
    * @param    store
    *           The key store where to save the private key.
    * @param    keyPair
    *           The asymmetric key pair, from which the private key will be extracted.
    * @param    alias
    *           The alias used to save the private key in the store.
    * @param    keyentryPassword
    *           The password to encrypt the private key.
    * @param    chain
    *           The certificate chain.
    * 
    * @throws   SSLCertGenException
    */
   private void saveKeyEntry(KeyStore                store,
                             AsymmetricCipherKeyPair keyPair,
                             String                  alias,
                             char[]                  keyentryPassword,
                             Certificate[]           chain)
   throws SSLCertGenException
   {
      try
      {
         PrivateKeyInfo privateInfo =
            PrivateKeyInfoFactory.createPrivateKeyInfo(keyPair.getPrivate());
   
         PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateInfo.getEncoded());
         KeyFactory kf = KeyFactory.getInstance("RSA");
         PrivateKey key = kf.generatePrivate(keySpec);
   
         store.setKeyEntry(alias, key, keyentryPassword, chain);
      }
      catch (IOException              |
             NoSuchAlgorithmException |
             InvalidKeySpecException  |
             KeyStoreException e)
      {
         throw new SSLCertGenException(e);
      }
   }

   /**
    * Using the passed field map and the Common Name, build the X500-style distinguished name.
    * 
    * @param    fieldMap
    *           A map with the subject fields.
    * @param    commonName
    *           The common name.
    *           
    * @return   X500-style subject.
    */
   private X500Name buildSubject(Map<String, String> fieldMap, String commonName)
   {
      X500NameBuilder builder = new X500NameBuilder();
      
      for (String key : fieldMap.keySet())
      {
         String val = fieldMap.get(key);
         builder.addRDN(BCStyle.INSTANCE.attrNameToOID(key), val);
      }
      
      builder.addRDN(BCStyle.CN, commonName);
      
      return builder.build();
   }
   
   /**
    * Generate a X509-style certificate from the Bouncy Castle-style X509-holder instance.
    * 
    * @param    cert
    *           The Bouncy Castle-style certificate.
    *           
    * @return   The X509 certificate.
    * 
    * @throws   SSLCertGenException
    *           If the certificate could not be converted.
    */
   private X509Certificate convertCertificate(X509CertificateHolder cert) 
   throws SSLCertGenException
   {
      JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
      converter.setProvider(BouncyCastleProvider.PROVIDER_NAME);
      
      try
      {
         return converter.getCertificate(cert);
      }
      catch (CertificateException e)
      {
         throw new SSLCertGenException(e);
      }
   }

   /**
    * Sets the trusted certificate chain.
    * 
    * @param    chain
    *           The root certificate chain up to the well-known authority
    */
   @Override
   public void setCertificateChain(Certificate[] chain)
   {
      certChain.clear();
      for (Certificate cert : chain)
      {
         certChain.add(cert);
      }
   }
}