X-Git-Url: https://git.rapsys.eu/.gitweb.cgi/acme/blobdiff_plain/9c3c698790057b5c32bb7daacb9269ddf4b677df..17feca20e7d0e958d43e934cdc4894a0457c2727:/acme.pm
diff --git a/acme.pm b/acme.pm
deleted file mode 100644
index db1c6d4..0000000
--- a/acme.pm
+++ /dev/null
@@ -1,760 +0,0 @@
-# This file is part of Acmepl
-#
-# Acmepl is is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see .
-#
-# Copyright (C) 2016 - 2017 Raphaël Gertz 
-
-# acme package
-package acme;
-
-# Best practice
-use strict;
-use warnings;
-
-# Symbol export
-use Exporter;
-our @ISA = qw(Exporter);
-our @EXPORT_OK = qw(DS CERT_DIR KEY_DIR REQUEST_CSR ACCOUNT_KEY SERVER_KEY SERVER_CRT CONFIG);
-
-# Load dependancies
-use Carp qw(carp confess);
-use Date::Parse qw(str2time);
-use Digest::SHA qw(sha256_base64);
-use Email::Valid;
-use File::Path qw(make_path);
-use File::Slurp qw(read_file write_file);
-use File::Temp; # qw( :seekable );
-use IPC::System::Simple qw(capturex);
-use JSON qw(from_json to_json);
-use LWP;
-use MIME::Base64 qw(encode_base64url encode_base64);
-use Net::Domain::TLD;
-use POSIX qw(EXIT_FAILURE);
-use Tie::IxHash;
-
-# Documentation links
-#XXX: see https://letsencrypt.github.io/acme-spec/ (probably based on https://ietf-wg-acme.github.io/acme/)
-#XXX: see jwk rfc http://www.rfc-editor.org/rfc/rfc7517.txt
-#XXX: see javascript implementation https://github.com/diafygi/gethttpsforfree/blob/gh-pages/js/index.js
-
-# Set constants
-use constant {
-	# Directory separator
-	DS => '/',
-
-	# Directory for certificates
-	CERT_DIR => 'cert',
-
-	# Directory for keys
-	KEY_DIR => 'key',
-
-	# Directory for pending cache
-	PENDING_DIR => 'pending',
-
-	# Request certificate file name
-	REQUEST_CSR => 'request.der',
-
-	# Account key file name
-	ACCOUNT_KEY => 'account.pem',
-
-	# Server private key
-	SERVER_KEY => 'server.pem',
-
-	# Server public certificate
-	SERVER_CRT => 'server.crt',
-
-	# rsa
-	KEY_TYPE => 'rsa',
-
-	# 2048|4096
-	KEY_SIZE => 4096,
-
-	# Acme infos
-	ACME_CERT => 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem',
-	ACME_DIR => 'https://acme-staging.api.letsencrypt.org/directory',
-	ACME_PROD_DIR => 'https://acme-v01.api.letsencrypt.org/directory',
-	ACME_TERMS => 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf',
-
-	# Version
-	VERSION => 'v0.6',
-
-	# Config
-	CONFIG => '/etc/acmepl/config'
-};
-
-# User agent object
-our $ua;
-
-# Strerr backup
-our $_stderr;
-
-# JSON Web Key (JWK)
-#XXX: tie to Tie::IxHash to keep a stable ordering of hash keys
-#our %jwk = (
-#	pubkey => undef,
-#	jwk => {
-#		alg => 'RS256',
-#		jwk => {
-#			# Exponent
-#			e => undef,
-#			# Key type
-#			kty => uc(KEY_TYPE),
-#			# Modulus
-#			n => undef
-#		}
-#	},
-#	thumbprint => undef
-#);
-tie(our %jwk, 'Tie::IxHash', pubkey => undef, jwk => undef, thumbprint => undef);
-tie(%{$jwk{jwk}}, 'Tie::IxHash', alg => 'RS256', jwk => undef);
-#XXX: strict ordering only really needed here for thumbprint sha256 digest
-tie(%{$jwk{jwk}{jwk}}, 'Tie::IxHash', e => undef, kty => uc(KEY_TYPE), n => undef);
-
-# Constructor
-sub new {
-	# Extract params
-	my ($class, $mail, $debug, $prod, @domains) = @_;
-
-	# Create self hash
-	my $self = {};
-
-	# Link self to package
-	bless($self, $class);
-
-	# Save debug
-	$self->{debug} = $debug;
-
-	# Save prod
-	$self->{prod} = $prod;
-
-	# Add extra check to mail validity
-	#XXX: mxcheck fail if there is only a A record on the domain
-	my $ev = Email::Valid->new(-fqdn => 1, -tldcheck => 1, -mxcheck => 1);
-
-	# Show error if check fail
-	if (! defined $ev->address($mail)) {
-		map { carp 'failed check: '.$_ if ($self->{debug}) } $ev->details();
-		confess 'Email::Valid->address failed';
-	}
-
-	# Save mail
-	$self->{mail} = $mail;
-
-	# Create resolver
-	my $res = new Net::DNS::Resolver();
-
-	# Check domains
-	map {
-		my $tld;
-
-		# Extract tld
-		unless (($tld) = $_ =~ m/\.(\w+)$/) {
-			confess $_.'\'s tld extraction failed';
-		}
-
-		# Check if tld exists
-		unless(Net::Domain::TLD::tld_exists($tld)) {
-			confess $tld.' tld from '.$_.' don\'t exists';
-		}
-
-		# Check if we get dns answer
-		#XXX: only search A type because letsencrypt don't support ipv6 (AAAA) yet
-		unless(my $rep = $res->search($_, 'A')) {
-			confess 'search A record for '.$_.' failed';
-		} else {
-			unless (scalar map { $_->type eq 'A' ? 1 : (); } $rep->answer) {
-				confess 'search recursively A record for '.$_.' failed';
-			}
-		}
-	} @domains;
-
-	# Save domains
-	@{$self->{domains}} = @domains;
-
-	# Return class reference
-	return $self;
-}
-
-# Prepare environement
-sub prepare {
-	my ($self) = @_;
-
-	# Create all paths
-	make_path(CERT_DIR, KEY_DIR, PENDING_DIR.'/'.$self->{mail}.'.'.($self->{prod} ? 'prod' : 'staging'), {error => \my $err});
-	if (@$err) {
-		map {
-			my ($file, $msg) = %$_;
-			carp ($file eq '' ? '' : $file.': ').$msg if ($self->{debug});
-		} @$err;
-		confess 'make_path failed';
-	}
-
-	# Create user agent
-	$ua = LWP::UserAgent->new;
-	$ua->agent(__PACKAGE__.'/'.VERSION)
-}
-
-# Drop stderr
-sub _dropStdErr {
-	# Save stderr
-	open($_stderr, '>&STDERR') or die $!;
-	# Close it
-	close(STDERR) or die $!;
-	# Send to /dev/null
-	open(STDERR, '>', '/dev/null') or die $!;
-}
-
-# Restore stderr
-sub _restoreStdErr {
-	# Close stderr
-	close(STDERR);
-	# Open it back
-	open(STDERR, '>&', $_stderr) or die $!;
-}
-
-# Generate required keys
-sub genKeys {
-	my ($self) = @_;
-
-	# Generate account and server key if required
-	map {
-		# Check key existence
-		if (! -f $_) {
-			# Drop stderr
-			_dropStdErr();
-			# Generate key
-			#XXX: we drop stderr here because openssl can't be quiet on this command
-			capturex('openssl', ('genrsa', '-out', $_, KEY_SIZE));
-			# Restore stderr
-			_restoreStdErr();
-		}
-	} (KEY_DIR.DS.ACCOUNT_KEY, KEY_DIR.DS.SERVER_KEY);
-
-	# Extract modulus and publicExponent jwk
-	#XXX: same here we tie to keep ordering
-	tie(%{$self->{account}}, 'Tie::IxHash', %jwk);
-	map {
-		if (/^Modulus=([0-9A-F]+)$/) {
-			# Extract to binary from hex and convert to base64 url
-			$self->{account}{jwk}{jwk}{n} = encode_base64url(pack("H*", $1) =~ s/^\0+//r);
-		} elsif (/^publicExponent:\s([0-9]+)\s\(0x[0-1]+\)$/) {
-			# Extract to binary from int, trim leading zeros and convert to base64 url
-			chomp ($self->{account}{jwk}{jwk}{e} = encode_base64url(pack("N", $1) =~ s/^\0+//r));
-		}
-	} capturex('openssl', ('rsa', '-text', '-in', KEY_DIR.DS.ACCOUNT_KEY, '-noout', '-modulus'));
-
-	# Drop stderr
-	_dropStdErr();
-	# Extract account public key
-	$self->{account}{pubkey} = join('', map { chomp; $_; } capturex('openssl', ('rsa', '-in', KEY_DIR.DS.ACCOUNT_KEY, '-pubout')));
-	# Restore stderr
-	_restoreStdErr();
-
-	# Store thumbprint
-	#XXX: convert base64 to base64 url
-	$self->{account}{thumbprint} = (sha256_base64(to_json($self->{account}{jwk}{jwk})) =~ s/=+\z//r) =~ tr[+/][-_]r;
-}
-
-# Generate certificate request
-sub genCsr {
-	my ($self) = @_;
-
-	# Openssl config template
-	my $oct = File::Temp->new();
-
-	# Load template from data
-	map { s/__EMAIL_ADDRESS__/$self->{mail}/; s/__COMMON_NAME__/$self->{domains}[0]/; print $oct $_; } ;
-
-	# Close data
-	close(DATA);
-
-	# Append domain names
-	my $i = 1;
-	map { print $oct 'DNS.'.$i++.' = '.$_."\n"; } @{$self->{domains}};
-
-	# Generate csr
-	capturex('openssl', ('req', '-new', '-outform', 'DER', '-key', KEY_DIR.DS.SERVER_KEY, '-config', $oct->filename, '-out', CERT_DIR.DS.REQUEST_CSR));
-
-	# Close oct
-	close($oct);
-}
-
-# Directory call
-sub directory {
-	my ($self) = @_;
-
-	# Set time
-	my $time = time;
-
-	# Set directory
-	my $dir = $self->{prod} ? ACME_PROD_DIR : ACME_DIR;
-
-	# Create a request
-	my $req = HTTP::Request->new(GET => $dir.'?'.$time);
-
-	# Get request
-	my $res = $ua->request($req);
-
-	# Handle error
-	unless ($res->is_success) {
-		confess 'GET '.$dir.'?'.$time.' failed: '.$res->status_line;
-	}
-
-	# Save nonce
-	$self->{nonce} = $res->headers->{'replay-nonce'};
-
-	# Merge uris in self content
-	%$self = (%$self, %{from_json($res->content)});
-}
-
-# Post request
-sub _post {
-	my ($self, $uri, $payload) = @_;
-
-	# Protected field
-	my $protected = encode_base64url(to_json({nonce => $self->{nonce}}));
-
-	# Payload field
-	$payload = encode_base64url(to_json($payload));
-
-	# Sign temp file
-	my $stf = File::Temp->new();
-
-	# Append protect.payload to stf
-	print $stf $protected.'.'.$payload;
-
-	# Close stf
-	close($stf);
-
-	# Generate digest of stf
-	my $signature = encode_base64url(join('', capturex('openssl', ('dgst', '-sha256', '-binary', '-sign', KEY_DIR.DS.ACCOUNT_KEY, $stf->filename))) =~ s/^\0+//r);
-
-	# Create a request
-	my $req = HTTP::Request->new(POST => $uri);
-	
-	# Set new-reg request content
-	$req->content(to_json({
-		header => $self->{account}{jwk},
-		protected => $protected,
-		payload => $payload,
-		signature => $signature
-	}));
-
-	# Post request
-	my $res = $ua->request($req);
-
-	# Save nonce
-	if (defined $res->headers->{'replay-nonce'}) {
-		$self->{nonce} = $res->headers->{'replay-nonce'};
-	}
-
-	# Return res object
-	return $res;
-}
-
-# Resolve dns and check content
-#XXX: see https://community.centminmod.com/threads/looks-like-letsencrypt-dns-01-is-ready.5845/#12 for example
-sub _dnsCheck {
-	my ($self, $domain, $token) = @_;
-
-	# Generate signature from content
-	my $signature = ((sha256_base64($token.'.'.$self->{account}{thumbprint})) =~ s/=+\z//r) =~ tr[+/][-_]r;
-
-	# Fix domain
-	$domain = '_acme-challenge.'.$domain.'.';
-
-	# Create resolver
-	my $res = new Net::DNS::Resolver();
-
-	# Check if we get dns answer
-	unless(my $rep = $res->search($domain, 'TXT')) {
-		carp 'TXT record search for '.$domain.' failed' if ($self->{debug});
-		return;
-	} else {
-		unless (scalar map { $_->type eq 'TXT' && $_->txtdata =~ /^$signature$/ ? 1 : (); } $rep->answer) {
-			carp 'TXT record recursive search for '.$domain.' failed' if ($self->{debug});
-			return;
-		}
-	}
-
-	return 1;
-}
-
-# Get uri and check content
-sub _httpCheck {
-	my ($self, $domain, $token) = @_;
-
-	# Create a request
-	my $req = HTTP::Request->new(GET => 'http://'.$domain.'/.well-known/acme-challenge/'.$token);
-
-	# Load config if available
-	my $config = undef;
-	if (
-		#XXX: use eval to workaround a fatal in from_json
-		defined eval {
-			# Check that file exists
-			-f CONFIG &&
-			# Read it
-			($config = read_file(CONFIG)) &&
-			# Decode it
-			($config = from_json($config)) &&
-			# Check defined
-			$config->{thumbprint}
-		}
-	) {
-		# Try to write thumbprint
-		write_file($config->{thumbprint}, $self->{account}{thumbprint});
-	}
-
-	# Get request
-	my $res = $ua->request($req);
-
-	# Handle error
-	unless ($res->is_success) {
-		carp 'GET http://'.$domain.'/.well-known/acme-challenge/'.$token.' failed: '.$res->status_line if ($self->{debug});
-		return;
-	}
-
-	# Handle invalid content
-	unless($res->content =~ /^$token.$self->{account}{thumbprint}\s*$/) {
-		carp 'GET http://'.$domain.'/.well-known/acme-challenge/'.$token.' content match failed: /^'.$token.'.'.$self->{account}{thumbprint}.'\s*$/ !~ '.$res->content if ($self->{debug});
-		return;
-	}
-
-	# Return success
-	return 1;
-}
-
-# Register account
-#XXX: see doc at https://ietf-wg-acme.github.io/acme/#rfc.section.6.3
-sub register {
-	my ($self) = @_;
-
-	# Post new-reg request
-	#XXX: contact array may contain a tel:+33612345678 for example
-	my $res = $self->_post($self->{'new-reg'}, {resource => 'new-reg', contact => ['mailto:'.$self->{mail}], agreement => ACME_TERMS});
-
-	# Handle error
-	unless ($res->is_success || $res->code eq 409) {
-		confess 'POST '.$self->{'new-reg'}.' failed: '.$res->status_line;
-	}
-
-	# Update mail informations
-	if ($res->code eq 409) {
-		# Save registration uri
-		$self->{'reg'} = $res->headers->{location};
-
-		# Post reg request
-		#XXX: contact array may contain a tel:+33612345678 for example
-		$res = $self->_post($self->{'reg'}, {resource => 'reg', contact => ['mailto:'.$self->{mail}]});
-
-		# Handle error
-		unless ($res->is_success) {
-			confess 'POST '.$self->{'reg'}.' failed: '.$res->status_line;
-		}
-	}
-}
-
-# Authorize domains
-sub authorize {
-	my ($self) = @_;
-
-	# Create challenges hash
-	%{$self->{challenges}} = ();
-
-	# Pending list
-	my @pending = ();
-
-	# Create or load auth request for each domain
-	map {
-		# Init content
-		my $content = undef;
-
-		# Init file
-		my $file = PENDING_DIR.'/'.$self->{mail}.'.'.($self->{prod} ? 'prod' : 'staging').'/'.$_;
-
-		# Load auth request content or post a new one
-		#TODO: add more check on cache file ???
-		if (
-			#XXX: use eval to workaround a fatal in from_json
-			! defined eval {
-				# Check that file exists
-				-f $file &&
-				# Read it
-				($content = read_file($file)) &&
-				# Decode it
-				($content = from_json($content))
-			# Check expiration
-			} || (str2time($content->{expires}) <= time()+3600)
-		) {
-			# Post new-authz request
-			my $res = $self->_post($self->{'new-authz'}, {resource => 'new-authz', identifier => {type => 'dns', value => $_}, existing => 'accept'});
-
-			# Handle error
-			unless ($res->is_success) {
-				confess 'POST '.$self->{'new-authz'}.' for '.$_.' failed: '.$res->status_line;
-			}
-
-			# Decode content
-			$content = from_json($res->content);
-
-			# Check domain
-			unless (defined $content->{identifier}{value} && $content->{identifier}{value} eq $_) {
-				confess 'domain matching '.$content->{identifier}{value}.' for '.$_.' failed: '.$res->status_line;
-			}
-
-			# Check status
-			unless ($content->{status} eq 'valid' or $content->{status} eq 'pending') {
-				confess 'POST '.$self->{'new-authz'}.' for '.$_.' failed: '.$res->status_line;
-			}
-
-			# Write to file
-			write_file($file, to_json($content));
-		}
-
-		# Add challenge
-		%{$self->{challenges}{$_}} = (
-			status => $content->{status},
-			expires => $content->{expires},
-			polls => []
-		);
-
-		# Save pending data
-		if ($content->{status} eq 'pending') {
-			# Extract validation data
-			foreach my $challenge (@{$content->{challenges}}) {
-				# One test already validated this auth request
-				if ($self->{challenges}{$_}{status} eq 'valid') {
-					next;
-				} elsif ($challenge->{status} eq 'valid') {
-					$self->{challenges}{$_}{status} = $challenge->{status};
-					next;
-				} elsif ($challenge->{status} eq 'pending') {
-					# Handle check
-					if (
-						($challenge->{type} =~ /^http-01$/ and $self->_httpCheck($_, $challenge->{token})) or
-						($challenge->{type} =~ /^dns-01$/ and $self->_dnsCheck($_, $challenge->{token}))
-					) {
-						# Post challenge request
-						my $res = $self->_post($challenge->{uri}, {resource => 'challenge', keyAuthorization => $challenge->{token}.'.'.$self->{account}{thumbprint}});
-
-						# Handle error
-						unless ($res->is_success) {
-							confess 'POST '.$challenge->{uri}.' failed: '.$res->status_line;
-						}
-
-						# Extract content
-						my $content = from_json($res->content);
-
-						# Save if valid
-						if ($content->{status} eq 'valid') {
-							$self->{challenges}{$_}{status} = $content->{status};
-						# Check is still polling
-						} elsif ($content->{status} eq 'pending') {
-							# Add to poll list for later use
-							push(@{$self->{challenges}{$_}{polls}}, {
-								type => (split(/-/, $challenge->{type}))[0],
-								status => $content->{status},
-								poll => $content->{uri}
-							});
-						}
-					}
-				}
-			}
-			# Check if check is challenge still in pending and no polls
-			if ($self->{challenges}{$_}{status} eq 'pending' && scalar @{$self->{challenges}{$_}{polls}} == 0) {
-				# Loop on all remaining challenges
-				foreach my $challenge (@{$content->{challenges}}) {
-					# Display help for http-01 check
-					if ($challenge->{type} eq 'http-01') {
-						print STDERR 'Create URI http://'.$_.'/.well-known/acme-challenge/'.$challenge->{token}.' with content '.$challenge->{token}.'.'.$self->{account}{thumbprint}."\n";
-					# Display help for dns-01 check
-					} elsif ($challenge->{type} eq 'dns-01') {
-						print STDERR 'Create TXT record _acme-challenge.'.$_.'. with value '.(((sha256_base64($challenge->{token}.'.'.$self->{account}{thumbprint})) =~ s/=+\z//r) =~ tr[+/][-_]r)."\n";
-					}
-				}
-			}
-		}
-	} @{$self->{domains}};
-
-	# Init max run
-	my $remaining = 10;
-
-	# Poll pending
-	while (--$remaining >= 0 and scalar map { $_->{status} eq 'valid' ? 1 : (); } values %{$self->{challenges}}) {
-		# Sleep
-		sleep(1);
-		# Poll remaining pending
-		map {
-			# Init domain
-			my $domain = $_;
-
-			# Poll remaining polls
-			map {
-				# Create a request
-				my $req = HTTP::Request->new(GET => $_->{poll});
-
-				# Get request
-				my $res = $ua->request($req);
-
-				# Handle error
-				unless ($res->is_success) {
-					carp 'GET '.$self->{challenges}{$_}{http_challenge}.' failed: '.$res->status_line if ($self->{debug});
-				}
-
-				# Extract content
-				my $content = from_json($res->content);
-
-				# Save status
-				if ($content->{status} ne 'pending') {
-					$self->{challenges}{$domain}{status} = $content->{status};
-				}
-			} @{$self->{challenges}{$_}{polls}};
-		} map { $self->{challenges}{$_}{status} eq 'pending' ? $_ : (); } keys %{$self->{challenges}};
-	} 
-
-	# Load config if available
-	my $config = undef;
-	if (
-		#XXX: use eval to workaround a fatal in from_json
-		defined eval {
-			# Check that file exists
-			-f CONFIG &&
-			# Read it
-			($config = read_file(CONFIG)) &&
-			# Decode it
-			($config = from_json($config)) &&
-			# Check defined
-			$config->{thumbprint}
-		}
-	) {
-		# Try to write thumbprint
-		write_file($config->{thumbprint}, '');
-	}
-
-	# Stop here with remaining chanllenge
-	if (scalar map { ! defined $_->{status} or $_->{status} ne 'valid' ? 1 : (); } values %{$self->{challenges}}) {
-		# Deactivate all activated domains 
-		#XXX: not implemented by letsencrypt
-		#map {
-		#	# Post deactivation request
-		#	my $res = $self->_post($self->{challenges}{$_}{http_uri}, {resource => 'authz', status => 'deactivated'});
-		#	# Handle error
-		#	unless ($res->is_success) {
-		#		confess 'POST '.$self->{challenges}{$_}{http_uri}.' failed: '.$res->status_line;
-		#	}
-		#} map { $self->{challenges}{$_}{status} eq 'valid' ? $_ : () } keys %{$self->{challenges}};
-
-		# Stop here as a domain of csr list failed authorization
-		if ($self->{debug}) {
-			my @domains = map { ! defined $self->{challenges}{$_}{status} or $self->{challenges}{$_}{status} ne 'valid' ? $_ : (); } keys %{$self->{challenges}};
-			confess 'Fix the challenge'.(scalar @domains > 1?'s':'').' for domain'.(scalar @domains > 1?'s':'').': '.join(', ', @domains);
-		} else {
-			exit EXIT_FAILURE;
-		}
-	}
-}
-
-# Issue certificate
-sub issue {
-	my ($self) = @_;
-
-	# Open csr file
-	open(my $fh, '<', CERT_DIR.DS.REQUEST_CSR) or die $!;
-
-	# Load csr
-	my $csr = encode_base64url(join('', <$fh>) =~ s/^\0+//r);
-
-	# Close csr file
-	close($fh) or die $!;
-
-	# Post certificate request
-	my $res = $self->_post($self->{'new-cert'}, {resource => 'new-cert', csr => $csr});
-
-	# Handle error
-	unless ($res->is_success) {
-		confess 'POST '.$self->{'new-cert'}.' failed: '.$res->status_line;
-	}
-
-	# Open crt file
-	open($fh, '>', CERT_DIR.DS.SERVER_CRT) or die $!;
-
-	# Convert to pem
-	print $fh '-----BEGIN CERTIFICATE-----'."\n".encode_base64($res->content).'-----END CERTIFICATE-----'."\n";
-
-	# Create a request
-	my $req = HTTP::Request->new(GET => ACME_CERT);
-
-	# Get request
-	$res = $ua->request($req);
-
-	# Handle error
-	unless ($res->is_success) {
-		carp 'GET '.ACME_CERT.' failed: '.$res->status_line if ($self->{debug});
-	}
-
-	# Append content
-	print $fh $res->content;
-
-	# Close file
-	close($fh) or die $!;
-
-	# Print success
-	carp 'Success, pem certificate in '.CERT_DIR.DS.SERVER_CRT if ($self->{debug});
-}
-
-1;
-
-__DATA__
-#
-# OpenSSL configuration file.
-# This is mostly being used for generation of certificate requests.
-#
-
-[ req ]
-default_bits		= 2048
-default_md		= sha256
-prompt			= no
-distinguished_name	= req_distinguished_name
-# The extentions to add to the self signed cert
-x509_extensions	= v3_ca
-# The extensions to add to a certificate request
-req_extensions = v3_req
-
-# This sets a mask for permitted string types. There are several options. 
-# utf8only: only UTF8Strings (PKIX recommendation after 2004).
-# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
-string_mask = utf8only
-
-[ req_distinguished_name ]
-countryName			= US
-stateOrProvinceName		= State or Province Name
-localityName			= Locality Name
-organizationName		= Organization Name
-organizationalUnitName		= Organizational Unit Name
-commonName			= __COMMON_NAME__
-emailAddress			= __EMAIL_ADDRESS__
-
-[ v3_req ]
-basicConstraints = CA:false
-keyUsage = nonRepudiation, digitalSignature, keyEncipherment
-subjectAltName = email:move
-subjectAltName = @alt_names
-
-[ v3_ca ]
-subjectKeyIdentifier = hash
-authorityKeyIdentifier = keyid:always,issuer
-basicConstraints = CA:true
-
-[alt_names]