/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.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.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package net.jini.jeri.ssl;

import java.net.Socket;
import java.security.AccessController;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;
import net.jini.io.UnsupportedConstraintException;
import net.jini.security.AuthenticationPermission;

/**
 * An AuthManager for clients.  Uses the fact that client connections only
 * share SSLContexts after a single client and server principal have been
 * chosen.
 *
 * @author Sun Microsystems, Inc.
 */
class ClientAuthManager extends AuthManager {

    /* -- Fields -- */

    /** Client logger */
    private static final Logger logger = clientLogger;

    /** The server certificate chosen by the first handshake. */
    private X509Certificate serverCredential;

    /** The server principal chosen by the first handshake. */
    private X500Principal serverPrincipal;

    /**
     * The private credential supplied by chooseClientAlias in the last
     * handshake or null if none was supplied.
     */
    private X500PrivateCredential clientCredential;

    /** The client principal chosen by the first handshake. */
    private X500Principal clientPrincipal;

    /**
     * The exception that occurred within the last call to chooseClientAlias if
     * no credential could be supplied.
     */
    private Exception clientCredentialException;

    /**
     * The latest time for which all client and server credentials remain
     * valid.
     */
    private long credentialsValidUntil = 0;

    /** The permission to check for the last cached credential */
    private AuthenticationPermission authenticationPermission;

    /* -- Constructors -- */

    /**
     * Creates an AuthManager that retrieves principals and credentials for
     * authentication from the specified subject.  If permittedLocalPrincipals
     * is non-null, then the principals used for authentication are restricted
     * to the elements of that set.  If permittedRemotePrincipals is non-null,
     * then the server principals accepted are restricted to the elements of
     * that set.
     *
     * @param subject the subject for retrieving principals and credentials
     * @param permittedLocalPrincipals if non-null, then only principals in
     *	      this set may be used for authentication
     * @param permittedRemotePrincipals if non-null, then only principals in
     *	      this set will be trusted when authenticating the peer
     * @throws NoSuchAlgorithmException if the trust manager factory algorithm
     *	       is not found
     */
    ClientAuthManager(Subject subject,
		      Set permittedLocalPrincipals,
		      Set permittedRemotePrincipals)
	throws NoSuchAlgorithmException
    {
	super(subject, permittedLocalPrincipals, permittedRemotePrincipals);
    }

    /* -- Methods -- */

    /**
     * Returns true if the last handshake authenticated the client, else
     * false.
     */
    synchronized boolean getClientAuthenticated() {
	return clientCredential != null;
    }

    /**
     * Returns the last SecurityException or GeneralSecurityException that
     * occurred when attempting to choose client credentials, or null if no
     * exception occurred.
     */
    synchronized Exception getClientCredentialException() {
	return clientCredentialException;
    }

    /**
     * Checks if the subject still contains the proper credentials, and the
     * current access control context has the proper AuthenticationPermission,
     * to use the current session.  Callers should only call this method if
     * client authentication is being used.
     *
     * @param constraints the requested constraints
     * @param serverSubject the server subject for the connection
     * @throws SecurityException if the access control context does not have
     *	       the proper AuthenticationPermission
     * @throws UnsupportedConstraintException if the subject does not contain
     *	       the proper credentials
     */
    synchronized void checkAuthentication()
	throws UnsupportedConstraintException
    {
	if (clientCredential == null) {
	    throw new UnsupportedConstraintException(
		"Client is not authenticated");
	} else if (clientCredential.isDestroyed()) {
	    throw new UnsupportedConstraintException(
		"Private credentials are destroyed");
	} else if (System.currentTimeMillis() > credentialsValidUntil) {
	    throw new UnsupportedConstraintException(
		"Certificates are no longer valid");
	}
	if (subjectIsReadOnly) {
	    SecurityManager sm = System.getSecurityManager();
	    if (sm != null) {
		sm.checkPermission(authenticationPermission);
	    }
	} else {
	    Subject subject = getSubject();
	    X509Certificate cert = clientCredential.getCertificate();
	    if (SubjectCredentials.getPrincipal(subject, cert) == null) {
		throw new UnsupportedConstraintException("Missing principal");
	    }
	    CertPath chain =
		SubjectCredentials.getCertificateChain(subject, cert);
	    if (chain == null) {
		throw new UnsupportedConstraintException(
		    "Missing public credentials");
	    }
	    X500PrivateCredential pc = getPrivateCredential(
		cert, authenticationPermission);
	    if (pc == null) {
		throw new UnsupportedConstraintException(
		    "Missing private credentials");
	    } else if (!equalPrivateCredentials(clientCredential, pc)) {
		throw new UnsupportedConstraintException(
		    "Wrong private credentials");
	    }
	}
    }

    /**
     * Gets the private credential for the specified X.509 certificate,
     * checking for AuthenticationPermission to connect with the last server
     * principal.
     *
     * @param cert the certificate for the local principal
     * @return the associated private credential or null if not found
     * @throws SecurityException if the access control context does not have
     *	       the proper AuthenticationPermission
     */
    synchronized X500PrivateCredential getPrivateCredential(
	X509Certificate cert)
    {
	return getPrivateCredential(cert, getAuthenticationPermission(cert));
    }

    /**
     * Gets the private credential for the specified X.509 certificate,
     * checking for the specified AuthenticationPermission.
     *
     * @param cert the certificate for the local principal
     * @param ap the permission needed to connect to the peer
     * @return the associated private credential or null if not found
     * @throws SecurityException if the access control context does not have
     *	       the proper AuthenticationPermission
     */
    private X500PrivateCredential getPrivateCredential(
	X509Certificate cert, AuthenticationPermission ap)
    {
	Subject subject = getSubject();
	if (subject == null) {
	    return null;
	}
	SecurityManager sm = System.getSecurityManager();
	if (sm != null) {
	    sm.checkPermission(ap);
	}
	return (X500PrivateCredential) AccessController.doPrivileged(
	    new SubjectCredentials.GetPrivateCredentialAction(
		subject, cert));
    }

    /** Returns the client logger */
    Logger getLogger() {
	return logger;
    }

    /**
     * Returns the permission needed to connect to the last server principal
     * with the specified client certificate.
     */
    private AuthenticationPermission getAuthenticationPermission(
	X509Certificate cert)
    {
	Set client = Collections.singleton(cert.getSubjectX500Principal());
	Set server = (serverPrincipal == null)
	    ? null : Collections.singleton(serverPrincipal);
	return new AuthenticationPermission(client, server, "connect");
    }

    /** Returns the server principal chosen. */
    synchronized X500Principal getServerPrincipal() {
	return serverPrincipal;
    }

    /** Returns the client principal chosen. */
    synchronized X500Principal getClientPrincipal() {
	return clientPrincipal;
    }

    /* -- X500TrustManager -- */

    /**
     * Override this X509TrustManager method in order to cache the server
     * principal and to continue to choose the same one.
     */
    public synchronized void checkServerTrusted(X509Certificate[] chain,
						String authType)
	throws CertificateException
    {
	super.checkServerTrusted(chain, authType);
	if (serverPrincipal == null) {
	    serverCredential = chain[0];
	    serverPrincipal = serverCredential.getSubjectX500Principal();
	    setPermittedRemotePrincipals(
		Collections.singleton(serverPrincipal));
	    credentialsValidUntil = certificatesValidUntil(chain);
	} else if (!serverCredential.equals(chain[0])) {
	    throw new CertificateException("Server credentials changed");
	}
    }

    /* -- Implement X509KeyManager -- */

    public String[] getClientAliases(String keyType, Principal[] issuers) {
	String[] result = getAliases(keyType, issuers);
	if (logger.isLoggable(Level.FINE)) {
	    logger.log(Level.FINE,
		       "get client aliases for key type {0}\n" +
		       "and issuers {1}\nreturns {2}",
		       new Object[] {
			   keyType, toString(issuers), toString(result)
		       });
	}
	return result;
    }
    
    public String[] getServerAliases(String keyType, Principal[] issuers) {
	return null;
    }

    public synchronized String chooseClientAlias(
	String[] keyTypes, Principal[] issuers, Socket socket)
    {
	/*
	 * Only choose new client credentials for the first handshake.
	 * Otherwise, just use the previous client credentials.
	 */
	if (clientCredentialException != null) {
	    return null;
	} else if (clientCredential == null) {
	    List exceptions = null;
	    for (int i = 0; i < keyTypes.length; i++) {
		Exception exception;
		try {
		    clientCredential = chooseCredential(keyTypes[i], issuers);
		    if (clientCredential != null) {
			break;
		    }
		    continue;
		} catch (GeneralSecurityException e) {
		    exception = e;
		} catch (SecurityException e) {
		    exception = e;
		}
		if (exceptions == null) {
		    exceptions = new ArrayList();
		}
		exceptions.add(exception);
	    }
	    if (clientCredential == null) {
		if (exceptions == null) {
		    clientCredentialException =
			new GeneralSecurityException("Credentials not found");
		} else if (exceptions.size() == 1) {
		    clientCredentialException = (Exception) exceptions.get(0);
		} else {
		    for (int i = exceptions.size(); --i >= 0; ) {
			Exception e = (Exception) exceptions.get(i);
			if (!(e instanceof SecurityException)) {
			    clientCredentialException =
				new GeneralSecurityException(
				    exceptions.toString());
			    break;
			}
		    }
		    if (clientCredentialException == null) {
			clientCredentialException = 
			    new SecurityException(exceptions.toString());
		    }
		}
		return null;
	    }
	}
	X509Certificate cert = clientCredential.getCertificate();
	clientPrincipal = cert.getSubjectX500Principal();
	credentialsValidUntil =
	    Math.min(credentialsValidUntil,
		     certificatesValidUntil(
			 SubjectCredentials.getCertificateChain(
			     getSubject(), cert)));
	authenticationPermission = getAuthenticationPermission(cert);
	String result = SubjectCredentials.getCertificateName(
	    clientCredential.getCertificate());
	if (logger.isLoggable(Level.FINE)) {
	    logger.log(
		Level.FINE,
		"choose client alias for key types {0}\nand issuers {1}\n" +
		"returns {2}",
		new Object[] { toString(keyTypes), toString(issuers), result });
	}
	return result;
    }

    public String chooseServerAlias(
	String keyType, Principal[] issuers, Socket socket)
    {
	return null;
    }
}
