CmsCreator.java

/*
 * Copyright ConsenSys AG.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.pki.cms;

import org.hyperledger.besu.pki.keystore.KeyStoreWrapper;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.EllipticCurve;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.annotations.VisibleForTesting;
import org.apache.tuweni.bytes.Bytes;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;

/** The Cms creator. */
public class CmsCreator {

  static {
    if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
      Security.addProvider(new BouncyCastleProvider());
    }
  }

  private final String certificateAlias;
  private final KeyStoreWrapper keyStore;

  /**
   * Instantiates a new Cms creator.
   *
   * @param keyStore the key store
   * @param certificateAlias the certificate alias
   */
  public CmsCreator(final KeyStoreWrapper keyStore, final String certificateAlias) {
    this.keyStore = keyStore;
    this.certificateAlias = certificateAlias;
  }

  /**
   * Creates a CMS message with the content parameter, signed with the certificate with alias
   * defined in the {@code CmsCreator} constructor. The certificate chain is also included so the
   * recipient has the information needed to build a trusted certificate path when validating this
   * message.
   *
   * @param contentToSign the content that will be signed and added to the message
   * @return the CMS message bytes
   */
  @SuppressWarnings("rawtypes")
  public Bytes create(final Bytes contentToSign) {
    try {
      // Certificates that will be sent
      final List<X509Certificate> x509Certificates =
          Stream.of(keyStore.getCertificateChain(certificateAlias))
              .map(X509Certificate.class::cast)
              .collect(Collectors.toList());
      final Store certs = new JcaCertStore(x509Certificates);

      // Private key of the certificate that will sign the message
      final PrivateKey privateKey = keyStore.getPrivateKey(certificateAlias);

      final ContentSigner contentSigner =
          new JcaContentSignerBuilder(
                  getPreferredSignatureAlgorithm(keyStore.getPublicKey(certificateAlias)))
              .build(privateKey);

      final CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator();

      // Additional intermediate certificates for path building
      cmsGenerator.addCertificates(certs);

      final DigestCalculatorProvider digestCalculatorProvider =
          new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
      // The first certificate in the list (leaf certificate is the signer)
      cmsGenerator.addSignerInfoGenerator(
          new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
              .build(contentSigner, x509Certificates.get(0)));

      // Add signed content
      final CMSTypedData cmsData = new CMSProcessableByteArray(contentToSign.toArray());
      final CMSSignedData cmsSignedData = cmsGenerator.generate(cmsData, false);

      return Bytes.wrap(cmsSignedData.getEncoded());
    } catch (final Exception e) {
      throw new RuntimeException("Error creating CMS data", e);
    }
  }

  /**
   * Gets preferred signature algorithm for EC or RSA keys
   *
   * @param pub the public key
   * @return the preferred signature algorithm
   */
  @VisibleForTesting
  public static String getPreferredSignatureAlgorithm(final PublicKey pub) {
    switch (pub.getAlgorithm()) {
      case "EC" -> {
        final EllipticCurve curve = ((ECPublicKey) pub).getParams().getCurve();
        return switch (curve.getField().getFieldSize()) {
          case 224, 256 -> "SHA256withECDSA";
          case 384 -> "SHA384withECDSA";
          case 521 -> "SHA512withECDSA";
          default -> throw new IllegalArgumentException("Elliptic curve not supported: " + curve);
        };
      }
      case "RSA" -> {
        return "SHA256WithRSAEncryption";
      }
      default ->
          throw new UnsupportedOperationException(
              "Private key algorithm not supported: " + pub.getAlgorithm());
    }
  }
}