acme-client 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +36 -0
- data/README.md +138 -94
- data/lib/acme/client.rb +223 -63
- data/lib/acme/client/certificate_request.rb +0 -2
- data/lib/acme/client/error.rb +52 -13
- data/lib/acme/client/faraday_middleware.rb +23 -24
- data/lib/acme/client/jwk/base.rb +7 -6
- data/lib/acme/client/jwk/ecdsa.rb +0 -2
- data/lib/acme/client/resources.rb +4 -2
- data/lib/acme/client/resources/account.rb +49 -0
- data/lib/acme/client/resources/authorization.rb +62 -32
- data/lib/acme/client/resources/challenges.rb +20 -5
- data/lib/acme/client/resources/challenges/base.rb +26 -22
- data/lib/acme/client/resources/challenges/dns01.rb +1 -1
- data/lib/acme/client/resources/challenges/http01.rb +1 -1
- data/lib/acme/client/resources/directory.rb +75 -0
- data/lib/acme/client/resources/order.rb +58 -0
- data/lib/acme/client/version.rb +1 -1
- metadata +5 -5
- data/lib/acme/client/certificate.rb +0 -30
- data/lib/acme/client/resources/challenges/tls_sni01.rb +0 -25
- data/lib/acme/client/resources/registration.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ccaa8fb7fadbae1a6d93f99ad319ca95261994cbec494ab885028aae2f894a6a
|
4
|
+
data.tar.gz: 224d05d0f66cb37ffcbcf1bf05f649e0278dc097518b927dcc4a2e635ba29357
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c461b3d255fc35d3b21411632ff4f26700fd0969547f8a7a93879c719e2383f58badfc0278bc45df4780b293094af3b2d560494ac3f00076dae23affbe697682
|
7
|
+
data.tar.gz: 477869ea08075d8e9d70afa80f4576f6a0ffb936bfb70244b0bf6f8d34d2da593d76acf6231d266fd920eeb0fbf4045e4605869c77f9b06377487cbb4902c014
|
data/.rubocop.yml
CHANGED
@@ -99,5 +99,41 @@ Style/BlockDelimiters:
|
|
99
99
|
Style/Lambda:
|
100
100
|
Enabled: false
|
101
101
|
|
102
|
+
Style/GuardClause:
|
103
|
+
Enabled: false
|
104
|
+
|
105
|
+
Style/Alias:
|
106
|
+
Enabled: false
|
107
|
+
|
108
|
+
Lint/AmbiguousOperator:
|
109
|
+
Enabled: false
|
110
|
+
|
111
|
+
Metrics/MethodLength:
|
112
|
+
Enabled: false
|
113
|
+
|
114
|
+
Metrics/PerceivedComplexity:
|
115
|
+
Enabled: false
|
116
|
+
|
117
|
+
Metrics/CyclomaticComplexity:
|
118
|
+
Enabled: false
|
119
|
+
|
120
|
+
Metrics/AbcSize:
|
121
|
+
Enabled: false
|
122
|
+
|
123
|
+
Metrics/ClassLength:
|
124
|
+
Enabled: false
|
125
|
+
|
126
|
+
Style/MutableConstant:
|
127
|
+
Enabled: false
|
128
|
+
|
129
|
+
Style/GlobalVars:
|
130
|
+
Enabled: false
|
131
|
+
|
132
|
+
Style/ExpandPathArguments:
|
133
|
+
Enabled: false
|
134
|
+
|
135
|
+
Security/JSONLoad:
|
136
|
+
Enabled: false
|
137
|
+
|
102
138
|
Naming/AccessorMethodName:
|
103
139
|
Enabled: false
|
data/README.md
CHANGED
@@ -1,17 +1,15 @@
|
|
1
|
-
# Deprecated
|
2
|
-
|
3
|
-
This branch track the client for ACMEv1.
|
4
|
-
|
5
1
|
# Acme::Client
|
6
2
|
|
7
3
|
[![Build Status](https://travis-ci.org/unixcharles/acme-client.svg?branch=master)](https://travis-ci.org/unixcharles/acme-client)
|
8
4
|
|
9
|
-
`acme-client` is a client implementation of the [
|
5
|
+
`acme-client` is a client implementation of the [ACMEv2](https://github.com/ietf-wg-acme/acme) protocol in Ruby.
|
10
6
|
|
11
|
-
You can find the ACME reference implementations of the [server](https://github.com/letsencrypt/boulder) in Go and the [client](https://github.com/
|
7
|
+
You can find the ACME reference implementations of the [server](https://github.com/letsencrypt/boulder) in Go and the [client](https://github.com/certbot/certbot) in Python.
|
12
8
|
|
13
9
|
ACME is part of the [Letsencrypt](https://letsencrypt.org/) project, which goal is to provide free SSL/TLS certificates with automation of the acquiring and renewal process.
|
14
10
|
|
11
|
+
You can find ACMEv1 compatible client in the [acme-v1](https://github.com/unixcharles/acme-client/tree/acme-v1) branch.
|
12
|
+
|
15
13
|
## Installation
|
16
14
|
|
17
15
|
Via RubyGems:
|
@@ -25,143 +23,189 @@ gem 'acme-client'
|
|
25
23
|
```
|
26
24
|
|
27
25
|
## Usage
|
26
|
+
* [Setting up a client](#setting-up-a-client)
|
27
|
+
* [Account management](#account-management)
|
28
|
+
* [Obtaining a certificate](#obtaining-a-certificate)
|
29
|
+
* [Ordering a certificate](#ordering-a-certificate)
|
30
|
+
* [Completing an HTTP challenge](#preparing-for-http-challenge)
|
31
|
+
* [Completing an DNS challenge](#preparing-for-dns-challenge)
|
32
|
+
* [Requesting a challenge verification](#requesting-a-challenge-verification)
|
33
|
+
* [Downloading a certificate](#downloading-a-certificate)
|
34
|
+
* [Extra](#extra)
|
35
|
+
* [Certificate revokation](#certificate-revokation)
|
36
|
+
* [Certificate renewal](#certificate-renewal)
|
37
|
+
|
38
|
+
## Setting up a client
|
39
|
+
|
40
|
+
The client is initialized with a private key and the directory of your ACME provider.
|
41
|
+
|
42
|
+
LetsEncrypt's `directory` is `https://acme-v02.api.letsencrypt.org/directory`.
|
43
|
+
|
44
|
+
They also have a staging enpoind at `https://acme-staging-v02.api.letsencrypt.org/directory`.
|
28
45
|
|
29
|
-
|
46
|
+
`acme-ruby` expects `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC`
|
30
47
|
|
31
|
-
|
48
|
+
You can generate one in Ruby using OpenSSL.
|
32
49
|
|
33
50
|
```ruby
|
34
|
-
# We're going to need a private key.
|
35
51
|
require 'openssl'
|
36
52
|
private_key = OpenSSL::PKey::RSA.new(4096)
|
53
|
+
```
|
54
|
+
|
55
|
+
Or load one from a PEM file
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
require 'openssl'
|
59
|
+
OpenSSL::PKey::RSA.new(File.read('/path/to/private_key.pem'))
|
60
|
+
```
|
37
61
|
|
38
|
-
|
39
|
-
# WARNING: This endpoint is the production endpoint, which is rate limited and will produce valid certificates.
|
40
|
-
# You should probably use the staging endpoint for all your experimentation:
|
41
|
-
# endpoint = 'https://acme-staging.api.letsencrypt.org/'
|
42
|
-
endpoint = 'https://acme-v01.api.letsencrypt.org/'
|
62
|
+
See [RSA](https://ruby.github.io/openssl/OpenSSL/PKey/RSA.html) and [EC](https://ruby.github.io/openssl/OpenSSL/PKey/EC.html) for documentation.
|
43
63
|
|
44
|
-
# Initialize the client
|
45
|
-
require 'acme-client'
|
46
|
-
client = Acme::Client.new(private_key: private_key, endpoint: endpoint, connection_options: { request: { open_timeout: 5, timeout: 5 } })
|
47
64
|
|
48
|
-
|
49
|
-
|
65
|
+
```ruby
|
66
|
+
client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory')
|
67
|
+
```
|
68
|
+
|
69
|
+
If your account is already registered, you can save some API calls by passing your key ID directly. This will avoid an unnecessary API call to retrieve it from your private key.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory', kid: 'https://example.com/acme/acct/1')
|
73
|
+
```
|
74
|
+
|
75
|
+
## Account management
|
76
|
+
|
77
|
+
Accounts are tied to a private key. Before being allowed to create orders, the account must be registered and the ToS accepted using the private key. The account will be assigned a key ID.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory')
|
81
|
+
account = client.new_account(contact: 'mailto:info@example.com', terms_of_service_agreed: true)
|
82
|
+
```
|
83
|
+
|
84
|
+
After the registration you can retrieve the account key indentifier (kid).
|
50
85
|
|
51
|
-
|
52
|
-
|
86
|
+
```ruby
|
87
|
+
client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory')
|
88
|
+
account = client.new_account(contact: 'mailto:info@example.com', terms_of_service_agreed: true)
|
89
|
+
account.kid # => <kid string>
|
53
90
|
```
|
54
91
|
|
55
|
-
|
92
|
+
## Obtaining a certificate
|
93
|
+
### Ordering a certificate
|
94
|
+
|
95
|
+
To order a new certificate, the client must provide a list of identifiers.
|
96
|
+
|
97
|
+
The returned order will contain a list of `Authorization` that need to be completed in other to finalize the order, generally one per identifier.
|
98
|
+
|
99
|
+
Each authorization contains multiple challenges, typically a `dns-01` and a `http-01` challenge. The applicant is only required to complete one the challenges.
|
56
100
|
|
57
|
-
|
101
|
+
You can access the challenge you wish to complete using the `#dns` or `#http` method.
|
58
102
|
|
59
103
|
```ruby
|
60
|
-
|
104
|
+
order = client.new_order(identifiers: ['example.com'])
|
105
|
+
authorization = order.authorizations.first
|
106
|
+
challenge = authorization.http
|
107
|
+
```
|
61
108
|
|
62
|
-
|
63
|
-
# and _must not_ try to solve another challenge.
|
64
|
-
authorization.status # => 'pending'
|
109
|
+
### Preparing for HTTP challenge
|
65
110
|
|
66
|
-
|
67
|
-
# any associated challenges via Acme::Client#fetch_authorization.
|
68
|
-
authorization.uri # => '...'
|
111
|
+
To complete the HTTP challenge, you must return a file using HTTP.
|
69
112
|
|
70
|
-
|
71
|
-
challenge = authorization.http01
|
113
|
+
The path follows the following format:
|
72
114
|
|
73
|
-
|
115
|
+
> .well-known/acme-challenge/#{token}
|
74
116
|
|
75
|
-
|
76
|
-
challenge.token # => "some_token"
|
117
|
+
And the file content is the key authorization. The HTTP01 object has utility methods to generate them.
|
77
118
|
|
78
|
-
|
79
|
-
|
119
|
+
```ruby
|
120
|
+
> http_challenge.content_type # => 'text/plain'
|
121
|
+
> http_challenge.file_content # => example_token.TO1xJ0UDgfQ8WY5zT3txynup87UU3PhcDEIcuPyw4QU
|
122
|
+
> http_challenge.filename # => '.well-known/acme-challenge/example_token'
|
123
|
+
> http_challenge.token # => 'example_token'
|
124
|
+
```
|
80
125
|
|
81
|
-
|
82
|
-
challenge.file_content # => 'string token and JWK thumbprint'
|
126
|
+
For test purposes you can just save the challenge file and use Ruby to serve it:
|
83
127
|
|
84
|
-
|
85
|
-
|
128
|
+
```bash
|
129
|
+
ruby -run -e httpd public -p 8080 --bind-address 0.0.0.0
|
130
|
+
```
|
131
|
+
|
132
|
+
### Preparing for DNS challenge
|
86
133
|
|
87
|
-
|
88
|
-
FileUtils.mkdir_p( File.join( 'public', File.dirname( challenge.filename ) ) )
|
134
|
+
To complete the DNS challenge, you must set a DNS record to prove that you control the domain.
|
89
135
|
|
90
|
-
|
91
|
-
File.write( File.join( 'public', challenge.filename), challenge.file_content )
|
136
|
+
The DNS01 object has utility methods to generate them.
|
92
137
|
|
93
|
-
|
94
|
-
|
138
|
+
```ruby
|
139
|
+
dns_challenge.record_name # => '_acme-challenge'
|
140
|
+
dns_challenge.record_type # => 'TXT'
|
141
|
+
dns_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8'
|
142
|
+
```
|
95
143
|
|
96
|
-
|
97
|
-
# You can run a webserver in another console for that purpose. You may need to forward ports on your router.
|
98
|
-
#
|
99
|
-
# $ ruby -run -e httpd public -p 8080 --bind-address 0.0.0.0
|
144
|
+
### Requesting a challenge verification
|
100
145
|
|
101
|
-
|
102
|
-
challenge = client.fetch_authorization(File.read('authorization_uri')).http01
|
146
|
+
Once you are ready to complete the challenge, you can request the server perform the verification.
|
103
147
|
|
104
|
-
|
105
|
-
challenge.
|
106
|
-
|
148
|
+
```ruby
|
149
|
+
challenge.request_validation
|
150
|
+
```
|
107
151
|
|
108
|
-
|
109
|
-
sleep(1)
|
152
|
+
The validation is performed asynchronously and can take some time to be performed by the server.
|
110
153
|
|
111
|
-
|
112
|
-
# if the former is 'valid' you can already issue a certificate and the status of
|
113
|
-
# the challenge is not relevant and in fact may never change from pending.
|
114
|
-
challenge.authorization.verify_status # => 'valid'
|
115
|
-
challenge.error # => nil
|
154
|
+
You can poll until its status change.
|
116
155
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
156
|
+
```ruby
|
157
|
+
while challenge.status == 'pending'
|
158
|
+
sleep(2)
|
159
|
+
challenge.reload
|
160
|
+
end
|
161
|
+
challenge.status # => 'valid'
|
121
162
|
```
|
122
163
|
|
123
|
-
###
|
164
|
+
### Downloading a certificate
|
165
|
+
|
166
|
+
Once all required authorizations have been validated through challenges, the order can be finalized using a CSR ([Certificate Signing Request](https://en.wikipedia.org/wiki/Certificate_signing_request)).
|
167
|
+
|
168
|
+
A CSR can be slightly tricky to generate using OpenSSL from Ruby standard library. `acme-client` provide a utility class `CertificateRequest` to help with that.
|
124
169
|
|
125
|
-
|
170
|
+
Certificate generation happens asynchronously. You may need to poll.
|
126
171
|
|
127
172
|
```ruby
|
128
|
-
|
129
|
-
|
130
|
-
|
173
|
+
csr = Acme::Client::CertificateRequest.new(private_key: private_key, subject: { common_name: 'example.com' })
|
174
|
+
order.finalize(csr: csr)
|
175
|
+
sleep(1) while order.status == 'processing'
|
176
|
+
order.certificate # => PEM-formatted certificate
|
177
|
+
```
|
178
|
+
|
179
|
+
## Extra
|
131
180
|
|
132
|
-
|
133
|
-
# a valid DER encoded CSR when calling to_der on it. For example an
|
134
|
-
# OpenSSL::X509::Request should work too.
|
135
|
-
certificate = client.new_certificate(csr) # => #<Acme::Client::Certificate ....>
|
181
|
+
### Certificate revokation
|
136
182
|
|
137
|
-
|
138
|
-
File.write("privkey.pem", certificate.request.private_key.to_pem)
|
139
|
-
File.write("cert.pem", certificate.to_pem)
|
140
|
-
File.write("chain.pem", certificate.chain_to_pem)
|
141
|
-
File.write("fullchain.pem", certificate.fullchain_to_pem)
|
183
|
+
To revoke a certificate you can call `#revoke` with the certificate.
|
142
184
|
|
143
|
-
|
144
|
-
|
145
|
-
# :Port => 8443,
|
146
|
-
# :DocumentRoot => Dir.pwd,
|
147
|
-
# :SSLEnable => true,
|
148
|
-
# :SSLPrivateKey => OpenSSL::PKey::RSA.new( File.read('privkey.pem') ),
|
149
|
-
# :SSLCertificate => OpenSSL::X509::Certificate.new( File.read('cert.pem') )); trap('INT') { s.shutdown }; s.start"
|
185
|
+
```ruby
|
186
|
+
client.revoke(certificate: certificate)
|
150
187
|
```
|
151
188
|
|
152
|
-
|
189
|
+
### Certificate renewal
|
153
190
|
|
154
|
-
|
191
|
+
The is no renewal process, just create a new order.
|
155
192
|
|
156
|
-
|
193
|
+
|
194
|
+
## Not implemented
|
195
|
+
|
196
|
+
- Account Key Roll-over.
|
197
|
+
|
198
|
+
## Requirements
|
157
199
|
|
158
200
|
Ruby >= 2.1
|
159
201
|
|
160
202
|
## Development
|
161
203
|
|
162
|
-
All the tests use VCR to mock the interaction with the server
|
163
|
-
|
164
|
-
|
204
|
+
All the tests use VCR to mock the interaction with the server. If you need to record new interaction you can specify the directory URL with the `ACME_DIRECTORY_URL` environment variable.
|
205
|
+
|
206
|
+
```
|
207
|
+
ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory rspec
|
208
|
+
```
|
165
209
|
|
166
210
|
## Pull request?
|
167
211
|
|
data/lib/acme/client.rb
CHANGED
@@ -12,7 +12,6 @@ module Acme; end
|
|
12
12
|
class Acme::Client; end
|
13
13
|
|
14
14
|
require 'acme/client/version'
|
15
|
-
require 'acme/client/certificate'
|
16
15
|
require 'acme/client/certificate_request'
|
17
16
|
require 'acme/client/self_sign_certificate'
|
18
17
|
require 'acme/client/resources'
|
@@ -22,15 +21,14 @@ require 'acme/client/error'
|
|
22
21
|
require 'acme/client/util'
|
23
22
|
|
24
23
|
class Acme::Client
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
def initialize(jwk: nil, private_key: nil, endpoint: DEFAULT_ENDPOINT, directory_uri: nil, connection_options: {})
|
24
|
+
DEFAULT_DIRECTORY = 'http://127.0.0.1:4000/directory'.freeze
|
25
|
+
repo_url = 'https://github.com/unixcharles/acme-client'
|
26
|
+
USER_AGENT = "Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze
|
27
|
+
CONTENT_TYPES = {
|
28
|
+
pem: 'application/pem-certificate-chain'
|
29
|
+
}
|
30
|
+
|
31
|
+
def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {})
|
34
32
|
if jwk.nil? && private_key.nil?
|
35
33
|
raise ArgumentError, 'must specify jwk or private_key'
|
36
34
|
end
|
@@ -41,93 +39,255 @@ class Acme::Client
|
|
41
39
|
Acme::Client::JWK.from_private_key(private_key)
|
42
40
|
end
|
43
41
|
|
44
|
-
@
|
42
|
+
@kid, @connection_options = kid, connection_options
|
43
|
+
@directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options)
|
45
44
|
@nonces ||= []
|
46
|
-
load_directory!
|
47
45
|
end
|
48
46
|
|
49
|
-
attr_reader :jwk, :nonces
|
47
|
+
attr_reader :jwk, :nonces
|
50
48
|
|
51
|
-
def
|
49
|
+
def new_account(contact:, terms_of_service_agreed: nil)
|
52
50
|
payload = {
|
53
|
-
|
51
|
+
contact: Array(contact)
|
54
52
|
}
|
55
53
|
|
56
|
-
|
57
|
-
|
54
|
+
if terms_of_service_agreed
|
55
|
+
payload[:termsOfServiceAgreed] = terms_of_service_agreed
|
56
|
+
end
|
57
|
+
|
58
|
+
response = post(endpoint_for(:new_account), payload: payload, mode: :jws)
|
59
|
+
@kid = response.headers.fetch(:location)
|
60
|
+
|
61
|
+
if response.body.nil? || response.body.empty?
|
62
|
+
account
|
63
|
+
else
|
64
|
+
arguments = attributes_from_account_response(response)
|
65
|
+
Acme::Client::Resources::Account.new(self, url: @kid, **arguments)
|
66
|
+
end
|
58
67
|
end
|
59
68
|
|
60
|
-
def
|
61
|
-
payload = {
|
62
|
-
|
63
|
-
|
64
|
-
type: 'dns',
|
65
|
-
value: domain
|
66
|
-
}
|
67
|
-
}
|
69
|
+
def account_update(contact: nil, terms_of_service_agreed: nil)
|
70
|
+
payload = {}
|
71
|
+
payload[:contact] = Array(contact) if contact
|
72
|
+
payload[:termsOfServiceAgreed] = terms_of_service_agreed if terms_of_service_agreed
|
68
73
|
|
69
|
-
response =
|
70
|
-
|
74
|
+
response = post(kid, payload: payload)
|
75
|
+
arguments = attributes_from_account_response(response)
|
76
|
+
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
|
71
77
|
end
|
72
78
|
|
73
|
-
def
|
74
|
-
response =
|
75
|
-
|
79
|
+
def account_deactivate
|
80
|
+
response = post(kid, payload: { status: 'deactivated' })
|
81
|
+
arguments = attributes_from_account_response(response)
|
82
|
+
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
|
76
83
|
end
|
77
84
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
85
|
+
def account
|
86
|
+
@kid ||= begin
|
87
|
+
response = post(endpoint_for(:new_account), payload: { onlyReturnExisting: true }, mode: :jwk)
|
88
|
+
response.headers.fetch(:location)
|
89
|
+
end
|
83
90
|
|
84
|
-
response =
|
85
|
-
|
91
|
+
response = post(@kid)
|
92
|
+
arguments = attributes_from_account_response(response)
|
93
|
+
Acme::Client::Resources::Account.new(self, url: @kid, **arguments)
|
86
94
|
end
|
87
95
|
|
88
|
-
def
|
89
|
-
|
90
|
-
endpoint = @operation_endpoints.fetch('revoke-cert')
|
91
|
-
response = connection.post(endpoint, payload)
|
92
|
-
response.success?
|
96
|
+
def kid
|
97
|
+
@kid ||= account.kid
|
93
98
|
end
|
94
99
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
100
|
+
def new_order(identifiers:, not_before: nil, not_after: nil)
|
101
|
+
payload = {}
|
102
|
+
payload['identifiers'] = if identifiers.is_a?(Hash)
|
103
|
+
identifiers
|
104
|
+
else
|
105
|
+
Array(identifiers).map do |identifier|
|
106
|
+
{ type: 'dns', value: identifier }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
payload['notBefore'] = not_before if not_before
|
110
|
+
payload['notAfter'] = not_after if not_after
|
111
|
+
|
112
|
+
response = post(endpoint_for(:new_order), payload: payload)
|
113
|
+
arguments = attributes_from_order_response(response)
|
114
|
+
Acme::Client::Resources::Order.new(self, **arguments)
|
98
115
|
end
|
99
116
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
117
|
+
def order(url:)
|
118
|
+
response = get(url)
|
119
|
+
arguments = attributes_from_order_response(response)
|
120
|
+
Acme::Client::Resources::Order.new(self, **arguments.merge(url: url))
|
121
|
+
end
|
122
|
+
|
123
|
+
def finalize(url:, csr:)
|
124
|
+
unless csr.respond_to?(:to_der)
|
125
|
+
raise ArgumentError, 'csr must respond to `#to_der`'
|
126
|
+
end
|
127
|
+
|
128
|
+
base64_der_csr = Acme::Client::Util.urlsafe_base64(csr.to_der)
|
129
|
+
response = post(url, payload: { csr: base64_der_csr })
|
130
|
+
arguments = attributes_from_order_response(response)
|
131
|
+
Acme::Client::Resources::Order.new(self, **arguments)
|
132
|
+
end
|
133
|
+
|
134
|
+
def certificate(url:)
|
135
|
+
response = download(url, format: :pem)
|
136
|
+
response.body
|
137
|
+
end
|
138
|
+
|
139
|
+
def authorization(url:)
|
140
|
+
response = get(url)
|
141
|
+
arguments = attributes_from_authorization_response(response)
|
142
|
+
Acme::Client::Resources::Authorization.new(self, url: url, **arguments)
|
143
|
+
end
|
144
|
+
|
145
|
+
def deactivate_authorization(url:)
|
146
|
+
response = post(url, payload: { status: 'deactivated' })
|
147
|
+
arguments = attributes_from_authorization_response(response)
|
148
|
+
Acme::Client::Resources::Authorization.new(self, url: url, **arguments)
|
149
|
+
end
|
150
|
+
|
151
|
+
def challenge(url:)
|
152
|
+
response = get(url)
|
153
|
+
arguments = attributes_from_challenge_response(response)
|
154
|
+
Acme::Client::Resources::Challenges.new(self, **arguments)
|
155
|
+
end
|
156
|
+
|
157
|
+
def request_challenge_validation(url:, key_authorization:)
|
158
|
+
response = post(url, payload: { keyAuthorization: key_authorization })
|
159
|
+
arguments = attributes_from_challenge_response(response)
|
160
|
+
Acme::Client::Resources::Challenges.new(self, **arguments)
|
161
|
+
end
|
162
|
+
|
163
|
+
def revoke(certificate:, reason: nil)
|
164
|
+
der_certificate = if certificate.respond_to?(:to_der)
|
165
|
+
certificate.to_der
|
166
|
+
else
|
167
|
+
OpenSSL::X509::Certificate.new(certificate).to_der
|
104
168
|
end
|
169
|
+
|
170
|
+
base64_der_certificate = Acme::Client::Util.urlsafe_base64(der_certificate)
|
171
|
+
payload = { certificate: base64_der_certificate }
|
172
|
+
payload[:reason] = reason unless reason.nil?
|
173
|
+
|
174
|
+
response = post(endpoint_for(:revoke_certificate), payload: payload)
|
175
|
+
response.success?
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_nonce
|
179
|
+
response = Faraday.head(endpoint_for(:new_nonce), nil, 'User-Agent' => USER_AGENT)
|
180
|
+
nonces << response.headers['replay-nonce']
|
181
|
+
true
|
182
|
+
end
|
183
|
+
|
184
|
+
def meta
|
185
|
+
@directory.meta
|
186
|
+
end
|
187
|
+
|
188
|
+
def terms_of_service
|
189
|
+
@directory.terms_of_service
|
190
|
+
end
|
191
|
+
|
192
|
+
def website
|
193
|
+
@directory.website
|
194
|
+
end
|
195
|
+
|
196
|
+
def caa_identities
|
197
|
+
@directory.caa_identities
|
198
|
+
end
|
199
|
+
|
200
|
+
def external_account_required
|
201
|
+
@directory.external_account_required
|
105
202
|
end
|
106
203
|
|
107
204
|
private
|
108
205
|
|
206
|
+
def attributes_from_account_response(response)
|
207
|
+
extract_attributes(
|
208
|
+
response.body,
|
209
|
+
:status,
|
210
|
+
[:term_of_service, 'termsOfServiceAgreed'],
|
211
|
+
:contact
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
def attributes_from_order_response(response)
|
216
|
+
attributes = extract_attributes(
|
217
|
+
response.body,
|
218
|
+
:status,
|
219
|
+
:expires,
|
220
|
+
[:finalize_url, 'finalize'],
|
221
|
+
[:authorization_urls, 'authorizations'],
|
222
|
+
[:certificate_url, 'certificate'],
|
223
|
+
:identifiers
|
224
|
+
)
|
225
|
+
|
226
|
+
attributes[:url] = response.headers[:location] if response.headers[:location]
|
227
|
+
attributes
|
228
|
+
end
|
229
|
+
|
230
|
+
def attributes_from_authorization_response(response)
|
231
|
+
extract_attributes(response.body, :identifier, :status, :expires, :challenges, :wildcard)
|
232
|
+
end
|
233
|
+
|
234
|
+
def attributes_from_challenge_response(response)
|
235
|
+
extract_attributes(response.body, :status, :url, :token, :type, :error)
|
236
|
+
end
|
237
|
+
|
238
|
+
def extract_attributes(input, *attributes)
|
239
|
+
attributes
|
240
|
+
.map {|fields| Array(fields) }
|
241
|
+
.each_with_object({}) { |(key, field), hash|
|
242
|
+
field ||= key.to_s
|
243
|
+
hash[key] = input[field]
|
244
|
+
}
|
245
|
+
end
|
246
|
+
|
247
|
+
def post(url, payload: {}, mode: :kid)
|
248
|
+
connection = connection_for(url: url, mode: mode)
|
249
|
+
connection.post(url, payload)
|
250
|
+
end
|
251
|
+
|
252
|
+
def get(url, mode: :kid)
|
253
|
+
connection = connection_for(url: url, mode: mode)
|
254
|
+
connection.get(url)
|
255
|
+
end
|
256
|
+
|
257
|
+
def download(url, format:)
|
258
|
+
connection = connection_for(url: url, mode: :download)
|
259
|
+
connection.get do |request|
|
260
|
+
request.url(url)
|
261
|
+
request.headers['Accept'] = CONTENT_TYPES.fetch(format)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def connection_for(url:, mode:)
|
266
|
+
uri = URI(url)
|
267
|
+
endpoint = "#{uri.scheme}://#{uri.hostname}:#{uri.port}"
|
268
|
+
@connections ||= {}
|
269
|
+
@connections[mode] ||= {}
|
270
|
+
@connections[mode][endpoint] ||= new_connection(endpoint: endpoint, mode: mode)
|
271
|
+
end
|
272
|
+
|
273
|
+
def new_connection(endpoint:, mode:)
|
274
|
+
Faraday.new(endpoint, **@connection_options) do |configuration|
|
275
|
+
configuration.use Acme::Client::FaradayMiddleware, client: self, mode: mode
|
276
|
+
configuration.adapter Faraday.default_adapter
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
109
280
|
def fetch_chain(response, limit = 10)
|
110
281
|
links = response.headers['link']
|
111
282
|
if limit.zero? || links.nil? || links['up'].nil?
|
112
283
|
[]
|
113
284
|
else
|
114
|
-
issuer =
|
285
|
+
issuer = get(links['up'])
|
115
286
|
[OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit - 1)]
|
116
287
|
end
|
117
288
|
end
|
118
289
|
|
119
|
-
def
|
120
|
-
@
|
121
|
-
response = connection.get(@directory_uri)
|
122
|
-
body = response.body
|
123
|
-
{
|
124
|
-
'new-reg' => body.fetch('new-reg'),
|
125
|
-
'new-authz' => body.fetch('new-authz'),
|
126
|
-
'new-cert' => body.fetch('new-cert'),
|
127
|
-
'revoke-cert' => body.fetch('revoke-cert')
|
128
|
-
}
|
129
|
-
else
|
130
|
-
DIRECTORY_DEFAULT
|
131
|
-
end
|
290
|
+
def endpoint_for(key)
|
291
|
+
@directory.endpoint_for(key)
|
132
292
|
end
|
133
293
|
end
|