acmesmith 0.11.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +63 -0
- data/README.md +7 -13
- data/acmesmith.gemspec +1 -1
- data/config.sample.yml +3 -2
- data/lib/acmesmith/certificate.rb +35 -12
- data/lib/acmesmith/challenge_responders/base.rb +42 -4
- data/lib/acmesmith/challenge_responders/manual_dns.rb +45 -0
- data/lib/acmesmith/challenge_responders/route53.rb +90 -68
- data/lib/acmesmith/client.rb +140 -78
- data/lib/acmesmith/command.rb +48 -11
- data/lib/acmesmith/config.rb +8 -5
- data/lib/acmesmith/post_issuing_hooks.rb +1 -18
- data/lib/acmesmith/version.rb +1 -1
- metadata +7 -6
- data/lib/acmesmith/acme_client.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b8046774804fb258740fcaaf2bbb19c823618082
|
4
|
+
data.tar.gz: 4b4c648818d71005f48d216aa12f63be9a292cfe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8100bb4a174e42bce6e3a74d997c2cb59e40be2ebeb1c7dffb89b7e92ba408417dd57a2ed7f495d65e7ca24f027c4119deb38b627e0ecd337833c41946a57a4a
|
7
|
+
data.tar.gz: 589c69691b1e55f5e39af424232acedd918108aecb88e14e4e0d4f1c1fa6e85c6837ee0d36fd0821005de649a38f2d10322334d160d0832c963718bb06abe01a
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
## v2.0.0 (2018-05-18)
|
2
|
+
|
3
|
+
### Notable changes
|
4
|
+
|
5
|
+
- Support ACME v2
|
6
|
+
- Drop ACME v1 support
|
7
|
+
- Challenge responder
|
8
|
+
- New `dns-01` challenge responder `manual_dns` is bundled for manual DNS intervention.
|
9
|
+
- New API to allow challenge responders to respond many challenges at once, for efficiency
|
10
|
+
- Added its support to `route53` responder
|
11
|
+
|
12
|
+
#### Compatibility note
|
13
|
+
|
14
|
+
- `config['endpoint']` is removed. Use `config['directory']` to specify ACME v2 directory resource URL.
|
15
|
+
- The deprecated `config['post_issueing_hook']` is removed as planned.
|
16
|
+
|
17
|
+
### CLI
|
18
|
+
|
19
|
+
#### Compatibility note
|
20
|
+
|
21
|
+
- Renamed several subcommands due to the changes in ACME (v2) semantics.
|
22
|
+
|
23
|
+
- `acmesmith register` -> `acmesmith new-account`
|
24
|
+
- `acmesmith request` -> `acmesmith order`
|
25
|
+
|
26
|
+
The previous names remain working, but are now marked as deprecated. These will be removed in the future release.
|
27
|
+
|
28
|
+
- Place warning for `acmesmith authorize` due to lack of implementation
|
29
|
+
|
30
|
+
(At this moment, LE doesn't provide new-authz API)
|
31
|
+
|
32
|
+
### API and Internals
|
33
|
+
|
34
|
+
(Interface of `Client` class is still in beta. It's designed to be an external API, but interface are still subject to change)
|
35
|
+
|
36
|
+
#### Compatibility note
|
37
|
+
|
38
|
+
- `config['endpoint']` is removed. Use `config['directory']` to specify ACME v2 directory resource URL.
|
39
|
+
- Several renames due to the changes in ACME (v2) semantics.
|
40
|
+
|
41
|
+
- `Client#register` -> `new_account`
|
42
|
+
- `Client#request` -> `order`
|
43
|
+
|
44
|
+
- Place warning for `Client#authorize` due to lack of implementation
|
45
|
+
|
46
|
+
(At this moment, LE doesn't provide new-authz API)
|
47
|
+
|
48
|
+
- `Certificate#chain` now returns `Array<OpenSSL::X509::Certificate>`. Use `Certificate#chain_pems` to retrieve in `String`.
|
49
|
+
|
50
|
+
Note: Value for `:chain` key in a `Hash` returned by `Certificate#export` is kept `String` for Storages plugin compatibility.
|
51
|
+
|
52
|
+
#### New Features
|
53
|
+
|
54
|
+
- `ChallengeResponders::Base` now allows to respond many challenges at once.
|
55
|
+
- Added `#respond_all` and `#cleanup_all()` method to respond many challenges.
|
56
|
+
- Added `#cap_respond_all?` method to indicate a responder instance supports this capability or not.
|
57
|
+
- Base class now implements `respond`, `cleanup` for classes which implement only the new `*_all` method.
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
## Prior versions
|
62
|
+
|
63
|
+
See https://github.com/sorah/acmesmith/releases
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Acmesmith: A simple, effective ACME client to use with many servers and a cloud
|
1
|
+
# Acmesmith: A simple, effective ACME v2 client to use with many servers and a cloud
|
2
2
|
|
3
3
|
Acmesmith is an [ACME (Automatic Certificate Management Environment)](https://github.com/ietf-wg-acme/acme) client that works perfect on environment with multiple servers. This client saves certificate and keys on cloud services (e.g. AWS S3) securely, then allow to deploy issued certificates onto your servers smoothly. This works well on [Let's encrypt](https://letsencrypt.org).
|
4
4
|
|
@@ -6,20 +6,15 @@ This tool is written in Ruby, but Acmesmith saves certificates in simple scheme,
|
|
6
6
|
|
7
7
|
## Features
|
8
8
|
|
9
|
-
- ACME client designed to work on multiple servers
|
9
|
+
- ACME v2 client designed to work on multiple servers
|
10
10
|
- ACME registration, domain authorization, certificate requests
|
11
11
|
- Tested against [Let's encrypt](https://letsencrypt.org)
|
12
12
|
- Storing keys in several ways
|
13
13
|
- Challenge response
|
14
14
|
- Many cloud services support
|
15
|
-
- AWS S3 storage and
|
15
|
+
- AWS S3 storage and Route 53 `dns-01` responder support out-of-the-box
|
16
16
|
- 3rd party plugins available for OpenStack designate, Google Cloud DNS, simple http-01, and Google Cloud Storage. See [Plugins](#3rd-party-plugins) below
|
17
17
|
|
18
|
-
### Planned
|
19
|
-
|
20
|
-
- Automated deployments support (post issurance hook)
|
21
|
-
- Example shellscripts to fetch certificates
|
22
|
-
|
23
18
|
## Installation
|
24
19
|
|
25
20
|
Add this line to your application's Gemfile:
|
@@ -39,12 +34,11 @@ Or install it yourself as:
|
|
39
34
|
## Usage
|
40
35
|
|
41
36
|
```
|
42
|
-
$ acmesmith
|
37
|
+
$ acmesmith new-account CONTACT # Create account key (contact e.g. mailto:xxx@example.org)
|
43
38
|
```
|
44
39
|
|
45
40
|
```
|
46
|
-
$ acmesmith
|
47
|
-
$ acmesmith request COMMON_NAME [SAN] # request certificate for CN +COMMON_NAME+ with SANs +SAN+
|
41
|
+
$ acmesmith order COMMON_NAME [SAN] # request certificate for CN +COMMON_NAME+ with SANs +SAN+
|
48
42
|
$ acmesmith add-san COMMON_NAME [SAN] # re-request existing certificate of CN with additional SAN(s)
|
49
43
|
```
|
50
44
|
|
@@ -77,8 +71,8 @@ See `acmesmith help [subcommand]` for more help.
|
|
77
71
|
See [config.sample.yml](./config.sample.yml) to start. Default configuration file is `./acmesmith.yml`.
|
78
72
|
|
79
73
|
``` yaml
|
80
|
-
|
81
|
-
#
|
74
|
+
directory: https://acme-staging-v02.api.letsencrypt.org/
|
75
|
+
# directory: https://acme-v02.api.letsencrypt.org/ # productilon
|
82
76
|
|
83
77
|
storage:
|
84
78
|
# configure where to store keys and certificates; described later
|
data/acmesmith.gemspec
CHANGED
@@ -21,7 +21,7 @@ Acmesmith is an [ACME (Automatic Certificate Management Environment)](https://gi
|
|
21
21
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.add_dependency "acme-client", '~>
|
24
|
+
spec.add_dependency "acme-client", '~> 2'
|
25
25
|
spec.add_dependency "aws-sdk-acm"
|
26
26
|
spec.add_dependency "aws-sdk-route53"
|
27
27
|
spec.add_dependency "aws-sdk-s3"
|
data/config.sample.yml
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
2
|
-
#
|
1
|
+
directory: https://acme-staging-v02.api.letsencrypt.org/directory
|
2
|
+
# directory: https://acme-v02.api.letsencrypt.org/directory
|
3
|
+
|
3
4
|
storage:
|
4
5
|
type: s3
|
5
6
|
region: 'ap-northeast-1'
|
@@ -4,8 +4,13 @@ module Acmesmith
|
|
4
4
|
class Certificate
|
5
5
|
class PassphraseRequired < StandardError; end
|
6
6
|
|
7
|
-
def self.
|
8
|
-
|
7
|
+
def self.split_pems(pems)
|
8
|
+
pems.each_line.slice_before(/^-----BEGIN CERTIFICATE-----$/).map(&:join)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.by_issuance(pem_chain, csr)
|
12
|
+
pems = split_pems(pem_chain)
|
13
|
+
new(*pems, csr.private_key, nil, csr)
|
9
14
|
end
|
10
15
|
|
11
16
|
def initialize(certificate, chain, private_key, key_passphrase = nil, csr = nil)
|
@@ -17,14 +22,27 @@ module Acmesmith
|
|
17
22
|
else
|
18
23
|
raise TypeError, 'certificate is expected to be a String or OpenSSL::X509::Certificate'
|
19
24
|
end
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
chain = case chain
|
26
|
+
when String
|
27
|
+
self.class.split_pems(chain)
|
28
|
+
when Array
|
29
|
+
chain
|
30
|
+
when nil
|
31
|
+
[]
|
32
|
+
else
|
33
|
+
raise TypeError, 'chain is expected to be an Array<String or OpenSSL::X509::Certificate> or nil'
|
34
|
+
end
|
35
|
+
|
36
|
+
@chain = chain.map { |cert|
|
37
|
+
case cert
|
38
|
+
when OpenSSL::X509::Certificate
|
39
|
+
cert
|
40
|
+
when String
|
41
|
+
OpenSSL::X509::Certificate.new(cert)
|
42
|
+
else
|
43
|
+
raise TypeError, 'chain is expected to be an Array<String or OpenSSL::X509::Certificate> or nil'
|
44
|
+
end
|
45
|
+
}
|
28
46
|
|
29
47
|
case private_key
|
30
48
|
when String
|
@@ -71,7 +89,11 @@ module Acmesmith
|
|
71
89
|
end
|
72
90
|
|
73
91
|
def fullchain
|
74
|
-
"#{certificate.to_pem}\n#{
|
92
|
+
"#{certificate.to_pem}\n#{issuer_pems}".gsub(/\n+/,?\n)
|
93
|
+
end
|
94
|
+
|
95
|
+
def issuer_pems
|
96
|
+
chain.map(&:to_pem).join("\n")
|
75
97
|
end
|
76
98
|
|
77
99
|
def common_name
|
@@ -91,8 +113,9 @@ module Acmesmith
|
|
91
113
|
def export(passphrase, cipher: OpenSSL::Cipher.new('aes-256-cbc'))
|
92
114
|
{}.tap do |h|
|
93
115
|
h[:certificate] = certificate.to_pem
|
94
|
-
h[:chain] =
|
116
|
+
h[:chain] = issuer_pems
|
95
117
|
h[:fullchain] = fullchain
|
118
|
+
|
96
119
|
h[:private_key] = if passphrase
|
97
120
|
private_key.export(cipher, passphrase)
|
98
121
|
else
|
@@ -1,19 +1,57 @@
|
|
1
1
|
module Acmesmith
|
2
2
|
module ChallengeResponders
|
3
3
|
class Base
|
4
|
+
# Return supported challenge types
|
4
5
|
def support?(type)
|
5
6
|
raise NotImplementedError
|
6
7
|
end
|
7
8
|
|
9
|
+
# Return 'true' if implements respond_all method.
|
10
|
+
def cap_respond_all?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
8
14
|
def initialize()
|
9
15
|
end
|
10
16
|
|
11
|
-
|
12
|
-
|
17
|
+
# Respond to the given challenges (1 or more).
|
18
|
+
def respond_all(*domain_and_challenges)
|
19
|
+
if cap_respond_all?
|
20
|
+
raise NotImplementedError
|
21
|
+
else
|
22
|
+
domain_and_challenges.each do |dc|
|
23
|
+
respond(*dc)
|
24
|
+
end
|
25
|
+
end
|
13
26
|
end
|
14
27
|
|
15
|
-
|
16
|
-
|
28
|
+
# Clean up responses for the given challenges (1 or more).
|
29
|
+
def cleanup_all(*domain_and_challenges)
|
30
|
+
if cap_respond_all?
|
31
|
+
raise NotImplementedError
|
32
|
+
else
|
33
|
+
domain_and_challenges.each do |dc|
|
34
|
+
cleanup(*dc)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# If cap_respond_all? is true, you don't need to implement this method.
|
40
|
+
def respond(domain, challenge)
|
41
|
+
if cap_respond_all?
|
42
|
+
respond_all([domain, challenge])
|
43
|
+
else
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# If cap_respond_all? is true, you don't need to implement this method.
|
49
|
+
def cleanup(domain, challenge)
|
50
|
+
if cap_respond_all?
|
51
|
+
cleanup_all([domain, challenge])
|
52
|
+
else
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
17
55
|
end
|
18
56
|
end
|
19
57
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'acmesmith/challenge_responders/base'
|
2
|
+
|
3
|
+
module Acmesmith
|
4
|
+
module ChallengeResponders
|
5
|
+
class ManualDns < Base
|
6
|
+
class HostedZoneNotFound < StandardError; end
|
7
|
+
class AmbiguousHostedZones < StandardError; end
|
8
|
+
|
9
|
+
def support?(type)
|
10
|
+
# Acme::Client::Resources::Challenges::DNS01
|
11
|
+
type == 'dns-01'
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(options={})
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond(domain, challenge)
|
18
|
+
puts "=> Responding challenge dns-01 for #{domain}"
|
19
|
+
puts
|
20
|
+
|
21
|
+
domain = canonical_fqdn(domain)
|
22
|
+
record_name = "#{challenge.record_name}.#{domain}"
|
23
|
+
record_type = challenge.record_type
|
24
|
+
record_content = "\"#{challenge.record_content}\""
|
25
|
+
|
26
|
+
puts "#{record_name}. 5 IN #{record_type} #{record_content}"
|
27
|
+
|
28
|
+
puts "(Hit enter when DNS record get ready)"
|
29
|
+
$stdin.gets
|
30
|
+
end
|
31
|
+
|
32
|
+
def cleanup(domain, challenge)
|
33
|
+
domain = canonical_fqdn(domain)
|
34
|
+
record_name = "#{challenge.record_name}.#{domain}"
|
35
|
+
puts "=> It's now okay to delete DNS record for #{record_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def canonical_fqdn(domain)
|
41
|
+
"#{domain}.".sub(/\.+$/, '')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -13,6 +13,10 @@ module Acmesmith
|
|
13
13
|
type == 'dns-01'
|
14
14
|
end
|
15
15
|
|
16
|
+
def cap_respond_all?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
16
20
|
def initialize(aws_access_key: nil, hosted_zone_map: {})
|
17
21
|
@route53 = Aws::Route53::Client.new({region: 'us-east-1'}.tap do |opt|
|
18
22
|
opt[:credentials] = Aws::Credentials.new(aws_access_key['access_key_id'], aws_access_key['secret_access_key'], aws_access_key['session_token']) if aws_access_key
|
@@ -21,84 +25,102 @@ module Acmesmith
|
|
21
25
|
@hosted_zone_cache = {}
|
22
26
|
end
|
23
27
|
|
24
|
-
def
|
25
|
-
|
28
|
+
def respond_all(*domain_and_challenges)
|
29
|
+
challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) }
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
record_type = challenge.record_type
|
30
|
-
record_content = "\"#{challenge.record_content}\""
|
31
|
-
zone_id = find_hosted_zone(domain)
|
32
|
-
|
33
|
-
puts " * UPSERT: #{record_type} #{record_name.inspect}, #{record_content.inspect} on #{zone_id}"
|
34
|
-
change_resp = @route53.change_resource_record_sets(
|
35
|
-
hosted_zone_id: zone_id, # required
|
36
|
-
change_batch: { # required
|
37
|
-
comment: "ACME challenge response",
|
38
|
-
changes: [
|
39
|
-
{
|
40
|
-
action: "UPSERT",
|
41
|
-
resource_record_set: { # required
|
42
|
-
name: record_name, # required
|
43
|
-
type: record_type,
|
44
|
-
ttl: 5,
|
45
|
-
resource_records: [
|
46
|
-
value: record_content
|
47
|
-
],
|
48
|
-
},
|
49
|
-
},
|
50
|
-
],
|
51
|
-
},
|
52
|
-
)
|
53
|
-
|
54
|
-
change_id = change_resp.change_info.id
|
55
|
-
puts " * requested change: #{change_id}"
|
56
|
-
|
57
|
-
puts "=> Waiting for change"
|
58
|
-
while (resp = @route53.get_change(id: change_id)).change_info.status != 'INSYNC'
|
59
|
-
puts " * change #{change_id.inspect} is still #{resp.change_info.status.inspect} ..."
|
60
|
-
sleep 5
|
31
|
+
zone_and_batches = challenges_by_hosted_zone.map do |zone_id, dcs|
|
32
|
+
[zone_id, change_batch_for_challenges(dcs, action: 'UPSERT')]
|
61
33
|
end
|
62
34
|
|
63
|
-
|
35
|
+
change_ids = request_changing_rrset(zone_and_batches, comment: 'for challenge response')
|
36
|
+
wait_for_sync(change_ids)
|
64
37
|
end
|
65
38
|
|
66
|
-
def
|
67
|
-
|
39
|
+
def cleanup_all(*domain_and_challenges)
|
40
|
+
challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) }
|
68
41
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
puts " * DELETE: #{record_type} #{record_name.inspect}, #{record_content.inspect} on #{zone_id}"
|
76
|
-
change_resp = @route53.change_resource_record_sets(
|
77
|
-
hosted_zone_id: zone_id, # required
|
78
|
-
change_batch: { # required
|
79
|
-
comment: "ACME challenge response: cleanup",
|
80
|
-
changes: [
|
81
|
-
{
|
82
|
-
action: "DELETE", # required, accepts CREATE, DELETE, UPSERT
|
83
|
-
resource_record_set: { # required
|
84
|
-
name: record_name, # required
|
85
|
-
type: record_type,
|
86
|
-
ttl: 5,
|
87
|
-
resource_records: [
|
88
|
-
value: record_content
|
89
|
-
],
|
90
|
-
},
|
91
|
-
},
|
92
|
-
],
|
93
|
-
},
|
94
|
-
)
|
95
|
-
|
96
|
-
change_id = change_resp.change_info.id
|
97
|
-
puts " * requested: #{change_id}"
|
42
|
+
zone_and_batches = challenges_by_hosted_zone.map do |zone_id, dcs|
|
43
|
+
[zone_id, change_batch_for_challenges(dcs, action: 'DELETE', comment: '(cleanup)')]
|
44
|
+
end
|
45
|
+
|
46
|
+
request_changing_rrset(zone_and_batches, comment: 'to remove challenge responses')
|
98
47
|
end
|
99
48
|
|
100
49
|
private
|
101
50
|
|
51
|
+
def request_changing_rrset(zone_and_batches, comment: nil)
|
52
|
+
puts "=> Requesting RRSet change #{comment}"
|
53
|
+
puts
|
54
|
+
change_ids = zone_and_batches.map do |(zone_id, change_batch)|
|
55
|
+
puts " * #{zone_id}:"
|
56
|
+
change_batch.fetch(:changes).each do |b|
|
57
|
+
rrset = b.fetch(:resource_record_set)
|
58
|
+
puts " - #{b.fetch(:action)}: #{rrset.fetch(:name)} #{rrset.fetch(:ttl)} #{rrset.fetch(:type)} #{rrset.dig(:resource_records, 0, :value)}"
|
59
|
+
end
|
60
|
+
print " ... "
|
61
|
+
|
62
|
+
resp = @route53.change_resource_record_sets(
|
63
|
+
hosted_zone_id: zone_id, # required
|
64
|
+
change_batch: change_batch,
|
65
|
+
)
|
66
|
+
change_id = resp.change_info.id
|
67
|
+
|
68
|
+
puts "[ ok ] #{change_id}"
|
69
|
+
puts
|
70
|
+
change_id
|
71
|
+
end
|
72
|
+
|
73
|
+
change_ids
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def wait_for_sync(change_ids)
|
78
|
+
puts "=> Waiting for change to be in sync"
|
79
|
+
puts
|
80
|
+
|
81
|
+
all_sync = false
|
82
|
+
until all_sync
|
83
|
+
sleep 4
|
84
|
+
|
85
|
+
all_sync = true
|
86
|
+
change_ids.each do |id|
|
87
|
+
change = @route53.get_change(id: id)
|
88
|
+
|
89
|
+
sync = change.change_info.status == 'INSYNC'
|
90
|
+
all_sync = false unless sync
|
91
|
+
|
92
|
+
puts " * #{id}: #{change.change_info.status}"
|
93
|
+
sleep 0.2
|
94
|
+
end
|
95
|
+
end
|
96
|
+
puts
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
def change_batch_for_challenges(domain_and_challenges, comment: nil, action: 'UPSERT')
|
101
|
+
{
|
102
|
+
comment: "ACME challenge response #{comment}",
|
103
|
+
changes: domain_and_challenges.map do |d,c|
|
104
|
+
{
|
105
|
+
action: action,
|
106
|
+
resource_record_set: rrset_for_challenge(d,c),
|
107
|
+
}
|
108
|
+
end,
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def rrset_for_challenge(domain, challenge)
|
113
|
+
domain = canonical_fqdn(domain)
|
114
|
+
{
|
115
|
+
name: "#{challenge.record_name}.#{domain}",
|
116
|
+
type: challenge.record_type,
|
117
|
+
ttl: 5,
|
118
|
+
resource_records: [
|
119
|
+
value: "\"#{challenge.record_content}\"",
|
120
|
+
],
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
102
124
|
def canonical_fqdn(domain)
|
103
125
|
"#{domain}.".sub(/\.+$/, '')
|
104
126
|
end
|
data/lib/acmesmith/client.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'acmesmith/account_key'
|
2
|
-
require 'acmesmith/acme_client'
|
3
2
|
require 'acmesmith/certificate'
|
4
3
|
require 'acmesmith/save_certificate_service'
|
4
|
+
require 'acme-client'
|
5
5
|
|
6
6
|
module Acmesmith
|
7
7
|
class Client
|
@@ -9,100 +9,57 @@ module Acmesmith
|
|
9
9
|
@config ||= config
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
12
|
+
def new_account(contact, tos_agreed: true)
|
13
13
|
key = AccountKey.generate
|
14
|
-
acme =
|
15
|
-
|
16
|
-
registration.agree_terms
|
14
|
+
acme = Acme::Client.new(private_key: key.private_key, directory: config.fetch('directory'))
|
15
|
+
client = acme.new_account(contact: contact, terms_of_service_agreed: tos_agreed)
|
17
16
|
|
18
17
|
storage.put_account_key(key, account_key_passphrase)
|
19
18
|
|
20
19
|
key
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
responder = config.challenge_responders.find do |x|
|
29
|
-
challenge = challenges.find { |_| x.support?(_.class::CHALLENGE_TYPE) }
|
30
|
-
end
|
31
|
-
{domain: domain, authz: authz, responder: responder, challenge: challenge}
|
22
|
+
def order(*identifiers, not_before: nil, not_after: nil)
|
23
|
+
puts "=> Ordering a certificate for the following identifiers:"
|
24
|
+
puts
|
25
|
+
identifiers.each do |id|
|
26
|
+
puts " * #{id}"
|
32
27
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
28
|
+
puts
|
29
|
+
puts "=> Generating CSR"
|
30
|
+
csr = Acme::Client::CertificateRequest.new(subject: { common_name: identifiers.first }, names: identifiers[1..-1])
|
31
|
+
puts "=> Placing an order"
|
32
|
+
order = acme.new_order(identifiers: identifiers, not_before: not_before, not_after: not_after)
|
33
|
+
|
34
|
+
unless order.authorizations.empty? || order.status == 'ready'
|
35
|
+
puts "=> Looking for required domain authorizations"
|
36
|
+
puts
|
37
|
+
order.authorizations.map(&:domain).each do |domain|
|
38
|
+
puts " * #{domain}"
|
37
39
|
end
|
40
|
+
puts
|
38
41
|
|
39
|
-
|
40
|
-
puts "=> Requesting verifications..."
|
41
|
-
acme.request_verification(target[:challenge])
|
42
|
-
end
|
43
|
-
loop do
|
44
|
-
all_valid = true
|
45
|
-
targets.each do |target|
|
46
|
-
next if target[:valid]
|
47
|
-
|
48
|
-
status = acme.verify_status(target[:challenge])
|
49
|
-
puts " * [#{target[:domain]}] verify_status: #{status}"
|
50
|
-
|
51
|
-
if status == 'valid'
|
52
|
-
target[:valid] = true
|
53
|
-
next
|
54
|
-
end
|
55
|
-
|
56
|
-
all_valid = false
|
57
|
-
if status == "invalid"
|
58
|
-
err = target[:challenge].error
|
59
|
-
puts " ! [#{target[:domain]}] #{err["type"]}: #{err["detail"]}"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
break if all_valid
|
63
|
-
sleep 3
|
64
|
-
end
|
65
|
-
puts "=> Done"
|
66
|
-
ensure
|
67
|
-
targets.each do |target|
|
68
|
-
target[:responder].cleanup(target[:domain], target[:challenge])
|
69
|
-
end
|
42
|
+
process_authorizations(order.authorizations)
|
70
43
|
end
|
71
|
-
end
|
72
44
|
|
73
|
-
|
74
|
-
csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: sans)
|
75
|
-
retried = false
|
76
|
-
acme_cert = begin
|
77
|
-
acme.new_certificate(csr)
|
78
|
-
rescue Acme::Client::Error::Unauthorized => e
|
79
|
-
raise unless config.auto_authorize_on_request
|
80
|
-
raise if retried
|
81
|
-
|
82
|
-
puts "=> Authorizing unauthorized domain names"
|
83
|
-
# https://github.com/letsencrypt/boulder/blob/b9369a481415b3fe31e010b34e2ff570b89e42aa/ra/ra.go#L604
|
84
|
-
m = e.message.match(/authorizations for these names not found or expired: ((?:[a-zA-Z0-9_.\-]+(?:,\s+|$))+)/)
|
85
|
-
if m && m[1]
|
86
|
-
domains = m[1].split(/,\s+/)
|
87
|
-
else
|
88
|
-
warn " ! Error message on certificate request was #{e.message.inspect} and acmesmith couldn't determine which domain names are unauthorized (maybe a bug)"
|
89
|
-
warn " ! Attempting to authorize all domains in this certificate reuqest for now."
|
90
|
-
domains = [common_name, *sans]
|
91
|
-
end
|
92
|
-
puts " * #{domains.join(', ')}"
|
93
|
-
authorize(*domains)
|
94
|
-
retried = true
|
95
|
-
retry
|
96
|
-
end
|
45
|
+
cert = process_order_finalization(order, csr)
|
97
46
|
|
98
|
-
|
47
|
+
puts "=> Certificate issued"
|
48
|
+
puts
|
49
|
+
print " * securing into the storage ..."
|
99
50
|
storage.put_certificate(cert, certificate_key_passphrase)
|
51
|
+
puts " [ ok ]"
|
52
|
+
puts
|
100
53
|
|
101
54
|
execute_post_issue_hooks(cert)
|
102
55
|
|
103
56
|
cert
|
104
57
|
end
|
105
58
|
|
59
|
+
def authorize(*identifiers)
|
60
|
+
raise NotImplementedError, "Domain authorization in advance is still not available in acme-client (v2). Required authorizations will be performed when ordering certificates"
|
61
|
+
end
|
62
|
+
|
106
63
|
def post_issue_hooks(common_name)
|
107
64
|
cert = storage.get_certificate(common_name)
|
108
65
|
execute_post_issue_hooks(cert)
|
@@ -110,9 +67,12 @@ module Acmesmith
|
|
110
67
|
|
111
68
|
def execute_post_issue_hooks(certificate)
|
112
69
|
hooks = config.post_issuing_hooks(certificate.common_name)
|
70
|
+
return if hooks.empty?
|
71
|
+
puts "=> Executing post issuing hooks for CN=#{certificate.common_name}"
|
113
72
|
hooks.each do |hook|
|
114
73
|
hook.run(certificate: certificate)
|
115
74
|
end
|
75
|
+
puts
|
116
76
|
end
|
117
77
|
|
118
78
|
def certificate_versions(common_name)
|
@@ -201,7 +161,7 @@ module Acmesmith
|
|
201
161
|
puts " Not valid after: #{not_after}"
|
202
162
|
next unless (cert.certificate.not_after.utc - Time.now.utc) < (days.to_i * 86400)
|
203
163
|
puts " * Renewing: CN=#{cert.common_name}, SANs=#{cert.sans.join(',')}"
|
204
|
-
|
164
|
+
order(cert.common_name, *cert.sans)
|
205
165
|
end
|
206
166
|
end
|
207
167
|
|
@@ -210,11 +170,113 @@ module Acmesmith
|
|
210
170
|
cert = storage.get_certificate(common_name)
|
211
171
|
sans = cert.sans + add_sans
|
212
172
|
puts " * SANs will be: #{sans.join(?,)}"
|
213
|
-
|
173
|
+
order(cert.common_name, *sans)
|
214
174
|
end
|
215
175
|
|
216
176
|
private
|
217
177
|
|
178
|
+
def process_order_finalization(order, csr)
|
179
|
+
puts "=> Finalizing the order"
|
180
|
+
puts
|
181
|
+
|
182
|
+
print " * Requesting..."
|
183
|
+
order.finalize(csr: csr)
|
184
|
+
puts" [ ok ]"
|
185
|
+
|
186
|
+
while %w(ready processing).include?(order.status)
|
187
|
+
order.reload()
|
188
|
+
puts " * Waiting for procession: status=#{order.status}"
|
189
|
+
sleep 2
|
190
|
+
end
|
191
|
+
puts
|
192
|
+
|
193
|
+
Certificate.by_issuance(order.certificate, csr)
|
194
|
+
end
|
195
|
+
|
196
|
+
def process_authorizations(authzs)
|
197
|
+
return if authzs.empty?
|
198
|
+
|
199
|
+
targets = authzs.map do |authz|
|
200
|
+
challenges = authz.challenges
|
201
|
+
challenge = nil
|
202
|
+
responder = config.challenge_responders.find do |x|
|
203
|
+
challenge = challenges.find { |_| x.support?(_.challenge_type) }
|
204
|
+
end
|
205
|
+
{domain: authz.domain, authz: authz, responder: responder, responder_id: responder.__id__, challenge: challenge}
|
206
|
+
end
|
207
|
+
target_by_responders = targets.group_by{ |_| _.fetch(:responder_id) }.map { |_, ts| [ts[0].fetch(:responder), ts] }
|
208
|
+
|
209
|
+
begin
|
210
|
+
target_by_responders.each do |responder, ts|
|
211
|
+
puts "=> Responsing to the challenges for the following identifier:"
|
212
|
+
puts
|
213
|
+
puts " * Responder: #{responder.class}"
|
214
|
+
puts " * Identifiers:"
|
215
|
+
ts.each do |target|
|
216
|
+
puts " - #{target.fetch(:domain)} (#{target.fetch(:challenge).challenge_type})"
|
217
|
+
end
|
218
|
+
puts
|
219
|
+
|
220
|
+
responder.respond_all(*ts.map{ |t| [t.fetch(:domain), t.fetch(:challenge)] })
|
221
|
+
end
|
222
|
+
|
223
|
+
puts "=> Requesting validations..."
|
224
|
+
puts
|
225
|
+
targets.each do |target|
|
226
|
+
print " * #{target[:domain]} (#{target[:challenge].challenge_type}) ..."
|
227
|
+
target[:challenge].request_validation()
|
228
|
+
puts " [ ok ]"
|
229
|
+
end
|
230
|
+
puts
|
231
|
+
|
232
|
+
puts "=> Waiting for the validation..."
|
233
|
+
puts
|
234
|
+
|
235
|
+
loop do
|
236
|
+
all_valid = true
|
237
|
+
any_error = false
|
238
|
+
targets.each do |target|
|
239
|
+
next if target[:valid]
|
240
|
+
|
241
|
+
target[:challenge].reload
|
242
|
+
status = target[:challenge].status
|
243
|
+
|
244
|
+
puts " * [#{target[:domain]}] status: #{status}"
|
245
|
+
|
246
|
+
if status == 'valid'
|
247
|
+
target[:valid] = true
|
248
|
+
next
|
249
|
+
end
|
250
|
+
|
251
|
+
all_valid = false
|
252
|
+
if status == 'invalid'
|
253
|
+
any_error = true
|
254
|
+
err = target[:challenge].error
|
255
|
+
puts " ! [#{target[:domain]}] error: #{err.inspect}"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
break if all_valid || any_error
|
259
|
+
sleep 3
|
260
|
+
end
|
261
|
+
puts
|
262
|
+
|
263
|
+
target_by_responders.each do |responder, ts|
|
264
|
+
puts "=> Cleaning the responses the challenges for the following identifier:"
|
265
|
+
puts
|
266
|
+
puts " * Responder: #{responder.class}"
|
267
|
+
puts " * Identifiers:"
|
268
|
+
ts.each do |target|
|
269
|
+
puts " - #{target.fetch(:domain)} (#{target.fetch(:challenge).challenge_type})"
|
270
|
+
end
|
271
|
+
puts
|
272
|
+
|
273
|
+
responder.cleanup_all(*ts.map{ |t| [t.fetch(:domain), t.fetch(:challenge)] })
|
274
|
+
end
|
275
|
+
|
276
|
+
puts "=> Authorized!"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
218
280
|
def config
|
219
281
|
@config
|
220
282
|
end
|
@@ -230,7 +292,7 @@ module Acmesmith
|
|
230
292
|
end
|
231
293
|
|
232
294
|
def acme
|
233
|
-
@acme ||=
|
295
|
+
@acme ||= Acme::Client.new(private_key: account_key.private_key, directory: config.fetch('directory'))
|
234
296
|
end
|
235
297
|
|
236
298
|
def certificate_key_passphrase
|
data/lib/acmesmith/command.rb
CHANGED
@@ -8,22 +8,36 @@ module Acmesmith
|
|
8
8
|
class_option :config, default: './acmesmith.yml', aliases: %w(-c)
|
9
9
|
class_option :passphrase_from_env, type: :boolean, aliases: %w(-E), default: nil, desc: 'Read $ACMESMITH_ACCOUNT_KEY_PASSPHRASE and $ACMESMITH_CERTIFICATE_KEY_PASSPHRASE for passphrases'
|
10
10
|
|
11
|
-
desc "
|
12
|
-
def
|
13
|
-
|
14
|
-
|
11
|
+
desc "new-account CONTACT", "Create account key (contact e.g. mailto:xxx@example.org)"
|
12
|
+
def new_account(contact)
|
13
|
+
puts "=> Creating an account ..."
|
14
|
+
key = client.new_account(contact)
|
15
|
+
puts "=> Public Key:"
|
16
|
+
puts "\n#{key.private_key.public_key.to_pem}"
|
15
17
|
end
|
16
18
|
|
17
|
-
desc "authorize DOMAIN [DOMAIN ...]", "Get authz for DOMAIN."
|
19
|
+
desc "authorize DOMAIN [DOMAIN ...]", "(Implementation disabled for v2) Get authz for DOMAIN."
|
18
20
|
def authorize(*domains)
|
19
|
-
|
21
|
+
warn "! WARNING: 'acmesmith authorize' is not available"
|
22
|
+
warn "!"
|
23
|
+
warn "! TL;DR: Go ahead; Just run 'acmesmith order'."
|
24
|
+
warn "!"
|
25
|
+
warn "! Pre-authorization have not implemented yet in acme-client.gem (v2) library."
|
26
|
+
warn "! But, required domain authorizations will be performed automatically when ordering a certificate."
|
27
|
+
warn "!"
|
28
|
+
warn "! Pro Tips: Let's encrypt doesn't provide pre-authorization as of May 18, 2018."
|
29
|
+
warn "!"
|
30
|
+
# client.authorize(*domains)
|
20
31
|
end
|
21
32
|
|
22
|
-
desc "
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
33
|
+
desc "order COMMON_NAME [SAN]", "order certificate for CN +COMMON_NAME+ with SANs +SAN+"
|
34
|
+
method_option :show_certificate, type: :boolean, aliases: %w(-s), default: true, desc: 'show an issued certificate in PEM and text when exiting'
|
35
|
+
def order(common_name, *sans)
|
36
|
+
cert = client.order(common_name, *sans)
|
37
|
+
if options[:show_certificate]
|
38
|
+
puts cert.certificate.to_text
|
39
|
+
puts cert.certificate.to_pem
|
40
|
+
end
|
27
41
|
end
|
28
42
|
|
29
43
|
desc "post-issue-hooks COMMON_NAME", "Run all post-issuing hooks for common name. (for testing purpose)"
|
@@ -131,6 +145,29 @@ module Acmesmith
|
|
131
145
|
client.add_san(common_name, *add_sans)
|
132
146
|
end
|
133
147
|
|
148
|
+
desc "register CONTACT", "(deprecated, use 'acmesmith new-account')"
|
149
|
+
def register(contact)
|
150
|
+
warn "!"
|
151
|
+
warn "! DEPRECATION WARNING: Use 'acmesmith new-account' command"
|
152
|
+
warn "! There is no user-facing breaking changes. It takes the same arguments with 'acmesmith register'."
|
153
|
+
warn "!"
|
154
|
+
warn "! This is due to change in semantics of ACME v2. ACME v2 defines 'new-account' instead of 'register' in v1."
|
155
|
+
warn "!"
|
156
|
+
new_account(contact)
|
157
|
+
end
|
158
|
+
|
159
|
+
desc "request COMMON_NAME [SAN]", "(deprecated, use 'acmesmith order')"
|
160
|
+
method_option :show_certificate, type: :boolean, aliases: %w(-s), default: true, desc: 'show an issued certificate in PEM and text when exiting'
|
161
|
+
def request(common_name, *sans)
|
162
|
+
warn "!"
|
163
|
+
warn "! DEPRECATION WARNING: Use 'acmesmith order' command"
|
164
|
+
warn "! There is no user-facing breaking changes. It takes the same arguments with 'acmesmith request'."
|
165
|
+
warn "!"
|
166
|
+
warn "! This is due to change in semantics of ACME v2. ACME v2 defines 'order' instead of 'request' in v1."
|
167
|
+
warn "!"
|
168
|
+
order(common_name, *sans)
|
169
|
+
end
|
170
|
+
|
134
171
|
private
|
135
172
|
|
136
173
|
def client
|
data/lib/acmesmith/config.rb
CHANGED
@@ -19,13 +19,12 @@ module Acmesmith
|
|
19
19
|
raise ArgumentError, "config['storage'] must be provided"
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
raise ArgumentError, "config['
|
22
|
+
if @config['endpoint'] and !@config['directory']
|
23
|
+
raise ArgumentError, "config['directory'] must be provided, e.g. https://acme-v02.api.letsencrypt.org/directory or https://acme-staging-v02.api.letsencrypt.org/directory\n\nNOTE: We have dropped ACME v1 support since acmesmith v2.0.0. Specify v2 directory API URL using config['directory']."
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
@config['post_issuing_hooks'] = @config.delete('post_issueing_hooks')
|
26
|
+
unless @config['directory']
|
27
|
+
raise ArgumentError, "config['directory'] must be provided, e.g. https://acme-v02.api.letsencrypt.org/directory or https://acme-staging-v02.api.letsencrypt.org/directory"
|
29
28
|
end
|
30
29
|
end
|
31
30
|
|
@@ -33,6 +32,10 @@ module Acmesmith
|
|
33
32
|
@config[key]
|
34
33
|
end
|
35
34
|
|
35
|
+
def fetch(*args)
|
36
|
+
@config.fetch(*args)
|
37
|
+
end
|
38
|
+
|
36
39
|
def merge!(pair)
|
37
40
|
@config.merge!(pair)
|
38
41
|
end
|
@@ -1,26 +1,9 @@
|
|
1
1
|
require 'acmesmith/utils/finder'
|
2
2
|
|
3
3
|
module Acmesmith
|
4
|
-
module PostIssueingHooks
|
5
|
-
def self.find(name)
|
6
|
-
warn "!! DEPRECATION WARNING: PostIssueingHooks.find is deprecated, use PostIssuingHooks.find (#{caller[0]})"
|
7
|
-
return Utils::Finder.find(self, 'acmesmith/post_issueing_hooks', name)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
4
|
module PostIssuingHooks
|
12
5
|
def self.find(name)
|
13
|
-
|
14
|
-
return Utils::Finder.find(self, 'acmesmith/post_issuing_hooks', name)
|
15
|
-
rescue Utils::Finder::NotFound => e
|
16
|
-
begin
|
17
|
-
klass = Utils::Finder.find(PostIssueingHooks, 'acmesmith/post_issueing_hooks', name)
|
18
|
-
warn "!! DEPRECATION WARNING (#{klass}): Placing in acmesmith/post_issueing_hooks/... is deprecated. Move to acmesmith/post_issuing_hooks/..."
|
19
|
-
return klass
|
20
|
-
rescue Utils::Finder::NotFound
|
21
|
-
raise e
|
22
|
-
end
|
23
|
-
end
|
6
|
+
Utils::Finder.find(self, 'acmesmith/post_issuing_hooks', name)
|
24
7
|
end
|
25
8
|
end
|
26
9
|
end
|
data/lib/acmesmith/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acmesmith
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- sorah (Shota Fukumori)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acme-client
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: aws-sdk-acm
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,6 +138,7 @@ files:
|
|
138
138
|
- ".gitignore"
|
139
139
|
- ".rspec"
|
140
140
|
- ".travis.yml"
|
141
|
+
- CHANGELOG.md
|
141
142
|
- Gemfile
|
142
143
|
- LICENSE.txt
|
143
144
|
- README.md
|
@@ -148,10 +149,10 @@ files:
|
|
148
149
|
- docs/vendor/aws.md
|
149
150
|
- lib/acmesmith.rb
|
150
151
|
- lib/acmesmith/account_key.rb
|
151
|
-
- lib/acmesmith/acme_client.rb
|
152
152
|
- lib/acmesmith/certificate.rb
|
153
153
|
- lib/acmesmith/challenge_responders.rb
|
154
154
|
- lib/acmesmith/challenge_responders/base.rb
|
155
|
+
- lib/acmesmith/challenge_responders/manual_dns.rb
|
155
156
|
- lib/acmesmith/challenge_responders/route53.rb
|
156
157
|
- lib/acmesmith/client.rb
|
157
158
|
- lib/acmesmith/command.rb
|
@@ -191,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
191
192
|
version: '0'
|
192
193
|
requirements: []
|
193
194
|
rubyforge_project:
|
194
|
-
rubygems_version: 2.
|
195
|
+
rubygems_version: 2.6.8
|
195
196
|
signing_key:
|
196
197
|
specification_version: 4
|
197
198
|
summary: ACME client (Let's encrypt client) to manage certificate in multi server
|
@@ -1,64 +0,0 @@
|
|
1
|
-
require 'acme-client'
|
2
|
-
|
3
|
-
module Acmesmith
|
4
|
-
class AcmeClient
|
5
|
-
# @param account_key [Acmesmith::AccountKey]
|
6
|
-
# @param endpoint [String]
|
7
|
-
def initialize(account_key, endpoint)
|
8
|
-
@acme = Acme::Client.new(private_key: account_key.private_key, endpoint: endpoint)
|
9
|
-
end
|
10
|
-
|
11
|
-
# @param contact [String]
|
12
|
-
def register(contact)
|
13
|
-
retry_once_on_bad_nonce do
|
14
|
-
@acme.register(contact: contact)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# @param domain [String]
|
19
|
-
def authorize(domain)
|
20
|
-
retry_once_on_bad_nonce do
|
21
|
-
@acme.authorize(domain: domain)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# @param csr [Acme::Client::CertificateRequest]
|
26
|
-
def new_certificate(csr)
|
27
|
-
retry_once_on_bad_nonce do
|
28
|
-
@acme.new_certificate(csr)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# @param challenge [Acme::Client::Resources::Challenges::Base]
|
33
|
-
def request_verification(challenge)
|
34
|
-
retry_once_on_bad_nonce do
|
35
|
-
challenge.request_verification
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# @param challenge [Acme::Client::Resources::Challenges::Base]
|
40
|
-
def verify_status(challenge)
|
41
|
-
retry_once_on_bad_nonce do
|
42
|
-
challenge.verify_status
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def retry_once_on_bad_nonce(&block)
|
49
|
-
retried = false
|
50
|
-
begin
|
51
|
-
block.call
|
52
|
-
rescue Acme::Client::Error::BadNonce => e
|
53
|
-
# Let's Encrypt returns badNonce error when the client sends too-old
|
54
|
-
# nonce. So retry the request once.
|
55
|
-
if retried
|
56
|
-
raise e
|
57
|
-
else
|
58
|
-
retried = true
|
59
|
-
retry
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|