#!/usr/bin/env perl

#
# Copyright (c) 2014-2015 Opsmate, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# Except as contained in this notice, the name(s) of the above copyright
# holders shall not be used in advertising or otherwise to promote the
# sale, use or other dealings in this Software without prior written
# authorization.
#

#
# This program is designed to be used with the online SSLMate service at
# <https://sslmate.com/>.  Use of the SSLMate service is governed by the
# Terms and Conditions available online at <https://sslmate.com/terms>.
#

use 5.010;	# 5.10
use strict;
use warnings;
use Getopt::Long;
use Errno;
use Fcntl;
use POSIX qw(:sys_wait_h strftime);
use Cwd qw(realpath);
use Digest::SHA qw(sha1_hex sha256_hex);
use File::Basename;
use File::Temp;
use IO::Handle;
use IPC::Open2;
use List::Util qw(max sum);
use FindBin;
use Encode;

				# Debian/Ubuntu package		RHEL/CentOS package
				# --------------------------------------------------
use URI::Escape;		# liburi-perl			perl-URI
use JSON::PP; # core in 5.13.9+	# libjson-perl			perl-JSON
use Term::ReadKey;		# libterm-readkey-perl		perl-TermReadKey

use lib '/usr/local/share/sslmate/perllib';
use SSLMate;
use SSLMate::HTTPSClient;

our $API_ENDPOINT = 'https://sslmate.com/api/v2';
our $PKCS12_PASSWORD = 'sslmate';
our $DEFAULT_CERT_TIMEOUT = 600;

our $DEFAULT_LIBEXEC_DIR = '/usr/local/libexec/sslmate';
our $LIBEXEC_DIR = $ENV{'SSLMATE_LIBEXEC_DIR'} // $DEFAULT_LIBEXEC_DIR // "$FindBin::Bin/../libexec/sslmate";

our $DEFAULT_SHARE_DIR = '/usr/local/share/sslmate';
our $SHARE_DIR = $ENV{'SSLMATE_SHARE_DIR'} // $DEFAULT_SHARE_DIR // "$FindBin::Bin/../share/sslmate";

our $batch = 0;
our $quiet = 0;
our $verbose = 0;
our $config_profile;
our %global_config;
our %personal_config;
our %ephemeral_config;
our $https_client;
our $http_approval_map;
our $dns_approval_map;

sub print_usage {
	my ($out) = @_;

	#          |--------------------------------------------------------------------------------| 80 chars
	print $out "Usage: sslmate [OPTIONS] COMMAND [ARGS]\n";
	print $out "\n";
	print $out "Commands:\n";
	print $out " sslmate buy HOSTNAME             Buy a certificate for the given hostname\n";
	print $out " sslmate renew HOSTNAME           Renew the certificate for the given hostname\n";
	print $out " sslmate reissue HOSTNAME         Reissue the certificate for given hostname\n";
	print $out " sslmate rekey HOSTNAME           Rekey the certificate for given hostname\n";
	print $out " sslmate revoke [-a] HOSTNAME     Revoke the certificate for given hostname\n";
	print $out " sslmate download HOSTNAME        Download the certificate for given hostname\n";
	print $out " sslmate import KEYFILE CERTFILE  Import this certificate to your account\n";
	print $out " sslmate list                     List certificates in your SSLMate account\n";
	print $out " sslmate edit OPTIONS HOSTNAME    Edit certificate settings (e.g. auto-renew)\n";
	print $out " sslmate test HOSTNAME            Check if the certificate is properly installed\n";
	print $out " sslmate mkconfig TEMPLATE NAME   Generate configuration for the certificate\n";
	print $out " sslmate retry-approval HOSTNAME  Retry the approval process for a pending cert\n";
	print $out " sslmate link                     Link this system with your SSLMate account\n";
	print $out " sslmate help                     Display help\n";
	print $out " sslmate version                  Print the version of SSLMate that's installed\n";
	print $out "\n";
	print $out "Valid global options:\n";
	print $out " -p, --profile=NAME               Use the given configuration profile\n";
	print $out " --batch                          Never prompt for confirmation or information\n";
	print $out " --verbose                        Display additional information\n";
	print $out "\n";
	print $out "Run 'sslmate help COMMAND' for more information on a specific command.\n";
}

# Decode user input according to the current locale. The result can be passed to uri_escape_utf8
# and it will Do The Right Thing.
sub decode_input {
	my ($input) = @_;
	my $locale_encoding = eval {
		require I18N::Langinfo;
		require Encode;
		Encode::find_encoding(I18N::Langinfo::langinfo(I18N::Langinfo::CODESET()));
	};
	if (!defined $locale_encoding) {
		chomp $@;
		warn "Warning: Unable to determine input character encoding. Non-ASCII characters might not be interpreted correctly. (Details: $@)\n";
		return $input;
	}
	my $output = eval { $locale_encoding->decode($input, 1) };
	if (!defined $output) {
		chomp $@;
		die "Error: Input is not valid " . $locale_encoding->name . ". Please check your locale settings or try again with pure ASCII input. (Details: $@)\n";
	}
	return $output;
}

sub object_subset {
	my $obj = shift;
	return { map { $_ => $obj->{$_} } @_ };
}

sub english_join {
	my $sep = shift;
	if (@_ <= 2) {
		return join(" $sep ", @_);
	} else {
		return join(', ', @_[0..@_-2]) . ", $sep " . $_[@_-1];
	}
}

sub prompt_user {
	my ($message) = @_;

	print $message;
	my $answer = <STDIN>;
	die "Error: Input ended prematurely.\n" unless defined($answer);
	chomp $answer;
	return decode_input($answer);
}

sub prompt_yesno {
	while (defined(my $answer = prompt_user("Enter yes or no: "))) {
		if ($answer eq 'yes') {
			return 1;
		} elsif ($answer eq 'no') {
			return 0;
		} else {
			print "I did not understand that.\n";
		}
	}
}

sub prompt_password {
	my ($message) = @_;

	print $message;

	my $password = '';
	ReadMode(4);
	my %ctrl = GetControlChars;
	while (defined(my $key = ReadKey(0))) {
		if ($key eq "\n" || $key eq "\r" || $key eq $ctrl{EOF}) { # e.g. Ctrl+D
			print "\n";
			last;
		} elsif ($key eq $ctrl{INTERRUPT}) { # e.g. Ctrl+C
			$password = undef;
			last;
		} elsif ($key eq $ctrl{ERASE}) {
			if ($password ne '') {
				chop $password;
				print "\b \b";
			}
		} elsif ($key eq $ctrl{KILL} || $key eq $ctrl{ERASEWORD}) { # e.g. Ctrl+U, Ctrl+W
			while ($password ne '') {
				chop $password;
				print "\b \b";
			}
		} else {
			$password = $password . $key;
			print "*";
		}
	}
	ReadMode(0);

	return decode_input($password);
}

sub is_wildcard_name {
	my ($name) = @_;
	return $name =~ /^\*\./;
}

sub restore_file_permissions {
	my ($destfile, $srcfile) = @_;

	my @srcstat = stat($srcfile) or die "Error: $srcfile: $!\n";
	if (not @srcstat) {
		warn "Warning: unable to preserve permissions of $srcfile: stat failed: $!\n";
		return;
	}

	# 1. Restore user and group
	if (!chown($srcstat[4], -1, $destfile)) {
		warn "Warning: unable to preserve ownership of $srcfile: chown failed: $!\n";
	}
	if (!chown(-1, $srcstat[5], $destfile)) {
		warn "Warning: unable to preserve group ownership of $srcfile: chown failed: $!\n";
	}

	# 2. Restore mode
	my $facls;
	if ($^O eq 'linux') {
		# Try to use getfacl so that FACLs are preserved.  But getfacl might not be
		# installed, so quietly fall back to regular chmod if getfacl fails.  Note
		# that getfacl/setfacl also get/set normal permissions, and work even on
		# filesystems which don't support FACLs.
		pipe(my $getfacl_reader, my $getfacl_writer) or die "Error: pipe failed: $!";
		my $getfacl_pid = fork;
		die "Error: fork failed: $!" unless defined $getfacl_pid;
		if ($getfacl_pid == 0) {
			open(STDIN, '<', '/dev/null');
			open(STDOUT, '>&', $getfacl_writer) or die "Error: dup failed: $!";
			open(STDERR, '>', '/dev/null');
			close($getfacl_reader);
			exec('getfacl', $srcfile);
			exit 1;
		}
		close($getfacl_writer);

		my $getfacl_output = do { local $/; <$getfacl_reader> };
		close($getfacl_reader);

		waitpid($getfacl_pid, 0) or die "Error: waitpid failed: $!";
		$facls = $getfacl_output if $? == 0;
	}
	if (defined $facls) {
		open(my $setfacl_writer, '|-', 'setfacl', '-M-', $destfile) or die "Error: fork failed: $!";
		print $setfacl_writer $facls;
		close($setfacl_writer) or warn "Warning: unable to preserve permissions of $srcfile: setfacl failed\n";
	} else {
		chmod($srcstat[2] & 07777, $destfile) or warn "Warning: unable to preserve permissions of $srcfile: chmod failed: $!\n";
	}
}

sub validate_cn {
	my ($cn) = @_;
	# Most validation happens server-side, but a slash can't be encoded in a URL, so we have
	# to check for it client-side.
	if ($cn =~ /\//) {
		print STDERR "Error: $cn: invalid common name (contains a slash)\n";
		return 0;
	}
	return 1;
}

sub config_has {
	my ($name) = @_;

	return defined $ephemeral_config{$name} || defined $personal_config{$name} || defined $global_config{$name};
}

sub get_config {
	my ($name, $default_value) = @_;

	return $ephemeral_config{$name} if defined $ephemeral_config{$name};
	return $personal_config{$name} if defined $personal_config{$name};
	return $global_config{$name}   if defined $global_config{$name};

	return $default_value;
}

sub get_config_with_prefix {
	my ($prefix) = @_;

	my %ret;

	for my $config (\%global_config, \%personal_config, \%ephemeral_config) {
		for my $key (grep { index($_, $prefix) == 0 } keys %$config) {
			$ret{substr($key, length $prefix)} = $config->{$key};
		}
	}

	return \%ret;
}

sub migrate_config_option {
	my ($config_ref, $old_name, $new_name) = @_;

	if (exists $config_ref->{$old_name}) {
		$config_ref->{$new_name} = $config_ref->{$old_name} unless exists $config_ref->{$new_name};
		delete $config_ref->{$old_name};
	}
}

sub migrate_api_creds {
	my ($config_ref) = @_;

	if (exists $config_ref->{account_id}) {
		if (exists $config_ref->{api_key} && not($config_ref->{api_key} =~ /_/)) {
			$config_ref->{api_key} = join('_', $config_ref->{account_id}, $config_ref->{api_key});
		}
		delete $config_ref->{account_id};
	}
}

sub read_config_file {
	my ($filename) = @_;

	open(my $config_fh, '<', $filename) or die "Error: Unable to open $filename for reading: $!\n";
	my %config_hash = map { my @f = split(' ', $_, 2); $f[0] => $f[1] } grep(/^[^#]/, map { chomp; $_ } <$config_fh>);
	close($config_fh);
	migrate_config_option(\%config_hash, 'api-endpoint', 'api_endpoint');
	migrate_config_option(\%config_hash, 'account-id', 'account_id');
	migrate_config_option(\%config_hash, 'api-key', 'api_key');
	migrate_api_creds(\%config_hash);
	return %config_hash;
}
sub write_config_file {
	my ($filename, $config_ref) = @_;

	sysopen(my $config_fh, $filename, O_WRONLY | O_TRUNC | O_CREAT, 0600) or die "Error: Unable to open $filename for writing: $!\n";
	for my $param_name (keys %$config_ref) {
		print $config_fh $param_name . ' ' . $config_ref->{$param_name} . "\n";
	}
	close($config_fh);
}
sub get_personal_config_path {
	return $ENV{'SSLMATE_CONFIG'} if $ENV{'SSLMATE_CONFIG'};
	return $ENV{'HOME'} . '/.sslmate' . ($config_profile ? "-$config_profile" : "") if $ENV{'HOME'};
	die "Error: Neither \$SSLMATE_CONFIG nor \$HOME environment variables set.\n";
}
sub get_global_config_path {
	return '/etc/sslmate' . ($config_profile ? "-$config_profile" : "") . '.conf';
}
sub load_config {
	# Personal config
	%personal_config = ();

	my $personal_config_path = get_personal_config_path;
	if (-e $personal_config_path) {
		%personal_config = read_config_file($personal_config_path);
	}

	# Global config
	%global_config = ();

	my $global_config_path = get_global_config_path;
	# global config file might be readable only by root, so only attempt
	# to access if it's readable.
	if (-r $global_config_path) {
		%global_config = read_config_file($global_config_path);
	}
}
sub save_config {
	write_config_file(get_personal_config_path, \%personal_config);
}
sub is_linked {
	return config_has('api_key');
}
sub parse_bool {
	my ($str) = @_;
	return undef unless defined($str);
	$str = lc $str;
	if ($str eq 'yes' || $str eq 'true' || $str eq 'on' || $str eq 'enabled' || $str eq '1') {
		return 1;
	} elsif ($str eq 'no' || $str eq 'false' || $str eq 'off' || $str eq 'disabled' || $str eq '0') {
		return 0;
	} else {
		print STDERR "Warning: ignoring unknown boolean value '$str' (expected 'yes' or 'no')\n";
		return undef;
	}
}

sub init_default_paths {
	my ($do_mkdir) = @_;
	$do_mkdir //= 1;
	if (!config_has("key_directory") && !config_has("cert_directory")) {
		if ($> == 0) {
			my $default_directory = '/etc/sslmate' . ($config_profile ? "-$config_profile" : "");
			if ($do_mkdir && !mkdir($default_directory, 0755)) {
				die "Error: Unable to create $default_directory: $!\n" unless $!{EEXIST};
			}
			$global_config{'key_directory'} = $default_directory;
			$global_config{'cert_directory'} = $default_directory;
		}
	}
	warn "Warning: the honor_umask option is deprecated and will be removed in a future version of SSLMate.\n" if config_has('honor_umask');
	unless (get_config('honor_umask', 'no') eq 'yes') {
		umask 0022;
	}
}

sub read_file {
	my ($filename) = @_;
	open(my $fh, '<', $filename) or return undef;
	my $contents = do { local $/; <$fh> };
	close($fh);
	return $contents;
}

sub file_contents_are {
	my ($filename, $contents) = @_;
	if (defined(my $actual_contents = read_file($filename))) {
		return $actual_contents eq $contents;
	} else {
		return 0;
	}
}

sub make_openssl_req_cnf {
	my ($dn) = @_;
	my $tempfile = File::Temp->new();
	print $tempfile <<EOF;
[ req ]
distinguished_name	= req_distinguished_name
prompt			= no
[ req_distinguished_name ]
EOF
	for my $component (qw/C ST L O OU CN/) {
		print $tempfile $component . " = " . $dn->{$component} . "\n" if defined $dn->{$component};
	}
	close $tempfile;
	return $tempfile;
}

our %file_types = (
	crt => 'Bare certificate',
	chain => 'Certificate chain',
	root => 'Root certificate',
	chained => 'Certificate with chain',
	combined => 'Combined PEM file',
	'chain+root' => 'Certificate chain with root',
	p12 => 'PKCS#12 file',
	jks => 'Java keystore',
);
our %file_extensions = (
	crt => '.crt',
	chain => '.chain.crt',
	root => '.root.crt',
	chained => '.chained.crt',
	combined => '.combined.pem',
	'chain+root' => '.chain+root.crt',
	p12 => '.p12',
	jks => '.jks',
);
our %file_type_contains_private_key = (
	combined => 1,
	p12 => 1,
	jks => 1,
);

sub get_enabled_cert_types {
	my %enabled;

	if (config_has('cert_formats')) {
		# (For backwards compat. Although this option was never publicized, some people may be depending on it.)
		for my $type (split /,\s*|\s+/, get_config("cert_formats")) {
			die "Unknown file type in cert_formats config option: $type\n" unless exists $file_types{$type};
			$enabled{$type} = 1;
		}
	} else {
		# Types enabled by default:
		$enabled{chained} = 1;
	}

	# Types enabled/disabled by cert_format.$type config options:
	for my $type (keys %file_types) {
		if (defined(my $enabled = parse_bool(get_config("cert_format.$type")))) {
			if ($enabled) {
				$enabled{$type} = 1;
			} else {
				delete $enabled{$type};
			}
		}
	}

	# Types that are always enabled and can't be disabled by config:
	$enabled{crt} = 1;
	$enabled{chain} = 1;

	return keys %enabled;
}

sub get_cert_paths {
	my ($cn) = @_;

	if ($cn =~ /^[*]([.].*)$/ && config_has("wildcard_filename")) {
		$cn = get_config("wildcard_filename") . $1;
	}

	my $key_directory = config_has("key_directory") ? get_config("key_directory") . "/" : "";
	my $cert_directory = config_has("cert_directory") ? get_config("cert_directory") . "/" : "";

	my $paths = {};
	$paths->{key} = $key_directory . $cn . ".key";
	for my $type (get_enabled_cert_types) {
		$paths->{$type} = $cert_directory . $cn . $file_extensions{$type};
	}
	return $paths;
}

sub print_cert_paths {
	my ($paths, $key_status, $cert_status) = @_;

	$key_status //= '';
	$cert_status //= '';

	my @rows;

	if (defined $paths->{key}) {
		if ($key_status eq 'missing') {
			push @rows, ["Private key", "(not found - should be " . $paths->{key} . ")"];
		} elsif ($key_status eq 'old') {
			push @rows, ["Private key", $paths->{key} . " (out-of-date)"];
		} else {
			push @rows, ["Private key", $paths->{key}];
		}
	}

	for my $type (qw/crt chain chained combined root chain+root p12 jks/) {
		next unless defined $paths->{$type};

		if ($file_type_contains_private_key{$type} && $key_status eq 'missing') {
			push @rows, [$file_types{$type}, '(private key not found)'];
		} elsif ($cert_status eq 'pending') {
			push @rows, [$file_types{$type}, "(not yet issued - will be " . $paths->{$type} . ")"];
		} elsif ($cert_status eq 'temporary') {
			push @rows, [$file_types{$type}, $paths->{$type} . " (temporary)"];
		} else {
			push @rows, [$file_types{$type}, $paths->{$type}];
		}
	}

	my $field_width = max(map { length($_->[0]) } @rows);
	for my $row (@rows) {
		printf "%*s: %s\n", $field_width, $row->[0], $row->[1];
	}
}

sub has_existing_files {
	my @filenames = @_;

	my $already_exists = 0;
	for my $filename (@filenames) {
		if (defined $filename && -e $filename) {
			#print STDERR "Error: a file named '$filename' already exists. Please move/remove" . (!config_has("key_directory") && !config_has("cert_directory") ? " or run sslmate from a different directory" : "") . ".\n";
			print STDERR "Error: a file named '$filename' already exists.\n";
			$already_exists++;
		}
	}
	return $already_exists;
}

sub open_key_file {
	my ($filename, $overwrite) = @_;

	my $flags = O_WRONLY | O_CREAT;
	$flags |= O_EXCL unless $overwrite;

	my $fh;
	sysopen($fh, $filename, $flags, 0600)
		or die "Error: unable to open '$filename' for writing: $!\n";
	return $fh;
}

sub write_pkcs12_file {
	my ($out_file, $key, $crt, $chain) = @_;

	pipe(my $openssl_stdin, my $to_openssl_stdin) or die "Error: pipe failed: $!";
	my $openssl_pid = fork;
	die "Error: fork failed: $!" unless defined $openssl_pid;
	if ($openssl_pid == 0) {
		open(STDIN, '<&', $openssl_stdin) or die "Error: dup failed: $!";
		open(STDOUT, '>&', $out_file) or die "Error: dup failed: $!";
		close($to_openssl_stdin);
		$ENV{PKCS12_PASSWORD} = $PKCS12_PASSWORD;
		exec('openssl', 'pkcs12', '-export', '-passout', 'env:PKCS12_PASSWORD');
		die "Error: Unable to run 'openssl pkcs12' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	close($openssl_stdin);
	print $to_openssl_stdin join('', $key, $crt, $chain);
	close($to_openssl_stdin);
	waitpid($openssl_pid, 0) or die "waitpid failed: $!";
	unless ($? == 0) {
		warn "Warning: 'openssl pkcs12' command failed; could not create PKCS#12 file\n";
		return 0;
	}
	return 1;
}

sub write_jks_file {
	my ($out_file, $key, $crt, $chain) = @_;

	my $tempdir = File::Temp->newdir();
	my $temp_pkcs12_filename = "$tempdir/in.p12";
	my $temp_jks_filename = "$tempdir/out.jks";

	# Write a temporary PKCS#12 file
	open(my $temp_pkcs12_file, '>', $temp_pkcs12_filename) or die "Unable to open $temp_pkcs12_filename for writing: $!\n";
	unless (write_pkcs12_file($temp_pkcs12_file, $key, $crt, $chain)) {
		warn "Warning: could not create Java Key Store file because creating the PKCS#12 file failed\n";
		return 0;
	}
	close($temp_pkcs12_file);

	# Fork and exec keytool to convert the PKCS#12 file to a Java Key Store
	pipe(my $keytool_reader, my $keytool_writer) or die "Error: pipe failed: $!";
	my $keytool_pid = fork;
	die "Error: fork failed: $!" unless defined $keytool_pid;
	if ($keytool_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $keytool_writer) or die "Error: dup failed: $!";
		open(STDERR, '>&', $keytool_writer) or die "Error: dup failed: $!";
		close($keytool_reader);
		exec('keytool', '-importkeystore',
				'-srckeystore', $temp_pkcs12_filename,
				'-srcstoretype', 'pkcs12',
				'-srcstorepass', $PKCS12_PASSWORD,
				'-deststorepass', $PKCS12_PASSWORD,
				'-destkeystore', $temp_jks_filename,
				'-noprompt');
		die "Unable to run 'keytool' command: " . ($!{ENOENT} ? 'keytool command not found' : $!) . "\n";
		exit 1;
	}
	close($keytool_writer);
	my $keytool_output = do { local $/; <$keytool_reader> };
	chomp $keytool_output;
	close($keytool_reader);
	waitpid($keytool_pid, 0) or die "Error: waitpid failed: $!";
	unless ($? == 0) {
		warn "Warning: could not create Java Key Store file: $keytool_output\n";
		return 0;
	}

	# Now read the temporary Java Key Store file and write it to $out_file
	my $temp_jks_file;
	unless (open($temp_jks_file, '<', $temp_jks_filename)) {
		warn "Warning: could not create Java Key Store file: $temp_jks_filename: $!\n";
		return 0;
	}
	print $out_file do { local $/; <$temp_jks_file> };
	return 1;
}

sub write_cert_file {
	my ($type, $file, $key, $crt, $chain, $root) = @_;

	if ($type eq 'crt') {
		print $file $crt;
		return 1;
	} elsif ($type eq 'chain') {
		print $file $chain;
		return 1;
	} elsif ($type eq 'root') {
		print $file $root;
		return 1;
	} elsif ($type eq 'chained') {
		print $file $crt;
		print $file $chain;
		return 1;
	} elsif ($type eq 'combined') {
		print $file $key;
		print $file $crt;
		print $file $chain;
		return 1;
	} elsif ($type eq 'chain+root') {
		print $file $chain;
		print $file $root;
		return 1;
	} elsif ($type eq 'p12') {
		return write_pkcs12_file($file, $key, $crt, $chain);
	} elsif ($type eq 'jks') {
		return write_jks_file($file, $key, $crt, $chain);
	}
	return 0;
}

sub write_cert_files {
	my ($paths, $new_key_filename, $crt, $chain, $root) = @_;

	my $key = read_file($new_key_filename // $paths->{key});
	my %files;

	for my $type (keys %file_types) {
		next unless defined $paths->{$type};

		if ($file_type_contains_private_key{$type}) {
			next unless defined $key;
		}

		my $tempfile = File::Temp->new(DIR => dirname($paths->{$type}), TEMPLATE => '.sslmate.XXXXXX');
		if (-e $paths->{$type}) {
			# Preserve existing permissions if this file already exists
			restore_file_permissions($tempfile, $paths->{$type});
		} elsif ($file_type_contains_private_key{$type}) {
			# By default, files which contain the private key should have the same
			# permissions as the private key itself.
			if (-e $paths->{key}) {
				restore_file_permissions($tempfile, $paths->{key});
			} else {
				# And if the private key doesn't exist, use restrictive permissions
				chmod(0600 & ~umask, $tempfile);
			}
		} else {
			# Files which don't contain a private key are world-readable by default
			chmod(0666 & ~umask, $tempfile);
		}

		write_cert_file($type, $tempfile, $key, $crt, $chain, $root) or next; # TODO: if this fails, should we delete the current file of this type?  Should we hide it from the cert path output?

		$files{$type} = $tempfile;
	}

	# Rename the new files on top of the old files:
	if (defined $new_key_filename && $new_key_filename ne $paths->{key}) {
		restore_file_permissions($new_key_filename, $paths->{key}) if -e $paths->{key};
		rename($new_key_filename, $paths->{key})
				or die "Error: " . $paths->{key} . ': ' . $! . "\n";
	}

	for my $type (keys %files) {
		rename($files{$type}->filename, $paths->{$type})
					or die "Error: " . $paths->{$type} . ': ' . $! . "\n";
		$files{$type}->unlink_on_destroy(0);
	}
}

sub qs_escape {
	my ($str) = @_;
	return uri_escape_utf8($str, '^A-Za-z0-9\-\._');
}

sub to_json_bool {
	my ($x) = @_;
	return $x ? JSON::PP::true : JSON::PP::false;
}

sub api_call {
	my ($method, $command, $creds, $query_string, $post_data, $post_data_type) = @_;

	$https_client //= SSLMate::HTTPSClient->new;

	$query_string = SSLMate::HTTPSClient::make_query_string($query_string) if ref($query_string) eq 'HASH';
	$post_data_type //= 'application/x-www-form-urlencoded';
	if (ref($post_data) eq 'HASH') {
		if ($post_data_type eq 'application/x-www-form-urlencoded') {
			$post_data = SSLMate::HTTPSClient::make_query_string($post_data);
		} elsif ($post_data_type eq 'application/json') {
			$post_data = encode_json($post_data);
		}
	}
	$command .= "?$query_string" if defined($query_string) && length($query_string);

	my $max_total_delay = 20;
	my $next_retry_delay = 2;

	my $uri = (get_config('api_endpoint') // $API_ENDPOINT) . $command;
	my $headers = {};
	$headers->{'Content-Type'} = $post_data_type if defined($post_data);
	my $encoded_creds;
	if (defined $creds) {
		$encoded_creds = { username => $creds->{username}, password => encode('utf8', $creds->{password}) };
	}
	while (1) {
		my ($http_status, $content_type, $response_data) = eval {
			$https_client->request($method, $uri, $headers, $encoded_creds, $post_data)
		};
		if (not defined $http_status) {
			print STDERR "Error: Unable to contact SSLMate server: $@";
			return;
		}

		$content_type //= '';

		if ($content_type ne 'application/json') {
			print STDERR "Error: received unexpected response from SSLMate server: response not JSON (content-type=$content_type; status=$http_status)\n";
			return;
		}

		my $response_obj = eval { decode_json($$response_data) };
		if (!defined($response_obj)) {
			chomp $@;
			print STDERR "Error: received malformed response from SSLMate server: $@\n";
			return;
		}

		if ($http_status == 503 && defined $response_obj->{retry_after}) {
			# Service unavailable. Retry with exponential backoff, but never
			# retry sooner than the retry_after specified by the server.
			my $delay = max($next_retry_delay, $response_obj->{retry_after});
			if ($delay <= $max_total_delay) {
				$max_total_delay -= $delay;
				$next_retry_delay *= 2;
				sleep($delay);
				next;
			}
		}

		return ($http_status, $response_obj);
	}
}

sub default_api_credentials {
	return { username => get_config('api_key'), password => '' };
}

sub anon_api_call {
	my ($method, $command, $query_string, $post_data, $post_data_type) = @_;
	return api_call($method, $command, undef, $query_string, $post_data, $post_data_type);
}

sub authed_api_call {
	my ($method, $command, $query_string, $post_data, $post_data_type) = @_;
	return api_call($method, $command, default_api_credentials, $query_string, $post_data, $post_data_type);
}

sub format_chain {
	my ($chain) = @_;
	return ref($chain) eq 'ARRAY' ? join('', @$chain) :
	       defined($chain) ? $chain : '';
}

sub format_approval_method {
	my ($raw_method) = @_;
	return 'Email' if $raw_method eq 'email';
	return 'HTTP' if $raw_method eq 'http';
	return 'DNS' if $raw_method eq 'dns';
	return $raw_method;
}

sub load_approval_map {
	my ($type) = @_;

	my $param = $type . '_approval_map';
	my $path = get_config($param);
	if (not defined $path) {
		return undef;
	}
	my $fh;
	if (not open($fh, '<', $path)) {
		die "Cannot open $path: $!\n";
	}
	my $map = {};
	for my $line (<$fh>) {
		chomp $line;
		my ($hostname, $command, @param_words) = split(' ', $line);
		my $params = {};
		unless ($command =~ /^\//) {
			$params = get_config_with_prefix("$type.$command.");
		}
		my $index = 0;
		for (@param_words) {
			if (my ($name, $value) = /^([^=]+)=(.*)$/) {
				$params->{$name} = $value;
			} else {
				$params->{$index++} = $_;
			}
		}
		$map->{$hostname} = { command => $command, params => $params };
	}
	return $map;
}

sub available_approval_handlers {
	my ($type) = @_;
	my @handlers;
	my $dirpath = "$LIBEXEC_DIR/approval/$type";
	if (opendir(my $dh, $dirpath)) {
		@handlers = map { { command => $_, params => get_config_with_prefix("$type.$_.") } } grep { !/^\./ && -x "$dirpath/$_" } readdir($dh);
		closedir($dh);
	}
	return @handlers;
}

sub exec_approval_handler {
	my ($type, $handler, @args) = @_;

	pipe(my $from_handler_stderr, my $handler_stderr) or die "Error: pipe failed: $!";
	my $handler_pid = fork;
	die "Error: fork failed: $!" unless defined $handler_pid;
	if ($handler_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDERR, '>&', $handler_stderr) or die "Error: dup failed: $!";
		close($from_handler_stderr);

		$ENV{PERL5LIB} = join(':', @INC); # Ensure handler can find SSLMate module if it's written in Perl
		$ENV{PARAMS} = join(' ', sort keys %{$handler->{params}});
		for my $name (keys %{$handler->{params}}) {
			$ENV{"PARAM_$name"} = $handler->{params}->{$name};
		}
		my $path = $handler->{command};
		unless ($path =~ /^\//) {
			$path = "$LIBEXEC_DIR/approval/$type/$path";
		}

		exec($path, @args);
		die "Error: Unable to exec '$path': $!\n";
	}
	close($handler_stderr);

	my $handler_stderr_string = do { local $/; <$from_handler_stderr> };
	close($from_handler_stderr);

	waitpid($handler_pid, 0) or die "waitpid failed: $!";
	my $handler_exit_code = WIFEXITED($?) ? WEXITSTATUS($?) : -1;

	return ($handler_exit_code, \$handler_stderr_string);
}

sub prepare_http_approval {
	my ($action, $options) = @_;
	$http_approval_map //= load_approval_map('http');
	my @tried_hostnames;
	my @handlers_stderr;
	for my $option (@$options) {
		push @tried_hostnames, $option->{hostname};
		if (defined($http_approval_map) && defined(my $handler = $http_approval_map->{$option->{hostname}})) {
			my ($status, $stderr) = exec_approval_handler('http', $handler, $action, @$option{qw{hostname path content}});
			print STDERR $$stderr;
			return if $status == 0;
		} else {
			for my $handler (available_approval_handlers('http')) {
				my ($status, $stderr) = exec_approval_handler('http', $handler, $action, @$option{qw{hostname path content}});
				if ($status == 0) {
					print STDERR $$stderr;
					return;
				} elsif ($status == 3) {
					# This handler can't be used without parameters
				} elsif ($status == 4) {
					# This handler couldn't handle this host. Try the next handler...
					push @handlers_stderr, $stderr;
				} elsif ($status == 5 || $status == 126 || $status == 127) {
					# This handler isn't usable due to missing dependencies. Try the next handler...
					# (126 and 127 are returned by /usr/bin/env and are actually standarized by SUSv2)
					push @handlers_stderr, $stderr;
				} else {
					# This handler could handle this host, but there was an error.
					# Do not try any further handlers, but do continue to the next option.
					print STDERR $$stderr;
					last;
				}
			}
		}
	}

	if ($verbose) {
		print STDERR $$_ for @handlers_stderr;
	}
	die "No HTTP approval handler available for " . english_join('or', @tried_hostnames) . "." . (not($verbose) && @handlers_stderr ? " Specify the --verbose option for details." : "") . "\n";
}

sub prepare_dns_approval {
	my ($action, $options) = @_;
	$dns_approval_map //= load_approval_map('dns');
	my @tried_names;
	my @handlers_stderr;
	for my $option (@$options) {
		my $name = $option->{name};
		push @tried_names, $name;
		my $handler;
		if (defined($dns_approval_map)) {
			while (!defined($handler) && $name ne '') {
				$handler = $dns_approval_map->{$name};
				$name =~ s/^[^.]*\.//;
			}
		}
		if (defined($handler)) {
			my ($status, $stderr) = exec_approval_handler('dns', $handler, $action, @$option{qw{name type value}});
			print STDERR $$stderr;
			return if $status == 0;
		} else {
			for my $handler (available_approval_handlers('dns')) {
				my ($status,$stderr) = exec_approval_handler('dns', $handler, $action, @$option{qw{name type value}});
				if ($status == 0) {
					print STDERR $$stderr;
					return;
				} elsif ($status == 3) {
					# This handler can't be used without parameters
				} elsif ($status == 4) {
					# This handler couldn't handle this host. Try the next handler...
					push @handlers_stderr, $stderr;
				} elsif ($status == 5 || $status == 126 || $status == 127) {
					# This handler isn't usable due to missing dependencies. Try the next handler...
					# (126 and 127 are returned by /usr/bin/env and are actually standarized by SUSv2)
					push @handlers_stderr, $stderr;
				} else {
					# This handler could handle this host, but there was an error.
					# Do not try any further handlers, but do continue to the next option.
					print STDERR $$stderr;
					last;
				}
			}
		}
	}
	if ($verbose) {
		print STDERR $$_ for @handlers_stderr;
	}
	die "No DNS approval handler available for " . english_join('or', @tried_names) . "." . (not($verbose) && @handlers_stderr ? " Specify the --verbose option for details." : "") . "\n";
}

sub compare_approval_option {
	my ($a, $b) = @_;
	for my $key (keys %$a, keys %$b) {
		return 0 unless exists($a->{$key}) && exists($b->{$key});
		return 0 unless $a->{$key} eq $b->{$key};
	}
	return 1;
}

sub compare_approval_options {
	my ($a, $b) = @_;
	return 0 unless @$a == @$b;
	for my $i (0..@$a-1) {
		return 0 unless compare_approval_option($a->[$i], $b->[$i]);
	}
	return 1;
}

sub prompt_for_manual_dns_approval {
	my ($options) = @_;

	print "\n";
	if (@$options == 1) {
		print "Please add the following DNS record to your domain's DNS:\n";
	} else {
		print "Please add one of the following DNS records to your domain's DNS:\n";
	}
	print "\n";
	for my $option (@$options) {
		print "    " . join(' ', @$option{qw{name type value}}) . "\n";
	}
	print "\n";
	print "You may remove any DNS record you previously added for this certificate.\n";
	print "You should leave the new DNS record in place as long as this certificate\n";
	print "is in use.\n";
	print "\n";
	while (1) {
		my $answer = prompt_user('Press ENTER when done (or q to quit): ');
		if ($answer eq '') {
			return 1;
		} elsif ($answer eq 'q') {
			return 0;
		}
	}
}

sub remove_approval_for {
	my ($hostname, $old_obj, $new_obj) = @_;

	if ($old_obj->{approval_method} eq 'http') {
		if (!defined($new_obj) ||
				$new_obj->{approval_method} ne 'http' ||
				!compare_approval_options($old_obj->{http_approval}->{options},
							  $new_obj->{http_approval}->{options})) {
			my $successful = eval {
				prepare_http_approval('del', $old_obj->{http_approval}->{options});
				1;
			};
			# Question: Display a warning if not successful?
		}
	} elsif ($old_obj->{approval_method} eq 'dns') {
		if (!defined($new_obj) ||
				$new_obj->{approval_method} ne 'dns' ||
				!compare_approval_options($old_obj->{dns_approval}->{options},
							  $new_obj->{dns_approval}->{options})) {
			print "Removing old DNS approval records for $hostname...\n";
			my $successful = eval {
				prepare_dns_approval('del', $old_obj->{dns_approval}->{options});
				1;
			};
			# Question: Display a warning if not successful?
		}
	}
}

sub add_approval_for {
	my ($hostname, $obj) = @_;

	my $successful = 0;

	if ($obj->{approval_method} eq 'http') {
		$successful = eval {
			prepare_http_approval('add', $obj->{http_approval}->{options});
			1;
		};
		if (not $successful) {
			if ($obj->{http_approval}->{status}->{ready}) {
				print STDERR "Notice: HTTP approval for $hostname was already present.\n";
				$successful = 1;
			} else {
				my $why = $@;
				chomp $why;
				print STDERR "Error: unable to automatically configure HTTP approval for $hostname: $why\n";
			}
		}
	} elsif ($obj->{approval_method} eq 'dns') {
		print "Adding DNS approval record for $hostname...\n";
		$successful = eval {
			prepare_dns_approval('add', $obj->{dns_approval}->{options});
			1;
		};
		if (not $successful) {
			if ($obj->{dns_approval}->{status}->{ready}) {
				print STDERR "Notice: DNS approval record for $hostname was already present.\n";
				$successful = 1;
			} else {
				my $why = $@;
				chomp $why;
				if ($batch) {
					print STDERR "Error: unable to automatically configure DNS approval for $hostname: $why\n";
				} else {
					print "Notice: unable to automatically configure DNS approval for $hostname: $why\n";
					$successful = prompt_for_manual_dns_approval($obj->{dns_approval}->{options});
				}
			}
		}
	} else {
		$successful = 1;
	}

	return $successful;
}

sub prepare_approval {
	my ($old_cert, $new_cert) = @_;

	if (defined($old_cert) && $old_cert->{exists}) {
		remove_approval_for($new_cert->{cn}, $old_cert, $new_cert);

		if (defined $old_cert->{sans}) {
			for my $old_san_obj (@{$old_cert->{sans}}) {
				next unless $old_san_obj->{type} eq 'dns';

				my $new_san_obj = (grep { $_->{type} eq $old_san_obj->{type} &&
							  $_->{value} eq $old_san_obj->{value} }
						   @{$new_cert->{sans}})[0];
				remove_approval_for($old_san_obj->{value}, $old_san_obj, $new_san_obj);
			}
		}
	}

	add_approval_for($new_cert->{cn}, $new_cert) or return 0;

	if (defined $new_cert->{sans}) {
		for my $san_obj (@{$new_cert->{sans}}) {
			next unless $san_obj->{type} eq 'dns';
			add_approval_for($san_obj->{value}, $san_obj) or return 0;
		}
	}

	return 1;
}

sub compare_approval_method {
	my ($a, $b) = @_;

	return 0 if $a->{approval_method} ne $b->{approval_method};
	if ($a->{approval_method} eq 'email') {
		return 0 if $a->{approver_email} ne $b->{approver_email};
	}
	return 1;
}

sub compare_san_obj {
	my ($a, $b) = @_;

	return 0 if !compare_approval_method($a, $b);
	return 1;
}

sub openssl_genrsa {
	my ($key_file, $nbits) = @_;

	my $openssl_pid = fork;
	die "Error: fork failed: $!" unless defined $openssl_pid;
	if ($openssl_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $key_file) or die "Error: dup failed: $!";
		open(STDERR, '>', '/dev/null');
		exec('openssl', 'genrsa', $nbits);
		die "Error: Unable to run 'openssl genrsa' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	waitpid($openssl_pid, 0) or die "waitpid failed: $!";
	die "Error: 'openssl genrsa' command failed.\n" unless $? == 0;
}

sub openssl_ecparam_genkey {
	my ($key_file, $curve_name) = @_;

	my $openssl_pid = fork;
	die "Error: fork failed: $!" unless defined $openssl_pid;
	if ($openssl_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $key_file) or die "Error: dup failed: $!";
		open(STDERR, '>', '/dev/null');
		exec('openssl', 'ecparam', '-name', $curve_name, '-genkey');
		die "Error: Unable to run 'openssl ecparam' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	waitpid($openssl_pid, 0) or die "waitpid failed: $!";
	die "Error: 'openssl ecparam' command failed.\n" unless $? == 0;
}

sub genkey {
	my ($key_file, $key_type) = @_;
	$key_type //= get_config('key_type', 'rsa');
	if (lc $key_type eq 'rsa') {
		openssl_genrsa($key_file, get_config('rsa_bits', 2048));
	} elsif (lc $key_type eq 'ecdsa') {
		openssl_ecparam_genkey($key_file, get_config('ecdsa_curve', 'prime256v1'));
	} else {
		print STDERR "Error: invalid key type: $key_type (valid options are 'rsa' and 'ecdsa')\n";
		return 0;
	}
	return 1;
}

sub make_dn {
	my ($cn, $country_code) = @_;
	return { CN => $cn, C => $country_code // 'US', ST => 'Some-State', O => 'Internet Widgits Pty Ltd' };
}

sub openssl_req {
	my ($key_filename, $dn) = @_;

	my $openssl_req_cnf = make_openssl_req_cnf($dn);
	pipe(my $openssl_reader, my $openssl_writer) or die "Error: pipe failed: $!";
	my $openssl_pid = fork;
	die "Error: fork failed: $!" unless defined $openssl_pid;
	if ($openssl_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $openssl_writer) or die "Error: dup failed: $!";
		close($openssl_reader);
		exec('openssl', 'req', '-new', '-key', $key_filename, '-config', $openssl_req_cnf->filename);
		die "Error: Unable to run 'openssl req' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	close($openssl_writer);

	my $csr_data = do { local $/; <$openssl_reader> };
	close($openssl_reader);

	waitpid($openssl_pid, 0) or die "Error: waitpid failed: $!";
	die "Error: 'openssl req' command failed - is $key_filename a valid key file?\n" unless $? == 0;

	return $csr_data;
}

sub extract_crt_from_file {
	my ($crt_filename, $outform) = @_;

	$outform //= 'DER';

	pipe(my $openssl_reader, my $openssl_writer) or die "Error: pipe failed: $!";
	my $openssl_pid = fork;
	die "Error: fork failed: $!" unless defined $openssl_pid;
	if ($openssl_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $openssl_writer) or die "Error: dup failed: $!";
		close($openssl_reader);
		exec('openssl', 'x509', '-in', $crt_filename, '-outform', $outform);
		die "Error: Unable to run 'openssl x509' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	close($openssl_writer);

	my $crt = do { local $/; <$openssl_reader> };
	close($openssl_reader);

	waitpid($openssl_pid, 0) or die "waitpid failed: $!";
	die "Error: 'openssl x509' command failed - is $crt_filename a valid certificate file?\n" unless $? == 0;

	return $crt;
}

sub extract_pubkey_from_key {
	my ($key_filename, $outform) = @_;

	$outform //= 'DER';

	# The pkey command is only available in OpenSSL 1.0.0 and higher. But older versions of
	# OpenSSL don't support ECC anyways, so it's OK to just use the rsa command instead.
	my $has_pkey_command = grep /^pkey$/, `openssl list-standard-commands`;
	my $pkey_command = $has_pkey_command ? 'pkey' : 'rsa';

	pipe(my $openssl_reader, my $openssl_writer) or die "Error: pipe failed: $!";
	my $openssl_pid = fork;
	die "Error: fork failed: $!" unless defined $openssl_pid;
	if ($openssl_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $openssl_writer) or die "Error: dup failed: $!";
		open(STDERR, '>', '/dev/null') if $pkey_command eq 'rsa'; # rsa command outputs spurious text to stderr
		close($openssl_reader);
		exec('openssl', $pkey_command, '-in', $key_filename, '-pubout', '-outform', $outform);
		die "Error: Unable to run 'openssl $pkey_command' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	close($openssl_writer);

	my $pubkey = do { local $/; <$openssl_reader> };
	close($openssl_reader);

	waitpid($openssl_pid, 0) or die "waitpid failed: $!";
	die "Error: 'openssl $pkey_command' command failed - is $key_filename a valid private key file?\n" unless $? == 0;

	return $pubkey;
}

sub extract_pubkey_from_crt {
	my ($crt_filename, $outform) = @_;

	$outform //= 'DER';

	pipe(my $x509_reader, my $x509_writer) or die "Error: pipe failed: $!";
	defined(my $x509_pid = fork) or die "Error: fork failed: $!";
	if ($x509_pid == 0) {
		open(STDIN, '<', '/dev/null');
		open(STDOUT, '>&', $x509_writer) or die "Error: dup failed: $!";
		close($x509_reader);
		exec('openssl', 'x509', '-in', $crt_filename, '-pubkey', '-noout');
		die "Error: Unable to run 'openssl x509' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
	}
	close($x509_writer);

	my ($pkey_pid, $pkey_command);
	my $pubkey;
	if ($outform eq 'PEM') {
		$pubkey = do { local $/; <$x509_reader> };
		close($x509_reader);
	} else {
		# The pkey command is only available in OpenSSL 1.0.0 and higher. But older versions of
		# OpenSSL don't support ECC anyways, so it's OK to just use the rsa command instead.
		my $has_pkey_command = grep /^pkey$/, `openssl list-standard-commands`;
		$pkey_command = $has_pkey_command ? 'pkey' : 'rsa';

		pipe(my $pkey_reader, my $pkey_writer) or die "Error: pipe failed: $!";
		defined($pkey_pid = fork) or die "Error: fork failed: $!";

		if ($pkey_pid == 0) {
			open(STDIN, '<&', $x509_reader) or die "Error: dup failed: $!";
			open(STDOUT, '>&', $pkey_writer) or die "Error: dup failed: $!";
			open(STDERR, '>', '/dev/null') if $pkey_command eq 'rsa'; # rsa command outputs spurious text to stderr
			close($pkey_reader);
			exec('openssl', $pkey_command, '-pubin', '-pubout', '-outform', $outform);
			die "Error: Unable to run 'openssl $pkey_command' command: " . ($!{ENOENT} ? 'openssl command not found' : $!) . "\n";
		}
		close($x509_reader);
		close($pkey_writer);

		$pubkey = do { local $/; <$pkey_reader> };
		close($pkey_reader);
	}

	# Wait for 'openssl x509' first, because that's the command that could fail because of user error.
	# (If 'openssl pkey' fails, it's because 'openssl x509' failed also, unless something really exceptional happened.)
	waitpid($x509_pid, 0) or die "waitpid failed: $!";
	die "Error: 'openssl x509' command failed - is $crt_filename a valid certificate file?\n" unless $? == 0;

	if (defined $pkey_pid) {
		waitpid($pkey_pid, 0) or die "waitpid failed: $!";
		die "Error: 'openssl $pkey_command' command failed\n" unless $? == 0;
	}

	return $pubkey;
}

sub wait_for_cert {
	my ($cn, $cert_instance_id, $timeout, $accept_dummy) = @_;

	my $start_time = time;
	my $warn_after = $start_time + int($timeout / 3);
	my $timeout_after = $start_time + $timeout;
	my $warned = 0;

	while (1) {
		my $now = time;
		my $poll;
		unless ($accept_dummy) {
			if ($now < $warn_after) {
				$poll = $warn_after - $now;
			} elsif ($now < $timeout_after) {
				$poll = $timeout_after - $now;
			} else {
				$poll = 0;
			}

			$poll = 180 if $poll > 180; # upper bound of 180 seconds
		}

		my ($status, $response) = authed_api_call('GET', '/certs/' . qs_escape($cn) . '/instances/' . $cert_instance_id, { poll => $poll, expand => ['crt','chain','root'] });
		exit 1 unless defined $response; # TODO: repeat ? b/c this could be a timeout situation

		if ($status == 200) {
			return ($response->{crt}, format_chain($response->{chain}), $response->{root}, $response->{state} eq 'active' ? 0 : 1);
		} elsif ($response->{reason} eq 'not_ready') {
			my $now = time;
			my $retry_after = $response->{retry_after} // 5;
			if ($now < $warn_after) {
				$retry_after = 1 if $retry_after < 1; # lower bound of 1 second
			} elsif ($now < $timeout_after) {
				if (not $warned) {
					print "Sorry, your certificate isn't ready yet. I'll keep waiting, but if you'd rather do this later, you can hit Ctrl+C and we'll send you an email when it's ready.\n";
					$warned = 1;
				}
				$retry_after = 10 if $retry_after < 10; # lower bound of 10 seconds
			} else {
				# Timed out
				print STDERR "Sorry, your certificate still isn't ready. We'll send you an email when it's ready.\n";
				return;
			}
			sleep($retry_after);
		} else {
			print STDERR "Error: " . $response->{message} . "\n";
			exit 1;
		}
	}
}

sub format_money {
	my ($amount) = @_;
	return sprintf("%.2f", $amount / 100);
}

sub prompt_for_approval {
	my ($hostname, $approval_methods) = @_;

	print "To prove that you are authorized to obtain a certificate for $hostname,\n";
	print "you must respond to an email sent to one of the following addresses, or add\n";
	print "a DNS record to your domain.\n";
	print "\n";
	print "How would you like to prove authorization?\n";
	print "\n";

	my @choices;
	if ($approval_methods->{email}->{available}) {
		for my $email (@{$approval_methods->{email}->{addresses}}) {
			my $i = int(@choices) + 1;
			print "$i. $email\n";
			push @choices, [ 'email', $email ];
		}
	}
	if ($approval_methods->{dns}->{available}) {
		my $i = int(@choices) + 1;
		print "$i. Add a DNS record\n";
		push @choices, [ 'dns' ];
	}

	my $num_choices = int(@choices);
	my $mesg = "Enter 1-$num_choices (or q to quit): ";

	while (1) {
		my $answer = prompt_user($mesg);
		if ($answer eq 'q') {
			return;
		} elsif ($answer =~ /^\d+$/ and $answer >= 1 and $answer <= $num_choices) {
			return @{$choices[$answer - 1]};
		} else {
			print "That is not a number between 1 and $num_choices.\n";
		}
	}
}

sub product_info_to_approval_methods {
	my ($product_info) = @_;
	my $approval_methods = {};
	for my $method (@{$product_info->{approval_methods}}) {
		$approval_methods->{$method}->{available} = 1;
		if ($method eq 'email') {
			$approval_methods->{email}->{addresses} = $product_info->{approver_emails};
		}
	}
	return $approval_methods;
}

sub order_can_be_paid {
	my ($product_info) = @_;

	if ($product_info->{price}->{currency} ne "USD") {
		print STDERR "Error: this version of the sslmate command does not support non-USD currencies. Please download a new version as per the instructions at https://sslmate.com.\n";
		return 0;
	}
	if ($product_info->{payment} && $product_info->{payment}->{method} eq 'none') {
		print STDERR "Error: your account has no payment method on file. Please visit https://sslmate.com/account to update your account.\n";
		return 0;
	}
	if ($product_info->{payment} && $product_info->{payment}->{method} eq 'credit_card' && $product_info->{payment}->{credit_card}->{expired}) {
		print STDERR "Error: your credit card is expired. Please visit https://sslmate.com/account to update your credit card.\n";
		return 0;
	}
	return 1;
}

sub prompt_for_order_confirmation {
	my ($product_info, %other_info) = @_;

	my $cn = $product_info->{cn};

	print "\n";
	print "============ Order summary ============\n";
	my @sans;
	if (defined $other_info{sans}) {
		for my $san (@{$other_info{sans}}) {
			push @sans, $san->{value} if $san->{type} eq 'dns';
		}
	} else {
		push @sans, grep { $_ ne $cn } @{$product_info->{default_sans}};
	}
	if (@sans) {
		print "      Hostnames: ";
	} else {
		print "       Hostname: ";
	}
	print $cn . "\n";
	for my $san (@sans) {
		print "                 " . $san . "\n";
	}
	if (defined $product_info->{years}) {
		print "        Product: " . $product_info->{description} . "\n";
		print "          Price: " . format_money($product_info->{price}->{base_price}) . " / year\n";
		print "          Years: " . $product_info->{years} . "\n";
	} else {
		print "        Product: " . $product_info->{description} . "\n";
		print "          Price: " . format_money($product_info->{price}->{base_price}) . "\n";
	}
	if (defined $other_info{auto_renew}) {
		print "     Auto-Renew: " . ($other_info{auto_renew} ? "Yes" : "No") . "\n";
	}
	if (defined $other_info{approval_methods}) {
		if (defined $other_info{sans}) {
			my $first = 1;
			for my $hostname ($cn, @sans) {
				if ($first) {
					print "Approval Method: ";
					$first = 0;
				} else {
					print "                 ";
				}
				my $approval_method = $other_info{approval_methods}->{$hostname};
				if ($approval_method eq 'email') {
					print "Email to " . $other_info{approver_emails}->{$hostname} . " (for $hostname)\n";
				} else {
					print format_approval_method($approval_method) . " (for $hostname)\n";
				}
			}
		} else {
			my $approval_method = $other_info{approval_methods}->{$cn};
			if ($approval_method eq 'email') {
				print " Approver Email: " . $other_info{approver_emails}->{$cn} . "\n";
			} else {
				print "Approval Method: " . format_approval_method($approval_method) . "\n";
			}
		}
	}
	print "\n";
	print "=========== Payment details ===========\n";
	if ($product_info->{price}->{amount_due}) {
		print "Payment Method: ";
		if ($product_info->{payment}->{method} eq 'credit_card') {
			my $card = $product_info->{payment}->{credit_card};
			print $card->{type} . " ending in " . $card->{last4};
		} elsif ($product_info->{payment}->{method} eq 'balance') {
			print "Account Balance";
		} else {
			print "Other";
		}
		print "\n";
	}
	if ($product_info->{price}->{discount}) {
		print "      Discount: " . format_money($product_info->{price}->{discount}) . " (USD)\n";
	}
	print "    Amount Due: " . format_money($product_info->{price}->{amount_due}) . " (USD)\n";
	print "\n";

	while (1) {
		my $answer = prompt_user('Press ENTER to confirm order (or q to quit): ');
		if ($answer eq '') {
			return 1;
		} elsif ($answer eq 'q') {
			return 0;
		}
	}
}

sub get_product_info {
	my ($type, $cn, %other_info) = @_;

	my ($status, $response) = authed_api_call('GET', '/products/' . qs_escape($type), { cn => $cn, %other_info });
	return undef unless defined $response;

	if ($status != 200) {
		print STDERR "Error: " . $response->{message} . "\n";
		return undef;
	}
	return $response;
}

sub get_approval_methods {
	my ($type, $cn, %other_info) = @_;

	my ($status, $response) = authed_api_call('GET', '/products/' . qs_escape($type) . '/approval', { cn => $cn, %other_info });
	return undef unless defined $response;

	if ($status != 200) {
		print STDERR "Error: " . $response->{message} . "\n";
		return undef;
	}
	return $response;
}

sub do_link {
	my ($persistent) = @_;

	print "If you don't have an account yet, visit https://sslmate.com/signup\n";
	my $username = prompt_user("Enter your SSLMate username: ");
	my $password = prompt_password("Enter your SSLMate password: ");
	return 0 if not defined $password;
	if ($persistent) {
		print "Linking account... ";
	} else {
		print "Authenticating... ";
	}
	STDOUT->flush;

	my ($status, $response) = api_call('GET', '/api_credentials', { username => $username, password => $password });
	return 0 if not defined $response;

	if ($status == 200) {
		if ($persistent) {
			$personal_config{api_key} = $response->{api_key};
			save_config;
		} else {
			$ephemeral_config{api_key} = $response->{api_key};
		}
		print "Done.\n";
		unless ($persistent) {
			print "Tip: if you don't want to have to type your password every time, you can run 'sslmate link' to link this system with your account.\n\n";
		}
		return 1;
	} else {
		print STDERR "Error: " . $response->{message} . "\n";
		return 0;
	}
}

sub command_link {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate link\n";
		return 0;
	} elsif (@ARGV > 0) {
		print STDERR "Error: sslmate link takes no arguments.\n";
		print STDERR "Usage: sslmate link\n";
		return 2;
	}

	load_config;

	print "Note: sslmate has already been linked with an account.\nContinue to link it with a different account, or press Ctrl+C to exit.\n" if is_linked;

	do_link(1) or return 1;

	return 0;
}

sub do_wait_for_cert {
	my ($cn, $cert_instance, $timeout, $accept_dummy, $paths, $new_key_filename) = @_;

	if ($cert_instance->{approval_method} eq 'email') {
		print "You will soon receive an email at " . $cert_instance->{approver_email} . " from " . $cert_instance->{approval_email_from} . ". Follow the instructions in the email to verify your ownership of your domain.\n\n";

		if ($timeout == 0) {
			print "Once you've verified ownership, you will be able to download your certificate with the 'sslmate download' command.\n";
			return 0;
		} elsif ($accept_dummy) {
			print "Once you've verified ownership, you will be able to download your certificate with the 'sslmate download' command.  In the meantime, you can configure your server with the temporary certificate, but this certificate will NOT be trusted by clients.\n";
		} else {
			print "Once you've verified ownership, your certificate will be automatically downloaded.  If you'd rather do this later, press Ctrl+C and download your certificate with the 'sslmate download' command instead.\n\n";
		}
	}

	print "Waiting for ownership confirmation...\n" unless $accept_dummy;

	my ($crt, $chain_crt, $root_crt, $is_dummy) = wait_for_cert($cn, $cert_instance->{id}, $timeout, $accept_dummy) or return 0;
	$chain_crt //= '';

	write_cert_files($paths, $new_key_filename, $crt, $chain_crt, $root_crt);

	print "\n";

	my ($key_status, $cert_status);
	if (! -f $paths->{key}) {
		$key_status = 'missing';
	}
	if ($is_dummy) {
		print "A temporary, self-signed, certificate has been downloaded.\n\n";
		$cert_status = 'temporary';
	} else {
		print "Your certificate is ready for use!\n\n";
	}

	print_cert_paths($paths, $key_status, $cert_status);
	#print "\n";
	#print "(" . $paths->{chained} . " is the concatenation of " . $paths->{crt} . " and " . $paths->{chain} . "; consult your program's documentation to determine whether you specify the certificate and chain in separate files or in one file.)\n";
	print "\n";
	print "Tip: generate configuration for this cert with the 'sslmate mkconfig' command.\n";
	print "Tip: test this cert's installation by running 'sslmate test $cn'.\n";
	return 1;
}

sub parse_approval_arg {
	my ($default_ref, $map_ref) = @_;

	return sub {
		my (undef, $opt_value) = @_;
		if ($opt_value =~ /^([^=]*)=(.*)$/) {
			$map_ref->{$1} = $2;
		} else {
			$$default_ref = $opt_value;
		}
	};
}

sub command_buy {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate buy [OPTIONS] HOSTNAME ...\n\n";
		print "Example: sslmate buy www.example.com\n";
		print "         sslmate buy '*.example.com'\n";
		print "         sslmate buy www.example.com dev.example.com www.example.org\n";
		print "\n";
		print "Common options:\n";
		print " --auto-renew         automatically renew this certificate before it expires\n";
		print " --no-auto-renew      don't automatically renew this certificate\n";
		print " --ev                 purchase an extended validation (EV) certificate\n";
		print " --coupon=CODE        use the given coupon code for a discount\n";
		print " --invoice-note=NOTE  include the given note with the invoice\n";
		print " --email-invoice-to=ADDRESS\n";
		print "                      email an invoice to the given address\n";
		print "\n";
		print "Batch options:\n";
		print " --approval=METHOD    use the given approval method (email or dns)\n";
		print " --approval=HOSTNAME=METHOD\n";
		print "                      use the given approval method for given hostname\n";
		print " --email=ADDRESS      use the given approver email address\n";
		print " --email=HOSTNAME=ADDRESS\n";
		print "                      use the given approver email address for given hostname\n";
		print " --timeout=SECONDS    wait at most SECONDS seconds for cert to be issued\n";
		print " --no-wait            return immediately; don't wait for cert to be issued\n";
		print " --temp               return immediately with a temporary certificate\n";
		print "\n";
		print "Advanced options:\n";
		print " -f, --force          replace existing files, certificates\n";
		print " --key-type=TYPE      type of key to generate ('rsa' or 'ecdsa')\n";
		print " --multi              buy multi-hostname cert even if only one hostname specified\n";
		return 0;
	}

	my %opts;
	my $auto_renew = undef;
	my $force = 0;
	my $cert_type = undef;
	my $ev = 0;
	my $default_approval_method = 'email';
	my %approval_methods;
	my $default_approver_email = undef;
	my %approver_emails;
	my @sans;
	my $timeout = undef;
	my $no_wait = 0;
	my $accept_dummy = 0;
	my $multi = 0;
	my $key_type = undef;
	my $csr_path = undef;
	my $days = undef;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('auto-renew!', \$auto_renew,
			    'force|f', \$force,
			    'batch', \$batch, # compat with pre-1.0
			    'type=s', \$cert_type,
			    'ev', \$ev,
			    'approval=s', parse_approval_arg(\$default_approval_method, \%approval_methods),
			    'email=s', parse_approval_arg(\$default_approver_email, \%approver_emails),
			    'days=i', \$days,
			    'timeout=i', \$timeout,
			    'no-wait', \$no_wait,
			    'temp', \$accept_dummy,
			    'multi', \$multi,
			    'key-type=s', \$key_type,
			    'csr=s', \$csr_path,
			    'coupon=s', \$opts{coupon_code},
			    'email-invoice-to=s', \$opts{email_invoice_to},
			    'invoice-note=s', \$opts{invoice_note}) or return 2;

	if (@ARGV < 1) {
		print STDERR "Error: you must specify the hostname for the certificate.\n";
		print STDERR "Example: sslmate buy www.example.com\n";
		print STDERR "     or: sslmate buy '*.example.com'\n";
		print STDERR "See 'sslmate help buy' for help.\n";
		return 2;
	}
	if ($no_wait && $accept_dummy) {
		print STDERR "Error: --no-wait and --temp are mutually exclusive.\n";
		return 2;
	}
	if (defined($timeout) && $accept_dummy) {
		print STDERR "Error: --timeout and --temp are mutually exclusive.\n";
		return 2;
	}
	if (defined($timeout) && $no_wait) {
		print STDERR "Error: --timeout and --no-wait are mutually exclusive.\n";
		return 2;
	}
	$timeout = 0 if $no_wait;
	if ($ev && !defined($timeout) && !$accept_dummy) {
		# --ev implies --temp, unless --timeout or --temp were already specified
		$accept_dummy = 1;
	}
	$timeout //= $DEFAULT_CERT_TIMEOUT;

	my $csr = undef;
	if (defined $csr_path) {
		my $csr_fh;
		if (!open($csr_fh, '<', $csr_path)) {
			print STDERR "Error: $csr_path: $!\n";
			return 1;
		}
		$csr = do { local $/; <$csr_fh> };
		close($csr_fh);
	}

	my $cn = lc shift @ARGV;
	push @sans, map(lc, @ARGV);
	$cert_type //= $ev ? 'ev' : 'dv';
	$multi = 1 if @sans;
	validate_cn($cn) or return 1;

	load_config;
	if (!is_linked) {
		if ($batch || not(-t STDIN)) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}

	init_default_paths;

	# Future TODO: support reusing key file if one already exists

	#
	# 0. Retrieve the current cert object from the server
	#
	my ($status, $cert);
	($status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn), { expand => ['current','pending','csr'] }) or return 1;
	if ($status != 200) {
		print STDERR "Error: " . $cert->{message} . "\n";
		return 1;
	}
	$cn = $cert->{cn}; # So that we use the canonical CN

	if (!$cert->{dn}) {
		print STDERR "Error: your account has no contact details set. Please visit https://sslmate.com/account to update your account.\n";
		return 1;
	}
	#
	# Make sure files/certs don't already exist
	#
	my $paths = get_cert_paths($cn);
	unless ($force) {
		my $errors = 0;

		$errors += has_existing_files(@{$paths}{keys %$paths});
		if (defined($cert->{pending})) {
			print STDERR "Error: a certificate for $cn is already pending issuance.\n";
			print STDERR "Tip: to change a certificate's approval method, use 'sslmate edit'.\n";
			print STDERR "Tip: to restart a certificate's approval process, use 'sslmate retry-approval'.\n";
			$errors++;
		}
		if (defined($cert->{current}) && !$cert->{current}->{expiring}) {
			print STDERR "Error: your account already has an active certificate for $cn.\n";
			print STDERR "Tip: to rekey this certificate, run 'sslmate rekey $cn'.\n";
			$errors++;
		}
		if ($errors) {
			print STDERR "Tip: use --force to override the above error" . ($errors == 1 ? "" : "s") . ".\n";
			return 1;
		}
	}

	#
	# Get product/pricing info from server
	#
	# This is done mainly to prompt the user for confirmation and provide early
	# error detection, so elide it when in batch mode to reduce roundtrips to server.
	#
	my $product_info;
	my $authorized_charge;
	my $authorized_charge_currency;

	unless ($batch) {
		$product_info = get_product_info($cert_type // $cert->{type}, $cn,
						 days => $days,
						 coupon_code => $opts{coupon_code},
						 sans => $multi ? int(@sans) : undef) or return 1;

		return 1 unless order_can_be_paid($product_info);

		$authorized_charge = $product_info->{price}->{amount_due};
		$authorized_charge_currency = $product_info->{price}->{currency};
	}

	#
	# Determine the approval method for the CN, and each SAN
	#
	for my $hostname ($cn, @sans) {
		my $method = $approval_methods{$hostname} // $default_approval_method;
		my $email = $approver_emails{$hostname} // $default_approver_email;

		if ($method eq 'email' && not defined($email)) {
			if ($batch) {
				print STDERR "Error: no approver email address specified for $cn.\n";
				print STDERR "Tip: in batch mode, you must specify approver email addresses with --email.\n";
				print STDERR "See 'sslmate help buy' for help.\n";
				return 2;
			}

			my $approval_methods = get_approval_methods($cert_type // $cert->{type}, $hostname) or return 1;
			($method, $email) = prompt_for_approval($hostname, $approval_methods) or return 1;
		}

		$approval_methods{$hostname} = $method;
		$approver_emails{$hostname} = $email;
	}

	#
	# Construct a list of SAN objects
	#
	my @san_objs;
	for my $san_hostname (@sans) {
		push @san_objs, { type => 'dns',
				  value => $san_hostname,
				  approval_method => $approval_methods{$san_hostname},
				  approver_email => $approver_emails{$san_hostname} };
	}

	#
	# Ask user for confirmation, if not in batch mode
	#
	unless ($batch) {
		prompt_for_order_confirmation($product_info,
					      sans => $multi ? \@san_objs : undef,
					      approver_emails => \%approver_emails,
					      approval_methods => \%approval_methods,
					      auto_renew => $auto_renew // $cert->{auto_renew}) or return 1;

	}

	#
	# Generate key/CSR
	#
	my $key_file;
	my $exit_with_error = sub {
		unlink($paths->{key}) if defined($key_file);
		authed_api_call('POST', '/certs/' . qs_escape($cn), undef, object_subset($cert, qw/type approval_method approver_email sans csr/), 'application/json') if defined($cert) && $cert->{exists};
		exit 1;
	};
	unless (defined $csr) {
		$key_file = open_key_file($paths->{key}, $force);

		print "Generating private key... "; STDOUT->flush;
		truncate($key_file, 0);
		genkey($key_file, $key_type) or $exit_with_error->();
		close($key_file);
		print "Done.\n";

		print "Generating CSR... "; STDOUT->flush;
		$csr = openssl_req($paths->{key}, $cert->{dn});
		print "Done.\n";
	}

	#
	# Create/update the cert object on the server
	#
	my $request = {	auto_renew	=> to_json_bool($auto_renew // $cert->{auto_renew}),
			type		=> $cert_type,
			approval_method	=> $approval_methods{$cn},
			approver_email	=> $approver_emails{$cn},
			sans		=> $multi ? \@san_objs : undef,
			csr		=> $csr };

	($status, my $new_cert) = authed_api_call('POST', '/certs/' . qs_escape($cn), { expand => ['dns_approval.status','http_approval.status'] }, $request, 'application/json') or $exit_with_error->();
	if ($status != 200) {
		print STDERR "Error: " . $new_cert->{message} . "\n";
		$cert = undef;
		$exit_with_error->();
	}

	prepare_approval($cert, $new_cert) or $exit_with_error->();

	#
	# Buy the certificate from SSLMate
	#
	print "Placing order...\n";
	$request = { days			=> $days,
		     authorized_charge		=> $authorized_charge,
		     authorized_charge_currency	=> $authorized_charge_currency,
		     %opts };

	my $cert_instance;
	($status, $cert_instance) = authed_api_call('POST', '/certs/' . qs_escape($cn) . '/buy', undef, $request) or $exit_with_error->();
	if ($status != 200 && $cert_instance->{reason} eq 'daily_buy_limit_exceeded') {
		print STDERR "Error: this purchase would exceed your daily spending limit.\n";
		print STDERR "To change your limit, visit https://sslmate.com/account\n";
		$exit_with_error->() if $batch;
		my $password = prompt_password("Enter your SSLMate password to approve this purchase: ");
		my ($account_id, undef) = split('_', get_config('api_key'));
		($status, $cert_instance) = api_call('POST', '/certs/' . qs_escape($cn) . '/buy', { username => $account_id, password => $password }, undef, $request) or $exit_with_error->();
	}

	if ($status != 200) {
		if ($cert_instance->{reason} eq 'price_not_authorized') {
			print STDERR "Error: the price of this certificate has changed. Please run 'sslmate buy' again.\n";
		} else {
			print STDERR "Error: " . $cert_instance->{message} . "\n";
		}
		$exit_with_error->();
	}

	print "Order complete.\n\n";
	if (!do_wait_for_cert($cn, $cert_instance, $timeout, $accept_dummy, $paths, $paths->{key})) {
		print "\n";
		print_cert_paths($paths, undef, 'pending');
		return 12 unless $timeout == 0;
	}
	return 0;
}

sub command_reissue_rekey {
	my $command = shift;
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate $command [OPTIONS] HOSTNAME\n\n";
		print "Example: sslmate $command www.example.com\n";
		if ($command eq 'reissue') {
			print "\n";
			print "Options:\n";
			print " --same-key         reissue with the same key instead of generating a new key\n";
		}
		print "\n";
		print "Batch options:\n";
		print " --timeout=SECONDS  wait at most SECONDS seconds for new cert to be issued\n";
		print " --no-wait          return immediately; don't wait for new cert to be issued\n";
		if ($command eq 'rekey') {
			print "\n";
			print "Advanced options:\n";
			print " -f, --force        overwrite existing files\n";
			print " --key-type=TYPE    type of key to generate ('rsa' or 'ecdsa')\n";
		}
		return 0;
	}

	my $force = 0;
	my $same_key = 0;
	my $timeout = undef;
	my $no_wait = 0;
	my $key_type = undef;
	my $csr_path = undef;

	my @optspec = ('timeout=i', \$timeout,
		       'no-wait', \$no_wait);
	#if ($command eq 'rekey') { Uncomment with SSLMate 2.0
		push @optspec, ('force|f', \$force,
				'csr=s', \$csr_path,
				'key-type=s', \$key_type);
	#}
	if ($command eq 'reissue') {
		push @optspec, ('same-key', \$same_key);
	}

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions(@optspec) or return 2;

	if (@ARGV != 1) {
		print STDERR "Error: you must specify the hostname of the certificate to $command.\n";
		print STDERR "Example: sslmate $command www.example.com\n";
		return 2;
	}
	my $rekey = 0;
	if ($command eq 'rekey') {
		$rekey = 1;
	} elsif ($command eq 'reissue') {
		unless ($same_key) {
			$rekey = 1;
			print STDERR "Warning: 'sslmate reissue' without the --same-key option is deprecated.\n";
			print STDERR "Starting with SSLMate 2.0, --same-key will be implied.  To reissue\n";
			print STDERR "a certificate with a new key, use 'sslmate rekey' instead.\n";
			sleep(3) unless $batch;
			print STDERR "\n";
		}
	}

	if (defined($timeout) && $no_wait) {
		print STDERR "Error: --timeout and --no-wait are mutually exclusive.\n";
		return 2;
	}
	$timeout = 0 if $no_wait;

	my $csr = undef;
	if (defined $csr_path) {
		my $csr_fh;
		if (!open($csr_fh, '<', $csr_path)) {
			print STDERR "Error: $csr_path: $!\n";
			return 1;
		}
		$csr = do { local $/; <$csr_fh> };
		close($csr_fh);
	}

	my $cn = lc $ARGV[0];
	validate_cn($cn) or return 1;

	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}

	init_default_paths;

	# 1. Retrieve the current cert object from the server
	my ($cert_status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn), { 'expand' => [ 'current', 'pending', 'csr' ] }) or return 1;
	if ($cert_status != 200) {
		print STDERR "Error: " . $cert->{message} . "\n";
		return 1;
	}
	$cn = $cert->{cn}; # So that we use the canonical CN

	if (!$cert->{dn}) {
		print STDERR "Error: your account has no contact details set. Please visit https://sslmate.com/account to update your account.\n";
		exit 1;
	}

	if (not $cert->{exists}) {
		print STDERR "Error: $cn: there is no certificate in your account with this common name.\n";
		return 1;
	}
	if (not defined $cert->{current}) {
		print STDERR "Error: this certificate is not active.  Only active (non-expired, non-pending) certs may be reissued.  Please purchase a new certificate with 'sslmate buy'.\n";
		return 1;
	}
	if ($cert->{current}->{source} eq 'import') {
		print STDERR "Error: this certificate was not purchased from SSLMate.  To reissue this cert, please see the vendor where you purchased it.\n";
		return 1;
	}

	# 2. Open the files
	my $paths = get_cert_paths($cn);
	my $new_key_filename = undef;

	my $exit_with_error = sub { exit 1; };

	if ($rekey) {
		$exit_with_error = sub {
			unlink($new_key_filename) if defined($new_key_filename);
			authed_api_call('POST', '/certs/' . qs_escape($cn), undef, object_subset($cert, qw/csr/)) if defined($cert);
			exit 1;
		};

		unless (defined $csr) {
			$new_key_filename = $paths->{key} . ".new";
			unless ($force) {
				exit 1 if has_existing_files($new_key_filename);
			}
			my $key_file = open_key_file($new_key_filename, $force);

			# 3. Generate new key and CSR
			print "Generating private key... "; STDOUT->flush;
			truncate($key_file, 0);
			genkey($key_file, $key_type) or $exit_with_error->();
			close($key_file);
			print "Done.\n";

			print "Generating CSR... "; STDOUT->flush;
			$csr = openssl_req($new_key_filename, $cert->{dn});
			print "Done.\n";
		}

		# 4. Update the CSR server-side
		($cert_status, my $new_cert) = authed_api_call('POST', '/certs/' . qs_escape($cn), { expand => ['dns_approval.status','http_approval.status'] }, { csr => $csr }) or $exit_with_error->();
		if ($cert_status != 200) {
			print STDERR "Error: " . $new_cert->{message} . "\n";
			$cert = undef;
			$exit_with_error->();
		}
		prepare_approval($cert, $new_cert) or $exit_with_error->();
	}

	# 5. Reissue the certificate
	print "Reissuing cert...\n";
	my ($reissue_status, $cert_instance) = authed_api_call('POST', '/certs/' . qs_escape($cn) . '/reissue') or $exit_with_error->();
	if ($reissue_status != 200) {
		print STDERR "Error: " . $cert_instance->{message} . "\n";
		$exit_with_error->();
	}

	$timeout //= ($cert_instance->{type} eq 'ev' ? 0 : $DEFAULT_CERT_TIMEOUT);

	print "Reissue complete.\n\n";
	if (!do_wait_for_cert($cn, $cert_instance, $timeout, 0, $paths, $new_key_filename)) {
		print "\n";
		if (config_has('key_directory') && config_has('cert_directory')) {
			print "Note: the new private key has been temporarily saved in $new_key_filename and will be installed automatically when 'sslmate download' downloads the new certificate.\n" if defined $new_key_filename;
		} else {
			print_cert_paths({ %$paths, (defined $new_key_filename ? (key => $new_key_filename) : ()) }, undef, 'pending');
		}
		return 12 unless $timeout == 0;
	}
	return 0;
}

sub command_revoke {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate revoke [OPTIONS] HOSTNAME\n\n";
		print "Example: sslmate revoke www.example.com\n";
		print "\n";
		print "Valid options:\n";
		print " -a, --all    revoke ALL certificates, even the most recent\n";
		print "\n";
		print "Note: By default, 'sslmate revoke' revokes all but the most recent certificate.\n";
		print "      To revoke even the most recent certificate, use the --all option.\n";
		print "\n";
		print "Tip: To replace a compromised key, first rekey the cert with 'sslmate rekey'\n";
		print "     and then revoke the old cert(s) with 'sslmate revoke' WITHOUT --all.\n";
		return 0;
	}

	my $all = 0;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('all|a', \$all) or return 2;

	if (@ARGV != 1) {
		print STDERR "Error: you must specify the hostname of the certificate to revoke.\n";
		print STDERR "Example: sslmate revoke www.example.com\n";
		return 2;
	}

	my $cn = lc $ARGV[0];
	validate_cn($cn) or return 1;

	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	if ($all && !$batch) {
		print "WARNING: ALL instances of this certificate will be revoked, even the most\n";
		print "recent one.  You will not be able to use or reissue this certificate unless\n";
		print "you purchase it again.\n\n";
		if (-t STDIN) {
			print "Do you understand and want to continue?\n\n";
			exit 1 unless prompt_yesno();
		} else {
			print "Error: will not continue unless --batch global option is specified.\n";
			exit 1;
		}
	}

	print "Revoking cert...\n";

	my ($status, $response) = authed_api_call('POST', '/certs/'.qs_escape($cn).'/revoke', undef, { all => $all }) or return 1;

	if ($status != 200) {
		print STDERR "Error: " . $response->{message} . "\n";
		return 1;
	}

	if ($response->{num_imported} > 0) {
		print STDERR "Error: the certificate for '$cn' was imported.\n";
		print STDERR "To revoke this cert, please contact the vendor where you purchased it.\n";
		return 1;
	} elsif ($response->{num_revoked} == 0 && $response->{num_active} == 0) {
		print STDERR "Error: your account contains no active certificates for '$cn'.\n";
		return 1;
	} elsif ($response->{num_revoked} == 0) {
		print STDERR "Error: the certificate for '$cn' is still in use.\n";
		print STDERR "Before revoking this certificate, please rekey it by running 'sslmate rekey $cn'.  Alternatively, if you want to revoke this certificate even though it's still in use, pass the -a option to 'sslmate revoke'.\n";
		return 1;
	} else {
		print "Successfully revoked the certificate for $cn.\n";
		print "Please allow up to two business days for this revocation to be processed. You will receive an email when this revocation is complete.\n";
		return 0;
	}
}

sub command_renew {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate renew [OPTIONS] HOSTNAME\n\n";
		print "Example: sslmate renew www.example.com\n";
		print "         sslmate renew '*.example.com'\n";
		print "\n";
		print "Common options:\n";
		print " --coupon=CODE        use the given coupon code for a discount\n";
		print " --invoice-note=NOTE  include the given note with the invoice\n";
		print " --email-invoice-to=ADDRESS\n";
		print "                      email an invoice to the given address\n";
		print "\n";
		print "Batch options:\n";
		print " --timeout=SECONDS    wait at most SECONDS seconds for new cert to be issued\n";
		print " --no-wait            return immediately; don't wait for new cert to be issued\n";
		print "\n";
		print "Advanced options:\n";
		print " -f, --force          replace existing files, certificates\n";
		return 0;
	}

	my %opts;
	my $force = 0;
	my $timeout = undef;
	my $days = undef;
	my $no_wait = 0;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('force|f', \$force,
			    'batch', \$batch, # compat with pre-1.0
			    'days=i', \$days,
			    'timeout=i', \$timeout,
			    'no-wait', \$no_wait,
			    'coupon=s', \$opts{coupon_code},
			    'email-invoice-to=s', \$opts{email_invoice_to},
			    'invoice-note=s', \$opts{invoice_note}) or return 2;

	if (@ARGV != 1) {
		print STDERR "Error: you must specify the hostname for the certificate.\n";
		print STDERR "Example: sslmate renew www.example.com\n";
		print STDERR "     or: sslmate renew '*.example.com'\n";
		print STDERR "See 'sslmate help renew' for help.\n";
		return 2;
	}

	if (defined($timeout) && $no_wait) {
		print STDERR "Error: --timeout and --no-wait are mutually exclusive.\n";
		return 2;
	}
	$timeout = 0 if $no_wait;

	my $cn = $ARGV[0];
	validate_cn($cn) or return 1;

	load_config;
	if (!is_linked) {
		if ($batch || not(-t STDIN)) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}

	init_default_paths;

	# Retrieve the current cert object from the server
	my ($status, $cert);
	($status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn), { expand => ['current','pending','dns_approval.status','http_approval.status'] }) or exit 1;
	if ($status != 200) {
		print STDERR "Error: " . $cert->{message} . "\n";
		exit 1;
	}
	if (not $cert->{exists}) {
		print STDERR "Error: $cn: there is no certificate in your account with this common name.\n";
		return 1;
	}
	$cn = $cert->{cn}; # So that we use the canonical CN

	if (!$cert->{dn}) {
		print STDERR "Error: your account has no contact details set. Please visit https://sslmate.com/account to update your account.\n";
		exit 1;
	}

	my $paths = get_cert_paths($cn);
	my $pubkey_hash = undef;
	if (-f $paths->{key} && -r $paths->{key}) {
		$pubkey_hash = sha256_hex(extract_pubkey_from_key($paths->{key}));
	}

	unless ($force) {
		my $errors = 0;

		if (defined($cert->{pending})) {
			print STDERR "Error: a certificate for $cn is already pending issuance.\n";
			print STDERR "Tip: to change a certificate's approval method, use 'sslmate edit'.\n";
			print STDERR "Tip: to restart a certificate's approval process, use 'sslmate retry-approval'.\n";
			$errors++;
		}
		if (defined($cert->{current}) && !$cert->{current}->{expiring}) {
			print STDERR "Error: the certificate for $cn is not about to expire.\n";
			print STDERR "Tip: to reissue this certificate, run 'sslmate reissue $cn'.\n";
			$errors++;
		}
		if ($errors) {
			print STDERR "Tip: use --force to override the above error" . ($errors == 1 ? "" : "s") . ".\n";
			exit 1;
		}
	}

	my $authorized_charge;
	my $authorized_charge_currency;

	unless ($batch) {
		# Get product/pricing info from the server, ask user for confirmation

		my $product_info = get_product_info($cert->{type}, $cn,
						 days => $days,
						 coupon_code => $opts{coupon_code},
						 sans => defined($cert->{sans}) ? int(@{$cert->{sans}}) : undef) or return 1;

		exit 1 unless order_can_be_paid($product_info);

		my $approver_emails = { $cn => $cert->{approver_email} };
		my $approval_methods = { $cn => $cert->{approval_method} };
		if (defined $cert->{sans}) {
			for my $san (@{$cert->{sans}}) {
				if ($san->{type} eq 'dns') {
					$approver_emails->{$san->{value}} = $san->{approver_email};
					$approval_methods->{$san->{value}} = $san->{approval_method};
				}
			}
		}

		prompt_for_order_confirmation($product_info,
					      sans => $cert->{sans},
					      approver_emails => $approver_emails,
					      approval_methods => $approval_methods,
					      auto_renew => $cert->{auto_renew}) or exit 1;

		$authorized_charge_currency = $product_info->{price}->{currency};
		$authorized_charge = $product_info->{price}->{amount_due};
	}

	my $new_cert;
	my $exit_with_error;
	if (defined $pubkey_hash && $cert->{pubkey_hash} ne $pubkey_hash) {
		# The locally-installed key is out of sync with the key registered with the server.
		# So, generate a CSR from the locally-installed key and register it with the server.
		print "Generating CSR... "; STDOUT->flush;
		my $csr = openssl_req($paths->{key}, $cert->{dn});
		print "Done.\n";

		($status, $new_cert) = authed_api_call('POST', '/certs/' . qs_escape($cn), { expand => ['dns_approval.status','http_approval.status'] }, { csr => \$csr }) or exit 1;
		if ($status != 200) {
			print STDERR "Error: " . $new_cert->{message} . "\n";
			exit 1;
		}
		$exit_with_error = sub {
			authed_api_call('POST', '/certs/' . qs_escape($cn), undef, object_subset($cert, qw/csr/));
			exit 1;
		};
	} else {
		$new_cert = $cert;
		$exit_with_error = sub {
			exit 1;
		};
	}
	prepare_approval($cert, $new_cert) or $exit_with_error->();

	# Renew the certificate
	print "Placing order...\n";
	my $request = { days				=> $days,
			authorized_charge		=> $authorized_charge,
			authorized_charge_currency	=> $authorized_charge_currency,
			%opts };

	my $cert_instance;
	($status, $cert_instance) = authed_api_call('POST', '/certs/' . qs_escape($cn) . '/buy', undef, $request) or $exit_with_error->();
	if ($status != 200) {
		if ($cert_instance->{reason} eq 'price_not_authorized') {
			print STDERR "Error: the price of this certificate has changed. Please run 'sslmate renew' again.\n";
		} else {
			print STDERR "Error: " . $cert_instance->{message} . "\n";
		}
		$exit_with_error->();
	}

	$timeout //= ($cert_instance->{type} eq 'ev' ? 0 : $DEFAULT_CERT_TIMEOUT);

	print "Renewal complete.\n\n";
	if (!do_wait_for_cert($cn, $cert_instance, $timeout, 0, $paths)) {
		return 12 unless $timeout == 0;
	}
	return 0;
}

sub command_req {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate req [OPTIONS] HOSTNAME\n\n";
		print "Example: sslmate req www.example.com\n";
		print "         sslmate req '*.example.com'\n";
		print "\n";
		print "Valid options:\n";
		print " -f, --force        overwrite existing files\n";
		print " --key-file=FILE    write private key to FILE (- for stdout)\n";
		print " --csr-file=FILE    write CSR to FILE (- for stdout)\n";
		print " --key-type=TYPE    type of key to generate ('rsa' or 'ecdsa')\n";
		print " --dn NAME=VALUE    set the given DN attribute in the request\n";
		return 0;
	}

	my $force = 0;
	my $dn = {};
	my $key_type;
	my $key_filename;
	my $csr_filename;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('force|f', \$force,
			    'dn=s', $dn,
			    'key-file=s', \$key_filename,
			    'csr-file=s', \$csr_filename,
			    'key-type=s', \$key_type) or return 2;

	if (@ARGV != 1) {
		print STDERR "Error: you must specify the hostname for the certificate.\n";
		print STDERR "Example: sslmate req www.example.com\n";
		print STDERR "     or: sslmate req '*.example.com'\n";
		print STDERR "See 'sslmate help req' for help.\n";
		return 2;
	}

	my $cn = lc $ARGV[0];
	$key_filename //= "$cn.key";
	$csr_filename //= "$cn.csr";
	if (keys %$dn) {
		$dn->{C} //= 'US';
		$dn->{CN} = $cn;
	} else {
		$dn = make_dn($cn);
	}

	# 0. Open the files
	my $open_flags = O_WRONLY | O_CREAT | O_TRUNC;
	$open_flags |= O_EXCL unless $force;
	my $key_file;
	if ($key_filename eq '-') {
		open($key_file, '>&STDOUT');
	} else {
		if (!sysopen($key_file, $key_filename, $open_flags, 0600)) {
			print STDERR "Error: unable to open '$key_filename' for writing: $!\n";
			return 1;
		}
	}
	my $csr_file;
	if ($csr_filename eq '-') {
		open($csr_file, '>&STDOUT');
	} else {
		if (!sysopen($csr_file, $csr_filename, $open_flags, 0666)) {
			print STDERR "Error: unable to open '$csr_filename' for writing: $!\n";
			return 1;
		}
	}

	# 1. Generate a key
	print STDERR "Generating private key... "; STDERR->flush;
	genkey($key_file, $key_type) or return 1;
	close($key_file);
	print STDERR "$key_filename\n";

	# 2. Generate the CSR
	print STDERR "Generating CSR... "; STDERR->flush;
	print $csr_file openssl_req("$cn.key", $dn);
	close($csr_file);
	print STDERR "$csr_filename\n";

	return 0;
}

sub command_download {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate download [OPTIONS] HOSTNAME ...\n";
		print "Example: sslmate download www.example.com\n";
		print "         sslmate download --all\n";
		print "\n";
		print "Valid options:\n";
		print " -a, --all    download certificate for every key in the SSLMate keys directory\n";
		print " --temp       download a temporary certificate if real certificate is not ready\n";
		return 0;
	}

	my $all = 0;
	my $accept_dummy = 0;
	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('all|a', \$all,
			    'temp', \$accept_dummy) or return 2;

	if ((!$all && @ARGV == 0) || ($all && @ARGV > 0)) {
		print STDERR "Error: you must specify hostname(s) of certificate(s) to download OR use --all.\n";
		print STDERR "Example: sslmate download www.example.com\n";
		print STDERR "     or: sslmate download --all\n";
		return 2;
	}

	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}

	init_default_paths;

	if ($all) {
		# scan the key directory, populate @ARGV for every .key file
		my $key_directory = get_config('key_directory') // '.';
		opendir(my $dh, $key_directory) or die "Error: unable to read key directory: $key_directory: $!\n";
		my @cns = map { s/\.key$//; $_ } grep { /\.key$/ } readdir($dh);
		closedir($dh);

		if (defined(my $wildcard_filename = get_config('wildcard_filename'))) {
			@cns = map { s/^$wildcard_filename\./*./; $_ } @cns;
		}

		push @ARGV, @cns;

		if (@ARGV == 0) {
			print "No certificates to download.\n" unless $quiet;
			return 10;
		}
	}

	my @certs;
	my $errors = 0;
	for my $cn (@ARGV) {
		$cn = lc $cn;
		validate_cn($cn) or return 1;
		my ($status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn), { expand => ['current.crt', 'current.chain', 'current.root', 'pending.crt', 'pending.chain', 'pending.root'], _command => 'download' }) or return 1;
		if ($status != 200) {
			print STDERR "Error: " . $cert->{message} . "\n";
			return 1;
		} elsif (not $cert->{exists}) {
			print STDERR "Error: $cn: There is no certificate in your account with this name.\n";
			++$errors;
			next;
		}
		$cn = $cert->{cn}; # So that we use the canonical CN

		my $paths = get_cert_paths($cn);
		my ($pubkey_hash, $new_pubkey_hash);
		if (-f $paths->{key} && -r $paths->{key}) {
			$pubkey_hash = sha256_hex(extract_pubkey_from_key($paths->{key}));
		}
		if (-f $paths->{key} . ".new" && -r $paths->{key} . ".new") {
			$new_pubkey_hash = sha256_hex(extract_pubkey_from_key($paths->{key} . ".new"));
		}

		my ($crt, $chain, $root);
		my $is_dummy = 0;
		my $install_new_key = 0;
		my $key_is_out_of_date = 0;

		if ($cert->{current} && defined($new_pubkey_hash) && $cert->{current}->{pubkey_hash} eq $new_pubkey_hash) {
			$crt = $cert->{current}->{crt};
			$chain = format_chain($cert->{current}->{chain});
			$root = $cert->{current}->{root};
			$install_new_key = 1;
		} elsif ($cert->{current} && (!defined($pubkey_hash) || $cert->{current}->{pubkey_hash} eq $pubkey_hash)) {
			$crt = $cert->{current}->{crt};
			$chain = format_chain($cert->{current}->{chain});
			$root = $cert->{current}->{root};
		} elsif (!$cert->{current} && $cert->{pending} && (!defined($pubkey_hash) || $cert->{pending}->{pubkey_hash} eq $pubkey_hash)) {
			$crt = $cert->{pending}->{crt};
			$chain = format_chain($cert->{pending}->{chain});
			$root = $cert->{pending}->{root};
			$is_dummy = 1;
		} elsif (defined($pubkey_hash)) {
			my ($status, $response) = authed_api_call('GET', '/certs/'.qs_escape($cn).'/instances/pubkey_hash:'.qs_escape($pubkey_hash), { expand => ['crt', 'chain', 'root'], _command => 'download2' }) or return 1;
			if ($status != 200) {
				if ($response->{reason} eq 'no_such_pubkey_hash') {
					print STDERR "Error: $cn: There is no certificate in your account that matches the private key " . $paths->{key} . "\n";
					++$errors;
					next;
				} else {
					print STDERR "Error: $cn: " . $response->{message} . "\n";
					return 1;
				}
			}
			if ($response->{state} ne 'active' && $response->{state} ne 'pending') {
				print STDERR "Error: $cn (with private key " . $paths->{key} . "): is " . $response->{state} . "\n";
				++$errors;
				next;
			}
			$crt = $response->{crt};
			$chain = format_chain($response->{chain});
			$root = $response->{root};
			$is_dummy = 1 if $response->{state} ne 'active';
			$key_is_out_of_date = 1;
		} else {
			print STDERR "Error: $cn: This certificate is not active.\n";
			++$errors;
			next;
		}

		print STDERR "Warning: $cn: the key file for this certificate (" . $paths->{key} . ") is out-of-date (the certificate has been reissued with a newer key). You should install the new key and then re-run 'sslmate download' to download the corresponding certificate files.\n" if $key_is_out_of_date;

		$chain //= '';

		my $missing_files = 0;
		for my $type (keys %file_types) {
			if (defined $paths->{$type} && ! -e $paths->{$type}) {
				$missing_files = 1;
			}
		}
		if (not($missing_files) &&
				not($install_new_key) &&
				file_contents_are($paths->{crt}, $crt) &&
				file_contents_are($paths->{chain}, $chain) &&
				(not(defined $paths->{root}) || file_contents_are($paths->{root}, $root)) &&
				(not(defined $paths->{'chain+root'}) || file_contents_are($paths->{'chain+root'}, join('', $chain, $root)))) {
			# Files did not change
			next;
		}

		if ($is_dummy && (!$accept_dummy || $crt eq '')) {
			print STDERR "Error: The certificates for $cn" . (defined($pubkey_hash) ? " (with private key " . $paths->{key} . ")" : "") . " have not yet been issued. Please try again later.\n";
			++$errors;
			next;
		}

		push @certs, { cn => $cn,
			       paths => $paths,
			       crt => $crt,
			       chain => $chain,
			       root => $root,
			       is_dummy => $is_dummy,
			       install_new_key => $install_new_key,
			       key_is_out_of_date => $key_is_out_of_date,
			     };
	}

	if (@certs == 0) {
		return 1 if $errors;
		print "All certificate files already downloaded and up-to-date.\n" unless $quiet;
		return 10;
	}

	for my $cert (@certs) {
		my $cn = $cert->{cn};
		my $paths = $cert->{paths};
		write_cert_files($paths,
		                 $cert->{install_new_key} ? $paths->{key} . ".new" : undef,
		                 $cert->{crt},
		                 $cert->{chain},
				 $cert->{root});

		my ($key_status, $cert_status);
		if (! -f $paths->{key}) {
			$key_status = 'missing';
		} elsif ($cert->{key_is_out_of_date}) {
			$key_status = 'old';
		}
		if ($cert->{is_dummy}) {
			print "A temporary, self-signed, certificate for $cn has been downloaded.\n\n";
			$cert_status = 'temporary';
		} else {
			print "The certificate for $cn has been downloaded.\n\n";
		}
		print_cert_paths($paths, $key_status, $cert_status);
		print "\n";
	}
	if ($errors) {
		print "Some new certificates were downloaded, but other certificates had errors. See above.\n";
	}
	return 0;
}

sub print_row {
	my ($data, $widths) = @_;

	for my $i (0..@$data-1) {
		print "  " if $i > 0;
		printf "%*s", -$widths->[$i], ($data->[$i] // '-');
	}
	print "\n";
}

sub get_local_cert_info {
	my ($paths) = @_;

	my ($key_info, $crt_info);
	if (-e $paths->{key}) {
		eval {
			my $raw_pubkey = extract_pubkey_from_key($paths->{key});
			$key_info = {
				pubkey_hash => sha256_hex($raw_pubkey),
			};
		}
	}
	if (-e $paths->{crt}) {
		eval {
			my $raw_crt = extract_crt_from_file($paths->{crt});
			my $raw_pubkey = extract_pubkey_from_crt($paths->{crt});
			$crt_info = {
				sha1_fingerprint => sha1_hex($raw_crt),
				sha256_fingerprint => sha256_hex($raw_crt),
				pubkey_hash => sha256_hex($raw_pubkey),
			};
		}
	}

	return { key => $key_info, crt => $crt_info };
}

sub make_cert_info {
	my ($cert, $local_info) = @_;

	my $latest = $cert->{pending} // $cert->{current};

	my $local_status = 'none';
	if ($local_info->{crt}) {
		if (not($latest) || $local_info->{crt}->{sha256_fingerprint} ne ($latest->{sha256_fingerprint} // '')) {
			$local_status = 'out_of_date';
		} elsif (not $local_info->{key}) {
			$local_status = 'missing_key';
		} elsif ($local_info->{crt}->{pubkey_hash} ne $local_info->{key}->{pubkey_hash}) {
			$local_status = 'mismatched_key';
		} elsif ($latest->{state} eq 'pending') {
			$local_status = 'installed_temporary';
		} else {
			$local_status = 'installed';
		}
	} elsif ($local_info->{key}) {
		$local_status = 'missing_crt';
	}

	return {
		name => $cert->{cn},
		status => $latest ? $latest->{state} : undef,
		expiration => $latest ? $latest->{expiration} : undef,
		local_status => $local_status,
		fingerprint => $latest ? $latest->{sha1_fingerprint} : undef,
		sha256_fingerprint => $latest ? $latest->{sha256_fingerprint} : undef,
		auto_renew => int($cert->{auto_renew}),
		type => $latest ? $latest->{type} : $cert->{type},
		approver_email => $cert->{approver_email},
		approval_email_from => $latest ? $latest->{approval_email_from} : undef,
		approval_method => $cert->{approval_method},
		alt_names => $cert->{sans} ? [ map { $_->{value} } grep { $_->{type} eq 'dns' } @{$cert->{sans}} ] : undef,
	};
}

my %cert_column_titles = (
	name =>			'Name',
	status =>		'Status',
	expiration =>		'Expiration',
	local_status => 	'Local Status',
	fingerprint =>		'Fingerprint',
	sha256_fingerprint =>	'Fingerprint (SHA-256)',
	auto_renew =>		'Auto-renew',
	type =>			'Type',
	approver_email =>	'Approver Email',
	approval_email_from =>	'Approval Email From',
	approval_method =>	'Approval',
);

sub format_hash {
	my ($raw_hash) = @_;
	$raw_hash =~ s/([0-9a-zA-z][0-9a-zA-Z])/:\U$1\E/g;
	return substr($raw_hash, 1);
}
sub format_expiration {
	my ($seconds) = @_;
	return strftime('%Y-%m-%d', localtime($seconds));
}
sub format_status {
	my ($raw_status) = @_;
	return 'Active' if $raw_status eq 'active';
	return 'Expired' if $raw_status eq 'expired';
	return 'Revoked' if $raw_status eq 'revoked';
	return 'Pending' if $raw_status eq 'pending';
	return 'Canceled' if $raw_status eq 'canceled';
	return $raw_status;
}
sub format_local_status {
	my ($raw_local_status) = @_;
	return 'None' if $raw_local_status eq 'none';
	return 'Out-of-date' if $raw_local_status eq 'out_of_date';
	return 'No key file' if $raw_local_status eq 'missing_key';
	return 'Mismatched key' if $raw_local_status eq 'mismatched_key';
	return 'Temporary' if $raw_local_status eq 'installed_temporary';
	return 'Installed' if $raw_local_status eq 'installed';
	return 'No crt file' if $raw_local_status eq 'missing_crt';
	return $raw_local_status;
}
sub format_type {
	my ($raw_type) = @_;
	return 'DV' if $raw_type eq 'dv';
	return 'EV' if $raw_type eq 'ev';
	return $raw_type;
}
sub format_yesno {
	my ($value) = @_;
	return $value ? 'Yes' : 'No';
}
my %cert_column_formatters = (
	expiration => \&format_expiration,
	status => \&format_status,
	local_status => \&format_local_status,
	fingerprint => \&format_hash,
	sha256_fingerprint => \&format_hash,
	auto_renew => \&format_yesno,
	type => \&format_type,
	approval_method => \&format_approval_method,
);

sub format_cert_row {
	my ($cert_info, @columns) = @_;

	my @row;
	for my $column (@columns) {
		if (exists $cert_column_formatters{$column}) {
			push @row, defined($cert_info->{$column}) ? $cert_column_formatters{$column}->($cert_info->{$column}) : undef;
		} else {
			push @row, $cert_info->{$column};
		}
	}
	return \@row;
}

sub command_list {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate list [OPTIONS]\n";
		print "\n";
		print "Valid options:\n";
		print " --local             display only certificates that are installed locally\n";
		print " --no-local          exclude certificates that are installed locally\n";
		print " -c, --columns=COLS  include the given columns, where COLS is comma-separated\n";
		print " -s, --sort=COLS     sort by the given column(s) (expiration by default)\n";
		print " -z                  machine-parseable output\n";
		print "\n";
		print "Valid columns:\n";
		print " " . join(', ', sort keys %cert_column_titles) . "\n"; # TODO: wrap this at 80 columns
		#print " name, type, status, expiration, local_status, fingerprint, sha256_fingerprint,\n";
		#print " auto_renew, approver_email\n";
		return 0;
	}

	my $local;
	my $columns;
	my $sort_columns = 'expiration';
	my $machine_output;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('local!', \$local,
			    'sort|s=s', \$sort_columns,
			    'columns|c=s', \$columns,
			    'z', \$machine_output) or return 2;

	my @sort_columns = split(/,/, $sort_columns);

	if ($machine_output && !defined($columns)) {
		print STDERR "Error: --columns option is required if -z is used.\n";
		return 2;
	}

	my @columns = defined($columns) ? split(/,/, $columns) : qw/name type status expiration local_status/;
	for my $column (@columns) {
		if (not exists $cert_column_titles{$column}) {
			print STDERR "Error: Invalid column: $column\n";
			return 1;
		}
	}
	if (not @columns) {
		print STDERR "Error: no columns specified\n";
		return 1;
	}

	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	my ($status, $response) = authed_api_call('GET', '/certs', { expand => ['current', 'pending'] }) or return 1;
	if ($status != 200) {
		print STDERR "Error: " . $response->{message} . "\n";
		return 1;
	}
	my @records;
	for my $cert_obj (@{$response->{data}}) {
		my $local_cert_info = get_local_cert_info(get_cert_paths($cert_obj->{cn}));
		if (defined $local) {
			my $is_local = $local_cert_info->{crt} || $local_cert_info->{key};
			next if $local && !$is_local;
			next if !$local && $is_local;
		}
		push @records, make_cert_info($cert_obj, $local_cert_info);
	}

	# TODO: column-aware sorting
	@records = sort {
		for (@sort_columns) {
			my ($dir, $column_name) = /^\^(.*)$/ ? (-1, $1) : (1, $_);
			my $cmp = $dir * (($a->{$column_name} // '') cmp ($b->{$column_name} // ''));
			return $cmp unless $cmp == 0;
		}
		return 0;
	} @records;

	if ($machine_output) {
		for my $record (@records) {
			print join($ENV{'OFS'} // "\0", map { $_ // '' } @{$record}{@columns}) . ($ENV{'ORS'} // "\0");
		}
	} else {
		my @rows;
		for my $record (@records) {
			push @rows, format_cert_row($record, @columns);
		}

		my @titles = @cert_column_titles{@columns};
		my @widths;
		for my $i (0..@columns-1) {
			push @widths, max(map { length($_->[$i] // '-') } \@titles, @rows);
		}
		print_row(\@titles, \@widths);
		print '-' x (sum(@widths) + 2*(@columns-1)) . "\n";
		print_row($_, \@widths) for @rows;
	}

	return 0;
}

sub command_show {
	local @ARGV = @_;

	my %field_titles = (%cert_column_titles, alt_names => 'Alt Names');

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate show [OPTIONS] HOSTNAME\n";
		print "\n";
		print "Valid options:\n";
		print " -f, --fields=FIELDS  include the given fields, where FIELDS is comma-separated\n";
		print " --json               JSON output\n";
#		print " -z                   machine-parseable output\n";
		print "\n";
		print "Valid fields:\n";
		print " " . join(', ', sort keys %field_titles) . "\n"; # TODO: wrap this at 80 columns
		return 0;
	}

	my $fields;
	my $machine_output = 0;
	my $json_output = 0;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('fields|f=s', \$fields,
			    'json', \$json_output,
			    'z', \$machine_output) or return 2;

	if ($json_output && $machine_output) {
		print STDERR "Error: --json and -z are mutually exclusive.\n";
		return 2;
	}
	if ($machine_output && !defined($fields)) {
		print STDERR "Error: --fields option is required if -z is used.\n";
		return 2;
	}

	if (@ARGV != 1) {
		print STDERR "Error: you must specify certificate name on command line.\n";
		return 2;
	}
	my $cn = lc $ARGV[0];

	my @fields = defined($fields) ? split(/,/, $fields) : qw/name alt_names type status expiration auto_renew approval_method approver_email fingerprint local_status/;
	for my $field (@fields) {
		if (not exists $field_titles{$field}) {
			print STDERR "Error: Invalid field: $field\n";
			return 1;
		}
	}
	if (not @fields) {
		print STDERR "Error: no fields specified\n";
		return 1;
	}

	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	my ($status, $cert_obj) = authed_api_call('GET', '/certs/' . qs_escape($cn), { expand => ['current', 'pending'] }) or return 1;
	if ($status != 200) {
		print STDERR "Error: " . $cert_obj->{message} . "\n";
		return 1;
	}
	my $local_cert_info = get_local_cert_info(get_cert_paths($cert_obj->{cn}));
	my $cert_info = make_cert_info($cert_obj, $local_cert_info);

	if ($json_output) {
		print encode_json($cert_info);
	} elsif ($machine_output) { # TODO
		print STDERR "Error: -z not yet implemented\n";
		return 1;
	} else {
		my @rows;
		for my $field (@fields) {
			my $title = $field_titles{$field};
			my $value = $cert_info->{$field};
			my $formatter = exists($cert_column_formatters{$field}) ?
					$cert_column_formatters{$field} :
					sub { return shift; };

			next unless defined $value;

			if (ref $value eq 'ARRAY') {
				push @rows, [ "$title: ", @$value ? $formatter->($value->[0]) : '' ];
				for my $v (@{$value}[1..@$value-1]) {
					push @rows, [ "", $formatter->($v) ];
				}
			} else {
				push @rows, [ "$title: ", $formatter->($value) ];
			}
		}

		my $max_width = max(map { length($_->[0]) } @rows);
		printf "%*s%s\n", $max_width, $_->[0], $_->[1] for @rows;
	}

	return 0;
}

sub command_edit {
	local *print_our_usage = sub {
		my ($out) = @_;

		print $out "Usage: sslmate edit OPTIONS... HOSTNAME\n";
		print $out "\n";
		print $out "Valid options:\n";
		print $out " --approval=METHOD    use the given approval method (email or dns)\n";
		print $out " --approval=HOSTNAME=METHOD\n";
		print $out "                      use the given approval method for given hostname\n";
		print $out " --email=ADDRESS      change the approver email of this cert\n";
		print $out " --email=HOSTNAME=ADDRESS\n";
		print $out "                      change the approver email for given hostname\n";
		print $out " --auto-renew         enable auto-renew for this cert\n";
		print $out " --no-auto-renew      disable auto-renew for this cert\n";
		print $out " --add-name=HOSTNAME  add the given hostname (takes effect upon reissue)\n";
		print $out " --rm-name=HOSTNAME   remove the given hostname (takes effect upon reissue)\n";
		print $out " --type=dv|ev         change the type of cert (takes effect upon renewal)\n";
		print $out " --multi              convert to multi-hostname cert (takes effect upon renewal)\n";
		print $out " --no-multi           convert to single-hostname cert (takes effect upon renewal)\n";
	};

	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print_our_usage(*STDOUT);
		return 0;
	}

	my $auto_renew;
	my $default_approval_method;
	my %approval_methods;
	my $default_approver_email;
	my %approver_emails;
	my @add_sans;
	my @rm_sans;
	my $type;
	my $multi;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('auto-renew!', \$auto_renew,
			    'approval=s', parse_approval_arg(\$default_approval_method, \%approval_methods),
			    'email=s', parse_approval_arg(\$default_approver_email, \%approver_emails),
			    'add-name=s', \@add_sans,
			    'rm-name=s', \@rm_sans,
			    'type=s', \$type,
			    'multi!', \$multi) or return 2;
	if (@ARGV == 0) {
		print STDERR "Error: you must specify the name of the certificate to edit.\n";
		print_our_usage(*STDERR);
		return 2;
	}

	my $cn = lc $ARGV[0];
	validate_cn($cn) or return 1;
	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	#
	# Retrieve current cert object from server
	#
	my ($cert_status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn)) or return 1;
	if ($cert_status != 200) {
		print STDERR "Error: " . $cert->{message} . "\n";
		return 1;
	}
	$cn = $cert->{cn}; # So that we use the canonical CN

	if (not $cert->{exists}) {
		print STDERR "Error: $cn: there is no certificate in your account with this common name.\n";
		return 1;
	}

	#
	# Add, remove, and update SANs
	#
	$multi //= defined($cert->{sans});

	my $new_san_objs;
	my $modified_sans = 0;
	my $modified_approval = 0;
	if (defined($cert->{sans}) && $multi) {
		# Was already a SAN cert and user doesn't want to change that.
		# Iterate the current list of SAN objects and build a list of updated SAN
		# objects in @$new_san_objs.  Set $modified_sans to 1 if we make any changes.
		$new_san_objs = [];
		for my $san_obj (@{$cert->{sans}}) {
			if ($san_obj->{type} ne 'dns') {
				# We don't know how to deal with non-DNS SANs, so just pass it through unmodified
				push @$new_san_objs, $san_obj;
				next;
			}
			my $hostname = $san_obj->{value};
			if (grep { $_ eq $hostname } map(lc, @rm_sans)) {
				# Delete this SAN by not copying it through to @$new_san_objs.
				$modified_sans = 1;
				$modified_approval = 1;
				next;
			}

			my $approval_method = $approval_methods{$hostname} // $default_approval_method // $san_obj->{approval_method};
			my $approver_email = $approver_emails{$hostname} // $default_approver_email // $san_obj->{approver_email};
			if ($approval_method eq 'email' && not(defined $approver_email)) {
				print STDERR "Error: $hostname: no approver email defined for this hostname.\n";
				print STDERR "Tip: use '--email $hostname=ADDRESS' to specify the approver email for this hostname.\n";
				return 2;
			}
			my $new_san_obj = { type => 'dns',
					    value => $hostname,
					    approval_method => $approval_method,
					    approver_email => $approver_email };
			push @$new_san_objs, $new_san_obj;
			$modified_sans ||= !compare_san_obj($san_obj, $new_san_obj);
			$modified_approval ||= !compare_approval_method($san_obj, $new_san_obj);
		}
	} elsif (defined $cert->{sans}) {
		# Was a SAN cert but user wants to make it a non-SAN cert
		print STDERR "Notice: conversion to single-hostname certificate will take place upon renewal.\n";
		$new_san_objs = undef;
		$modified_sans = 1;
	} elsif ($multi) {
		# Wasn't a SAN cert but user wants to make it one
		print STDERR "Notice: conversion to multi-hostname certificate will take place upon renewal.\n";
		$new_san_objs = [];
		$modified_sans = 1;
	}
	# Add new SANs to @new_san_objs, if they aren't already there
	for my $hostname (map lc, @add_sans) {
		if (not $multi) {
			$multi = 1;
			$new_san_objs = [];
			$modified_sans = 1;
			print STDERR "Notice: this certificate is not currently a multi-hostname certificate.\n";
			print STDERR "Notice: automatically converting to a multi-hostname certificate...\n";
			print STDERR "Notice: conversion will take place upon renewal.\n";
			print STDERR "Warning: starting with SSLMate 2.0, you will need to explicitly specify --multi.\n";
			sleep(2) unless $batch;
		}

		next if grep { $_->{type} eq 'dns' && $_->{value} eq $hostname } @$new_san_objs;

		my $approval_method = $approval_methods{$hostname} // $default_approval_method;
		my $approver_email = $approver_emails{$hostname} // $default_approver_email;

		if (not defined $approval_method) {
			print STDERR "Error: $hostname: no approval method defined for this hostname.\n";
			print STDERR "Tip: use '--approval $hostname=METHOD' to specify the approval method for this hostname.\n";
			return 2;
		}
		if ($approval_method eq 'email' && not(defined $approver_email)) {
			print STDERR "Error: $hostname: no approver email defined for this hostname.\n";
			print STDERR "Tip: use '--email $hostname=ADDRESS' to specify the approver email for this hostname.\n";
			return 2;
		}

		push @$new_san_objs, { type => 'dns',
				       value => $hostname,
				       approval_method => $approval_method,
				       approver_email => $approver_email };
		$modified_sans = 1;
		$modified_approval = 1;
	}

	#
	# Update the cert object on the server
	#
	my $new_cert = { approval_method => $approval_methods{$cn} // $default_approval_method // $cert->{approval_method},
			 approver_email => $approver_emails{$cn} // $default_approver_email // $cert->{approver_email} };

	if ($new_cert->{approval_method} eq 'email' && not(defined $new_cert->{approver_email})) {
		print STDERR "Error: $cn: no approver email defined for this hostname.\n";
		if ($cert->{sans}) {
			print STDERR "Tip: use '--email $cn=ADDRESS' to specify the approver email for this hostname.\n";
		} else {
			print STDERR "Tip: use '--email ADDRESS' to specify the approver email.\n";
		}
		return 2;
	}

	$modified_approval ||= !compare_approval_method($cert, $new_cert);

	$new_cert->{auto_renew} = to_json_bool($auto_renew) if defined $auto_renew;
	$new_cert->{type} = $type if defined $type;
	$new_cert->{sans} = $new_san_objs if $modified_sans;

	($cert_status, $new_cert) = authed_api_call('POST', '/certs/' . qs_escape($cn), { expand => ['pending','dns_approval.status','http_approval.status'] }, $new_cert, 'application/json') or return 1;
	if ($cert_status != 200) {
		print STDERR "Error: " . $new_cert->{message} . "\n";
		return 1;
	}

	if ($modified_approval) {
		if (!prepare_approval($cert, $new_cert)) {
			authed_api_call('POST', '/certs/' . qs_escape($cn), undef, object_subset($cert, qw/approval_method approver_email auto_renew type sans/), 'application/json');
			return 1;
		}
	}

	#
	# If this cert is pending, and we just changed the approval method, redo approval
	#
	if (defined($new_cert->{pending}) && $modified_approval) {
		if (not($new_cert->{sans}) && $new_cert->{approval_method} eq 'email') {
			print "Resending approval email to " . $new_cert->{approver_email} . "... ";
		} else {
			print "Re-initiating certificate approval... ";
		}
		STDOUT->flush;
		my ($status, $response) = authed_api_call('POST',  '/certs/' . qs_escape($cn) . '/redo_approval', undef, {}) or return 1;
		if ($status != 200) {
			print "\n";
			print STDERR "Error: " . $response->{message} . "\n";
			return 1;
		}
		print "Done.\n";
	}
	return 0;
}

sub command_resend_email {
	local *print_our_usage = sub {
		my ($out) = @_;

		print $out "Usage: sslmate resend-email [OPTIONS] HOSTNAME\n";
		print $out "\n";
		print $out "Valid options:\n";
		print $out " --email=ADDRESS     send to the given address\n";
	};

	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print_our_usage(*STDOUT);
		return 0;
	}

	my $approver_email;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('email=s', \$approver_email) or return 2;
	if (@ARGV == 0) {
		print STDERR "Error: you must specify the name of the certificate.\n";
		print_our_usage(*STDERR);
		return 2;
	}

	print STDERR "Warning: 'sslmate resend-email' is deprecated in favor of 'sslmate retry-approval'.\n";
	print STDERR "Warning: 'sslmate resend-email' will be removed in SSLMate 2.0.\n";
	sleep(2) unless $batch;

	my $cn = lc $ARGV[0];
	validate_cn($cn) or return 1;
	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	if (defined $approver_email) {
		# Retrieve current cert object from server
		my ($cert_status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn)) or return 1;
		if ($cert_status != 200) {
			print STDERR "Error: " . $cert->{message} . "\n";
			return 1;
		}

		# Update the cert object on the server
		($cert_status, my $new_cert) = authed_api_call('POST', '/certs/' . qs_escape($cn), undef, { approval_method => 'email', approver_email => $approver_email }) or return 1;
		if ($cert_status != 200) {
			print STDERR "Error: " . $new_cert->{message} . "\n";
			return 1;
		}
		prepare_approval($cert, $new_cert) or return 1;
	}

	print "Resending approval email...\n";
	my ($status, $response) = authed_api_call('POST',  '/certs/' . qs_escape($cn) . '/redo_approval', undef, {}) or return 1;
	if ($status != 200) {
		print STDERR "Error: " . $response->{message} . "\n";
		return 1;
	}
	print "Approval email resent to " . $response->{approver_email} . ".\n";
	return 0;
}

sub command_retry_approval {
	local *print_our_usage = sub {
		my ($out) = @_;

		print $out "Usage: sslmate retry-approval HOSTNAME\n";
	};

	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print_our_usage(*STDOUT);
		return 0;
	}

	my $cn = lc $ARGV[0];
	validate_cn($cn) or return 1;
	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	my ($cert_status, $cert) = authed_api_call('GET', '/certs/' . qs_escape($cn), { expand => ['dns_approval.status','http_approval.status'] }) or return 1;
	if ($cert_status != 200) {
		print STDERR "Error: " . $cert->{message} . "\n";
		return 1;
	}
	if (not $cert->{exists}) {
		print STDERR "Error: $cn: there is no certificate in your account with this common name.\n";
		return 1;
	}

	# Re-prepare the approval. This should have already happened, but we do it again
	# in case the process was interrupted.
	prepare_approval(undef, $cert) or return 1;

	print "Retrying approval for $cn... ";
	my ($status, $response) = authed_api_call('POST',  '/certs/' . qs_escape($cn) . '/redo_approval', undef, {}) or return 1;
	if ($status != 200) {
		print STDERR "Error: " . $response->{message} . "\n";
		return 1;
	}
	print "Done.\n";
	return 0;
}

sub command_test {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate test [OPTIONS] COMMONNAME\n";
		print "Example: sslmate test www.example.com\n";
		print "\n";
		print "Valid options:\n";
		print " -p, --port=NUMBER   test the given port number (default: 443)\n";
		print " -h, --host=HOSTNAME test the given host (defaults to the common name)\n";
		return 0;
	}

	my $port = 443;
	my @hostname;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('port|p=i', \$port,
			    'host|h=s', \@hostname) or return 2;

	if (@ARGV == 0) {
		print STDERR "Error: you must specify the name of the certificate to test.\n";
		print STDERR "Example: sslmate test www.example.com\n";
		return 2;
	}

	my $cn = lc $ARGV[0];
	validate_cn($cn) or return 1;

	load_config;
	if (!is_linked) {
		unless (-t STDIN) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}
	init_default_paths(0);

	my ($status, $response) = authed_api_call('GET', '/certs/' . qs_escape($cn) . '/test',
						  { port => $port,
						    hostname => \@hostname }) or return 1;

	if ($status != 200) {
		print STDERR "Error: $cn: " . $response->{message} . "\n";
		return 1;
	}

	if (not @{$response->{result}}) {
		print STDERR "Error: could not resolve hostname.\n";
		print STDERR "To specify a hostname, use the --host option.\n" unless @hostname;
		return 1;
	}

	my $num_errors = 0;
	my $num_cert_errors = 0;
	my $num_chain_errors = 0;
	for my $result (@{$response->{result}}) {
		my $host = $result->{hostname} . " (" . $result->{ip_address} . ")";
		if ($result->{status} ne 'conclusive') {
			print "$host: Error: " . $result->{error} . "\n";
			++$num_errors;
		} elsif (not $result->{correct}) {
			print "$host: Incorrect certificate installed\n";
			++$num_errors;
			++$num_cert_errors;
		} elsif (not $result->{chained}) {
			print "$host: Chain certificate not installed\n";
			++$num_errors;
			++$num_cert_errors;
			++$num_chain_errors;
		} elsif ($result->{dummy}) {
			print "$host: Good (temporary, self-signed certificate)\n";
		} else {
			print "$host: Good\n";
		}
	}

	if ($cn =~ /^\*\./ && not(@hostname)) {
		print STDERR "Tip: use the --host option to test a particular hostname.\n";
	}
	if ($num_chain_errors > 0) {
		print STDERR "Tip: make sure you have configured your server with either the .chained.crt file\n     OR with BOTH the .crt file and the .chain.crt file.\n";
	}
	if ($num_cert_errors > 0) {
		print STDERR "Tip: use the 'sslmate mkconfig' command to generate the correct configuration.\n";
	}

	return 11 unless $num_errors == 0;
	return 0;
}

sub command_import {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate import [OPTIONS] KEYFILE CERTFILE\n";
		print "\n";
		print "Example: sslmate import www.example.com.key www.example.com.crt\n";
		print "\n";
		print "Valid options:\n";
		print " -f, --force        replace existing files, certificates\n";
		print " --auto-renew       automatically renew this certificate before it expires\n";
		print " --no-auto-renew    don't automatically renew this certificate\n";
		print " --no-install       don't copy the key/cert to the local key/cert directories\n";
		print " --approval=METHOD  use the given approval method (email or dns)\n";
		print " --approval=HOSTNAME=METHOD\n";
		print "                    use the given approval method for given hostname\n";
		print " --email=ADDRESS    use the given approver email address\n";
		print " --email=HOSTNAME=ADDRESS\n";
		print "                    use the given approver email address for given hostname\n";
		return 0;
	}

	my $cn = undef;
	my $auto_renew = undef;
	my $force = 0;
	my $install_locally = 1;
	my $default_approval_method = 'email';
	my %approval_methods;
	my $default_approver_email = undef;
	my %approver_emails;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('auto-renew!', \$auto_renew,
			    'force|f', \$force,
			    'install!', \$install_locally,
			    'approval=s', parse_approval_arg(\$default_approval_method, \%approval_methods),
			    'email=s', parse_approval_arg(\$default_approver_email, \%approver_emails)) or return 2;

	if (@ARGV != 2) {
		print STDERR "Error: you must specify the key file and cert file paths.\n";
		print STDERR "Example: sslmate import www.example.com.key www.example.com.crt\n";
		return 2;
	}
	my ($source_key_path, $source_crt_path) = @ARGV;

	# 0. Load config, etc.
	load_config;
	if (!is_linked) {
		if ($batch || not(-t STDIN)) {
			print STDERR "Error: you have not yet linked this system with your SSLMate account.\n";
			print STDERR "Please run 'sslmate link'.\n";
			return 1;
		}
		do_link or return 1;
	}

	init_default_paths;

	# 1. Read the key
	open(my $source_key_file, '<', $source_key_path) or die "Error: Unable to open $source_key_path: $!\n";
	my $key = do { local $/; <$source_key_file> };
	close($source_key_file);
	my $pubkey_hash = sha256_hex(extract_pubkey_from_key($source_key_path));

	# 2. Read the crt
	-f $source_crt_path && -r $source_crt_path or die "Error: No such file: $source_crt_path\n";
	my $crt = extract_crt_from_file($source_crt_path, 'PEM');
	defined($crt) or die "Error: Unable to parse $source_crt_path - is it a valid certificate file?\n";

	# 3. Parse the crt
	# We could do this with a Perl module like Crypt::X509 (well, except for determining EV status), but
	# in order to keep the dependency footprint small, we do this with an API call instead.
	my ($status, $crt_info) = authed_api_call('POST', '/utils/parse_crt', undef, { crt => \$crt }) or exit 1;
	if ($status != 200) {
		print STDERR "Error: $source_crt_path: " . $crt_info->{message} . "\n";
		return 1;
	}
	$cn //= lc $crt_info->{cn};
	if ($crt_info->{pubkey_hash} ne $pubkey_hash) {
		print STDERR "Error: The certificate ($source_crt_path)'s public key does not match the private key ($source_key_path)\n";
		exit 1;
	}
	validate_cn($cn) or return 1;

	my $type = $crt_info->{ev} ? 'ev' : 'dv';

	# 4. Retrieve the current cert object from the server
	my $cert_obj;
	($status, $cert_obj) = authed_api_call('GET', '/certs/' . qs_escape($cn), { expand => ['current'] }) or exit 1;
	if ($status != 200) {
		print STDERR "Error: " . $cert_obj->{message} . "\n";
		exit 1;
	}
	$cn = $cert_obj->{cn}; # So that we use the canonical CN

	# 5. Check for existing files/certs
	my $paths;
	my $key_already_installed = 0;
	my $crt_already_installed = 0;
	if ($install_locally) {
		$paths = get_cert_paths($cn);
		if (realpath($source_key_path) eq realpath($paths->{key})) {
			$key_already_installed = 1;
		}
		if (realpath($source_crt_path) eq realpath($paths->{crt})) {
			$crt_already_installed = 1;
		}
	}
	unless ($force) {
		my $errors = 0;

		if ($install_locally) {
			my @check_files;
			for my $file_type (keys %$paths) {
				# Don't care if .key or .crt files exist if they're the same as the files we're importing.
				next if $file_type eq 'key' && $key_already_installed;
				next if $file_type eq 'crt' && $crt_already_installed;
				push @check_files, $file_type;
			}
			$errors += has_existing_files(@{$paths}{@check_files});
		}
		if (defined($cert_obj->{current}) && $cert_obj->{current}->{expiration} >= $crt_info->{expiration}) {
			print STDERR "Error: your account already has an active certificate for $cn.\n";
			$errors++;
		}
		if ($errors) {
			print STDERR "Tip: use --force to override the above error" . ($errors == 1 ? "" : "s") . ".\n";
			exit 1;
		}
	}

	# 6. Process SANs, approval methods, etc.
	my $product_info = get_product_info($type, $cn) or exit 1;

	my $has_nondns_sans;
	my @default_sans;
	my @nondefault_sans;
	for my $san (@{$crt_info->{sans}}) {
		if ($san->{type} ne 'dns') {
			$has_nondns_sans = 1;
		} elsif (grep { lc($_) eq lc($san->{value}) } @{$product_info->{default_sans}}) {
			push @default_sans, $san->{value};
		} else {
			push @nondefault_sans, $san->{value};
		}
	}

	my @sans;
	my @unsupported_sans;
	if (@nondefault_sans) {
		# This cert contains SANs that wouldn't be covered by the default SANs for this product.
		# Make sure they're all supported!
		if (is_wildcard_name($cn)) {
			# No SANs are supported on wildcard certs
			push @unsupported_sans, @nondefault_sans;
		} else {
			for my $san (@nondefault_sans) {
				if (is_wildcard_name($san)) {
					# Wildcard SANs not supported
					push @unsupported_sans, $san;
				} else {
					push @sans, lc $san;
				}
			}
		}
	}
	if (@sans) {
		# If we explicitly specify SANs, no SANs are added by default, so also
		# copy @default_sans to @sans...
		for my $default_san (map lc, @default_sans) {
			next if grep { $_ eq $default_san } ($cn, @sans);
			push @sans, $default_san;
		}
	}

	unless ($batch) {
		if ($has_nondns_sans) {
			print "WARNING: This certificate contains non-DNS subject alternative names.\n";
			print "SSLMate does not support non-DNS subject alternative names.  When this\n";
			print "certificate renews, the non-DNS subject alternative names will be dropped.\n\n";
			print "Do you understand and want to import this certificate anyways?\n\n";
			exit 1 unless prompt_yesno();
			print "\n";
		}
		if (@unsupported_sans) {
			print "WARNING: This certificate contains the following subject alternative names\n";
			print "that are not supported by SSLMate:\n\n";
			for my $san (@unsupported_sans) {
				print "\t$san\n";
			}
			print "\nWhen this certificate renews, these subject alternative names will be dropped.\n";
			print "Do you understand and want to import this certificate anyways?\n\n";
			exit 1 unless prompt_yesno();
			print "\n";
		}
	}

	# Determine the approval method for the CN, and each SAN
	for my $hostname ($cn, @sans) {
		my $method = $approval_methods{$hostname} // $default_approval_method;
		my $email = $approver_emails{$hostname} // $default_approver_email;

		if ($method eq 'email' && not defined($email)) {
			if ($batch) {
				print STDERR "Error: no approver email address specified for $cn.\n";
				print STDERR "Tip: in batch mode, you must specify approver email addresses with --email.\n";
				print STDERR "See 'sslmate help import' for help.\n";
				return 2;
			}

			my $approval_methods = get_approval_methods($type, $hostname) or return 1;
			($method, $email) = prompt_for_approval($hostname, $approval_methods) or return 1;
		}

		$approval_methods{$hostname} = $method;
		$approver_emails{$hostname} = $email;
	}

	# Construct a list of SAN objects
	my @san_objs;
	for my $san_hostname (@sans) {
		push @san_objs, { type => 'dns',
				  value => $san_hostname,
				  approval_method => $approval_methods{$san_hostname},
				  approver_email => $approver_emails{$san_hostname} };
	}

	# 7. Generate the CSR
	my $csr = openssl_req($source_key_path, $cert_obj->{dn} // make_dn($cn));

	my $key_file;
	if ($install_locally && !$key_already_installed) {
		$key_file = open_key_file($paths->{key}, $force);
	}

	my $exit_with_error = sub {
		unlink($paths->{key}) if $install_locally && !$key_already_installed;
		authed_api_call('POST', '/certs/' . qs_escape($cn), undef, object_subset($cert_obj, qw/type approval_method approver_email sans csr/), 'application/json') if defined($cert_obj) && $cert_obj->{exists};
		exit 1;
	};

	# 8. Create/update the cert object on the server
	my $request = {	auto_renew	=> to_json_bool($auto_renew // $cert_obj->{auto_renew}),
			type		=> $type,
			approval_method	=> $approval_methods{$cn},
			approver_email	=> $approver_emails{$cn},
			sans		=> int(@san_objs) ? \@san_objs : undef,
			csr		=> $csr };

	($status, my $new_cert_obj) = authed_api_call('POST', '/certs/' . qs_escape($cn), { expand => ['dns_approval.status','http_approval.status'] }, $request, 'application/json') or $exit_with_error->();
	if ($status != 200) {
		print STDERR "Error: " . $new_cert_obj->{message} . "\n";
		$cert_obj = undef;
		$exit_with_error->();
	}

	prepare_approval($cert_obj, $new_cert_obj) or $exit_with_error->();

	# 9. Create a new cert instance with this crt file
	print "Importing certificate for $cn...\n";
	my $cert_instance;
	($status, $cert_instance) = authed_api_call('POST', '/certs/' . qs_escape($cn) . '/instances',
							{ expand => ['chain','root'] },
							{ crt => \$crt }) or $exit_with_error->();
	if ($status != 200) {
		print STDERR "Error: " . $cert_instance->{message} . "\n";
		$exit_with_error->();
	}

	# 10. Write the key, crt, and chain to the destination directory
	if ($install_locally) {
		if (defined $key_file) {
			# write .key file
			truncate($key_file, 0);
			print $key_file $key;
			close $key_file;
		}

		write_cert_files($paths, undef, $crt, format_chain($cert_instance->{chain}), $cert_instance->{root});
	}

	print "\n";
	print "Your certificate has been imported to SSLMate.\n\n";
	if ($install_locally) {
		print_cert_paths($paths);
	}

	return 0;
}

sub interpolate_config_template {
	my ($in, $paths) = @_;
	my $in_len = length($in);
	my $out = '';
	my $begin = 0;
	while ($begin < $in_len) {
		my $end = index($in, '__', $begin);
		if ($end == -1) {
			$out .= substr($in, $begin);
			$begin = $in_len;
		} else {
			$out .= substr($in, $begin, $end - $begin);
			$begin = $end + 2;
			$end = index($in, '__', $begin);
			if ($end == -1) {
				die "Error: Malformed configuration template\n";
			}
			my $varname = substr($in, $begin, $end - $begin);
			if ($varname eq 'CERT_PATH') {
				$out .= $paths->{crt};
			} elsif ($varname eq 'KEY_PATH') {
				$out .= $paths->{key};
			} elsif ($varname eq 'CHAIN_PATH') {
				$out .= $paths->{chain};
			} elsif ($varname eq 'DHPARAMS_PATH') {
				$out .= "$SHARE_DIR/dhparams/dh2048-group14.pem";
			} elsif ($varname =~ /^(.*)_PATH$/ && exists $file_types{lc $1}) {
				my $file_type = lc $1;
				if (not exists $paths->{$file_type}) {
					die "Error: This software requires a " . lc($file_types{$file_type}) . " file, but SSLMate is not configured to create this type of file. Please set 'cert_format.$file_type yes' in your SSLMate configuration file, and then run 'sslmate download' to create this file.\n";
				}
				$out .= $paths->{$file_type};
			} else {
				die "Error: This configuration template is not compatible with this version of the SSLMate client. Please upgrade to the latest client (unknown template variable '$varname').\n";
			}
			$begin = $end + 2;
		}
	}

	return $out;
}

sub command_mkconfig {
	local @ARGV = @_;
	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));

	my $help_opt = 0;
	my $templates_opt = 0;
	my $no_security_opt = 0;
	$getopt->getoptions('templates', \$templates_opt,
			    'no-security', \$no_security_opt,
			    'help|?', \$help_opt) or return 2;

	if ($help_opt) {
		print "Usage: sslmate mkconfig [--no-security] TEMPLATE COMMONNAME\n";
		print "   or: sslmate mkconfig --templates\n";
		print "Example: sslmate mkconfig apache www.example.com\n";
		print "\n";
		print "Valid options:\n";
		print " --templates    output a list of available config templates\n";
		print " --no-security  don't include recommended security settings\n";
		return 0;
	}

	load_config;
	init_default_paths(0);

	if ($templates_opt) {
		my ($status, $response) = anon_api_call('GET', '/config_templates') or return 1;
		if ($status != 200) {
			print STDERR "Error: " . $response->{message} . "\n";
			return 1;
		}

		print join("\n", sort(map { $_->{name} } @{$response->{data}}), '');

		return 0;
	}

	if (@ARGV != 2) {
		print STDERR "Error: you must specify the template and certificate name.\n";
		print STDERR "Example: sslmate mkconfig apache www.example.com\n";
		print STDERR "Run 'sslmate help mkconfig' for help.\n";
		return 2;
	}

	my ($template_name, $common_name) = @ARGV;

	my ($status, $response) = anon_api_call('GET', '/config_templates/' . qs_escape($template_name),
						{ expand => [ 'template' ],
						  include_security_settings => $no_security_opt ? undef : 1 }) or return 1;

	if ($status != 200) {
		if (($response->{reason} // '') eq 'template_not_found') {
			print STDERR "Error: $template_name: Unknown configuration template.\n";
			print STDERR "Run 'sslmate mkconfig --templates' for a list for available templates.\n";
			return 1;
		}

		print STDERR "Error: $template_name: " . $response->{message} . "\n";
		return 1;
	}

	my $paths = get_cert_paths($common_name);
	print interpolate_config_template($response->{template}, $paths);
	return 0;
}

sub command_help {
	local @ARGV = @_;

	my $print_libexec_dir = 0;
	my $print_share_dir = 0;
	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('libexec-dir', \$print_libexec_dir,
			    'share-dir', \$print_share_dir) or return 2;

	if ($print_libexec_dir) {
		print "$LIBEXEC_DIR\n";
	} elsif ($print_share_dir) {
		print "$SHARE_DIR\n";
	} elsif (@ARGV == 0 || $ARGV[0] eq 'help') {
		print_usage(*STDOUT);
	} else {
		main($ARGV[0], "-?");
	}
	return 0;
}

sub command_version {
	local @ARGV = @_;

	if (@ARGV >= 1 && $ARGV[0] eq "-?") {
		print "Usage: sslmate version [OPTIONS]\n";
		print "\n";
		print "Valid options:\n";
		print " --no-check         don't check for the latest version\n";
		print " --is-latest        exit non-zero if there is a newer version\n";
		return 0;
	}

	my $check = 1;
	my $is_latest = 0;
	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case permute bundling));
	$getopt->getoptions('check!', \$check,
			    'is-latest', \$is_latest) or return 2;

	die "Error: --no-check and --is-latest are mutually-exclusive.\n" if $is_latest && !$check;

	print "SSLMate $SSLMate::VERSION\n" unless $is_latest;
	if ($check) {
		load_config;
		my ($status, $response) = anon_api_call('GET', '/latest_client_version');

		if (not $response) {
			print STDERR "Error: unable to determine latest available version.\n";
			exit 1 if $is_latest;
		} elsif ($status != 200) {
			print STDERR "Error: unable to determine latest available version: " . $response->{message} . "\n";
			exit 1 if $is_latest;
		} else {
			if ($SSLMate::VERSION ne $response->{latest_client_version}) {
				exit 10 if $is_latest;
				print "New version of SSLMate available: " . $response->{latest_client_version} . "\n";
			}
		}
	}
	return 0;
}

sub main {
	local @ARGV = @_;

	my $getopt = Getopt::Long::Parser->new;
	$getopt->configure(qw(no_ignore_case no_permute bundling));
	$getopt->getoptions('profile|p=s', \$config_profile,
			    'batch', \$batch,
#			    'quiet', \$quiet,
			    'verbose', \$verbose) or return 2;

	if (@ARGV == 0) {
		print_usage(*STDERR);
		return 2;
	}

	my $command = shift @ARGV;

	if ($command eq 'buy') {
		return command_buy @ARGV;
	} elsif ($command eq 'reissue' || $command eq 'rekey') {
		return command_reissue_rekey $command, @ARGV;
	} elsif ($command eq 'revoke') {
		return command_revoke @ARGV;
	} elsif ($command eq 'renew') {
		return command_renew @ARGV;
	} elsif ($command eq 'req') {
		return command_req @ARGV;
	} elsif ($command eq 'download') {
		return command_download @ARGV;
	} elsif ($command eq 'list') {
		return command_list @ARGV;
	} elsif ($command eq 'show') {
		return command_show @ARGV;
	} elsif ($command eq 'edit') {
		return command_edit @ARGV;
	} elsif ($command eq 'resend-email') {
		return command_resend_email @ARGV;
	} elsif ($command eq 'retry-approval') {
		return command_retry_approval @ARGV;
	} elsif ($command eq 'test') {
		return command_test @ARGV;
	} elsif ($command eq 'import') {
		return command_import @ARGV;
	} elsif ($command eq 'link') {
		return command_link @ARGV;
	} elsif ($command eq 'mkconfig') {
		return command_mkconfig @ARGV;
	} elsif ($command eq 'help') {
		return command_help @ARGV;
	} elsif ($command eq 'version') {
		return command_version @ARGV;
	} else {
		print STDERR "sslmate: '$command' is not a valid sslmate command.  See 'sslmate help'.\n";
		return 1;
	}
}

exit main map { decode_input $_ } @ARGV;
