acmesmith 2.8.0 → 2.9.0

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: 252b913c94d04e35760101aedc61410f00bc59c806830f86d1348237c213700b
4
- data.tar.gz: e494fc15b7bbdbc1b5b59dfcbb654d654c7908defa749106c8e37cd52d3d882e
3
+ metadata.gz: 5230018ae89aac8b573c6353b1f447b078538a2ac3211e4d626eeba429a2a5af
4
+ data.tar.gz: a5ee69e253291d3ce838f248fed18ae5a29a01bd2aea4895b582e34ceeae5c9a
5
5
  SHA512:
6
- metadata.gz: 2b075a5fc9d32491774864c8035a6de3b3eb8dd03ccea006330f63cce4beac3c8ca4304b51d6b6d6bf406aacf196424c8dd7aaa7f772226240aa2b0138c40474
7
- data.tar.gz: c209c626c45fa16f547ca0bea14271bb8c850592b5ef205a7c19269d5c7f0cb941101caaf75e9cdd510e46f5731723bf7cf73abf9878de95171ecd07aed0f5cc
6
+ metadata.gz: 27e82c020a219babe961f13db21bf72ee6c3084ce16ef023da4efbf494d004a91def0cc0f7c96316f9540249e51a888b803fdc48a82970ef319afebe4de08ada
7
+ data.tar.gz: ab4c2e3277e4af4cbbed7de6087c11c11ee4c0d741892a4da6ea458297fd726db87e49a0e73d32dff52111d61c4fdc60db04a775db70fcb2e06c05cf997f2454
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [unreleased]
2
+
3
+ ## v2.9.0 (2026-03-14)
4
+
5
+ ### Enhancements
6
+
7
+ - `acmesmith order` and configuration file gain `profiles` configuration to allow switching ACME profiles such as `shortlived` [#81](https://github.com/sorah/acmesmith/pull/81)
8
+
9
+ ```yaml
10
+ profiles:
11
+ - name: "shortlived"
12
+ filter:
13
+ subject_name_suffix: [".shortlived.example.invalid"]
14
+ ```
15
+
16
+ ### Changes
17
+
18
+ - Prebuilt Docker images are no longer pushed to Dockerhub. Use GHCR instead: [ghcr.io/sorah/acmesmith](https://ghcr.io/sorah/acmesmith)
19
+
1
20
  ## v2.8.0 (2025-11-11)
2
21
 
3
22
  ### Enhancements
@@ -11,8 +30,8 @@
11
30
 
12
31
  ### Updates
13
32
 
14
- - Update gemspec to the latest bundler's provided template. This removes certain irrevant files from a released gem package. [#73](https://github.com/sorah/acmesmith/issues/73)
15
- - Use rubygems.org trusted publishing via GitHub Actions [#73](https://github.com/sorah/acmesmith/issues/73)
33
+ - Update gemspec to the latest bundler's provided template. This removes certain irrevant files from a released gem package. [#75](https://github.com/sorah/acmesmith/issues/75)
34
+ - Use rubygems.org trusted publishing via GitHub Actions [#75](https://github.com/sorah/acmesmith/issues/75)
16
35
 
17
36
  ## v2.7.1 (2025-08-21)
18
37
 
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM sorah/ruby:3.4-dev as builder
1
+ FROM ghcr.io/sorah-rbpkg/ruby:3.4-dev as builder
2
2
 
3
3
  #RUN apt-get update \
4
4
  # && apt-get install -y libmysqlclient-dev git-core \
@@ -10,13 +10,14 @@ COPY Gemfile.lock /app/
10
10
  COPY acmesmith.gemspec /app/
11
11
  RUN sed -i -e 's|Acmesmith::VERSION|"0.0.0"|g' -e '/^require.*acmesmith.version/d' -e '/`git/d' acmesmith.gemspec
12
12
 
13
- RUN bundle install --path /gems --jobs 100 --without development
13
+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
14
+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
15
+ apt-get update \
16
+ && apt-get install -y --no-install-recommends libssl-dev
14
17
 
15
- FROM sorah/ruby:3.4
18
+ RUN bundle install --path /gems --jobs 100 --without development
16
19
 
17
- #RUN apt-get update \
18
- # && apt-get install -y libmysqlclient20 \
19
- # && rm -rf /var/lib/apt/lists/*
20
+ FROM ghcr.io/sorah-rbpkg/ruby:3.4
20
21
 
21
22
  WORKDIR /app
22
23
  COPY . /app/
data/README.md CHANGED
@@ -42,7 +42,7 @@ docker run -v /path/to/acmesmith.yml:/app/acmesmith.yml:ro sorah/acmesmith:lates
42
42
 
43
43
  [`Dockerfile`](./Dockerfile) is available. Default confguration file is at `/app/acmesmith.yml`.
44
44
 
45
- Pre-built docker images are provided at https://hub.docker.com/r/sorah/acmesmith for your convenience
45
+ Pre-built docker images are provided at https://ghcr.io/sorah/acmesmith for your convenience
46
46
  Built with GitHub Actions & [sorah-rbpkg/dockerfiles](https://github.com/sorah-rbpkg/dockerfiles).
47
47
 
48
48
  ## Usage
@@ -176,6 +176,28 @@ chain_preferences:
176
176
  - '\Aapp\d+.example.org\z'
177
177
  ```
178
178
 
179
+ ### Profiles
180
+
181
+ Some ACME CAs support [certificate profiles](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/) that allow selecting different certificate types (e.g., short-lived, classic). Use the `list-profiles` command to discover available profiles from your CA:
182
+
183
+ ```
184
+ $ acmesmith list-profiles
185
+ ```
186
+
187
+ To configure profile selection per domain, add `profiles` to your configuration. The first matching rule wins:
188
+
189
+ ```yaml
190
+ profiles:
191
+ - name: "shortlived"
192
+ filter:
193
+ subject_name_suffix:
194
+ - .shortlived.example.com
195
+ - name: "classic"
196
+ # no filter = default/fallback
197
+ ```
198
+
199
+ The `filter` block supports the same options as challenge responders: `subject_name_exact`, `subject_name_suffix`, and `subject_name_regexp`.
200
+
179
201
  ## Vendor dependent notes
180
202
 
181
203
  - [./docs/vendor/aws.md](./docs/vendor/aws.md): IAM and KMS key policies, and some tips
@@ -195,7 +217,7 @@ bundle exec rspec
195
217
  integration test using [letsencrypt/pebble](https://github.com/letsencrypt/pebble). needs Docker:
196
218
 
197
219
  ```
198
- ACMESMITH_CI_START_PEBBLE=1 CI=1 bundle exec -t integration_pebble
220
+ ACMESMITH_CI_START_PEBBLE=1 bundle exec rspec -t integration_pebble
199
221
  ```
200
222
 
201
223
  ## Writing plugins
data/config.sample.yml CHANGED
@@ -38,6 +38,21 @@ challenge_responders:
38
38
  # Last resort
39
39
  - route53: {}
40
40
 
41
+ ###
42
+ ### Profiles (optional)
43
+ ###
44
+
45
+ ## ACME certificate profiles allow selecting different certificate types.
46
+ ## Use `acmesmith list-profiles` to discover available profiles.
47
+ ## First matching rule wins.
48
+ # profiles:
49
+ # - name: "shortlived"
50
+ # filter:
51
+ # subject_name_suffix:
52
+ # - .shortlived.example.com
53
+ # - name: "classic"
54
+ # # no filter = default/fallback
55
+
41
56
  ###
42
57
  ### advanced options
43
58
  ###
@@ -1,18 +1,14 @@
1
- require 'acmesmith/domain_name_filter'
1
+ require 'acmesmith/subject_name_filter'
2
2
 
3
3
  module Acmesmith
4
4
  class ChallengeResponderFilter
5
- def initialize(responder, subject_name_exact: nil, subject_name_suffix: nil, subject_name_regexp: nil)
5
+ def initialize(responder, **filter)
6
6
  @responder = responder
7
- @domain_name_filter = DomainNameFilter.new(
8
- exact: subject_name_exact,
9
- suffix: subject_name_suffix,
10
- regexp: subject_name_regexp,
11
- )
7
+ @subject_name_filter = SubjectNameFilter.new(**filter)
12
8
  end
13
9
 
14
10
  def applicable?(domain)
15
- @domain_name_filter.match?(domain) && @responder.applicable?(domain)
11
+ @subject_name_filter.match?(domain) && @responder.applicable?(domain)
16
12
  end
17
13
  end
18
14
  end
@@ -42,7 +42,7 @@ module Acmesmith
42
42
  end
43
43
 
44
44
  def warn_test
45
- unless ENV['CI']
45
+ unless ENV['ACMESMITH_ACKNOWLEDGE_PEBBLE_CHALLTESTSRV_IS_INSECURE']
46
46
  $stderr.puts '!!!!!!!!! WARNING WARNING WARNING !!!!!!!!!'
47
47
  $stderr.puts '!!!! pebble-challtestsrv command is for TEST USAGE ONLY. It is trivially insecure, offering no authentication. Only use pebble-challtestsrv in a controlled test environment.'
48
48
  $stderr.puts '!!!! https://github.com/letsencrypt/pebble/blob/master/cmd/pebble-challtestsrv/README.md'
@@ -30,6 +30,10 @@ module Acmesmith
30
30
  raise NotImplementedError, "Domain authorization in advance is still not available in acme-client (v2). Required authorizations will be performed when ordering certificates"
31
31
  end
32
32
 
33
+ def list_profiles
34
+ acme.profiles
35
+ end
36
+
33
37
  def post_issue_hooks(name)
34
38
  cert = load_certificate_from_storage(name)
35
39
  execute_post_issue_hooks(cert)
@@ -221,6 +225,7 @@ module Acmesmith
221
225
  private_key: private_key,
222
226
  challenge_responder_rules: config.challenge_responders,
223
227
  chain_preferences: config.chain_preferences,
228
+ profile_rules: config.profile_rules,
224
229
  not_before: not_before,
225
230
  not_after: not_after
226
231
  )
@@ -54,6 +54,19 @@ module Acmesmith
54
54
  end
55
55
  map 'post-issue-hooks' => :post_issue_hooks
56
56
 
57
+ desc "list-profiles", "List available ACME certificate profiles"
58
+ def list_profiles
59
+ profiles = client.list_profiles
60
+ if profiles.nil? || profiles.empty?
61
+ puts "No profiles available from this ACME directory"
62
+ return
63
+ end
64
+ profiles.each do |name, description|
65
+ puts "#{name}: #{description}"
66
+ end
67
+ end
68
+ map 'list-profiles' => :list_profiles
69
+
57
70
  desc "list [NAME]", "list certificates or its versions"
58
71
  def list(name = nil)
59
72
  if name
@@ -2,6 +2,7 @@ require 'yaml'
2
2
  require 'acmesmith/storages'
3
3
  require 'acmesmith/challenge_responders'
4
4
  require 'acmesmith/challenge_responder_filter'
5
+ require 'acmesmith/subject_name_filter'
5
6
  require 'acmesmith/domain_name_filter'
6
7
  require 'acmesmith/post_issuing_hooks'
7
8
 
@@ -9,6 +10,7 @@ module Acmesmith
9
10
  class Config
10
11
  ChallengeResponderRule = Struct.new(:challenge_responder, :filter, keyword_init: true)
11
12
  ChainPreference = Struct.new(:root_issuer_name, :root_issuer_key_id, :filter, keyword_init: true)
13
+ ProfileRule = Data.define(:name, :filter)
12
14
 
13
15
  def self.load_yaml(path)
14
16
  new YAML.load_file(path)
@@ -35,6 +37,10 @@ module Acmesmith
35
37
  if @config.key?('chain_preferences') && !@config.fetch('chain_preferences').kind_of?(Array)
36
38
  raise ArgumentError, "config['chain_preferences'] must be an Array"
37
39
  end
40
+
41
+ if @config.key?('profiles') && !@config.fetch('profiles').kind_of?(Array)
42
+ raise ArgumentError, "config['profiles'] must be an Array"
43
+ end
38
44
  end
39
45
 
40
46
  def [](key)
@@ -124,6 +130,16 @@ module Acmesmith
124
130
  end
125
131
  end
126
132
 
133
+ def profile_rules
134
+ @profile_rules ||= begin
135
+ specs = @config['profiles'] || []
136
+ specs.map do |spec|
137
+ filter = spec.fetch('filter', {}).map { |k,v| [k.to_sym, v] }.to_h
138
+ ProfileRule.new(name: spec['name'], filter: SubjectNameFilter.new(**filter))
139
+ end
140
+ end
141
+ end
142
+
127
143
  # def post_actions
128
144
  # end
129
145
  end
@@ -14,18 +14,21 @@ module Acmesmith
14
14
  # @param chain_preferences [Array<Acmesmith::Config::ChainPreference>] chain_preferences
15
15
  # @param not_before [Time]
16
16
  # @param not_after [Time]
17
- def initialize(acme:, common_name:, identifiers:, private_key:, challenge_responder_rules:, chain_preferences:, not_before: nil, not_after: nil)
17
+ def initialize(acme:, common_name:, identifiers:, private_key:, challenge_responder_rules:, chain_preferences:, profile_rules: [], not_before: nil, not_after: nil)
18
18
  @acme = acme
19
19
  @common_name = common_name
20
20
  @identifiers = identifiers
21
21
  @private_key = private_key
22
22
  @challenge_responder_rules = challenge_responder_rules
23
23
  @chain_preferences = chain_preferences
24
+ @profile_rules = profile_rules
24
25
  @not_before = not_before
25
26
  @not_after = not_after
27
+
28
+ @order_url = nil # https://github.com/unixcharles/acme-client/pull/263
26
29
  end
27
30
 
28
- attr_reader :acme, :common_name, :identifiers, :private_key, :challenge_responder_rules, :chain_preferences, :not_before, :not_after
31
+ attr_reader :acme, :common_name, :identifiers, :private_key, :challenge_responder_rules, :chain_preferences, :profile_rules, :not_before, :not_after
29
32
 
30
33
  def perform!
31
34
  puts "=> Ordering a certificate for the following identifiers:"
@@ -35,9 +38,15 @@ module Acmesmith
35
38
  puts " * SAN: #{san}"
36
39
  end
37
40
 
41
+ resolved_profile = profile
42
+ if resolved_profile
43
+ puts
44
+ puts " * Profile: #{resolved_profile}"
45
+ end
46
+
38
47
  puts
39
48
  puts "=> Placing an order"
40
- @order = acme.new_order(identifiers: identifiers, not_before: not_before, not_after: not_after)
49
+ @order = acme.new_order(identifiers: identifiers, not_before: not_before, not_after: not_after, profile: resolved_profile)
41
50
  puts " * URL: #{order.url}"
42
51
 
43
52
  ensure_authorization()
@@ -72,12 +81,16 @@ module Acmesmith
72
81
  puts
73
82
 
74
83
  print " * Requesting..."
84
+ @order_url = order.url if defined?(Acme::Client::Error::OrderNotReloadable)
75
85
  order.finalize(csr: csr)
76
86
  puts" [ ok ]"
77
87
  end
78
88
 
79
89
  def wait_order_for_complete
90
+ # Workaround for https://github.com/unixcharles/acme-client/pull/263
91
+
80
92
  while %w(ready processing).include?(order.status)
93
+ order.instance_variable_set(:@url, @order_url) if @order_url
81
94
  order.reload()
82
95
  puts " * Waiting for complete: status=#{order.status}"
83
96
  sleep 2
@@ -104,6 +117,10 @@ module Acmesmith
104
117
  identifiers[1..-1]
105
118
  end
106
119
 
120
+ def profile
121
+ profile_rules.find { |rule| rule.filter.match?(common_name) }&.name
122
+ end
123
+
107
124
  # @return [Acme::Client::CertificateRequest]
108
125
  def csr
109
126
  @csr ||= Acme::Client::CertificateRequest.new(subject: { common_name: common_name }, names: sans, private_key: private_key)
@@ -0,0 +1,17 @@
1
+ require 'acmesmith/domain_name_filter'
2
+
3
+ module Acmesmith
4
+ class SubjectNameFilter
5
+ def initialize(subject_name_exact: nil, subject_name_suffix: nil, subject_name_regexp: nil)
6
+ @domain_name_filter = DomainNameFilter.new(
7
+ exact: subject_name_exact,
8
+ suffix: subject_name_suffix,
9
+ regexp: subject_name_regexp,
10
+ )
11
+ end
12
+
13
+ def match?(domain)
14
+ @domain_name_filter.match?(domain)
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Acmesmith
2
- VERSION = "2.8.0"
2
+ VERSION = "2.9.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acmesmith
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sorah Fukumori
@@ -183,6 +183,7 @@ files:
183
183
  - lib/acmesmith/storages/base.rb
184
184
  - lib/acmesmith/storages/filesystem.rb
185
185
  - lib/acmesmith/storages/s3.rb
186
+ - lib/acmesmith/subject_name_filter.rb
186
187
  - lib/acmesmith/utils/finder.rb
187
188
  - lib/acmesmith/version.rb
188
189
  homepage: https://github.com/sorah/acmesmith
@@ -206,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
207
  - !ruby/object:Gem::Version
207
208
  version: '0'
208
209
  requirements: []
209
- rubygems_version: 3.6.9
210
+ rubygems_version: 4.0.3
210
211
  specification_version: 4
211
212
  summary: ACME client (Let's encrypt client) to manage certificate in multi server
212
213
  environment with cloud services (e.g. AWS)