/***** BEGIN LICENSE BLOCK ***** * Version: EPL 1.0/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Eclipse Public * License Version 1.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.eclipse.org/legal/epl-v10.html * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * Copyright (C) 2006 Ola Bini <ola@ologix.com> * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the EPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the EPL, the GPL or the LGPL. ***** END LICENSE BLOCK *****/ package org.jruby.ext.openssl; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collections; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509TrustManager; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyClass; import org.jruby.RubyHash; import org.jruby.RubyInteger; import org.jruby.RubyModule; import org.jruby.RubyNumeric; import org.jruby.RubyObject; import org.jruby.RubySymbol; import org.jruby.anno.JRubyMethod; import org.jruby.common.IRubyWarnings.ID; import org.jruby.runtime.Arity; import org.jruby.runtime.Block; import org.jruby.runtime.BlockCallback; import org.jruby.runtime.CallBlock; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.Visibility; import org.jruby.util.ByteList; import org.jruby.ext.openssl.x509store.Certificate; import org.jruby.ext.openssl.x509store.Name; import org.jruby.ext.openssl.x509store.Store; import org.jruby.ext.openssl.x509store.StoreContext; import org.jruby.ext.openssl.x509store.X509AuxCertificate; import org.jruby.ext.openssl.x509store.X509Object; import org.jruby.ext.openssl.x509store.X509Utils; import static org.jruby.ext.openssl.StringHelper.*; import static org.jruby.ext.openssl.SSL.*; import static org.jruby.ext.openssl.X509._X509; import static org.jruby.ext.openssl.X509Cert._Certificate; import static org.jruby.ext.openssl.OpenSSL.debug; import static org.jruby.ext.openssl.OpenSSL.debugStackTrace; import static org.jruby.ext.openssl.OpenSSL.warn; import static org.jruby.ext.openssl.Utils.hasNonNilInstanceVariable; /** * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a> */ public class SSLContext extends RubyObject { private static final long serialVersionUID = -6955774230685920773L; // Mapping table for OpenSSL's SSL_METHOD -> JSSE's SSLContext algorithm. private static final HashMap<String, String> SSL_VERSION_OSSL2JSSE; // Mapping table for JSEE's enabled protocols for the algorithm. private static final Map<String, String[]> ENABLED_PROTOCOLS; static { SSL_VERSION_OSSL2JSSE = new LinkedHashMap<String, String>(20, 1); ENABLED_PROTOCOLS = new HashMap<String, String[]>(8, 1); SSL_VERSION_OSSL2JSSE.put("TLSv1", "TLSv1"); SSL_VERSION_OSSL2JSSE.put("TLSv1_server", "TLSv1"); SSL_VERSION_OSSL2JSSE.put("TLSv1_client", "TLSv1"); ENABLED_PROTOCOLS.put("TLSv1", new String[] { "TLSv1" }); SSL_VERSION_OSSL2JSSE.put("SSLv2", "SSLv2"); SSL_VERSION_OSSL2JSSE.put("SSLv2_server", "SSLv2"); SSL_VERSION_OSSL2JSSE.put("SSLv2_client", "SSLv2"); ENABLED_PROTOCOLS.put("SSLv2", new String[] { "SSLv2" }); SSL_VERSION_OSSL2JSSE.put("SSLv3", "SSLv3"); SSL_VERSION_OSSL2JSSE.put("SSLv3_server", "SSLv3"); SSL_VERSION_OSSL2JSSE.put("SSLv3_client", "SSLv3"); ENABLED_PROTOCOLS.put("SSLv3", new String[] { "SSLv3" }); SSL_VERSION_OSSL2JSSE.put("SSLv23", "SSL"); SSL_VERSION_OSSL2JSSE.put("SSLv23_server", "SSL"); SSL_VERSION_OSSL2JSSE.put("SSLv23_client", "SSL"); if ( OpenSSL.javaVersion7(true) ) { // >= 1.7 ENABLED_PROTOCOLS.put("SSL", new String[] { "SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2" }); } else { ENABLED_PROTOCOLS.put("SSL", new String[] { "SSLv2", "SSLv3", "TLSv1" }); } // Historically we were ahead of MRI to support TLS // ... thus the non-standard names version names : SSL_VERSION_OSSL2JSSE.put("TLS", "TLS"); ENABLED_PROTOCOLS.put("TLS", new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }); SSL_VERSION_OSSL2JSSE.put("TLSv1.1", "TLSv1.1"); SSL_VERSION_OSSL2JSSE.put("TLSv1_1_server", "TLSv1.1"); SSL_VERSION_OSSL2JSSE.put("TLSv1_1_client", "TLSv1.1"); ENABLED_PROTOCOLS.put("TLSv1.1", new String[] { "TLSv1.1" }); SSL_VERSION_OSSL2JSSE.put("TLSv1_1", "TLSv1.1"); // supported on MRI 2.x SSL_VERSION_OSSL2JSSE.put("TLSv1_2", "TLSv1.2"); // supported on MRI 2.x ENABLED_PROTOCOLS.put("TLSv1.2", new String[] { "TLSv1.2" }); SSL_VERSION_OSSL2JSSE.put("TLSv1.2", "TLSv1.2"); // just for completeness SSL_VERSION_OSSL2JSSE.put("TLSv1_2_server", "TLSv1.2"); SSL_VERSION_OSSL2JSSE.put("TLSv1_2_client", "TLSv1.2"); } private static ObjectAllocator SSLCONTEXT_ALLOCATOR = new ObjectAllocator() { public IRubyObject allocate(Ruby runtime, RubyClass klass) { return new SSLContext(runtime, klass); } }; public static void createSSLContext(final Ruby runtime, final RubyModule SSL) { // OpenSSL::SSL RubyClass SSLContext = SSL.defineClassUnder("SSLContext", runtime.getObject(), SSLCONTEXT_ALLOCATOR); final ThreadContext context = runtime.getCurrentContext(); SSLContext.addReadWriteAttribute(context, "cert"); SSLContext.addReadWriteAttribute(context, "key"); SSLContext.addReadWriteAttribute(context, "client_ca"); SSLContext.addReadWriteAttribute(context, "ca_file"); SSLContext.addReadWriteAttribute(context, "ca_path"); SSLContext.addReadWriteAttribute(context, "timeout"); SSLContext.addReadWriteAttribute(context, "verify_mode"); SSLContext.addReadWriteAttribute(context, "verify_depth"); SSLContext.addReadWriteAttribute(context, "verify_callback"); SSLContext.addReadWriteAttribute(context, "options"); SSLContext.addReadWriteAttribute(context, "cert_store"); SSLContext.addReadWriteAttribute(context, "extra_chain_cert"); SSLContext.addReadWriteAttribute(context, "client_cert_cb"); SSLContext.addReadWriteAttribute(context, "session_id_context"); SSLContext.addReadWriteAttribute(context, "tmp_dh_callback"); SSLContext.addReadWriteAttribute(context, "servername_cb"); SSLContext.addReadWriteAttribute(context, "renegotiation_cb"); SSLContext.defineAlias("ssl_timeout", "timeout"); SSLContext.defineAlias("ssl_timeout=", "timeout="); SSLContext.defineAnnotatedMethods(SSLContext.class); SSLContext.undefineMethod("dup"); final Set<String> methodKeys = SSL_VERSION_OSSL2JSSE.keySet(); final RubyArray methods = runtime.newArray( methodKeys.size() ); for ( final String method : methodKeys ) { if ( method.equals("SSLv2") || method.startsWith("SSLv2_") ) { continue; // do not report SSLv2, SSLv2_server, SSLv2_client } if ( method.indexOf('.') == -1 ) { // do not "officially" report TLSv1.1 and TLSv1.2 methods.append( runtime.newSymbol(method) ); } } SSLContext.defineConstant("METHODS", methods); // in 1.8.7 as well as 1.9.3 : // [:TLSv1, :TLSv1_server, :TLSv1_client, :SSLv3, :SSLv3_server, :SSLv3_client, :SSLv23, :SSLv23_server, :SSLv23_client] // in 2.0.0 : // [:TLSv1, :TLSv1_server, :TLSv1_client, :TLSv1_2, :TLSv1_2_server, :TLSv1_2_client, :TLSv1_1, :TLSv1_1_server, // :TLSv1_1_client, :SSLv3, :SSLv3_server, :SSLv3_client, :SSLv23, :SSLv23_server, :SSLv23_client] SSLContext.setConstant("SESSION_CACHE_OFF", runtime.newFixnum(SESSION_CACHE_OFF)); SSLContext.setConstant("SESSION_CACHE_CLIENT", runtime.newFixnum(SESSION_CACHE_CLIENT)); SSLContext.setConstant("SESSION_CACHE_SERVER", runtime.newFixnum(SESSION_CACHE_SERVER)); SSLContext.setConstant("SESSION_CACHE_BOTH", runtime.newFixnum(SESSION_CACHE_BOTH)); SSLContext.setConstant("SESSION_CACHE_NO_AUTO_CLEAR", runtime.newFixnum(SESSION_CACHE_NO_AUTO_CLEAR)); SSLContext.setConstant("SESSION_CACHE_NO_INTERNAL_LOOKUP", runtime.newFixnum(SESSION_CACHE_NO_INTERNAL_LOOKUP)); SSLContext.setConstant("SESSION_CACHE_NO_INTERNAL_STORE", runtime.newFixnum(SESSION_CACHE_NO_INTERNAL_STORE)); SSLContext.setConstant("SESSION_CACHE_NO_INTERNAL", runtime.newFixnum(SESSION_CACHE_NO_INTERNAL)); // DEFAULT_CERT_STORE = OpenSSL::X509::Store.new // DEFAULT_CERT_STORE.set_default_paths // if defined?(OpenSSL::X509::V_FLAG_CRL_CHECK_ALL) // DEFAULT_CERT_STORE.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL // end final X509Store DEFAULT_CERT_STORE = X509Store.newStore(runtime); DEFAULT_CERT_STORE.set_default_paths(context); final IRubyObject V_FLAG_CRL_CHECK_ALL = _X509(runtime).getConstantAt("V_FLAG_CRL_CHECK_ALL"); if ( V_FLAG_CRL_CHECK_ALL != null ) DEFAULT_CERT_STORE.set_flags(V_FLAG_CRL_CHECK_ALL); SSLContext.setConstant("DEFAULT_CERT_STORE", DEFAULT_CERT_STORE); // DEFAULT_PARAMS = { // :ssl_version => "SSLv23", // :verify_mode => OpenSSL::SSL::VERIFY_PEER, // :ciphers => "ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW", // :options => OpenSSL::SSL::OP_ALL, // } // on MRI 2.1 (should not matter for us) : // :options => defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS) ? // OpenSSL::SSL::OP_ALL & ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS : // OpenSSL::SSL::OP_ALL final RubyHash DEFAULT_PARAMS = new RubyHash(runtime); IRubyObject ssl_version = StringHelper.newString(runtime, new byte[] { 'S','S','L','v','2','3' }); DEFAULT_PARAMS.op_aset(context, runtime.newSymbol("ssl_version"), ssl_version); IRubyObject verify_mode = runtime.newFixnum(VERIFY_PEER); DEFAULT_PARAMS.op_aset(context, runtime.newSymbol("verify_mode"), verify_mode); IRubyObject ciphers = StringHelper.newString(runtime, new byte[] { 'A','L','L',':', '!','A','D','H',':', '!','E','X','P','O','R','T',':', '!','S','S','L','v','2',':', 'R','C','4','+','R','S','A',':', '+','H','I','G','H',':', '+','M','E','D','I','U','M',':', '+','L','O','W' }); DEFAULT_PARAMS.op_aset(context, runtime.newSymbol("ciphers"), ciphers); IRubyObject options = runtime.newFixnum(OP_ALL); DEFAULT_PARAMS.op_aset(context, runtime.newSymbol("options"), options); SSLContext.setConstant("DEFAULT_PARAMS", DEFAULT_PARAMS); } static final int SESSION_CACHE_OFF = 0; static final int SESSION_CACHE_CLIENT = 1; static final int SESSION_CACHE_SERVER = 2; static final int SESSION_CACHE_BOTH = 3; // 1 | 2 static final int SESSION_CACHE_NO_AUTO_CLEAR = 128; static final int SESSION_CACHE_NO_INTERNAL_LOOKUP = 256; static final int SESSION_CACHE_NO_INTERNAL_STORE = 512; static final int SESSION_CACHE_NO_INTERNAL = 768; public SSLContext(Ruby runtime, RubyClass type) { super(runtime,type); } SSLContext(Ruby runtime) { super(runtime, _SSLContext(runtime)); } private String ciphers = CipherStrings.SSL_DEFAULT_CIPHER_LIST; private String protocol = "SSL"; // SSLv23 in OpenSSL by default private boolean protocolForServer = true; private boolean protocolForClient = true; private PKey t_key; private X509Cert t_cert; private int verifyResult = 1; /* avoid 0 (= X509_V_OK) just in case */ //private int sessionCacheMode; // 2 default on MRI private int sessionCacheSize; // 20480 private InternalContext internalContext; @JRubyMethod(required = 0, optional = 1, visibility = Visibility.PRIVATE) public IRubyObject initialize(IRubyObject[] args) { if ( args.length > 0 ) set_ssl_version(args[0]); return initializeImpl(); } final SSLContext initializeImpl() { return this; } @JRubyMethod public IRubyObject setup(final ThreadContext context) { final Ruby runtime = context.runtime; if ( isFrozen() ) return runtime.getNil(); synchronized(this) { if ( isFrozen() ) return runtime.getNil(); this.freeze(context); } final X509Store certStore = getCertStore(); // TODO: handle tmp_dh_callback : // #if !defined(OPENSSL_NO_DH) // if (RTEST(ossl_sslctx_get_tmp_dh_cb(self))){ // SSL_CTX_set_tmp_dh_callback(ctx, ossl_tmp_dh_callback); // } // else{ // SSL_CTX_set_tmp_dh_callback(ctx, ossl_default_tmp_dh_callback); // } // #endif IRubyObject value; value = getInstanceVariable("@key"); final PKey key; if ( value != null && ! value.isNil() ) { if ( ! ( value instanceof PKey ) ) { throw runtime.newTypeError("OpenSSL::PKey::PKey expected but got @key = " + value.inspect()); } key = (PKey) value; } else { key = getCallbackKey(context); } value = getInstanceVariable("@cert"); final X509Cert cert; if ( value != null && ! value.isNil() ) { if ( ! ( value instanceof X509Cert ) ) { throw runtime.newTypeError("OpenSSL::X509::Certificate expected but got @cert = " + value.inspect()); } cert = (X509Cert) value; } else { cert = getCallbackCert(context); } value = getInstanceVariable("@client_ca"); final List<X509AuxCertificate> clientCert; if ( value != null && ! value.isNil() ) { if ( value.respondsTo("each") ) { clientCert = convertToAuxCerts(context, value); } else { if ( ! ( value instanceof X509Cert ) ) { throw runtime.newTypeError("OpenSSL::X509::Certificate expected but got @client_ca = " + value.inspect()); } clientCert = Collections.singletonList( ((X509Cert) value).getAuxCert() ); } } else clientCert = Collections.emptyList(); value = getInstanceVariable("@extra_chain_cert"); final List<X509AuxCertificate> extraChainCert; if ( value != null && ! value.isNil() ) { extraChainCert = convertToAuxCerts(context, value); } else { extraChainCert = null; } value = getInstanceVariable("@verify_mode"); final int verifyMode; if ( value != null && ! value.isNil() ) { verifyMode = RubyNumeric.fix2int(value); } else { verifyMode = SSL.VERIFY_NONE; // 0x00 } value = getInstanceVariable("@timeout"); final int timeout; if ( value != null && ! value.isNil() ) { timeout = RubyNumeric.fix2int(value); } else { timeout = 0; } final Store store = certStore != null ? certStore.getStore() : new Store(); final String caFile = getCaFile(); final String caPath = getCaPath(); if (caFile != null || caPath != null) { try { if (store.loadLocations(runtime, caFile, caPath) == 0) { runtime.getWarnings().warn(ID.MISCELLANEOUS, "can't set verify locations"); } } catch (Exception e) { if ( e instanceof RuntimeException ) debugStackTrace(runtime, e); throw newSSLError(runtime, e); } } value = getInstanceVariable("@verify_callback"); if ( value != null && ! value.isNil() ) { store.setExtraData(1, value); } else { store.setExtraData(1, null); } value = getInstanceVariable("@verify_depth"); if ( value != null && ! value.isNil() ) { store.setDepth(RubyNumeric.fix2int(value)); } else { store.setDepth(-1); } value = getInstanceVariable("@servername_cb"); if ( value != null && ! value.isNil() ) { // SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb); } // NOTE: no API under javax.net to support session get/new/remove callbacks /* val = ossl_sslctx_get_sess_id_ctx(self); if (!NIL_P(val)){ StringValue(val); if (!SSL_CTX_set_session_id_context(ctx, (unsigned char *)RSTRING_PTR(val), RSTRING_LEN(val))){ ossl_raise(eSSLError, "SSL_CTX_set_session_id_context:"); } } if (RTEST(rb_iv_get(self, "@session_get_cb"))) { SSL_CTX_sess_set_get_cb(ctx, ossl_sslctx_session_get_cb); OSSL_Debug("SSL SESSION get callback added"); } if (RTEST(rb_iv_get(self, "@session_new_cb"))) { SSL_CTX_sess_set_new_cb(ctx, ossl_sslctx_session_new_cb); OSSL_Debug("SSL SESSION new callback added"); } if (RTEST(rb_iv_get(self, "@session_remove_cb"))) { SSL_CTX_sess_set_remove_cb(ctx, ossl_sslctx_session_remove_cb); OSSL_Debug("SSL SESSION remove callback added"); } */ try { internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert, verifyMode, timeout); } catch (GeneralSecurityException e) { throw newSSLError(runtime, e); } return runtime.getTrue(); } @JRubyMethod public RubyArray ciphers(final ThreadContext context) { return matchedCiphers(context); } private RubyArray matchedCiphers(final ThreadContext context) { final Ruby runtime = context.runtime; try { final String[] supported = getSupportedCipherSuites(this.protocol); final Collection<CipherStrings.Def> cipherDefs = CipherStrings.matchingCiphers(this.ciphers, supported, false); final IRubyObject[] cipherList = new IRubyObject[ cipherDefs.size() ]; int i = 0; for ( CipherStrings.Def def : cipherDefs ) { cipherList[i++] = runtime.newArrayNoCopy( newUTF8String(runtime, def.name), // 0 newUTF8String(runtime, sslVersionString(def.algorithms)), // 1 runtime.newFixnum(def.algStrengthBits), // 2 runtime.newFixnum(def.algBits) // 3 ); } return runtime.newArrayNoCopy(cipherList); } catch (GeneralSecurityException gse) { throw newSSLError(runtime, gse.getMessage()); } } @JRubyMethod(name = "ciphers=") public IRubyObject set_ciphers(final ThreadContext context, final IRubyObject ciphers) { if ( ciphers.isNil() ) { this.ciphers = CipherStrings.SSL_DEFAULT_CIPHER_LIST; } else if ( ciphers instanceof RubyArray ) { final RubyArray ciphs = (RubyArray) ciphers; StringBuilder cipherStr = new StringBuilder(); String sep = ""; for ( int i = 0; i < ciphs.size(); i++ ) { cipherStr.append(sep).append( ciphs.eltInternal(i).toString() ); sep = ":"; } this.ciphers = cipherStr.toString(); } else { this.ciphers = ciphers.asString().toString(); if ( "DEFAULT".equals( this.ciphers ) ) { this.ciphers = CipherStrings.SSL_DEFAULT_CIPHER_LIST; } } if ( matchedCiphers(context).isEmpty() ) { throw newSSLError(context.runtime, "no cipher match"); } return ciphers; } @JRubyMethod(name = "ssl_version=") public IRubyObject set_ssl_version(IRubyObject version) { final String versionStr; if ( version instanceof RubySymbol ) { versionStr = version.toString(); } else { versionStr = version.convertToString().toString(); } final String protocol = SSL_VERSION_OSSL2JSSE.get(versionStr); if ( protocol == null ) { throw getRuntime().newArgumentError("unknown SSL method `"+ versionStr +"'"); } this.protocol = protocol; protocolForServer = ! versionStr.endsWith("_client"); protocolForClient = ! versionStr.endsWith("_server"); return version; } final String getProtocol() { return this.protocol; } // ## // # Sets the parameters for this SSL context to the values in +params+. // # 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) // 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 // self.cert_store = DEFAULT_CERT_STORE // end // end // return params // end @JRubyMethod(optional = 1) public IRubyObject set_params(final ThreadContext context, final IRubyObject[] args) { final RubyHash params; final RubyClass SSLContext = _SSLContext(context.runtime); RubyHash DEFAULT_PARAMS = (RubyHash) SSLContext.getConstantAt("DEFAULT_PARAMS"); if ( args.length == 0 ) params = DEFAULT_PARAMS; else { params = (RubyHash) DEFAULT_PARAMS.merge(context, args[0], Block.NULL_BLOCK); } final SSLContext self = this; params.visitAll(new RubyHash.Visitor() { @Override public void visit(IRubyObject name, IRubyObject value) { self.callMethod(context, name.toString() + '=', value); } }); IRubyObject verify_mode = self.getInstanceVariable("@verify_mode"); if ( verify_mode != null && ! verify_mode.isNil() && RubyNumeric.fix2int(verify_mode) != SSL.VERIFY_NONE ) { if ( ! hasNonNilInstanceVariable(self, "@ca_file") && ! hasNonNilInstanceVariable(self, "@ca_path") && ! hasNonNilInstanceVariable(self, "@cert_store") ) { IRubyObject DEFAULT_CERT_STORE = SSLContext.getConstantAt("DEFAULT_CERT_STORE"); self.setInstanceVariable("@cert_store", DEFAULT_CERT_STORE); } } return params; } @JRubyMethod(name = "session_cache_mode") public IRubyObject session_cache_mode() { return getRuntime().getNil(); //return getRuntime().newFixnum(sessionCacheMode); } @JRubyMethod(name = "session_cache_mode=") public IRubyObject set_session_cache_mode(IRubyObject mode) { //this.sessionCacheMode = RubyInteger.fix2int(mode); //return mode; warn(getRuntime().getCurrentContext(), "SSLContext#session_cache_mode= has no effect under JRuby"); return session_cache_mode(); } @JRubyMethod(name = "session_cache_size") public IRubyObject session_cache_size() { return getRuntime().newFixnum(sessionCacheSize); } @JRubyMethod(name = "session_cache_size=") public IRubyObject set_session_cache_size(IRubyObject size) { this.sessionCacheSize = RubyInteger.fix2int(size); return size; } @JRubyMethod(name = "session_cache_stats") public RubyHash session_cache_stats(final ThreadContext context) { // NOTE: session cache NOT IMPLEMENTED // { :connect_renegotiate=>0, :cache_full=>0, :accept_good=>0, // :connect=>0, :timeouts=>0, :accept_renegotiate=>0, :accept=>0, // :cache_hits=>0, :cache_num=>0, :cb_hits=>0, :connect_good=>0, // :cache_misses=>0 } return RubyHash.newHash(context.runtime); } @JRubyMethod(name = "security_level=") public IRubyObject set_security_level(ThreadContext context, IRubyObject level) { warn(context, "WARNING: SSLContext#security_level= has no effect under JRuby"); return context.nil; } boolean isProtocolForServer() { return protocolForServer; } boolean isProtocolForClient() { return protocolForClient; } int getLastVerifyResult() { return verifyResult; } void setLastVerifyResult(int verifyResult) { this.verifyResult = verifyResult; } private static String cachedProtocol = null; private static String[] cachedSupportedCipherSuites; private static String[] getSupportedCipherSuites(final String protocol) throws GeneralSecurityException { if ( cachedProtocol == null ) { synchronized(SSLContext.class) { if ( cachedProtocol == null ) { cachedSupportedCipherSuites = dummySSLEngine(protocol).getSupportedCipherSuites(); cachedProtocol = protocol; return cachedSupportedCipherSuites; } } } if ( protocol.equals(cachedProtocol) ) return cachedSupportedCipherSuites; return dummySSLEngine(protocol).getSupportedCipherSuites(); } private static SSLEngine dummySSLEngine(final String protocol) throws GeneralSecurityException { javax.net.ssl.SSLContext sslContext = SecurityHelper.getSSLContext(protocol); sslContext.init(null, null, null); return sslContext.createSSLEngine(); } // should keep SSLContext as a member for introducin SSLSession. later... final SSLEngine createSSLEngine(String peerHost, int peerPort) throws NoSuchAlgorithmException, KeyManagementException { final SSLEngine engine; // an empty peerHost implies no SNI (RFC 3546) support requested if ( peerHost == null || peerHost.length() == 0 ) { // no hints for an internal session reuse strategy engine = internalContext.getSSLContext().createSSLEngine(); } // SNI is attempted for valid peerHost hostname on Java >= 7 // if peerHost is set to an IP address Java does not use SNI else { engine = internalContext.getSSLContext().createSSLEngine(peerHost, peerPort); } engine.setEnabledCipherSuites( getCipherSuites(engine.getSupportedCipherSuites()) ); engine.setEnabledProtocols( getEnabledProtocols(engine) ); return engine; } private String[] getCipherSuites(final String[] supported) { Collection<CipherStrings.Def> cipherDefs = CipherStrings.matchingCiphers(this.ciphers, supported, true); final String[] result = new String[ cipherDefs.size() ]; int i = 0; for ( CipherStrings.Def def : cipherDefs ) { result[ i++ ] = def.getCipherSuite(); } return result; } private String[] getEnabledProtocols(final SSLEngine engine) { final String[] enabledProtocols = ENABLED_PROTOCOLS.get(protocol); if ( enabledProtocols != null ) { final long options = getOptions(); final String[] engineProtocols = engine.getEnabledProtocols(); final List<String> protocols = new ArrayList<String>(enabledProtocols.length); for ( final String enabled : enabledProtocols ) { if (((options & SSL.OP_NO_SSLv2) != 0) && enabled.equals("SSLv2")) { continue; } if (((options & SSL.OP_NO_SSLv3) != 0) && enabled.equals("SSLv3")) { continue; } if (((options & SSL.OP_NO_TLSv1) != 0) && enabled.equals("TLSv1")) { continue; } for ( final String allowed : engineProtocols ) { if ( allowed.equals(enabled) ) protocols.add(allowed); } } return protocols.toArray( new String[ protocols.size() ] ); } return new String[0]; } private static final byte[] TLSv1 = { 'T','L','S','v','1' }; private static final byte[] SSLv2 = { 'S','S','L','v','2' }; private static final byte[] SSLv3 = { 'S','S','L','v','3' }; private ByteList sslVersionString(long bits) { final ByteList str = new ByteList(18); boolean first = true; if ( ( bits & CipherStrings.SSL_SSLV3 ) != 0 ) { if ( ! first ) str.append('/'); first = false; str.append( TLSv1 ); str.append('/'); str.append( SSLv3 ); } if ( ( bits & CipherStrings.SSL_SSLV2 ) != 0 ) { if ( ! first ) str.append('/'); // first = false; str.append( SSLv2 ); } return str; } private PKey getCallbackKey(final ThreadContext context) { if ( t_key != null ) return t_key; initFromCallback(context); return t_key; } private X509Cert getCallbackCert(final ThreadContext context) { if ( t_cert != null ) return t_cert; initFromCallback(context); return t_cert; } private void initFromCallback(final ThreadContext context) { final IRubyObject callback = getInstanceVariable("@client_cert_cb"); if ( callback != null && ! callback.isNil() ) { IRubyObject arr = callback.callMethod(context, "call", this); if ( ! ( arr instanceof RubyArray ) ) { throw context.runtime.newTypeError("expected @client_cert_cb.call to return an Array but got: " + arr.getMetaClass().getName()); } final IRubyObject cert = ((RubyArray) arr).entry(0); final IRubyObject key = ((RubyArray) arr).entry(1); if ( ! ( cert instanceof X509Cert ) ) { throw context.runtime.newTypeError(cert.inspect() + " is not an instance of OpenSSL::X509::Certificate"); } if ( ! ( key instanceof PKey ) ) { throw context.runtime.newTypeError(key.inspect() + " is not an instance of OpenSSL::PKey::PKey"); } t_cert = (X509Cert) cert; t_key = (PKey) key; } } private X509Store getCertStore() { IRubyObject cert_store = getInstanceVariable("@cert_store"); if ( cert_store instanceof X509Store ) { return (X509Store) cert_store; } return null; } private String getCaFile() { IRubyObject ca_file = getInstanceVariable("@ca_file"); if ( ca_file != null && ! ca_file.isNil() ) { return ca_file.asString().toString(); } return null; } private String getCaPath() { IRubyObject ca_path = getInstanceVariable("@ca_path"); if ( ca_path != null && ! ca_path.isNil() ) { return ca_path.asString().toString(); } return null; } private long getOptions() { IRubyObject options = getInstanceVariable("@options"); if ( options != null && ! options.isNil() ) { return RubyNumeric.fix2long(options); } return 0; } private static List<X509AuxCertificate> convertToAuxCerts(final ThreadContext context, IRubyObject value) { final RubyModule SSLContext = _SSLContext(context.runtime); final RubyModule Certificate = _Certificate(context.runtime); if ( value instanceof RubyArray ) { final RubyArray val = (RubyArray) value; final int size = val.size(); final ArrayList<X509AuxCertificate> result = new ArrayList<X509AuxCertificate>(size); for ( int i=0; i<size; i++ ) result.add( assureCertificate(context, Certificate, val.eltInternal(i)).getAuxCert() ); return result; } if ( value instanceof List ) { final List<X509Cert> val = (List) value; final int size = val.size(); final ArrayList<X509AuxCertificate> result = new ArrayList<X509AuxCertificate>(size); for ( int i=0; i<size; i++ ) result.add( assureCertificate(context, Certificate, val.get(i)).getAuxCert() ); return result; } // else : final ArrayList<X509AuxCertificate> result = new ArrayList<X509AuxCertificate>(); Utils.invoke(context, value, "each", CallBlock.newCallClosure(value, SSLContext, Arity.NO_ARGUMENTS, new BlockCallback() { public IRubyObject call(ThreadContext context, IRubyObject[] args, Block block) { result.add( assureCertificate(context, Certificate, args[0]).getAuxCert() ); return context.nil; } }, context) ); return result; } private static X509Cert assureCertificate(final ThreadContext context, final RubyModule Certificate, final IRubyObject cert) { if ( ! ( Certificate.isInstance(cert) ) ) { throw context.runtime.newTypeError("wrong argument : " + cert.inspect() + " is not a " + Certificate.getName()); } return (X509Cert) cert; } static RubyClass _SSLContext(final Ruby runtime) { return (RubyClass) _SSL(runtime).getConstantAt("SSLContext"); } private InternalContext createInternalContext(ThreadContext context, final X509Cert xCert, final PKey pKey, final Store store, final List<X509AuxCertificate> clientCert, final List<X509AuxCertificate> extraChainCert, final int verifyMode, final int timeout) throws NoSuchAlgorithmException, KeyManagementException { InternalContext internalContext = new InternalContext(xCert, pKey, store, clientCert, extraChainCert, verifyMode, timeout); internalContext.initSSLContext(context); return internalContext; } /** * c: SSL_CTX */ private class InternalContext { InternalContext( final X509Cert xCert, final PKey pKey, final Store store, final List<X509AuxCertificate> clientCert, final List<X509AuxCertificate> extraChainCert, final int verifyMode, final int timeout) throws NoSuchAlgorithmException, KeyManagementException { if ( pKey != null && xCert != null ) { this.privateKey = pKey.getPrivateKey(); this.keyAlgorithm = pKey.getAlgorithm(); this.cert = xCert.getAuxCert(); } else { this.privateKey = null; this.keyAlgorithm = null; this.cert = null; } this.store = store; this.clientCert = clientCert; this.extraChainCert = extraChainCert; this.verifyMode = verifyMode; //this.timeout = timeout; // initialize SSL context : final javax.net.ssl.SSLContext sslContext = SecurityHelper.getSSLContext(protocol); if ( protocolForClient ) { final SSLSessionContext clientContext = sslContext.getClientSessionContext(); clientContext.setSessionTimeout(timeout); if ( sessionCacheSize >= 0 ) { clientContext.setSessionCacheSize(sessionCacheSize); } } if ( protocolForServer ) { final SSLSessionContext serverContext = sslContext.getClientSessionContext(); serverContext.setSessionTimeout(timeout); if ( sessionCacheSize >= 0 ) { serverContext.setSessionCacheSize(sessionCacheSize); } } this.sslContext = sslContext; } protected void initSSLContext(final ThreadContext context) throws KeyManagementException { final KeyManager[] keyManager = new KeyManager[] { new KeyManagerImpl(this) }; final TrustManager[] trustManager = new TrustManager[] { new TrustManagerImpl(this) }; // SSLContext (internals) on Sun JDK : // private final java.security.Provider provider; "SunJSSE" // private final javax.net.ssl.SSLContextSpi; sun.security.ssl.SSLContextImpl sslContext.init(keyManager, trustManager, OpenSSL.getSecureRandomFrom(context)); // if secureRandom == null JSSE will try : // - new SecureRandom(); // - SecureRandom.getInstance("PKCS11", cryptoProvider); } final Store store; final X509AuxCertificate cert; final String keyAlgorithm; final PrivateKey privateKey; final int verifyMode; final List<X509AuxCertificate> clientCert; // assumed always != null final List<X509AuxCertificate> extraChainCert; // empty assumed == null //final int timeout; private final javax.net.ssl.SSLContext sslContext; // part of ssl_verify_cert_chain StoreContext createStoreContext(final String purpose) { if ( store == null ) return null; final StoreContext storeContext = new StoreContext(store); if ( storeContext.init(null, null) == 0 ) return null; // for verify_cb storeContext.setExtraData(1, store.getExtraData(1)); if ( purpose != null ) storeContext.setDefault(purpose); storeContext.verifyParameter.inherit(store.verifyParameter); return storeContext; } final javax.net.ssl.SSLContext getSSLContext() { return sslContext; } void setLastVerifyResult(int lastVerifyResult) { SSLContext.this.setLastVerifyResult(lastVerifyResult); } } private static class KeyManagerImpl extends X509ExtendedKeyManager { final InternalContext internalContext; KeyManagerImpl(InternalContext internalContext) { super(); this.internalContext = internalContext; } @Override public String chooseEngineClientAlias(String[] keyType, java.security.Principal[] issuers, javax.net.ssl.SSLEngine engine) { if (internalContext.privateKey == null) return null; for (int i = 0; i < keyType.length; i++) { if (keyType[i].equalsIgnoreCase(internalContext.keyAlgorithm)) { return keyType[i]; } } return null; } @Override public String chooseEngineServerAlias(String keyType, java.security.Principal[] issuers, javax.net.ssl.SSLEngine engine) { if (internalContext.privateKey == null) return null; if (keyType.equalsIgnoreCase(internalContext.keyAlgorithm)) { return keyType; } return null; } @Override public String chooseClientAlias(String[] keyType, java.security.Principal[] issuers, java.net.Socket socket) { return null; } @Override public String chooseServerAlias(String keyType, java.security.Principal[] issuers, java.net.Socket socket) { return null; } @Override // c: ssl3_output_cert_chain public java.security.cert.X509Certificate[] getCertificateChain(String alias) { final List<java.security.cert.X509Certificate> chain; if ( internalContext.extraChainCert != null ) { chain = (List) internalContext.extraChainCert; } else if ( internalContext.cert != null ) { chain = new ArrayList<java.security.cert.X509Certificate>(8); StoreContext storeCtx = internalContext.createStoreContext(null); X509AuxCertificate x = internalContext.cert; while (true) { chain.add(x); if ( x.getIssuerDN().equals(x.getSubjectDN()) ) break; try { final Name name = new Name(x.getIssuerX500Principal()); X509Object[] s_obj = new X509Object[1]; if (storeCtx.getBySubject(X509Utils.X509_LU_X509, name, s_obj) <= 0) { break; } x = ((Certificate) s_obj[0]).x509; } catch (RuntimeException e) { debugStackTrace(e); break; } catch (Exception e) { debug("KeyManagerImpl bySubject failed", e); break; } } } else { chain = Collections.EMPTY_LIST; } return chain.toArray( new java.security.cert.X509Certificate[chain.size()] ); } @Override public String[] getClientAliases(String keyType, java.security.Principal[] issuers) { return null; } @Override public java.security.PrivateKey getPrivateKey(String alias) { return internalContext.privateKey; // might be null } @Override public String[] getServerAliases(String keyType, java.security.Principal[] issuers) { return null; } } private static class TrustManagerImpl implements X509TrustManager { final InternalContext internalContext; TrustManagerImpl(InternalContext internalContext) { super(); this.internalContext = internalContext; } @Override public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { checkTrusted("ssl_client", chain); } @Override public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { checkTrusted("ssl_server", chain); } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { final int size = internalContext.clientCert.size(); return internalContext.clientCert.toArray( new java.security.cert.X509Certificate[size] ); } // c: ssl_verify_cert_chain private void checkTrusted(final String purpose, final X509Certificate[] chain) throws CertificateException { if ( chain != null && chain.length > 0 ) { if ( (internalContext.verifyMode & SSL.VERIFY_PEER) != 0 ) { // verify_peer final StoreContext storeContext = internalContext.createStoreContext(purpose); if ( storeContext == null ) { throw new CertificateException("couldn't initialize store"); } storeContext.setCertificate(chain[0]); storeContext.setChain(chain); verifyChain(storeContext); } } else { if ( (internalContext.verifyMode & SSL.VERIFY_FAIL_IF_NO_PEER_CERT) != 0 ) { // fail if no peer cert throw new CertificateException("no peer certificate"); } } } private void verifyChain(final StoreContext storeContext) throws CertificateException { final int ok; try { ok = storeContext.verifyCertificate(); } catch (Exception e) { internalContext.setLastVerifyResult(storeContext.error); if ( storeContext.error == X509Utils.V_OK ) { internalContext.setLastVerifyResult(X509Utils.V_ERR_CERT_REJECTED); } throw new CertificateException("certificate verify failed", e); } internalContext.setLastVerifyResult(storeContext.error); if ( ok == 0 ) { throw new CertificateException("certificate verify failed"); } } } }// SSLContext