bullion 0.1.2 → 0.3.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/.roxanne.yml +14 -0
- data/.rubocop.yml +25 -6
- data/.ruby-version +1 -0
- data/.travis.yml +2 -1
- data/Dockerfile +2 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +99 -89
- data/README.md +2 -2
- data/Rakefile +40 -37
- data/bin/console +3 -3
- data/bullion.gemspec +38 -36
- data/config/puma.rb +1 -1
- data/config.ru +5 -5
- data/db/migrate/20210104060422_create_certificates.rb +1 -1
- data/db/migrate/20210105060406_create_orders.rb +1 -1
- data/db/migrate/20210106052306_create_authorizations.rb +1 -1
- data/db/schema.rb +20 -21
- data/lib/bullion/acme/error.rb +9 -9
- data/lib/bullion/challenge_client.rb +4 -4
- data/lib/bullion/challenge_clients/dns.rb +36 -21
- data/lib/bullion/challenge_clients/http.rb +12 -8
- data/lib/bullion/helpers/acme.rb +30 -40
- data/lib/bullion/helpers/service.rb +2 -2
- data/lib/bullion/helpers/ssl.rb +50 -42
- data/lib/bullion/models/account.rb +1 -1
- data/lib/bullion/models/certificate.rb +2 -2
- data/lib/bullion/models/challenge.rb +5 -5
- data/lib/bullion/models/nonce.rb +1 -1
- data/lib/bullion/models.rb +6 -6
- data/lib/bullion/rspec/challenge_clients/dns.rb +22 -0
- data/lib/bullion/rspec/challenge_clients/http.rb +16 -0
- data/lib/bullion/service.rb +3 -2
- data/lib/bullion/services/ca.rb +107 -91
- data/lib/bullion/services/ping.rb +6 -6
- data/lib/bullion/version.rb +3 -3
- data/lib/bullion.rb +58 -45
- data/scripts/build.sh +3 -0
- data/scripts/release.sh +9 -0
- data/scripts/test.sh +6 -0
- metadata +65 -30
data/config.ru
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
# \ -s puma
|
4
4
|
|
5
|
-
require
|
5
|
+
require "bullion"
|
6
6
|
Bullion.validate_config!
|
7
7
|
|
8
|
-
require
|
9
|
-
require
|
8
|
+
require "prometheus/middleware/collector"
|
9
|
+
require "prometheus/middleware/exporter"
|
10
10
|
|
11
11
|
use Rack::ShowExceptions
|
12
12
|
use Rack::Deflater
|
@@ -15,8 +15,8 @@ use Prometheus::Middleware::Exporter
|
|
15
15
|
|
16
16
|
# Prometheus metrics are on /metrics
|
17
17
|
mappings = {
|
18
|
-
|
19
|
-
|
18
|
+
"/ping" => Bullion::Services::Ping.new,
|
19
|
+
"/acme" => Bullion::Services::CA.new
|
20
20
|
}
|
21
21
|
|
22
22
|
run Rack::URLMap.new(mappings)
|
@@ -10,7 +10,7 @@ class CreateCertificates < ActiveRecord::Migration[6.1]
|
|
10
10
|
t.text :alternate_names
|
11
11
|
t.string :requester
|
12
12
|
t.boolean :validated, null: false, default: false, index: true
|
13
|
-
t.
|
13
|
+
t.bigint :serial, null: false, index: { unique: true }
|
14
14
|
|
15
15
|
t.timestamps
|
16
16
|
end
|
@@ -4,7 +4,7 @@
|
|
4
4
|
class CreateOrders < ActiveRecord::Migration[6.1]
|
5
5
|
def change
|
6
6
|
create_table :orders do |t|
|
7
|
-
t.string :status, null: false, default:
|
7
|
+
t.string :status, null: false, default: "pending", index: true
|
8
8
|
t.timestamp :expires, null: false, index: true
|
9
9
|
t.text :identifiers, null: false
|
10
10
|
t.timestamp :not_before, null: false
|
@@ -4,7 +4,7 @@
|
|
4
4
|
class CreateAuthorizations < ActiveRecord::Migration[6.1]
|
5
5
|
def change
|
6
6
|
create_table :authorizations do |t|
|
7
|
-
t.string :status, null: false, default:
|
7
|
+
t.string :status, null: false, default: "pending", index: true
|
8
8
|
t.timestamp :expires, null: false, index: true
|
9
9
|
t.text :identifier, null: false
|
10
10
|
|
data/db/schema.rb
CHANGED
@@ -10,25 +10,24 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema.define(version: 2021_01_06_060335) do
|
14
|
-
|
13
|
+
ActiveRecord::Schema[7.0].define(version: 2021_01_06_060335) do
|
15
14
|
create_table "accounts", force: :cascade do |t|
|
16
15
|
t.boolean "tos_agreed", default: true, null: false
|
17
16
|
t.text "public_key", null: false
|
18
17
|
t.text "contacts", null: false
|
19
|
-
t.datetime "created_at",
|
20
|
-
t.datetime "updated_at",
|
18
|
+
t.datetime "created_at", null: false
|
19
|
+
t.datetime "updated_at", null: false
|
21
20
|
t.index ["public_key"], name: "index_accounts_on_public_key", unique: true
|
22
21
|
t.index ["tos_agreed"], name: "index_accounts_on_tos_agreed"
|
23
22
|
end
|
24
23
|
|
25
24
|
create_table "authorizations", force: :cascade do |t|
|
26
25
|
t.string "status", default: "pending", null: false
|
27
|
-
t.datetime "expires", null: false
|
26
|
+
t.datetime "expires", precision: nil, null: false
|
28
27
|
t.text "identifier", null: false
|
29
28
|
t.integer "order_id"
|
30
|
-
t.datetime "created_at",
|
31
|
-
t.datetime "updated_at",
|
29
|
+
t.datetime "created_at", null: false
|
30
|
+
t.datetime "updated_at", null: false
|
32
31
|
t.index ["expires"], name: "index_authorizations_on_expires"
|
33
32
|
t.index ["order_id"], name: "index_authorizations_on_order_id"
|
34
33
|
t.index ["status"], name: "index_authorizations_on_status"
|
@@ -41,9 +40,9 @@ ActiveRecord::Schema.define(version: 2021_01_06_060335) do
|
|
41
40
|
t.text "alternate_names"
|
42
41
|
t.string "requester"
|
43
42
|
t.boolean "validated", default: false, null: false
|
44
|
-
t.
|
45
|
-
t.datetime "created_at",
|
46
|
-
t.datetime "updated_at",
|
43
|
+
t.bigint "serial", null: false
|
44
|
+
t.datetime "created_at", null: false
|
45
|
+
t.datetime "updated_at", null: false
|
47
46
|
t.index ["csr_fingerprint"], name: "index_certificates_on_csr_fingerprint"
|
48
47
|
t.index ["serial"], name: "index_certificates_on_serial", unique: true
|
49
48
|
t.index ["subject"], name: "index_certificates_on_subject"
|
@@ -53,12 +52,12 @@ ActiveRecord::Schema.define(version: 2021_01_06_060335) do
|
|
53
52
|
create_table "challenges", force: :cascade do |t|
|
54
53
|
t.string "acme_type", null: false
|
55
54
|
t.string "status", default: "pending", null: false
|
56
|
-
t.datetime "expires", null: false
|
55
|
+
t.datetime "expires", precision: nil, null: false
|
57
56
|
t.string "token", null: false
|
58
|
-
t.datetime "validated"
|
57
|
+
t.datetime "validated", precision: nil
|
59
58
|
t.integer "authorization_id"
|
60
|
-
t.datetime "created_at",
|
61
|
-
t.datetime "updated_at",
|
59
|
+
t.datetime "created_at", null: false
|
60
|
+
t.datetime "updated_at", null: false
|
62
61
|
t.index ["acme_type"], name: "index_challenges_on_acme_type"
|
63
62
|
t.index ["authorization_id"], name: "index_challenges_on_authorization_id"
|
64
63
|
t.index ["expires"], name: "index_challenges_on_expires"
|
@@ -67,21 +66,21 @@ ActiveRecord::Schema.define(version: 2021_01_06_060335) do
|
|
67
66
|
|
68
67
|
create_table "nonces", force: :cascade do |t|
|
69
68
|
t.string "token", null: false
|
70
|
-
t.datetime "created_at",
|
71
|
-
t.datetime "updated_at",
|
69
|
+
t.datetime "created_at", null: false
|
70
|
+
t.datetime "updated_at", null: false
|
72
71
|
t.index ["token"], name: "index_nonces_on_token", unique: true
|
73
72
|
end
|
74
73
|
|
75
74
|
create_table "orders", force: :cascade do |t|
|
76
75
|
t.string "status", default: "pending", null: false
|
77
|
-
t.datetime "expires", null: false
|
76
|
+
t.datetime "expires", precision: nil, null: false
|
78
77
|
t.text "identifiers", null: false
|
79
|
-
t.datetime "not_before", null: false
|
80
|
-
t.datetime "not_after", null: false
|
78
|
+
t.datetime "not_before", precision: nil, null: false
|
79
|
+
t.datetime "not_after", precision: nil, null: false
|
81
80
|
t.integer "certificate_id"
|
82
81
|
t.integer "account_id"
|
83
|
-
t.datetime "created_at",
|
84
|
-
t.datetime "updated_at",
|
82
|
+
t.datetime "created_at", null: false
|
83
|
+
t.datetime "updated_at", null: false
|
85
84
|
t.index ["account_id"], name: "index_orders_on_account_id"
|
86
85
|
t.index ["certificate_id"], name: "index_orders_on_certificate_id"
|
87
86
|
t.index ["expires"], name: "index_orders_on_expires"
|
data/lib/bullion/acme/error.rb
CHANGED
@@ -6,11 +6,11 @@ module Bullion
|
|
6
6
|
class Error < Bullion::Error
|
7
7
|
# @see https://tools.ietf.org/html/rfc8555#section-6.7
|
8
8
|
def acme_type
|
9
|
-
|
9
|
+
"genericError"
|
10
10
|
end
|
11
11
|
|
12
12
|
def acme_preface
|
13
|
-
|
13
|
+
"urn:ietf:params:acme:error:"
|
14
14
|
end
|
15
15
|
|
16
16
|
def acme_error
|
@@ -22,49 +22,49 @@ module Bullion
|
|
22
22
|
# ACME exception for bad CSRs
|
23
23
|
class BadCsr < Bullion::Acme::Error
|
24
24
|
def acme_type
|
25
|
-
|
25
|
+
"badCSR"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
# ACME exception for bad Nonces
|
30
30
|
class BadNonce < Bullion::Acme::Error
|
31
31
|
def acme_type
|
32
|
-
|
32
|
+
"badNonce"
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
# ACME exception for invalid contacts in accounts
|
37
37
|
class InvalidContact < Bullion::Acme::Error
|
38
38
|
def acme_type
|
39
|
-
|
39
|
+
"invalidContact"
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
# ACME exception for invalid orders
|
44
44
|
class InvalidOrder < Bullion::Acme::Error
|
45
45
|
def acme_type
|
46
|
-
|
46
|
+
"invalidOrder"
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
50
|
# ACME exception for malformed requests
|
51
51
|
class Malformed < Bullion::Acme::Error
|
52
52
|
def acme_type
|
53
|
-
|
53
|
+
"malformed"
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
57
|
# ACME exception for unsupported contacts in accounts
|
58
58
|
class UnsupportedContact < Bullion::Acme::Error
|
59
59
|
def acme_type
|
60
|
-
|
60
|
+
"unsupportedContact"
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
64
|
# Non-standard exception for unsupported challenge types
|
65
65
|
class UnsupportedChallengeType < Bullion::Acme::Error
|
66
66
|
def acme_error
|
67
|
-
|
67
|
+
"urn:ietf:params:bullion:error:unsupportedChallengeType"
|
68
68
|
end
|
69
69
|
end
|
70
70
|
end
|
@@ -5,7 +5,7 @@ module Bullion
|
|
5
5
|
class ChallengeClient
|
6
6
|
ChallengeClientMetric = Prometheus::Client::Histogram.new(
|
7
7
|
:challenge_execution_seconds,
|
8
|
-
docstring:
|
8
|
+
docstring: "Challenge execution histogram in seconds",
|
9
9
|
labels: %i[acme_type status]
|
10
10
|
)
|
11
11
|
MetricsRegistry.register(ChallengeClientMetric)
|
@@ -28,7 +28,7 @@ module Bullion
|
|
28
28
|
success = perform
|
29
29
|
if success
|
30
30
|
LOGGER.info "Validated #{type} #{identifier}"
|
31
|
-
challenge.status =
|
31
|
+
challenge.status = "valid"
|
32
32
|
challenge.validated = Time.now
|
33
33
|
else
|
34
34
|
sleep rand(2..4)
|
@@ -38,7 +38,7 @@ module Bullion
|
|
38
38
|
|
39
39
|
unless success
|
40
40
|
LOGGER.info "Failed to validate #{type} #{identifier}"
|
41
|
-
challenge.status =
|
41
|
+
challenge.status = "invalid"
|
42
42
|
end
|
43
43
|
|
44
44
|
challenge.save
|
@@ -53,7 +53,7 @@ module Bullion
|
|
53
53
|
# rubocop:enable Metrics/MethodLength
|
54
54
|
|
55
55
|
def identifier
|
56
|
-
challenge.authorization.identifier[
|
56
|
+
challenge.authorization.identifier["value"]
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
@@ -6,42 +6,57 @@ module Bullion
|
|
6
6
|
# @see https://tools.ietf.org/html/rfc8555#section-8.4
|
7
7
|
class DNS < ChallengeClient
|
8
8
|
def type
|
9
|
-
|
9
|
+
"DNS01"
|
10
10
|
end
|
11
11
|
|
12
12
|
def perform
|
13
13
|
value = dns_value
|
14
|
-
|
15
|
-
digester = OpenSSL::Digest.new('SHA256')
|
16
|
-
digest = digester.digest "#{challenge.token}.#{challenge.thumbprint}"
|
17
|
-
# clean up the digest output so it can match the provided challenge value
|
18
|
-
expected_value = Base64.urlsafe_encode64(digest).sub(/[\s=]*\z/, '')
|
14
|
+
expected_value = digest_value("#{challenge.token}.#{challenge.thumbprint}")
|
19
15
|
|
20
16
|
value == expected_value
|
21
17
|
end
|
22
18
|
|
19
|
+
def digest_value(string)
|
20
|
+
digester = OpenSSL::Digest.new("SHA256")
|
21
|
+
digest = digester.digest(string)
|
22
|
+
# clean up the digest output so it can match the provided challenge value
|
23
|
+
Base64.urlsafe_encode64(digest).sub(/[\s=]*\z/, "")
|
24
|
+
end
|
25
|
+
|
23
26
|
def dns_value
|
24
27
|
name = "_acme-challenge.#{identifier}"
|
25
28
|
|
26
29
|
# Randomly select a nameserver to pull the TXT record
|
27
30
|
nameserver = NAMESERVERS.sample
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
LOGGER.debug "Looking up #{name}"
|
33
|
+
records = records_for(name, nameserver)
|
34
|
+
raise "Failed to find records for #{name}" unless records
|
35
|
+
|
36
|
+
record = records.map(&:strings).flatten.first
|
37
|
+
LOGGER.debug "Resolved #{name} to value #{record}"
|
38
|
+
record
|
39
|
+
rescue Resolv::ResolvError
|
40
|
+
msg = ["Resolution error for #{name}"]
|
41
|
+
msg << "via #{nameserver}" if nameserver
|
42
|
+
LOGGER.info msg.join(" ")
|
43
|
+
false
|
44
|
+
rescue StandardError => e
|
45
|
+
msg = ["Error '#{e.message}' for #{name}"]
|
46
|
+
msg << "with #{nameserver}" if nameserver
|
47
|
+
LOGGER.warn msg.join(" ")
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def records_for(name, nameserver = nil)
|
52
|
+
if nameserver
|
53
|
+
Resolv::DNS.open(nameserver:) do |dns|
|
54
|
+
dns.getresources(name, Resolv::DNS::Resource::IN::TXT)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
Resolv::DNS.open do |dns|
|
58
|
+
dns.getresources(name, Resolv::DNS::Resource::IN::TXT)
|
35
59
|
end
|
36
|
-
record = records.map(&:strings).flatten.first
|
37
|
-
LOGGER.debug "Resolved #{name} to value #{record}"
|
38
|
-
record
|
39
|
-
rescue Resolv::ResolvError
|
40
|
-
LOGGER.info "Resolution error for #{name} via #{nameserver}"
|
41
|
-
false
|
42
|
-
rescue StandardError => e
|
43
|
-
LOGGER.warn "Error '#{e.message}' for #{name} with #{nameserver}"
|
44
|
-
false
|
45
60
|
end
|
46
61
|
end
|
47
62
|
end
|
@@ -6,28 +6,32 @@ module Bullion
|
|
6
6
|
# @see https://tools.ietf.org/html/rfc8555#section-8.3
|
7
7
|
class HTTP < ChallengeClient
|
8
8
|
def type
|
9
|
-
|
9
|
+
"HTTP01"
|
10
10
|
end
|
11
11
|
|
12
12
|
def perform
|
13
13
|
response = begin
|
14
|
-
|
15
|
-
challenge_url,
|
16
|
-
verify: false,
|
17
|
-
headers: { 'User-Agent' => "Bullion/#{Bullion::VERSION}" }
|
18
|
-
).body
|
14
|
+
retrieve_body(challenge_url)
|
19
15
|
rescue SocketError
|
20
16
|
LOGGER.debug "Failed to connect to #{challenge_url}"
|
21
|
-
|
17
|
+
""
|
22
18
|
end
|
23
19
|
|
24
|
-
token, thumbprint = response.split(
|
20
|
+
token, thumbprint = response.split(".")
|
25
21
|
token == challenge.token && thumbprint == challenge.thumbprint
|
26
22
|
end
|
27
23
|
|
28
24
|
def challenge_url
|
29
25
|
"http://#{identifier}/.well-known/acme-challenge/#{challenge.token}"
|
30
26
|
end
|
27
|
+
|
28
|
+
def retrieve_body(url)
|
29
|
+
HTTParty.get(
|
30
|
+
url,
|
31
|
+
verify: false,
|
32
|
+
headers: { "User-Agent" => "Bullion/#{Bullion::VERSION}" }
|
33
|
+
).body
|
34
|
+
end
|
31
35
|
end
|
32
36
|
end
|
33
37
|
end
|
data/lib/bullion/helpers/acme.rb
CHANGED
@@ -17,7 +17,7 @@ module Bullion
|
|
17
17
|
|
18
18
|
# check nonce
|
19
19
|
if validate_nonce
|
20
|
-
nonce = Models::Nonce.where(token: @header_data[
|
20
|
+
nonce = Models::Nonce.where(token: @header_data["nonce"]).first
|
21
21
|
raise Bullion::Acme::Errors::BadNonce unless nonce
|
22
22
|
|
23
23
|
nonce.destroy
|
@@ -27,7 +27,7 @@ module Bullion
|
|
27
27
|
@json_body[:protected],
|
28
28
|
@json_body[:payload],
|
29
29
|
@json_body[:signature]
|
30
|
-
].join(
|
30
|
+
].join(".")
|
31
31
|
|
32
32
|
# Either use the provided key or find the current user's public key
|
33
33
|
public_key = key || user_public_key
|
@@ -36,14 +36,14 @@ module Bullion
|
|
36
36
|
compat_public_key = openssl_compat(public_key)
|
37
37
|
|
38
38
|
# Validate the payload was signed with the private key for the public key
|
39
|
-
if @payload_data && @payload_data !=
|
40
|
-
JWT.decode(jwt_data, compat_public_key, @header_data[
|
39
|
+
if @payload_data && @payload_data != ""
|
40
|
+
JWT.decode(jwt_data, compat_public_key, true, { algorithm: @header_data["alg"] })
|
41
41
|
else
|
42
|
-
digest = digest_from_alg(@header_data[
|
42
|
+
digest = digest_from_alg(@header_data["alg"])
|
43
43
|
|
44
|
-
sig = if @header_data[
|
44
|
+
sig = if @header_data["alg"].downcase.start_with?("es")
|
45
45
|
ecdsa_sig_to_der(signature)
|
46
|
-
elsif @header_data[
|
46
|
+
elsif @header_data["alg"].downcase.start_with?("rs")
|
47
47
|
Base64.urlsafe_decode64(signature)
|
48
48
|
end
|
49
49
|
|
@@ -65,7 +65,7 @@ module Bullion
|
|
65
65
|
end
|
66
66
|
|
67
67
|
def extract_payload_data
|
68
|
-
if @json_body[:payload] && @json_body[:payload] !=
|
68
|
+
if @json_body[:payload] && @json_body[:payload] != ""
|
69
69
|
JSON.parse(Base64.decode64(@json_body[:payload]))
|
70
70
|
else
|
71
71
|
@json_body[:payload]
|
@@ -73,52 +73,39 @@ module Bullion
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def user_public_key
|
76
|
-
@user = if @header_data[
|
77
|
-
user_id = @header_data[
|
76
|
+
@user = if @header_data["kid"]
|
77
|
+
user_id = @header_data["kid"].split("/").last
|
78
78
|
return unless user_id
|
79
79
|
|
80
80
|
Models::Account.find(user_id)
|
81
81
|
else
|
82
|
-
Models::Account.where(public_key: @header_data[
|
82
|
+
Models::Account.where(public_key: @header_data["jwk"]).last
|
83
83
|
end
|
84
84
|
|
85
85
|
@user.public_key
|
86
86
|
end
|
87
87
|
|
88
|
-
def extract_csr_attrs(csr)
|
89
|
-
csr.attributes.select { |a| a.oid == 'extReq' }.map { |a| a.value.map(&:value) }
|
90
|
-
end
|
91
|
-
|
92
|
-
def extract_csr_sans(csr_attrs)
|
93
|
-
csr_attrs.flatten.select { |a| a.value.first.value == 'subjectAltName' }
|
94
|
-
end
|
95
|
-
|
96
|
-
def extract_csr_domains(csr_sans)
|
97
|
-
csr_decoded_sans = OpenSSL::ASN1.decode(csr_sans.first.value[1].value)
|
98
|
-
csr_decoded_sans.select { |v| v.tag == 2 }.map(&:value)
|
99
|
-
end
|
100
|
-
|
101
88
|
# Validation helpers
|
102
89
|
|
103
90
|
def validate_account_data(hash)
|
104
|
-
unless [true, false, nil].include?(hash[
|
91
|
+
unless [true, false, nil].include?(hash["onlyReturnExisting"])
|
105
92
|
raise Bullion::Acme::Errors::Malformed,
|
106
|
-
"Invalid onlyReturnExisting: #{hash[
|
93
|
+
"Invalid onlyReturnExisting: #{hash["onlyReturnExisting"]}"
|
107
94
|
end
|
108
95
|
|
109
|
-
unless hash[
|
96
|
+
unless hash["contact"].is_a?(Array)
|
110
97
|
raise Bullion::Acme::Errors::InvalidContact,
|
111
|
-
"Invalid contacts format: #{hash[
|
98
|
+
"Invalid contacts format: #{hash["contact"].class}, #{hash}"
|
112
99
|
end
|
113
100
|
|
114
|
-
unless hash[
|
101
|
+
unless hash["contact"].size.positive?
|
115
102
|
raise Bullion::Acme::Errors::InvalidContact,
|
116
|
-
|
103
|
+
"Empty contacts list"
|
117
104
|
end
|
118
105
|
|
119
106
|
# Contacts must be a valid email
|
120
107
|
# TODO: find a better email verification approach
|
121
|
-
unless hash[
|
108
|
+
unless hash["contact"].grep_v(/^mailto:[a-zA-Z0-9@.+-]{3,}/).empty?
|
122
109
|
raise Bullion::Acme::Errors::UnsupportedContact
|
123
110
|
end
|
124
111
|
|
@@ -131,31 +118,34 @@ module Bullion
|
|
131
118
|
csr_domains = extract_csr_domains(csr_sans)
|
132
119
|
csr_cn = cn_from_csr(csr)
|
133
120
|
|
134
|
-
order_domains = order.identifiers.map { |i| i[
|
121
|
+
order_domains = order.identifiers.map { |i| i["value"] }
|
122
|
+
|
123
|
+
# Make sure the CSR has a valid public key
|
124
|
+
raise Bullion::Acme::Errors::BadCsr unless csr.verify(csr.public_key)
|
135
125
|
|
136
|
-
return false unless order.status ==
|
137
|
-
raise Bullion::Acme::Errors::
|
138
|
-
raise Bullion::Acme::Errors::
|
126
|
+
return false unless order.status == "ready"
|
127
|
+
raise Bullion::Acme::Errors::BadCsr unless csr_domains.include?(csr_cn)
|
128
|
+
raise Bullion::Acme::Errors::BadCsr unless csr_domains.sort == order_domains.sort
|
139
129
|
|
140
130
|
true
|
141
131
|
end
|
142
132
|
|
143
133
|
def validate_order(hash)
|
144
|
-
validate_order_nb_and_na(hash[
|
134
|
+
validate_order_nb_and_na(hash["notBefore"], hash["notAfter"])
|
145
135
|
|
146
136
|
# Don't approve empty orders
|
147
|
-
raise Bullion::Acme::Errors::InvalidOrder,
|
137
|
+
raise Bullion::Acme::Errors::InvalidOrder, "Empty order!" if hash["identifiers"].empty?
|
148
138
|
|
149
|
-
order_domains = hash[
|
139
|
+
order_domains = hash["identifiers"].select { |ident| ident["type"] == "dns" }
|
150
140
|
|
151
141
|
# Don't approve an order with identifiers that _aren't_ of type 'dns'
|
152
|
-
unless hash[
|
142
|
+
unless hash["identifiers"] == order_domains
|
153
143
|
raise Bullion::Acme::Errors::InvalidOrder, 'Only type "dns" allowed'
|
154
144
|
end
|
155
145
|
|
156
146
|
# Extract domains that end with something in our allowed domains list
|
157
147
|
valid_domains = order_domains.reject do |domain|
|
158
|
-
endings = CA_DOMAINS.select { |d| domain[
|
148
|
+
endings = CA_DOMAINS.select { |d| domain["value"].end_with?(d) }
|
159
149
|
endings.empty?
|
160
150
|
end
|
161
151
|
|
@@ -5,8 +5,8 @@ module Bullion
|
|
5
5
|
# Sinatra service helper methods
|
6
6
|
module Service
|
7
7
|
def add_acme_headers(nonce, additional: {})
|
8
|
-
headers[
|
9
|
-
headers[
|
8
|
+
headers["Replay-Nonce"] = nonce
|
9
|
+
headers["Link"] = "<#{uri("/directory")}>;rel=\"index\""
|
10
10
|
|
11
11
|
additional.each do |name, value|
|
12
12
|
headers[name.to_s] = value.to_s
|