code: /*
   * Copyright (c) 2019 the Eclipse Milo Authors
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  
  package org.eclipse.milo.opcua.stack.core.util;
  
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.StringWriter;
  import java.security.KeyPair;
  import java.security.cert.Certificate;
  import java.security.cert.CertificateEncodingException;
  import java.security.cert.CertificateException;
  import java.security.cert.CertificateFactory;
  import java.security.cert.CertificateParsingException;
  import java.security.cert.X509Certificate;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.stream.Collectors;
  
  import com.google.common.base.Preconditions;
  import com.google.common.primitives.Bytes;
  import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
  import org.bouncycastle.asn1.x500.X500Name;
  import org.bouncycastle.asn1.x500.style.IETFUtils;
  import org.bouncycastle.asn1.x500.style.RFC4519Style;
  import org.bouncycastle.asn1.x509.Extension;
  import org.bouncycastle.asn1.x509.ExtensionsGenerator;
  import org.bouncycastle.asn1.x509.GeneralName;
  import org.bouncycastle.asn1.x509.GeneralNames;
  import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
  import org.bouncycastle.openssl.MiscPEMGenerator;
  import org.bouncycastle.operator.ContentSigner;
  import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
  import org.bouncycastle.pkcs.PKCS10CertificationRequest;
  import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
  import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
  import org.bouncycastle.util.io.pem.PemWriter;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaException;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
  
  import static org.eclipse.milo.opcua.stack.core.util.DigestUtil.sha1;
  
  public class CertificateUtil {
  
      public static final int SUBJECT_ALT_NAME_DNS_NAME = GeneralName.dNSName;
      public static final int SUBJECT_ALT_NAME_IP_ADDRESS = GeneralName.iPAddress;
      public static final int SUBJECT_ALT_NAME_URI = GeneralName.uniformResourceIdentifier;
  
      /**
       * Decode a DER-encoded X.509 certificate.
       *
       * @param certificateBytes DER-encoded certificate bytes.
       * @return an {@link X509Certificate}
       * @throws UaException if decoding the certificate fails.
       */
      public static X509Certificate decodeCertificate(byte[] certificateBytes) throws UaException {
          Preconditions.checkNotNull(certificateBytes, "certificateBytes cannot be null");
  
          return decodeCertificate(new ByteArrayInputStream(certificateBytes));
      }
  
      /**
       * Decode a DER-encoded X.509 certificate.
       *
       * @param inputStream {@link InputStream} containing DER-encoded certificate bytes.
       * @return an {@link X509Certificate}
       * @throws UaException if decoding the certificate fails.
       */
      public static X509Certificate decodeCertificate(InputStream inputStream) throws UaException {
          return decodeCertificates(inputStream).get(0);
      }
  
      /**
       * Decode either a sequence of DER-encoded X.509 certificates or a PKCS#7 certificate chain.
       *
       * @param certificateBytes the byte[] to decode from.
       * @return a {@link List} of certificates deocded from {@code certificateBytes}.
       * @throws UaException if decoding fails.
       */
      public static List<X509Certificate> decodeCertificates(byte[] certificateBytes) throws UaException {
          return decodeCertificates(new ByteArrayInputStream(certificateBytes));
      }
  
      /**
       * Decode either a sequence of DER-encoded X.509 certificates or a PKCS#7 certificate chain.
       *
       * @param inputStream the {@link InputStream} to decode from.
       * @return a {@link List} of certificates decoded from {@code inputStream}.
       * @throws UaException if decoding fails.
       */
      public static List<X509Certificate> decodeCertificates(InputStream inputStream) throws UaException {
          Preconditions.checkNotNull(inputStream, "inputStream cannot be null");
  
          CertificateFactory factory;
  
          try {
              factory = CertificateFactory.getInstance("X.509");
          } catch (CertificateException e) {
              throw new UaException(StatusCodes.Bad_InternalError, e);
          }
  
          try {
              Collection<? extends Certificate> certificates =
                  factory.generateCertificates(inputStream);
  
              return certificates.stream()
                  .map(X509Certificate.class::cast)
                  .collect(Collectors.toList());
          } catch (CertificateException e) {
              throw new UaException(StatusCodes.Bad_CertificateInvalid, e);
          }
      }
  
      /**
       * Generate a {@link PKCS10CertificationRequest} for the provided {@code certificate} and {@code keyPair}.
       *
       * @param keyPair     the {@link KeyPair} for {@code certificate}.
       * @param certificate the {@link X509Certificate} to request signing for.
       * @return a {@link PKCS10CertificationRequest}.
       * @throws Exception if creating the signing request fails for any reason.
       */
      public static PKCS10CertificationRequest generateCsr(
          KeyPair keyPair,
          X509Certificate certificate
      ) throws Exception {
  
          PKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder(
              certificate.getSubjectX500Principal(),
              certificate.getPublicKey()
          );
  
          GeneralNames subjectAltNames = new GeneralNames(
              getSubjectAltNames(certificate)
                  .toArray(new GeneralName[0])
          );
  
          ExtensionsGenerator extGen = new ExtensionsGenerator();
          extGen.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);
          builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate());
  
          JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(
              certificate.getSigAlgName()
          );
  
          ContentSigner signer = signerBuilder.build(keyPair.getPrivate());
  
          return builder.build(signer);
      }
  
      /**
       * Generate a {@link PKCS10CertificationRequest}.
       *
       * @param keyPair            the {@link KeyPair} containing Public and Private keys.
       * @param subjectName        the subject name, in RFC 4519 style. (CN=foo,O=bar)
       * @param sanUri             the URI to request in the SAN.
       * @param sanDnsNames        the DNS names to request in the SAN.
       * @param sanIpAddresses     the IP addresses to request in the SAN.
       * @param signatureAlgorithm the signature algorithm to use when generating the signature to validate the
       *                           certificate.
       * @return a {@link PKCS10CertificationRequest}.
       * @throws Exception if creating the signing request fails for any reason.
       */
      public static PKCS10CertificationRequest generateCsr(
          KeyPair keyPair,
          String subjectName,
          String sanUri,
          List<String> sanDnsNames,
          List<String> sanIpAddresses,
          String signatureAlgorithm
      ) throws Exception {
  
          X500Name subject = new X500Name(
              IETFUtils.rDNsFromString(
                  subjectName,
                  RFC4519Style.INSTANCE
              )
          );
  
          return generateCsr(keyPair, subject, sanUri, sanDnsNames, sanIpAddresses, signatureAlgorithm);
      }
  
      /**
       * Generate a {@link PKCS10CertificationRequest}.
       *
       * @param keyPair            the {@link KeyPair} containing Public and Private keys.
       * @param subject            the subject name {@link X500Name}.
       * @param sanUri             the URI to request in the SAN.
       * @param sanDnsNames        the DNS names to request in the SAN.
       * @param sanIpAddresses     the IP addresses to request in the SAN.
       * @param signatureAlgorithm the signature algorithm to use when generating the signature to validate the
       *                           certificate.
       * @return a {@link PKCS10CertificationRequest}.
       * @throws Exception if creating the signing request fails for any reason.
       */
      public static PKCS10CertificationRequest generateCsr(
          KeyPair keyPair,
          X500Name subject,
          String sanUri,
          List<String> sanDnsNames,
          List<String> sanIpAddresses,
          String signatureAlgorithm
      ) throws Exception {
  
          PKCS10CertificationRequestBuilder builder = new PKCS10CertificationRequestBuilder(
              subject,
              SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())
          );
  
          List<GeneralName> generalNames = new ArrayList<>();
  
          generalNames.add(new GeneralName(SUBJECT_ALT_NAME_URI, sanUri));
  
          sanDnsNames.stream()
              .map(n -> new GeneralName(SUBJECT_ALT_NAME_DNS_NAME, n))
              .forEach(generalNames::add);
  
          sanIpAddresses.stream()
              .map(n -> new GeneralName(SUBJECT_ALT_NAME_IP_ADDRESS, n))
              .forEach(generalNames::add);
  
          ExtensionsGenerator extGen = new ExtensionsGenerator();
  
          extGen.addExtension(
              Extension.subjectAlternativeName,
              false,
              new GeneralNames(generalNames.toArray(new GeneralName[0]))
          );
  
          builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate());
  
          JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
  
          ContentSigner signer = signerBuilder.build(keyPair.getPrivate());
  
          return builder.build(signer);
      }
  
      /**
       * Get a PEM-encoded String containing a PKCS #10 certificate signing request.
       *
       * @param csr the {@link X509Certificate} to request signing for.
       * @return a PEM-encoded String containing a PKCS #10 CSR.
       * @throws IOException if creating the signing request fails for any reason.
       */
      public static String getCsrPem(PKCS10CertificationRequest csr) throws IOException {
          StringWriter stringWriter = new StringWriter();
  
          try (PemWriter pemWriter = new PemWriter(stringWriter)) {
              pemWriter.writeObject(new MiscPEMGenerator(csr));
              pemWriter.flush();
          }
  
          return stringWriter.toString();
      }
  
      private static List<GeneralName> getSubjectAltNames(X509Certificate certificate) {
          try {
              List<GeneralName> generalNames = new ArrayList<>();
  
              Collection<List<?>> subjectAltNames = certificate.getSubjectAlternativeNames();
              if (subjectAltNames == null) subjectAltNames = Collections.emptyList();
  
              for (List<?> idAndValue : subjectAltNames) {
                  if (idAndValue != null && idAndValue.size() == 2) {
                      Object id = idAndValue.get(0);
                      String value = Objects.toString(idAndValue.get(1));
  
                      if (Objects.equals(id, SUBJECT_ALT_NAME_DNS_NAME)) {
                          generalNames.add(new GeneralName(SUBJECT_ALT_NAME_DNS_NAME, value));
                      } else if (Objects.equals(id, SUBJECT_ALT_NAME_IP_ADDRESS)) {
                          generalNames.add(new GeneralName(SUBJECT_ALT_NAME_IP_ADDRESS, value));
                      } else if (Objects.equals(id, SUBJECT_ALT_NAME_URI)) {
                          generalNames.add(new GeneralName(SUBJECT_ALT_NAME_URI, value));
                      }
                  }
              }
  
              return generalNames;
          } catch (CertificateParsingException e) {
              return Collections.emptyList();
          }
      }
  
      /**
       * Extract the value of a given SubjectAltName field from a {@link X509Certificate}.
       *
       * @param certificate the certificate.
       * @param field       the field number.
       * @return an {@link Optional} containing the value in the field.
       * @see #SUBJECT_ALT_NAME_IP_ADDRESS
       * @see #SUBJECT_ALT_NAME_DNS_NAME
       * @see #SUBJECT_ALT_NAME_URI
       */
      public static List<Object> getSubjectAltNameField(X509Certificate certificate, int field) {
          try {
              List<Object> values = new ArrayList<>();
  
              Collection<List<?>> subjectAltNames = certificate.getSubjectAlternativeNames();
              if (subjectAltNames == null) subjectAltNames = Collections.emptyList();
  
              for (List<?> idAndValue : subjectAltNames) {
                  if (idAndValue != null && idAndValue.size() == 2) {
                      if (idAndValue.get(0).equals(field)) {
                          Object value = idAndValue.get(1);
                          if (value != null) values.add(value);
                      }
                  }
              }
  
              return values;
          } catch (CertificateParsingException e) {
              return Collections.emptyList();
          }
      }
  
      /**
       * Get the URI from the {@code certificate}'s Subject Alternative Name extension, if it's present.
       *
       * @param certificate the certificate to get the URI from.
       * @return the value of the SAN URI, if present.
       */
      public static Optional<String> getSanUri(X509Certificate certificate) {
          List<Object> values = getSubjectAltNameField(certificate, SUBJECT_ALT_NAME_URI);
  
          return values.stream()
              .filter(v -> v instanceof String)
              .map(String.class::cast)
              .findFirst();
      }
  
      /**
       * Get the DNS names from the {@code certificate}'s Subject Alternative Name extension, if it's present.
       *
       * @param certificate the certificate to get the DNS names from.
       * @return the values of the SAN DNS names, or empty list if none are present.
       */
      public static List<String> getSanDnsNames(X509Certificate certificate) {
          List<Object> values = getSubjectAltNameField(certificate, SUBJECT_ALT_NAME_DNS_NAME);
  
          return values.stream()
              .filter(v -> v instanceof String)
              .map(String.class::cast)
              .collect(Collectors.toList());
      }
  
      /**
       * Get the IP addresses from the {@code certificate}'s Subject Alternative Name extension, if it's present.
       *
       * @param certificate the certificate to get the IP addresses from.
       * @return the values of the SAN IP addresses, or empty list if none are present.
       */
      public static List<String> getSanIpAddresses(X509Certificate certificate) {
          List<Object> values = getSubjectAltNameField(certificate, SUBJECT_ALT_NAME_IP_ADDRESS);
  
          return values.stream()
              .filter(v -> v instanceof String)
              .map(String.class::cast)
              .collect(Collectors.toList());
      }
  
      public static ByteString thumbprint(X509Certificate certificate) throws UaException {
          try {
              return ByteString.of(sha1(certificate.getEncoded()));
          } catch (CertificateEncodingException e) {
              throw new UaException(StatusCodes.Bad_CertificateInvalid, e);
          }
      }
  
      public static ByteString getCertificateChainBytes(List<X509Certificate> certificateChain) throws UaException {
          List<byte[]> certificates = new ArrayList<>(certificateChain.size());
  
          for (X509Certificate certificate : certificateChain) {
              try {
                  certificates.add(certificate.getEncoded());
              } catch (CertificateEncodingException e) {
                  throw new UaException(StatusCodes.Bad_CertificateInvalid, e);
              }
          }
  
          byte[] encoded = certificates.stream()
              .reduce(new byte[0], Bytes::concat);
  
          return ByteString.of(encoded);
      }
  
  }
  