Skip to content

Commit c2da8af

Browse files
author
Stephen von Takach
committed
add support for ALPN
1 parent 97cf584 commit c2da8af

File tree

5 files changed

+552
-27
lines changed

5 files changed

+552
-27
lines changed

lib/ruby-tls/ssl.rb

Lines changed: 133 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,30 @@ def self.SSL_CTX_sess_set_cache_size(ssl_ctx, op)
147147
attach_function :SSL_CTX_set_cipher_list, [:ssl_ctx, :string], :int
148148
attach_function :SSL_CTX_set_session_id_context, [:ssl_ctx, :string, :buffer_length], :int
149149

150-
# TODO:: SSL_CTX_set_alpn_protos
150+
# OpenSSL before 1.0.2 do not have these methods
151+
begin
152+
attach_function :SSL_CTX_set_alpn_protos, [:ssl_ctx, :string, :uint], :int
153+
154+
SSL_TLSEXT_ERR_OK = 0
155+
SSL_TLSEXT_ERR_ALERT_WARNING = 1
156+
SSL_TLSEXT_ERR_ALERT_FATAL = 2
157+
SSL_TLSEXT_ERR_NOACK = 3
158+
159+
OPENSSL_NPN_UNSUPPORTED = 0
160+
OPENSSL_NPN_NEGOTIATED = 1
161+
OPENSSL_NPN_NO_OVERLAP = 2
162+
163+
attach_function :SSL_select_next_proto, [:pointer, :pointer, :string, :uint, :string, :uint], :int
164+
165+
# array of str, unit8 out,uint8 in, *arg
166+
callback :alpn_select_cb, [:ssl, :pointer, :pointer, :string, :uint, :pointer], :int
167+
attach_function :SSL_CTX_set_alpn_select_cb, [:ssl_ctx, :alpn_select_cb, :pointer], :void
168+
169+
attach_function :SSL_get0_alpn_selected, [:ssl, :pointer, :pointer], :void
170+
ALPN_SUPPORTED = true
171+
rescue FFI::NotFoundError
172+
ALPN_SUPPORTED = false
173+
end
151174

152175

153176
# Deconstructor
@@ -201,19 +224,23 @@ def self.SSL_CTX_sess_set_cache_size(ssl_ctx, op)
201224
8
202225
end
203226

204-
CRYPTO_LOCK = 0x1
205-
LockingCB = FFI::Function.new(:void, [:int, :int, :string, :int]) do |mode, type, file, line|
206-
if (mode & CRYPTO_LOCK) != 0
207-
SSL_LOCKS[type].lock
208-
else
209-
# Unlock a lock
210-
SSL_LOCKS[type].unlock
211-
end
212-
end
227+
# Locking isn't provided as long as all writes are done on the same thread.
228+
# This is my main use case. Happy to enable it if someone requires it and can
229+
# get it to work on MRI Ruby (Currently only works on JRuby and Rubinius)
230+
# as MRI callbacks occur on a thread pool?
213231

214-
ThreadIdCB = FFI::Function.new(:ulong, []) do
215-
Thread.current.object_id
216-
end
232+
#CRYPTO_LOCK = 0x1
233+
#LockingCB = FFI::Function.new(:void, [:int, :int, :string, :int]) do |mode, type, file, line|
234+
# if (mode & CRYPTO_LOCK) != 0
235+
# SSL_LOCKS[type].lock
236+
# else
237+
# Unlock a lock
238+
# SSL_LOCKS[type].unlock
239+
# end
240+
#end
241+
#ThreadIdCB = FFI::Function.new(:ulong, []) do
242+
# Thread.current.object_id
243+
#end
217244

218245

219246
# INIT CODE
@@ -262,6 +289,29 @@ class Context
262289
CIPHERS = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH:!CAMELLIA:@STRENGTH'.freeze
263290
SESSION = 'ruby-tls'.freeze
264291

292+
293+
ALPN_LOOKUP = ThreadSafe::Cache.new
294+
ALPN_Select_CB = FFI::Function.new(:int, [
295+
# array of str, unit8 out,uint8 in, *arg
296+
:pointer, :pointer, :pointer, :string, :uint, :pointer
297+
]) do |ssl_p, out, outlen, inp, inlen, arg|
298+
ssl = Box::InstanceLookup[ssl_p.address]
299+
protos = ssl.context.alpn_str
300+
301+
status = SSL.SSL_select_next_proto(out, outlen, protos, protos.length, inp, inlen)
302+
303+
ssl.negotiated
304+
305+
case status
306+
when SSL::OPENSSL_NPN_UNSUPPORTED
307+
SSL::SSL_TLSEXT_ERR_ALERT_FATAL
308+
when SSL::OPENSSL_NPN_NEGOTIATED
309+
SSL::SSL_TLSEXT_ERR_OK
310+
when SSL::OPENSSL_NPN_NO_OVERLAP
311+
SSL::SSL_TLSEXT_ERR_ALERT_WARNING
312+
end
313+
end
314+
265315
def initialize(server, options = {})
266316
@is_server = server
267317
@ssl_ctx = SSL.SSL_CTX_new(server ? SSL.SSLv23_server_method : SSL.SSLv23_client_method)
@@ -274,16 +324,27 @@ def initialize(server, options = {})
274324
end
275325

276326
SSL.SSL_CTX_set_cipher_list(@ssl_ctx, options[:ciphers] || CIPHERS)
327+
@alpn_set = false
277328

278329
if @is_server
279330
SSL.SSL_CTX_sess_set_cache_size(@ssl_ctx, 128)
280331
SSL.SSL_CTX_set_session_id_context(@ssl_ctx, SESSION, 8)
332+
333+
if SSL::ALPN_SUPPORTED && options[:protocols]
334+
@alpn_str = Context.build_alpn_string(options[:protocols])
335+
SSL.SSL_CTX_set_alpn_select_cb(@ssl_ctx, ALPN_Select_CB, nil)
336+
@alpn_set = true
337+
end
281338
else
282339
set_private_key(options[:private_key])
283340
set_certificate(options[:cert_chain])
284-
end
285341

286-
# TODO:: Check for ALPN support
342+
# Check for ALPN support
343+
if SSL::ALPN_SUPPORTED && options[:protocols]
344+
protocols = Context.build_alpn_string(options[:protocols])
345+
@alpn_set = SSL.SSL_CTX_set_alpn_protos(@ssl_ctx, protocols, protocols.length) == 0
346+
end
347+
end
287348
end
288349

289350
def cleanup
@@ -295,11 +356,23 @@ def cleanup
295356

296357
attr_reader :is_server
297358
attr_reader :ssl_ctx
359+
attr_reader :alpn_set
360+
attr_reader :alpn_str
298361

299362

300363
private
301364

302365

366+
def self.build_alpn_string(protos)
367+
protocols = ''.force_encoding('ASCII-8BIT')
368+
protos.each do |prot|
369+
protocol = prot.to_s
370+
protocols << protocol.length
371+
protocols << protocol
372+
end
373+
protocols
374+
end
375+
303376
def set_private_key(key)
304377
err = if key.is_a? FFI::Pointer
305378
SSL.SSL_CTX_use_PrivateKey(@ssl_ctx, key)
@@ -338,6 +411,8 @@ def set_certificate(cert)
338411

339412

340413
class Box
414+
InstanceLookup = ThreadSafe::Cache.new
415+
341416
READ_BUFFER = 2048
342417

343418
SSL_VERIFY_PEER = 0x01
@@ -347,6 +422,7 @@ def initialize(server, transport, options = {})
347422

348423
@handshake_completed = false
349424
@handshake_signaled = false
425+
@negotiated = false
350426
@transport = transport
351427

352428
@read_buffer = FFI::MemoryPointer.new(:char, READ_BUFFER, false)
@@ -360,11 +436,9 @@ def initialize(server, transport, options = {})
360436

361437
@write_queue = []
362438

363-
# TODO:: if server && options[:alpn_string]
364-
# SSL_CTX_set_alpn_select_cb
365-
366439
InstanceLookup[@ssl.address] = self
367440

441+
@alpn_fallback = options[:fallback]
368442
if options[:verify_peer]
369443
SSL.SSL_set_verify(@ssl, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, VerifyCB)
370444
end
@@ -374,6 +448,7 @@ def initialize(server, transport, options = {})
374448

375449

376450
attr_reader :is_server
451+
attr_reader :context
377452
attr_reader :handshake_completed
378453

379454

@@ -382,6 +457,22 @@ def get_peer_cert
382457
SSL.SSL_get_peer_certificate(@ssl)
383458
end
384459

460+
def negotiated_protocol
461+
return nil unless @context.alpn_set
462+
463+
proto = FFI::MemoryPointer.new(:pointer, 1, true)
464+
len = FFI::MemoryPointer.new(:uint, 1, true)
465+
SSL.SSL_get0_alpn_selected(@ssl, proto, len)
466+
467+
resp = proto.get_pointer(0)
468+
if resp.address == 0
469+
:failed
470+
else
471+
length = len.get_uint(0)
472+
resp.read_string(length).to_sym
473+
end
474+
end
475+
385476
def start
386477
return unless @ready
387478

@@ -435,7 +526,30 @@ def decrypt(data)
435526

436527
def signal_handshake
437528
@handshake_signaled = true
438-
@transport.handshake_cb
529+
530+
# Check protocol support here
531+
if @context.alpn_set
532+
proto = negotiated_protocol
533+
534+
if proto == :failed
535+
if @negotiated
536+
# We should shutdown if this is the case
537+
@transport.close_cb
538+
return
539+
elsif @alpn_fallback
540+
# Client or Server with a client that doesn't support ALPN
541+
proto = @alpn_fallback.to_sym
542+
end
543+
end
544+
else
545+
proto = nil
546+
end
547+
548+
@transport.handshake_cb(proto)
549+
end
550+
551+
def negotiated
552+
@negotiated = true
439553
end
440554

441555
SSL_RECEIVED_SHUTDOWN = 2
@@ -474,8 +588,6 @@ def get_plain_text(buffer, ready)
474588
end
475589
end
476590

477-
478-
InstanceLookup = ThreadSafe::Cache.new
479591
VerifyCB = FFI::Function.new(:int, [:int, :pointer]) do |preverify_ok, x509_store|
480592
x509 = SSL.X509_STORE_CTX_get_current_cert(x509_store)
481593
ssl = SSL.X509_STORE_CTX_get_ex_data(x509_store, SSL.SSL_get_ex_data_X509_STORE_CTX_idx)

lib/ruby-tls/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module RubyTls
2-
VERSION = "2.0.1"
2+
VERSION = "2.1.0"
33
end

0 commit comments

Comments
 (0)