diff options
Diffstat (limited to 'ext/openssl/lib')
| -rw-r--r-- | ext/openssl/lib/net/ftptls.rb | 43 | ||||
| -rw-r--r-- | ext/openssl/lib/net/https.rb | 188 | ||||
| -rw-r--r-- | ext/openssl/lib/net/protocols.rb | 56 | ||||
| -rw-r--r-- | ext/openssl/lib/net/telnets.rb | 250 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl.rb | 41 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/bn.rb | 47 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/buffering.rb | 433 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/cipher.rb | 103 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/digest.rb | 99 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/hmac.rb | 78 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/marshal.rb | 30 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/pkcs5.rb | 22 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/pkey.rb | 493 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/ssl.rb | 484 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/version.rb | 6 | ||||
| -rw-r--r-- | ext/openssl/lib/openssl/x509.rb | 388 |
16 files changed, 2003 insertions, 758 deletions
diff --git a/ext/openssl/lib/net/ftptls.rb b/ext/openssl/lib/net/ftptls.rb deleted file mode 100644 index f433457923..0000000000 --- a/ext/openssl/lib/net/ftptls.rb +++ /dev/null @@ -1,43 +0,0 @@ -=begin -= $RCSfile$ -- SSL/TLS enhancement for Net::HTTP. - -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2003 Blaz Grilc <farmer@gmx.co.uk> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Requirements - -= Version - $Id$ - -= Notes - Tested on FreeBSD 5-CURRENT and 4-STABLE - - ruby 1.6.8 (2003-01-17) [i386-freebsd5] - - OpenSSL 0.9.7a Feb 19 2003 - - ruby-openssl-0.2.0.p0 - tested on ftp server: glftpd 1.30 -=end - -require 'socket' -require 'openssl' -require 'net/ftp' - -module Net - class FTPTLS < FTP - def login(user = "anonymous", passwd = nil, acct = nil) - ctx = OpenSSL::SSL::SSLContext.new('SSLv23') - ctx.key = nil - ctx.cert = nil - voidcmd("AUTH TLS") - @sock = OpenSSL::SSL::SSLSocket.new(@sock, ctx) - @sock.connect - super(user, passwd, acct) - voidcmd("PBSZ 0") - end - end -end diff --git a/ext/openssl/lib/net/https.rb b/ext/openssl/lib/net/https.rb deleted file mode 100644 index fb7f53c555..0000000000 --- a/ext/openssl/lib/net/https.rb +++ /dev/null @@ -1,188 +0,0 @@ -=begin -= $RCSfile$ -- SSL/TLS enhancement for Net::HTTP. - -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Requirements - This program requires Net 1.2.0 or higher version. - You can get it from RAA or Ruby's CVS repository. - -= Version - $Id$ - - 2001/11/06: Contiributed to Ruby/OpenSSL project. - -== class Net::HTTP - -== Example - -Simple HTTP client is here: - - require 'net/http' - host, port, path = "localhost", 80, "/" - if %r!http://(.*?)(?::(\d+))?(/.*)! =~ ARGV[0] - host = $1 - port = $2.to_i if $2 - path = $3 - end - h = Net::HTTP.new(host, port) - h.get2(path){ |resp| print resp.body } - -It can be replaced by follow one: - - require 'net/https' - host, port, path = "localhost", 80, "/" - if %r!(https?)://(.*?)(?::(\d+))?(/.*)! =~ ARGV[0] - scheme = $1 - host = $2 - port = $3 ? $3.to_i : ((scheme == "http") ? 80 : 443) - path = $4 - end - h = Net::HTTP.new(host, port) - h.use_ssl = true if scheme == "https" # enable SSL/TLS - h.get2(path){ |resp| print resp.body } - -=== Instance Methods - -: use_ssl - returns ture if use SSL/TLS with HTTP. - -: use_ssl=((|true_or_false|)) - sets use_ssl. - -: peer_cert - return the X.509 certificates the server presented. - -: key=((|key|)) - Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. - (This method is appeared in Michal Rokos's OpenSSL extention.) - -: key_file=((|path|)) - Sets a private key file to use in PEM format. - -: cert=((|cert|)) - Sets an OpenSSL::X509::Certificate object as client certificate. - (This method is appeared in Michal Rokos's OpenSSL extention.) - -: cert_file=((|path|)) - Sets pathname of a X.509 certification file in PEM format. - -: ca_file=((|path|)) - Sets path of a CA certification file in PEM format. - The file can contrain several CA certificats. - -: ca_path=((|path|)) - Sets path of a CA certification directory containing certifications - in PEM format. - -: verify_mode=((|mode|)) - Sets the flags for server the certification verification at - begining of SSL/TLS session. - OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable. - -: verify_callback=((|proc|)) - Sets the verify callback for the server certification verification. - -: verify_depth=((|num|)) - Sets the maximum depth for the certificate chain verification. - -: cert_store=((|store|)) - Sets the X509::Store to verify peer certificate. - -=end - -require 'net/protocols' -require 'net/http' - -module Net - class HTTP - class Conn < HTTPRequest - REQUEST_HAS_BODY=false - RESPONSE_HAS_BODY=false - METHOD="connect" - - def initialize - super nil, nil - end - - def exec( sock, addr, port, ver ) - @socket = sock - request(addr, port, ver) - end - - def request( addr, port, ver ) - @socket.writeline sprintf('CONNECT %s:%s HTTP/%s', addr, port, ver) - @socket.writeline '' - end - end - - module ProxyMod - def edit_path( path ) - if use_ssl - 'https://' + addr_port + path - else - 'http://' + addr_port + path - end - end - end - - def self.socket_type - SSLIO - end - - attr_reader :use_ssl - attr_writer :key, :cert - attr_writer :ca_file, :ca_path - attr_writer :verify_mode, :verify_callback, :verify_depth - attr_writer :cert_store, :timeout - attr_reader :peer_cert - - alias :default_initialize :initialize - - def initialize(*args) - default_initialize(*args) - @key = @cert = @ca_file = @ca_path = @verify_mode = - @verify_callback = @verify_depth = @timeout = @cert_store = nil - @already_connected = false - end - - def use_ssl=(flag) - if @already_connected && !@use_ssl - raise ProtocolError, "connection is alrady set up" - end - @use_ssl = flag - end - - def on_connect - if use_ssl - if proxy? - Conn.new.exec(@socket, @address, @port, "1.0") - resp = HTTPResponse.read_new(@socket) - if resp.code != '200' - raise resp.message - end - end - @socket.key = @key if @key - @socket.cert = @cert if @cert - @socket.ca_file = @ca_file - @socket.ca_path = @ca_path - @socket.verify_mode = @verify_mode - @socket.verify_callback = @verify_callback - @socket.verify_depth = @verify_depth - @socket.timeout = @timeout - @socket.cert_store = @cert_store - @socket.ssl_connect - @peer_cert = @socket.peer_cert - end - @already_connected = true - end - - end -end diff --git a/ext/openssl/lib/net/protocols.rb b/ext/openssl/lib/net/protocols.rb deleted file mode 100644 index 073d4f3027..0000000000 --- a/ext/openssl/lib/net/protocols.rb +++ /dev/null @@ -1,56 +0,0 @@ -=begin -= $RCSfile$ -- SSL/TLS enhancement for Net. - -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Requirements - This program requires Net 1.2.0 or higher version. - You can get it from RAA or Ruby's CVS repository. - -= Version - $Id$ - - 2001/11/06: Contiributed to Ruby/OpenSSL project. -=end - -require 'net/protocol' -require 'forwardable' -require 'openssl' - -module Net - class SSLIO < InternetMessageIO - extend Forwardable - - def_delegators(:@ssl_context, - :key=, :cert=, :key_file=, :cert_file=, - :ca_file=, :ca_path=, - :verify_mode=, :verify_callback=, :verify_depth=, - :timeout=, :cert_store=) - - def initialize(addr, port, otime = nil, rtime = nil, dout = nil) - super - @ssl_context = OpenSSL::SSL::SSLContext.new() - end - - def ssl_connect() - unless @ssl_context.verify_mode - warn "warning: peer certificate won't be verified in this SSL session." - @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - @socket = OpenSSL::SSL::SSLSocket.new(@socket, @ssl_context) - @socket.sync_close = true - @socket.connect - end - - def peer_cert - @socket.peer_cert - end - end -end diff --git a/ext/openssl/lib/net/telnets.rb b/ext/openssl/lib/net/telnets.rb deleted file mode 100644 index c7ecbd717a..0000000000 --- a/ext/openssl/lib/net/telnets.rb +++ /dev/null @@ -1,250 +0,0 @@ -=begin -= $RCSfile$ -- SSL/TLS enhancement for Net::Telnet. - -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Version - $Id$ - - 2001/11/06: Contiributed to Ruby/OpenSSL project. - -== class Net::Telnet - -This class will initiate SSL/TLS session automaticaly if the server -sent OPT_STARTTLS. Some options are added for SSL/TLS. - - host = Net::Telnet::new({ - "Host" => "localhost", - "Port" => "telnets", - ## follows are new options. - 'CertFile' => "user.crt", - 'KeyFile' => "user.key", - 'CAFile' => "/some/where/certs/casert.pem", - 'CAPath' => "/some/where/caserts", - 'VerifyMode' => SSL::VERIFY_PEER, - 'VerifyCallback' => verify_proc - }) - -Or, the new options ('Cert', 'Key' and 'CACert') are available from -Michal Rokos's OpenSSL module. - - cert_data = File.open("user.crt"){|io| io.read } - pkey_data = File.open("user.key"){|io| io.read } - cacert_data = File.open("your_ca.pem"){|io| io.read } - host = Net::Telnet::new({ - "Host" => "localhost", - "Port" => "telnets", - 'Cert' => OpenSSL::X509::Certificate.new(cert_data) - 'Key' => OpenSSL::PKey::RSA.new(pkey_data) - 'CACert' => OpenSSL::X509::Certificate.new(cacert_data) - 'CAFile' => "/some/where/certs/casert.pem", - 'CAPath' => "/some/where/caserts", - 'VerifyMode' => SSL::VERIFY_PEER, - 'VerifyCallback' => verify_proc - }) - -This class is expected to be a superset of usual Net::Telnet. -=end - -require "net/telnet" -require "openssl" - -module Net - class Telnet - attr_reader :ssl - - OPT_STARTTLS = 46.chr # "\056" # "\x2e" # Start TLS - TLS_FOLLOWS = 1.chr # "\001" # "\x01" # FOLLOWS (for STARTTLS) - - alias preprocess_orig preprocess - - def ssl?; @ssl; end - - def preprocess(string) - # combine CR+NULL into CR - string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"] - - # combine EOL into "\n" - string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"] - - string.gsub(/#{IAC}( - [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]| - [#{DO}#{DONT}#{WILL}#{WONT}][#{OPT_BINARY}-#{OPT_EXOPL}]| - #{SB}[#{OPT_BINARY}-#{OPT_EXOPL}] - (#{IAC}#{IAC}|[^#{IAC}])+#{IAC}#{SE} - )/xno) do - if IAC == $1 # handle escaped IAC characters - IAC - elsif AYT == $1 # respond to "IAC AYT" (are you there) - self.write("nobody here but us pigeons" + EOL) - '' - elsif DO[0] == $1[0] # respond to "IAC DO x" - if OPT_BINARY[0] == $1[1] - @telnet_option["BINARY"] = true - self.write(IAC + WILL + OPT_BINARY) - elsif OPT_STARTTLS[0] == $1[1] - self.write(IAC + WILL + OPT_STARTTLS) - self.write(IAC + SB + OPT_STARTTLS + TLS_FOLLOWS + IAC + SE) - else - self.write(IAC + WONT + $1[1..1]) - end - '' - elsif DONT[0] == $1[0] # respond to "IAC DON'T x" with "IAC WON'T x" - self.write(IAC + WONT + $1[1..1]) - '' - elsif WILL[0] == $1[0] # respond to "IAC WILL x" - if OPT_BINARY[0] == $1[1] - self.write(IAC + DO + OPT_BINARY) - elsif OPT_ECHO[0] == $1[1] - self.write(IAC + DO + OPT_ECHO) - elsif OPT_SGA[0] == $1[1] - @telnet_option["SGA"] = true - self.write(IAC + DO + OPT_SGA) - else - self.write(IAC + DONT + $1[1..1]) - end - '' - elsif WONT[0] == $1[0] # respond to "IAC WON'T x" - if OPT_ECHO[0] == $1[1] - self.write(IAC + DONT + OPT_ECHO) - elsif OPT_SGA[0] == $1[1] - @telnet_option["SGA"] = false - self.write(IAC + DONT + OPT_SGA) - else - self.write(IAC + DONT + $1[1..1]) - end - '' - elsif SB[0] == $1[0] # respond to "IAC SB xxx IAC SE" - if OPT_STARTTLS[0] == $1[1] && TLS_FOLLOWS[0] == $2[0] - @sock = OpenSSL::SSL::SSLSocket.new(@sock) - @sock.cert_file = @options['CertFile'] - @sock.cert = @options['Cert'] unless @sock.cert - @sock.key_file = @options['KeyFile'] - @sock.key = @options['Key'] unless @sock.key - @sock.ca_cert = @options['CACert'] - @sock.ca_file = @options['CAFile'] - @sock.ca_path = @options['CAPath'] - @sock.timeout = @options['Timeout'] - @sock.verify_mode = @options['VerifyMode'] - @sock.verify_callback = @options['VerifyCallback'] - @sock.verify_depth = @options['VerifyDepth'] - @sock.connect - @ssl = true - end - '' - else - '' - end - end - end # preprocess - - alias waitfor_org waitfor - - def waitfor(options) - time_out = @options["Timeout"] - waittime = @options["Waittime"] - - if options.kind_of?(Hash) - prompt = if options.has_key?("Match") - options["Match"] - elsif options.has_key?("Prompt") - options["Prompt"] - elsif options.has_key?("String") - Regexp.new( Regexp.quote(options["String"]) ) - end - time_out = options["Timeout"] if options.has_key?("Timeout") - waittime = options["Waittime"] if options.has_key?("Waittime") - else - prompt = options - end - - if time_out == false - time_out = nil - end - - line = '' - buf = '' - @rest = '' unless @rest - - until(prompt === line and not IO::select([@sock], nil, nil, waittime)) - unless IO::select([@sock], nil, nil, time_out) - raise TimeoutError, "timed-out; wait for the next data" - end - begin - c = @rest + @sock.sysread(1024 * 1024) - @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") - if @options["Telnetmode"] - pos = 0 - catch(:next){ - while true - case c[pos] - when IAC[0] - case c[pos+1] - when DO[0], DONT[0], WILL[0], WONT[0] - throw :next unless c[pos+2] - pos += 3 - when SB[0] - ret = detect_sub_negotiation(c, pos) - throw :next unless ret - pos = ret - when nil - throw :next - else - pos += 2 - end - when nil - throw :next - else - pos += 1 - end - end - } - - buf = preprocess(c[0...pos]) - @rest = c[pos..-1] - end - @log.print(buf) if @options.has_key?("Output_log") - line.concat(buf) - yield buf if block_given? - rescue EOFError # End of file reached - if line == '' - line = nil - yield nil if block_given? - end - break - end - end - line - end - - private - - def detect_sub_negotiation(data, pos) - return nil if data.length < pos+6 # IAC SB x param IAC SE - pos += 3 - while true - case data[pos] - when IAC[0] - if data[pos+1] == SE[0] - pos += 2 - return pos - else - pos += 2 - end - when nil - return nil - else - pos += 1 - end - end - end - - end -end diff --git a/ext/openssl/lib/openssl.rb b/ext/openssl/lib/openssl.rb index 24a9eed136..98fa8d39f2 100644 --- a/ext/openssl/lib/openssl.rb +++ b/ext/openssl/lib/openssl.rb @@ -1,24 +1,41 @@ +# frozen_string_literal: true =begin -= $RCSfile$ -- Loader for all OpenSSL C-space and Ruby-space definitions - = Info 'OpenSSL for Ruby 2' project Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> All rights reserved. = Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Version - $Id$ + This program is licensed under the same licence as Ruby. + (See the file 'COPYING'.) =end require 'openssl.so' -require 'openssl/bn' -require 'openssl/cipher' -require 'openssl/digest' -require 'openssl/ssl' -require 'openssl/x509' +require_relative 'openssl/bn' +require_relative 'openssl/cipher' +require_relative 'openssl/digest' +require_relative 'openssl/hmac' +require_relative 'openssl/pkcs5' +require_relative 'openssl/pkey' +require_relative 'openssl/ssl' +require_relative 'openssl/version' +require_relative 'openssl/x509' +module OpenSSL + # :call-seq: + # OpenSSL.secure_compare(string, string) -> true or false + # + # Constant time memory comparison. Inputs are hashed using SHA-256 to mask + # the length of the secret. Returns +true+ if the strings are identical, + # +false+ otherwise. + # + # This method is expensive due to the SHA-256 hashing. In most cases, where + # the input lengths are known to be equal or are not sensitive, + # OpenSSL.fixed_length_secure_compare should be used instead. + def self.secure_compare(a, b) + hashed_a = OpenSSL::Digest.digest('SHA256', a) + hashed_b = OpenSSL::Digest.digest('SHA256', b) + OpenSSL.fixed_length_secure_compare(hashed_a, hashed_b) && a == b + end +end diff --git a/ext/openssl/lib/openssl/bn.rb b/ext/openssl/lib/openssl/bn.rb index e7cbf2cfaf..e4889a140c 100644 --- a/ext/openssl/lib/openssl/bn.rb +++ b/ext/openssl/lib/openssl/bn.rb @@ -1,35 +1,40 @@ -=begin -= $RCSfile$ -- Ruby-space definitions that completes C-space funcs for BN - -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Version - $Id$ -=end - -## -# Should we care what if somebody require this file directly? -#require 'openssl' +# frozen_string_literal: true +#-- +# +# = Ruby-space definitions that completes C-space funcs for BN +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'COPYING'.) +#++ module OpenSSL class BN include Comparable + + def pretty_print(q) + q.object_group(self) { + q.text ' ' + q.text to_i.to_s + } + end end # BN end # OpenSSL ## +#-- # Add double dispatch to Integer -# +#++ class Integer + # Casts an Integer as an OpenSSL::BN + # + # See `man bn` for more info. def to_bn OpenSSL::BN::new(self) end end # Integer - diff --git a/ext/openssl/lib/openssl/buffering.rb b/ext/openssl/lib/openssl/buffering.rb index fdbd71bc0c..1464a4292d 100644 --- a/ext/openssl/lib/openssl/buffering.rb +++ b/ext/openssl/lib/openssl/buffering.rb @@ -1,25 +1,65 @@ -=begin -= $RCSfile$ -- Buffering mix-in module. +# coding: binary +# frozen_string_literal: true +#-- +#= Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org> +# All rights reserved. +# +#= Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'COPYING'.) +#++ -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org> - All rights reserved. +## +# OpenSSL IO buffering mix-in module. +# +# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO. +# +# You typically won't use this module directly, you can see it implemented in +# OpenSSL::SSL::SSLSocket. -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) +module OpenSSL::Buffering + include Enumerable -= Version - $Id$ -=end + # A buffer which will retain binary encoding. + class Buffer < String + unless String.method_defined?(:append_as_bytes) + alias_method :_append, :<< + def append_as_bytes(string) + if string.encoding == Encoding::BINARY + _append(string) + else + _append(string.b) + end + + self + end + end + + undef_method :concat + undef_method :<< + end + + ## + # The "sync mode" of the SSLSocket. + # + # See IO#sync for full details. -module Buffering - include Enumerable attr_accessor :sync + + ## + # Default size to read from or write to the SSLSocket for buffer operations. + BLOCK_SIZE = 1024*16 - def initialize(*args) + ## + # Creates an instance of OpenSSL's buffering IO module. + + def initialize(*) + super + @eof = false + @rbuffer = Buffer.new @sync = @io.sync end @@ -28,46 +68,171 @@ module Buffering # private + ## + # Fills the buffer from the underlying SSLSocket + def fill_rbuff - @rbuffer = "" unless defined? @rbuffer begin - if self.respond_to?(:to_io) - IO.select([self.to_io], nil, nil) - end - @rbuffer << self.sysread(BLOCK_SIZE) + @rbuffer.append_as_bytes(self.sysread(BLOCK_SIZE)) + rescue Errno::EAGAIN + retry rescue EOFError @eof = true end end + ## + # Consumes _size_ bytes from the buffer + def consume_rbuff(size=nil) - if @rbuffer.size == 0 - @eof = nil + if @rbuffer.empty? nil else size = @rbuffer.size unless size - ret = @rbuffer[0, size] - @rbuffer[0, size] = "" - ret + @rbuffer.slice!(0, size) end end public - def read(size=nil) - fill_rbuff unless defined? @rbuffer - @eof ||= nil + # call-seq: + # ssl.getbyte => 81 + # + # Get the next 8bit byte from `ssl`. Returns `nil` on EOF + def getbyte + read(1)&.ord + end + + # Get the next 8bit byte. Raises EOFError on EOF + def readbyte + raise EOFError if eof? + getbyte + end + + ## + # Reads _size_ bytes from the stream. If _buf_ is provided it must + # reference a string which will receive the data. + # + # See IO#read for full details. + + def read(size=nil, buf=nil) + if size == 0 + if buf + buf.clear + return buf + else + return "" + end + end until @eof break if size && size <= @rbuffer.size fill_rbuff end - consume_rbuff(size) + ret = consume_rbuff(size) || "" + if buf + buf.replace(ret) + ret = buf + end + (size && ret.empty?) ? nil : ret end - def gets(eol=$/) - fill_rbuff unless defined? @rbuffer + ## + # Reads at most _maxlen_ bytes from the stream. If _buf_ is provided it + # must reference a string which will receive the data. + # + # See IO#readpartial for full details. + + def readpartial(maxlen, buf=nil) + if maxlen == 0 + if buf + buf.clear + return buf + else + return "" + end + end + if @rbuffer.empty? + begin + return sysread(maxlen, buf) + rescue Errno::EAGAIN + retry + end + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + ret + end + + ## + # Reads at most _maxlen_ bytes in the non-blocking manner. + # + # When no data can be read without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so read_nonblock + # should be called again when the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so read_nonblock + # should be called again after the underlying IO is writable. + # + # OpenSSL::Buffering#read_nonblock needs two rescue clause as follows: + # + # # emulates blocking read (readpartial). + # begin + # result = ssl.read_nonblock(maxlen) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that read_nonblock writes to the underlying IO is + # when the peer requests a new TLS/SSL handshake. See openssl the FAQ for + # more details. http://www.openssl.org/support/faq.html + # + # By specifying a keyword argument _exception_ to +false+, you can indicate + # that read_nonblock should not raise an IO::Wait*able exception, but + # return the symbol +:wait_writable+ or +:wait_readable+ instead. At EOF, + # it will return +nil+ instead of raising EOFError. + + def read_nonblock(maxlen, buf=nil, exception: true) + if maxlen == 0 + if buf + buf.clear + return buf + else + return "" + end + end + if @rbuffer.empty? + return sysread_nonblock(maxlen, buf, exception: exception) + end + ret = consume_rbuff(maxlen) + if buf + buf.replace(ret) + ret = buf + end + ret + end + + ## + # Reads the next "line" from the stream. Lines are separated by _eol_. If + # _limit_ is provided the result will not be longer than the given number of + # bytes. + # + # _eol_ may be a String or Regexp. + # + # Unlike IO#gets the line read will not be assigned to +$_+. + # + # Unlike IO#gets the separator must be provided if a limit is provided. + + def gets(eol=$/, limit=nil, chomp: false) idx = @rbuffer.index(eol) - @eof ||= nil until @eof break if idx fill_rbuff @@ -78,16 +243,34 @@ module Buffering else size = idx ? idx+eol.size : nil end - consume_rbuff(size) + if size && limit && limit >= 0 + size = [size, limit].min + end + line = consume_rbuff(size) + if chomp && line + line.chomp!(eol) + end + line end + ## + # Executes the block for every line in the stream where lines are separated + # by _eol_. + # + # See also #gets + def each(eol=$/) - while line = self.gets(eol?) + while line = self.gets(eol) yield line end end alias each_line each + ## + # Reads lines from the stream which are separated by _eol_. + # + # See also #gets + def readlines(eol=$/) ary = [] while line = self.gets(eol) @@ -96,34 +279,61 @@ module Buffering ary end + ## + # Reads a line from the stream which is separated by _eol_. + # + # Raises EOFError if at end of file. + def readline(eol=$/) - raise EOFErorr if eof? + raise EOFError if eof? gets(eol) end + ## + # Reads one character from the stream. Returns nil if called at end of + # file. + def getc - c = read(1) - c ? c.to_i : nil + read(1) end - def each_byte + ## + # Calls the given block once for each byte in the stream. + + def each_byte # :yields: byte while c = getc - yield(c) + yield(c.ord) end end + ## + # Reads a one-character string from the stream. Raises an EOFError at end + # of file. + def readchar - raise EOFErorr if eof? + raise EOFError if eof? getc end + ## + # Pushes character _c_ back onto the stream such that a subsequent buffered + # character read will return it. + # + # Unlike IO#getc multiple bytes may be pushed back onto the stream. + # + # Has no effect on unbuffered reads (such as #sysread). + def ungetc(c) @rbuffer[0,0] = c.chr end + ## + # Returns true if the stream is at file which means there is no more data to + # be read. + def eof? - @eof ||= nil - @eof && @rbuffer.size == 0 + fill_rbuff if !@eof && @rbuffer.empty? + @eof && @rbuffer.empty? end alias eof eof? @@ -132,65 +342,162 @@ module Buffering # private + ## + # Writes _s_ to the buffer. When the buffer is full or #sync is true the + # buffer is flushed to the underlying socket. + def do_write(s) - @wbuffer = "" unless defined? @wbuffer - @wbuffer << s + @wbuffer = Buffer.new unless defined? @wbuffer + @wbuffer.append_as_bytes(s) + @sync ||= false - if @sync or @wbuffer.size > BLOCK_SIZE or idx = @wbuffer.rindex($/) - remain = idx ? idx + $/.size : @wbuffer.length - nwritten = 0 - while remain > 0 - nwrote = syswrite(@wbuffer[nwritten,remain]) - remain -= nwrote - nwritten += nwrote + buffer_size = @wbuffer.bytesize + if @sync or buffer_size > BLOCK_SIZE + nwrote = 0 + begin + while nwrote < buffer_size do + begin + chunk = if nwrote > 0 + @wbuffer.byteslice(nwrote, @wbuffer.bytesize) + else + @wbuffer + end + + nwrote += syswrite(chunk) + rescue Errno::EAGAIN + retry + end + end + ensure + if nwrote < @wbuffer.bytesize + @wbuffer[0, nwrote] = "" + else + @wbuffer.clear + end end - @wbuffer = "" end end public - def write(s) - do_write(s) - s.length + ## + # Writes _s_ to the stream. If the argument is not a String it will be + # converted using +.to_s+ method. Returns the number of bytes written. + + def write(*s) + s.inject(0) do |written, str| + do_write(str) + written + str.bytesize + end + end + + ## + # Writes _s_ in the non-blocking manner. + # + # If there is buffered data, it is flushed first. This may block. + # + # write_nonblock returns number of bytes written to the SSL connection. + # + # When no data can be written without blocking it raises + # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable. + # + # IO::WaitReadable means SSL needs to read internally so write_nonblock + # should be called again after the underlying IO is readable. + # + # IO::WaitWritable means SSL needs to write internally so write_nonblock + # should be called again after underlying IO is writable. + # + # So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows. + # + # # emulates blocking write. + # begin + # result = ssl.write_nonblock(str) + # rescue IO::WaitReadable + # IO.select([io]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [io]) + # retry + # end + # + # Note that one reason that write_nonblock reads from the underlying IO + # is when the peer requests a new TLS/SSL handshake. See the openssl FAQ + # for more details. http://www.openssl.org/support/faq.html + # + # By specifying a keyword argument _exception_ to +false+, you can indicate + # that write_nonblock should not raise an IO::Wait*able exception, but + # return the symbol +:wait_writable+ or +:wait_readable+ instead. + + def write_nonblock(s, exception: true) + flush + syswrite_nonblock(s, exception: exception) end - def << (s) + ## + # Writes _s_ to the stream. _s_ will be converted to a String using + # +.to_s+ method. + + def <<(s) do_write(s) self end + ## + # Writes _args_ to the stream along with a record separator. + # + # See IO#puts for full details. + def puts(*args) - s = "" + s = Buffer.new + if args.empty? + s.append_as_bytes("\n") + end args.each{|arg| - s << arg.to_s - unless /#{$/}\Z/o =~ s - s << $/ - end + s.append_as_bytes(arg.to_s) + s.sub!(/(?<!\n)\z/, "\n") } do_write(s) nil end + ## + # Writes _args_ to the stream. + # + # See IO#print for full details. + def print(*args) - s = "" - args.each{ |arg| s << arg.to_s } + s = Buffer.new + args.each{ |arg| s.append_as_bytes(arg.to_s) } do_write(s) nil end + ## + # Formats and writes to the stream converting parameters under control of + # the format string. + # + # See Kernel#sprintf for format string details. + def printf(s, *args) do_write(s % args) nil end + ## + # Flushes buffered data to the SSLSocket. + def flush osync = @sync @sync = true do_write "" + return self + ensure @sync = osync end + ## + # Closes the SSLSocket and flushes any unwritten data. + def close flush rescue nil sysclose diff --git a/ext/openssl/lib/openssl/cipher.rb b/ext/openssl/lib/openssl/cipher.rb index 11153104ee..ab75ac8e1a 100644 --- a/ext/openssl/lib/openssl/cipher.rb +++ b/ext/openssl/lib/openssl/cipher.rb @@ -1,52 +1,67 @@ -=begin -= $RCSfile$ -- Ruby-space predefined Cipher subclasses - -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Version - $Id$ -=end - -## -# Should we care what if somebody require this file directly? -#require 'openssl' +# frozen_string_literal: true +#-- +# = Ruby-space predefined Cipher subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'COPYING'.) +#++ module OpenSSL - module Cipher - %w(AES Cast5 BF DES Idea RC2 RC4 RC5).each{|cipher| - eval(<<-EOD) - class #{cipher} < Cipher - def initialize(*args) - args = args.join('-') - if args.size == 0 - super(\"#{cipher}\") - else - super(\"#{cipher}-#\{args\}\") - end - end - end - EOD + class Cipher + %w(AES CAST5 BF DES IDEA RC2 RC4 RC5).each{|name| + klass = Class.new(Cipher){ + define_method(:initialize){|*args| + cipher_name = args.inject(name){|n, arg| "#{n}-#{arg}" } + super(cipher_name.downcase) + } + } + const_set(name, klass) } - class Cipher - def random_key - str = OpenSSL::Random.random_bytes(self.key_len) - self.key = str - return str - end + %w(128 192 256).each{|keylen| + klass = Class.new(Cipher){ + define_method(:initialize){|mode = "CBC"| + super("aes-#{keylen}-#{mode}".downcase) + } + } + const_set("AES#{keylen}", klass) + } + + # call-seq: + # cipher.random_key -> key + # + # Generate a random key with OpenSSL::Random.random_bytes and sets it to + # the cipher, and returns it. + # + # You must call #encrypt or #decrypt before calling this method. + def random_key + str = OpenSSL::Random.random_bytes(self.key_len) + self.key = str + end - def random_iv - str = OpenSSL::Random.random_bytes(self.iv_len) - self.iv = str - return str - end + # call-seq: + # cipher.random_iv -> iv + # + # Generate a random IV with OpenSSL::Random.random_bytes and sets it to the + # cipher, and returns it. + # + # You must call #encrypt or #decrypt before calling this method. + def random_iv + str = OpenSSL::Random.random_bytes(self.iv_len) + self.iv = str end + + # Deprecated. + # + # This class is only provided for backwards compatibility. + # Use OpenSSL::Cipher. + class Cipher < Cipher; end + deprecate_constant :Cipher end # Cipher end # OpenSSL diff --git a/ext/openssl/lib/openssl/digest.rb b/ext/openssl/lib/openssl/digest.rb index 58622f543e..4e6dea8d03 100644 --- a/ext/openssl/lib/openssl/digest.rb +++ b/ext/openssl/lib/openssl/digest.rb @@ -1,44 +1,73 @@ -=begin -= $RCSfile$ -- Ruby-space predefined Digest subclasses +# frozen_string_literal: true +#-- +# = Ruby-space predefined Digest subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'COPYING'.) +#++ -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> - All rights reserved. +module OpenSSL + class Digest -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) + # Return the hash value computed with _name_ Digest. _name_ is either the + # long name or short name of a supported digest algorithm. + # + # === Example + # + # OpenSSL::Digest.digest("SHA256", "abc") -= Version - $Id$ -=end + def self.digest(name, data) + super(data, name) + end -## -# Should we care what if somebody require this file directly? -#require 'openssl' + %w(MD4 MD5 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512).each do |name| + klass = Class.new(self) + klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def initialize(data = nil) + super("#{name}", data) + end + RUBY -module OpenSSL - module Digest - - %w(DSS DSS1 MD2 MD4 MD5 MDC2 RIPEMD160 SHA SHA1).each{|digest| - eval(<<-EOD) - class #{digest} < Digest - def initialize(data=nil) - super(\"#{digest}\", data) - end - - def #{digest}::digest(data) - Digest::digest(\"#{digest}\", data) - end - - def #{digest}::hexdigest(data) - Digest::hexdigest(\"#{digest}\", data) - end + klass.singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def digest(data) + new.digest(data) end - EOD - } + def hexdigest(data) + new.hexdigest(data) + end + RUBY + const_set(name.tr('-', '_'), klass) + end + + # Deprecated. + # + # This class is only provided for backwards compatibility. + # Use OpenSSL::Digest instead. + class Digest < Digest; end # :nodoc: + deprecate_constant :Digest end # Digest -end # OpenSSL + # Returns a Digest subclass by _name_ + # + # require 'openssl' + # + # OpenSSL::Digest("MD5") + # # => OpenSSL::Digest::MD5 + # + # OpenSSL::Digest("Foo") + # # => NameError: wrong constant name Foo + + def Digest(name) + OpenSSL::Digest.const_get(name) + end + + module_function :Digest + +end # OpenSSL diff --git a/ext/openssl/lib/openssl/hmac.rb b/ext/openssl/lib/openssl/hmac.rb new file mode 100644 index 0000000000..c8c844d8d7 --- /dev/null +++ b/ext/openssl/lib/openssl/hmac.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module OpenSSL + class HMAC + # Securely compare with another HMAC instance in constant time. + def ==(other) + return false unless HMAC === other + return false unless self.digest.bytesize == other.digest.bytesize + + OpenSSL.fixed_length_secure_compare(self.digest, other.digest) + end + + # :call-seq: + # hmac.base64digest -> string + # + # Returns the authentication code an a Base64-encoded string. + def base64digest + [digest].pack("m0") + end + + class << self + # :call-seq: + # HMAC.digest(digest, key, data) -> aString + # + # Returns the authentication code as a binary string. The _digest_ parameter + # specifies the digest algorithm to use. This may be a String representing + # the algorithm name or an instance of OpenSSL::Digest. + # + # === Example + # key = 'key' + # data = 'The quick brown fox jumps over the lazy dog' + # + # hmac = OpenSSL::HMAC.digest('SHA1', key, data) + # #=> "\xDE|\x9B\x85\xB8\xB7\x8A\xA6\xBC\x8Az6\xF7\n\x90p\x1C\x9D\xB4\xD9" + def digest(digest, key, data) + hmac = new(key, digest) + hmac << data + hmac.digest + end + + # :call-seq: + # HMAC.hexdigest(digest, key, data) -> aString + # + # Returns the authentication code as a hex-encoded string. The _digest_ + # parameter specifies the digest algorithm to use. This may be a String + # representing the algorithm name or an instance of OpenSSL::Digest. + # + # === Example + # key = 'key' + # data = 'The quick brown fox jumps over the lazy dog' + # + # hmac = OpenSSL::HMAC.hexdigest('SHA1', key, data) + # #=> "de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9" + def hexdigest(digest, key, data) + hmac = new(key, digest) + hmac << data + hmac.hexdigest + end + + # :call-seq: + # HMAC.base64digest(digest, key, data) -> aString + # + # Returns the authentication code as a Base64-encoded string. The _digest_ + # parameter specifies the digest algorithm to use. This may be a String + # representing the algorithm name or an instance of OpenSSL::Digest. + # + # === Example + # key = 'key' + # data = 'The quick brown fox jumps over the lazy dog' + # + # hmac = OpenSSL::HMAC.base64digest('SHA1', key, data) + # #=> "3nybhbi3iqa8ino29wqQcBydtNk=" + def base64digest(digest, key, data) + [digest(digest, key, data)].pack("m0") + end + end + end +end diff --git a/ext/openssl/lib/openssl/marshal.rb b/ext/openssl/lib/openssl/marshal.rb new file mode 100644 index 0000000000..eb8eda4748 --- /dev/null +++ b/ext/openssl/lib/openssl/marshal.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +#-- +# = Ruby-space definitions to add DER (de)serialization to classes +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'COPYING'.) +#++ +module OpenSSL + module Marshal + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def _load(string) + new(string) + end + end + + def _dump(_level) + to_der + end + end +end diff --git a/ext/openssl/lib/openssl/pkcs5.rb b/ext/openssl/lib/openssl/pkcs5.rb new file mode 100644 index 0000000000..8dedc4beef --- /dev/null +++ b/ext/openssl/lib/openssl/pkcs5.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +#-- +# Ruby/OpenSSL Project +# Copyright (C) 2017 Ruby/OpenSSL Project Authors +#++ + +module OpenSSL + module PKCS5 + module_function + + # OpenSSL::PKCS5.pbkdf2_hmac has been renamed to OpenSSL::KDF.pbkdf2_hmac. + # This method is provided for backwards compatibility. + def pbkdf2_hmac(pass, salt, iter, keylen, digest) + OpenSSL::KDF.pbkdf2_hmac(pass, salt: salt, iterations: iter, + length: keylen, hash: digest) + end + + def pbkdf2_hmac_sha1(pass, salt, iter, keylen) + pbkdf2_hmac(pass, salt, iter, keylen, "sha1") + end + end +end diff --git a/ext/openssl/lib/openssl/pkey.rb b/ext/openssl/lib/openssl/pkey.rb new file mode 100644 index 0000000000..39871e15dd --- /dev/null +++ b/ext/openssl/lib/openssl/pkey.rb @@ -0,0 +1,493 @@ +# frozen_string_literal: true +#-- +# Ruby/OpenSSL Project +# Copyright (C) 2017 Ruby/OpenSSL Project Authors +#++ + +require_relative 'marshal' + +module OpenSSL::PKey + # Alias of PKeyError. Before version 4.0.0, this was a subclass of PKeyError. + DHError = PKeyError + + class DH + include OpenSSL::Marshal + + # :call-seq: + # dh.public_key -> dhnew + # + # Returns a new DH instance that carries just the \DH parameters. + # + # Contrary to the method name, the returned DH object contains only + # parameters and not the public key. + # + # This method is provided for backwards compatibility. In most cases, there + # is no need to call this method. + # + # For the purpose of re-generating the key pair while keeping the + # parameters, check OpenSSL::PKey.generate_key. + # + # Example: + # # OpenSSL::PKey::DH.generate by default generates a random key pair + # dh1 = OpenSSL::PKey::DH.generate(2048) + # p dh1.priv_key #=> #<OpenSSL::BN 1288347...> + # dhcopy = dh1.public_key + # p dhcopy.priv_key #=> nil + def public_key + DH.new(to_der) + end + + # :call-seq: + # dh.params -> hash + # + # Stores all parameters of key to a Hash. + # + # The hash has keys 'p', 'q', 'g', 'pub_key', and 'priv_key'. + def params + %w{p q g pub_key priv_key}.map { |name| + [name, send(name)] + }.to_h + end + + # :call-seq: + # dh.compute_key(pub_bn) -> string + # + # Returns a String containing a shared secret computed from the other + # party's public value. + # + # This method is provided for backwards compatibility, and calls #derive + # internally. + # + # === Parameters + # * _pub_bn_ is a OpenSSL::BN, *not* the DH instance returned by + # DH#public_key as that contains the DH parameters only. + def compute_key(pub_bn) + # FIXME: This is constructing an X.509 SubjectPublicKeyInfo and is very + # inefficient + obj = OpenSSL::ASN1.Sequence([ + OpenSSL::ASN1.Sequence([ + OpenSSL::ASN1.ObjectId("dhKeyAgreement"), + OpenSSL::ASN1.Sequence([ + OpenSSL::ASN1.Integer(p), + OpenSSL::ASN1.Integer(g), + ]), + ]), + OpenSSL::ASN1.BitString(OpenSSL::ASN1.Integer(pub_bn).to_der), + ]) + derive(OpenSSL::PKey.read(obj.to_der)) + end + + # :call-seq: + # dh.generate_key! -> self + # + # Generates a private and public key unless a private key already exists. + # If this DH instance was generated from public \DH parameters (e.g. by + # encoding the result of DH#public_key), then this method needs to be + # called first in order to generate the per-session keys before performing + # the actual key exchange. + # + # <b>Deprecated in version 3.0</b>. This method is incompatible with + # OpenSSL 3.0.0 or later. + # + # See also OpenSSL::PKey.generate_key. + # + # Example: + # # DEPRECATED USAGE: This will not work on OpenSSL 3.0 or later + # dh0 = OpenSSL::PKey::DH.new(2048) + # dh = dh0.public_key # #public_key only copies the DH parameters (contrary to the name) + # dh.generate_key! + # puts dh.private? # => true + # puts dh0.pub_key == dh.pub_key #=> false + # + # # With OpenSSL::PKey.generate_key + # dh0 = OpenSSL::PKey::DH.new(2048) + # dh = OpenSSL::PKey.generate_key(dh0) + # puts dh0.pub_key == dh.pub_key #=> false + def generate_key! + if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 + raise PKeyError, "OpenSSL::PKey::DH is immutable on OpenSSL 3.0; " \ + "use OpenSSL::PKey.generate_key instead" + end + + unless priv_key + tmp = OpenSSL::PKey.generate_key(self) + set_key(tmp.pub_key, tmp.priv_key) + end + self + end + + class << self + # :call-seq: + # DH.generate(size, generator = 2) -> dh + # + # Creates a new DH instance from scratch by generating random parameters + # and a key pair. + # + # See also OpenSSL::PKey.generate_parameters and + # OpenSSL::PKey.generate_key. + # + # +size+:: + # The desired key size in bits. + # +generator+:: + # The generator. + def generate(size, generator = 2, &blk) + dhparams = OpenSSL::PKey.generate_parameters("DH", { + "dh_paramgen_prime_len" => size, + "dh_paramgen_generator" => generator, + }, &blk) + OpenSSL::PKey.generate_key(dhparams) + end + + # Handle DH.new(size, generator) form here; new(str) and new() forms + # are handled by #initialize + def new(*args, &blk) # :nodoc: + if args[0].is_a?(Integer) + generate(*args, &blk) + else + super + end + end + end + end + + # Alias of PKeyError. Before version 4.0.0, this was a subclass of PKeyError. + DSAError = PKeyError + + class DSA + include OpenSSL::Marshal + + # :call-seq: + # dsa.public_key -> dsanew + # + # Returns a new DSA instance that carries just the \DSA parameters and the + # public key. + # + # This method is provided for backwards compatibility. In most cases, there + # is no need to call this method. + # + # For the purpose of serializing the public key, to PEM or DER encoding of + # X.509 SubjectPublicKeyInfo format, check PKey#public_to_pem and + # PKey#public_to_der. + def public_key + OpenSSL::PKey.read(public_to_der) + end + + # :call-seq: + # dsa.params -> hash + # + # Stores all parameters of key to a Hash. + # + # The hash has keys 'p', 'q', 'g', 'pub_key', and 'priv_key'. + def params + %w{p q g pub_key priv_key}.map { |name| + [name, send(name)] + }.to_h + end + + class << self + # :call-seq: + # DSA.generate(size) -> dsa + # + # Creates a new DSA instance by generating a private/public key pair + # from scratch. + # + # See also OpenSSL::PKey.generate_parameters and + # OpenSSL::PKey.generate_key. + # + # +size+:: + # The desired key size in bits. + def generate(size, &blk) + # FIPS 186-4 specifies four (L,N) pairs: (1024,160), (2048,224), + # (2048,256), and (3072,256). + # + # q size is derived here with compatibility with + # DSA_generator_parameters_ex() which previous versions of ruby/openssl + # used to call. + qsize = size >= 2048 ? 256 : 160 + dsaparams = OpenSSL::PKey.generate_parameters("DSA", { + "dsa_paramgen_bits" => size, + "dsa_paramgen_q_bits" => qsize, + }, &blk) + OpenSSL::PKey.generate_key(dsaparams) + end + + # Handle DSA.new(size) form here; new(str) and new() forms + # are handled by #initialize + def new(*args, &blk) # :nodoc: + if args[0].is_a?(Integer) + generate(*args, &blk) + else + super + end + end + end + + # :call-seq: + # dsa.syssign(string) -> string + # + # Computes and returns the \DSA signature of +string+, where +string+ is + # expected to be an already-computed message digest of the original input + # data. The signature is issued using the private key of this DSA instance. + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#sign_raw and PKey::PKey#verify_raw instead. + # + # +string+:: + # A message digest of the original input data to be signed. + # + # Example: + # dsa = OpenSSL::PKey::DSA.new(2048) + # doc = "Sign me" + # digest = OpenSSL::Digest.digest('SHA1', doc) + # + # # With legacy #syssign and #sysverify: + # sig = dsa.syssign(digest) + # p dsa.sysverify(digest, sig) #=> true + # + # # With #sign_raw and #verify_raw: + # sig = dsa.sign_raw(nil, digest) + # p dsa.verify_raw(nil, sig, digest) #=> true + def syssign(string) + q or raise PKeyError, "incomplete DSA" + private? or raise PKeyError, "Private DSA key needed!" + sign_raw(nil, string) + end + + # :call-seq: + # dsa.sysverify(digest, sig) -> true | false + # + # Verifies whether the signature is valid given the message digest input. + # It does so by validating +sig+ using the public key of this DSA instance. + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#sign_raw and PKey::PKey#verify_raw instead. + # + # +digest+:: + # A message digest of the original input data to be signed. + # +sig+:: + # A \DSA signature value. + def sysverify(digest, sig) + verify_raw(nil, sig, digest) + end + end + + if defined?(EC) + # Alias of PKeyError. Before version 4.0.0, this was a subclass of PKeyError. + ECError = PKeyError + + class EC + include OpenSSL::Marshal + + # :call-seq: + # key.dsa_sign_asn1(data) -> String + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#sign_raw and PKey::PKey#verify_raw instead. + def dsa_sign_asn1(data) + sign_raw(nil, data) + end + + # :call-seq: + # key.dsa_verify_asn1(data, sig) -> true | false + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#sign_raw and PKey::PKey#verify_raw instead. + def dsa_verify_asn1(data, sig) + verify_raw(nil, sig, data) + end + + # :call-seq: + # ec.dh_compute_key(pubkey) -> string + # + # Derives a shared secret by ECDH. _pubkey_ must be an instance of + # OpenSSL::PKey::EC::Point and must belong to the same group. + # + # This method is provided for backwards compatibility, and calls #derive + # internally. + def dh_compute_key(pubkey) + obj = OpenSSL::ASN1.Sequence([ + OpenSSL::ASN1.Sequence([ + OpenSSL::ASN1.ObjectId("id-ecPublicKey"), + group.to_der, + ]), + OpenSSL::ASN1.BitString(pubkey.to_octet_string(:uncompressed)), + ]) + derive(OpenSSL::PKey.read(obj.to_der)) + end + end + + class EC::Point + # :call-seq: + # point.to_bn([conversion_form]) -> OpenSSL::BN + # + # Returns the octet string representation of the EC point as an instance of + # OpenSSL::BN. + # + # If _conversion_form_ is not given, the _point_conversion_form_ attribute + # set to the group is used. + # + # See #to_octet_string for more information. + def to_bn(conversion_form = group.point_conversion_form) + OpenSSL::BN.new(to_octet_string(conversion_form), 2) + end + end + end + + # Alias of PKeyError. Before version 4.0.0, this was a subclass of PKeyError. + RSAError = PKeyError + + class RSA + include OpenSSL::Marshal + + # :call-seq: + # rsa.public_key -> rsanew + # + # Returns a new RSA instance that carries just the public key components. + # + # This method is provided for backwards compatibility. In most cases, there + # is no need to call this method. + # + # For the purpose of serializing the public key, to PEM or DER encoding of + # X.509 SubjectPublicKeyInfo format, check PKey#public_to_pem and + # PKey#public_to_der. + def public_key + OpenSSL::PKey.read(public_to_der) + end + + # :call-seq: + # rsa.params -> hash + # + # Stores all parameters of key to a Hash. + # + # The hash has keys 'n', 'e', 'd', 'p', 'q', 'dmp1', 'dmq1', and 'iqmp'. + def params + %w{n e d p q dmp1 dmq1 iqmp}.map { |name| + [name, send(name)] + }.to_h + end + + class << self + # :call-seq: + # RSA.generate(size, exponent = 65537) -> RSA + # + # Generates an \RSA keypair. + # + # See also OpenSSL::PKey.generate_key. + # + # +size+:: + # The desired key size in bits. + # +exponent+:: + # An odd Integer, normally 3, 17, or 65537. + def generate(size, exp = 0x10001, &blk) + OpenSSL::PKey.generate_key("RSA", { + "rsa_keygen_bits" => size, + "rsa_keygen_pubexp" => exp, + }, &blk) + end + + # Handle RSA.new(size, exponent) form here; new(str) and new() forms + # are handled by #initialize + def new(*args, &blk) # :nodoc: + if args[0].is_a?(Integer) + generate(*args, &blk) + else + super + end + end + end + + # :call-seq: + # rsa.private_encrypt(string) -> String + # rsa.private_encrypt(string, padding) -> String + # + # Encrypt +string+ with the private key. +padding+ defaults to + # PKCS1_PADDING, which is known to be insecure but is kept for backwards + # compatibility. The encrypted string output can be decrypted using + # #public_decrypt. + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#sign_raw and PKey::PKey#verify_raw, and + # PKey::PKey#verify_recover instead. + def private_encrypt(string, padding = PKCS1_PADDING) + n or raise PKeyError, "incomplete RSA" + private? or raise PKeyError, "private key needed." + sign_raw(nil, string, { + "rsa_padding_mode" => translate_padding_mode(padding), + }) + end + + # :call-seq: + # rsa.public_decrypt(string) -> String + # rsa.public_decrypt(string, padding) -> String + # + # Decrypt +string+, which has been encrypted with the private key, with the + # public key. +padding+ defaults to PKCS1_PADDING which is known to be + # insecure but is kept for backwards compatibility. + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#sign_raw and PKey::PKey#verify_raw, and + # PKey::PKey#verify_recover instead. + def public_decrypt(string, padding = PKCS1_PADDING) + n or raise PKeyError, "incomplete RSA" + verify_recover(nil, string, { + "rsa_padding_mode" => translate_padding_mode(padding), + }) + end + + # :call-seq: + # rsa.public_encrypt(string) -> String + # rsa.public_encrypt(string, padding) -> String + # + # Encrypt +string+ with the public key. +padding+ defaults to + # PKCS1_PADDING, which is known to be insecure but is kept for backwards + # compatibility. The encrypted string output can be decrypted using + # #private_decrypt. + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#encrypt and PKey::PKey#decrypt instead. + def public_encrypt(data, padding = PKCS1_PADDING) + n or raise PKeyError, "incomplete RSA" + encrypt(data, { + "rsa_padding_mode" => translate_padding_mode(padding), + }) + end + + # :call-seq: + # rsa.private_decrypt(string) -> String + # rsa.private_decrypt(string, padding) -> String + # + # Decrypt +string+, which has been encrypted with the public key, with the + # private key. +padding+ defaults to PKCS1_PADDING, which is known to be + # insecure but is kept for backwards compatibility. + # + # <b>Deprecated in version 3.0</b>. + # Consider using PKey::PKey#encrypt and PKey::PKey#decrypt instead. + def private_decrypt(data, padding = PKCS1_PADDING) + n or raise PKeyError, "incomplete RSA" + private? or raise PKeyError, "private key needed." + decrypt(data, { + "rsa_padding_mode" => translate_padding_mode(padding), + }) + end + + PKCS1_PADDING = 1 + SSLV23_PADDING = 2 + NO_PADDING = 3 + PKCS1_OAEP_PADDING = 4 + + private def translate_padding_mode(num) + case num + when PKCS1_PADDING + "pkcs1" + when SSLV23_PADDING + "sslv23" + when NO_PADDING + "none" + when PKCS1_OAEP_PADDING + "oaep" + else + raise PKeyError, "unsupported padding mode" + end + end + end +end diff --git a/ext/openssl/lib/openssl/ssl.rb b/ext/openssl/lib/openssl/ssl.rb index 811a935509..3268c126b9 100644 --- a/ext/openssl/lib/openssl/ssl.rb +++ b/ext/openssl/lib/openssl/ssl.rb @@ -1,24 +1,184 @@ +# frozen_string_literal: true =begin -= $RCSfile$ -- Ruby-space definitions that completes C-space funcs for SSL - = Info 'OpenSSL for Ruby 2' project Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org> All rights reserved. = Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Version - $Id$ + This program is licensed under the same licence as Ruby. + (See the file 'COPYING'.) =end -require 'openssl/buffering' +require "openssl/buffering" + +if defined?(OpenSSL::SSL) + +require "io/nonblock" +require "ipaddr" +require "socket" module OpenSSL module SSL + class SSLContext + DEFAULT_PARAMS = { # :nodoc: + :verify_mode => OpenSSL::SSL::VERIFY_PEER, + :verify_hostname => true, + :options => -> { + opts = OpenSSL::SSL::OP_ALL + opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS + opts |= OpenSSL::SSL::OP_NO_COMPRESSION + opts + }.call + } + + if !OpenSSL::OPENSSL_VERSION.start_with?("OpenSSL") + DEFAULT_PARAMS.merge!( + min_version: OpenSSL::SSL::TLS1_VERSION, + ciphers: %w{ + ECDHE-ECDSA-AES128-GCM-SHA256 + ECDHE-RSA-AES128-GCM-SHA256 + ECDHE-ECDSA-AES256-GCM-SHA384 + ECDHE-RSA-AES256-GCM-SHA384 + DHE-RSA-AES128-GCM-SHA256 + DHE-DSS-AES128-GCM-SHA256 + DHE-RSA-AES256-GCM-SHA384 + DHE-DSS-AES256-GCM-SHA384 + ECDHE-ECDSA-AES128-SHA256 + ECDHE-RSA-AES128-SHA256 + ECDHE-ECDSA-AES128-SHA + ECDHE-RSA-AES128-SHA + ECDHE-ECDSA-AES256-SHA384 + ECDHE-RSA-AES256-SHA384 + ECDHE-ECDSA-AES256-SHA + ECDHE-RSA-AES256-SHA + DHE-RSA-AES128-SHA256 + DHE-RSA-AES256-SHA256 + DHE-RSA-AES128-SHA + DHE-RSA-AES256-SHA + DHE-DSS-AES128-SHA256 + DHE-DSS-AES256-SHA256 + DHE-DSS-AES128-SHA + DHE-DSS-AES256-SHA + AES128-GCM-SHA256 + AES256-GCM-SHA384 + AES128-SHA256 + AES256-SHA256 + AES128-SHA + AES256-SHA + }.join(":").freeze, + ) + end + DEFAULT_PARAMS.freeze + + DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc: + DEFAULT_CERT_STORE.set_default_paths + + # A callback invoked at connect time to distinguish between multiple + # server names. + # + # The callback is invoked with an SSLSocket and a server name. The + # callback must return an SSLContext for the server name or nil. + attr_accessor :servername_cb + + # call-seq: + # SSLContext.new -> ctx + # SSLContext.new(:TLSv1) -> ctx + # SSLContext.new("SSLv23") -> ctx + # + # Creates a new SSL context. + # + # If an argument is given, #ssl_version= is called with the value. Note + # that this form is deprecated. New applications should use #min_version= + # and #max_version= as necessary. + def initialize(version = nil) + self.ssl_version = version if version + self.verify_mode = OpenSSL::SSL::VERIFY_NONE + self.verify_hostname = false + end + + ## + # call-seq: + # ctx.set_params(params = {}) -> params + # + # Sets saner defaults optimized for the use with HTTP-like protocols. + # + # If a Hash _params_ is given, the parameters are overridden with it. + # The keys in _params_ must be assignment methods on SSLContext. + # + # If the verify_mode is not VERIFY_NONE and ca_file, ca_path and + # cert_store are not set then the system default certificate store is + # used. + def set_params(params={}) + params = DEFAULT_PARAMS.merge(params) + self.options |= params.delete(:options) # set before min_version/max_version + params.each{|name, value| self.__send__("#{name}=", value) } + if self.verify_mode != OpenSSL::SSL::VERIFY_NONE + unless self.ca_file or self.ca_path or self.cert_store + if not defined?(Ractor) or Ractor.current == Ractor.main + self.cert_store = DEFAULT_CERT_STORE + else + self.cert_store = Ractor.current[:__openssl_default_store__] ||= + OpenSSL::X509::Store.new.tap { |store| + store.set_default_paths + } + end + end + end + return params + end + + # call-seq: + # ctx.ssl_version = :TLSv1 + # ctx.ssl_version = "SSLv23" + # + # Sets the SSL/TLS protocol version for the context. This forces + # connections to use only the specified protocol version. This is + # deprecated and only provided for backwards compatibility. Use + # #min_version= and #max_version= instead. + # + # === History + # As the name hints, this used to call the SSL_CTX_set_ssl_version() + # function which sets the SSL method used for connections created from + # the context. As of Ruby/OpenSSL 2.1, this accessor method is + # implemented to call #min_version= and #max_version= instead. + def ssl_version=(meth) + meth = meth.to_s if meth.is_a?(Symbol) + if /(?<type>_client|_server)\z/ =~ meth + meth = $` + if $VERBOSE + warn "#{caller(1, 1)[0]}: method type #{type.inspect} is ignored" + end + end + version = METHODS_MAP[meth.intern] or + raise ArgumentError, "unknown SSL method `%s'" % meth + self.min_version = self.max_version = version + end + + METHODS_MAP = { + SSLv23: 0, + SSLv2: OpenSSL::SSL::SSL2_VERSION, + SSLv3: OpenSSL::SSL::SSL3_VERSION, + TLSv1: OpenSSL::SSL::TLS1_VERSION, + TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION, + TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION, + }.freeze + private_constant :METHODS_MAP + + # The list of available SSL/TLS methods. This constant is only provided + # for backwards compatibility. + METHODS = METHODS_MAP.flat_map { |name,| + [name, :"#{name}_client", :"#{name}_server"] + }.freeze + deprecate_constant :METHODS + end + module SocketForwarder + # The file descriptor for the socket. + def fileno + to_io.fileno + end + def addr to_io.addr end @@ -27,12 +187,20 @@ module OpenSSL to_io.peeraddr end - def getsockopt(level, optname, optval) + def local_address + to_io.local_address + end + + def remote_address + to_io.remote_address + end + + def setsockopt(level, optname, optval) to_io.setsockopt(level, optname, optval) end - def setsockopt(level, optname) - to_io.setsockopt(level, optname) + def getsockopt(level, optname) + to_io.getsockopt(level, optname) end def fcntl(*args) @@ -42,47 +210,333 @@ module OpenSSL def closed? to_io.closed? end + + def do_not_reverse_lookup=(flag) + to_io.do_not_reverse_lookup = flag + end + + def close_on_exec=(value) + to_io.close_on_exec = value + end + + def close_on_exec? + to_io.close_on_exec? + end + + def wait(*args) + to_io.wait(*args) + end + + def wait_readable(*args) + to_io.wait_readable(*args) + end + + def wait_writable(*args) + to_io.wait_writable(*args) + end + + if IO.method_defined?(:timeout) + def timeout + to_io.timeout + end + + def timeout=(value) + to_io.timeout=(value) + end + end + end + + def verify_certificate_identity(cert, hostname) + should_verify_common_name = true + cert.extensions.each{|ext| + next if ext.oid != "subjectAltName" + ostr = OpenSSL::ASN1.decode(ext.to_der).value.last + sequence = OpenSSL::ASN1.decode(ostr.value) + sequence.value.each{|san| + case san.tag + when 2 # dNSName in GeneralName (RFC5280) + should_verify_common_name = false + return true if verify_hostname(hostname, san.value) + when 7 # iPAddress in GeneralName (RFC5280) + should_verify_common_name = false + if san.value.size == 4 || san.value.size == 16 + begin + return true if san.value == IPAddr.new(hostname).hton + rescue IPAddr::InvalidAddressError + end + end + end + } + } + if should_verify_common_name + cert.subject.to_a.each{|oid, value| + if oid == "CN" + return true if verify_hostname(hostname, value) + end + } + end + return false + end + module_function :verify_certificate_identity + + def verify_hostname(hostname, san) # :nodoc: + # RFC 5280, IA5String is limited to the set of ASCII characters + return false unless san.ascii_only? + return false unless hostname.ascii_only? + + # See RFC 6125, section 6.4.1 + # Matching is case-insensitive. + san_parts = san.downcase.split(".") + + # TODO: this behavior should probably be more strict + return san == hostname if san_parts.size < 2 + + # Matching is case-insensitive. + host_parts = hostname.downcase.split(".") + + # RFC 6125, section 6.4.3, subitem 2. + # If the wildcard character is the only character of the left-most + # label in the presented identifier, the client SHOULD NOT compare + # against anything but the left-most label of the reference + # identifier (e.g., *.example.com would match foo.example.com but + # not bar.foo.example.com or example.com). + return false unless san_parts.size == host_parts.size + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in + # which the wildcard character comprises a label other than the + # left-most label (e.g., do not match bar.*.example.net). + return false unless verify_wildcard(host_parts.shift, san_parts.shift) + + san_parts.join(".") == host_parts.join(".") + end + module_function :verify_hostname + + def verify_wildcard(domain_component, san_component) # :nodoc: + parts = san_component.split("*", -1) + + return false if parts.size > 2 + return san_component == domain_component if parts.size == 1 + + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + return false if domain_component.start_with?("xn--") && san_component != "*" + + parts[0].length + parts[1].length < domain_component.length && + domain_component.start_with?(parts[0]) && + domain_component.end_with?(parts[1]) end + module_function :verify_wildcard class SSLSocket include Buffering include SocketForwarder + + attr_reader :hostname + + # The underlying IO object. + attr_reader :io + alias :to_io :io + + # The SSLContext object used in this connection. + attr_reader :context + + # Whether to close the underlying socket as well, when the SSL/TLS + # connection is shut down. This defaults to +false+. + attr_accessor :sync_close + + # call-seq: + # ssl.sysclose => nil + # + # Sends "close notify" to the peer and tries to shut down the SSL + # connection gracefully. + # + # If sync_close is set to +true+, the underlying IO is also closed. + def sysclose + return if closed? + stop + io.close if sync_close + end + + # call-seq: + # ssl.post_connection_check(hostname) -> true + # + # Perform hostname verification following RFC 6125. + # + # This method MUST be called after calling #connect to ensure that the + # hostname of a remote peer has been verified. + def post_connection_check(hostname) + if peer_cert.nil? + msg = "Peer verification enabled, but no certificate received." + if using_anon_cipher? + msg += " Anonymous cipher suite #{cipher[0]} was negotiated. " \ + "Anonymous suites must be disabled to use peer verification." + end + raise SSLError, msg + end + + unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname) + raise SSLError, "hostname \"#{hostname}\" does not match the server certificate" + end + return true + end + + # call-seq: + # ssl.session -> aSession + # + # Returns the SSLSession object currently used, or nil if the session is + # not established. + def session + SSL::Session.new(self) + rescue SSL::Session::SessionError + nil + end + + # Close the stream for reading. + # This method is ignored by OpenSSL as there is no reasonable way to + # implement it, but exists for compatibility with IO. + def close_read + # Unsupported and ignored. + # Just don't read any more. + end + + # Closes the stream for writing. The behavior of this method depends on + # the version of OpenSSL and the TLS protocol in use. + # + # - Sends a 'close_notify' alert to the peer. + # - Does not wait for the peer's 'close_notify' alert in response. + # + # In TLS 1.2 and earlier: + # - On receipt of a 'close_notify' alert, responds with a 'close_notify' + # alert of its own and close down the connection immediately, + # discarding any pending writes. + # + # Therefore, on TLS 1.2, this method will cause the connection to be + # completely shut down. On TLS 1.3, the connection will remain open for + # reading only. + def close_write + stop + end + + private + + def using_anon_cipher? + ctx = OpenSSL::SSL::SSLContext.new + ctx.ciphers = "aNULL" + ctx.ciphers.include?(cipher) + end + + def client_cert_cb + @context.client_cert_cb + end + + def session_new_cb + @context.session_new_cb + end + + def session_get_cb + @context.session_get_cb + end + + class << self + + # call-seq: + # open(remote_host, remote_port, local_host=nil, local_port=nil, context: nil) + # + # Creates a new instance of SSLSocket. + # _remote\_host_ and _remote\_port_ are used to open TCPSocket. + # If _local\_host_ and _local\_port_ are specified, + # then those parameters are used on the local end to establish the connection. + # If _context_ is provided, + # the SSL Sockets initial params will be taken from the context. + # + # === Examples + # + # sock = OpenSSL::SSL::SSLSocket.open('localhost', 443) + # sock.connect # Initiates a connection to localhost:443 + # + # with SSLContext: + # + # ctx = OpenSSL::SSL::SSLContext.new + # sock = OpenSSL::SSL::SSLSocket.open('localhost', 443, context: ctx) + # sock.connect # Initiates a connection to localhost:443 with SSLContext + def open(remote_host, remote_port, local_host=nil, local_port=nil, context: nil) + sock = ::TCPSocket.open(remote_host, remote_port, local_host, local_port) + if context.nil? + return OpenSSL::SSL::SSLSocket.new(sock) + else + return OpenSSL::SSL::SSLSocket.new(sock, context) + end + end + end end + ## + # SSLServer represents a TCP/IP server socket with Secure Sockets Layer. class SSLServer include SocketForwarder + # When true then #accept works exactly the same as TCPServer#accept attr_accessor :start_immediately + # Creates a new instance of SSLServer. + # * _srv_ is an instance of TCPServer. + # * _ctx_ is an instance of OpenSSL::SSL::SSLContext. def initialize(svr, ctx) @svr = svr @ctx = ctx + unless ctx.session_id_context + # see #6137 - session id may not exceed 32 bytes + prng = ::Random.new($0.hash) + session_id = prng.bytes(16).unpack1('H*') + @ctx.session_id_context = session_id + end @start_immediately = true end + # Returns the TCPServer passed to the SSLServer when initialized. def to_io @svr end - def listen(basklog=5) + # See TCPServer#listen for details. + def listen(backlog=Socket::SOMAXCONN) @svr.listen(backlog) end + # See BasicSocket#shutdown for details. + def shutdown(how=Socket::SHUT_RDWR) + @svr.shutdown(how) + end + + # Works similar to TCPServer#accept. def accept - sock = @svr.accept + # Socket#accept returns [socket, addrinfo]. + # TCPServer#accept returns a socket. + # The following comma strips addrinfo. + sock, = @svr.accept begin ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx) ssl.sync_close = true ssl.accept if @start_immediately ssl - rescue SSLError => ex - sock.close + rescue Exception => ex + if ssl + ssl.close + else + sock.close + end raise ex end end + # See IO#close for details. def close @svr.close end end end end + +end diff --git a/ext/openssl/lib/openssl/version.rb b/ext/openssl/lib/openssl/version.rb new file mode 100644 index 0000000000..395a720a31 --- /dev/null +++ b/ext/openssl/lib/openssl/version.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module OpenSSL + # The version string of Ruby/OpenSSL. + VERSION = "4.0.2" +end diff --git a/ext/openssl/lib/openssl/x509.rb b/ext/openssl/lib/openssl/x509.rb index 40b1a6dd15..66765ffeab 100644 --- a/ext/openssl/lib/openssl/x509.rb +++ b/ext/openssl/lib/openssl/x509.rb @@ -1,22 +1,18 @@ -=begin -= $RCSfile$ -- Ruby-space definitions that completes C-space funcs for X509 and subclasses +# frozen_string_literal: true +#-- +# = Ruby-space definitions that completes C-space funcs for X509 and subclasses +# +# = Info +# 'OpenSSL for Ruby 2' project +# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> +# All rights reserved. +# +# = Licence +# This program is licensed under the same licence as Ruby. +# (See the file 'COPYING'.) +#++ -= Info - 'OpenSSL for Ruby 2' project - Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz> - All rights reserved. - -= Licence - This program is licenced under the same licence as Ruby. - (See the file 'LICENCE'.) - -= Version - $Id$ -=end - -## -# Should we care what if somebody require this file directly? -#require 'openssl' +require_relative 'marshal' module OpenSSL module X509 @@ -30,7 +26,7 @@ module OpenSSL end def create_ext_from_array(ary) - raise ExtensionError, "unexpected array form" if ary.size > 3 + raise ExtensionError, "unexpected array form" if ary.size > 3 create_ext(ary[0], ary[1], ary[2]) end @@ -40,20 +36,27 @@ module OpenSSL value.strip! create_ext(oid, value) end - + def create_ext_from_hash(hash) create_ext(hash["oid"], hash["value"], hash["critical"]) end - end # ExtensionFactory - + end + class Extension + include OpenSSL::Marshal + + def ==(other) + return false unless Extension === other + to_der == other.to_der + end + def to_s # "oid = critical, value" str = self.oid str << " = " str << "critical, " if self.critical? str << self.value.gsub(/\n/, ", ") end - + def to_h # {"oid"=>sn|ln, "value"=>value, "critical"=>true|false} {"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?} end @@ -61,14 +64,337 @@ module OpenSSL def to_a [ self.oid, self.value, self.critical? ] end - end # Extension - + + module Helpers + def find_extension(oid) + extensions.find { |e| e.oid == oid } + end + end + + module SubjectKeyIdentifier + include Helpers + + # Get the subject's key identifier from the subjectKeyIdentifier + # exteension, as described in RFC5280 Section 4.2.1.2. + # + # Returns the binary String key identifier or nil or raises + # ASN1::ASN1Error. + def subject_key_identifier + ext = find_extension("subjectKeyIdentifier") + return nil if ext.nil? + + ski_asn1 = ASN1.decode(ext.value_der) + if ext.critical? || ski_asn1.tag_class != :UNIVERSAL || ski_asn1.tag != ASN1::OCTET_STRING + raise ASN1::ASN1Error, "invalid extension" + end + + ski_asn1.value + end + end + + module AuthorityKeyIdentifier + include Helpers + + # Get the issuing certificate's key identifier from the + # authorityKeyIdentifier extension, as described in RFC5280 + # Section 4.2.1.1 + # + # Returns the binary String keyIdentifier or nil or raises + # ASN1::ASN1Error. + def authority_key_identifier + ext = find_extension("authorityKeyIdentifier") + return nil if ext.nil? + + aki_asn1 = ASN1.decode(ext.value_der) + if ext.critical? || aki_asn1.tag_class != :UNIVERSAL || aki_asn1.tag != ASN1::SEQUENCE + raise ASN1::ASN1Error, "invalid extension" + end + + key_id = aki_asn1.value.find do |v| + v.tag_class == :CONTEXT_SPECIFIC && v.tag == 0 + end + + key_id.nil? ? nil : key_id.value + end + end + + module CRLDistributionPoints + include Helpers + + # Get the distributionPoint fullName URI from the certificate's CRL + # distribution points extension, as described in RFC 5280 Section + # 4.2.1.13. + # + # Returns an array of strings or nil or raises ASN1::ASN1Error. + def crl_uris + ext = find_extension("crlDistributionPoints") + return nil if ext.nil? + + cdp_asn1 = ASN1.decode(ext.value_der) + if cdp_asn1.tag_class != :UNIVERSAL || cdp_asn1.tag != ASN1::SEQUENCE + raise ASN1::ASN1Error, "invalid extension" + end + + crl_uris = cdp_asn1.flat_map do |crl_distribution_point| + distribution_point = crl_distribution_point.value.find do |v| + v.tag_class == :CONTEXT_SPECIFIC && v.tag == 0 + end + full_name = distribution_point&.value&.find do |v| + v.tag_class == :CONTEXT_SPECIFIC && v.tag == 0 + end + full_name&.value&.select do |v| + v.tag_class == :CONTEXT_SPECIFIC && v.tag == 6 # uniformResourceIdentifier + end + end + + crl_uris.empty? ? nil : crl_uris.map(&:value) + end + end + + module AuthorityInfoAccess + include Helpers + + # Get the information and services for the issuer from the certificate's + # authority information access extension exteension, as described in RFC5280 + # Section 4.2.2.1. + # + # Returns an array of strings or nil or raises ASN1::ASN1Error. + def ca_issuer_uris + aia_asn1 = parse_aia_asn1 + return nil if aia_asn1.nil? + + ca_issuer = aia_asn1.value.select do |authority_info_access| + authority_info_access.value.first.value == "caIssuers" + end + + ca_issuer&.map(&:value)&.map(&:last)&.map(&:value) + end + + # Get the URIs for OCSP from the certificate's authority information access + # extension exteension, as described in RFC5280 Section 4.2.2.1. + # + # Returns an array of strings or nil or raises ASN1::ASN1Error. + def ocsp_uris + aia_asn1 = parse_aia_asn1 + return nil if aia_asn1.nil? + + ocsp = aia_asn1.value.select do |authority_info_access| + authority_info_access.value.first.value == "OCSP" + end + + ocsp&.map(&:value)&.map(&:last)&.map(&:value) + end + + private + + def parse_aia_asn1 + ext = find_extension("authorityInfoAccess") + return nil if ext.nil? + + aia_asn1 = ASN1.decode(ext.value_der) + if ext.critical? || aia_asn1.tag_class != :UNIVERSAL || aia_asn1.tag != ASN1::SEQUENCE + raise ASN1::ASN1Error, "invalid extension" + end + + aia_asn1 + end + end + end + class Name - def self.parse(str, type=ASN1::UTF8STRING) - ary = str.scan(/\s*([^\/,]+)\s*/).collect{|i| i[0].split("=") } - self.new(ary, type) + include OpenSSL::Marshal + + module RFC2253DN + Special = ',=+<>#;' + HexChar = /[0-9a-fA-F]/ + HexPair = /#{HexChar}#{HexChar}/ + HexString = /#{HexPair}+/ + Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/ + StringChar = /[^\\"#{Special}]/ + QuoteChar = /[^\\"]/ + AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/ + AttributeValue = / + (?!["#])((?:#{StringChar}|#{Pair})*)| + \#(#{HexString})| + "((?:#{QuoteChar}|#{Pair})*)" + /x + TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/ + + module_function + + def expand_pair(str) + return nil unless str + return str.gsub(Pair){ + pair = $& + case pair.size + when 2 then pair[1,1] + when 3 then Integer("0x#{pair[1,2]}").chr + else raise OpenSSL::X509::NameError, "invalid pair: #{str}" + end + } + end + + def expand_hexstring(str) + return nil unless str + der = str.gsub(HexPair){$&.to_i(16).chr } + a1 = OpenSSL::ASN1.decode(der) + return a1.value, a1.tag + end + + def expand_value(str1, str2, str3) + value = expand_pair(str1) + value, tag = expand_hexstring(str2) unless value + value = expand_pair(str3) unless value + return value, tag + end + + def scan(dn) + str = dn + ary = [] + while true + if md = TypeAndValue.match(str) + remain = md.post_match + type = md[1] + value, tag = expand_value(md[2], md[3], md[4]) rescue nil + if value + type_and_value = [type, value] + type_and_value.push(tag) if tag + ary.unshift(type_and_value) + if remain.length > 2 && remain[0] == ?, + str = remain[1..-1] + next + elsif remain.length > 2 && remain[0] == ?+ + raise OpenSSL::X509::NameError, + "multi-valued RDN is not supported: #{dn}" + elsif remain.empty? + break + end + end + end + msg_dn = dn[0, dn.length - str.length] + " =>" + str + raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}" + end + return ary + end + end + + class << self + # Parses the UTF-8 string representation of a distinguished name, + # according to RFC 2253. + # + # See also #to_utf8 for the opposite operation. + def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE) + ary = OpenSSL::X509::Name::RFC2253DN.scan(str) + self.new(ary, template) + end + + # Parses the string representation of a distinguished name. Two + # different forms are supported: + # + # - \OpenSSL format (<tt>X509_NAME_oneline()</tt>) used by + # <tt>#to_s</tt>. For example: <tt>/DC=com/DC=example/CN=nobody</tt> + # - \OpenSSL format (<tt>X509_NAME_print()</tt>) + # used by <tt>#to_s(OpenSSL::X509::Name::COMPAT)</tt>. For example: + # <tt>DC=com, DC=example, CN=nobody</tt> + # + # Neither of them is standardized and has quirks and inconsistencies + # in handling of escaped characters or multi-valued RDNs. + # + # Use of this method is discouraged in new applications. See + # Name.parse_rfc2253 and #to_utf8 for the alternative. + def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE) + if str.start_with?("/") + # /A=B/C=D format + ary = str[1..-1].split("/").map { |i| i.split("=", 2) } + else + # Comma-separated + ary = str.split(",").map { |i| i.strip.split("=", 2) } + end + self.new(ary, template) + end + + alias parse parse_openssl end - end # Name - end # X509 -end # OpenSSL + def pretty_print(q) + q.object_group(self) { + q.text ' ' + q.text to_s(OpenSSL::X509::Name::RFC2253) + } + end + end + + class Attribute + include OpenSSL::Marshal + + def ==(other) + return false unless Attribute === other + to_der == other.to_der + end + end + + class StoreContext + def cleanup + warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE + end + end + + class Certificate + include OpenSSL::Marshal + include Extension::SubjectKeyIdentifier + include Extension::AuthorityKeyIdentifier + include Extension::CRLDistributionPoints + include Extension::AuthorityInfoAccess + + def inspect + "#<#{self.class}: " \ + "subject=#{subject.inspect}, " \ + "issuer=#{issuer.inspect}, " \ + "serial=#{serial.inspect}, " \ + "not_before=#{not_before.inspect rescue "(error)"}, " \ + "not_after=#{not_after.inspect rescue "(error)"}>" + end + + def pretty_print(q) + q.object_group(self) { + q.breakable + q.text 'subject='; q.pp self.subject; q.text ','; q.breakable + q.text 'issuer='; q.pp self.issuer; q.text ','; q.breakable + q.text 'serial='; q.pp self.serial; q.text ','; q.breakable + q.text 'not_before='; q.pp self.not_before; q.text ','; q.breakable + q.text 'not_after='; q.pp self.not_after + } + end + + def self.load_file(path) + load(File.binread(path)) + end + end + + class CRL + include OpenSSL::Marshal + include Extension::AuthorityKeyIdentifier + + def ==(other) + return false unless CRL === other + to_der == other.to_der + end + end + + class Revoked + def ==(other) + return false unless Revoked === other + to_der == other.to_der + end + end + + class Request + include OpenSSL::Marshal + + def ==(other) + return false unless Request === other + to_der == other.to_der + end + end + end +end |
