001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2009-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017
018package org.opends.server.util;
019
020import java.security.KeyStoreException;
021import java.security.NoSuchAlgorithmException;
022import java.security.KeyPairGenerator;
023import java.security.KeyStore;
024import java.security.PrivateKey;
025import java.security.cert.Certificate;
026import java.security.cert.CertificateFactory;
027import java.security.cert.X509Certificate;
028
029import java.io.FileInputStream;
030import java.io.FileOutputStream;
031import java.io.InputStream;
032import java.lang.reflect.Constructor;
033import java.lang.reflect.Method;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.forgerock.util.Reject;
037
038import static org.opends.messages.UtilityMessages.*;
039import static org.opends.server.util.ServerConstants.CERTANDKEYGEN_PROVIDER;
040
041/**
042 * Provides a wrapper class that collects all of the JVM vendor and JDK version
043 * specific code in a single place.
044 */
045public final class Platform
046{
047
048  /** Prefix that determines which security package to use. */
049  private static final String pkgPrefix;
050
051  /** The two security package prefixes (IBM and SUN). */
052  private static final String IBM_SEC = "com.ibm.security";
053  private static final String SUN_SEC = "sun.security";
054
055  /** The CertAndKeyGen class is located in different packages depending on JVM environment. */
056  private static final String[] CERTANDKEYGEN_PATHS = new String[] {
057      "sun.security.x509.CertAndKeyGen",          // Oracle/Sun/OpenJDK 6,7
058      "sun.security.tools.keytool.CertAndKeyGen", // Oracle/Sun/OpenJDK 8
059      "com.ibm.security.x509.CertAndKeyGen",      // IBM SDK 7
060      "com.ibm.security.tools.CertAndKeyGen"      // IBM SDK 8
061    };
062
063  private static final PlatformIMPL IMPL;
064
065  /** The minimum java supported version. */
066  public static final String JAVA_MINIMUM_VERSION_NUMBER = "7.0";
067
068  static
069  {
070    String vendor = System.getProperty("java.vendor");
071
072    if (vendor.startsWith("IBM"))
073    {
074      pkgPrefix = IBM_SEC;
075    }
076    else
077    {
078      pkgPrefix = SUN_SEC;
079    }
080    IMPL = new DefaultPlatformIMPL();
081  }
082
083  /** Key size, key algorithm and signature algorithms used. */
084  public static enum KeyType
085  {
086    /** RSA key algorithm with 2048 bits size and SHA1withRSA signing algorithm. */
087    RSA("rsa", 2048, "SHA1WithRSA"),
088
089    /** Elliptic Curve key algorithm with 233 bits size and SHA1withECDSA signing algorithm. */
090    EC("ec", 256, "SHA1withECDSA");
091
092    /** Default key type used when none can be determined. */
093    public final static KeyType DEFAULT = RSA;
094
095    final String keyAlgorithm;
096    final int keySize;
097    final String signatureAlgorithm;
098
099    private KeyType(String keyAlgorithm, int keySize, String signatureAlgorithm)
100    {
101      this.keySize = keySize;
102      this.keyAlgorithm = keyAlgorithm;
103      this.signatureAlgorithm = signatureAlgorithm;
104    }
105
106    /**
107     * Check whether this key type is supported by the current JVM.
108     * @return true if this key type is supported, false otherwise.
109     */
110    public boolean isSupported()
111    {
112      try
113      {
114        return KeyPairGenerator.getInstance(keyAlgorithm.toUpperCase()) != null;
115      }
116      catch (NoSuchAlgorithmException e)
117      {
118        return false;
119      }
120    }
121
122    /**
123     * Get a KeyType based on the alias name.
124     *
125     * @param alias
126     *          certificate alias
127     * @return KeyTpe deduced from the alias.
128     */
129    public static KeyType getTypeOrDefault(String alias)
130    {
131      try
132      {
133        return KeyType.valueOf(alias.substring(alias.lastIndexOf('-') + 1).toUpperCase());
134      }
135      catch (Exception e)
136      {
137        return KeyType.DEFAULT;
138      }
139    }
140  }
141
142  /**
143   * Platform base class. Performs all of the certificate management functions.
144   */
145  private static abstract class PlatformIMPL
146  {
147    /** Time values used in validity calculations. */
148    private static final int SEC_IN_DAY = 24 * 60 * 60;
149
150    /** Methods pulled from the classes. */
151    private static final String GENERATE_METHOD = "generate";
152    private static final String GET_PRIVATE_KEY_METHOD = "getPrivateKey";
153    private static final String GET_SELFSIGNED_CERT_METHOD =
154      "getSelfCertificate";
155
156    /** Classes needed to manage certificates. */
157    private static final Class<?> certKeyGenClass, X500NameClass;
158
159    /** Constructors for each of the above classes. */
160    private static Constructor<?> certKeyGenCons, X500NameCons;
161
162    /** Filesystem APIs */
163
164    static
165    {
166
167      String certAndKeyGen = getCertAndKeyGenClassName();
168      if(certAndKeyGen == null)
169      {
170        LocalizableMessage msg = ERR_CERTMGR_CERTGEN_NOT_FOUND.get(CERTANDKEYGEN_PROVIDER);
171        throw new ExceptionInInitializerError(msg.toString());
172      }
173
174      String X500Name = pkgPrefix + ".x509.X500Name";
175      try
176      {
177        certKeyGenClass = Class.forName(certAndKeyGen);
178        X500NameClass = Class.forName(X500Name);
179        certKeyGenCons = certKeyGenClass.getConstructor(String.class,
180            String.class);
181        X500NameCons = X500NameClass.getConstructor(String.class);
182      }
183      catch (ClassNotFoundException e)
184      {
185        LocalizableMessage msg = ERR_CERTMGR_CLASS_NOT_FOUND.get(e.getMessage());
186        throw new ExceptionInInitializerError(msg.toString());
187      }
188      catch (SecurityException e)
189      {
190        LocalizableMessage msg = ERR_CERTMGR_SECURITY.get(e.getMessage());
191        throw new ExceptionInInitializerError(msg.toString());
192      }
193      catch (NoSuchMethodException e)
194      {
195        LocalizableMessage msg = ERR_CERTMGR_NO_METHOD.get(e.getMessage());
196        throw new ExceptionInInitializerError(msg.toString());
197      }
198    }
199
200    /**
201     * Try to decide which CertAndKeyGen class to use.
202     *
203     * @return a fully qualified class name or null
204     */
205    private static String getCertAndKeyGenClassName() {
206      String certAndKeyGen = System.getProperty(CERTANDKEYGEN_PROVIDER);
207      if (certAndKeyGen != null)
208      {
209        return certAndKeyGen;
210      }
211
212      for (String className : CERTANDKEYGEN_PATHS)
213      {
214        if (classExists(className))
215        {
216          return className;
217        }
218      }
219      return null;
220    }
221
222    /**
223     * A quick check to see if a class can be loaded. Doesn't check if
224     * it can be instantiated.
225     *
226     * @param className full class name to check
227     * @return true if the class is found
228     */
229    private static boolean classExists(final String className)
230    {
231      try {
232        Class.forName(className);
233        return true;
234      } catch (ClassNotFoundException | ClassCastException e) {
235        return false;
236      }
237    }
238
239    protected PlatformIMPL()
240    {
241    }
242
243
244
245    private final void deleteAlias(KeyStore ks, String ksPath, String alias,
246        char[] pwd) throws KeyStoreException
247    {
248      try
249      {
250        if (ks == null)
251        {
252          LocalizableMessage msg = ERR_CERTMGR_KEYSTORE_NONEXISTANT.get();
253          throw new KeyStoreException(msg.toString());
254        }
255        ks.deleteEntry(alias);
256        try (final FileOutputStream fs = new FileOutputStream(ksPath))
257        {
258          ks.store(fs, pwd);
259        }
260      }
261      catch (Exception e)
262      {
263        throw new KeyStoreException(ERR_CERTMGR_DELETE_ALIAS.get(alias, e.getMessage()).toString(), e);
264      }
265    }
266
267
268
269    private final void addCertificate(KeyStore ks, String ksType, String ksPath,
270        String alias, char[] pwd, String certPath) throws KeyStoreException
271    {
272      try
273      {
274        CertificateFactory cf = CertificateFactory.getInstance("X509");
275        if (ks == null)
276        {
277          ks = KeyStore.getInstance(ksType);
278          ks.load(null, pwd);
279        }
280        // Do not support certificate replies.
281        if (ks.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class))
282        {
283          LocalizableMessage msg = ERR_CERTMGR_CERT_REPLIES_INVALID.get(alias);
284          throw new KeyStoreException(msg.toString());
285        }
286        else if (!ks.containsAlias(alias)
287            || ks.entryInstanceOf(alias, KeyStore.TrustedCertificateEntry.class))
288        {
289          try (InputStream inStream = new FileInputStream(certPath)) {
290            trustedCert(alias, cf, ks, inStream);
291          }
292        }
293        else
294        {
295          LocalizableMessage msg = ERR_CERTMGR_ALIAS_INVALID.get(alias);
296          throw new KeyStoreException(msg.toString());
297        }
298        try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
299          ks.store(fileOutStream, pwd);
300        }
301      }
302      catch (Exception e)
303      {
304        throw new KeyStoreException(ERR_CERTMGR_ADD_CERT.get(alias, e.getMessage()).toString(), e);
305      }
306    }
307
308
309
310    private static final KeyStore generateSelfSignedCertificate(KeyStore ks,
311        String ksType, String ksPath, KeyType keyType, String alias, char[] pwd, String dn,
312        int validity) throws KeyStoreException
313    {
314      try
315      {
316        if (ks == null)
317        {
318          ks = KeyStore.getInstance(ksType);
319          ks.load(null, pwd);
320        }
321        else if (ks.containsAlias(alias))
322        {
323          LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias);
324          throw new KeyStoreException(msg.toString());
325        }
326
327        final Object keypair = newKeyPair(keyType);
328        final Object subject = newX500Name(dn);
329        generate(keypair, keyType.keySize);
330        final PrivateKey privateKey = getPrivateKey(keypair);
331        final Certificate[] certificateChain = new Certificate[] {
332          getSelfCertificate(keypair, subject, validity * SEC_IN_DAY)
333        };
334        ks.setKeyEntry(alias, privateKey, pwd, certificateChain);
335        try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
336          ks.store(fileOutStream, pwd);
337        }
338        return ks;
339      }
340      catch (Exception e)
341      {
342        throw new KeyStoreException(ERR_CERTMGR_GEN_SELF_SIGNED_CERT.get(alias, e.getMessage()).toString(), e);
343      }
344    }
345
346    private static Object newKeyPair(KeyType keyType) throws Exception
347    {
348      return certKeyGenCons.newInstance(keyType.keyAlgorithm, keyType.signatureAlgorithm);
349    }
350
351    private static Object newX500Name(String dn) throws Exception
352    {
353      return X500NameCons.newInstance(dn);
354    }
355
356    private static void generate(Object keypair, int keySize) throws Exception
357    {
358      Method certAndKeyGenGenerate = certKeyGenClass.getMethod(GENERATE_METHOD, int.class);
359      certAndKeyGenGenerate.invoke(keypair, keySize);
360    }
361
362    private static PrivateKey getPrivateKey(Object keypair) throws Exception
363    {
364      Method certAndKeyGetPrivateKey = certKeyGenClass.getMethod(GET_PRIVATE_KEY_METHOD);
365      return (PrivateKey) certAndKeyGetPrivateKey.invoke(keypair);
366    }
367
368    private static Certificate getSelfCertificate(Object keypair, Object subject, int days) throws Exception
369    {
370      Method getSelfCertificate = certKeyGenClass.getMethod(GET_SELFSIGNED_CERT_METHOD, X500NameClass, long.class);
371      return (Certificate) getSelfCertificate.invoke(keypair, subject, days);
372    }
373
374    /**
375     * Generate a x509 certificate from the input stream. Verification is done
376     * only if it is self-signed.
377     */
378    private void trustedCert(String alias, CertificateFactory cf, KeyStore ks,
379        InputStream in) throws KeyStoreException
380    {
381      try
382      {
383        if (ks.containsAlias(alias))
384        {
385          LocalizableMessage msg = ERR_CERTMGR_ALIAS_ALREADY_EXISTS.get(alias);
386          throw new KeyStoreException(msg.toString());
387        }
388        X509Certificate cert = (X509Certificate) cf.generateCertificate(in);
389        if (isSelfSigned(cert))
390        {
391          cert.verify(cert.getPublicKey());
392        }
393        ks.setCertificateEntry(alias, cert);
394      }
395      catch (Exception e)
396      {
397        throw new KeyStoreException(ERR_CERTMGR_TRUSTED_CERT.get(alias, e.getMessage()).toString(), e);
398      }
399    }
400
401
402
403    /**
404     * Check that the issuer and subject DNs match.
405     */
406    private boolean isSelfSigned(X509Certificate cert)
407    {
408      return cert.getSubjectDN().equals(cert.getIssuerDN());
409    }
410  }
411
412
413
414  /** Prevent instantiation. */
415  private Platform()
416  {
417  }
418
419
420
421  /**
422   * Add the certificate in the specified path to the provided keystore;
423   * creating the keystore with the provided type and path if it doesn't exist.
424   *
425   * @param ks
426   *          The keystore to add the certificate to, may be null if it doesn't
427   *          exist.
428   * @param ksType
429   *          The type to use if the keystore is created.
430   * @param ksPath
431   *          The path to the keystore if it is created.
432   * @param alias
433   *          The alias to store the certificate under.
434   * @param pwd
435   *          The password to use in saving the certificate.
436   * @param certPath
437   *          The path to the file containing the certificate.
438   * @throws KeyStoreException
439   *           If an error occurred adding the certificate to the keystore.
440   */
441  public static void addCertificate(KeyStore ks, String ksType, String ksPath,
442      String alias, char[] pwd, String certPath) throws KeyStoreException
443  {
444    IMPL.addCertificate(ks, ksType, ksPath, alias, pwd, certPath);
445  }
446
447
448
449  /**
450   * Delete the specified alias from the provided keystore.
451   *
452   * @param ks
453   *          The keystore to delete the alias from.
454   * @param ksPath
455   *          The path to the keystore.
456   * @param alias
457   *          The alias to use in the request generation.
458   * @param pwd
459   *          The keystore password to use.
460   * @throws KeyStoreException
461   *           If an error occurred deleting the alias.
462   */
463  public static void deleteAlias(KeyStore ks, String ksPath, String alias,
464      char[] pwd) throws KeyStoreException
465  {
466    IMPL.deleteAlias(ks, ksPath, alias, pwd);
467  }
468
469
470
471  /**
472   * Generate a self-signed certificate using the specified alias, dn string and
473   * validity period. If the keystore does not exist, it will be created using
474   * the specified keystore type and path.
475   *
476   * @param ks
477   *          The keystore to save the certificate in. May be null if it does
478   *          not exist.
479   * @param keyType
480   *          The keystore type to use if the keystore is created.
481   * @param ksPath
482   *          The path to the keystore if the keystore is created.
483   * @param ksType
484   *          Specify the key size, key algorithm and signature algorithms used.
485   * @param alias
486   *          The alias to store the certificate under.
487   * @param pwd
488   *          The password to us in saving the certificate.
489   * @param dn
490   *          The dn string used as the certificate subject.
491   * @param validity
492   *          The validity of the certificate in days.
493   * @throws KeyStoreException
494   *           If the self-signed certificate cannot be generated.
495   */
496  public static void generateSelfSignedCertificate(KeyStore ks, String ksType,
497      String ksPath, KeyType keyType, String alias, char[] pwd, String dn, int validity)
498      throws KeyStoreException
499  {
500    PlatformIMPL.generateSelfSignedCertificate(ks, ksType, ksPath, keyType, alias, pwd, dn, validity);
501  }
502
503  /**
504   * Default platform class.
505   */
506  private static class DefaultPlatformIMPL extends PlatformIMPL
507  {
508  }
509
510
511
512  /**
513   * Test if a platform java vendor property starts with the specified vendor
514   * string.
515   *
516   * @param vendor
517   *          The vendor to check for.
518   * @return {@code true} if the java vendor starts with the specified vendor
519   *         string.
520   */
521  public static boolean isVendor(String vendor)
522  {
523    String javaVendor = System.getProperty("java.vendor");
524    return javaVendor.startsWith(vendor);
525  }
526
527  /**
528   * Computes the number of replay/worker/cleaner threads based on the number of cpus in the system.
529   * Allows for a multiplier to be specified and a minimum value to be returned if not enough processors
530   * are present in the system.
531   *
532   * @param minimumValue at least this value should be returned.
533   * @param cpuMultiplier the scaling multiplier of the number of threads to return
534   * @return the number of threads based on the number of cpus in the system.
535   * @throws IllegalArgumentException if {@code cpuMultiplier} is a non positive number
536   */
537  public static int computeNumberOfThreads(int minimumValue, float cpuMultiplier)
538  {
539    Reject.ifTrue(cpuMultiplier < 0, "Multiplier must be a positive number");
540    return Math.max(minimumValue, (int)(Runtime.getRuntime().availableProcessors() * cpuMultiplier));
541  }
542}