EnclaveFactory.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.enclave;

import org.hyperledger.besu.util.InvalidConfigurationException;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

import com.google.common.io.Files;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.net.PfxOptions;
import org.apache.tuweni.net.tls.VertxTrustOptions;

/** The Enclave factory. */
public class EnclaveFactory {

  private final Vertx vertx;
  private static final int CONNECT_TIMEOUT = 1000;
  private static final boolean TRUST_CA = false;

  /**
   * Instantiates a new Enclave factory.
   *
   * @param vertx the vertx
   */
  public EnclaveFactory(final Vertx vertx) {
    this.vertx = vertx;
  }

  /**
   * Create enclave.
   *
   * @param enclaveUri the enclave uri
   * @return the enclave
   */
  public Enclave createVertxEnclave(final URI enclaveUri) {
    final HttpClientOptions clientOptions = createNonTlsClientOptions(enclaveUri);

    final RequestTransmitter vertxTransmitter =
        new VertxRequestTransmitter(vertx.createHttpClient(clientOptions));

    return new Enclave(vertxTransmitter);
  }

  /**
   * Create enclave.
   *
   * @param enclaveUri the enclave uri
   * @param privacyKeyStoreFile the privacy key store file
   * @param privacyKeyStorePasswordFile the privacy key store password file
   * @param privacyAllowlistFile the privacy allowlist file
   * @return the enclave
   */
  public Enclave createVertxEnclave(
      final URI enclaveUri,
      final Path privacyKeyStoreFile,
      final Path privacyKeyStorePasswordFile,
      final Path privacyAllowlistFile) {

    final HttpClientOptions clientOptions =
        createTlsClientOptions(
            enclaveUri, privacyKeyStoreFile, privacyKeyStorePasswordFile, privacyAllowlistFile);

    final RequestTransmitter vertxTransmitter =
        new VertxRequestTransmitter(vertx.createHttpClient(clientOptions));

    return new Enclave(vertxTransmitter);
  }

  private HttpClientOptions createNonTlsClientOptions(final URI enclaveUri) {

    if (enclaveUri.getPort() == -1) {
      throw new EnclaveIOException("Illegal URI - no port specified");
    }

    final HttpClientOptions clientOptions = new HttpClientOptions();
    clientOptions.setDefaultHost(enclaveUri.getHost());
    clientOptions.setDefaultPort(enclaveUri.getPort());
    clientOptions.setConnectTimeout(CONNECT_TIMEOUT);
    return clientOptions;
  }

  private HttpClientOptions createTlsClientOptions(
      final URI enclaveUri,
      final Path privacyKeyStoreFile,
      final Path privacyKeyStorePasswordFile,
      final Path privacyAllowlistFile) {

    final HttpClientOptions clientOptions = createNonTlsClientOptions(enclaveUri);
    try {
      if (privacyKeyStoreFile != null && privacyKeyStorePasswordFile != null) {
        clientOptions.setSsl(true);
        clientOptions.setPfxKeyCertOptions(
            convertFrom(privacyKeyStoreFile, privacyKeyStorePasswordFile));
      }
      clientOptions.setTrustOptions(
          VertxTrustOptions.allowlistServers(privacyAllowlistFile, TRUST_CA));
    } catch (final NoSuchFileException e) {
      throw new InvalidConfigurationException(
          "Requested file " + e.getMessage() + " does not exist at specified location.");
    } catch (final AccessDeniedException e) {
      throw new InvalidConfigurationException(
          "Current user does not have permissions to access " + e.getMessage());
    } catch (final IllegalArgumentException e) {
      throw new InvalidConfigurationException("Illegally formatted client fingerprint file.");
    } catch (final IOException e) {
      throw new InvalidConfigurationException("Failed to load TLS files " + e.getMessage());
    }
    return clientOptions;
  }

  private static PfxOptions convertFrom(final Path keystoreFile, final Path keystorePasswordFile)
      throws IOException {
    final String password = readSecretFromFile(keystorePasswordFile);
    return new PfxOptions().setPassword(password).setPath(keystoreFile.toString());
  }

  /**
   * Read secret from file.
   *
   * @param path the path
   * @return the string
   * @throws IOException the io exception
   */
  static String readSecretFromFile(final Path path) throws IOException {
    final String password =
        Files.asCharSource(path.toFile(), StandardCharsets.UTF_8).readFirstLine();
    if (password == null || password.isEmpty()) {
      throw new InvalidConfigurationException("Keystore password file is empty: " + path);
    }
    return password;
  }
}