/**********************************************************************
 *
 * Copyright (c) 2004 Olaf Willuhn
 * All rights reserved.
 * 
 * This software is copyrighted work licensed under the terms of the
 * Jameica License.  Please consult the file "LICENSE" for details. 
 *
 **********************************************************************/

package de.willuhn.jameica.security;

import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.Arrays;

import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import de.willuhn.jameica.system.Application;
import de.willuhn.jameica.system.OperationCanceledException;
import de.willuhn.logging.Level;
import de.willuhn.logging.Logger;


/**
 * Unser eigener TrustManager fuer die Zertifikatspruefung zwischen Client und Server.
 */
public class JameicaTrustManager implements X509TrustManager
{
  private X509TrustManager systemTrustManager   = null;
  private X509TrustManager parentTrustManager   = null;
  private CertPathValidator validator           = null;


  /**
   * ct.
   * @throws KeyStoreException
   * @throws Exception
   */
  public JameicaTrustManager() throws KeyStoreException, Exception
  {
    this.validator = CertPathValidator.getInstance("PKIX",BouncyCastleProvider.PROVIDER_NAME);
    
    if (Application.getConfig().getTrustJavaCerts())
    {
      Logger.info("trusting java trustmanager");
      this.parentTrustManager = getSystemTrustManager();
    }
    else
    {
      Logger.info("system trustmanager disabled, will use only jameicas trustmanager");
    }
  }

  /**
   * Liefert den System-Trustmanager von Java.
   * @return der System-Trustmanager.
   * @throws Exception
   */
  public synchronized X509TrustManager getSystemTrustManager() throws Exception
  {
    if (this.systemTrustManager != null)
      return this.systemTrustManager;
    
    // Wir ermitteln den System-TrustManager.
    // und lassen die Zertifikatspruefungen erstmal von dem machen
    // Nur wenn er die Zertifikate als nicht vertrauenswuerdig
    // einstuft, greifen wir ein und checken, ob wir das Zertifikat
    // in unserem eigenen Keystore haben.
    TrustManagerFactory factory = null;
    try
    {
      Logger.info("loading trustmanager SunX509");
      factory = TrustManagerFactory.getInstance("SunX509");
    }
    catch (NoSuchAlgorithmException e)
    {
      Logger.info("failed. fallback to IbmX509");
      factory = TrustManagerFactory.getInstance("IbmX509");
    }

    factory.init((KeyStore) null); // Wir initialisieren mit <code>null</code>, damit der System-Keystore genommen wird

    TrustManager[] trustmanagers = factory.getTrustManagers();
    if (trustmanagers == null || trustmanagers.length == 0)
    {
      Logger.warn("NO system trustmanager found");
      return null;
    }

    // uns interessiert nur der erste. Das ist der von Java selbst.
    this.systemTrustManager = (X509TrustManager) trustmanagers[0];
    return this.systemTrustManager;
  }
  
  /**
   * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], java.lang.String)
   */
  @Override
  public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
  {
    if (this.parentTrustManager != null)
    {
      try
      {
        Logger.debug("checking client certificate via system trustmanager");
        this.parentTrustManager.checkClientTrusted(chain,authType);
        Logger.info("client certificate trusted via system trustmanager [vendor: " + System.getProperty("java.vendor") + "]");
      }
      catch (CertificateException c)
      {
        Logger.debug("client certificate not found in system trustmanager, trying jameica trustmanager");
        Logger.write(Level.DEBUG,"CertificateException for debugging",c);
        this.checkTrusted(chain,authType);
      }
    }
    else
    {
      Logger.info("no system trustmanager found, checking client certificate via jameica trustmanager");
      this.checkTrusted(chain,authType);
    }
  }

  /**
   * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], java.lang.String)
   */
  @Override
  public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException
  {
    if (this.parentTrustManager != null)
    {
      try
      {
        Logger.debug("checking server certificate via system trustmanager");
        this.parentTrustManager.checkServerTrusted(certificates,authType);
        Logger.debug("server certificate trusted via system trustmanager [vendor: " + System.getProperty("java.vendor") + "]");
      }
      catch (CertificateException c)
      {
        Logger.debug("server certificate not found in system trustmanager, trying jameica trustmanager");
        
        ////////////////////////////////////////////////////////////////////////
        if (Logger.isLogging(Level.DEBUG))
        {
          Logger.write(Level.DEBUG,"CertificateException for debugging",c);
          X509Certificate[] trusted = getAcceptedIssuers();
          if (trusted != null && trusted.length > 0)
          {
            Logger.debug("jameica keystore contains the following certificates:");
            for (int i=0;i<trusted.length;++i)
              Logger.debug("  " + toString(trusted[i]));
          }
          else
          {
            Logger.debug("jameica keystore contains no certificates");
          }
        }
        ////////////////////////////////////////////////////////////////////////
        this.checkTrusted(certificates,authType);
      }
    }
    else
    {
      Logger.debug("no system trustmanager defined, checking server certificate via jameica trustmanager");
      this.checkTrusted(certificates,authType);
    }
  }


  /**
   * Checkt die Zertifikate.
   * @param chain
   * @param authType
   * @throws CertificateException
   */
  private void checkTrusted(X509Certificate[] chain, String authType) throws CertificateException
  {
    if (chain == null || chain.length == 0)
      throw new IllegalArgumentException("no certificates given to check, strange!");
    
    if (Logger.isLogging(Level.DEBUG))
    {
      Logger.debug("checking cert chain");
      for (int i=0;i<chain.length;++i)
        Logger.debug("  " + toString(chain[i]));
    }

    SSLFactory factory = Application.getSSLFactory();

    try
    {
      CertPath certPath = factory.getCertificateFactory().generateCertPath(Arrays.asList(chain));
      X509Certificate cert = (X509Certificate) certPath.getCertificates().get(0);

      boolean verified = false;
      
      // Code angelehnt an http://www.java2s.com/Open-Source/Java-Document/Apache-Harmony-Java-SE/org-package/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java.htm
      try
      {
        // Wir lassen erstmal den Validator checken. Der prueft aber streng,
        // dass die komplette Kette vertrauenswuerdig ist. Falls wir aber
        // ein Peer-Zertifikat haben, bei dem nur das Peer bei uns im Keystore
        // ist, das auststellende CA aber nicht, wirft der Validator eine
        // Exception. Daher pruefen wir vor dem Import noch, ob wir das
        // Peer selbst kennen.
        PKIXParameters params = new PKIXParameters(factory.getKeyStore());
        params.setRevocationEnabled(false); // wir haben keine CRLs
        validator.validate(certPath,params);
        Logger.debug("certificate chain trusted: " + toString(cert));
        verified = true;
      }
      catch (GeneralSecurityException e)
      {
        // OK, die Chain ist zwar unvollstaendig. Aber vielleicht kennen
        // wir das Peer selbst.
        X509Certificate[] trusted = this.getAcceptedIssuers();
        for (X509Certificate c:trusted)
        {
          if (cert.equals(c))
          {
            verified = true;
            Logger.debug("peer certificate trusted: " + toString(c));
            break;
          }
        }
      }

      
      // So, jetzt checken wir noch die Gueltigkeit
      if (verified)
      {
        DateFormat df = DateFormat.getDateInstance(DateFormat.DEFAULT, Application.getConfig().getLocale());
        String validFrom = df.format(cert.getNotBefore());
        String validTo   = df.format(cert.getNotAfter());
        try
        {
          cert.checkValidity();
          Logger.debug("validity: " + validFrom + " - " + validTo);
          return; // Alles i.O.
        }
        catch (CertificateExpiredException exp)
        {
          Logger.warn("certificate expired: " + validFrom + " - " + validTo);
          if (Application.getCallback().askUser(Application.getI18n().tr("Zertifikat abgelaufen. Trotzdem vertrauen?\nGültigkeit: {0} - {1}",new String[]{validFrom,validTo})))
            return; // Abgelaufen, aber der User ist damit einverstanden
        }
        catch (CertificateNotYetValidException not)
        {
          Logger.warn("certificate not yet valid: " + validFrom + " - " + validTo);
          if (Application.getCallback().askUser(Application.getI18n().tr("Zertifikat noch nicht gültig. Trotzdem vertrauen?\nGültigkeit: {0} - {1}",new String[]{validFrom,validTo})))
            return; // Noch nicht gueltig, aber der User ist damit einverstanden
        }
      }
      
      
      // nicht vertrauenswuerdig, also importieren
      // Wir uebernehmen nur das direkte Peer-Zertifikat. Ggf.
      // drueber haengende CA-Zertifikate nicht
      Logger.warn("import certificate: " + toString(cert));
      factory.addTrustedCertificate(cert);

      
    }
    catch (OperationCanceledException oe)
    {
      Logger.warn("operation cancelled");
      throw oe;
    }
    catch (Exception e)
    {
      Logger.error("error while checking trust",e);
      throw new CertificateException(Application.getI18n().tr("Fehler beim Prüfen des Zertifikats"));
    }
  }

  /**
   * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
   */
  @Override
  public X509Certificate[] getAcceptedIssuers()
  {
    try
    {
      return Application.getSSLFactory().getTrustedCertificates();
    }
    catch (Exception e)
    {
      Logger.error("unable to load trusted certificates, fallback to system trustmanager",e);
    }
    if (this.parentTrustManager != null)
      return this.parentTrustManager.getAcceptedIssuers();
    return new X509Certificate[0];
  }

  /**
   * Liefert eine textuelle Zusammenfassung ueber den Zertifikatsinhalt.
   * @param cert das Zertifikat.
   * @return String mit den wichtigsten Eckdaten.
   */
  private String toString(X509Certificate cert)
  {
    StringBuffer sb = new StringBuffer();
    sb.append("[subject: ");
    sb.append(cert.getSubjectDN().getName());
    sb.append("][valid from: ");
    sb.append(cert.getNotBefore().toString());
    sb.append(" to: ");
    sb.append(cert.getNotAfter().toString());
    sb.append("][serial: ");
    sb.append("0x" + cert.getSerialNumber().toString(16).toUpperCase());
    sb.append("]");
    return sb.toString();
  }
}

/**********************************************************************
 * $Log: JameicaTrustManager.java,v $
 * Revision 1.27  2011/06/27 17:51:43  willuhn
 * @N Man kann sich jetzt die Liste der von Java bereits mitgelieferten Aussteller-Zertifikate unter Datei->Einstellungen anzeigen lassen - um mal einen Ueberblick zu kriegen, wem man so eigentlich alles blind vertraut ;)
 * @N Mit der neuen Option "Aussteller-Zertifikaten von Java vertrauen" kann man die Vertrauensstellung zu diesen Zertifikaten deaktivieren - dann muss der User jedes Zertifikate explizit bestaetigen - auch wenn Java die CA kennt
 *
 * Revision 1.26  2010-12-17 16:49:29  willuhn
 * @C IllegalArgumentException werfen, wenn keine zu pruefenden Zertifikate uebergeben wurden. Mach die Standard-Impl (javax.net.ssl.X509TrustManager) auch so.
 *
 * Revision 1.25  2010-12-08 16:02:28  willuhn
 * @C Log-Level auf DEBUG geaendert
 *
 * Revision 1.24  2010/03/25 12:59:08  willuhn
 * @N InvalidAlgorithmParameterException ebenfalls fangen
 *
 * Revision 1.23  2010/03/11 14:43:57  willuhn
 * @N TrustManager ueberarbeitet
 *
 * Revision 1.22  2010/03/11 09:45:56  willuhn
 * @R Debug-Ausgaben entfernt
 *
 * Revision 1.21  2010/03/11 09:45:20  willuhn
 * @B System-Properties entfernt - fuehrte dazu, dass auch der System-Truststore nur die Jameica-Zertifikate kannte.
 *
 * Revision 1.20  2009/05/13 21:48:36  willuhn
 * @D typo
 *
 * Revision 1.19  2009/03/11 23:12:06  willuhn
 * @R unnuetzes return-Statement
 *
 * Revision 1.18  2009/01/18 00:03:46  willuhn
 * @N SSLFactory#addTrustedCertificate() liefert jetzt den erzeugten Alias-Namen des Keystore-Entries
 * @N SSLFactory#getTrustedCertificate(String) zum Abrufen eines konkreten Zertifikates
 **********************************************************************/