aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2019-10-18 15:43:18 +0300
committerSergey Poznyakoff <gray@gnu.org.ua>2019-10-18 15:43:18 +0300
commit7e22b3181f963e62a44620336bdcd7d40baacb3a (patch)
tree48882634e30ab86c3ca9f0a62e12db9e2563c93e
parentd62fd8fbc9c272e79a58c30e964702c52d1f56c9 (diff)
downloadacmeman-7e22b3181f963e62a44620336bdcd7d40baacb3a.tar.gz
acmeman-7e22b3181f963e62a44620336bdcd7d40baacb3a.tar.bz2
Switch to ACMEv2
* Makefile.PL: Require Net::ACME2 * lib/App/Acmeman.pm: Rewrite using Net::ACME2. Avoid re-creating account key/id. * lib/App/Acmeman/Config.pm: Provide default for verbose.
-rw-r--r--Makefile.PL3
-rw-r--r--lib/App/Acmeman.pm242
-rw-r--r--lib/App/Acmeman/Config.pm2
3 files changed, 160 insertions, 87 deletions
diff --git a/Makefile.PL b/Makefile.PL
index f7195d4..3b41713 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -14,14 +14,13 @@ my %makefile_args = (
PREREQ_PM => {
'Getopt::Long' => 2.34,
'File::Path' => 2.08,
'File::Basename' => 2.84,
'Test::NoWarnings' => 0,
'Crypt::RSA::Parse' => 0.043,
- 'Protocol::ACME' => 1.01,
- 'Protocol::ACME::Challenge::LocalFile' => 1.01,
+ 'Net::ACME2' => 0.33,
'Crypt::Format' => 0.06,
'Crypt::OpenSSL::PKCS10' => 0.16,
'Crypt::OpenSSL::RSA' => 0.28,
'Crypt::OpenSSL::X509' => 1.804,
'DateTime::Format::Strptime' => 1.42,
'LWP::UserAgent' => 6.05,
diff --git a/lib/App/Acmeman.pm b/lib/App/Acmeman.pm
index ad5899a..b95f87f 100644
--- a/lib/App/Acmeman.pm
+++ b/lib/App/Acmeman.pm
@@ -1,11 +1,10 @@
package App::Acmeman;
use strict;
use warnings;
-use Protocol::ACME;
-use Protocol::ACME::Challenge::LocalFile;
+use Net::ACME2::LetsEncrypt;
use Crypt::Format;
use Crypt::OpenSSL::PKCS10 qw(:const);
use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::X509;
use File::Basename;
use File::Path qw(make_path);
@@ -22,26 +21,24 @@ use App::Acmeman::Config;
use App::Acmeman::Domain qw(:files);
use Data::Dumper;
use Text::ParseWords;
use App::Acmeman::Log qw(:all :sysexits);
use feature 'state';
-our $VERSION = '2.02';
+our $VERSION = '2.90';
my $progdescr = "manages ACME certificates";
-my %acme_endpoint = (prod => 'acme-v01.api.letsencrypt.org',
- staging => 'acme-staging.api.letsencrypt.org');
my $letsencrypt_root_cert_url =
'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem';
sub new {
my $class = shift;
my $self = bless {
_progname => basename($0),
- _acme_host => 'prod',
+ _acme_host => 'production',
_command => 'renew',
_option => {
config_file => '/etc/acmeman.conf'
},
_domains => []
}, $class;
@@ -97,13 +94,13 @@ sub run {
my $self = shift;
$self->${ \$self->{_command} }();
}
sub cf { shift->{_cf} }
sub progname { shift->{_progname} }
-sub acme_host { $acme_endpoint{shift->{_acme_host}} }
+sub acme_host { shift->{_acme_host} }
sub option {
my ($self,$opt) = @_;
return $self->{_option}{$opt};
}
@@ -308,16 +305,12 @@ sub renew {
$self->collect;
unless ($self->selected_domains) {
debug(1, "nothing to do");
exit(0);
}
$self->coalesce;
-
- $self->{_challenge} = Protocol::ACME::Challenge::LocalFile->new({
- www_root => $self->cf->get(qw(core rootdir))
- });
my $renewed = 0;
foreach my $vhost ($self->selected_domains) {
if ($self->force_option || $self->domain_cert_expires($vhost)) {
if ($self->register_domain_certificate($vhost)) {
if (my $cmd = $vhost->postrenew) {
@@ -402,12 +395,89 @@ sub debug_to_loglevel {
my $self = shift;
my @lev = ('err', 'info', 'debug');
my $v = $self->cf->core->verbose;
return $lev[$v > $#lev ? $#lev : $v];
}
+sub save_challenge {
+ my ($self,$challenge) = @_;
+ my $file = File::Spec->catfile($self->cf->get(qw(core rootdir)), $challenge->get_path);
+ if (open(my $fh, '>', $file)) {
+ print $fh $self->acme->make_key_authorization($challenge);
+ close $fh;
+ debug(3, "wrote challenge file $file");
+ } else {
+ error("can't open $file for writing: $!");
+ die;
+ }
+}
+
+sub acme {
+ my $self = shift;
+ my $key_id;
+ my $account_key;
+
+ my $idfile = File::Spec->catfile($self->cf->get('core','rootdir'),
+ 'account.key_id');
+ my $keyfile = File::Spec->catfile($self->cf->get('core','rootdir'),
+ 'account.pem');
+ if (-r $idfile) {
+ if (open(my $fh, '<', $idfile)) {
+ chomp($key_id = <$fh>);
+ close $fh;
+ debug(3, "using key_id $key_id");
+ } else {
+ error("can't open $idfile for reading: $!");
+ }
+ }
+
+ if (-r $keyfile) {
+ if (open(my $fh, '<', $keyfile)) {
+ local $/ = undef;
+ $account_key = Crypt::OpenSSL::RSA->new_private_key(<$fh>);
+ close $fh;
+ } else {
+ error("can't open $keyfile for reading: $!");
+ }
+ } else {
+ $account_key = Crypt::OpenSSL::RSA->generate_key($self->cf->get('core', 'key-size'));
+ }
+
+ unless ($self->{_acme}) {
+ my $acme = Net::ACME2::LetsEncrypt->new(
+ environment => $self->acme_host,
+ key => $account_key->get_private_key_string(),
+ key_id => $key_id
+ );
+ $self->{_acme} = $acme;
+
+ unless ($acme->key_id()) {
+ # Create new account
+ debug(3, "creating account");
+ my $terms_url = $acme->get_terms_of_service();
+ $acme->create_account(termsOfServiceAgreed => 1);
+ debug(3, "saving account credentials");
+
+ if (open(my $fh, '>', $idfile)) {
+ print $fh $acme->key_id();
+ close $fh;
+ } else {
+ error("can't open $idfile for writing: $!");
+ }
+
+ if (open(my $fh, '>', $keyfile)) {
+ print $fh $account_key->get_private_key_string();
+ close $fh;
+ } else {
+ error("can't open $idfile for writing: $!");
+ }
+ }
+ }
+ return $self->{_acme};
+}
+
sub register_domain_certificate {
my ($self,$domain) = @_;
my $key_size = $self->cf->get('domain', $domain, 'key-size')
|| $self->cf->get('core', 'key-size');
@@ -419,83 +489,94 @@ sub register_domain_certificate {
} else {
debug(1, "issuing $crt: CN=$domain, alternatives=$alt, key_size=$key_size");
}
}
return 1 if $self->dry_run_option;
- my $account_key = Crypt::OpenSSL::RSA->generate_key($key_size);
- my $acme = Protocol::ACME->new(
- host => $self->acme_host,
- account_key => { buffer => $account_key->get_private_key_string(),
- format => 'PEM' },
- loglevel => $self->debug_to_loglevel()
+ my $acme = $self->acme;
+
+ # Create order
+ my $order = $acme->create_order(
+ identifiers => [
+ map { { type => 'dns', value => $_ } } $domain->names
+ ]
);
+ debug(3, "$domain: created order");
- eval {
- $acme->directory();
- $acme->register();
- $acme->accept_tos();
+ my $authz = $acme->get_authorization(($order->authorizations())[0]);
+
+ my ($challenge) = grep { $_->type() eq 'http-01' } $authz->challenges();
+ if (!$challenge) {
+ error("$domain: no challenge of acceptable type received");
+ return 0;
+ }
- foreach my $name ($domain->names) {
- $acme->authz($name);
- $acme->handle_challenge($self->challenge);
- $acme->check_challenge();
- $acme->cleanup_challenge($self->challenge);
- }
+ debug(3, "$domain: serving challenge");
+ $self->save_challenge($challenge);
+ $acme->accept_challenge($challenge);
- my $csr = $self->make_csr($domain, $key_size);
- my $cert = $acme->sign({ format => 'PEM',
- buffer => $csr->get_pem_req() });
- my $chain = $acme->chain();
-
- if (my $filename = $domain->file(KEY_FILE)) {
- debug(3, "writing $filename");
- $self->prep_dir($filename);
- my $u = umask(077);
- $csr->write_pem_pk($filename);
- umask($u);
-
- if ($filename = $domain->file(CA_FILE)) {
- $self->save_crt($domain, CA_FILE, $chain);
- }
- $self->save_crt($domain, CERT_FILE, $cert);
- } else {
- $filename = $domain->certificate_file;
- debug(3, "writing $filename");
- $self->prep_dir($filename);
- my $u = umask(077);
- open(my $fd, '>', $filename)
- or abend(EX_CANTCREAT,
- "can't open $filename for writing: $!");
- print $fd Crypt::Format::der2pem($cert, 'CERTIFICATE');
- print $fd "\n";
- print $fd Crypt::Format::der2pem($chain, 'CERTIFICATE');
- print $fd "\n";
- print $fd $csr->get_pem_pk();
- print $fd "\n";
- umask($u);
+ # FIXME
+ my $ret;
+ while (($ret = $acme->poll_authorization($authz)) eq 'pending') {
+ sleep 1
+ }
+ if ($ret ne 'valid') {
+ error("$domain: can't renew certificate: authorization: $ret");
+ return 0;
+ }
+
+ my $csr = $self->make_csr($domain, $key_size);
+ my $status = $acme->finalize_order($order, $csr->get_pem_req());
+ while ($status eq 'pending') {
+ sleep 1;
+ $status = $order->status()
+ }
+
+ unless ($status eq 'valid') {
+ error("$domain: can't renew certificate: finalize: $ret");
+ return 0;
+ }
+ my $chain = $acme->get_certificate_chain($order);
+
+ if (my $filename = $domain->file(KEY_FILE)) {
+ debug(3, "writing $filename");
+ $self->prep_dir($filename);
+ my $u = umask(077);
+ $csr->write_pem_pk($filename);
+ umask($u);
+
+ my $cert;
+ if ($chain =~ /(^-+BEGIN\s+CERTIFICATE-+$
+ .+?
+ ^-+END\s+CERTIFICATE-+$)
+ (.+)/msx) {
+ $cert = $1;
+ ($chain = $2) =~ s/^\s+//s;
+ } else {
+ $cert = $chain; # FIXME: not sure if that's right
}
- };
- if ($@) {
- if (UNIVERSAL::isa($@, 'Protocol::ACME::Exception')) {
- error("$domain: can't renew certificate: $@->{status}");
- if (exists($@->{error})) {
- error("$domain: $@->{error}{status} $@->{error}{detail}");
- } else {
- error("$domain: $@->{detail} $@->{type}");
- }
- } elsif (ref($@) eq '') {
- chomp $@;
- error("$domain: failed to renew certificate: $@");
- } else {
- error("$domain: failed to renew certificate");
- print STDERR Dumper([$@]);
- }
- return 0;
+
+ if ($filename = $domain->file(CA_FILE)) {
+ $self->save_crt($domain, CA_FILE, $chain);
+ }
+ $self->save_crt($domain, CERT_FILE, $cert);
+ } else {
+ $filename = $domain->certificate_file;
+ debug(3, "writing $filename");
+ $self->prep_dir($filename);
+ my $u = umask(077);
+ open(my $fd, '>', $filename)
+ or abend(EX_CANTCREAT, "can't open $filename for writing: $!");
+ print $fd $chain;
+ print $fd "\n";
+ print $fd $csr->get_pem_pk();
+ print $fd "\n";
+ umask($u);
}
+
return 1;
}
sub make_csr {
my ($self, $dom, $keysize) = @_;
my $req = Crypt::OpenSSL::PKCS10->new($keysize);
@@ -506,26 +587,19 @@ sub make_csr {
$req->add_ext_final();
$req->sign();
return $req;
}
sub save_crt {
- my $self = shift;
- my $domain = shift;
- my $type = shift;
+ my ($self, $domain, $type, $pem) = @_;
if (my $filename = $domain->file($type)) {
debug(3, "writing $filename");
$self->prep_dir($filename);
open(my $fd, '>', $filename);
-
- foreach my $der (@_) {
- my $pem = Crypt::Format::der2pem($der, 'CERTIFICATE');
- print $fd $pem;
- print $fd "\n";
- }
+ print $fd $pem;
close $fd;
return $filename;
}
}
sub runcmd {
diff --git a/lib/App/Acmeman/Config.pm b/lib/App/Acmeman/Config.pm
index 6badae3..6707ee0 100644
--- a/lib/App/Acmeman/Config.pm
+++ b/lib/App/Acmeman/Config.pm
@@ -115,13 +115,13 @@ __DATA__
time-delta = NUMBER :default=86400
source = STRING :default=apache :array
check-alt-names = BOOL :default=0
check-dns = BOOL :default=1
my-ip = STRING :array
key-size = NUMBER :default=4096
- verbose = NUMBER
+ verbose = NUMBER :default=0
[files ANY]
type = STRING :re="^(single|split)$"
certificate-file = STRING
key-file = STRING
ca-file = STRING
argument = STRING

Return to:

Send suggestions and report system problems to the System administrator.