acmesmith 2.7.1 → 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: de52b6d87714b27035507ce19c8dccc5c876cebebed05ce06d9865858f2f0fec
4
- data.tar.gz: 7aef801a33ea26c22db4a38c1c3af2d9432862dc3ffa383e47b23a12ab296531
3
+ metadata.gz: 5230018ae89aac8b573c6353b1f447b078538a2ac3211e4d626eeba429a2a5af
4
+ data.tar.gz: a5ee69e253291d3ce838f248fed18ae5a29a01bd2aea4895b582e34ceeae5c9a
5
5
  SHA512:
6
- metadata.gz: fbcfef0acbd3e20a3f9245a41fd7f3bd74e33bf7333f307ee32047a330b7b30c213260e59cc4571b98fc650a89b9d424fa66097a776f52ad2f9a0ecbb1eb5a4a
7
- data.tar.gz: 94479e7616793c0cf51bfd3083a1ce6195c6ff886d7516fb62e841b5d93eaf14540dd9793030d33b273ef9b6fc3fa3f06abbb8be43af3a08e47b7f871aea2b60
6
+ metadata.gz: 27e82c020a219babe961f13db21bf72ee6c3084ce16ef023da4efbf494d004a91def0cc0f7c96316f9540249e51a888b803fdc48a82970ef319afebe4de08ada
7
+ data.tar.gz: ab4c2e3277e4af4cbbed7de6087c11c11ee4c0d741892a4da6ea458297fd726db87e49a0e73d32dff52111d61c4fdc60db04a775db70fcb2e06c05cf997f2454
data/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
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
+
20
+ ## v2.8.0 (2025-11-11)
21
+
22
+ ### Enhancements
23
+
24
+ - post_issuing_hooks/shell: Gained `$CERT_NAME` environment variable which is to replace `$COMMON_NAME` for certificates without CN field. [#76](https://github.com/sorah/acmesmith/pull/76)
25
+ - certificate: gained `name` method (Certificate#name) in addition to Certificate#common_name, for certificates without CN field. Plugin authors are encouraged to migrate on this new API. [#76](https://github.com/sorah/acmesmith/pull/76)
26
+
27
+ ### Fixes
28
+
29
+ - A certificate name is now inherited to a new certificate during autorenew and add-san command for stability, otherwise it could be saved under an another name when CA has issued the new certificate with different subject field or SANs field due to its policy/behaviour change; If you're using 3rd party storage plugins, it has to be updated to use Certificate#name instead of Certificate#common_name to support certificates without CN field. [#77](https://github.com/sorah/acmesmith/pull/77)
30
+
31
+ ### Updates
32
+
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)
35
+
1
36
  ## v2.7.1 (2025-08-21)
2
37
 
3
38
  ### Bug fixes
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
@@ -1,6 +1,6 @@
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) <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>
3
+ [![ci](https://github.com/sorah/acmesmith/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/sorah/acmesmith/actions/workflows/build.yml) <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
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).
@@ -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,17 +1,20 @@
1
1
  # Post Issuing Hook: Shell
2
2
 
3
- Execute specified command on a shell. Environment variable `${COMMON_NAME}` is available.
3
+ Execute specified command on a shell. Environment variable `${CERT_NAME}` is available.
4
4
 
5
5
  ```yaml
6
6
  post_issuing_hooks:
7
7
  "test.example.com":
8
8
  - shell:
9
- command: mail -s "New cert for ${COMMON_NAME} has been issued" user@example.com < /dev/null
9
+ command: mail -s "New cert for ${CERT_NAME} has been issued" user@example.com < /dev/null
10
10
  - shell:
11
- command: touch /tmp/certs-has-been-issued-${COMMON_NAME}
11
+ command: touch /tmp/certs-has-been-issued-${CERT_NAME}
12
12
  "admin.example.com":
13
13
  - shell:
14
- command: /usr/bin/dosomethingelse ${COMMON_NAME}
14
+ command: /usr/bin/dosomethingelse ${CERT_NAME}
15
15
  ```
16
16
 
17
+ ## What happened to `${COMMON_NAME}`?
17
18
 
19
+ In modern CA behavior, certificates may be missing CN field, or entire subject field.
20
+ It is still available, but we recommend to use `${CERT_NAME}` instead, which obtains a certificate name from certain sources.
@@ -17,10 +17,11 @@ module Acmesmith
17
17
  # Return Acmesmith::Certificate by an issued certificate
18
18
  # @param pem_chain [String]
19
19
  # @param csr [Acme::Client::CertificateRequest]
20
+ # @param name [String, nil]
20
21
  # @return [Acmesmith::Certificate]
21
- def self.by_issuance(pem_chain, csr)
22
+ def self.by_issuance(pem_chain, csr, name: nil)
22
23
  pems = split_pems(pem_chain)
23
- new(pems[0], pems[1..-1], csr.private_key, nil, csr)
24
+ new(pems[0], pems[1..-1], csr.private_key, nil, csr, name: name)
24
25
  end
25
26
 
26
27
  # @param certificate [OpenSSL::X509::Certificate, String]
@@ -28,7 +29,9 @@ module Acmesmith
28
29
  # @param private_key [String, OpenSSL::PKey::PKey]
29
30
  # @param key_passphrase [String, nil]
30
31
  # @param csr [String, OpenSSL::X509::Request, nil]
31
- def initialize(certificate, chain, private_key, key_passphrase = nil, csr = nil)
32
+ # @param name [String, nil]
33
+ def initialize(certificate, chain, private_key, key_passphrase = nil, csr = nil, name: nil)
34
+ @name = name
32
35
  @certificate = case certificate
33
36
  when OpenSSL::X509::Certificate
34
37
  certificate
@@ -94,6 +97,8 @@ module Acmesmith
94
97
  # @return [OpenSSL::X509::Request]
95
98
  attr_reader :csr
96
99
 
100
+ attr_writer :name
101
+
97
102
  # Try to decrypt private_key if encrypted.
98
103
  # @param pw [String] passphrase for encrypted PEM
99
104
  # @raise [PrivateKeyDecrypted] if private_key is decrypted
@@ -128,18 +133,38 @@ module Acmesmith
128
133
  chain.map(&:to_pem).join("\n")
129
134
  end
130
135
 
131
- # @return [String] common name
136
+ # Returns a predicted certificate name, taken from common name or first SAN.
137
+ # Note that this value can contain colons (':') if name is taken from non-DNS subject alternative name.
138
+ # @return [String] certificate name
139
+ def name
140
+ @name || common_name || sans.first || all_sans.first
141
+ end
142
+
143
+ # Returns a certificate common name taken from the certificate subject's CN field.
144
+ # Under the real CA, CNs can be missing. Use #name instead to retrieve the certificate name for most cases.
145
+ # ref. https://github.com/letsencrypt/pebble/pull/491#pullrequestreview-2718607820
146
+ # @return [String, nil] common name
132
147
  def common_name
133
- certificate.subject.to_a.assoc('CN')[1]
148
+ certificate.subject.to_a.assoc('CN')&.fetch(1)
134
149
  end
135
150
 
136
- # @return [Array<String>] Subject Alternative Names (dNSname)
137
- def sans
151
+ # Returns a list of subject alternative names included in the certificate.
152
+ # @return [Array<String>] Subject Alternative Names
153
+ def all_sans
138
154
  certificate.extensions.select { |_| _.oid == 'subjectAltName' }.flat_map do |ext|
139
- ext.value.split(/,\s*/).select { |_| _.start_with?('DNS:') }.map { |_| _[4..-1] }
155
+ ext.value.split(/,\s*/)
140
156
  end
141
157
  end
142
158
 
159
+ # Returns a list of DNS subject alternative names included in the certificate.
160
+ # Strips DNS: prefix from returned values.
161
+ # @return [Array<String>] Subject Alternative Names (dNSname)
162
+ def sans
163
+ all_sans.select do |san|
164
+ san.start_with?('DNS:')
165
+ end.map { |_| _[4..-1] }
166
+ end
167
+
143
168
  # @return [String] Version string (consists of NotBefore time & certificate serial)
144
169
  def version
145
170
  "#{certificate.not_before.utc.strftime('%Y%m%d-%H%M%S')}_#{certificate.serial.to_i.to_s(16)}"
@@ -147,7 +172,7 @@ module Acmesmith
147
172
 
148
173
  # @return [OpenSSL::PKCS12]
149
174
  def pkcs12(passphrase)
150
- OpenSSL::PKCS12.create(passphrase, common_name, private_key, certificate, chain)
175
+ OpenSSL::PKCS12.create(passphrase, name, private_key, certificate, chain)
151
176
  end
152
177
 
153
178
  # @return [CertificateExport]
@@ -3,13 +3,13 @@ require 'acmesmith/certificate'
3
3
  module Acmesmith
4
4
  class CertificateRetrievingService
5
5
  # @param acme [Acme::Client]
6
- # @param common_name [String]
6
+ # @param name [String]
7
7
  # @param url [String] ACME Certificate URL
8
8
  # @param chain_preferences [Array<Acmesmith::Config::ChainPreference>]
9
- def initialize(acme, common_name, url, chain_preferences: [])
9
+ def initialize(acme, name, url, chain_preferences: [])
10
10
  @acme = acme
11
11
  @url = url
12
- @chain_preferences = chain_preferences.select { |_| _.filter.match?(common_name) }
12
+ @chain_preferences = chain_preferences.select { |_| _.filter.match?(name) }
13
13
  end
14
14
 
15
15
  attr_reader :acme
@@ -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,35 +30,39 @@ 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 post_issue_hooks(common_name)
34
- cert = storage.get_certificate(common_name)
33
+ def list_profiles
34
+ acme.profiles
35
+ end
36
+
37
+ def post_issue_hooks(name)
38
+ cert = load_certificate_from_storage(name)
35
39
  execute_post_issue_hooks(cert)
36
40
  end
37
41
 
38
42
  def execute_post_issue_hooks(certificate)
39
- hooks = config.post_issuing_hooks(certificate.common_name)
43
+ hooks = config.post_issuing_hooks(certificate.name)
40
44
  return if hooks.empty?
41
- puts "=> Executing post issuing hooks for CN=#{certificate.common_name}"
45
+ puts "=> Executing post issuing hooks for CN=#{certificate.name}"
42
46
  hooks.each do |hook|
43
47
  hook.run(certificate: certificate)
44
48
  end
45
49
  puts
46
50
  end
47
51
 
48
- def certificate_versions(common_name)
49
- storage.list_certificate_versions(common_name).sort
52
+ def certificate_versions(name)
53
+ storage.list_certificate_versions(name).sort
50
54
  end
51
55
 
52
56
  def certificates_list
53
57
  storage.list_certificates.sort
54
58
  end
55
59
 
56
- def current(common_name)
57
- storage.get_current_certificate_version(common_name)
60
+ def current(name)
61
+ storage.get_current_certificate_version(name)
58
62
  end
59
63
 
60
- def get_certificate(common_name, version: 'current', type: 'text')
61
- cert = storage.get_certificate(common_name, version: version)
64
+ def get_certificate(name, version: 'current', type: 'text')
65
+ cert = storage.get_certificate(name, version: version)
62
66
 
63
67
  certs = []
64
68
  case type
@@ -76,8 +80,8 @@ module Acmesmith
76
80
  certs
77
81
  end
78
82
 
79
- def save_certificate(common_name, version: 'current', mode: '0600', output:, type: 'fullchain')
80
- cert = storage.get_certificate(common_name, version: version)
83
+ def save_certificate(name, version: 'current', mode: '0600', output:, type: 'fullchain')
84
+ cert = storage.get_certificate(name, version: version)
81
85
  File.open(output, 'w', mode.to_i(8)) do |f|
82
86
  case type
83
87
  when 'certificate'
@@ -90,23 +94,23 @@ module Acmesmith
90
94
  end
91
95
  end
92
96
 
93
- def get_private_key(common_name, version: 'current')
94
- cert = storage.get_certificate(common_name, version: version)
97
+ def get_private_key(name, version: 'current')
98
+ cert = storage.get_certificate(name, version: version)
95
99
  cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
96
100
 
97
101
  cert.private_key.to_pem
98
102
  end
99
103
 
100
- def save_private_key(common_name, version: 'current', mode: '0600', output:)
101
- cert = storage.get_certificate(common_name, version: version)
104
+ def save_private_key(name, version: 'current', mode: '0600', output:)
105
+ cert = storage.get_certificate(name, version: version)
102
106
  cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
103
107
  File.open(output, 'w', mode.to_i(8)) do |f|
104
108
  f.puts(cert.private_key)
105
109
  end
106
110
  end
107
111
 
108
- def save_pkcs12(common_name, version: 'current', mode: '0600', output:, passphrase:)
109
- cert = storage.get_certificate(common_name, version: version)
112
+ def save_pkcs12(name, version: 'current', mode: '0600', output:, passphrase:)
113
+ cert = storage.get_certificate(name, version: version)
110
114
  cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
111
115
 
112
116
  p12 = cert.pkcs12(passphrase)
@@ -115,17 +119,18 @@ module Acmesmith
115
119
  end
116
120
  end
117
121
 
118
- def save(common_name, version: 'current', **kwargs)
119
- cert = storage.get_certificate(common_name, version: version)
122
+ def save(name, version: 'current', **kwargs)
123
+ cert = storage.get_certificate(name, version: version)
120
124
  cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
121
125
 
122
126
  SaveCertificateService.new(cert, **kwargs).perform!
123
127
  end
124
128
 
125
- def autorenew(days: 30, remaining_life: nil, common_names: nil)
126
- (common_names || storage.list_certificates).each do |cn|
129
+ def autorenew(days: 30, remaining_life: nil, names: nil)
130
+ (names || storage.list_certificates).each do |cn|
127
131
  puts "=> #{cn}"
128
- cert = storage.get_certificate(cn)
132
+ cert = load_certificate_from_storage(cn)
133
+
129
134
  not_after = cert.certificate.not_after.utc
130
135
 
131
136
  lifetime = cert.certificate.not_after.utc - cert.certificate.not_before.utc
@@ -139,17 +144,17 @@ module Acmesmith
139
144
  puts " Not valid after: #{not_after} (lifetime=#{format_duration(lifetime+1)}, remaining=#{format_duration(remaining)}, #{"%0.2f" % (ratio.to_f*100)}%)"
140
145
  next unless has_to_renew
141
146
 
142
- puts " * Renewing: CN=#{cert.common_name}, SANs=#{cert.sans.join(',')}"
143
- order_with_private_key(cert.common_name, *cert.sans, private_key: regenerate_private_key(cert.public_key))
147
+ puts " * Renewing: #{cert.name.inspect}, SANs=#{cert.sans.join(',')}"
148
+ order_with_private_key(cert.name, *cert.sans, private_key: regenerate_private_key(cert.public_key))
144
149
  end
145
150
  end
146
151
 
147
- def add_san(common_name, *add_sans)
148
- puts "=> reissuing CN=#{common_name} with new SANs #{add_sans.join(?,)}"
149
- cert = storage.get_certificate(common_name)
152
+ def add_san(name, *add_sans)
153
+ puts "=> reissuing #{name.inspect} with new SANs #{add_sans.join(?,)}"
154
+ cert = load_certificate_from_storage(name)
150
155
  sans = cert.sans + add_sans
151
156
  puts " * SANs will be: #{sans.join(?,)}"
152
- order_with_private_key(cert.common_name, *sans, private_key: regenerate_private_key(cert.public_key))
157
+ order_with_private_key(cert.name, *sans, private_key: regenerate_private_key(cert.public_key))
153
158
  end
154
159
 
155
160
  private
@@ -212,13 +217,15 @@ module Acmesmith
212
217
  end
213
218
  end
214
219
 
215
- def order_with_private_key(*identifiers, private_key:, not_before: nil, not_after: nil)
220
+ def order_with_private_key(name, *identifiers, private_key:, not_before: nil, not_after: nil)
216
221
  order = OrderingService.new(
217
222
  acme: acme,
218
- identifiers: identifiers,
223
+ common_name: name,
224
+ identifiers: [name, *identifiers],
219
225
  private_key: private_key,
220
226
  challenge_responder_rules: config.challenge_responders,
221
227
  chain_preferences: config.chain_preferences,
228
+ profile_rules: config.profile_rules,
222
229
  not_before: not_before,
223
230
  not_after: not_after
224
231
  )
@@ -258,5 +265,12 @@ module Acmesmith
258
265
  raise ArgumentError, "Unknown key type: #{template.class}"
259
266
  end
260
267
  end
268
+
269
+ # Load certificate from storage, inherit name property to loaded certificate to ensure stability of #name during renewal.
270
+ def load_certificate_from_storage(name)
271
+ retval = storage.get_certificate(name)
272
+ retval.name = name
273
+ retval
274
+ end
261
275
  end
262
276
  end