apartment_acme_client 0.0.3 → 0.0.5

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: e9e32d511e21e812a21bae30429bb7d11a0cdc62c878df8a73ffad85d0f38821
4
- data.tar.gz: 1b33b1bf1e99fdc674198994b76acec4fe302f625a59a712051c28b2c6a0ab50
3
+ metadata.gz: 66eaebf9ae761526e149a5bedd237deee499a95107edb9ce3c1e3916e29c058b
4
+ data.tar.gz: 4ab90728f136c38b453a24f54b760c84539c4333ae0cb24ba4122dcb54c124b6
5
5
  SHA512:
6
- metadata.gz: 52706bcc1e28859eeb2ce40391a31512d3382c4cc00726f4deb9cd9d974f464e7aca378b0a68974dadccc3f660ede923740c87938dd54a7b2200962c89f44db8
7
- data.tar.gz: 12dc42f69d30fba1d42bdb5415249730b9e57c61a5159a25249a1db519266c3f702a53fea6c6f93fcb1136a1071ff8818ffa34e6b0acf4ae34e3c2f1541b7e63
6
+ metadata.gz: ff22698eadfef2b8b2e6d0c25793ad5bb1228ff66262c039ef01b969aabb42458caf4d3e6f0f8485a4eca5273b878328661437fe4591f6d809317a44811f1ae1
7
+ data.tar.gz: 870ed58bb9befb87b788fa1976f4fd73b052982cc1b57be889c2783a1ec55372ce24d155b286fd5937c24bb910bbb69ece5cf101b3e7e298d8117adc51d7f100
data/README.md CHANGED
@@ -15,29 +15,27 @@ The goal of this gem is to solve the following problems:
15
15
 
16
16
  1) Make it easy to start using let's encrypt for multiple domains on one server
17
17
  1) Make it easy to periodically refresh a certificate which handles many domains
18
- 1) Make it possible to add a new DNS entry which refers to the server, and request a cert which now also covers that new domain.
18
+ 1) Make it possible to add a new custom DNS entry which refers to the server, and request a cert which now also covers that new domain.
19
+ 1) Make it easy to request a wildcard cert as well as individual domain certs
19
20
  1) Make it resilient, if a DNS record is removed, handle that by removing that domain from list requested for the cert.
20
21
 
21
22
  **Example Situation**:
22
23
 
23
- - Your application is known as example.com
24
+ - Your application is known as site.example.com
24
25
  - You allow users to create new accounts, and assign each account a separate subdomain,
25
- - e.g. alice.example.com, bob.example.com, charlie.example.com
26
+ - e.g. alice.site.example.com, bob.site.example.com, charlie.site.example.com
26
27
  - You allow users to also whitelabel the service by buying their own domains, and setting up CNAME records:
27
- - e.g. www.alice.com -> CNAME: alice.example.com
28
- - e.g. bobrocks.com -> CNAME: bob.example.com
28
+ - e.g. www.alice.com -> CNAME: alice.site.example.com
29
+ - e.g. bobrocks.com -> CNAME: bob.site.example.com
29
30
 
30
31
  **What can ApartmentAcmeClient do?**
31
32
 
32
33
  - Create a single Let's Encrypt SSL Certificate which covers all of:
33
- - example.com
34
- - alice.example.com
35
- - bob.example.com
36
- - charlie.example.com
34
+ - site.example.com
35
+ - *.site.example.com (which covers alice.site.example.com, bob.site.example.com, charlie.site.example.com)
37
36
  - www.alice.com
38
37
  - bobrocks.com
39
38
 
40
-
41
39
  SSL Certificates
42
40
  ----------------
43
41
 
@@ -70,6 +68,8 @@ See below for a detailed explanation of "First Time Setup"
70
68
 
71
69
  When setting this up the first time, it is recommended that you enable test-mode:
72
70
  ```ruby
71
+ # in config/initializers/apartment_acme_client.rb
72
+
73
73
  ApartmentAcmeClient.lets_encrypt_test_server_enabled = true
74
74
  ```
75
75
 
@@ -131,12 +131,27 @@ Define the code which will list the domains to check.
131
131
  # Should return an array of domains (without http/https prefixes)
132
132
 
133
133
  # It can be a straight array, or a callable object
134
- ApartmentAcmeClient.domains_to_check = -> { SomeModel.all.map(&:subdomain) }
134
+ # These should be all of the domains which are NOT
135
+ # covered by the wildcard settings
136
+ ApartmentAcmeClient.domains_to_check = -> { SomeModel.all.map(&:custom_domain) }
137
+ ApartmentAcmeClient.wildcard_domain = "site.example.com" # optional element
135
138
 
136
139
  # e.g.
137
140
  # ApartmentAcmeClient.domains_to_check = ["example.com", "alice.example.com", "alice.com"]
138
141
  ```
139
142
 
143
+ #### Wildcard domain
144
+
145
+ You can request a wildcard certificate for a domain (or a subdomain). In order to do this, the system must be able to write to the DNS provider.
146
+
147
+ Currently, only Route53 is supported as a DNS provider, and we use an `upsert` to write a TXT record to the system, in order to prove that we control the DNS for the domain.
148
+
149
+ If you specify `wildcard_domain` (the domain on which to request a wildcard cert), we will request a wilcard cert for `*.<wildcard_domain>`, and use AWS Route53 API to perform the domain-authorization.
150
+
151
+ The necessary permissions to be able to update the Route53 records for wildcard-cert update are:
152
+ - route53:ListHostedZones
153
+ - route53:ChangeResourceRecordSets
154
+
140
155
  #### Specify the common-name domain
141
156
 
142
157
  This is used to identify the certificate requested, and should be the same from week-to-week.
@@ -254,15 +269,14 @@ At this point, the only thing necessary is to run `rake encryption:renew_and_upd
254
269
 
255
270
  Each week, the certificates should be renewed. We have provided 2 ways to do this.
256
271
 
257
- a rake task:
272
+ straight invocation:
258
273
  ```ruby
259
- rake "encryption:renew_and_update_certificate"
274
+ ApartmentAcmeClient::RenewalService.run!
260
275
  ```
261
276
 
262
- straight invocation:
263
-
277
+ we provide a helper rake task:
264
278
  ```ruby
265
- ApartmentAcmeClient::RenewalService.run!
279
+ rake "encryption:renew_and_update_certificate"
266
280
  ```
267
281
 
268
282
  Please use whatever scheduling service you wish in order to ensure that this runs periodically.
data/Rakefile CHANGED
@@ -17,7 +17,6 @@ end
17
17
  APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
18
  load 'rails/tasks/engine.rake'
19
19
 
20
-
21
20
  load 'rails/tasks/statistics.rake'
22
21
 
23
22
  require 'bundler/gem_tasks'
@@ -7,6 +7,9 @@ require "apartment_acme_client/acme_client/proxy"
7
7
  require "apartment_acme_client/acme_client/real_client"
8
8
  require "apartment_acme_client/certificate_storage/proxy"
9
9
  require "apartment_acme_client/certificate_storage/s3"
10
+ require "apartment_acme_client/dns_api/check_dns"
11
+ require "apartment_acme_client/dns_api/fake"
12
+ require "apartment_acme_client/dns_api/route53"
10
13
  require "apartment_acme_client/nginx_configuration/proxy"
11
14
  require "apartment_acme_client/nginx_configuration/real"
12
15
  require "apartment_acme_client/file_manipulation/proxy"
@@ -27,6 +30,9 @@ module ApartmentAcmeClient
27
30
  end
28
31
  end
29
32
 
33
+ # An optional domain which will we request a wildcard certificate for
34
+ mattr_accessor :wildcard_domain
35
+
30
36
  # The base domain, a domain which is always going to be accessible.
31
37
  # because we need a common domain to be used on each request.
32
38
  # if not defined, the first 'domain_to_check' which succeeds will be used
@@ -1,21 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'acme-client'
2
4
 
3
5
  module ApartmentAcmeClient
4
6
  module AcmeClient
5
7
  class RealClient
6
- def initialize(private_key:)
8
+ attr_reader :csr_private_key
9
+ def initialize(acme_client_private_key:, csr_private_key:)
7
10
  @client = Acme::Client.new(
8
- private_key: private_key,
9
- endpoint: server_endpoint,
11
+ private_key: acme_client_private_key,
12
+ directory: server_directory
10
13
  )
14
+ @csr_private_key = csr_private_key
11
15
  end
12
16
 
13
17
  def register(email)
14
18
  # If the private key is not known to the server, we need to register it for the first time.
15
- registration = @client.register(contact: "mailto:#{email}")
16
-
17
- # You may need to agree to the terms of service (that's up the to the server to require it or not but boulder does by default)
18
- registration.agree_terms
19
+ account = @client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
20
+ Rollbar.info("New Let's Encrypt Account created with KID: #{account.kid}")
19
21
 
20
22
  true
21
23
  end
@@ -24,31 +26,35 @@ module ApartmentAcmeClient
24
26
  @client.authorize(domain: domain)
25
27
  end
26
28
 
29
+ def new_order(identifiers:)
30
+ @client.new_order(identifiers: identifiers)
31
+ end
32
+
27
33
  # Create a Certificate for our new set of domain names
28
34
  # returns that certificate
29
- def request_certificate(common_name:, domains:)
35
+ def request_certificate(common_name:, names:, order:)
30
36
  # We're going to need a certificate signing request. If not explicitly
31
37
  # specified, the first name listed becomes the common name.
32
- csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: domains)
33
-
34
- # We can now request a certificate. You can pass anything that returns
35
- # a valid DER encoded CSR when calling to_der on it. For example an
36
- # OpenSSL::X509::Request should work too.
37
- certificate = @client.new_certificate(csr) # => #<Acme::Client::Certificate ....>
38
+ csr = Acme::Client::CertificateRequest.new(private_key: csr_private_key, common_name: common_name, names: names)
39
+ order.finalize(csr: csr)
40
+ while order.status == 'processing'
41
+ sleep(1)
42
+ order.reload
43
+ end
38
44
 
39
- certificate
45
+ order.certificate
40
46
  end
41
47
 
42
48
  private
43
49
 
44
- def server_endpoint
50
+ def server_directory
45
51
  # We need an ACME server to talk to, see github.com/letsencrypt/boulder
46
52
  # WARNING: This endpoint is the production endpoint, which is rate limited and will produce valid certificates.
47
53
  # You should probably use the staging endpoint for all your experimentation:
48
54
  if ApartmentAcmeClient.lets_encrypt_test_server_enabled
49
- 'https://acme-staging.api.letsencrypt.org/'
55
+ 'https://acme-staging-v02.api.letsencrypt.org/directory'
50
56
  else
51
- 'https://acme-v01.api.letsencrypt.org/'
57
+ 'https://acme-v02.api.letsencrypt.org/directory'
52
58
  end
53
59
  end
54
60
  end
@@ -1,36 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'aws-sdk-s3'
2
4
 
3
5
  module ApartmentAcmeClient
4
6
  module CertificateStorage
5
7
  class S3
6
8
  def initialize
7
- @base_prefix = if ApartmentAcmeClient::lets_encrypt_test_server_enabled
8
- TEST_PREFIX
9
- else
10
- ""
11
- end
9
+ @base_prefix = if ApartmentAcmeClient.lets_encrypt_test_server_enabled
10
+ TEST_PREFIX
11
+ else
12
+ ''
13
+ end
12
14
  end
13
15
 
14
- ENCRYPTION_S3_NAME = 'server_encryption_client_private_key.der'.freeze
16
+ ENCRYPTION_S3_NAME = 'server_encryption_client_private_key.der'
17
+ CSR_ENCRYPTION_S3_NAME = 'csr_server_encryption_client_private_key.der'
15
18
 
16
- def store_certificate(certificate)
17
- # Save the certificate and the private key to files
18
- File.write(cert_path("privkey.pem"), certificate.request.private_key.to_pem)
19
- File.write(cert_path("cert.pem"), certificate.to_pem)
20
- File.write(cert_path("chain.pem"), certificate.chain_to_pem)
21
- File.write(cert_path("fullchain.pem"), certificate.fullchain_to_pem)
19
+ def store_certificate_string(certificate_string)
20
+ File.write(cert_path('cert.pem'), certificate_string)
21
+ store_s3_file(derived_filename('cert.pem'), certificate_string)
22
+ end
22
23
 
23
- store_s3_file(derived_filename("privkey.pem"), certificate.request.private_key.to_pem)
24
- store_s3_file(derived_filename("cert.pem"), certificate.to_pem)
25
- store_s3_file(derived_filename("chain.pem"), certificate.chain_to_pem)
26
- store_s3_file(derived_filename("fullchain.pem"), certificate.fullchain_to_pem)
24
+ def store_csr_private_key_string(csr_private_key_string)
25
+ File.write(cert_path('privkey.pem'), csr_private_key_string)
26
+ store_s3_file(derived_filename('privkey.pem'), csr_private_key_string)
27
27
  end
28
28
 
29
29
  # do we have a certificate on this server?
30
30
  # We cannot start nginx when it is pointing at a non-existing certificate,
31
31
  # so we need to check
32
32
  def cert_exists?
33
- File.exist?(cert_path("privkey.pem"))
33
+ File.exist?(cert_path('privkey.pem'))
34
34
  end
35
35
 
36
36
  def private_key
@@ -40,17 +40,32 @@ module ApartmentAcmeClient
40
40
  s3_object.get.body.read
41
41
  end
42
42
 
43
+ def csr_private_key
44
+ s3_object = s3_file(csr_private_key_s3_filename)
45
+ return nil unless s3_object.exists?
46
+
47
+ s3_object.get.body.read
48
+ end
49
+
43
50
  # saves a private key to s3
44
51
  def save_private_key(private_key)
45
52
  store_s3_file(private_key_s3_filename, private_key.to_der)
46
53
  end
47
54
 
55
+ def save_csr_private_key(private_key)
56
+ store_s3_file(csr_private_key_s3_filename, private_key.to_der)
57
+ end
58
+
48
59
  private
49
60
 
50
61
  def private_key_s3_filename
51
62
  derived_filename(ENCRYPTION_S3_NAME)
52
63
  end
53
64
 
65
+ def csr_private_key_s3_filename
66
+ derived_filename(CSR_ENCRYPTION_S3_NAME)
67
+ end
68
+
54
69
  def derived_filename(filename)
55
70
  "#{@base_prefix}#{filename}"
56
71
  end
@@ -0,0 +1,65 @@
1
+ module ApartmentAcmeClient
2
+ module DnsApi
3
+ # Check to see if a particular DNS record is
4
+ # present.
5
+ class CheckDns
6
+ attr_reader :root_domain, :dns_record
7
+
8
+ def initialize(root_domain, dns_record)
9
+ # ensure we only have the TLD, not a subdomain
10
+ @root_domain = root_domain.split(".").last(2).join(".")
11
+ @dns_record = dns_record
12
+ end
13
+
14
+ # Search DNS recodrs for any entries which are TXT and include
15
+ # the text 'value'
16
+ def check_dns(value)
17
+ valid = true
18
+
19
+ nameservers.each do |nameserver|
20
+ begin
21
+ records = Resolv::DNS.open(nameserver: nameserver) do |dns|
22
+ dns.getresources(
23
+ dns_record,
24
+ Resolv::DNS::Resource::IN::TXT
25
+ )
26
+ end
27
+ records = records.map(&:strings).flatten
28
+ valid = records.include?(value)
29
+ rescue Resolv::ResolvError
30
+ return false
31
+ end
32
+ return false unless valid
33
+ end
34
+
35
+ valid
36
+ end
37
+
38
+ def nameservers
39
+ return @nameservers if defined?(@nameservers)
40
+
41
+ @nameservers = []
42
+ Resolv::DNS.open(nameserver: '8.8.8.8') do |dns|
43
+ while nameservers.empty?
44
+ @nameservers = dns.getresources(
45
+ root_domain,
46
+ Resolv::DNS::Resource::IN::NS
47
+ ).map(&:name).map(&:to_s)
48
+ end
49
+ end
50
+
51
+ @nameservers
52
+ end
53
+
54
+ def wait_for_present(value, timeout_seconds: 60)
55
+ time = 1
56
+ until check_dns(value)
57
+ puts "Waiting for DNS to update"
58
+ sleep 1
59
+ time += 1
60
+ break if time > timeout_seconds
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,6 @@
1
+ module ApartmentAcmeClient
2
+ module DnsApi
3
+ class Fake
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,90 @@
1
+ require 'aws-sdk-route53'
2
+
3
+ module ApartmentAcmeClient
4
+ module DnsApi
5
+ # based on https://www.petekeen.net/lets-encrypt-without-certbot
6
+ class Route53
7
+ # The domain being requested for DNS update
8
+ # e.g. "site.example.com"
9
+ attr_reader :requested_domain
10
+
11
+ # the DNS TXT record label (full label, including domain)
12
+ attr_reader :label
13
+
14
+ # will be TXT
15
+ attr_reader :record_type
16
+
17
+ # array of value keys to be written
18
+ # (for wildcard certs, you'll have one for *.example.com, and one for example.com)
19
+ # e.g. ["One", "Two"]
20
+ attr_reader :values
21
+
22
+ def initialize(requested_domain:, dns_record_label:, record_type:, values:)
23
+ @requested_domain = requested_domain
24
+ @label = dns_record_label
25
+ @record_type = record_type
26
+ @values = values
27
+ end
28
+
29
+ # NOTE:
30
+ # if you get error like:
31
+ #
32
+ # "Invalid Resource Record: FATAL problem:
33
+ # InvalidCharacterString
34
+ # (Value should be enclosed in quotation marks) encountered with <value>"
35
+ #
36
+ # this means that the "Value" should include escape quotes.
37
+ # e.g. values: ["\"Something\"", "\"Other Thing\""]
38
+ def write_record
39
+ route53.change_resource_record_sets(options)
40
+ end
41
+
42
+ private
43
+
44
+ def options
45
+ change = {
46
+ action: 'UPSERT',
47
+ resource_record_set: {
48
+ name: label,
49
+ type: record_type,
50
+ ttl: 1,
51
+ resource_records: resource_record_values
52
+ }
53
+ }
54
+
55
+ {
56
+ hosted_zone_id: zone.id,
57
+ change_batch: {
58
+ changes: [change]
59
+ }
60
+ }
61
+ end
62
+
63
+ def root_domain
64
+ requested_domain.split(".").last(2).join(".")
65
+ end
66
+
67
+ def zone
68
+ @zone = route53.list_hosted_zones(max_items: 100)
69
+ .hosted_zones
70
+ .detect { |z| z.name = "#{root_domain}." }
71
+ end
72
+
73
+ def route53
74
+ # Note: The `region` doesn't matter, because Route53 is global.
75
+ @route53 ||= Aws::Route53::Client.new(region: 'us-east-1')
76
+ end
77
+
78
+ # createt an AwsRoute53 upsert with multiple value entries
79
+ def resource_record_values
80
+ values.map do |value|
81
+ if value.include?("\"")
82
+ { value: value }
83
+ else
84
+ { value: "\"#{value}\"" }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  # Initially, the system is only accessible via subdomain.example.com
4
6
  # But, as we add more Conventions, we want to be able to access those also,
5
7
  # thus we will need:
6
- # - a.subdomain.example.com
7
- # - b.subdomain.example.com
8
- # - c.subdomain.example.com
8
+ # - *.subdomain.example.com
9
9
  #
10
10
  # Also, each convention may add an "alias" for their convention, like:
11
11
  # - www.naucc.com
@@ -21,20 +21,21 @@ require 'openssl'
21
21
  #
22
22
  # Manage the encryption of the website (https).
23
23
  module ApartmentAcmeClient
24
- class Encryption
24
+ class Encryption # rubocop:disable Metrics/ClassLength
25
25
  def initialize
26
26
  @certificate_storage = ApartmentAcmeClient::CertificateStorage::Proxy.singleton
27
27
  end
28
28
 
29
29
  # Largely based on https://github.com/unixcharles/acme-client documentation
30
30
  def register_new(email)
31
- raise StandardError.new("Private key already exists") unless @certificate_storage.private_key.nil?
31
+ raise StandardError.new('Private key already exists') unless @certificate_storage.private_key.nil?
32
32
 
33
33
  private_key = create_private_key
34
34
 
35
35
  # Initialize the client
36
36
  new_client = ApartmentAcmeClient::AcmeClient::Proxy.singleton(
37
- private_key: private_key,
37
+ acme_client_private_key: private_key,
38
+ csr_private_key: nil, # not needed for 'register' call
38
39
  )
39
40
 
40
41
  new_client.register(email)
@@ -42,22 +43,75 @@ module ApartmentAcmeClient
42
43
  @certificate_storage.save_private_key(private_key)
43
44
  end
44
45
 
45
- def authorize_domains(domains)
46
- successful_domains = domains.select {|domain| authorize_domain(domain) }
47
- successful_domains
46
+ # Authorize a wildcard cert domain.
47
+ # to do this, we have to write to the Amazon Route53 DNS entry
48
+ # params:
49
+ # - authorizations - a list of authorizations, which may be http or dns based (ignore the non-wildcard ones)
50
+ # - wildcard_domain - the url of the wildcard's base domain (e.g. "site.example.com")
51
+ def authorize_domains_with_dns(authorizations, wildcard_domain:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
52
+ label = nil
53
+ record_type = nil
54
+ values = []
55
+
56
+ dns_authorizations = []
57
+ authorizations.each do |domain_authorization|
58
+ next unless domain_authorization.wildcard || domain_authorization.http.nil?
59
+
60
+ dns_authorizations << domain_authorization.dns
61
+ end
62
+
63
+ dns_authorizations.each do |authorization|
64
+ label = "#{authorization.record_name}.#{wildcard_domain}"
65
+ record_type = authorization.record_type
66
+ value = authorization.record_content
67
+ values << value
68
+ end
69
+
70
+ return unless values.any?
71
+
72
+ route53 = ApartmentAcmeClient::DnsApi::Route53.new(
73
+ requested_domain: wildcard_domain,
74
+ dns_record_label: label,
75
+ record_type: record_type,
76
+ values: values
77
+ )
78
+
79
+ route53.write_record
80
+
81
+ check_dns = ApartmentAcmeClient::DnsApi::CheckDns.new(wildcard_domain, label)
82
+
83
+ check_dns.wait_for_present(values.first)
84
+
85
+ if check_dns.check_dns(values.first)
86
+ # DNS is updated, proceed with cert request
87
+ dns_authorizations.each do |domain_authorization|
88
+ domain_authorization.request_validation
89
+
90
+ 30.times do
91
+ # may be 'pending' initially
92
+ break if domain_authorization.status == 'valid'
93
+
94
+ puts "Waiting for LetsEncrypt to authorize the domain. Status #{domain_authorization.status}"
95
+
96
+ # Wait a bit for the server to make the request, or just blink. It should be fast.
97
+ sleep(2)
98
+ domain_authorization.reload
99
+ end
100
+ end
101
+ else
102
+ # ERROR, DNS not updated in time
103
+ Rollbar.error("DNS Entry not found in timeout")
104
+ end
48
105
  end
49
106
 
50
- # authorizes a domain with letsencrypt server
107
+ # authorizes a single domain with letsencrypt server
51
108
  # returns true on success, false otherwise.
52
109
  #
53
110
  # from https://github.com/unixcharles/acme-client/tree/master#authorize-for-domain
54
- def authorize_domain(domain)
55
- authorization = client.authorize(domain: domain)
111
+ def authorize_domain_with_http(domain_authorization)
112
+ challenge = domain_authorization.http
56
113
 
57
- # This example is using the http-01 challenge type. Other challenges are dns-01 or tls-sni-01.
58
- challenge = authorization.http01
59
-
60
- # The http-01 method will require you to respond to a HTTP request.
114
+ # The http method will require you to respond to a HTTP request.
61
115
 
62
116
  # You can retrieve the challenge token
63
117
  challenge.token # => "some_token"
@@ -90,43 +144,81 @@ module ApartmentAcmeClient
90
144
  # challenge = client.challenge_from_hash(JSON.parse(File.read('challenge')))
91
145
 
92
146
  # Once you are ready to serve the confirmation request you can proceed.
93
- challenge.request_verification # => true
147
+ challenge.request_validation # => true
94
148
 
95
- success = false
96
- 10.times do
97
- if challenge.verify_status == 'valid' # may be 'pending' initially
98
- success = true
99
- break
100
- end
149
+ 30.times do
150
+ # may be 'pending' initially
151
+ break if challenge.status == 'valid'
152
+
153
+ puts "Waiting for letsencrypt to authorize the single domain. Status: #{challenge.status}"
101
154
 
102
155
  # Wait a bit for the server to make the request, or just blink. It should be fast.
103
- sleep(1)
156
+ sleep(2)
157
+ challenge.reload
104
158
  end
105
159
  File.delete(full_challenge_filename)
106
160
 
107
- success
161
+ challenge.status == 'valid'
162
+ end
163
+
164
+ # Create an order, perform authorization for each domain, and then
165
+ # request the certificate.
166
+ # - common name is used so that there is continuity of requests over time
167
+ # - domains are the list of individual http-based domains to be authorized
168
+ # - wildcard_domain is an optional wildcard domain to be authorized via DNS Record
169
+ #
170
+ # Returns the certificate
171
+ def request_certificate(common_name:, domains:, wildcard_domain: nil)
172
+ domain_names_requested = domains
173
+ domain_names_requested += [wildcard_domain, "*.#{wildcard_domain}"] if wildcard_domain.present?
174
+ order = client.new_order(identifiers: domain_names_requested)
175
+
176
+ # Do the HTTP authorizations
177
+ order.authorizations.each do |authorization|
178
+ next if authorization.wildcard || authorization.http.nil?
179
+
180
+ authorize_domain_with_http(authorization)
181
+ end
182
+ # Do the DNS (wildcard) authorizations
183
+ authorize_domains_with_dns(order.authorizations, wildcard_domain: wildcard_domain)
184
+
185
+ client.request_certificate(common_name: common_name, names: domain_names_requested, order: order)
108
186
  end
109
187
 
110
- def request_certificate(common_name:, domains:)
111
- client.request_certificate(common_name: common_name, domains: domains)
188
+ # for use in order to store this on the machine for NGINX use
189
+ def csr_private_key_string
190
+ csr_private_key.to_s
112
191
  end
113
192
 
114
193
  private
115
194
 
116
195
  def client
117
196
  @client ||= ApartmentAcmeClient::AcmeClient::Proxy.singleton(
118
- private_key: private_key,
197
+ acme_client_private_key: acme_client_private_key,
198
+ csr_private_key: csr_private_key,
119
199
  )
120
200
  end
121
201
 
122
202
  # Returns a private key
123
- def private_key
203
+ def acme_client_private_key
124
204
  private_key = @certificate_storage.private_key
125
205
  return nil unless private_key
126
206
 
127
207
  OpenSSL::PKey::RSA.new(private_key)
128
208
  end
129
209
 
210
+ def csr_private_key
211
+ private_key = @certificate_storage.csr_private_key
212
+
213
+ # create a new private key if one is not found
214
+ if private_key.nil?
215
+ private_key = create_private_key
216
+ @certificate_storage.save_csr_private_key(private_key)
217
+ end
218
+
219
+ OpenSSL::PKey::RSA.new(private_key)
220
+ end
221
+
130
222
  def create_private_key
131
223
  OpenSSL::PKey::RSA.new(4096)
132
224
  end
@@ -30,10 +30,10 @@ module ApartmentAcmeClient
30
30
 
31
31
  def filled_template
32
32
  return nil unless check_configuration
33
+
33
34
  fill_template(read_template, @options)
34
35
  end
35
36
 
36
-
37
37
  def default_options
38
38
  result = {}
39
39
  result[:public_folder] = ApartmentAcmeClient.public_folder
@@ -59,62 +59,60 @@ module ApartmentAcmeClient
59
59
 
60
60
  def default_template
61
61
  <<~THE_END
62
- #
63
- # A virtual host using mix of IP-, name-, and port-based configuration
64
- #
62
+ #
63
+ # A virtual host using mix of IP-, name-, and port-based configuration
64
+ #
65
65
 
66
- upstream app {
67
- # Path to Unicorn SOCK file, as defined previously
68
- server unix:<%= options[:socket_path] %> fail_timeout=0;
69
- }
66
+ upstream app {
67
+ # Path to Unicorn SOCK file, as defined previously
68
+ server unix:<%= options[:socket_path] %> fail_timeout=0;
69
+ }
70
70
 
71
- server {
71
+ server {
72
72
 
73
- # FOR HTTP
74
- listen 80;
73
+ # FOR HTTP
74
+ listen 80;
75
75
 
76
- gzip on;
76
+ gzip on;
77
77
 
78
- # Application root, as defined previously
79
- root <%= options[:public_folder] %>;
80
- server_name <%= options[:base_domain] %> *.<%= options[:base_domain] %>;
78
+ # Application root, as defined previously
79
+ root <%= options[:public_folder] %>;
80
+ server_name <%= options[:base_domain] %> *.<%= options[:base_domain] %>;
81
81
 
82
- try_files $uri/index.html $uri @app;
82
+ try_files $uri/index.html $uri @app;
83
83
 
84
- location @app {
85
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86
- proxy_set_header X-FORWARDED-PROTO $scheme;
87
- proxy_set_header Host $http_host;
88
- proxy_redirect off;
89
- proxy_pass http://app;
90
- }
84
+ location @app {
85
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86
+ proxy_set_header X-FORWARDED-PROTO $scheme;
87
+ proxy_set_header Host $http_host;
88
+ proxy_redirect off;
89
+ proxy_pass http://app;
90
+ }
91
91
 
92
- error_page 500 502 503 504 /500.html;
93
- client_max_body_size 4G;
94
- keepalive_timeout 10;
92
+ error_page 500 502 503 504 /500.html;
93
+ client_max_body_size 4G;
94
+ keepalive_timeout 10;
95
95
 
96
- # BELOW THIS LINE FOR HTTPS
97
- <% if options[:include_ssl] %>
98
- listen 443 default_server ssl;
96
+ # BELOW THIS LINE FOR HTTPS
97
+ <% if options[:include_ssl] %>
98
+ listen 443 default_server ssl;
99
99
 
100
- # The following should be enabled once everything is SSL
101
- # ssl on;
100
+ # The following should be enabled once everything is SSL
101
+ # ssl on;
102
102
 
103
- ssl_certificate <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>cert.pem;
104
- ssl_certificate_key <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>privkey.pem;
103
+ ssl_certificate <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>cert.pem;
104
+ ssl_certificate_key <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>privkey.pem;
105
105
 
106
- ssl_stapling on;
107
- ssl_stapling_verify on;
108
- ssl_trusted_certificate <%= options[:certificate_storage_folder] %>/<%= options[:cert_prefix] %>fullchain.pem;
106
+ ssl_stapling on;
107
+ ssl_stapling_verify on;
109
108
 
110
- ssl_session_timeout 5m;
111
- <% end %>
112
- }
109
+ ssl_session_timeout 5m;
110
+ <% end %>
111
+ }
113
112
  THE_END
114
113
  end
115
114
 
116
115
  def fill_template(template, options)
117
-
118
116
  # scope defined for use in binding to ERB
119
117
  def opts(options)
120
118
  options
@@ -1,22 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApartmentAcmeClient
2
4
  class RenewalService
3
5
  def self.run!
4
-
5
6
  good_domains, rejected_domains = ApartmentAcmeClient::DomainChecker.new.accessible_domains
6
7
  puts "All domains to be requested: #{good_domains}, invalid domains: #{rejected_domains}"
7
8
 
8
- domains = ApartmentAcmeClient::Encryption.new.authorize_domains(good_domains)
9
- puts "authorized-domains list: #{domains}"
9
+ common_name = ApartmentAcmeClient.common_name || good_domains.first
10
10
 
11
- common_name = ApartmentAcmeClient.common_name ? ApartmentAcmeClient.common_name : good_domains.first
12
- certificate = ApartmentAcmeClient::Encryption.new.request_certificate(common_name: common_name, domains: domains)
11
+ encryptor = ApartmentAcmeClient::Encryption.new
12
+ certificate = encryptor.request_certificate(
13
+ common_name: common_name,
14
+ domains: good_domains,
15
+ wildcard_domain: ApartmentAcmeClient.wildcard_domain
16
+ )
13
17
 
14
- ApartmentAcmeClient::CertificateStorage::Proxy.singleton.store_certificate(certificate)
18
+ ApartmentAcmeClient::CertificateStorage::Proxy.singleton.store_certificate_string(certificate)
19
+ ApartmentAcmeClient::CertificateStorage::Proxy.singleton.store_csr_private_key_string(encryptor.csr_private_key_string)
15
20
 
16
- puts "Restarting nginx with new certificate"
17
- ApartmentAcmeClient::FileManipulation::Proxy.singleton.restart_service("nginx")
21
+ puts 'Restarting nginx with new certificate'
22
+ ApartmentAcmeClient::FileManipulation::Proxy.singleton.restart_service('nginx')
18
23
 
19
- puts "done."
24
+ puts 'done.'
20
25
  end
21
26
  end
22
27
  end
@@ -1,3 +1,3 @@
1
1
  module ApartmentAcmeClient
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.5'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apartment_acme_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Dunlop
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-13 00:00:00.000000000 Z
11
+ date: 2020-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -36,16 +36,16 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.3.1
39
+ version: 2.0.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.3.1
46
+ version: 2.0.0
47
47
  - !ruby/object:Gem::Dependency
48
- name: aws-sdk-s3
48
+ name: aws-sdk-route53
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
@@ -59,19 +59,19 @@ dependencies:
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1'
61
61
  - !ruby/object:Gem::Dependency
62
- name: sqlite3
62
+ name: aws-sdk-s3
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - ">="
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
67
+ version: '1'
68
+ type: :runtime
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - ">="
72
+ - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '0'
74
+ version: '1'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: bundler
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,20 @@ dependencies:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
88
  version: '1.15'
89
+ - !ruby/object:Gem::Dependency
90
+ name: pry
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
89
103
  - !ruby/object:Gem::Dependency
90
104
  name: rake
91
105
  requirement: !ruby/object:Gem::Requirement
@@ -129,7 +143,21 @@ dependencies:
129
143
  - !ruby/object:Gem::Version
130
144
  version: '0'
131
145
  - !ruby/object:Gem::Dependency
132
- name: pry
146
+ name: rubocop
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: sqlite3
133
161
  requirement: !ruby/object:Gem::Requirement
134
162
  requirements:
135
163
  - - ">="
@@ -170,6 +198,9 @@ files:
170
198
  - lib/apartment_acme_client/acme_client/real_client.rb
171
199
  - lib/apartment_acme_client/certificate_storage/proxy.rb
172
200
  - lib/apartment_acme_client/certificate_storage/s3.rb
201
+ - lib/apartment_acme_client/dns_api/check_dns.rb
202
+ - lib/apartment_acme_client/dns_api/fake.rb
203
+ - lib/apartment_acme_client/dns_api/route53.rb
173
204
  - lib/apartment_acme_client/domain_checker.rb
174
205
  - lib/apartment_acme_client/encryption.rb
175
206
  - lib/apartment_acme_client/engine.rb
@@ -201,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
232
  version: '0'
202
233
  requirements: []
203
234
  rubyforge_project:
204
- rubygems_version: 2.7.8
235
+ rubygems_version: 2.7.6
205
236
  signing_key:
206
237
  specification_version: 4
207
238
  summary: Let's Encrypt interface for Multi-tenancy applications (like Apartment)