All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.security.certificate.CertificateHelper Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.security.certificate;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.base64.Base64;
import com.helger.commons.collection.ArrayHelper;
import com.helger.commons.io.stream.NonBlockingByteArrayInputStream;
import com.helger.commons.io.stream.StringInputStream;
import com.helger.commons.string.StringHelper;

/**
 * Some utility methods handling X.509 certificates.
 *
 * @author Philip Helger
 */
@Immutable
public final class CertificateHelper
{
  public static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
  public static final String END_CERTIFICATE = "-----END CERTIFICATE-----";

  @Deprecated (forRemoval = true, since = "11.1.1")
  public static final String BEGIN_CERTIFICATE_INVALID = "-----BEGINCERTIFICATE-----";
  @Deprecated (forRemoval = true, since = "11.1.1")
  public static final String END_CERTIFICATE_INVALID = "-----ENDCERTIFICATE-----";

  public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
  public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";

  public static final String CRLF = "\r\n";

  /** Character set used for String-Certificate conversion */
  public static final Charset CERT_CHARSET = StandardCharsets.ISO_8859_1;

  private static final Logger LOGGER = LoggerFactory.getLogger (CertificateHelper.class);

  @PresentForCodeCoverage
  private static final CertificateHelper INSTANCE = new CertificateHelper ();

  private CertificateHelper ()
  {}

  @Nonnull
  public static CertificateFactory getX509CertificateFactory () throws CertificateException
  {
    return CertificateFactory.getInstance ("X.509");
  }

  /**
   * Make sure, the provided String is surrounded by the PEM headers
   * {@link #BEGIN_CERTIFICATE} and {@link #END_CERTIFICATE}
   *
   * @param sCertString
   *        Certificate string to use.
   * @return The String with the surrounding headers and footers
   * @deprecated Use {@link #getCertificateWithPEMHeader(String)} instead
   */
  @Deprecated (forRemoval = true, since = "11.1.1")
  @Nonnull
  public static String getWithPEMHeader (@Nonnull final String sCertString)
  {
    return getCertificateWithPEMHeader (sCertString);
  }

  /**
   * Make sure, the provided String is surrounded by the PEM headers
   * {@link #BEGIN_CERTIFICATE} and {@link #END_CERTIFICATE}
   *
   * @param sCertString
   *        Certificate string to use.
   * @return The String with the surrounding headers and footers
   * @since 11.1.1
   */
  @Nonnull
  public static String getCertificateWithPEMHeader (@Nonnull final String sCertString)
  {
    String sRealCertString = sCertString;
    // Check without newline in case there are blanks between the string the
    // certificate
    if (!sRealCertString.startsWith (BEGIN_CERTIFICATE))
      sRealCertString = BEGIN_CERTIFICATE + "\n" + sRealCertString;
    if (!sRealCertString.trim ().endsWith (END_CERTIFICATE))
      sRealCertString += "\n" + END_CERTIFICATE;
    return sRealCertString;
  }

  /**
   * Remove any eventually preceding {@value #BEGIN_CERTIFICATE} and succeeding
   * {@value #END_CERTIFICATE} values from the passed certificate string.
   * Additionally all whitespaces of the string are removed.
   *
   * @param sCertificate
   *        The source certificate string. May be null.
   * @return null if the input string is null or
   *         empty, the stripped down string otherwise.
   */
  @Nullable
  public static String getWithoutPEMHeader (@Nullable final String sCertificate)
  {
    if (StringHelper.hasNoText (sCertificate))
      return null;

    // Remove special begin and end stuff
    String sRealCertificate = sCertificate.trim ();

    /**
     * Handle certain misconfiguration issues. E.g. for 9906:testconsip on
     *
     * 
     * http://b-c073e04afb234f70e74d3444ba3f8eaa.iso6523-actorid-upis.acc.edelivery.tech.ec.europa.eu/iso6523-actorid-upis%3A%3A9906%3Atestconsip/services/busdox-docid-qns%3A%3Aurn%3Aoasis%3Anames%3Aspecification%3Aubl%3Aschema%3Axsd%3AOrder-2%3A%3AOrder%23%23urn%3Awww.cenbii.eu%3Atransaction%3Abiitrns001%3Aver2.0%3Aextended%3Aurn%3Awww.peppol.eu%3Abis%3Apeppol3a%3Aver2.0%3A%3A2.1
     * 
*/ sRealCertificate = StringHelper.trimStart (sRealCertificate, BEGIN_CERTIFICATE_INVALID); sRealCertificate = StringHelper.trimEnd (sRealCertificate, END_CERTIFICATE_INVALID); // Remove regular PEM headers also sRealCertificate = StringHelper.trimStart (sRealCertificate, BEGIN_CERTIFICATE); sRealCertificate = StringHelper.trimEnd (sRealCertificate, END_CERTIFICATE); // Remove all existing whitespace characters return StringHelper.getWithoutAnySpaces (sRealCertificate); } /** * The certificate string needs to be emitted in portions of 64 characters. If * characters are left, than <CR><LF> ("\r\n") must be added to * the string so that the next characters start on a new line. After the last * part, no <CR><LF> is needed. Respective RFC parts are 1421 * 4.3.2.2 and 4.3.2.4 * * @param sCertificate * Original certificate string as stored in the DB * @param bIncludePEMHeader * true to include {@link #BEGIN_CERTIFICATE} header and * {@link #END_CERTIFICATE} footer. * @return The RFC 1421 compliant string. May be null if the * original string is null or empty. */ @Nullable public static String getRFC1421CompliantString (@Nullable final String sCertificate, final boolean bIncludePEMHeader) { return getRFC1421CompliantString (sCertificate, bIncludePEMHeader, CRLF); } /** * The certificate string needs to be emitted in portions of 64 characters. If * characters are left, than a line separator (e.g. <CR><LF> - * "\r\n") must be added to the string so that the next characters start on a * new line. After the last part, no line separator is needed. Respective RFC * parts are 1421 4.3.2.2 and 4.3.2.4 * * @param sCertificate * Original certificate string as stored in the DB * @param bIncludePEMHeader * true to include {@link #BEGIN_CERTIFICATE} header and * {@link #END_CERTIFICATE} footer. * @param sLineSeparator * The line separator to be used. May not be null. Usually * this is "\r\n" but may also be just "\n". * @return The RFC 1421 compliant string. May be null if the * original string is null or empty. * @since 8.5.5 */ @Nullable public static String getRFC1421CompliantString (@Nullable final String sCertificate, final boolean bIncludePEMHeader, @Nonnull final String sLineSeparator) { ValueEnforcer.notNull (sLineSeparator, "LineSeparator"); // Remove special begin and end stuff String sPlainString = getWithoutPEMHeader (sCertificate); if (StringHelper.hasNoText (sPlainString)) return null; // Start building the result final int nMaxLineLength = 64; // Start with the prefix final StringBuilder aSB = new StringBuilder (); if (bIncludePEMHeader) aSB.append (BEGIN_CERTIFICATE).append ('\n'); while (sPlainString.length () > nMaxLineLength) { // Append line + line separator aSB.append (sPlainString, 0, nMaxLineLength).append (sLineSeparator); // Remove the start of the string sPlainString = sPlainString.substring (nMaxLineLength); } // Append the rest aSB.append (sPlainString); // Add trailer if (bIncludePEMHeader) aSB.append ('\n').append (END_CERTIFICATE); return aSB.toString (); } /** * Convert the passed byte array to an X.509 certificate object. * * @param aCertBytes * The original certificate bytes. May be null or empty. * @return null if the passed byte array is null or * empty * @throws CertificateException * In case the passed string cannot be converted to an X.509 * certificate. */ @Nullable public static X509Certificate convertByteArrayToCertficate (@Nullable final byte [] aCertBytes) throws CertificateException { if (ArrayHelper.isEmpty (aCertBytes)) return null; // Certificate is always ISO-8859-1 encoded return convertStringToCertficate (new String (aCertBytes, CERT_CHARSET), false); } /** * Convert the passed byte array to an X.509 certificate object. * * @param aCertBytes * The original certificate bytes. May be null or empty. * @return null if the passed byte array is null, * empty or not a valid certificate. * @since 9.4.0 */ @Nullable public static X509Certificate convertByteArrayToCertficateOrNull (@Nullable final byte [] aCertBytes) { try { return convertByteArrayToCertficate (aCertBytes); } catch (final CertificateException ex) { return null; } } /** * Convert the passed String to an X.509 certificate without converting it to * a String first. * * @param aCertBytes * The certificate bytes. May be null. * @return null if the passed array is null or empty * @throws CertificateException * In case the passed bytes[] cannot be converted to an X.509 * certificate. * @since 9.3.4 */ @Nullable public static X509Certificate convertByteArrayToCertficateDirect (@Nullable final byte [] aCertBytes) throws CertificateException { if (ArrayHelper.isEmpty (aCertBytes)) { // No string -> no certificate return null; } final CertificateFactory aCertificateFactory = getX509CertificateFactory (); try (final NonBlockingByteArrayInputStream aBAIS = new NonBlockingByteArrayInputStream (aCertBytes)) { return (X509Certificate) aCertificateFactory.generateCertificate (aBAIS); } } @Nonnull private static X509Certificate _str2cert (@Nonnull final String sCertString, @Nonnull final CertificateFactory aCertificateFactory) throws CertificateException { final String sRealCertString = getRFC1421CompliantString (sCertString, true); try (final StringInputStream aIS = new StringInputStream (sRealCertString, CERT_CHARSET)) { return (X509Certificate) aCertificateFactory.generateCertificate (aIS); } } /** * Convert the passed String to an X.509 certificate. * * @param sCertString * The original text string. May be null or empty. The * String must be ISO-8859-1 encoded for the binary certificate to be * read! * @return null if the passed string is null or * empty * @throws CertificateException * In case the passed string cannot be converted to an X.509 * certificate. * @throws IllegalArgumentException * If the input string is e.g. invalid Base64 encoded. * @since 2.1.1 */ @Nullable public static X509Certificate convertStringToCertficate (@Nullable final String sCertString) throws CertificateException { return convertStringToCertficate (sCertString, false); } /** * Convert the passed String to an X.509 certificate. * * @param sCertString * The original text string. May be null or empty. The * String must be ISO-8859-1 encoded for the binary certificate to be * read! * @param bWithFallback * true to enable legacy fallback parsing * @return null if the passed string is null or * empty * @throws CertificateException * In case the passed string cannot be converted to an X.509 * certificate. * @throws IllegalArgumentException * If the input string is e.g. invalid Base64 encoded. * @since 2.1.1 */ @Nullable public static X509Certificate convertStringToCertficate (@Nullable final String sCertString, final boolean bWithFallback) throws CertificateException { if (StringHelper.hasNoText (sCertString)) { // No string -> no certificate return null; } final CertificateFactory aCertificateFactory = getX509CertificateFactory (); // Convert certificate string to an object try { return _str2cert (sCertString, aCertificateFactory); } catch (final IllegalArgumentException | CertificateException ex) { // In some weird configurations, the result string is a hex encoded // certificate instead of the string // -> Try to work around it if (LOGGER.isDebugEnabled ()) LOGGER.debug ("Failed to decode provided X.509 certificate string: " + sCertString); if (!bWithFallback) throw ex; String sHexDecodedString; try { sHexDecodedString = new String (StringHelper.getHexDecoded (sCertString), CERT_CHARSET); } catch (final IllegalArgumentException ex2) { // Can happen, when the source string has an odd length (like 3 or 117). // In this case the original exception is re-thrown throw ex; } return _str2cert (sHexDecodedString, aCertificateFactory); } } /** * Convert the passed String to an X.509 certificate, swallowing all errors. * * @param sCertString * The certificate string to be parsed. * @return null in case the certificate cannot be converted. * @see #convertStringToCertficate(String) * @since 9.3.4 */ @Nullable public static X509Certificate convertStringToCertficateOrNull (@Nullable final String sCertString) { try { return convertStringToCertficate (sCertString, false); } catch (final CertificateException | IllegalArgumentException ex) { return null; } } /** * Convert the passed X.509 certificate string to a byte array. * * @param sCertificate * The original certificate string. May be null or empty. * @return null if the passed string is null or * empty or an invalid Base64 string */ @Nullable public static byte [] convertCertificateStringToByteArray (@Nullable final String sCertificate) { // Remove prefix/suffix final String sPlainCert = getWithoutPEMHeader (sCertificate); if (StringHelper.hasNoText (sPlainCert)) return null; // The remaining string is supposed to be Base64 encoded -> decode return Base64.safeDecode (sPlainCert); } /** * Get the provided certificate as a byte array. * * @param aCert * The certificate to encode. May not be null. * @return The byte array * @throws IllegalArgumentException * If the certificate could not be encoded. Cause is a * {@link CertificateEncodingException}. * @since 10.0.0 */ @Nonnull @Nonempty public static byte [] getEncodedCertificate (@Nonnull final Certificate aCert) { ValueEnforcer.notNull (aCert, "Cert"); try { return aCert.getEncoded (); } catch (final CertificateEncodingException ex) { throw new IllegalArgumentException ("Failed to encode certificate " + aCert, ex); } } /** * Get the provided certificate as PEM (Base64) encoded String. * * @param aCert * The certificate to encode. May not be null. * @return The PEM string with {@link #BEGIN_CERTIFICATE} and * {@link #END_CERTIFICATE}. * @throws IllegalArgumentException * If the certificate could not be encoded. Cause is a * {@link CertificateEncodingException}. * @since 8.5.5 */ @Nonnull @Nonempty public static String getPEMEncodedCertificate (@Nonnull final Certificate aCert) { ValueEnforcer.notNull (aCert, "Cert"); try { final String sEncodedCert = Base64.encodeBytes (aCert.getEncoded ()); return BEGIN_CERTIFICATE + "\n" + sEncodedCert + "\n" + END_CERTIFICATE; } catch (final CertificateEncodingException ex) { throw new IllegalArgumentException ("Failed to encode certificate " + aCert, ex); } } /** * Check if the "not valid before"/"not valid after" of the provided X509 * certificate is valid per "now". * * @param aCert * The certificate to check. May not be null. * @return true if it is valid, false if not. * @since 9.3.8 */ public static boolean isCertificateValidPerNow (@Nonnull final X509Certificate aCert) { ValueEnforcer.notNull (aCert, "Cert"); try { aCert.checkValidity (); return true; } catch (final CertificateExpiredException | CertificateNotYetValidException ex) { return false; } } @Nullable public static PrivateKey convertStringToPrivateKey (@Nullable final String sPrivateKey) throws GeneralSecurityException { if (StringHelper.hasNoText (sPrivateKey)) return null; String sRealPrivateKey = StringHelper.trimStart (sPrivateKey, BEGIN_PRIVATE_KEY); sRealPrivateKey = StringHelper.trimEnd (sRealPrivateKey, END_PRIVATE_KEY); sRealPrivateKey = StringHelper.getWithoutAnySpaces (sRealPrivateKey); final byte [] aPrivateKeyBytes = Base64.safeDecode (sRealPrivateKey); if (aPrivateKeyBytes == null) return null; final KeyFactory aKeyFactory = KeyFactory.getInstance ("RSA"); final PKCS8EncodedKeySpec aKeySpec = new PKCS8EncodedKeySpec (aPrivateKeyBytes); return aKeyFactory.generatePrivate (aKeySpec); } /** * Check if the provided certificate is a CA (Certificate Authority) or not. * * @param aCert * The certificate to check. May not be null. * @return true if it is a CA, false if not. */ public static boolean isCA (@Nonnull final X509Certificate aCert) { ValueEnforcer.notNull (aCert, "Cert"); final byte [] aBCBytes = aCert.getExtensionValue (Extension.basicConstraints.getId ()); if (aBCBytes != null) { try { final ASN1Encodable aBCDecoded = JcaX509ExtensionUtils.parseExtensionValue (aBCBytes); if (aBCDecoded instanceof ASN1Sequence) { final ASN1Sequence aBCSequence = (ASN1Sequence) aBCDecoded; final BasicConstraints aBasicConstraints = BasicConstraints.getInstance (aBCSequence); if (aBasicConstraints != null) return aBasicConstraints.isCA (); } } catch (final IOException e) { // Fall through } } // Defaults to "no" return false; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy