/***** 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.x509store;
import org.jruby.ext.openssl.OpenSSL;
import org.jruby.ext.openssl.util.Cache;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_CERT_DIR;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_FILETYPE_ASN1;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_FILETYPE_DEFAULT;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_FILETYPE_PEM;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_LU_CRL;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_LU_FAIL;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_LU_X509;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_L_ADD_DIR;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_L_FILE_LOAD;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_R_BAD_X509_FILETYPE;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_R_INVALID_DIRECTORY;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_R_LOADING_CERT_DIR;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_R_LOADING_DEFAULTS;
import static org.jruby.ext.openssl.x509store.X509Utils.X509_R_WRONG_LOOKUP_TYPE;
import static org.jruby.ext.openssl.x509store.X509Utils.getDefaultCertificateDirectoryEnvironment;
import static org.jruby.ext.openssl.x509store.X509Utils.getDefaultCertificateFileEnvironment;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CRL;
import java.security.cert.CertificateException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import org.jruby.Ruby;
import org.jruby.RubyHash;
import org.jruby.ext.openssl.SecurityHelper;
import org.jruby.util.JRubyFile;
import org.jruby.util.SafePropertyAccessor;
import org.jruby.util.io.ChannelDescriptor;
import org.jruby.util.io.ChannelStream;
import org.jruby.util.io.FileExistsException;
import org.jruby.util.io.InvalidValueException;
import org.jruby.util.io.ModeFlags;
/**
* X509_LOOKUP
*
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
*/
public class Lookup {
boolean init = false;
boolean skip = false;
final LookupMethod method;
private final Ruby runtime;
Object methodData;
Store store;
/**
* c: X509_LOOKUP_new
*/
public Lookup(Ruby runtime, LookupMethod method) {
if ( method == null ) {
throw new IllegalArgumentException("null method");
}
this.method = method;
this.runtime = runtime;
final LookupMethod.NewItemFunction newItem = method.newItem;
if ( newItem != null && newItem != Function1.EMPTY ) {
final int result;
try {
result = newItem.call(this);
}
catch (Exception e) {
if ( e instanceof RuntimeException ) throw (RuntimeException) e;
throw new IllegalArgumentException("invalid lookup method", e);
}
if ( result == 0) throw new IllegalArgumentException("invalid lookup method");
}
}
/**
* c: X509_LOOKUP_load_file
*/
public int loadFile(CertificateFile.Path file) throws Exception {
return control(X509_L_FILE_LOAD, file.name, file.type, null);
}
/**
* c: X509_LOOKUP_add_dir
*/
public int addDir(CertificateHashDir.Dir dir) throws Exception {
return control(X509_L_ADD_DIR, dir.name, dir.type, null);
}
/**
* c: X509_LOOKUP_hash_dir
*/
public static LookupMethod hashDirLookup() {
return x509DirectoryLookup;
}
/**
* c: X509_LOOKUP_file
*/
public static LookupMethod fileLookup() {
return x509FileLookup;
}
/**
* c: X509_LOOKUP_ctrl
*/
public int control(final int cmd, final String argc, final long argl, final String[] ret) throws Exception {
if ( method == null ) return -1;
if ( method.control != null && method.control != Function5.EMPTY ) {
return method.control.call(this, Integer.valueOf(cmd), argc, Long.valueOf(argl), ret);
}
return 1;
}
private static final Cache<String, Object[]> certCache;
static {
// jruby.openssl.x509.cert.cache = true / false / 8
final String cache = SafePropertyAccessor.getProperty("jruby.openssl.x509.lookup.cache");
Cache<String, Object[]> certCacheInstance = null;
if ( cache != null ) {
try {
certCacheInstance = Cache.newStrongSoftCache( Integer.parseInt(cache) );
}
catch (NumberFormatException ex) {
if ( Boolean.parseBoolean(cache) ) {
certCacheInstance = Cache.newSoftCache();
}
}
}
if ( certCacheInstance == null ) certCacheInstance = Cache.getNullCache();
certCache = certCacheInstance;
}
/**
* c: X509_LOOKUP_load_cert_file
*/
public int loadCertificateFile(final String file, final int type) throws IOException, CertificateException {
if ( file == null ) return 1;
final Object[] cached = certCache.get(file);
BufferedReader reader = null;
try {
X509AuxCertificate auxCert;
if ( type == X509_FILETYPE_PEM ) {
int count = 0;
if ( cached != null ) {
boolean storeError = false; for ( int c = 0; c < cached.length; c++ ) {
auxCert = buildAuxFromCached((X509Certificate) cached[c]);
if ( ! storeError ) {
if ( store.addCertificate(auxCert) != 0 ) count++;
else { storeError = true; count = 0; } // return 0
}
}
}
else {
reader = new BufferedReader(new InputStreamReader(wrapJRubyNormalizedInputStream(file)));
final ArrayList<Object> cacheEntry = new ArrayList<Object>(8);
boolean storeError = false; for (;;) {
auxCert = PEMInputOutput.readX509Aux(reader, null);
if ( auxCert == null ) break;
cacheEntry.add( auxCert.cloneForCache() ); // make sure we cache aux
if ( ! storeError ) {
if ( store.addCertificate(auxCert) != 0 ) count++;
else { storeError = true; count = 0; } // return 0
}
}
certCache.put(file, cacheEntry.toArray( new Object[ cacheEntry.size() ] ));
}
return count;
}
else if ( type == X509_FILETYPE_ASN1 ) {
final X509Certificate cert;
if ( cached != null ) {
cert = (X509Certificate) cached[0];
auxCert = buildAuxFromCached(cert);
}
else {
InputStream in = wrapJRubyNormalizedInputStream(file);
cert = (X509Certificate)
SecurityHelper.getCertificateFactory("X.509").generateCertificate(in);
auxCert = new X509AuxCertificate(cert);
certCache.put(file, new Object[] { auxCert.cloneForCache() });
}
//if ( auxCert == null ) {
// X509Error.addError(13); return 0;
//}
return store.addCertificate(auxCert);
}
else {
X509Error.addError(X509_R_BAD_X509_FILETYPE);
return 0; // NOTE: really?
}
}
finally {
if ( reader != null ) {
try { reader.close(); } catch (Exception ignored) {}
}
}
}
private static X509AuxCertificate buildAuxFromCached(final X509Certificate cached) {
X509AuxCertificate auxCert = StoreContext.ensureAux(cached);
if ( cached == auxCert ) auxCert = auxCert.clone();
return auxCert;
}
/**
* c: X509_LOOKUP_load_crl_file
*/
public int loadCRLFile(final String file, final int type) throws Exception {
if ( file == null ) return 1;
BufferedReader reader = null;
try {
InputStream in = wrapJRubyNormalizedInputStream(file);
CRL crl;
if ( type == X509_FILETYPE_PEM ) {
reader = new BufferedReader(new InputStreamReader(in));
int count = 0; for (;;) {
crl = PEMInputOutput.readX509CRL(reader, null);
if ( crl == null ) break;
if ( store.addCRL(crl) == 0 ) return 0;
count++;
}
return count;
}
else if ( type == X509_FILETYPE_ASN1 ) {
crl = SecurityHelper.getCertificateFactory("X.509").generateCRL(in);
//if ( crl == null ) {
// X509Error.addError(13); return 0;
//}
return store.addCRL(crl);
}
else {
X509Error.addError(X509_R_BAD_X509_FILETYPE);
return 0; // NOTE: really?
}
}
finally {
if ( reader != null ) {
try { reader.close(); } catch (Exception ignored) {}
}
}
}
/**
* c: X509_LOOKUP_load_cert_crl_file
*/
public int loadCertificateOrCRLFile(final String file, final int type) throws IOException, CertificateException {
if ( type != X509_FILETYPE_PEM ) return loadCertificateFile(file, type);
final Object[] cached = certCache.get(file);
BufferedReader reader = null;
try {
int count = 0;
if ( cached != null ) {
for ( int c = 0; c < cached.length; c++ ) {
Object cert = cached[c];
if ( cert instanceof X509Certificate ) {
store.addCertificate(buildAuxFromCached((X509Certificate) cert));
count++;
}
else if ( cert instanceof CRL ) {
store.addCRL((CRL) cert);
count++;
}
}
}
else {
reader = new BufferedReader(new InputStreamReader(wrapJRubyNormalizedInputStream(file)));
final ArrayList<Object> cacheEntry = new ArrayList<Object>(8);
for (;;) {
Object cert = PEMInputOutput.readPEM(reader, null);
if ( cert == null ) break;
if ( cert instanceof X509Certificate ) {
X509AuxCertificate auxCert = StoreContext.ensureAux((X509Certificate) cert);
store.addCertificate(auxCert);
count++;
cert = auxCert.cloneForCache(); // make sure we cache aux
}
else if ( cert instanceof CRL ) {
store.addCRL((CRL) cert);
count++;
}
cacheEntry.add(cert);
}
certCache.put(file, cacheEntry.toArray( new Object[ cacheEntry.size() ] ));
}
return count;
}
finally {
if ( reader != null ) {
try { reader.close(); } catch (Exception ignored) {}
}
}
}
public int loadDefaultJavaCACertsFile(String certsFile) throws IOException, GeneralSecurityException {
final FileInputStream fin = new FileInputStream(certsFile);
int count = 0;
try {
// hardcode the keystore type, as we expect cacerts to be a java
// keystore - especially needed for jdk9
KeyStore keystore = SecurityHelper.getKeyStore("jks");
// null password - as the cacerts file isn't password protected
keystore.load(fin, null);
PKIXParameters params = new PKIXParameters(keystore);
for ( TrustAnchor trustAnchor : params.getTrustAnchors() ) {
X509Certificate certificate = trustAnchor.getTrustedCert();
store.addCertificate(certificate);
count++;
}
}
finally {
try { fin.close(); } catch (Exception ignored) {}
}
return count;
}
private InputStream wrapJRubyNormalizedInputStream(String file) throws IOException {
try {
return JRubyFile.createResource(runtime, file).inputStream();
}
catch (NoSuchMethodError e) { // JRubyFile.createResource.inputStream (JRuby < 1.7.17)
try {
ChannelDescriptor descriptor = ChannelDescriptor.open(runtime.getCurrentDirectory(), file, new ModeFlags(ModeFlags.RDONLY));
return ChannelStream.open(runtime, descriptor).newInputStream();
}
catch (NoSuchMethodError ex) {
File f = new File(file);
if ( ! f.isAbsolute() ) {
f = new File(runtime.getCurrentDirectory(), file);
}
return new BufferedInputStream(new FileInputStream(f));
}
catch (FileExistsException ex) {
// should not happen because ModeFlag does not contain CREAT.
OpenSSL.debugStackTrace(ex);
throw new IllegalStateException(ex);
}
catch (InvalidValueException ex) {
// should not happen because ModeFlasg does not contain APPEND.
OpenSSL.debugStackTrace(ex);
throw new IllegalStateException(ex);
}
}
}
private String envEntry(final String key) {
RubyHash env = (RubyHash) runtime.getObject().getConstant("ENV");
return (String) env.get( runtime.newString(key) );
}
/**
* c: X509_LOOKUP_free
*/
public void free() throws Exception {
if ( method != null && method.free != null && method.free != Function1.EMPTY ) {
method.free.call(this);
}
}
/**
* c: X509_LOOKUP_init
*/
public int init() throws Exception {
if ( method == null ) return 0;
if ( method.init != null && method.init != Function1.EMPTY ) {
return method.init.call(this);
}
return 1;
}
/**
* c: X509_LOOKUP_by_subject
*/
public int bySubject(final int type, final Name name, final X509Object[] ret) throws Exception {
if ( method == null || method.getBySubject == null || method.getBySubject == Function4.EMPTY ) {
return X509_LU_FAIL;
}
if ( skip ) return 0;
return method.getBySubject.call(this, Integer.valueOf(type), name, ret);
}
/**
* c: X509_LOOKUP_by_issuer_serial
*/
public int byIssuerSerialNumber(final int type, final Name name, final BigInteger serial, final X509Object[] ret) throws Exception {
if ( method == null || method.getByIssuerSerialNumber == null || method.getByIssuerSerialNumber == Function5.EMPTY ) {
return X509_LU_FAIL;
}
return method.getByIssuerSerialNumber.call(this, Integer.valueOf(type), name, serial, ret);
}
/**
* c: X509_LOOKUP_by_fingerprint
*/
public int byFingerprint(final int type, final String bytes, final X509Object[] ret) throws Exception {
if ( method == null || method.getByFingerprint == null || method.getByFingerprint == Function4.EMPTY ) {
return X509_LU_FAIL;
}
return method.getByFingerprint.call(this, Integer.valueOf(type), bytes, ret);
}
/**
* c: X509_LOOKUP_by_alias
*/
public int byAlias(final int type, final String alias, final X509Object[] ret) throws Exception {
if ( method == null || method.getByAlias == null || method.getByAlias == Function4.EMPTY ) {
return X509_LU_FAIL;
}
return method.getByAlias.call(this, Integer.valueOf(type), alias, ret);
}
/**
* c: X509_LOOKUP_shutdown
*/
public int shutdown() throws Exception {
if ( method == null ) return 0;
if ( method.shutdown != null && method.shutdown != Function1.EMPTY ) {
return method.shutdown.call(this);
}
return 1;
}
/**
* c: x509_file_lookup
*/
private final static LookupMethod x509FileLookup = new LookupMethod();
static {
x509FileLookup.name = "Load file into cache";
x509FileLookup.control = new ByFile();
}
/**
* c: x509_dir_lookup
*/
private final static LookupMethod x509DirectoryLookup = new LookupMethod();
static {
x509DirectoryLookup.name = "Load certs from files in a directory";
x509DirectoryLookup.newItem = new NewLookupDir();
x509DirectoryLookup.free = new FreeLookupDir();
x509DirectoryLookup.control = new LookupDirControl();
x509DirectoryLookup.getBySubject = new GetCertificateBySubject();
}
/**
* c: by_file_ctrl
*/
private static class ByFile implements LookupMethod.ControlFunction {
public int call(final Lookup ctx, final Integer cmd, final String argp, final Number argl, String[] ret) throws Exception {
int ok = 0;
String file = null;
final int arglInt = argl.intValue();
switch(cmd) {
case X509_L_FILE_LOAD:
if (arglInt == X509_FILETYPE_DEFAULT) {
try {
file = ctx.envEntry( getDefaultCertificateFileEnvironment() ); // ENV['SSL_CERT_FILE']
}
catch (RuntimeException e) {
OpenSSL.debug(ctx.runtime, "failed to read SSL_CERT_FILE", e);
}
if (file == null) {
file = X509Utils.X509_CERT_FILE.replace('/', File.separatorChar);
}
if (file.matches(".*\\.(crt|cer|pem)$")) {
ok = ctx.loadCertificateOrCRLFile(file, X509_FILETYPE_PEM) != 0 ? 1 : 0;
} else {
ok = (ctx.loadDefaultJavaCACertsFile(file) != 0) ? 1: 0;
}
// we ignore errors on loading default paths the same way MRI does it
} else {
if (arglInt == X509_FILETYPE_PEM) {
ok = (ctx.loadCertificateOrCRLFile(argp, X509_FILETYPE_PEM) != 0) ? 1 : 0;
} else {
ok = (ctx.loadCertificateFile(argp, arglInt) != 0) ? 1 : 0;
}
}
break;
}
return ok;
}
}
/**
* c: BY_DIR, lookup_dir_st
*/
private static class LookupDir {
Collection<String> dirs;
Collection<Integer> dirsType;
}
/**
* c: new_dir
*/
private static class NewLookupDir implements LookupMethod.NewItemFunction {
public int call(final Lookup lookup) {
final LookupDir lookupDir = new LookupDir();
lookupDir.dirs = new ArrayList<String>();
lookupDir.dirsType = new ArrayList<Integer>();
lookup.methodData = lookupDir;
return 1;
}
}
/**
* c: free_dir
*/
private static class FreeLookupDir implements LookupMethod.FreeFunction {
public int call(final Lookup lookup) {
final LookupDir lookupDir = (LookupDir) lookup.methodData;
lookupDir.dirs = null;
lookupDir.dirsType = null;
lookup.methodData = null;
return -1;
}
}
/**
* c: dir_ctrl
*/
private static class LookupDirControl implements LookupMethod.ControlFunction {
public int call(final Lookup ctx, final Integer cmd, String argp, Number argl, String[] retp) {
int ret = 0;
final LookupDir lookupData = (LookupDir) ctx.methodData;
switch ( cmd ) {
case X509_L_ADD_DIR :
if ( argl.intValue() == X509_FILETYPE_DEFAULT ) {
String certDir = null;
try {
certDir = getDefaultCertificateDirectory(ctx);
}
catch (RuntimeException e) { }
if ( certDir != null ) {
ret = addCertificateDirectory(lookupData, certDir, X509_FILETYPE_PEM);
} else {
ret = addCertificateDirectory(lookupData, X509_CERT_DIR, X509_FILETYPE_PEM);
}
if ( ret == 0 ) {
X509Error.addError(X509_R_LOADING_CERT_DIR);
}
}
else {
ret = addCertificateDirectory(lookupData, argp, argl.intValue());
}
break;
}
return ret;
}
private static String getDefaultCertificateDirectory(final Lookup ctx) {
return ctx.envEntry( getDefaultCertificateDirectoryEnvironment() );
}
/**
* c: add_cert_dir
*/
private int addCertificateDirectory(final LookupDir ctx, final String dir, final int type) {
if ( dir == null || dir.isEmpty() ) {
X509Error.addError(X509_R_INVALID_DIRECTORY);
return 0;
}
String[] dirs = dir.split(File.pathSeparator);
for ( int i=0; i<dirs.length; i++ ) {
if ( dirs[i].length() == 0 ) {
continue;
}
if ( ctx.dirs.contains(dirs[i]) ) {
continue;
}
ctx.dirsType.add(type);
ctx.dirs.add(dirs[i]);
}
return 1;
}
}
/**
* c: get_cert_by_subject
*/
private static class GetCertificateBySubject implements LookupMethod.BySubjectFunction {
public int call(final Lookup lookup, final Integer type, final Name name, final X509Object[] ret) throws Exception {
if ( name == null ) return 0;
int ok = 0;
final String postfix;
if ( type == X509_LU_X509 ) {
postfix = "";
}
else if ( type == X509_LU_CRL ) {
postfix = "r";
}
else {
X509Error.addError(X509_R_WRONG_LOOKUP_TYPE);
return ok;
}
final LookupDir context = (LookupDir) lookup.methodData;
final String hash = String.format("%08x", name.hash());
final StringBuilder buffer = new StringBuilder(48);
final Iterator<Integer> iter = context.dirsType.iterator();
for ( final String dir : context.dirs ) {
final int dirType = iter.next();
for ( int k = 0; ; k++ ) {
buffer.setLength(0); // reset - clear buffer
buffer.append(dir).append(File.separatorChar);
buffer.append(hash);
buffer.append('.').append(postfix).append(k);
final String path = buffer.toString();
if ( ! new File(path).exists() ) break;
if ( type == X509_LU_X509 ) {
if ( lookup.loadCertificateFile(path, dirType) == 0 ) {
break;
}
} else if ( type == X509_LU_CRL ) {
if ( lookup.loadCRLFile(path, dirType) == 0 ) {
break;
}
}
}
X509Object tmp = null;
for ( X509Object obj : lookup.store.getObjects() ) {
if ( obj.type() == type && obj.isName(name) ) {
tmp = obj; break;
}
}
if ( tmp != null ) {
ok = 1; ret[0] = tmp; break;
}
}
return ok;
}
}
}// X509_LOOKUP