Information Security Programming in Ruby @nahi
@nahi - Twitter, Github Software Engineer at https://www.treasuredata.com OSS developer and enthusiast; committer of CRuby and JRuby Information Security Specialist
Information Security Programming in Ruby scripts: https://github.com/nahi/ruby-crypt/tree/master/odrk05
References JUS 2003 “PKI入門 - Ruby/OpenSSLを触りながら学ぶPKI” https://github.com/nahi/ruby-crypt/raw/master/jus-pki.ppt RubyKaigi 2006 “セキュアアプリケーションプログラミング” https://github.com/nahi/ruby- crypt/blob/master/rubykaigi2006/RubyKaigi2006_SAP_20060610.pdf RubyConf 2012 “Ruby HTTP clients comparison” http://www.slideshare.net/HiroshiNakamura/rubyhttp-clients-comparison
Information Security Programming Confidentially Authentication Integrity (Availability) (Privacy)
(D) S for external C [F] Encryption in S [G] Encryption in C [E] authentication (C) S for internal C (B) C for external S 7 Implementation Patterns (A) C for internal S (A) (A) (B) (B) (C) (D) [F] [G] [E] [E] Orange: Implementation target Gray: External system
(D) S for external C [F] Encryption in S [G] Encryption in C [E] authentication (C) S for internal C (B) C for external S 7 Implementation Patterns (A) C for internal S (A) (A) (B) (B) (C) (D) [F] [G] [E] [E] Orange: Implementation target Gray: External system
… in Ruby (A) C for internal S (B) C for external S (C) S for internal C (D) S for external C [E] authentication [F] Encryption in S [G] Encryption in C (A) (A) (B) (B) (C) [E] [F] [G] (D) [E] Blue: Acceptable Orange: Pitfalls Red: No way
Protected communication Fixed server authentication ➔ SSL configuration: CBC, SSLv3.0, compression, TLSv1.0, RC4, DHE1024, … ➔ Fails for wrong endpoint (A) C for internal S (A) (A)
SSL configuration require 'httpclient' client = HTTPClient.new client.get('https://www.ruby-lang.org/en/').status % ruby a1.rb ok: "/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA" ok: "/C=BE/O=GlobalSign nv-sa/CN=GlobalSign Domain Validation CA - SHA256 - G2" ok: "/OU=Domain Control Validated/CN=*.ruby-lang.org" Protocol version: TLSv1.2 Cipher: ["ECDHE-RSA-AES128-GCM-SHA256", "TLSv1/SSLv3", 128, 128] State: SSLOK : SSL negotiation finished successfully
Fails for wrong endpoint require 'httpclient' client = HTTPClient.new client.get('https://hyogo-9327.herokussl.com/en/').status % ruby -d a2.rb ok: "/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA" ok: "/C=BE/O=GlobalSign nv-sa/CN=GlobalSign Domain Validation CA - SHA256 - G2" ok: "/OU=Domain Control Validated/CN=*.ruby-lang.org" Protocol version: TLSv1.2 Cipher: ["ECDHE-RSA-AES128-GCM-SHA256", "TLSv1/SSLv3", 128, 128] State: SSLOK : SSL negotiation finished successfully Exception `OpenSSL::SSL::SSLError' - hostname "hyogo-9327. herokussl.com" does not match the server certificate
require 'aws-sdk' class KMSEncryptor CTX = { 'purpose' => 'odrk05 demonstration' } GCM_IV_SIZE = 12; GCM_TAG_SIZE = 16 def initialize(region, key_id) @region, @key_id = region, key_id @kms = Aws::KMS::Client.new(region: @region) end def generate_data_key resp = @kms.generate_data_key_without_plaintext( key_id: @key_id, encryption_context: CTX, key_spec: 'AES_128' ) resp.ciphertext_blob end def with_key(wrapped_key) key = nil begin key = @kms.decrypt( ciphertext_blob: wrapped_key, encryption_context: CTX ).plaintext yield key ensure # TODO: confirm that key is deleted from memory key.tr!("0-xff".force_encoding('BINARY'), "0") end end
Fails for weak connection require 'httpclient' client = HTTPClient.new client.ssl_config.ssl_version = :TLSv1_2 client.get('https://localhost:17443/').status =begin % ruby a3.rb SSL_connect returned=1 errno=0 state=SSLv3 read server hello A: wrong version number (OpenSSL::SSL::SSLError) =end
Net::HTTP sample require 'net/https' class NetHTTPClient < Net::HTTP require 'httpclient' def do_start if $DEBUG && @use_ssl self.verify_callback = HTTPClient::SSLConfig.new(nil). method(:default_verify_callback) end super end def on_connect if $DEBUG && @use_ssl ssl_socket = @socket.io if ssl_socket.respond_to?(:ssl_version) warn("Protocol version: #{ssl_socket.ssl_version}") end warn("Cipher: #{ssl_socket.cipher.inspect}") warn("State: #{ssl_socket.state}") end super end end # => # => client = NetHTTPClient.new( "www.ruby-lang.org", 443) client.use_ssl = true client.cert_store = store = OpenSSL::X509::Store.new store.set_default_paths client.get("/")
Protected communication Restricted server authentication ➔ SSL configuration ➔ Fails for revoked server (B) C for external S (A) (A) (B) (B)
Revocation check require 'httpclient' # >= 2.7.0 client = HTTPClient.new client.get('https://test-sspev.verisign.com:2443/test-SSPEV- revoked-verisign.html').status % ruby b.rb # => 200 % jruby b.rb # => 200 % jruby -J-Dcom.sun.security.enableCRLDP=true -J-Dcom.sun.net.ssl.checkRevocation=true b.rb OpenSSL::SSL::SSLError: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: Certificate has been revoked, reason: UNSPECIFIED, revocation date: Thu Oct 30 06:29:37 JST 2014, authority: CN=Symantec Class 3 EV SSL CA - G3, OU=Symantec Trust Network, O=Symantec Corporation, C=US OpenSSL...?
Protected communication Restricted client authentication ➔ SSL configuration ➔ Server key management ➔ Certificate rotation ➔ Fails for unexpected clients (C) S for internal C (C)
WEBrick SSL server require 'webrick/https' require 'logger' logger = Logger.new(STDERR) server = WEBrick::HTTPServer.new( BindAddress: "localhost", Logger: logger, Port: 17443, DocumentRoot: '/dev/null', SSLEnable: true, SSLCACertificateFile: 'ca-chain.cert', SSLCertificate: OpenSSL::X509::Certificate.new( File.read('server.cert')), SSLPrivateKey: OpenSSL::PKey::RSA.new( File.read('server.key')), ) basic_auth=WEBrick::HTTPAuth::BasicAuth.new( Logger: logger, Realm: 'auth', UserDB: WEBrick::HTTPAuth::Htpasswd.new( 'htpasswd') ) # => # => server.mount('/hello', WEBrick::HTTPServlet::ProcHandler.new( ->(req, res) { basic_auth.authenticate(req, res) res['content-type'] = 'text/plain' res.body = 'hello' }) ) trap(:INT) do server.shutdown end t = Thread.new { Thread.current.abort_on_exception = true server.start } while server.status != :Running sleep 0.1 raise unless t.alive? end puts $$ t.join
Protected communication Client authentication ➔ SSL configuration ➔ Server key management ➔ Certificate rotation ➔ Fails for unexpected clients ➔ Recovery from key compromise You have better solutions (Apache, Nginx, ELB, …) (D) S for external C (C) (D)
Client authentication On unprotected network ➔ Cipher algorithm ➔ Tamper detection ➔ Constant time operation Use well-known library [E] authentication [E] [E]
Data protection at rest ➔ Cipher algorithm ➔ Encryption key management ◆ Storage ◆ Usage authn / authz ◆ Usage auditing ◆ Rotation ➔ Tamper detection ➔ Processing throughput / latency [F] Encryption in S / [G] in C [F] [G]
require 'aws-sdk' class KMSEncryptor CTX = { 'purpose' => 'odrk05 demonstration' } GCM_IV_SIZE = 12; GCM_TAG_SIZE = 16 def initialize(region, key_id) @region, @key_id = region, key_id @kms = Aws::KMS::Client.new(region: @region) end def generate_data_key resp = @kms.generate_data_key_without_plaintext( key_id: @key_id, encryption_context: CTX, key_spec: 'AES_128' ) resp.ciphertext_blob end def with_key(wrapped_key) key = nil begin key = @kms.decrypt( ciphertext_blob: wrapped_key, encryption_context: CTX ).plaintext yield key ensure # TODO: confirm that key is deleted from memory key.tr!("0-xff".force_encoding('BINARY'), "0") end end
def encrypt(wrapped_key, plaintext) with_key(wrapped_key) do |key| cipher = OpenSSL::Cipher::Cipher.new('aes-128-gcm') iv = OpenSSL::Random.random_bytes(GCM_IV_SIZE) cipher.encrypt; cipher.key = key;cipher.iv = iv iv + cipher.update(plaintext) + cipher.final end end def decrypt(wrapped_key, ciphertext) with_key(wrapped_key) do |key| iv, data = ciphertext.unpack("a#{GCM_IV_SIZE}a*") auth_tag = data.slice!(data.bytesize - GCM_TAG_SIZE, GCM_TAG_SIZE) cipher = OpenSSL::Cipher::Cipher.new('aes-128-gcm') cipher.decrypt; cipher.key = key; cipher.iv = iv cipher.auth_tag = auth_tag cipher.update(data) + cipher.final end end end encryptor = KMSEncryptor.new('ap-northeast-1', 'alias/nahi-test-tokyo') # generate key for each data, customer, or something wrapped_key = encryptor.generate_data_key plaintext = File.read(__FILE__) ciphertext = encryptor.encrypt(wrapped_key, plaintext) # save wrapped_key and ciphertext in DB, File or somewhere # restore wrapped_key and ciphertext from DB, File or somewhere puts encryptor.decrypt(wrapped_key, ciphertext) jruby-openssl does not support aes-gcm… -> next page
if defined?(JRuby) require 'java' java_import 'javax.crypto.Cipher' java_import 'javax.crypto.SecretKey' java_import 'javax.crypto.spec.SecretKeySpec' java_import 'javax.crypto.spec.GCMParameterSpec' class KMSEncryptor # Overrides def encrypt(wrapped_key, plaintext) with_key(wrapped_key) do |key| cipher = Cipher.getInstance('AES/GCM/PKCS5Padding') iv = OpenSSL::Random.random_bytes(GCM_IV_SIZE) spec = GCMParameterSpec.new(GCM_TAG_SIZE * 8, iv.to_java_bytes) cipher.init(1, SecretKeySpec.new(key.to_java_bytes, 0, key.bytesize, 'AES'), spec) ciphertext = String.from_java_bytes( cipher.doFinal(plaintext.to_java_bytes), Encoding::BINARY) iv + ciphertext end end # Overrides def decrypt(wrapped_key, ciphertext) with_key(wrapped_key) do |key| cipher = Cipher.getInstance('AES/GCM/PKCS5Padding') iv, data = ciphertext.unpack("a#{GCM_IV_SIZE}a*") spec = GCMParameterSpec.new(GCM_TAG_SIZE * 8, iv.to_java_bytes) cipher.init(2, SecretKeySpec.new(key.to_java_bytes, 0, key.bytesize, 'AES'), spec) String.from_java_bytes(cipher.doFinal(data.to_java_bytes), Encoding::BINARY) end end end end aes-128-gcm in JRuby!
… in Ruby (A) C for internal S (B) C for external S (C) S for internal C (D) S for external C [E] authentication [F] Encryption in S [G] Encryption in C (A) (A) (B) (B) (C) [E] [F] [G] (D) [E] Blue: Acceptable Orange: Pitfalls Red: No way

Information security programming in ruby

  • 1.
  • 2.
    @nahi - Twitter,Github Software Engineer at https://www.treasuredata.com OSS developer and enthusiast; committer of CRuby and JRuby Information Security Specialist
  • 3.
    Information Security Programming inRuby scripts: https://github.com/nahi/ruby-crypt/tree/master/odrk05
  • 4.
    References JUS 2003 “PKI入門- Ruby/OpenSSLを触りながら学ぶPKI” https://github.com/nahi/ruby-crypt/raw/master/jus-pki.ppt RubyKaigi 2006 “セキュアアプリケーションプログラミング” https://github.com/nahi/ruby- crypt/blob/master/rubykaigi2006/RubyKaigi2006_SAP_20060610.pdf RubyConf 2012 “Ruby HTTP clients comparison” http://www.slideshare.net/HiroshiNakamura/rubyhttp-clients-comparison
  • 5.
  • 6.
    (D) S forexternal C [F] Encryption in S [G] Encryption in C [E] authentication (C) S for internal C (B) C for external S 7 Implementation Patterns (A) C for internal S (A) (A) (B) (B) (C) (D) [F] [G] [E] [E] Orange: Implementation target Gray: External system
  • 7.
    (D) S forexternal C [F] Encryption in S [G] Encryption in C [E] authentication (C) S for internal C (B) C for external S 7 Implementation Patterns (A) C for internal S (A) (A) (B) (B) (C) (D) [F] [G] [E] [E] Orange: Implementation target Gray: External system
  • 8.
    … in Ruby (A)C for internal S (B) C for external S (C) S for internal C (D) S for external C [E] authentication [F] Encryption in S [G] Encryption in C (A) (A) (B) (B) (C) [E] [F] [G] (D) [E] Blue: Acceptable Orange: Pitfalls Red: No way
  • 9.
    Protected communication Fixed serverauthentication ➔ SSL configuration: CBC, SSLv3.0, compression, TLSv1.0, RC4, DHE1024, … ➔ Fails for wrong endpoint (A) C for internal S (A) (A)
  • 10.
    SSL configuration require 'httpclient' client= HTTPClient.new client.get('https://www.ruby-lang.org/en/').status % ruby a1.rb ok: "/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA" ok: "/C=BE/O=GlobalSign nv-sa/CN=GlobalSign Domain Validation CA - SHA256 - G2" ok: "/OU=Domain Control Validated/CN=*.ruby-lang.org" Protocol version: TLSv1.2 Cipher: ["ECDHE-RSA-AES128-GCM-SHA256", "TLSv1/SSLv3", 128, 128] State: SSLOK : SSL negotiation finished successfully
  • 11.
    Fails for wrongendpoint require 'httpclient' client = HTTPClient.new client.get('https://hyogo-9327.herokussl.com/en/').status % ruby -d a2.rb ok: "/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA" ok: "/C=BE/O=GlobalSign nv-sa/CN=GlobalSign Domain Validation CA - SHA256 - G2" ok: "/OU=Domain Control Validated/CN=*.ruby-lang.org" Protocol version: TLSv1.2 Cipher: ["ECDHE-RSA-AES128-GCM-SHA256", "TLSv1/SSLv3", 128, 128] State: SSLOK : SSL negotiation finished successfully Exception `OpenSSL::SSL::SSLError' - hostname "hyogo-9327. herokussl.com" does not match the server certificate
  • 12.
    require 'aws-sdk' class KMSEncryptor CTX= { 'purpose' => 'odrk05 demonstration' } GCM_IV_SIZE = 12; GCM_TAG_SIZE = 16 def initialize(region, key_id) @region, @key_id = region, key_id @kms = Aws::KMS::Client.new(region: @region) end def generate_data_key resp = @kms.generate_data_key_without_plaintext( key_id: @key_id, encryption_context: CTX, key_spec: 'AES_128' ) resp.ciphertext_blob end def with_key(wrapped_key) key = nil begin key = @kms.decrypt( ciphertext_blob: wrapped_key, encryption_context: CTX ).plaintext yield key ensure # TODO: confirm that key is deleted from memory key.tr!("0-xff".force_encoding('BINARY'), "0") end end
  • 13.
    Fails for weakconnection require 'httpclient' client = HTTPClient.new client.ssl_config.ssl_version = :TLSv1_2 client.get('https://localhost:17443/').status =begin % ruby a3.rb SSL_connect returned=1 errno=0 state=SSLv3 read server hello A: wrong version number (OpenSSL::SSL::SSLError) =end
  • 14.
    Net::HTTP sample require 'net/https' classNetHTTPClient < Net::HTTP require 'httpclient' def do_start if $DEBUG && @use_ssl self.verify_callback = HTTPClient::SSLConfig.new(nil). method(:default_verify_callback) end super end def on_connect if $DEBUG && @use_ssl ssl_socket = @socket.io if ssl_socket.respond_to?(:ssl_version) warn("Protocol version: #{ssl_socket.ssl_version}") end warn("Cipher: #{ssl_socket.cipher.inspect}") warn("State: #{ssl_socket.state}") end super end end # => # => client = NetHTTPClient.new( "www.ruby-lang.org", 443) client.use_ssl = true client.cert_store = store = OpenSSL::X509::Store.new store.set_default_paths client.get("/")
  • 15.
    Protected communication Restricted serverauthentication ➔ SSL configuration ➔ Fails for revoked server (B) C for external S (A) (A) (B) (B)
  • 16.
    Revocation check require 'httpclient'# >= 2.7.0 client = HTTPClient.new client.get('https://test-sspev.verisign.com:2443/test-SSPEV- revoked-verisign.html').status % ruby b.rb # => 200 % jruby b.rb # => 200 % jruby -J-Dcom.sun.security.enableCRLDP=true -J-Dcom.sun.net.ssl.checkRevocation=true b.rb OpenSSL::SSL::SSLError: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: Certificate has been revoked, reason: UNSPECIFIED, revocation date: Thu Oct 30 06:29:37 JST 2014, authority: CN=Symantec Class 3 EV SSL CA - G3, OU=Symantec Trust Network, O=Symantec Corporation, C=US OpenSSL...?
  • 17.
    Protected communication Restricted clientauthentication ➔ SSL configuration ➔ Server key management ➔ Certificate rotation ➔ Fails for unexpected clients (C) S for internal C (C)
  • 18.
    WEBrick SSL server require'webrick/https' require 'logger' logger = Logger.new(STDERR) server = WEBrick::HTTPServer.new( BindAddress: "localhost", Logger: logger, Port: 17443, DocumentRoot: '/dev/null', SSLEnable: true, SSLCACertificateFile: 'ca-chain.cert', SSLCertificate: OpenSSL::X509::Certificate.new( File.read('server.cert')), SSLPrivateKey: OpenSSL::PKey::RSA.new( File.read('server.key')), ) basic_auth=WEBrick::HTTPAuth::BasicAuth.new( Logger: logger, Realm: 'auth', UserDB: WEBrick::HTTPAuth::Htpasswd.new( 'htpasswd') ) # => # => server.mount('/hello', WEBrick::HTTPServlet::ProcHandler.new( ->(req, res) { basic_auth.authenticate(req, res) res['content-type'] = 'text/plain' res.body = 'hello' }) ) trap(:INT) do server.shutdown end t = Thread.new { Thread.current.abort_on_exception = true server.start } while server.status != :Running sleep 0.1 raise unless t.alive? end puts $$ t.join
  • 19.
    Protected communication Client authentication ➔SSL configuration ➔ Server key management ➔ Certificate rotation ➔ Fails for unexpected clients ➔ Recovery from key compromise You have better solutions (Apache, Nginx, ELB, …) (D) S for external C (C) (D)
  • 20.
    Client authentication On unprotectednetwork ➔ Cipher algorithm ➔ Tamper detection ➔ Constant time operation Use well-known library [E] authentication [E] [E]
  • 21.
    Data protection atrest ➔ Cipher algorithm ➔ Encryption key management ◆ Storage ◆ Usage authn / authz ◆ Usage auditing ◆ Rotation ➔ Tamper detection ➔ Processing throughput / latency [F] Encryption in S / [G] in C [F] [G]
  • 22.
    require 'aws-sdk' class KMSEncryptor CTX= { 'purpose' => 'odrk05 demonstration' } GCM_IV_SIZE = 12; GCM_TAG_SIZE = 16 def initialize(region, key_id) @region, @key_id = region, key_id @kms = Aws::KMS::Client.new(region: @region) end def generate_data_key resp = @kms.generate_data_key_without_plaintext( key_id: @key_id, encryption_context: CTX, key_spec: 'AES_128' ) resp.ciphertext_blob end def with_key(wrapped_key) key = nil begin key = @kms.decrypt( ciphertext_blob: wrapped_key, encryption_context: CTX ).plaintext yield key ensure # TODO: confirm that key is deleted from memory key.tr!("0-xff".force_encoding('BINARY'), "0") end end
  • 23.
    def encrypt(wrapped_key, plaintext) with_key(wrapped_key)do |key| cipher = OpenSSL::Cipher::Cipher.new('aes-128-gcm') iv = OpenSSL::Random.random_bytes(GCM_IV_SIZE) cipher.encrypt; cipher.key = key;cipher.iv = iv iv + cipher.update(plaintext) + cipher.final end end def decrypt(wrapped_key, ciphertext) with_key(wrapped_key) do |key| iv, data = ciphertext.unpack("a#{GCM_IV_SIZE}a*") auth_tag = data.slice!(data.bytesize - GCM_TAG_SIZE, GCM_TAG_SIZE) cipher = OpenSSL::Cipher::Cipher.new('aes-128-gcm') cipher.decrypt; cipher.key = key; cipher.iv = iv cipher.auth_tag = auth_tag cipher.update(data) + cipher.final end end end encryptor = KMSEncryptor.new('ap-northeast-1', 'alias/nahi-test-tokyo') # generate key for each data, customer, or something wrapped_key = encryptor.generate_data_key plaintext = File.read(__FILE__) ciphertext = encryptor.encrypt(wrapped_key, plaintext) # save wrapped_key and ciphertext in DB, File or somewhere # restore wrapped_key and ciphertext from DB, File or somewhere puts encryptor.decrypt(wrapped_key, ciphertext) jruby-openssl does not support aes-gcm… -> next page
  • 24.
    if defined?(JRuby) require 'java' java_import'javax.crypto.Cipher' java_import 'javax.crypto.SecretKey' java_import 'javax.crypto.spec.SecretKeySpec' java_import 'javax.crypto.spec.GCMParameterSpec' class KMSEncryptor # Overrides def encrypt(wrapped_key, plaintext) with_key(wrapped_key) do |key| cipher = Cipher.getInstance('AES/GCM/PKCS5Padding') iv = OpenSSL::Random.random_bytes(GCM_IV_SIZE) spec = GCMParameterSpec.new(GCM_TAG_SIZE * 8, iv.to_java_bytes) cipher.init(1, SecretKeySpec.new(key.to_java_bytes, 0, key.bytesize, 'AES'), spec) ciphertext = String.from_java_bytes( cipher.doFinal(plaintext.to_java_bytes), Encoding::BINARY) iv + ciphertext end end # Overrides def decrypt(wrapped_key, ciphertext) with_key(wrapped_key) do |key| cipher = Cipher.getInstance('AES/GCM/PKCS5Padding') iv, data = ciphertext.unpack("a#{GCM_IV_SIZE}a*") spec = GCMParameterSpec.new(GCM_TAG_SIZE * 8, iv.to_java_bytes) cipher.init(2, SecretKeySpec.new(key.to_java_bytes, 0, key.bytesize, 'AES'), spec) String.from_java_bytes(cipher.doFinal(data.to_java_bytes), Encoding::BINARY) end end end end aes-128-gcm in JRuby!
  • 25.
    … in Ruby (A)C for internal S (B) C for external S (C) S for internal C (D) S for external C [E] authentication [F] Encryption in S [G] Encryption in C (A) (A) (B) (B) (C) [E] [F] [G] (D) [E] Blue: Acceptable Orange: Pitfalls Red: No way