bullion 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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['kty']
10
- when 'RSA'
9
+ case key_data["kty"]
10
+ when "RSA"
11
11
  key_data_to_rsa(key_data)
12
- when 'EC'
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
- "#{csrdata}-----END CERTIFICATE REQUEST-----"
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['e']
26
- modulus = key_data['n']
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
- 'P-256' => 'prime256v1',
39
- 'P-384' => 'secp384r1',
40
- 'P-521' => 'secp521r1'
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['crv']])
44
- x = base64_to_octet(key_data['x'])
45
- y = base64_to_octet(key_data['y'])
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('C*').map do |byte|
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?('256')
64
- OpenSSL::Digest.new('SHA256')
65
- elsif alg.end_with?('384')
66
- OpenSSL::Digest.new('SHA384')
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('SHA512')
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('H*') }
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([['CN', common_name, OpenSSL::ASN1::UTF8STRING]])
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('basicConstraints', 'CA:FALSE', true)
108
+ ef.create_extension("basicConstraints", "CA:FALSE", true)
109
109
  )
110
110
  new_cert.add_extension(
111
- ef.create_extension('keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature', true)
111
+ ef.create_extension("keyUsage", "keyEncipherment,dataEncipherment,digitalSignature", true)
112
112
  )
113
113
  new_cert.add_extension(
114
- ef.create_extension('subjectKeyIdentifier', 'hash')
114
+ ef.create_extension("subjectKeyIdentifier", "hash")
115
115
  )
116
116
  new_cert.add_extension(
117
- ef.create_extension('extendedKeyUsage', 'serverAuth')
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('subjectAltName', valid_alts.join(',')))
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 = extract_csr_seq(raw_attributes)
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(attrs)
147
- attrs.select { |a| a.oid == 'extReq' }.map(&:value).first
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
- sequence.value.each do |v|
153
- v.each do |innerv|
154
- if innerv.value[0].value == 'subjectAltName'
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 { |alt| alt.match(/#{CA_DOMAIN}$/) }
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('/').select { |name| name =~ /^CN=/ }
179
+ cns = csr.subject.to_s.split("/").grep(/^CN=/)
172
180
 
173
- return cns.first.split('=').last if cns && !cns.empty?
181
+ return cns.first.split("=").last if cns && !cns.empty?
174
182
  end
175
183
 
176
- csr_sans(csr).first.split(':').last
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('SHA256'))
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?
@@ -20,7 +20,7 @@ module Bullion
20
20
  order.not_before = not_before if not_before
21
21
  order.not_after = not_after if not_after
22
22
  order.account = self
23
- order.status = 'pending'
23
+ order.status = "pending"
24
24
  order.identifiers = identifiers
25
25
  order.save
26
26
 
@@ -11,7 +11,7 @@ module Bullion
11
11
  validates_presence_of :subject
12
12
 
13
13
  def init_values
14
- self.serial ||= SecureRandom.hex(8).to_i(16)
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('/').select { |name| name =~ /^CN=/ }.first.split('=').last
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('SHA256')
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 'dns-01'
27
- ChallengeClients::DNS.new(self)
28
- when 'http-01'
29
- ChallengeClients::HTTP.new(self)
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."
@@ -15,7 +15,7 @@ module Bullion
15
15
  # Delete old nonces
16
16
  def self.clean_up!
17
17
  # nonces older than this can safely be deleted
18
- where('created_at < ?', Time.now - 86_400).delete_all
18
+ where("created_at < ?", Time.now - 86_400).delete_all
19
19
  end
20
20
  end
21
21
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
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'
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
@@ -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?('json') && !request.body.to_s.empty?
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