bullion 0.1.0 → 0.1.2
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/.gitignore +3 -0
- data/.images/logo.png +0 -0
- data/.rubocop.yml +32 -0
- data/.travis.yml +12 -4
- data/Dockerfile +55 -0
- data/Gemfile +4 -2
- data/Gemfile.lock +148 -0
- data/LICENSE.txt +1 -1
- data/README.md +48 -16
- data/Rakefile +88 -3
- data/bin/console +4 -3
- data/bullion.gemspec +38 -15
- data/config.ru +22 -0
- data/config/puma.rb +3 -0
- data/db/migrate/20210104000000_create_accounts.rb +14 -0
- data/db/migrate/20210104060422_create_certificates.rb +18 -0
- data/db/migrate/20210105060406_create_orders.rb +19 -0
- data/db/migrate/20210106052306_create_authorizations.rb +16 -0
- data/db/migrate/20210106055421_create_challenges.rb +18 -0
- data/db/migrate/20210106060335_create_nonces.rb +12 -0
- data/db/schema.rb +92 -0
- data/lib/bullion.rb +93 -2
- data/lib/bullion/acme/error.rb +72 -0
- data/lib/bullion/challenge_client.rb +59 -0
- data/lib/bullion/challenge_clients/dns.rb +49 -0
- data/lib/bullion/challenge_clients/http.rb +33 -0
- data/lib/bullion/helpers/acme.rb +202 -0
- data/lib/bullion/helpers/service.rb +17 -0
- data/lib/bullion/helpers/ssl.rb +214 -0
- data/lib/bullion/models.rb +8 -0
- data/lib/bullion/models/account.rb +33 -0
- data/lib/bullion/models/authorization.rb +31 -0
- data/lib/bullion/models/certificate.rb +37 -0
- data/lib/bullion/models/challenge.rb +37 -0
- data/lib/bullion/models/nonce.rb +22 -0
- data/lib/bullion/models/order.rb +39 -0
- data/lib/bullion/service.rb +26 -0
- data/lib/bullion/services/ca.rb +370 -0
- data/lib/bullion/services/ping.rb +36 -0
- data/lib/bullion/version.rb +7 -1
- data/scripts/docker-entrypoint.sh +9 -0
- metadata +302 -17
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Models
|
5
|
+
# ACMEv2 Account model
|
6
|
+
class Account < ActiveRecord::Base
|
7
|
+
serialize :contacts, Array
|
8
|
+
serialize :public_key, Hash
|
9
|
+
|
10
|
+
validates_uniqueness_of :public_key
|
11
|
+
|
12
|
+
has_many :orders
|
13
|
+
|
14
|
+
def kid
|
15
|
+
id
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_order(identifiers:, not_before: nil, not_after: nil)
|
19
|
+
order = Order.new
|
20
|
+
order.not_before = not_before if not_before
|
21
|
+
order.not_after = not_after if not_after
|
22
|
+
order.account = self
|
23
|
+
order.status = 'pending'
|
24
|
+
order.identifiers = identifiers
|
25
|
+
order.save
|
26
|
+
|
27
|
+
order.prep_authorizations!
|
28
|
+
|
29
|
+
order
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Models
|
5
|
+
# ACMEv2 Authorization model
|
6
|
+
class Authorization < ActiveRecord::Base
|
7
|
+
serialize :identifier, Hash
|
8
|
+
|
9
|
+
after_initialize :init_values, unless: :persisted?
|
10
|
+
|
11
|
+
belongs_to :order
|
12
|
+
has_many :challenges
|
13
|
+
|
14
|
+
validates :status, inclusion: { in: %w[invalid pending ready processing valid deactivated] }
|
15
|
+
|
16
|
+
def init_values
|
17
|
+
self.expires ||= Time.now + (60 * 60)
|
18
|
+
end
|
19
|
+
|
20
|
+
def prep_challenges!
|
21
|
+
%w[http-01 dns-01].each do |type|
|
22
|
+
chall = Challenge.new
|
23
|
+
chall.authorization = self
|
24
|
+
chall.acme_type = type
|
25
|
+
|
26
|
+
chall.save
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Models
|
5
|
+
# SSL Certificate model
|
6
|
+
class Certificate < ActiveRecord::Base
|
7
|
+
serialize :alternate_names
|
8
|
+
|
9
|
+
after_initialize :init_values, unless: :persisted?
|
10
|
+
|
11
|
+
validates_presence_of :subject
|
12
|
+
|
13
|
+
def init_values
|
14
|
+
self.serial ||= SecureRandom.hex(8).to_i(16)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fingerprint
|
18
|
+
Base64.encode64(OpenSSL::Digest::SHA1.digest(data))
|
19
|
+
end
|
20
|
+
|
21
|
+
def cn
|
22
|
+
subject.split('/').select { |name| name =~ /^CN=/ }.first.split('=').last
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.from_csr(csr)
|
26
|
+
subjt = csr.subject if csr.subject && !csr.subject.to_s.empty?
|
27
|
+
|
28
|
+
cert = new(
|
29
|
+
csr_fingerprint: Base64.encode64(OpenSSL::Digest::SHA1.digest(csr.to_pem)).chomp
|
30
|
+
)
|
31
|
+
|
32
|
+
cert.subject = subjt if subjt
|
33
|
+
cert
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Models
|
5
|
+
# ACMEv2 Challenge model
|
6
|
+
class Challenge < ActiveRecord::Base
|
7
|
+
after_initialize :init_values, unless: :persisted?
|
8
|
+
|
9
|
+
belongs_to :authorization
|
10
|
+
|
11
|
+
validates :acme_type, inclusion: { in: %w[http-01 dns-01] }
|
12
|
+
validates :status, inclusion: { in: %w[invalid pending processing valid] }
|
13
|
+
|
14
|
+
def init_values
|
15
|
+
self.expires ||= Time.now + (60 * 60)
|
16
|
+
self.token ||= SecureRandom.alphanumeric(48)
|
17
|
+
end
|
18
|
+
|
19
|
+
def thumbprint
|
20
|
+
cipher = OpenSSL::Digest.new('SHA256')
|
21
|
+
cipher.hexdigest authorization.order.account.public_key.to_json
|
22
|
+
end
|
23
|
+
|
24
|
+
def client
|
25
|
+
case acme_type
|
26
|
+
when 'dns-01'
|
27
|
+
ChallengeClients::DNS.new(self)
|
28
|
+
when 'http-01'
|
29
|
+
ChallengeClients::HTTP.new(self)
|
30
|
+
else
|
31
|
+
raise Bullion::Acme::Errors::UnsupportedChallengeType,
|
32
|
+
"Challenge type '#{acme_type}' is not supported by Bullion."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Models
|
5
|
+
# ACMEv2 Nonce model
|
6
|
+
class Nonce < ActiveRecord::Base
|
7
|
+
after_initialize :init_values, unless: :persisted?
|
8
|
+
|
9
|
+
validates_uniqueness_of :token
|
10
|
+
|
11
|
+
def init_values
|
12
|
+
self.token ||= SecureRandom.alphanumeric
|
13
|
+
end
|
14
|
+
|
15
|
+
# Delete old nonces
|
16
|
+
def self.clean_up!
|
17
|
+
# nonces older than this can safely be deleted
|
18
|
+
where('created_at < ?', Time.now - 86_400).delete_all
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Models
|
5
|
+
# ACMEv2 Order model
|
6
|
+
class Order < ActiveRecord::Base
|
7
|
+
serialize :identifiers, Array
|
8
|
+
|
9
|
+
after_initialize :init_values, unless: :persisted?
|
10
|
+
|
11
|
+
belongs_to :account
|
12
|
+
has_many :authorizations
|
13
|
+
|
14
|
+
validates :status, inclusion: { in: %w[invalid pending ready processing valid] }
|
15
|
+
|
16
|
+
def init_values
|
17
|
+
self.expires ||= Time.now + (60 * 60)
|
18
|
+
self.not_before ||= Time.now
|
19
|
+
self.not_after ||= Time.now + (60 * 60 * 24 * 90) # 90 days
|
20
|
+
end
|
21
|
+
|
22
|
+
def prep_authorizations!
|
23
|
+
identifiers.each do |identifier|
|
24
|
+
authorization = Authorization.new
|
25
|
+
authorization.order = self
|
26
|
+
authorization.identifier = identifier
|
27
|
+
|
28
|
+
authorization.save
|
29
|
+
|
30
|
+
authorization.prep_challenges!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def certificate
|
35
|
+
Certificate.find(certificate_id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
# Parent class for API services
|
5
|
+
class Service < Sinatra::Base
|
6
|
+
register Sinatra::ActiveRecordExtension
|
7
|
+
helpers Sinatra::CustomLogger
|
8
|
+
|
9
|
+
configure do
|
10
|
+
set :protection, except: :http_origin
|
11
|
+
set :logging, true
|
12
|
+
set :logger, Bullion::LOGGER
|
13
|
+
set :database, DB_CONNECTION_SETTINGS
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
# Sets up a useful variable (@json_body) for accessing a parsed request body
|
18
|
+
if request.content_type&.include?('json') && !request.body.to_s.empty?
|
19
|
+
request.body.rewind
|
20
|
+
@json_body = JSON.parse(request.body.read, symbolize_names: true)
|
21
|
+
end
|
22
|
+
rescue StandardError => e
|
23
|
+
halt(400, { error: "Request must be JSON: #{e.message}" }.to_json)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,370 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module Services
|
5
|
+
# ACME CA web service
|
6
|
+
class CA < Service
|
7
|
+
helpers Helpers::Acme
|
8
|
+
helpers Helpers::Service
|
9
|
+
helpers Helpers::Ssl
|
10
|
+
|
11
|
+
before do
|
12
|
+
Models::Nonce.clean_up! if rand(5) > 2 # randomly clean up
|
13
|
+
@new_nonce = Models::Nonce.create.token
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
if request.options?
|
18
|
+
@allowed_types ||= ['POST']
|
19
|
+
headers 'Access-Control-Allow-Methods' => @allowed_types
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
options '/directory' do
|
24
|
+
@allowed_types = ['GET']
|
25
|
+
halt 200
|
26
|
+
end
|
27
|
+
|
28
|
+
options '/nonces' do
|
29
|
+
@allowed_types = %w[HEAD GET]
|
30
|
+
halt 200
|
31
|
+
end
|
32
|
+
|
33
|
+
options '/accounts' do
|
34
|
+
halt 200
|
35
|
+
end
|
36
|
+
|
37
|
+
options '/accounts/:id' do
|
38
|
+
halt 200
|
39
|
+
end
|
40
|
+
|
41
|
+
options '/accounts/:id/orders' do
|
42
|
+
halt 200
|
43
|
+
end
|
44
|
+
|
45
|
+
options '/orders' do
|
46
|
+
halt 200
|
47
|
+
end
|
48
|
+
|
49
|
+
options '/orders/:id' do
|
50
|
+
halt 200
|
51
|
+
end
|
52
|
+
|
53
|
+
options '/orders/:id/finalize' do
|
54
|
+
halt 200
|
55
|
+
end
|
56
|
+
|
57
|
+
options '/authorizations/:id' do
|
58
|
+
halt 200
|
59
|
+
end
|
60
|
+
|
61
|
+
options '/challenges/:id' do
|
62
|
+
halt 200
|
63
|
+
end
|
64
|
+
|
65
|
+
options '/certificates/:id' do
|
66
|
+
halt 200
|
67
|
+
end
|
68
|
+
|
69
|
+
# Non-standard endpoint that returns the CA bundle for Bullion
|
70
|
+
# Trusting this bundle should be sufficient to trust all Bullion-issued certs
|
71
|
+
options '/cabundle' do
|
72
|
+
@allowed_types = ['GET']
|
73
|
+
halt 200
|
74
|
+
end
|
75
|
+
|
76
|
+
# The directory is used to find all required URLs for the ACME endpoints
|
77
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.1.1
|
78
|
+
get '/directory' do
|
79
|
+
content_type 'application/json'
|
80
|
+
|
81
|
+
{
|
82
|
+
newNonce: uri('/nonces'),
|
83
|
+
newAccount: uri('/accounts'),
|
84
|
+
newOrder: uri('/orders'),
|
85
|
+
revokeCert: uri('/revokecert'),
|
86
|
+
keyChange: uri('/keychanges'),
|
87
|
+
# non-standard entries:
|
88
|
+
caBundle: uri('/cabundle')
|
89
|
+
}.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
# Responds with Bullion's PEM-encoded public cert
|
93
|
+
get '/cabundle' do
|
94
|
+
expires 3600 * 48, :public, :must_revalidate
|
95
|
+
content_type 'application/x-pem-file'
|
96
|
+
|
97
|
+
attachment 'cabundle.pem'
|
98
|
+
Bullion.ca_cert.to_pem
|
99
|
+
end
|
100
|
+
|
101
|
+
# Retrieves a Nonce via a HEAD request
|
102
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.2
|
103
|
+
head '/nonces' do
|
104
|
+
add_acme_headers @new_nonce, additional: { 'Cache-Control' => 'no-store' }
|
105
|
+
|
106
|
+
halt 200
|
107
|
+
end
|
108
|
+
|
109
|
+
# Retrieves a Nonce via a GET request
|
110
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.2
|
111
|
+
get '/nonces' do
|
112
|
+
add_acme_headers @new_nonce, additional: { 'Cache-Control' => 'no-store' }
|
113
|
+
|
114
|
+
halt 204
|
115
|
+
end
|
116
|
+
|
117
|
+
# Creates an account or verifies that an account exists
|
118
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.3
|
119
|
+
post '/accounts' do
|
120
|
+
header_data = JSON.parse(Base64.decode64(@json_body[:protected]))
|
121
|
+
begin
|
122
|
+
parse_acme_jwt(header_data['jwk'], validate_nonce: false)
|
123
|
+
|
124
|
+
validate_account_data(@payload_data)
|
125
|
+
rescue Bullion::Acme::Error => e
|
126
|
+
content_type 'application/problem+json'
|
127
|
+
halt 400, { type: e.acme_error, detail: e.message }.to_json
|
128
|
+
end
|
129
|
+
|
130
|
+
user = Models::Account.where(
|
131
|
+
public_key: header_data['jwk']
|
132
|
+
).first
|
133
|
+
|
134
|
+
if @payload_data['onlyReturnExisting']
|
135
|
+
content_type 'application/problem+json'
|
136
|
+
halt 400, { type: 'urn:ietf:params:acme:error:accountDoesNotExist' }.to_json unless user
|
137
|
+
end
|
138
|
+
|
139
|
+
user ||= Models::Account.new(public_key: header_data['jwk'])
|
140
|
+
user.tos_agreed = true
|
141
|
+
user.contacts = @payload_data['contact']
|
142
|
+
user.save
|
143
|
+
|
144
|
+
content_type 'application/json'
|
145
|
+
add_acme_headers @new_nonce, additional: { 'Location' => uri("/accounts/#{user.id}") }
|
146
|
+
|
147
|
+
halt 201, {
|
148
|
+
'status': user.tos_agreed? ? 'valid' : 'pending',
|
149
|
+
'contact': user.contacts,
|
150
|
+
'orders': uri("/accounts/#{user.id}/orders")
|
151
|
+
}.to_json
|
152
|
+
end
|
153
|
+
|
154
|
+
# Endpoint for updating accounts
|
155
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.3.2
|
156
|
+
post '/accounts/:id' do
|
157
|
+
parse_acme_jwt
|
158
|
+
|
159
|
+
unless params[:id] == @user.id
|
160
|
+
content_type 'application/json'
|
161
|
+
add_acme_headers @new_nonce
|
162
|
+
|
163
|
+
halt 403, { error: 'Accounts can only view or update themselves' }.to_json
|
164
|
+
end
|
165
|
+
|
166
|
+
content_type 'application/json'
|
167
|
+
|
168
|
+
{
|
169
|
+
'status': 'valid',
|
170
|
+
'orders': uri("/accounts/#{@user.id}/orders"),
|
171
|
+
'contact': @user.contacts
|
172
|
+
}.to_json
|
173
|
+
end
|
174
|
+
|
175
|
+
post '/accounts/:id/orders' do
|
176
|
+
parse_acme_jwt
|
177
|
+
|
178
|
+
unless params[:id] == @user.id
|
179
|
+
content_type 'application/json'
|
180
|
+
add_acme_headers @new_nonce
|
181
|
+
|
182
|
+
halt 403, { error: 'Accounts can only view or update themselves' }.to_json
|
183
|
+
end
|
184
|
+
|
185
|
+
content_type 'application/json'
|
186
|
+
add_acme_headers @new_nonce
|
187
|
+
|
188
|
+
{
|
189
|
+
orders: @user.orders.map { |order| uri("/orders/#{order.id}") }
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Endpoint for creating new orders
|
194
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.4
|
195
|
+
post '/orders' do
|
196
|
+
parse_acme_jwt
|
197
|
+
|
198
|
+
# Only identifiers of type "dns" are supported
|
199
|
+
identifiers = @payload_data['identifiers'].select { |i| i['type'] == 'dns' }
|
200
|
+
|
201
|
+
validate_order(@payload_data)
|
202
|
+
|
203
|
+
order = @user.start_order(
|
204
|
+
identifiers: identifiers,
|
205
|
+
not_before: @payload_data['notBefore'],
|
206
|
+
not_after: @payload_data['notAfter']
|
207
|
+
)
|
208
|
+
|
209
|
+
content_type 'application/json'
|
210
|
+
add_acme_headers @new_nonce, additional: { 'Location' => uri("/orders/#{order.id}") }
|
211
|
+
|
212
|
+
halt 201, {
|
213
|
+
status: order.status,
|
214
|
+
expires: order.expires,
|
215
|
+
notBefore: order.not_before,
|
216
|
+
notAfter: order.not_after,
|
217
|
+
identifiers: order.identifiers,
|
218
|
+
authorizations: order.authorizations.map { |a| uri("/authorizations/#{a.id}") },
|
219
|
+
finalize: uri("/orders/#{order.id}/finalize")
|
220
|
+
}.to_json
|
221
|
+
rescue Bullion::Acme::Error => e
|
222
|
+
content_type 'application/problem+json'
|
223
|
+
halt 400, { type: e.acme_error, detail: e.message }.to_json
|
224
|
+
end
|
225
|
+
|
226
|
+
# Retrieve existing Orders
|
227
|
+
post '/orders/:id' do
|
228
|
+
parse_acme_jwt
|
229
|
+
|
230
|
+
content_type 'application/json'
|
231
|
+
add_acme_headers @new_nonce
|
232
|
+
|
233
|
+
order = Models::Order.find(params[:id])
|
234
|
+
|
235
|
+
data = {
|
236
|
+
status: order.status,
|
237
|
+
expires: order.expires,
|
238
|
+
notBefore: order.not_before,
|
239
|
+
notAfter: order.not_after,
|
240
|
+
identifiers: order.identifiers,
|
241
|
+
authorizations: order.authorizations.map { |a| uri("/authorizations/#{a.id}") },
|
242
|
+
finalize: uri("/orders/#{order.id}/finalize")
|
243
|
+
}
|
244
|
+
|
245
|
+
data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.status == 'valid'
|
246
|
+
|
247
|
+
data.to_json
|
248
|
+
end
|
249
|
+
|
250
|
+
# Submit an order for finalization/signing
|
251
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.4
|
252
|
+
post '/orders/:id/finalize' do
|
253
|
+
parse_acme_jwt
|
254
|
+
|
255
|
+
content_type 'application/json'
|
256
|
+
add_acme_headers @new_nonce, additional: { 'Location' => uri("/orders/#{order.id}") }
|
257
|
+
|
258
|
+
raw_csr_data = Base64.urlsafe_decode64(@payload_data['csr'])
|
259
|
+
encoded_csr = Base64.encode64(raw_csr_data)
|
260
|
+
|
261
|
+
csr_data = openssl_compat_csr(encoded_csr)
|
262
|
+
|
263
|
+
csr = OpenSSL::X509::Request.new(csr_data)
|
264
|
+
|
265
|
+
order = Models::Order.find(params[:id])
|
266
|
+
|
267
|
+
unless validate_csr(csr) && validate_acme_csr(order, csr)
|
268
|
+
content_type 'application/problem+json'
|
269
|
+
halt 400, {
|
270
|
+
type: Bullion::Acme::Errors::BadCSR.new.acme_error,
|
271
|
+
detail: 'CSR failed validation'
|
272
|
+
}.to_json
|
273
|
+
end
|
274
|
+
|
275
|
+
cert_id = sign_csr(csr, @user.contacts.first, acme: true).last
|
276
|
+
|
277
|
+
order.certificate_id = cert_id
|
278
|
+
order.status = 'valid'
|
279
|
+
order.save
|
280
|
+
|
281
|
+
data = {
|
282
|
+
status: order.status,
|
283
|
+
expires: order.expires,
|
284
|
+
notBefore: order.not_before,
|
285
|
+
notAfter: order.not_after,
|
286
|
+
identifiers: order.identifiers,
|
287
|
+
authorizations: order.authorizations.map { |a| uri("/authorizations/#{a.id}") },
|
288
|
+
finalize: uri("/orders/#{order.id}/finalize")
|
289
|
+
}
|
290
|
+
|
291
|
+
data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.status == 'valid'
|
292
|
+
|
293
|
+
data.to_json
|
294
|
+
end
|
295
|
+
|
296
|
+
# Shows that the client controls the account private key
|
297
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.5
|
298
|
+
post '/authorizations/:id' do
|
299
|
+
parse_acme_jwt
|
300
|
+
|
301
|
+
content_type 'application/json'
|
302
|
+
add_acme_headers @new_nonce
|
303
|
+
|
304
|
+
authorization = Models::Authorization.find(params[:id])
|
305
|
+
halt 404 unless authorization
|
306
|
+
|
307
|
+
data = {
|
308
|
+
status: authorization.status,
|
309
|
+
expires: authorization.expires,
|
310
|
+
identifier: authorization.identifier,
|
311
|
+
challenges: authorization.challenges.map do |c|
|
312
|
+
chash = {}
|
313
|
+
chash[:type] = c.acme_type
|
314
|
+
chash[:url] = uri("/challenges/#{c.id}")
|
315
|
+
chash[:token] = c.token
|
316
|
+
chash[:status] = c.status
|
317
|
+
chash[:validated] = c.validated if c.status == 'valid'
|
318
|
+
|
319
|
+
chash
|
320
|
+
end
|
321
|
+
}
|
322
|
+
|
323
|
+
data.to_json
|
324
|
+
end
|
325
|
+
|
326
|
+
# Starts server verification of a challenge (either HTTP call or DNS lookup)
|
327
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.5.1
|
328
|
+
post '/challenges/:id' do
|
329
|
+
parse_acme_jwt
|
330
|
+
|
331
|
+
content_type 'application/json'
|
332
|
+
add_acme_headers @new_nonce
|
333
|
+
|
334
|
+
challenge = Models::Challenge.find(params[:id])
|
335
|
+
|
336
|
+
# Oddly enough, cert-manager uses a GET request for retrieving Challenge info
|
337
|
+
challenge.client.attempt unless @parsed_body[:payload] == ''
|
338
|
+
|
339
|
+
data = {
|
340
|
+
type: challenge.acme_type,
|
341
|
+
status: challenge.status,
|
342
|
+
expires: challenge.expires,
|
343
|
+
token: challenge.token,
|
344
|
+
url: uri("/challenges/#{challenge.id}")
|
345
|
+
}
|
346
|
+
|
347
|
+
data[:validated] = challenge.validated if challenge.status == 'valid'
|
348
|
+
|
349
|
+
data.to_json
|
350
|
+
end
|
351
|
+
|
352
|
+
# Retrieves a signed certificate
|
353
|
+
# @see https://tools.ietf.org/html/rfc8555#section-7.4.2
|
354
|
+
post '/certificates/:id' do
|
355
|
+
parse_acme_jwt
|
356
|
+
|
357
|
+
order = Models::Order.where(certificate_id: params[:id]).first
|
358
|
+
if order && order.status == 'valid'
|
359
|
+
content_type 'application/pem-certificate-chain'
|
360
|
+
|
361
|
+
cert = Models::Certificate.find(params[:id])
|
362
|
+
|
363
|
+
cert.data + Bullion.ca_cert.to_pem
|
364
|
+
else
|
365
|
+
halt(422, { 'error': 'Order not valid' }.to_json)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|