acmesmith 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +2 -0
- data/.github/workflows/build.yml +4 -4
- data/CHANGELOG.md +9 -1
- data/Gemfile.lock +46 -42
- data/README.md +26 -1
- data/acmesmith.gemspec +1 -1
- data/docs/challenge_responders/route53.md +10 -1
- data/docs/vendor/aws.md +7 -1
- data/lib/acmesmith/certificate_retrieving_service.rb +126 -0
- data/lib/acmesmith/challenge_responder_filter.rb +8 -13
- data/lib/acmesmith/challenge_responders/route53.rb +11 -1
- data/lib/acmesmith/client.rb +1 -0
- data/lib/acmesmith/config.rb +20 -0
- data/lib/acmesmith/domain_name_filter.rb +22 -0
- data/lib/acmesmith/ordering_service.rb +12 -3
- data/lib/acmesmith/storages/s3.rb +2 -1
- data/lib/acmesmith/version.rb +1 -1
- metadata +16 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18161ba6306c7d98accbbdfdec37599c67c8ba6e3d84a75e2ce52dc7cf2561b6
|
4
|
+
data.tar.gz: 4fdb29425b0e3a9c998bd86ad755c46dd01244936d718531309ebac17d57e4bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 292b84e68ccaa7e7e3f946cdd7e3caaf281155085cd1813e2e4903f8a9737c67b35e8dd8b46ba95fa023865b6e329cd3be923da54a48f6220148d0cfa086e719
|
7
|
+
data.tar.gz: 8c25c548b5c4dcc11afe50279773f9991f22f750b2ea23fbe7be5f38f8e7ece8d04dec7c2705d11c7ecaebdad7669c77fe1af9178f4bfda2d55f8ca4197167c6
|
data/.github/FUNDING.yml
ADDED
data/.github/workflows/build.yml
CHANGED
@@ -18,9 +18,9 @@ jobs:
|
|
18
18
|
strategy:
|
19
19
|
fail-fast: false
|
20
20
|
matrix:
|
21
|
-
ruby-version: ['2.
|
21
|
+
ruby-version: ['2.7', '3.0', '3.1']
|
22
22
|
container:
|
23
|
-
image: sorah/ruby:${{ matrix.ruby-version }}-dev
|
23
|
+
image: public.ecr.aws/sorah/ruby:${{ matrix.ruby-version }}-dev
|
24
24
|
steps:
|
25
25
|
|
26
26
|
- name: Cache bundled gems
|
@@ -40,7 +40,7 @@ jobs:
|
|
40
40
|
strategy:
|
41
41
|
fail-fast: false
|
42
42
|
matrix:
|
43
|
-
ruby-version: ['2.
|
43
|
+
ruby-version: ['2.7', '3.0', '3.1']
|
44
44
|
|
45
45
|
# FIXME: once GitHub Actions gains support of adding command line arguments to container
|
46
46
|
# services:
|
@@ -72,7 +72,7 @@ jobs:
|
|
72
72
|
|
73
73
|
- run: 'docker run -d --net=host --rm letsencrypt/pebble pebble -config /test/config/pebble-config.json -strict -dnsserver 127.0.0.1:8053'
|
74
74
|
- run: 'docker run -d --net=host --rm letsencrypt/pebble-challtestsrv pebble-challtestsrv -management :8055 -defaultIPv4 127.0.0.1'
|
75
|
-
- run: 'docker run --net=host -e CI --rm -v $(pwd):/work -v $(realpath ~/bundle):/bundle sorah/ruby:${{ matrix.ruby-version }}-dev sh -c "cd /work && bundle install --path /bundle && bundle exec rspec -fd -t integration_pebble"'
|
75
|
+
- run: 'docker run --net=host -e CI --rm -v $(pwd):/work -v $(realpath ~/bundle):/bundle public.ecr.aws/sorah/ruby:${{ matrix.ruby-version }}-dev sh -c "cd /work && bundle install --path /bundle && bundle exec rspec -fd -t integration_pebble"'
|
76
76
|
|
77
77
|
docker-build:
|
78
78
|
name: docker-build
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
-
## v2.
|
1
|
+
## v2.5.0 (2020-10-09)
|
2
|
+
|
3
|
+
### Enhancement
|
4
|
+
|
5
|
+
- Gains `chain_preferences` configuration to choose alternate chain. [#47](https://github.com/sorah/acmesmith/pull/47)
|
6
|
+
- route53: Gains `substitution_map` to allow delegation of `_acme-challenge` via predefined CNAME record. [#53](https://github.com/sorah/acmesmith/pull/53)
|
7
|
+
- s3: Gains `endpoint` option. [#52](https://github.com/sorah/acmesmith/pull/52)
|
8
|
+
|
9
|
+
## v2.4.0 (2020-12-03)
|
2
10
|
|
3
11
|
### Enhancement
|
4
12
|
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
acmesmith (2.
|
5
|
-
acme-client (
|
4
|
+
acmesmith (2.5.0)
|
5
|
+
acme-client (>= 2.0.7, < 3)
|
6
6
|
aws-sdk-acm
|
7
7
|
aws-sdk-route53
|
8
8
|
aws-sdk-s3
|
@@ -11,55 +11,59 @@ PATH
|
|
11
11
|
GEM
|
12
12
|
remote: https://rubygems.org/
|
13
13
|
specs:
|
14
|
-
acme-client (2.0.
|
15
|
-
faraday (>= 0
|
16
|
-
|
17
|
-
aws-
|
18
|
-
aws-
|
19
|
-
|
14
|
+
acme-client (2.0.11)
|
15
|
+
faraday (>= 1.0, < 3.0.0)
|
16
|
+
faraday-retry (~> 1.0)
|
17
|
+
aws-eventstream (1.2.0)
|
18
|
+
aws-partitions (1.644.0)
|
19
|
+
aws-sdk-acm (1.52.0)
|
20
|
+
aws-sdk-core (~> 3, >= 3.127.0)
|
20
21
|
aws-sigv4 (~> 1.1)
|
21
|
-
aws-sdk-core (3.
|
22
|
+
aws-sdk-core (3.159.0)
|
22
23
|
aws-eventstream (~> 1, >= 1.0.2)
|
23
|
-
aws-partitions (~> 1, >= 1.
|
24
|
+
aws-partitions (~> 1, >= 1.525.0)
|
24
25
|
aws-sigv4 (~> 1.1)
|
25
|
-
jmespath (~> 1.
|
26
|
-
aws-sdk-kms (1.
|
27
|
-
aws-sdk-core (~> 3, >= 3.
|
26
|
+
jmespath (~> 1, >= 1.6.1)
|
27
|
+
aws-sdk-kms (1.58.0)
|
28
|
+
aws-sdk-core (~> 3, >= 3.127.0)
|
28
29
|
aws-sigv4 (~> 1.1)
|
29
|
-
aws-sdk-route53 (1.
|
30
|
-
aws-sdk-core (~> 3, >= 3.
|
30
|
+
aws-sdk-route53 (1.65.0)
|
31
|
+
aws-sdk-core (~> 3, >= 3.127.0)
|
31
32
|
aws-sigv4 (~> 1.1)
|
32
|
-
aws-sdk-s3 (1.
|
33
|
-
aws-sdk-core (~> 3, >= 3.
|
33
|
+
aws-sdk-s3 (1.114.0)
|
34
|
+
aws-sdk-core (~> 3, >= 3.127.0)
|
34
35
|
aws-sdk-kms (~> 1)
|
35
|
-
aws-sigv4 (~> 1.
|
36
|
-
aws-sigv4 (1.
|
36
|
+
aws-sigv4 (~> 1.4)
|
37
|
+
aws-sigv4 (1.5.2)
|
37
38
|
aws-eventstream (~> 1, >= 1.0.2)
|
38
|
-
diff-lcs (1.
|
39
|
-
faraday (
|
40
|
-
|
41
|
-
ruby2_keywords
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
rspec-
|
54
|
-
|
39
|
+
diff-lcs (1.4.4)
|
40
|
+
faraday (2.6.0)
|
41
|
+
faraday-net_http (>= 2.0, < 3.1)
|
42
|
+
ruby2_keywords (>= 0.0.4)
|
43
|
+
faraday-net_http (3.0.1)
|
44
|
+
faraday-retry (1.0.3)
|
45
|
+
jmespath (1.6.1)
|
46
|
+
mini_portile2 (2.8.0)
|
47
|
+
nokogiri (1.13.3)
|
48
|
+
mini_portile2 (~> 2.8.0)
|
49
|
+
racc (~> 1.4)
|
50
|
+
racc (1.6.0)
|
51
|
+
rake (13.0.6)
|
52
|
+
rspec (3.10.0)
|
53
|
+
rspec-core (~> 3.10.0)
|
54
|
+
rspec-expectations (~> 3.10.0)
|
55
|
+
rspec-mocks (~> 3.10.0)
|
56
|
+
rspec-core (3.10.1)
|
57
|
+
rspec-support (~> 3.10.0)
|
58
|
+
rspec-expectations (3.10.1)
|
55
59
|
diff-lcs (>= 1.2.0, < 2.0)
|
56
|
-
rspec-support (~> 3.
|
57
|
-
rspec-mocks (3.
|
60
|
+
rspec-support (~> 3.10.0)
|
61
|
+
rspec-mocks (3.10.2)
|
58
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
59
|
-
rspec-support (~> 3.
|
60
|
-
rspec-support (3.
|
61
|
-
ruby2_keywords (0.0.
|
62
|
-
thor (1.
|
63
|
+
rspec-support (~> 3.10.0)
|
64
|
+
rspec-support (3.10.2)
|
65
|
+
ruby2_keywords (0.0.5)
|
66
|
+
thor (1.2.1)
|
63
67
|
|
64
68
|
PLATFORMS
|
65
69
|
ruby
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Acmesmith: A simple, effective ACME v2 client to use with many servers and a cloud
|
2
2
|
|
3
|
-
![ci](https://github.com/sorah/acmesmith/workflows/ci/badge.svg?event=push)
|
3
|
+
![ci](https://github.com/sorah/acmesmith/workflows/ci/badge.svg?event=push) <a href='https://ko-fi.com/J3J8CKMUU' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://cdn.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
4
|
+
|
4
5
|
|
5
6
|
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).
|
6
7
|
|
@@ -151,6 +152,30 @@ are configurable per certificate's common-name.
|
|
151
152
|
- Shell script: [shell](./docs/post_issuing_hooks/shell.md)
|
152
153
|
- Amazon Certificate Manager (ACM): [acm](./docs/post_issuing_hooks/acm.md)
|
153
154
|
|
155
|
+
### Chain preference
|
156
|
+
|
157
|
+
If you want to prefer an alternative chain given by CA ([RFC8555 Section 7.4.2.](https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2)), use the following configuration. Preference may be delcared with common name.
|
158
|
+
|
159
|
+
When chain preferences are configured for the common name of an ordered certificate, Acmesmith will retrieve all available alternative chains and evaluate rules in an configured order. The first chain matched to a rule will be used and saved to a storage.
|
160
|
+
|
161
|
+
During rule evaluation, a root issuer name and key id are taken from the last available intermediate (Issuer and AKI) provided in a chain, when a chain doesn't have a root certificate (trust anchor).
|
162
|
+
|
163
|
+
```yaml
|
164
|
+
chain_preferences:
|
165
|
+
- root_issuer_name: "ISRG Root X1"
|
166
|
+
### Optionally, you may specify CA SKI/AKI:
|
167
|
+
# root_issuer_key_id: "79:b4:59:e6:7b:b6:e5:e4:01:73:80:08:88:c8:1a:58:f6:e9:9b:6e"
|
168
|
+
|
169
|
+
### Filter by common name (optional)
|
170
|
+
filter:
|
171
|
+
exact:
|
172
|
+
- my-app.example.com
|
173
|
+
suffix:
|
174
|
+
- .example.org
|
175
|
+
regexp:
|
176
|
+
- '\Aapp\d+.example.org\z'
|
177
|
+
```
|
178
|
+
|
154
179
|
## Vendor dependent notes
|
155
180
|
|
156
181
|
- [./docs/vendor/aws.md](./docs/vendor/aws.md): IAM and KMS key policies, and some tips
|
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.0.7', '< 3'
|
25
25
|
spec.add_dependency "aws-sdk-acm"
|
26
26
|
spec.add_dependency "aws-sdk-route53"
|
27
27
|
spec.add_dependency "aws-sdk-s3"
|
@@ -24,7 +24,16 @@ challenge_responders:
|
|
24
24
|
|
25
25
|
# Restore to original records on cleanup (after domain authorization). Default to false.
|
26
26
|
# Useful when you need to keep existing record as long as possible.
|
27
|
-
restore_to_original_records:
|
27
|
+
restore_to_original_records: false
|
28
|
+
|
29
|
+
### Substitution record names map (optional)
|
30
|
+
## This specifies alias for specific _acme-challenge record. For instance the following example
|
31
|
+
## updates _acme-challenge.test-example-com.example.org instead of _acme-challenge.test.example.com.
|
32
|
+
##
|
33
|
+
## This eases using the route53 responder for domains not managed in route53, by registering CNAME record to
|
34
|
+
## the alias record name on the original record name in advance. This is called delegation.
|
35
|
+
substitution_map:
|
36
|
+
"test.example.com.": "test-example-com.example.org."
|
28
37
|
```
|
29
38
|
|
30
39
|
## IAM Policy
|
data/docs/vendor/aws.md
CHANGED
@@ -20,12 +20,18 @@
|
|
20
20
|
"Effect": "Allow",
|
21
21
|
"Action": "route53:ChangeResourceRecordSets",
|
22
22
|
"Resource": ["arn:aws:route53:::hostedzone/*"]
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"Effect": "Allow",
|
26
|
+
"Action": "route53:ListResourceRecordSets",
|
27
|
+
"Resource": ["arn:aws:route53:::hostedzone/*"]
|
23
28
|
}
|
24
29
|
]
|
25
30
|
}
|
26
31
|
```
|
27
32
|
|
28
|
-
|
33
|
+
- You can limit allowed hosted zone by `Resource` of `route53:ChangeResourceRecordSets` grant
|
34
|
+
- `route53:ListResourceRecordSets` will be only required when `restore_to_original_records` is set
|
29
35
|
|
30
36
|
##### Only fetching certificates
|
31
37
|
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'acmesmith/certificate'
|
2
|
+
|
3
|
+
module Acmesmith
|
4
|
+
class CertificateRetrievingService
|
5
|
+
# @param acme [Acme::Client]
|
6
|
+
# @param common_name [String]
|
7
|
+
# @param url [String] ACME Certificate URL
|
8
|
+
# @param chain_preferences [Array<Acmesmith::Config::ChainPreference>]
|
9
|
+
def initialize(acme, common_name, url, chain_preferences: [])
|
10
|
+
@acme = acme
|
11
|
+
@url = url
|
12
|
+
@chain_preferences = chain_preferences.select { |_| _.filter.match?(common_name) }
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :acme
|
16
|
+
attr_reader :url
|
17
|
+
attr_reader :chain_preferences
|
18
|
+
|
19
|
+
def pem_chain
|
20
|
+
response = download(url, format: :pem)
|
21
|
+
pem = response.body
|
22
|
+
|
23
|
+
return pem if chain_preferences.empty?
|
24
|
+
|
25
|
+
puts " * Retrieving all chains..."
|
26
|
+
alternative_urls = Array(response.headers.dig('link', 'alternate'))
|
27
|
+
alternative_chains = alternative_urls.map { |_| CertificateChain.new(download(_, format: :pem).body) }
|
28
|
+
|
29
|
+
chains = [CertificateChain.new(pem), *alternative_chains]
|
30
|
+
|
31
|
+
chains.each_with_index do |chain, i|
|
32
|
+
puts " #{i.succ}. #{chain.to_s}"
|
33
|
+
end
|
34
|
+
puts
|
35
|
+
|
36
|
+
chain_preferences.each do |rule|
|
37
|
+
chains.each_with_index do |chain, i|
|
38
|
+
if chain.match?(name: rule.root_issuer_name, key_id: rule.root_issuer_key_id)
|
39
|
+
puts " * Chain chosen: ##{i.succ}"
|
40
|
+
return chain.pem_chain
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
warn " ! Preferred chain is not available, chain chosen: #1"
|
46
|
+
chains.first.pem_chain
|
47
|
+
end
|
48
|
+
|
49
|
+
class CertificateChain
|
50
|
+
def initialize(pem_chain)
|
51
|
+
@pem_chain = pem_chain
|
52
|
+
@pems = Certificate.split_pems(pem_chain)
|
53
|
+
@certificates = @pems.map { |_| OpenSSL::X509::Certificate.new(_) }
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :pem_chain
|
57
|
+
attr_reader :certificates
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
certificates[1..-1].map do |c|
|
61
|
+
"s:#{c.subject},i:#{c.issuer}"
|
62
|
+
end.join(" | ")
|
63
|
+
end
|
64
|
+
|
65
|
+
def match?(name: nil, key_id: nil)
|
66
|
+
has_root = top.issuer == top.subject
|
67
|
+
|
68
|
+
if name
|
69
|
+
return false unless name == (has_root ? top.subject : top.issuer).to_a.assoc('CN')[1]
|
70
|
+
end
|
71
|
+
|
72
|
+
if key_id
|
73
|
+
top_key_id = if has_root
|
74
|
+
value_der(top.extensions.find { |e| e.oid == 'subjectKeyIdentifier' })&.slice(2..-1)
|
75
|
+
else
|
76
|
+
value_der(top.extensions.find { |e| e.oid == 'authorityKeyIdentifier' })&.slice(4,20)
|
77
|
+
end&.unpack1('H*')&.downcase
|
78
|
+
return false unless key_id.downcase.gsub(/:/,'') == top_key_id
|
79
|
+
end
|
80
|
+
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def top
|
85
|
+
@top ||= find_top()
|
86
|
+
end
|
87
|
+
|
88
|
+
private def find_top
|
89
|
+
c = certificates.first
|
90
|
+
while c
|
91
|
+
up = find_issuer(c)
|
92
|
+
return c unless up
|
93
|
+
c = up
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private def find_issuer(cert)
|
98
|
+
return nil if cert.issuer == cert.subject
|
99
|
+
|
100
|
+
# https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.1
|
101
|
+
# sequence(\x30\x16) context-specific(\x80\x14) + keyid
|
102
|
+
aki = value_der(cert.extensions.find { |e| e.oid == 'authorityKeyIdentifier' })
|
103
|
+
|
104
|
+
# compare using SKI as a AKI DER. this doesn't support AKI using other than keyid but it should be okay
|
105
|
+
certificates.find do |c|
|
106
|
+
ski_der = value_der(c.extensions.find { |e| e.oid == 'subjectKeyIdentifier' })
|
107
|
+
next unless ski_der
|
108
|
+
hdr = "\x30\x16\x80\x14".b
|
109
|
+
keyid = ski_der[2..-1]
|
110
|
+
|
111
|
+
"#{hdr}#{keyid}" == aki && cert.issuer == c.subject
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private def value_der(ext)
|
116
|
+
return nil unless ext
|
117
|
+
ext.respond_to?(:value_der) ? ext.value_der : ext.to_der[9..-1]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private def download(url, format:)
|
122
|
+
# XXX: Use of private API https://github.com/unixcharles/acme-client/blob/5990b3105569a9d791ea011e0c5e57506eb54353/lib/acme/client.rb#L311
|
123
|
+
acme.__send__(:download, url, format: format)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -1,23 +1,18 @@
|
|
1
|
+
require 'acmesmith/domain_name_filter'
|
2
|
+
|
1
3
|
module Acmesmith
|
2
4
|
class ChallengeResponderFilter
|
3
5
|
def initialize(responder, subject_name_exact: nil, subject_name_suffix: nil, subject_name_regexp: nil)
|
4
6
|
@responder = responder
|
5
|
-
@
|
6
|
-
|
7
|
-
|
7
|
+
@domain_name_filter = DomainNameFilter.new(
|
8
|
+
exact: subject_name_exact,
|
9
|
+
suffix: subject_name_suffix,
|
10
|
+
regexp: subject_name_regexp,
|
11
|
+
)
|
8
12
|
end
|
9
13
|
|
10
14
|
def applicable?(domain)
|
11
|
-
|
12
|
-
return false unless @subject_name_exact.include?(domain)
|
13
|
-
end
|
14
|
-
if @subject_name_suffix
|
15
|
-
return false unless @subject_name_suffix.any? { |suffix| domain.end_with?(suffix) }
|
16
|
-
end
|
17
|
-
if @subject_name_regexp
|
18
|
-
return false unless @subject_name_regexp.any? { |regexp| domain.match?(regexp) }
|
19
|
-
end
|
20
|
-
@responder.applicable?(domain)
|
15
|
+
@domain_name_filter.match?(domain) && @responder.applicable?(domain)
|
21
16
|
end
|
22
17
|
end
|
23
18
|
end
|
@@ -17,7 +17,7 @@ module Acmesmith
|
|
17
17
|
true
|
18
18
|
end
|
19
19
|
|
20
|
-
def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {}, restore_to_original_records: false)
|
20
|
+
def initialize(aws_access_key: nil, assume_role: nil, hosted_zone_map: {}, restore_to_original_records: false, substitution_map: {})
|
21
21
|
aws_options = {region: 'us-east-1'}.tap do |opt|
|
22
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
|
23
23
|
end
|
@@ -37,9 +37,13 @@ module Acmesmith
|
|
37
37
|
|
38
38
|
@restore_to_original_records = restore_to_original_records
|
39
39
|
@original_records = {}
|
40
|
+
|
41
|
+
@substitution_map = substitution_map.map { |k,v| [canonical_fqdn(k), v] }.to_h
|
40
42
|
end
|
41
43
|
|
42
44
|
def respond_all(*domain_and_challenges)
|
45
|
+
domain_and_challenges = apply_substitution_for_domain_and_challenges(domain_and_challenges)
|
46
|
+
|
43
47
|
save_original_records(*domain_and_challenges) if @restore_to_original_records
|
44
48
|
|
45
49
|
challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) }
|
@@ -60,6 +64,8 @@ module Acmesmith
|
|
60
64
|
end
|
61
65
|
|
62
66
|
def cleanup_all(*domain_and_challenges)
|
67
|
+
domain_and_challenges = apply_substitution_for_domain_and_challenges(domain_and_challenges)
|
68
|
+
|
63
69
|
challenges_by_hosted_zone = domain_and_challenges.group_by { |(domain, _)| find_hosted_zone(domain) }
|
64
70
|
|
65
71
|
zone_and_batches = challenges_by_hosted_zone.map do |zone_id, dcs|
|
@@ -264,6 +270,10 @@ module Acmesmith
|
|
264
270
|
end
|
265
271
|
end
|
266
272
|
|
273
|
+
def apply_substitution_for_domain_and_challenges(domain_and_challenges)
|
274
|
+
domain_and_challenges.map { |(domain, challenge)| [@substitution_map.fetch(canonical_fqdn(domain), domain), challenge] }
|
275
|
+
end
|
276
|
+
|
267
277
|
def list_existing_rrsets(hosted_zone_id, name)
|
268
278
|
rrsets = []
|
269
279
|
start_record_name = name
|
data/lib/acmesmith/client.rb
CHANGED
data/lib/acmesmith/config.rb
CHANGED
@@ -2,11 +2,13 @@ require 'yaml'
|
|
2
2
|
require 'acmesmith/storages'
|
3
3
|
require 'acmesmith/challenge_responders'
|
4
4
|
require 'acmesmith/challenge_responder_filter'
|
5
|
+
require 'acmesmith/domain_name_filter'
|
5
6
|
require 'acmesmith/post_issuing_hooks'
|
6
7
|
|
7
8
|
module Acmesmith
|
8
9
|
class Config
|
9
10
|
ChallengeResponderRule = Struct.new(:challenge_responder, :filter, keyword_init: true)
|
11
|
+
ChainPreference = Struct.new(:root_issuer_name, :root_issuer_key_id, :filter, keyword_init: true)
|
10
12
|
|
11
13
|
def self.load_yaml(path)
|
12
14
|
new YAML.load_file(path)
|
@@ -29,6 +31,10 @@ module Acmesmith
|
|
29
31
|
unless @config['directory']
|
30
32
|
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"
|
31
33
|
end
|
34
|
+
|
35
|
+
if @config.key?('chain_preferences') && !@config.fetch('chain_preferences').kind_of?(Array)
|
36
|
+
raise ArgumentError, "config['chain_preferences'] must be an Array"
|
37
|
+
end
|
32
38
|
end
|
33
39
|
|
34
40
|
def [](key)
|
@@ -104,6 +110,20 @@ module Acmesmith
|
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
113
|
+
def chain_preferences
|
114
|
+
@preferred_chains ||= begin
|
115
|
+
specs = @config['chain_preferences'] || []
|
116
|
+
specs.flat_map do |spec|
|
117
|
+
filter = spec.fetch('filter', {}).map { |k,v| [k.to_sym, v] }.to_h
|
118
|
+
ChainPreference.new(
|
119
|
+
root_issuer_name: spec['root_issuer_name'],
|
120
|
+
root_issuer_key_id: spec['root_issuer_key_id'],
|
121
|
+
filter: DomainNameFilter.new(**filter),
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
107
127
|
# def post_actions
|
108
128
|
# end
|
109
129
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Acmesmith
|
2
|
+
class DomainNameFilter
|
3
|
+
def initialize(exact: nil, suffix: nil, regexp: nil)
|
4
|
+
@exact = exact && [*exact].flatten.compact
|
5
|
+
@suffix = suffix && [*suffix].flatten.compact
|
6
|
+
@regexp = regexp && [*regexp].flatten.compact.map{ |_| Regexp.new(_) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def match?(domain)
|
10
|
+
if @exact
|
11
|
+
return false unless @exact.include?(domain)
|
12
|
+
end
|
13
|
+
if @suffix
|
14
|
+
return false unless @suffix.any? { |suffix| domain.end_with?(suffix) }
|
15
|
+
end
|
16
|
+
if @regexp
|
17
|
+
return false unless @regexp.any? { |regexp| domain.match?(regexp) }
|
18
|
+
end
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'acmesmith/authorization_service'
|
2
2
|
require 'acmesmith/certificate'
|
3
|
+
require 'acmesmith/certificate_retrieving_service'
|
3
4
|
|
4
5
|
module Acmesmith
|
5
6
|
class OrderingService
|
@@ -8,17 +9,19 @@ module Acmesmith
|
|
8
9
|
# @param acme [Acme::Client] ACME client
|
9
10
|
# @param identifiers [Array<String>] Array of domain names for a ordering certificate. The first item will be a common name.
|
10
11
|
# @param challenge_responder_rules [Array<Acmesmith::Config::ChallengeResponderRule>] responders
|
12
|
+
# @param chain_preferences [Array<Acmesmith::Config::ChainPreference>] chain_preferences
|
11
13
|
# @param not_before [Time]
|
12
14
|
# @param not_after [Time]
|
13
|
-
def initialize(acme:, identifiers:, challenge_responder_rules:, not_before: nil, not_after: nil)
|
15
|
+
def initialize(acme:, identifiers:, challenge_responder_rules:, chain_preferences:, not_before: nil, not_after: nil)
|
14
16
|
@acme = acme
|
15
17
|
@identifiers = identifiers
|
16
18
|
@challenge_responder_rules = challenge_responder_rules
|
19
|
+
@chain_preferences = chain_preferences
|
17
20
|
@not_before = not_before
|
18
21
|
@not_after = not_after
|
19
22
|
end
|
20
23
|
|
21
|
-
attr_reader :acme, :identifiers, :challenge_responder_rules, :not_before, :not_after
|
24
|
+
attr_reader :acme, :identifiers, :challenge_responder_rules, :chain_preferences, :not_before, :not_after
|
22
25
|
|
23
26
|
def perform!
|
24
27
|
puts "=> Ordering a certificate for the following identifiers:"
|
@@ -38,7 +41,7 @@ module Acmesmith
|
|
38
41
|
finalize_order()
|
39
42
|
wait_order_for_complete()
|
40
43
|
|
41
|
-
@certificate = Certificate.by_issuance(
|
44
|
+
@certificate = Certificate.by_issuance(pem_chain, csr)
|
42
45
|
|
43
46
|
puts
|
44
47
|
puts "=> Certificate issued"
|
@@ -77,6 +80,12 @@ module Acmesmith
|
|
77
80
|
end
|
78
81
|
end
|
79
82
|
|
83
|
+
# @return String
|
84
|
+
def pem_chain
|
85
|
+
url = order.certificate_url or raise NotCompleted, "not completed yet"
|
86
|
+
CertificateRetrievingService.new(acme, common_name, url, chain_preferences: chain_preferences).pem_chain
|
87
|
+
end
|
88
|
+
|
80
89
|
def certificate
|
81
90
|
@certificate or raise NotCompleted, "not completed yet"
|
82
91
|
end
|
@@ -7,7 +7,7 @@ require 'acmesmith/certificate'
|
|
7
7
|
module Acmesmith
|
8
8
|
module Storages
|
9
9
|
class S3 < Base
|
10
|
-
def initialize(aws_access_key: nil, bucket:, prefix: nil, region:, use_kms: true, kms_key_id: nil, kms_key_id_account: nil, kms_key_id_certificate_key: nil, pkcs12_passphrase: nil, pkcs12_common_names: nil)
|
10
|
+
def initialize(aws_access_key: nil, bucket:, prefix: nil, region:, use_kms: true, kms_key_id: nil, kms_key_id_account: nil, kms_key_id_certificate_key: nil, pkcs12_passphrase: nil, pkcs12_common_names: nil, endpoint: nil)
|
11
11
|
@region = region
|
12
12
|
@bucket = bucket
|
13
13
|
@prefix = prefix
|
@@ -25,6 +25,7 @@ module Acmesmith
|
|
25
25
|
|
26
26
|
@s3 = Aws::S3::Client.new({region: region}.tap do |opt|
|
27
27
|
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
|
28
|
+
opt[:endpoint] = endpoint if endpoint
|
28
29
|
end)
|
29
30
|
end
|
30
31
|
|
data/lib/acmesmith/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acmesmith
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sorah Fukumori
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acme-client
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.7
|
20
|
+
- - "<"
|
18
21
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
22
|
+
version: '3'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.0.7
|
30
|
+
- - "<"
|
25
31
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
32
|
+
version: '3'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: aws-sdk-acm
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -136,6 +142,7 @@ extensions: []
|
|
136
142
|
extra_rdoc_files: []
|
137
143
|
files:
|
138
144
|
- ".dockerignore"
|
145
|
+
- ".github/FUNDING.yml"
|
139
146
|
- ".github/stale.yml"
|
140
147
|
- ".github/workflows/build.yml"
|
141
148
|
- ".gitignore"
|
@@ -162,6 +169,7 @@ files:
|
|
162
169
|
- lib/acmesmith/account_key.rb
|
163
170
|
- lib/acmesmith/authorization_service.rb
|
164
171
|
- lib/acmesmith/certificate.rb
|
172
|
+
- lib/acmesmith/certificate_retrieving_service.rb
|
165
173
|
- lib/acmesmith/challenge_responder_filter.rb
|
166
174
|
- lib/acmesmith/challenge_responders.rb
|
167
175
|
- lib/acmesmith/challenge_responders/base.rb
|
@@ -171,6 +179,7 @@ files:
|
|
171
179
|
- lib/acmesmith/client.rb
|
172
180
|
- lib/acmesmith/command.rb
|
173
181
|
- lib/acmesmith/config.rb
|
182
|
+
- lib/acmesmith/domain_name_filter.rb
|
174
183
|
- lib/acmesmith/ordering_service.rb
|
175
184
|
- lib/acmesmith/post_issueing_hooks.rb
|
176
185
|
- lib/acmesmith/post_issueing_hooks/base.rb
|
@@ -206,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
215
|
- !ruby/object:Gem::Version
|
207
216
|
version: '0'
|
208
217
|
requirements: []
|
209
|
-
rubygems_version: 3.
|
218
|
+
rubygems_version: 3.4.0.dev
|
210
219
|
signing_key:
|
211
220
|
specification_version: 4
|
212
221
|
summary: ACME client (Let's encrypt client) to manage certificate in multi server
|