lib/mix/tasks/x509.gen.suite.ex

defmodule Mix.Tasks.X509.Gen.Suite do
  @shortdoc "Generates a suite of test certificates"

  @default_path "priv/cert/suite"

  @warning """
  WARNING: only use the generated certificates for testing in a closed network
  environment, such as running a development server on `localhost`.
  For production, staging, or testing servers on the public internet, obtain a
  proper certificate, for example from [Let's Encrypt](https://letsencrypt.org).
  """

  @moduledoc """
  Generates a suite of test certificates.

      mix x509.gen.suite
      mix x509.gen.suite test.local

  Please refer to the documentation for `X509.Test.Suite` for information about
  the scenarios that may be tested using the generated certificates.

  #{@warning}

  ## Arguments

  This tasks takes a single, optional argument: the domain name to use in the
  hostnames embedded in the certificates. If no domain name is specified, the
  default domain from `X509.Test.Suite` will be used.

  Other (optional) arguments:

    * '--password' ('-p'): if set, an encrypted, password protected version of
      each private key will be created
    * `--crlserver` (`-c`): the base URL for the CRL server to be used for
      CRL distribution points
    * `--output` (`-o`): the path where the certificates and keys should be
      stored (default: #{@default_path})
    * `--force` (`-f`): overwite existing files without prompting for
      confirmation

  Requires OTP 20 or later.
  """

  use Mix.Task
  import Mix.Generator

  @doc false
  def run(all_args) do
    {opts, args} =
      OptionParser.parse!(
        all_args,
        aliases: [p: :password, c: :crlserver, o: :output, f: :force],
        strict: [password: :string, crlserver: :string, output: :string, force: :boolean]
      )

    path = opts[:output] || @default_path
    password = opts[:password]
    crl_opts = [crl_server: opts[:crlserver]]
    force = opts[:force] || false

    suite_opts =
      case args do
        [] -> crl_opts
        [domain] -> Keyword.put(crl_opts, :domain, domain)
      end

    suite = X509.Test.Suite.new(suite_opts)

    server_key_pem = X509.PrivateKey.to_pem(suite.server_key)
    create_file(Path.join(path, "server_key.pem"), server_key_pem, force: force)

    if password do
      server_key_enc_pem = X509.PrivateKey.to_pem(suite.server_key, password: password)
      create_file(Path.join(path, "server_key_enc.pem"), server_key_enc_pem, force: force)
    end

    other_key_pem = X509.PrivateKey.to_pem(suite.other_key)
    create_file(Path.join(path, "other_key.pem"), other_key_pem, force: force)

    if password do
      other_key_enc_pem = X509.PrivateKey.to_pem(suite.other_key, password: password)
      create_file(Path.join(path, "other_key_enc.pem"), other_key_enc_pem, force: force)
    end

    client_key_pem = X509.PrivateKey.to_pem(suite.client_key)
    create_file(Path.join(path, "client_key.pem"), client_key_pem, force: force)

    if password do
      client_key_enc_pem = X509.PrivateKey.to_pem(suite.client_key, password: password)
      create_file(Path.join(path, "client_key_enc.pem"), client_key_enc_pem, force: force)
    end

    cacerts_pem =
      suite.cacerts
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "cacerts.pem"), cacerts_pem, force: force)

    alternate_cacerts_pem =
      suite.alternate_cacerts
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "alternate_cacerts.pem"), alternate_cacerts_pem, force: force)

    chain_pem =
      suite.chain
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "chain.pem"), chain_pem, force: force)

    ca_and_chain_pem =
      (suite.cacerts ++ suite.chain)
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "ca_and_chain.pem"), ca_and_chain_pem, force: force)

    expired_chain_pem =
      suite.expired_chain
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "expired_chain.pem"), expired_chain_pem, force: force)

    revoked_chain_pem =
      suite.revoked_chain
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "revoked_chain.pem"), revoked_chain_pem, force: force)

    alternate_chain_pem =
      suite.alternate_chain
      |> Enum.map(&X509.Certificate.from_der!/1)
      |> Enum.map(&X509.Certificate.to_pem/1)
      |> Enum.join()

    create_file(Path.join(path, "alternate_chain.pem"), alternate_chain_pem, force: force)

    valid_pem = X509.Certificate.to_pem(suite.valid)
    create_file(Path.join(path, "valid.pem"), valid_pem, force: force)

    wildcard_pem = X509.Certificate.to_pem(suite.wildcard)
    create_file(Path.join(path, "wildcard.pem"), wildcard_pem, force: force)

    expired_pem = X509.Certificate.to_pem(suite.expired)
    create_file(Path.join(path, "expired.pem"), expired_pem, force: force)

    revoked_pem = X509.Certificate.to_pem(suite.revoked)
    create_file(Path.join(path, "revoked.pem"), revoked_pem, force: force)

    selfsigned_pem = X509.Certificate.to_pem(suite.selfsigned)
    create_file(Path.join(path, "selfsigned.pem"), selfsigned_pem, force: force)

    client_pem = X509.Certificate.to_pem(suite.client)
    create_file(Path.join(path, "client.pem"), client_pem, force: force)

    for {name, crl} <- suite.crls do
      crl_der = X509.CRL.to_der(crl)
      create_file(Path.join(path, name), crl_der, force: force)
    end

    print_shell_instructions(path)
  end

  defp print_shell_instructions(path) do
    Mix.shell().info("""

    The certificates and keys can be found in #{path}.

    #{@warning}
    """)
  end
end