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/lib/bullion/helpers/ssl.rb
CHANGED
@@ -6,24 +6,24 @@ module Bullion
|
|
6
6
|
module Ssl
|
7
7
|
# Converts the incoming key data to an OpenSSL public key usable to verify JWT signatures
|
8
8
|
def openssl_compat(key_data)
|
9
|
-
case key_data[
|
10
|
-
when
|
9
|
+
case key_data["kty"]
|
10
|
+
when "RSA"
|
11
11
|
key_data_to_rsa(key_data)
|
12
|
-
when
|
12
|
+
when "EC"
|
13
13
|
key_data_to_ecdsa(key_data)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
def openssl_compat_csr(csrdata)
|
18
18
|
"-----BEGIN CERTIFICATE REQUEST-----\n" \
|
19
|
-
|
19
|
+
"#{csrdata}-----END CERTIFICATE REQUEST-----"
|
20
20
|
end
|
21
21
|
|
22
22
|
# @see https://tools.ietf.org/html/rfc7518#page-30
|
23
23
|
def key_data_to_rsa(key_data)
|
24
24
|
key = OpenSSL::PKey::RSA.new
|
25
|
-
exponent = key_data[
|
26
|
-
modulus = key_data[
|
25
|
+
exponent = key_data["e"]
|
26
|
+
modulus = key_data["n"]
|
27
27
|
|
28
28
|
key.set_key(
|
29
29
|
base64_to_long(modulus),
|
@@ -35,14 +35,14 @@ module Bullion
|
|
35
35
|
|
36
36
|
def key_data_to_ecdsa(key_data)
|
37
37
|
crv_mapping = {
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
"P-256" => "prime256v1",
|
39
|
+
"P-384" => "secp384r1",
|
40
|
+
"P-521" => "secp521r1"
|
41
41
|
}
|
42
42
|
|
43
|
-
key = OpenSSL::PKey::EC.new(crv_mapping[key_data[
|
44
|
-
x = base64_to_octet(key_data[
|
45
|
-
y = base64_to_octet(key_data[
|
43
|
+
key = OpenSSL::PKey::EC.new(crv_mapping[key_data["crv"]])
|
44
|
+
x = base64_to_octet(key_data["x"])
|
45
|
+
y = base64_to_octet(key_data["y"])
|
46
46
|
|
47
47
|
key_bn = OpenSSL::BN.new("\x04#{x}#{y}", 2)
|
48
48
|
key.public_key = OpenSSL::PKey::EC::Point.new(key.group, key_bn)
|
@@ -50,7 +50,7 @@ module Bullion
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def base64_to_long(data)
|
53
|
-
Base64.urlsafe_decode64(data).to_s.unpack(
|
53
|
+
Base64.urlsafe_decode64(data).to_s.unpack("C*").map do |byte|
|
54
54
|
to_hex(byte)
|
55
55
|
end.join.to_i(16)
|
56
56
|
end
|
@@ -60,12 +60,12 @@ module Bullion
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def digest_from_alg(alg)
|
63
|
-
if alg.end_with?(
|
64
|
-
OpenSSL::Digest.new(
|
65
|
-
elsif alg.end_with?(
|
66
|
-
OpenSSL::Digest.new(
|
63
|
+
if alg.end_with?("256")
|
64
|
+
OpenSSL::Digest.new("SHA256")
|
65
|
+
elsif alg.end_with?("384")
|
66
|
+
OpenSSL::Digest.new("SHA384")
|
67
67
|
else
|
68
|
-
OpenSSL::Digest.new(
|
68
|
+
OpenSSL::Digest.new("SHA512")
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
@@ -78,7 +78,7 @@ module Bullion
|
|
78
78
|
r = joined_ints[0..31]
|
79
79
|
s = joined_ints[32..]
|
80
80
|
# Unpack each word to a hex string
|
81
|
-
hexnums = [r, s].map { |n| n.unpack1(
|
81
|
+
hexnums = [r, s].map { |n| n.unpack1("H*") }
|
82
82
|
# Convert each to an Integer
|
83
83
|
ints = hexnums.map { |bn| bn.to_i(16) }
|
84
84
|
# Convert each Integer to a BigNumber
|
@@ -96,7 +96,7 @@ module Bullion
|
|
96
96
|
end
|
97
97
|
|
98
98
|
def simple_subject(common_name)
|
99
|
-
OpenSSL::X509::Name.new([[
|
99
|
+
OpenSSL::X509::Name.new([["CN", common_name, OpenSSL::ASN1::UTF8STRING]])
|
100
100
|
end
|
101
101
|
|
102
102
|
def manage_csr_extensions(csr, new_cert)
|
@@ -105,16 +105,16 @@ module Bullion
|
|
105
105
|
ef.subject_certificate = new_cert
|
106
106
|
ef.issuer_certificate = Bullion.ca_cert
|
107
107
|
new_cert.add_extension(
|
108
|
-
ef.create_extension(
|
108
|
+
ef.create_extension("basicConstraints", "CA:FALSE", true)
|
109
109
|
)
|
110
110
|
new_cert.add_extension(
|
111
|
-
ef.create_extension(
|
111
|
+
ef.create_extension("keyUsage", "keyEncipherment,dataEncipherment,digitalSignature", true)
|
112
112
|
)
|
113
113
|
new_cert.add_extension(
|
114
|
-
ef.create_extension(
|
114
|
+
ef.create_extension("subjectKeyIdentifier", "hash")
|
115
115
|
)
|
116
116
|
new_cert.add_extension(
|
117
|
-
ef.create_extension(
|
117
|
+
ef.create_extension("extendedKeyUsage", "serverAuth")
|
118
118
|
)
|
119
119
|
|
120
120
|
# Alternate Names
|
@@ -122,7 +122,7 @@ module Bullion
|
|
122
122
|
existing_sans = filter_sans(csr_sans(csr))
|
123
123
|
valid_alts = (["DNS:#{cn}"] + [*existing_sans]).uniq
|
124
124
|
|
125
|
-
new_cert.add_extension(ef.create_extension(
|
125
|
+
new_cert.add_extension(ef.create_extension("subjectAltName", valid_alts.join(",")))
|
126
126
|
|
127
127
|
# return the updated cert and any subject alternate names added
|
128
128
|
[new_cert, valid_alts]
|
@@ -132,7 +132,7 @@ module Bullion
|
|
132
132
|
raw_attributes = csr.attributes
|
133
133
|
return [] unless raw_attributes
|
134
134
|
|
135
|
-
seq =
|
135
|
+
seq = extract_csr_attrs(csr)
|
136
136
|
return [] unless seq
|
137
137
|
|
138
138
|
values = extract_san_values(seq)
|
@@ -143,37 +143,45 @@ module Bullion
|
|
143
143
|
values.select { |v| v.tag == 2 }.map { |v| "DNS:#{v.value}" }
|
144
144
|
end
|
145
145
|
|
146
|
-
def extract_csr_attrs(
|
147
|
-
|
146
|
+
def extract_csr_attrs(csr)
|
147
|
+
csr.attributes.select { |a| a.oid == "extReq" }.map { |a| a.value.map(&:value) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def extract_csr_sans(csr_attrs)
|
151
|
+
csr_attrs.flatten.select { |a| a.value.first.value == "subjectAltName" }
|
152
|
+
end
|
153
|
+
|
154
|
+
def extract_csr_domains(csr_sans)
|
155
|
+
csr_decoded_sans = OpenSSL::ASN1.decode(csr_sans.first.value[1].value)
|
156
|
+
csr_decoded_sans.select { |v| v.tag == 2 }.map(&:value)
|
148
157
|
end
|
149
158
|
|
150
159
|
def extract_san_values(sequence)
|
160
|
+
unpacked_sequence = sequence
|
161
|
+
unpacked_sequence = unpacked_sequence.first while unpacked_sequence.first.is_a?(Array)
|
151
162
|
seqvalues = nil
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
seqvalues = innerv.value[1].value
|
156
|
-
break
|
157
|
-
end
|
158
|
-
break if seqvalues
|
159
|
-
end
|
163
|
+
unpacked_sequence.each do |outer_value|
|
164
|
+
seqvalues = outer_value.value[1].value if outer_value.value[0].value == "subjectAltName"
|
165
|
+
break if seqvalues
|
160
166
|
end
|
161
167
|
seqvalues
|
162
168
|
end
|
163
169
|
|
164
170
|
def filter_sans(potential_sans)
|
165
171
|
# Select only those that are part of the appropriate domain
|
166
|
-
potential_sans.select
|
172
|
+
potential_sans.select do |alt|
|
173
|
+
CA_DOMAINS.filter_map { |domain| alt.end_with?(".#{domain}") }.any?
|
174
|
+
end
|
167
175
|
end
|
168
176
|
|
169
177
|
def cn_from_csr(csr)
|
170
178
|
if csr.subject.to_s
|
171
|
-
cns = csr.subject.to_s.split(
|
179
|
+
cns = csr.subject.to_s.split("/").grep(/^CN=/)
|
172
180
|
|
173
|
-
return cns.first.split(
|
181
|
+
return cns.first.split("=").last if cns && !cns.empty?
|
174
182
|
end
|
175
183
|
|
176
|
-
csr_sans(csr).first.split(
|
184
|
+
csr_sans(csr).first.split(":").last
|
177
185
|
end
|
178
186
|
|
179
187
|
# Signs an ACME CSR
|
@@ -191,14 +199,14 @@ module Bullion
|
|
191
199
|
# Force a subject if the cert doesn't have one
|
192
200
|
cert.subject = simple_subject(cn_from_csr(csr)) unless cert.subject
|
193
201
|
|
194
|
-
csr_cert.subject = cert.subject.to_s
|
202
|
+
csr_cert.subject = simple_subject(cert.subject.to_s)
|
195
203
|
|
196
204
|
csr_cert.public_key = csr.public_key
|
197
205
|
csr_cert.issuer = Bullion.ca_cert.issuer
|
198
206
|
|
199
207
|
csr_cert, sans = manage_csr_extensions(csr, csr_cert)
|
200
208
|
|
201
|
-
csr_cert.sign(Bullion.ca_key, OpenSSL::Digest.new(
|
209
|
+
csr_cert.sign(Bullion.ca_key, OpenSSL::Digest.new("SHA256"))
|
202
210
|
|
203
211
|
cert.data = csr_cert.to_pem
|
204
212
|
cert.alternate_names = sans unless sans.empty?
|
@@ -11,7 +11,7 @@ module Bullion
|
|
11
11
|
validates_presence_of :subject
|
12
12
|
|
13
13
|
def init_values
|
14
|
-
self.serial ||= SecureRandom.hex(
|
14
|
+
self.serial ||= SecureRandom.hex(4).to_i(16)
|
15
15
|
end
|
16
16
|
|
17
17
|
def fingerprint
|
@@ -19,7 +19,7 @@ module Bullion
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def cn
|
22
|
-
subject.split(
|
22
|
+
subject.split("/").grep(/^CN=/).first.split("=").last
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.from_csr(csr)
|
@@ -17,16 +17,16 @@ module Bullion
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def thumbprint
|
20
|
-
cipher = OpenSSL::Digest.new(
|
20
|
+
cipher = OpenSSL::Digest.new("SHA256")
|
21
21
|
cipher.hexdigest authorization.order.account.public_key.to_json
|
22
22
|
end
|
23
23
|
|
24
24
|
def client
|
25
25
|
case acme_type
|
26
|
-
when
|
27
|
-
|
28
|
-
when
|
29
|
-
|
26
|
+
when "dns-01"
|
27
|
+
DNS_CHALLENGE_CLIENT.new(self)
|
28
|
+
when "http-01"
|
29
|
+
HTTP_CHALLENGE_CLIENT.new(self)
|
30
30
|
else
|
31
31
|
raise Bullion::Acme::Errors::UnsupportedChallengeType,
|
32
32
|
"Challenge type '#{acme_type}' is not supported by Bullion."
|
data/lib/bullion/models/nonce.rb
CHANGED
data/lib/bullion/models.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
3
|
+
require "bullion/models/account"
|
4
|
+
require "bullion/models/authorization"
|
5
|
+
require "bullion/models/certificate"
|
6
|
+
require "bullion/models/challenge"
|
7
|
+
require "bullion/models/nonce"
|
8
|
+
require "bullion/models/order"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module RSpec
|
5
|
+
module ChallengeClients
|
6
|
+
# A test DNS challenge client resolver for RSpec
|
7
|
+
class DNS < ::Bullion::ChallengeClients::DNS
|
8
|
+
FakeDNSRecord = Struct.new("FakeDNSRecord", :strings)
|
9
|
+
|
10
|
+
def records_for(name, _nameserver = nil)
|
11
|
+
return [] unless name == "_acme-challenge.#{identifier}"
|
12
|
+
|
13
|
+
[
|
14
|
+
FakeDNSRecord.new(
|
15
|
+
digest_value("#{challenge.token}.#{challenge.thumbprint}")
|
16
|
+
)
|
17
|
+
]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bullion
|
4
|
+
module RSpec
|
5
|
+
module ChallengeClients
|
6
|
+
# A test HTTP challenge client resolver for RSpec
|
7
|
+
class HTTP < ::Bullion::ChallengeClients::HTTP
|
8
|
+
def retrieve_body(url)
|
9
|
+
return "" unless url == "http://#{identifier}/.well-known/acme-challenge/#{challenge.token}"
|
10
|
+
|
11
|
+
"#{challenge.token}.#{challenge.thumbprint}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/bullion/service.rb
CHANGED
@@ -15,12 +15,13 @@ module Bullion
|
|
15
15
|
|
16
16
|
before do
|
17
17
|
# Sets up a useful variable (@json_body) for accessing a parsed request body
|
18
|
-
if request.content_type&.include?(
|
18
|
+
if request.content_type&.include?("json") && !request.body.read.empty?
|
19
|
+
p request.body
|
19
20
|
request.body.rewind
|
20
21
|
@json_body = JSON.parse(request.body.read, symbolize_names: true)
|
21
22
|
end
|
22
23
|
rescue StandardError => e
|
23
|
-
halt(400, { error: "Request must be JSON: #{e.message}" }.to_json)
|
24
|
+
halt(400, { error: "Request must be JSON: #{e.message}}" }.to_json)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
end
|