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 +4 -4
- data/CHANGELOG.md +35 -0
- data/Dockerfile +7 -6
- data/README.md +25 -3
- data/config.sample.yml +15 -0
- data/docs/post_issuing_hooks/shell.md +7 -4
- data/lib/acmesmith/certificate.rb +34 -9
- data/lib/acmesmith/certificate_retrieving_service.rb +3 -3
- data/lib/acmesmith/challenge_responder_filter.rb +4 -8
- data/lib/acmesmith/challenge_responders/pebble_challtestsrv_dns.rb +1 -1
- data/lib/acmesmith/client.rb +45 -31
- data/lib/acmesmith/command.rb +53 -40
- data/lib/acmesmith/config.rb +16 -0
- data/lib/acmesmith/ordering_service.rb +24 -10
- data/lib/acmesmith/post_issuing_hooks/shell.rb +2 -2
- data/lib/acmesmith/save_certificate_service.rb +1 -1
- data/lib/acmesmith/storages/base.rb +8 -8
- data/lib/acmesmith/storages/filesystem.rb +16 -16
- data/lib/acmesmith/storages/s3.rb +16 -16
- data/lib/acmesmith/subject_name_filter.rb +17 -0
- data/lib/acmesmith/version.rb +1 -1
- metadata +8 -14
- data/.github/FUNDING.yml +0 -2
- data/.github/stale.yml +0 -17
- data/.github/workflows/build.yml +0 -113
- data/.gitignore +0 -12
- data/.rspec +0 -2
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -84
- data/acmesmith.gemspec +0 -33
- data/script/console +0 -14
- data/script/setup +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5230018ae89aac8b573c6353b1f447b078538a2ac3211e4d626eeba429a2a5af
|
|
4
|
+
data.tar.gz: a5ee69e253291d3ce838f248fed18ae5a29a01bd2aea4895b582e34ceeae5c9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
18
|
+
RUN bundle install --path /gems --jobs 100 --without development
|
|
16
19
|
|
|
17
|
-
|
|
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
|
-
](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://
|
|
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
|
|
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 `${
|
|
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 ${
|
|
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-${
|
|
11
|
+
command: touch /tmp/certs-has-been-issued-${CERT_NAME}
|
|
12
12
|
"admin.example.com":
|
|
13
13
|
- shell:
|
|
14
|
-
command: /usr/bin/dosomethingelse ${
|
|
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
|
-
|
|
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
|
-
#
|
|
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')
|
|
148
|
+
certificate.subject.to_a.assoc('CN')&.fetch(1)
|
|
134
149
|
end
|
|
135
150
|
|
|
136
|
-
#
|
|
137
|
-
|
|
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*/)
|
|
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,
|
|
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
|
|
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,
|
|
9
|
+
def initialize(acme, name, url, chain_preferences: [])
|
|
10
10
|
@acme = acme
|
|
11
11
|
@url = url
|
|
12
|
-
@chain_preferences = chain_preferences.select { |_| _.filter.match?(
|
|
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/
|
|
1
|
+
require 'acmesmith/subject_name_filter'
|
|
2
2
|
|
|
3
3
|
module Acmesmith
|
|
4
4
|
class ChallengeResponderFilter
|
|
5
|
-
def initialize(responder,
|
|
5
|
+
def initialize(responder, **filter)
|
|
6
6
|
@responder = responder
|
|
7
|
-
@
|
|
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
|
-
@
|
|
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['
|
|
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'
|
data/lib/acmesmith/client.rb
CHANGED
|
@@ -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
|
|
34
|
-
|
|
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.
|
|
43
|
+
hooks = config.post_issuing_hooks(certificate.name)
|
|
40
44
|
return if hooks.empty?
|
|
41
|
-
puts "=> Executing post issuing hooks for CN=#{certificate.
|
|
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(
|
|
49
|
-
storage.list_certificate_versions(
|
|
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(
|
|
57
|
-
storage.get_current_certificate_version(
|
|
60
|
+
def current(name)
|
|
61
|
+
storage.get_current_certificate_version(name)
|
|
58
62
|
end
|
|
59
63
|
|
|
60
|
-
def get_certificate(
|
|
61
|
-
cert = storage.get_certificate(
|
|
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(
|
|
80
|
-
cert = storage.get_certificate(
|
|
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(
|
|
94
|
-
cert = storage.get_certificate(
|
|
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(
|
|
101
|
-
cert = storage.get_certificate(
|
|
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(
|
|
109
|
-
cert = storage.get_certificate(
|
|
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(
|
|
119
|
-
cert = storage.get_certificate(
|
|
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,
|
|
126
|
-
(
|
|
129
|
+
def autorenew(days: 30, remaining_life: nil, names: nil)
|
|
130
|
+
(names || storage.list_certificates).each do |cn|
|
|
127
131
|
puts "=> #{cn}"
|
|
128
|
-
cert =
|
|
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:
|
|
143
|
-
order_with_private_key(cert.
|
|
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(
|
|
148
|
-
puts "=> reissuing
|
|
149
|
-
cert =
|
|
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.
|
|
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
|
-
|
|
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
|