Module: Bullion::Helpers::Ssl

Defined in:
lib/bullion/helpers/ssl.rb

Overview

SSL-related helper methods rubocop:disable Metrics/ModuleLength

Instance Method Summary collapse

Instance Method Details

#base64_to_long(data) ⇒ Object



82
83
84
85
86
# File 'lib/bullion/helpers/ssl.rb', line 82

def base64_to_long(data)
  Base64.urlsafe_decode64(data).to_s.unpack("C*").map do |byte|
    to_hex(byte)
  end.join.to_i(16)
end

#base64_to_octet(data) ⇒ Object



88
89
90
# File 'lib/bullion/helpers/ssl.rb', line 88

def base64_to_octet(data)
  Base64.urlsafe_decode64(data)
end

#build_valid_alt_names(csr) ⇒ Object



217
218
219
220
221
222
# File 'lib/bullion/helpers/ssl.rb', line 217

def build_valid_alt_names(csr)
  cn = cn_or_first_san_from_csr(csr)
  csr_domains = domains_from_csr(csr)
  existing_sans = filter_sans(csr_domains).map { |d| "DNS:#{d}" }
  (["DNS:#{cn}"] + [*existing_sans]).uniq
end

#cn_from_csr(csr) ⇒ Object



194
195
196
197
198
199
200
# File 'lib/bullion/helpers/ssl.rb', line 194

def cn_from_csr(csr)
  return unless csr.subject.to_s

  cns = csr.subject.to_s.split("/").grep(/^CN=/)

  cns.first.split("=").last if cns && !cns.empty?
end

#cn_or_first_san_from_csr(csr) ⇒ Object



202
203
204
205
206
207
208
209
# File 'lib/bullion/helpers/ssl.rb', line 202

def cn_or_first_san_from_csr(csr)
  cn = cn_from_csr(csr)
  return cn if cn

  csr_attrs = extract_csr_attrs(csr)
  csr_sans = extract_csr_sans(csr_attrs)
  extract_csr_domains(csr_sans).first
end

#digest_from_alg(alg) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/bullion/helpers/ssl.rb', line 92

def digest_from_alg(alg)
  # EdDSA doesn't use a separate digest step
  return nil if alg.downcase == "eddsa"

  if alg.end_with?("256")
    OpenSSL::Digest.new("SHA256")
  elsif alg.end_with?("384")
    OpenSSL::Digest.new("SHA384")
  else
    OpenSSL::Digest.new("SHA512")
  end
end

#domains_from_csr(csr) ⇒ Object



211
212
213
214
215
# File 'lib/bullion/helpers/ssl.rb', line 211

def domains_from_csr(csr)
  csr_attrs = extract_csr_attrs(csr)
  csr_sans = extract_csr_sans(csr_attrs)
  extract_csr_domains(csr_sans)
end

#ecdsa_sig_to_der(incoming) ⇒ Object

This is for ECDSA keys.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/bullion/helpers/ssl.rb', line 107

def ecdsa_sig_to_der(incoming)
  # Base64 decode the signature
  joined_ints = Base64.urlsafe_decode64(incoming)
  # Break it apart into the two 32-byte words
  r = joined_ints[0..31]
  s = joined_ints[32..]
  # Unpack each word to a hex string
  hexnums = [r, s].map { |n| n.unpack1("H*") }
  # Convert each to an Integer
  ints = hexnums.map { |bn| bn.to_i(16) }
  # Convert each Integer to a BigNumber
  bns = ints.map { |int| OpenSSL::BN.new(int) }
  # Wrap each BigNum in a ASN1 encoding
  asn1_wrapped = bns.map { |bn| OpenSSL::ASN1::Integer.new(bn) }
  # Create an ASN1 sequence from the ASN1-wrapped Integers
  seq = OpenSSL::ASN1::Sequence.new(asn1_wrapped)
  # Return the DER-encoded sequence for verification
  seq.to_der
end

#extract_csr_attrs(csr) ⇒ Object



162
163
164
# File 'lib/bullion/helpers/ssl.rb', line 162

def extract_csr_attrs(csr)
  csr.attributes.select { |a| a.oid == "extReq" }.map { |a| a.value.map(&:value) }
end

#extract_csr_domains(csr_sans) ⇒ Object



170
171
172
173
174
# File 'lib/bullion/helpers/ssl.rb', line 170

def extract_csr_domains(csr_sans)
  subject_alt_names = csr_sans.first.value.find { |v| v.tag == 4 }
  csr_decoded_sans = OpenSSL::ASN1.decode(subject_alt_names.value)
  csr_decoded_sans.value.select { |v| v.tag == 2 }.map(&:value)
end

#extract_csr_sans(csr_attrs) ⇒ Object



166
167
168
# File 'lib/bullion/helpers/ssl.rb', line 166

def extract_csr_sans(csr_attrs)
  csr_attrs.flatten.select { |a| a.value.first.value == "subjectAltName" }
end

#extract_san_values(sequence) ⇒ Object



176
177
178
179
180
181
182
183
184
185
# File 'lib/bullion/helpers/ssl.rb', line 176

def extract_san_values(sequence)
  unpacked_sequence = sequence
  unpacked_sequence = unpacked_sequence.first while unpacked_sequence.first.is_a?(Array)
  seqvalues = nil
  unpacked_sequence.each do |outer_value|
    seqvalues = outer_value.value[1].value if outer_value.value[0].value == "subjectAltName"
    break if seqvalues
  end
  seqvalues
end

#filter_sans(potential_sans) ⇒ Object



187
188
189
190
191
192
# File 'lib/bullion/helpers/ssl.rb', line 187

def filter_sans(potential_sans)
  # Select only those that are part of the appropriate domain
  potential_sans.select do |alt|
    Bullion.config.ca.domains.filter_map { |domain| alt.end_with?(".#{domain}") }.any?
  end
end

#key_data_to_ecdsa(key_data) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/bullion/helpers/ssl.rb', line 37

def key_data_to_ecdsa(key_data)
  crv_mapping = {
    "P-256" => "prime256v1",
    "secp256k1" => "secp256k1",
    "P-384" => "secp384r1",
    "P-521" => "secp521r1"
  }

  x = base64_to_octet(key_data["x"])
  y = base64_to_octet(key_data["y"])
  curve_name = crv_mapping[key_data["crv"]]
  raise "Unknown curve" unless curve_name

  key_group = OpenSSL::PKey::EC::Group.new(curve_name)
  key_bn = OpenSSL::BN.new("\x04#{x}#{y}", 2)
  key_point = OpenSSL::PKey::EC::Point.new(key_group, key_bn)

  pk_sequence = OpenSSL::ASN1::Sequence.new(
    [OpenSSL::ASN1::ObjectId("id-ecPublicKey"), OpenSSL::ASN1::ObjectId(curve_name)]
  )
  bitstring = OpenSSL::ASN1::BitString.new(key_point.to_octet_string(:uncompressed))

  outer_sequence = OpenSSL::ASN1::Sequence.new([pk_sequence, bitstring])

  OpenSSL::PKey::EC.new(outer_sequence.to_der)
end

#key_data_to_eddsa(key_data) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/bullion/helpers/ssl.rb', line 64

def key_data_to_eddsa(key_data)
  curve = key_data["crv"]
  x = base64_to_octet(key_data["x"])

  # For JWT verification with jwt-eddsa gem, we need Ed25519::VerifyKey
  case curve
  when "Ed25519"
    # The raw public key bytes are exactly what Ed25519::VerifyKey expects
    Ed25519::VerifyKey.new(x)
  when "Ed448"
    # Ed448 not currently supported by the ed25519 gem
    raise Bullion::Acme::Errors::BadPublicKey,
          "EdDSA with Ed448 is not supported; only Ed25519 is supported"
  else
    raise Bullion::Acme::Errors::BadPublicKey, "Unsupported EdDSA curve: #{curve}"
  end
end

#key_data_to_rsa(key_data) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/bullion/helpers/ssl.rb', line 21

def key_data_to_rsa(key_data)
  exponent = base64_to_long(key_data["e"])
  modulus = base64_to_long(key_data["n"])

  data_sequence = OpenSSL::ASN1::Sequence.new(
    [
      OpenSSL::ASN1::Integer.new(modulus),
      OpenSSL::ASN1::Integer.new(exponent)
    ]
  )

  outer_sequence = OpenSSL::ASN1::Sequence.new(data_sequence)

  OpenSSL::PKey::RSA.new(outer_sequence.to_der)
end

#manage_csr_extensions(csr, new_cert) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/bullion/helpers/ssl.rb', line 135

def manage_csr_extensions(csr, new_cert)
  # Build extensions
  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = new_cert
  ef.issuer_certificate = Bullion.ca_cert
  new_cert.add_extension(
    ef.create_extension("basicConstraints", "CA:FALSE", true)
  )
  new_cert.add_extension(
    ef.create_extension("keyUsage", "keyEncipherment,dataEncipherment,digitalSignature", true)
  )
  new_cert.add_extension(
    ef.create_extension("subjectKeyIdentifier", "hash")
  )
  new_cert.add_extension(
    ef.create_extension("extendedKeyUsage", "serverAuth")
  )

  # Alternate Names
  valid_alts = build_valid_alt_names(csr)

  new_cert.add_extension(ef.create_extension("subjectAltName", valid_alts.join(",")))

  # return the updated cert and any subject alternate names added
  [new_cert, valid_alts]
end

#openssl_compat(key_data) ⇒ Object

Converts the incoming key data to an OpenSSL public key usable to verify JWT signatures



9
10
11
12
13
14
15
16
17
18
# File 'lib/bullion/helpers/ssl.rb', line 9

def openssl_compat(key_data)
  case key_data["kty"]
  when "RSA"
    key_data_to_rsa(key_data)
  when "EC"
    key_data_to_ecdsa(key_data)
  when "OKP"
    key_data_to_eddsa(key_data)
  end
end

#sign_csr(csr, username) ⇒ Object

Signs an ACME CSR rubocop:disable Metrics/AbcSize



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/bullion/helpers/ssl.rb', line 226

def sign_csr(csr, username)
  cert = Models::Certificate.from_csr(csr)
  # Create a OpenSSL cert using select info from the CSR
  csr_cert = OpenSSL::X509::Certificate.new
  csr_cert.serial = cert.serial
  csr_cert.version = 2 # OpenSSL uses zero-indexed versions: 2 = x509v3
  csr_cert.not_before = Time.now
  # only 90 days for ACMEv2
  csr_cert.not_after = csr_cert.not_before + (3 * 30 * 24 * 60 * 60)

  # Force a subject if the cert doesn't have one
  cert.subject = simple_subject(cn_or_first_san_from_csr(csr)) unless cert.subject

  csr_cert.subject = simple_subject(cert.subject.to_s)

  csr_cert.public_key = csr.public_key
  csr_cert.issuer = Bullion.ca_cert.subject

  csr_cert, sans = manage_csr_extensions(csr, csr_cert)

  csr_cert.sign(Bullion.ca_key, OpenSSL::Digest.new("SHA256"))

  cert.data = csr_cert.to_pem
  cert.alternate_names = sans unless sans.empty?
  cert.requester = username
  cert.validated = true
  cert.save

  [csr_cert, cert.id]
end

#simple_subject(common_name) ⇒ Object



131
132
133
# File 'lib/bullion/helpers/ssl.rb', line 131

def simple_subject(common_name)
  OpenSSL::X509::Name.new([["CN", common_name, OpenSSL::ASN1::UTF8STRING]])
end

#to_hex(int) ⇒ Object



127
128
129
# File 'lib/bullion/helpers/ssl.rb', line 127

def to_hex(int)
  int < 16 ? "0#{int.to_s(16)}" : int.to_s(16)
end