letsencrypt-rails-heroku 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e19d5080608ad213dfa35f80bbbca7d8aefcfeb7724b0d94efef50e56208f749
4
- data.tar.gz: 8567453f9e4d49488692a81bcfe64299a342c8e2cc8c9395116ff7c518842cb8
3
+ metadata.gz: 8d58e3d5c2daf94175a089ec08d9f4833752ec89acc6b208597b7a40ee7c6b2f
4
+ data.tar.gz: e6822c34e00a731d7cf1fbd3116e50274c6b4d1a04e5c35478254eadf91b8006
5
5
  SHA512:
6
- metadata.gz: e6e647b7ccffae1ea0690a3711bbb37ed773aa9a09c54fcfeb6d7d964ad097cc59524f222dd314e1af4c9ad066319d87feca72af7833055cf2d8e346e42cec7e
7
- data.tar.gz: 91e1769ca76f84c11de507dce46da2c56c431186ef6793f6ff12b934c664403255721bba2b5dcc99bdbf1530f21f2d219dba0d540e2016a64e9caaae85bb2560
6
+ metadata.gz: 51ef3ec5bfaa44fe033ee54ab089a1d3d3db39efd6b7be44fe1032b8534d4dbed6569fabaf8513055981004477e069f8a06bcf5f9479fed419feea6155188d00
7
+ data.tar.gz: f2ac8a3044287f5c635b32693c02d9558a669db406adb8e1d0025e6d02736ce44fae4f2e0c13da801eb6c86ede526e83d1e477c2115ea6a3f92765c25aae34c8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ # 2.0.0
2
+
3
+ Thanks to [@mashedkeyboard](https://github.com/mashedkeyboard) for their
4
+ work on ACME v2, saving registration, and DNS-based validation.
5
+
6
+ - *BREAKING* You must indicate your acceptance of Let's Encrypt's terms
7
+ and conditions by setting the `ACME_TERMS_AGREED` configuration variable.
8
+ - *BREAKING* Removed `ACME_ENDPOINT` environment variable reference. We never
9
+ documented that we support alternative endpoints, and we never tested it,
10
+ and the gem is called *letsencrypt*-rails-heroku, so let's not pretend.
11
+ Please get in touch if you were using this configuration variable, we'd
12
+ like to hear from you! Psst; you can still set `acme_directory` when
13
+ configuring the gem in an initializer.
14
+ - Use version 2 of the ACME API, paving the way for DNS validation.
15
+ - Save private key & key ID variables after registering with Let's Encrypt.
16
+ This will create two new permanent environment variables, `ACME_PRIVATE_KEY`
17
+ and `ACME_KEY_ID`.
18
+
1
19
  # 1.2.1
2
20
 
3
21
  - Update `rack` and `nokogiri` dependencies due to reported vulnerabilities
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem 'acme-client', '~> 0.4.0'
3
+ gem 'acme-client', '~> 2.0'
4
4
  gem 'platform-api', '~> 2.2'
5
5
 
6
6
  group :development do
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- acme-client (0.4.1)
4
+ acme-client (2.0.3)
5
5
  faraday (~> 0.9, >= 0.9.1)
6
6
  activesupport (5.0.0)
7
7
  concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -86,7 +86,7 @@ PLATFORMS
86
86
  ruby
87
87
 
88
88
  DEPENDENCIES
89
- acme-client (~> 0.4.0)
89
+ acme-client (~> 2.0)
90
90
  bundler (~> 1.0)
91
91
  juwelier (~> 2.1.0)
92
92
  platform-api (~> 2.2)
data/README.md CHANGED
@@ -61,19 +61,24 @@ end
61
61
 
62
62
  ## Configuring
63
63
 
64
- By default the gem will try to use the following set of configuration variables,
65
- which you should set.
64
+ By default the gem will try to use the following set of configuration
65
+ variables. You must set:
66
+
67
+ * `ACME_EMAIL`: Your email address, should be valid.
68
+ * `ACME_TERMS_AGREED`: Existence of this environment variable represents your
69
+ agreement to [Let's Encrypt's terms of service](https://letsencrypt.org/repository/).
70
+ * `HEROKU_TOKEN`: An API token for this app. See below
71
+ * `HEROKU_APP`: Name of Heroku app e.g. bottomless-cavern-7173
72
+
73
+ You can also set:
66
74
 
67
75
  * `ACME_DOMAIN`: Comma separated list of domains for which you want
68
76
  certificates, e.g. `example.com,www.example.com`. Your Heroku app should be
69
- configured to answer to all these domains, because LetsEncrypt will make a
77
+ configured to answer to all these domains, because Let's Encrypt will make a
70
78
  request to verify ownership.
71
79
 
72
80
  If you leave this blank, the gem will try and use the Heroku API to get a
73
81
  list of configured domains for your app, and verify all of them.
74
- * `ACME_EMAIL`: Your email address, should be valid.
75
- * `HEROKU_TOKEN`: An API token for this app. See below
76
- * `HEROKU_APP`: Name of Heroku app e.g. bottomless-cavern-7173
77
82
  * `SSL_TYPE`: Optional: One of `sni` or `endpoint`, defaults to `sni`.
78
83
  `endpoint` requires your app to have an
79
84
  [SSL endpoint addon](https://elements.heroku.com/addons/ssl) configured.
@@ -84,6 +89,14 @@ the challenge / validation process:
84
89
  * `ACME_CHALLENGE_FILENAME`: The path of the file LetsEncrypt will request.
85
90
  * `ACME_CHALLENGE_FILE_CONTENT`: The content of that challenge file.
86
91
 
92
+ It will also create two permanent environment variables after the first run:
93
+
94
+ * `ACME_PRIVATE_KEY`: Private key used to create requests for certificates.
95
+ * `ACME_KEY_ID`: Key ID assigned to your private key by Let's Encrypt.
96
+
97
+ If you remove these, a new account will be created and new environment
98
+ variables will be set.
99
+
87
100
  ## Creating a Heroku token
88
101
 
89
102
  Use the `heroku-oauth` toolbelt plugin to generate an access token suitable
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.1
1
+ 2.0.0
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: letsencrypt-rails-heroku 1.2.1 ruby lib
5
+ # stub: letsencrypt-rails-heroku 2.0.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "letsencrypt-rails-heroku".freeze
9
- s.version = "1.2.1"
9
+ s.version = "2.0.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Pixie Labs".freeze, "David Somers".freeze, "Abigail McPhillips".freeze]
14
- s.date = "2019-04-12"
14
+ s.date = "2019-05-17"
15
15
  s.description = "This gem automatically handles creation, renewal, and applying SSL certificates from LetsEncrypt to your Heroku account.".freeze
16
16
  s.email = "team@pixielabs.io".freeze
17
17
  s.extra_rdoc_files = [
@@ -44,7 +44,7 @@ Gem::Specification.new do |s|
44
44
  s.specification_version = 4
45
45
 
46
46
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
- s.add_runtime_dependency(%q<acme-client>.freeze, ["~> 0.4.0"])
47
+ s.add_runtime_dependency(%q<acme-client>.freeze, ["~> 2.0"])
48
48
  s.add_runtime_dependency(%q<platform-api>.freeze, ["~> 2.2"])
49
49
  s.add_development_dependency(%q<shoulda>.freeze, [">= 0"])
50
50
  s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
@@ -52,7 +52,7 @@ Gem::Specification.new do |s|
52
52
  s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
53
53
  s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
54
54
  else
55
- s.add_dependency(%q<acme-client>.freeze, ["~> 0.4.0"])
55
+ s.add_dependency(%q<acme-client>.freeze, ["~> 2.0"])
56
56
  s.add_dependency(%q<platform-api>.freeze, ["~> 2.2"])
57
57
  s.add_dependency(%q<shoulda>.freeze, [">= 0"])
58
58
  s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
@@ -61,7 +61,7 @@ Gem::Specification.new do |s|
61
61
  s.add_dependency(%q<simplecov>.freeze, [">= 0"])
62
62
  end
63
63
  else
64
- s.add_dependency(%q<acme-client>.freeze, ["~> 0.4.0"])
64
+ s.add_dependency(%q<acme-client>.freeze, ["~> 2.0"])
65
65
  s.add_dependency(%q<platform-api>.freeze, ["~> 2.2"])
66
66
  s.add_dependency(%q<shoulda>.freeze, [">= 0"])
67
67
  s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
@@ -1,12 +1,16 @@
1
1
  module Letsencrypt
2
2
  module Error
3
- # Exception raised when LetsEncrypt encounters an issue verifying the challenge.
3
+ # LetsEncrypt encountered an issue verifying the challenge.
4
4
  class VerificationError < StandardError; end
5
- # Exception raised when challenge URL is not available.
5
+ # LetsEncrypt encountered an issue finalizing the order.
6
+ class FinalizationError < StandardError; end
7
+ # Challenge URL is not available.
6
8
  class ChallengeUrlError < StandardError; end
7
- # Exception raised on timeout of challenge verification.
9
+ # Domain verification took longer than we'd like.
8
10
  class VerificationTimeoutError < StandardError; end
9
- # Exception raised when an error occurs adding the certificate to Heroku.
11
+ # Order finalization took longer than we'd like.
12
+ class FinalizationTimeoutError < StandardError; end
13
+ # Error adding the certificate to Heroku.
10
14
  class HerokuCertificateError < StandardError; end
11
15
  end
12
16
  end
@@ -14,26 +14,36 @@ module Letsencrypt
14
14
  configuration.acme_challenge_file_content
15
15
  end
16
16
 
17
+ def self.registered?
18
+ configuration.acme_private_key && configuration.acme_key_id
19
+ end
20
+
17
21
  class Configuration
18
22
  attr_accessor :heroku_token, :heroku_app, :acme_email, :acme_domain,
19
- :acme_endpoint, :ssl_type
23
+ :acme_directory, :ssl_type, :acme_terms_agreed
20
24
 
21
25
  # Not settable by user; part of the gem's behaviour.
22
- attr_reader :acme_challenge_filename, :acme_challenge_file_content
26
+ attr_reader :acme_challenge_filename, :acme_challenge_file_content,
27
+ :acme_private_key, :acme_key_id
23
28
 
24
29
  def initialize
25
30
  @heroku_token = ENV["HEROKU_TOKEN"]
26
31
  @heroku_app = ENV["HEROKU_APP"]
27
32
  @acme_email = ENV["ACME_EMAIL"]
28
33
  @acme_domain = ENV["ACME_DOMAIN"]
29
- @acme_endpoint = ENV["ACME_ENDPOINT"] || 'https://acme-v01.api.letsencrypt.org/'
34
+ @acme_directory = 'https://acme-v02.api.letsencrypt.org/directory'
35
+ @acme_terms_agreed = ENV["ACME_TERMS_AGREED"]
30
36
  @ssl_type = ENV["SSL_TYPE"] || 'sni'
37
+
31
38
  @acme_challenge_filename = ENV["ACME_CHALLENGE_FILENAME"]
32
39
  @acme_challenge_file_content = ENV["ACME_CHALLENGE_FILE_CONTENT"]
40
+
41
+ @acme_private_key = ENV["ACME_PRIVATE_KEY"]
42
+ @acme_key_id = ENV["ACME_KEY_ID"]
33
43
  end
34
44
 
35
45
  def valid?
36
- heroku_token && heroku_app && acme_email
46
+ heroku_token && heroku_app && acme_email && acme_terms_agreed
37
47
  end
38
48
  end
39
49
  end
@@ -8,24 +8,48 @@ namespace :letsencrypt do
8
8
  desc 'Renew your LetsEncrypt certificate'
9
9
  task :renew do
10
10
  # Check configuration looks OK
11
- abort "letsencrypt-rails-heroku is configured incorrectly. Are you missing an environment variable or other configuration? You should have a heroku_token, heroku_app and acme_email configured either via a `Letsencrypt.configure` block in an initializer or as environment variables." unless Letsencrypt.configuration.valid?
11
+ abort "letsencrypt-rails-heroku is configured incorrectly. Are you missing an environment variable or other configuration? You should have heroku_token, heroku_app, acme_email and acme_terms_agreed configured either via a `Letsencrypt.configure` block in an initializer or as environment variables." unless Letsencrypt.configuration.valid?
12
12
 
13
13
  # Set up Heroku client
14
14
  heroku = PlatformAPI.connect_oauth Letsencrypt.configuration.heroku_token
15
15
  heroku_app = Letsencrypt.configuration.heroku_app
16
16
 
17
- # Create a private key
18
- print "Creating account key..."
19
- private_key = OpenSSL::PKey::RSA.new(4096)
20
- puts "Done!"
17
+ if Letsencrypt.registered?
18
+ puts "Using existing registration details"
19
+ private_key = OpenSSL::PKey::RSA.new(Letsencrypt.configuration.acme_private_key)
20
+ key_id = Letsencrypt.configuration.acme_key_id
21
+ else
22
+ # Create a private key
23
+ print "Creating account key..."
24
+ private_key = OpenSSL::PKey::RSA.new(4096)
25
+ puts "Done!"
21
26
 
22
- client = Acme::Client.new(private_key: private_key, endpoint: Letsencrypt.configuration.acme_endpoint, connection_options: { request: { open_timeout: 5, timeout: 5 } })
27
+ client = Acme::Client.new(private_key: private_key,
28
+ directory: Letsencrypt.configuration.acme_directory,
29
+ connection_options: {
30
+ request: {
31
+ open_timeout: 5,
32
+ timeout: 5
33
+ }
34
+ })
23
35
 
24
- print "Registering with LetsEncrypt..."
25
- registration = client.register(contact: "mailto:#{Letsencrypt.configuration.acme_email}")
36
+ print "Registering with LetsEncrypt..."
37
+ account = client.new_account(contact: "mailto:#{Letsencrypt.configuration.acme_email}",
38
+ terms_of_service_agreed: true)
26
39
 
27
- registration.agree_terms
28
- puts "Done!"
40
+ key_id = account.kid
41
+ puts "Done!"
42
+ print "Saving account details as configuration variables..."
43
+ heroku.config_var.update(heroku_app,
44
+ 'ACME_PRIVATE_KEY' => private_key.to_pem,
45
+ 'ACME_KEY_ID' => account.kid)
46
+ puts "Done!"
47
+ end
48
+
49
+ # Make a new Acme::Client with whichever private_key & key_id we ended up with.
50
+ client = Acme::Client.new(private_key: private_key,
51
+ directory: Letsencrypt.configuration.acme_directory,
52
+ kid: key_id)
29
53
 
30
54
  domains = []
31
55
  if Letsencrypt.configuration.acme_domain
@@ -36,11 +60,12 @@ namespace :letsencrypt do
36
60
  puts "Using #{domains.length} configured Heroku domain(s) for this app..."
37
61
  end
38
62
 
39
- domains.each do |domain|
40
- puts "Performing verification for #{domain}:"
63
+ order = client.new_order(identifiers: domains)
41
64
 
42
- authorization = client.authorize(domain: domain)
43
- challenge = authorization.http01
65
+ order.authorizations.each do |authorization|
66
+ puts "Performing verification for #{authorization.domain}:"
67
+
68
+ challenge = authorization.http
44
69
 
45
70
  print "Setting config vars on Heroku..."
46
71
  heroku.config_var.update(heroku_app, {
@@ -78,24 +103,23 @@ namespace :letsencrypt do
78
103
 
79
104
  print "Giving LetsEncrypt some time to verify..."
80
105
  # Once you are ready to serve the confirmation request you can proceed.
81
- challenge.request_verification # => true
82
- challenge.verify_status # => 'pending'
106
+ challenge.request_validation
83
107
 
84
108
  start_time = Time.now
85
-
86
- while challenge.verify_status == 'pending'
109
+ while challenge.status == 'pending'
87
110
  if Time.now - start_time >= 30
88
111
  failure_message = "Failed - timed out waiting for challenge verification."
89
112
  raise Letsencrypt::Error::VerificationTimeoutError, failure_message
90
113
  end
91
- sleep(3)
114
+ sleep(2)
115
+ challenge.reload
92
116
  end
93
117
 
94
118
  puts "Done!"
95
119
 
96
- unless challenge.verify_status == 'valid'
120
+ unless challenge.status == 'valid'
97
121
  puts "Problem verifying challenge."
98
- failure_message = "Status: #{challenge.verify_status}, Error: #{challenge.error}"
122
+ failure_message = "Status: #{challenge.status}, Error: #{challenge.error}"
99
123
  raise Letsencrypt::Error::VerificationError, failure_message
100
124
  end
101
125
 
@@ -110,10 +134,33 @@ namespace :letsencrypt do
110
134
  })
111
135
 
112
136
  # Create CSR
113
- csr = Acme::Client::CertificateRequest.new(names: domains)
137
+ csr_private_key = OpenSSL::PKey::RSA.new 4096
138
+ csr = Acme::Client::CertificateRequest.new(names: domains,
139
+ private_key: csr_private_key)
114
140
 
141
+ print "Asking LetsEncrypt to finalize our certificate order..."
115
142
  # Get certificate
116
- certificate = client.new_certificate(csr) # => #<Acme::Client::Certificate ....>
143
+ order.finalize(csr: csr)
144
+
145
+ # Wait for order to process
146
+ start_time = Time.now
147
+ while order.status == 'processing'
148
+ if Time.now - start_time >= 30
149
+ failure_message = "Failed - timed out waiting for order finalization"
150
+ raise Letsencrypt::Error::FinalizationTimeoutError, failure_message
151
+ end
152
+ sleep(2)
153
+ order.reload
154
+ end
155
+
156
+ puts "Done!"
157
+
158
+ unless order.status == 'valid'
159
+ failure_message = "Problem finalizing order - status: #{order.status}"
160
+ raise Letsencrypt::Error::FinalizationError, failure_message
161
+ end
162
+
163
+ certificate = order.certificate # => PEM-formatted certificate
117
164
 
118
165
  # Send certificates to Heroku via API
119
166
 
@@ -124,23 +171,24 @@ namespace :letsencrypt do
124
171
  heroku.ssl_endpoint
125
172
  end
126
173
 
127
- # First check for existing certificates:
128
- certificates = endpoint.list(heroku_app)
174
+ certificate_info = {
175
+ certificate_chain: certificate,
176
+ private_key: csr_private_key.to_pem
177
+ }
178
+
179
+ # Fetch existing certificate from Heroku (if any). We just use the first
180
+ # one; if someone has more than one, they're probably not actually using
181
+ # this gem. Could also be an error?
182
+ existing_certificate = endpoint.list(heroku_app)[0]
129
183
 
130
184
  begin
131
- if certificates.any?
132
- print "Updating existing certificate #{certificates[0]['name']}..."
133
- endpoint.update(heroku_app, certificates[0]['name'], {
134
- certificate_chain: certificate.fullchain_to_pem,
135
- private_key: certificate.request.private_key.to_pem
136
- })
185
+ if existing_certificate
186
+ print "Updating existing certificate #{existing_certificate['name']}..."
187
+ endpoint.update(heroku_app, existing_certificates['name'], certificate_info)
137
188
  puts "Done!"
138
189
  else
139
190
  print "Adding new certificate..."
140
- endpoint.create(heroku_app, {
141
- certificate_chain: certificate.fullchain_to_pem,
142
- private_key: certificate.request.private_key.to_pem
143
- })
191
+ endpoint.create(heroku_app, certificate_info)
144
192
  puts "Done!"
145
193
  end
146
194
  rescue Excon::Error::UnprocessableEntity => e
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: letsencrypt-rails-heroku
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pixie Labs
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2019-04-12 00:00:00.000000000 Z
13
+ date: 2019-05-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: acme-client
@@ -18,14 +18,14 @@ dependencies:
18
18
  requirements:
19
19
  - - "~>"
20
20
  - !ruby/object:Gem::Version
21
- version: 0.4.0
21
+ version: '2.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - "~>"
27
27
  - !ruby/object:Gem::Version
28
- version: 0.4.0
28
+ version: '2.0'
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: platform-api
31
31
  requirement: !ruby/object:Gem::Requirement