Skip to content
This repository was archived by the owner on Dec 30, 2020. It is now read-only.

Commit

Permalink
Merge pull request #166 from will-in-wi/acmev2
Browse files Browse the repository at this point in the history
ACMEv2 port
  • Loading branch information
will-in-wi authored Apr 16, 2020
2 parents 58dc8fa + eeb994f commit 97afc24
Show file tree
Hide file tree
Showing 29 changed files with 183 additions and 188 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
/spec/.examples.txt

/.ruby-version

/spec/tmp
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require: rubocop-performance

AllCops:
Exclude:
# These are autogenerated binstubs.
Expand Down
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ language: ruby
rvm:
- 2.3.1
- 2.2
before_install:
- gem install bundler
cache: bundler
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ Unreleased

* Your change here!

v4.0.0

* Support ACMEv2
* No longer adds intermediate certs to bundle, as these don't appear to be provided.

v3.2.0

* New `--force` argument for easier handling of `endpoint` switching. Fixes [#132](https://github.com/will-in-wi/letsencrypt-webfaction/issues/132)
Expand Down
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ source 'https://rubygems.org'
gemspec

group :development, :test do
gem 'bundler', '~> 1.11'
gem 'pry', '~> 0.10'
gem 'pry-byebug', '~> 3.3'
gem 'pry-doc'
gem 'rake', '~> 12.0'
gem 'rspec', '~> 3.4'
gem 'rubocop', '~> 0.51'
gem 'rubocop', '~> 0.68.0' # Forcing 0.68, as Ruby 2.2 is unsupported after this.
gem 'rubocop-performance'
gem 'simplecov', '~> 0.11'
gem 'timecop', '~> 0.9.1'
gem 'webmock', '~> 3.4'
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ And finally, THANK YOU to all of you who have filed issues, contributed code and

## Previous Readme

*NOTE: Version 3 is out and requires some manual changes. See [the upgrade guide for details](docs/upgrading.md).*
*NOTE: Version 4 is out and requires some manual changes. See [the upgrade guide for details](docs/upgrading.md).*

This tool automates the process of using LetsEncrypt on WebFaction hosts. It can be added to the Cron scheduled task runner where it will validate your domains automatically, obtain the certificates, and then install them using the Webfaction API.

Expand Down Expand Up @@ -77,7 +77,7 @@ After saving `~/.bash_profile`, run the command `source $HOME/.bash_profile` to

Run `letsencrypt_webfaction init` to generate a registration cert and the config file. Open the config file `nano -w ~/letsencrypt_webfaction.toml` and edit to reflect your configuration.

Now, you are ready to run `letsencrypt_webfaction run` from your SSH session to get certificates. Note that by default the config file `letsencrypt_webfaction.toml` is pointed at the LetsEncrypt staging endpoint (the line that says: `endpoint = "https://acme-staging.api.letsencrypt.org/"`); meaning you will only get "test" certificates installed while using the stage endpoint. To issue live certificates you will need to comment out default line, and uncomment the production endpoint line (the line that says: `endpoint = "https://acme-v01.api.letsencrypt.org/" # Production`).
Now, you are ready to run `letsencrypt_webfaction run` from your SSH session to get certificates. Note that by default the config file `letsencrypt_webfaction.toml` is pointed at the LetsEncrypt staging endpoint (the line that says: `directory = "https://acme-staging-v02.api.letsencrypt.org/directory"`); meaning you will only get "test" certificates installed while using the stage endpoint. To issue live certificates you will need to comment out default line, and uncomment the production endpoint line (the line that says: `directory = "https://acme-v02.api.letsencrypt.org/directory" # Production`).

When you have tested with staging, you can remove the certificate from WebFaction control panel (make sure no webapps are using it first) and re-run with the production endpoint.

Expand Down Expand Up @@ -112,7 +112,7 @@ Generate certs and add to them to the control panel. This command has the follow

### Testing

To test certificate issuance, consider using the [LetsEncrypt staging server](https://community.letsencrypt.org/t/testing-against-the-lets-encrypt-staging-environment/6763). This doesn't have the rate limit of 5 certs per domain every 7 days. You can change the `endpoint` config line to be `https://acme-staging.api.letsencrypt.org/` in order to test the system.
To test certificate issuance, consider using the [LetsEncrypt staging server](https://community.letsencrypt.org/t/testing-against-the-lets-encrypt-staging-environment/6763). This doesn't have the rate limit of 5 certs per domain every 7 days. You can change the `directory` config line to be `https://acme-staging-v02.api.letsencrypt.org/directory` in order to test the system.

After switching endpoints, you will likely want to run the command with `--force` in order to reissue all certificates from the new endpoint.

Expand Down
10 changes: 10 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Upgrading from v3 to v4

Switching to ACMEv2 broke backwards compatibility in a couple ways.

- You need to change the `endpoint` entry in your config to `directory` and update it to staging or production.
```toml
directory = "https://acme-staging-v02.api.letsencrypt.org/directory" # Staging
#directory = "https://acme-v02.api.letsencrypt.org/directory" # Production
```

# Upgrading from v2 to v3

Version 3 has a number of major ease of use improvements that break backwards compatibility:
Expand Down
2 changes: 1 addition & 1 deletion letsencrypt_webfaction.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Gem::Specification.new do |spec|

spec.required_ruby_version = '>= 2.2.0'

spec.add_runtime_dependency 'acme-client', '~> 1.0'
spec.add_runtime_dependency 'acme-client', '~> 2.0'
spec.add_runtime_dependency 'toml-rb', '~> 1.1'

# This will be required for Ruby 2.4. But it is incompatible for Ruby <2.3. Unsupporting Ruby 2.4 for the moment.
Expand Down
2 changes: 1 addition & 1 deletion lib/letsencrypt_webfaction.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module LetsencryptWebfaction
VERSION = '3.2.0'.freeze
VERSION = '4.0.0'.freeze
end
2 changes: 1 addition & 1 deletion lib/letsencrypt_webfaction/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def new(args) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
$stderr.puts "Missing command. Must be one of #{SUPPORTED_COMMANDS.keys.join(', ')}"
raise LetsencryptWebfaction::AppExitError, 'Missing command'
elsif v2_command?(args)
$stderr.puts 'It looks like you are trying to run a version 2 command in version 3'
$stderr.puts 'It looks like you are trying to run a version 2 command in version 4'
$stderr.puts 'See https://github.com/will-in-wi/letsencrypt-webfaction/blob/master/docs/upgrading.md'
raise LetsencryptWebfaction::AppExitError, 'v2 command'
else
Expand Down
2 changes: 1 addition & 1 deletion lib/letsencrypt_webfaction/application/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
module LetsencryptWebfaction
module Application
class Init
def initialize(_); end # rubocop:disable Naming/UncommunicativeMethodParamName
def initialize(_); end

def run!
copy_config_file
Expand Down
14 changes: 4 additions & 10 deletions lib/letsencrypt_webfaction/application/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,14 @@ def private_key
end

def client
@_client ||= Acme::Client.new(private_key: private_key, endpoint: @options.endpoint)
@_client ||= Acme::Client.new(private_key: private_key, directory: @options.directory)
end

def register_key
# If the private key is not known to the server, we need to register it for the first time.
registration = client.register(contact: "mailto:#{@options.letsencrypt_account_email}")

# You'll may need to agree to the term (that's up the to the server to require it or not but boulder does by default)
registration.agree_terms
rescue Acme::Client::Error::Malformed => e
# Stupid hack if the registration already exists.
return if e.message == 'Registration key is already in use'
return if client.kid

raise
# If the private key is not known to the server, we need to register it for the first time.
client.new_account(contact: "mailto:#{@options.letsencrypt_account_email}", terms_of_service_agreed: true)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/letsencrypt_webfaction/application/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module LetsencryptWebfaction
module Application
class Version
def initialize(_); end # rubocop:disable Naming/UncommunicativeMethodParamName
def initialize(_); end

def run!
puts LetsencryptWebfaction::VERSION
Expand Down
5 changes: 3 additions & 2 deletions lib/letsencrypt_webfaction/certificate_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

module LetsencryptWebfaction
class CertificateInstaller
def initialize(cert_name, certificate, credentials)
def initialize(cert_name, certificate, private_key, credentials)
@cert_name = cert_name
@certificate = certificate
@private_key = private_key
@credentials = credentials
end

Expand All @@ -15,7 +16,7 @@ def install!
else
'create_certificate'
end
@credentials.call(action, @cert_name, @certificate.to_pem, @certificate.request.private_key.to_pem, @certificate.chain_to_pem)
@credentials.call(action, @cert_name, @certificate, @private_key.to_pem)

true
end
Expand Down
18 changes: 15 additions & 3 deletions lib/letsencrypt_webfaction/certificate_issuer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,31 @@ def call

private

def order
@_order ||= @client.new_order(identifiers: @cert_config.domains)
end

def validator
@_validator ||= LetsencryptWebfaction::DomainValidator.new @cert_config.domains, @client, @cert_config.public_dirs
@_validator ||= LetsencryptWebfaction::DomainValidator.new order, @client, @cert_config.public_dirs
end

def certificate_installer
@_certificate_installer ||= LetsencryptWebfaction::CertificateInstaller.new(@cert_config.cert_name, certificate, @api_credentials)
@_certificate_installer ||= LetsencryptWebfaction::CertificateInstaller.new(@cert_config.cert_name, certificate, csr.private_key, @api_credentials)
end

def certificate
# We can now request a certificate, you can pass anything that returns
# a valid DER encoded CSR when calling to_der on it, for example a
# OpenSSL::X509::Request too.
@_certificate ||= @client.new_certificate(csr)
@_certificate ||= begin
order.finalize(csr: csr)
while order.status == 'processing'
sleep(2)
order.reload
end

order.certificate
end
end

def csr
Expand Down
25 changes: 11 additions & 14 deletions lib/letsencrypt_webfaction/domain_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

module LetsencryptWebfaction
class DomainValidator
def initialize(domains, client, public_dirs)
@domains = domains
def initialize(order, client, public_dirs)
@order = order
@client = client
@public_dirs = public_dirs.map { |dir| File.expand_path(dir) }
end

def validate! # rubocop:disable Metrics/MethodLength
write_files!

challenges.map(&:request_verification).tap do |requests|
challenges.map(&:request_validation).tap do |requests|
next unless requests.any?(&:!)

$stderr.puts 'Failed to request validations.'
return false
end

10.times do
challenges.each(&:reload)
break if no_challenges_pending?

sleep(1)
sleep(2)
end

return true if all_challenges_valid?
Expand All @@ -32,20 +33,16 @@ def validate! # rubocop:disable Metrics/MethodLength

private

def authorizations
@authorizations ||= @domains.map { |domain| @client.authorize(domain: domain) }
end

def challenges
@challenges ||= authorizations.map(&:http01)
@challenges ||= @order.authorizations.map(&:http)
end

def no_challenges_pending?
challenges.none? { |challenge| challenge.authorization.verify_status == 'pending' }
challenges.none? { |challenge| challenge.status == 'pending' }
end

def all_challenges_valid?
challenges.reject { |challenge| challenge.authorization.verify_status == 'valid' }.empty?
challenges.reject { |challenge| challenge.status == 'valid' }.empty?
end

def write_files!
Expand All @@ -61,7 +58,7 @@ def write_files!
end

def print_errors
validations = authorizations.map(&:domain).zip(challenges)
validations = @order.authorizations.map(&:domain).zip(challenges)
$stderr.puts 'Failed to verify statuses.'
validations.each { |tuple| Validation.new(*tuple).print_error }
end
Expand All @@ -73,7 +70,7 @@ def initialize(domain, challenge)
end

def print_error # rubocop:disable Metrics/MethodLength
case @challenge.authorization.verify_status
case @challenge.status
when 'valid'
$stderr.puts "#{@domain}: Success"
when 'invalid'
Expand All @@ -82,7 +79,7 @@ def print_error # rubocop:disable Metrics/MethodLength
when 'pending'
$stderr.puts "#{@domain}: Still pending, but timed out"
else
$stderr.puts "#{@domain}: Unexpected authorization status #{@challenge.authorization.verify_status}"
$stderr.puts "#{@domain}: Unexpected authorization status #{@challenge.status}"
end
end

Expand Down
7 changes: 4 additions & 3 deletions lib/letsencrypt_webfaction/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

module LetsencryptWebfaction
class Options
NON_BLANK_FIELDS = %i[username password letsencrypt_account_email endpoint api_url servername].freeze
NON_BLANK_FIELDS = %i[username password letsencrypt_account_email directory api_url servername].freeze

WEBFACTION_API_URL = 'https://api.webfaction.com/'.freeze

Expand Down Expand Up @@ -39,8 +39,8 @@ def letsencrypt_account_email
@config['letsencrypt_account_email']
end

def endpoint
@config['endpoint']
def directory
@config['directory']
end

def api_url
Expand All @@ -57,6 +57,7 @@ def certificates

def errors
{}.tap do |e|
e[:endpoint] = 'needs to be updated to directory. See upgrade documentation.' if @config.key?('endpoint')
NON_BLANK_FIELDS.each do |field|
e[field] = "can't be blank" if public_send(field).nil? || public_send(field) == ''
end
Expand Down
2 changes: 1 addition & 1 deletion lib/letsencrypt_webfaction/options/certificate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module LetsencryptWebfaction
class Options
class Certificate
SUPPORTED_VALIDATION_METHODS = ['http01'].freeze
VALID_CERT_NAME = /[^a-zA-Z\d_]/
VALID_CERT_NAME = /[^a-zA-Z\d_]/.freeze
VALID_KEY_SIZES = [2048, 4096].freeze

def initialize(args)
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/test.config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
key_size: 2048
endpoint: 'https://acme.example.com/'
directory: 'https://acme.example.com/'
domains:
- 'example.com'
- 'www.example.com'
Expand Down
4 changes: 2 additions & 2 deletions spec/fixtures/test_invalid_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ letsencrypt_account_email = "[email protected]"

# The ACME endpoint. Use the staging server until you get everything working.
# Then switch to the production endpoint.
endpoint = "https://acme-staging.api.letsencrypt.org/" # Staging
#endpoint = "https://acme-v01.api.letsencrypt.org/" # Production
directory = "https://acme-staging.api.letsencrypt.org/" # Staging
#directory = "https://acme-v01.api.letsencrypt.org/" # Production

# The URL to the WebFaction API. You should not change this under normal
# circumstances.
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/test_public.config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
key_size: 2048
endpoint: 'https://acme.example.com/'
directory: 'https://acme.example.com/'
domains:
- 'example.com'
- 'www.example.com'
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/test_valid_config.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
username = "myusername"
password = "mypassword"
letsencrypt_account_email = "[email protected]"
endpoint = "https://acme-staging.api.letsencrypt.org/" # Staging
directory = "https://acme-staging.api.letsencrypt.org/" # Staging
api_url = "https://wfserverapi.example.com/"
servername = "myservername"

Expand Down
Loading

0 comments on commit 97afc24

Please sign in to comment.