bullion 0.1.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|