information security programming in ruby
TRANSCRIPT
Information Security Programming in Ruby
@nahi
@nahi - Twitter, GithubSoftware 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
ReferencesJUS 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 targetGray: 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 targetGray: 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: AcceptableOrange: PitfallsRed: 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 configurationrequire 'httpclient'client = HTTPClient.newclient.get('https://www.ruby-lang.org/en/').status
% ruby a1.rbok: "/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.2Cipher: ["ECDHE-RSA-AES128-GCM-SHA256", "TLSv1/SSLv3", 128, 128]State: SSLOK : SSL negotiation finished successfully
Fails for wrong endpointrequire 'httpclient'client = HTTPClient.newclient.get('https://hyogo-9327.herokussl.com/en/').status
% ruby -d a2.rbok: "/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.2Cipher: ["ECDHE-RSA-AES128-GCM-SHA256", "TLSv1/SSLv3", 128, 128]State: SSLOK : SSL negotiation finished successfullyException `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 connectionrequire 'httpclient'client = HTTPClient.newclient.ssl_config.ssl_version = :TLSv1_2client.get('https://localhost:17443/').status
=begin% ruby a3.rbSSL_connect returned=1 errno=0 state=SSLv3 read server hello A: wrong version number (OpenSSL::SSL::SSLError)=end
Net::HTTP samplerequire '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 endend# =>
# =>client = NetHTTPClient.new( "www.ruby-lang.org", 443)client.use_ssl = trueclient.cert_store = store = OpenSSL::X509::Store.newstore.set_default_pathsclient.get("/")
Protected communication
Restricted server authentication
➔ SSL configuration
➔ Fails for revoked server
(B) C for external S
(A)
(A)
(B)
(B)
Revocation checkrequire 'httpclient' # >= 2.7.0client = HTTPClient.newclient.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.rbOpenSSL::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 serverrequire '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.shutdownend
t = Thread.new { Thread.current.abort_on_exception = true server.start}while server.status != :Running sleep 0.1 raise unless t.alive?endputs $$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 endend
encryptor = KMSEncryptor.new('ap-northeast-1', 'alias/nahi-test-tokyo')# generate key for each data, customer, or somethingwrapped_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 somewhereputs 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 endend
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: AcceptableOrange: PitfallsRed: No way