Class: MidiSmtpServer::TlsTransport

Inherits:
Object
  • Object
show all
Defined in:
lib/midi-smtp-server/tls-transport.rb

Overview

class for TlsTransport

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cert_path, key_path, ciphers, methods, cert_cn, cert_san, logger) ⇒ TlsTransport

Returns a new instance of TlsTransport.



28
29
30
31
32
33
34
35
36
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/midi-smtp-server/tls-transport.rb', line 28

def initialize(cert_path, key_path, ciphers, methods, cert_cn, cert_san, logger)
  # if need to debug something while working with openssl
  # OpenSSL::debug = true

  # save references
  @logger = logger
  @cert_path = cert_path.to_s == '' ? nil : cert_path.strip
  @key_path = key_path.to_s == '' ? nil : key_path.strip
  # create SSL context
  @ssl_context = OpenSSL::SSL::SSLContext.new
  @ssl_context.ciphers = ciphers.to_s == '' ? TLS_CIPHERS_ADVANCED_PLUS : ciphers
  @ssl_context.ssl_version = methods.to_s == '' ? TLS_METHODS_ADVANCED : methods
  # check cert_path and key_path
  if @cert_path.nil?
    # if none cert_path was set, create a self signed test certificate
    # and try to setup common subject and subject alt name(s) for cert
    @cert_cn = cert_cn.to_s.strip
    @cert_san = ([@cert_cn] + (cert_san.nil? ? [] : cert_san)).uniq
    # as well as IP Address extension entries for subject alt name(s) if ipv4 or ipv6 address
    @cert_san_ip = []
    @cert_san.each { |san| @cert_san_ip << san if san =~ Resolv::IPv4::Regex || san =~ Resolv::IPv6::Regex }
    # initialize self certificate and key
    logger.debug("SSL: using self generated test certificate! CN=#{@cert_cn} SAN=[#{@cert_san.join(',')}]")
    @ssl_context.key = OpenSSL::PKey::RSA.new 4096
    @ssl_context.cert = OpenSSL::X509::Certificate.new
    @ssl_context.cert.version = 2
    @ssl_context.cert.serial = 1
    # the subject and the issuer are identical only for test certificate
    @ssl_context.cert.subject = OpenSSL::X509::Name.new [['CN', @cert_cn]]
    @ssl_context.cert.issuer = @ssl_context.cert.subject
    @ssl_context.cert.public_key = @ssl_context.key
    # valid for 90 days
    @ssl_context.cert.not_before = Time.now
    @ssl_context.cert.not_after = Time.now + (60 * 60 * 24 * 90)
    # setup some cert extensions
    x509_extension_factory = OpenSSL::X509::ExtensionFactory.new
    x509_extension_factory.subject_certificate = @ssl_context.cert
    x509_extension_factory.issuer_certificate = @ssl_context.cert
    @ssl_context.cert.add_extension(x509_extension_factory.create_extension('basicConstraints', 'CA:FALSE', false))
    @ssl_context.cert.add_extension(x509_extension_factory.create_extension('keyUsage', 'digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment', false))
    @ssl_context.cert.add_extension(x509_extension_factory.create_extension('subjectAltName', (@cert_san.map { |san| "DNS:#{san}" } + @cert_san_ip.map { |ip| "IP:#{ip}" }).join(', '), false))
    @ssl_context.cert.sign @ssl_context.key, OpenSSL::Digest.new('SHA256')
    logger.debug("SSL: generated test certificate\r\n#{@ssl_context.cert.to_text}")
  else
    # if any is set, test the paths
    raise "File \"#{@cert_path}\" does not exist or is not a regular file. Could not load certificate." unless File.file?(@cert_path.to_s)
    raise "File \"#{@key_path}\" does not exist or is not a regular file. Could not load private key." unless @key_path.nil? || File.file?(@key_path.to_s)
    # try to load certificate and key
    cert_lines = File.read(@cert_path.to_s).lines
    # check if the cert file contains a chain of certs
    cert_indexes = cert_lines.each_with_index.map { |line, index| index if line.downcase.include?('-begin certificate-') }.compact
    # create each cert in the chain
    certs = []
    cert_indexes.each_with_index do |cert_index, current_index|
      end_index = current_index + 1 < cert_indexes.length ? cert_indexes[current_index + 1] : -1
      certs << OpenSSL::X509::Certificate.new(cert_lines[cert_index..end_index].join)
    end
    # add the cert and optional found chain to context
    @ssl_context.cert = certs.first
    @ssl_context.extra_chain_cert = certs[1..]
    # check if key was given by separate file or should be included in cert
    if @key_path.nil?
      key_index = cert_lines.index { |line| line =~ /-begin[^-]+key-/i }
      end_index = cert_lines.index { |line| line =~ /-end[^-]+key-/i }
      @ssl_context.key = OpenSSL::PKey::RSA.new(cert_lines[key_index..end_index].join)
    else
      @ssl_context.key = OpenSSL::PKey::RSA.new(File.open(@key_path.to_s))
    end
  end
end

Instance Attribute Details

#ssl_contextObject (readonly)

current TLS OpenSSL::SSL::SSLContext



26
27
28
# File 'lib/midi-smtp-server/tls-transport.rb', line 26

def ssl_context
  @ssl_context
end

Instance Method Details

#start(io) ⇒ Object

start ssl connection over existing tcpserver socket



100
101
102
103
104
105
106
107
108
109
# File 'lib/midi-smtp-server/tls-transport.rb', line 100

def start(io)
  # start SSL negotiation
  ssl = OpenSSL::SSL::SSLSocket.new(io, @ssl_context)
  # connect to server socket
  ssl.accept
  # make sure to close also the underlying io
  ssl.sync_close = true
  # return as new io socket
  return ssl
end