letsencrypt-rails-heroku 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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