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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4089bdb36940e87e2f996a6c820a2f06505024f6ae9721c3da332999c2145998
4
- data.tar.gz: 1abf984f4f5d82a0d266bb5aee3905708ba1392238439663af0bc8ac9aa700b0
3
+ metadata.gz: 18161ba6306c7d98accbbdfdec37599c67c8ba6e3d84a75e2ce52dc7cf2561b6
4
+ data.tar.gz: 4fdb29425b0e3a9c998bd86ad755c46dd01244936d718531309ebac17d57e4bb
5
5
  SHA512:
6
- metadata.gz: d1147c0742b2bd14205b89ca76f4b011453d2eeadf0df2aaad094b7e3c32907de322aed48c0d4fce40a4120ddd782c5c67e6da1504644fce3db679b4452ef774
7
- data.tar.gz: e8bc441a9cb9fff624f338fb33946bab80bf742848e695130b17bd807ba8da98d79e5999459ce824cf6ebcdac6206f156026a17adc754aa6bde8356cc3a915f1
6
+ metadata.gz: 292b84e68ccaa7e7e3f946cdd7e3caaf281155085cd1813e2e4903f8a9737c67b35e8dd8b46ba95fa023865b6e329cd3be923da54a48f6220148d0cfa086e719
7
+ data.tar.gz: 8c25c548b5c4dcc11afe50279773f9991f22f750b2ea23fbe7be5f38f8e7ece8d04dec7c2705d11c7ecaebdad7669c77fe1af9178f4bfda2d55f8ca4197167c6
@@ -0,0 +1,2 @@
1
+ ko_fi: sorah
2
+ github: [sorah]
@@ -18,9 +18,9 @@ jobs:
18
18
  strategy:
19
19
  fail-fast: false
20
20
  matrix:
21
- ruby-version: ['2.6', '2.7']
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.6', '2.7']
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.4.0 (2020-05-12)
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.4.0)
5
- acme-client (~> 2)
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.7)
15
- faraday (>= 0.17, < 2.0.0)
16
- aws-eventstream (1.1.0)
17
- aws-partitions (1.402.0)
18
- aws-sdk-acm (1.38.0)
19
- aws-sdk-core (~> 3, >= 3.109.0)
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.109.3)
22
+ aws-sdk-core (3.159.0)
22
23
  aws-eventstream (~> 1, >= 1.0.2)
23
- aws-partitions (~> 1, >= 1.239.0)
24
+ aws-partitions (~> 1, >= 1.525.0)
24
25
  aws-sigv4 (~> 1.1)
25
- jmespath (~> 1.0)
26
- aws-sdk-kms (1.39.0)
27
- aws-sdk-core (~> 3, >= 3.109.0)
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.44.0)
30
- aws-sdk-core (~> 3, >= 3.109.0)
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.86.0)
33
- aws-sdk-core (~> 3, >= 3.109.0)
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.1)
36
- aws-sigv4 (1.2.2)
36
+ aws-sigv4 (~> 1.4)
37
+ aws-sigv4 (1.5.2)
37
38
  aws-eventstream (~> 1, >= 1.0.2)
38
- diff-lcs (1.3)
39
- faraday (1.1.0)
40
- multipart-post (>= 1.2, < 3)
41
- ruby2_keywords
42
- jmespath (1.4.0)
43
- mini_portile2 (2.4.0)
44
- multipart-post (2.1.1)
45
- nokogiri (1.10.9)
46
- mini_portile2 (~> 2.4.0)
47
- rake (13.0.1)
48
- rspec (3.9.0)
49
- rspec-core (~> 3.9.0)
50
- rspec-expectations (~> 3.9.0)
51
- rspec-mocks (~> 3.9.0)
52
- rspec-core (3.9.2)
53
- rspec-support (~> 3.9.3)
54
- rspec-expectations (3.9.2)
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.9.0)
57
- rspec-mocks (3.9.1)
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.9.0)
60
- rspec-support (3.9.3)
61
- ruby2_keywords (0.0.2)
62
- thor (1.0.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", '~> 2'
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: true
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
- Note: You can limit allowed hosted zone by modifying `Resource` of `route53:ChangeResourceRecordSets`
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
- @subject_name_exact = subject_name_exact && [*subject_name_exact].flatten.compact
6
- @subject_name_suffix = subject_name_suffix && [*subject_name_suffix].flatten.compact
7
- @subject_name_regexp = subject_name_regexp && [*subject_name_regexp].flatten.compact.map{ |_| Regexp.new(_) }
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
- if @subject_name_exact
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
@@ -26,6 +26,7 @@ module Acmesmith
26
26
  acme: acme,
27
27
  identifiers: identifiers,
28
28
  challenge_responder_rules: config.challenge_responders,
29
+ chain_preferences: config.chain_preferences,
29
30
  not_before: not_before,
30
31
  not_after: not_after
31
32
  )
@@ -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(order.certificate, csr)
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
 
@@ -1,3 +1,3 @@
1
1
  module Acmesmith
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
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.0
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: 2020-12-02 00:00:00.000000000 Z
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: '2'
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: '2'
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.1.2
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