/***** 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