/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.jini.jeri.ssl;
import com.sun.jini.logging.Levels;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
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.X509Certificate;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;
import net.jini.security.AuthenticationPermission;
/**
* An AuthManager for servers. Invalidates sessions when a new key is returned
* of a particular key type.
*
* @author Sun Microsystems, Inc.
*/
class ServerAuthManager extends AuthManager {
/* -- Fields -- */
/** Server transport logger */
private static final Logger logger = serverLogger;
/** The SSLSessionContext for all connections. */
private final SSLSessionContext sslSessionContext;
/** The subject's private credentials, if the subject is read-only. */
private final X500PrivateCredential[] readOnlyPrivateCredentials;
/**
* Maps a key type to last private credentials returned for that key type,
* or a String describing problems that prevented getting private
* credentials.
*/
private final Map credentialCache = new HashMap(2);
/** The SSL session for the last successful call to checkCredentials. */
private Reference sessionCache = new SoftReference(null);
/**
* The time when the credentials for the session in the session cache
* become invalid.
*/
private long credentialsValidUntil = 0;
/* -- Constructors -- */
/**
* Creates an AuthManager that retrieves principals and credentials for
* authentication from the specified subject.
*
* @param subject the subject for retrieving principals and credentials
* @throws NoSuchAlgorithmException if the trust manager factory algorithm
* is not found
*/
ServerAuthManager(Subject subject,
Set permittedPrincipals,
SSLSessionContext sslSessionContext)
throws NoSuchAlgorithmException
{
super(subject, permittedPrincipals, null);
this.sslSessionContext = sslSessionContext;
readOnlyPrivateCredentials =
!subjectIsReadOnly || subject == null ? null
: (X500PrivateCredential[]) AccessController.doPrivileged(
new SubjectCredentials.GetAllPrivateCredentialsAction(
subject));
}
/* -- Methods -- */
/**
* Returns the principal that the server used to authenticate for the
* specified session. Returns null if the session is not found or if the
* server did not authenticate itself.
*/
X509Certificate getServerCertificate(SSLSession session) {
synchronized (credentialCache) {
if (sslSessionContext.getSession(session.getId()) != null) {
Object val = credentialCache.get(
getKeyAlgorithm(session.getCipherSuite()));
if (val instanceof X500PrivateCredential) {
X500PrivateCredential cred = (X500PrivateCredential) val;
if (!cred.isDestroyed()) {
return cred.getCertificate();
}
}
}
return null;
}
}
/**
* Checks if the server subject still contains the proper credentials to
* use the specified session. Uses the credential cache to find the
* credentials for sessions with this session's key type. Callers should
* only call this method if server authentication is being used.
*
* @param session the session to check
* @param clientSubject the client subject for the connection, which should
* be read-only if it is not null
* @throws GeneralSecurityException if there is a problem with the
* credentials
* @throws SecurityException if the current access control context does not
* have the proper AuthenticationPermission or if the subject does
* not contain the proper credentials
*/
void checkCredentials(SSLSession session, Subject clientSubject)
throws GeneralSecurityException
{
synchronized (credentialCache) {
if (sslSessionContext.getSession(session.getId()) == null) {
throw new SecurityException("Session not valid");
}
Object val = credentialCache.get(
getKeyAlgorithm(session.getCipherSuite()));
if (val == null) {
throw new SecurityException(
"No credential cached for key type");
} else if (val instanceof String) {
throw new SecurityException((String) val);
}
X500PrivateCredential cred = (X500PrivateCredential) val;
if (cred.isDestroyed()) {
throw new SecurityException(
"Private credentials are destroyed");
} else if (subjectIsReadOnly
&& session.equals(sessionCache.get())
&& System.currentTimeMillis() < credentialsValidUntil)
{
return;
} else {
credentialsValidUntil = checkCredentials(
cred, clientSubject, "accept");
sessionCache = new SoftReference(session);
}
}
}
/**
* Checks that the principals and credentials associated with the specified
* private credential are present and valid in the server subject, and that
* the caller has permission to access them given the specified client
* subject and permission action. Returns the time until which the
* certificates are valid if successful, otherwise throws
* SecurityException. The clientSubject should be read-only if it is not
* null.
*/
private long checkCredentials(X500PrivateCredential cred,
Subject clientSubject,
String permissionAction)
{
Subject subject = getSubject();
if (subject == null) {
throw new SecurityException("Missing subject");
}
X509Certificate cert = cred.getCertificate();
if (SubjectCredentials.getPrincipal(subject, cert) == null) {
throw new SecurityException("Missing principal");
}
CertPath chain =
SubjectCredentials.getCertificateChain(subject, cert);
if (chain == null) {
throw new SecurityException("Missing public credentials");
}
long validUntil = certificatesValidUntil(chain);
if (clientSubject != null) {
assert clientSubject.isReadOnly();
CertPath clientChain = (CertPath)
clientSubject.getPublicCredentials().iterator().next();
validUntil = Math.min(
validUntil, certificatesValidUntil(clientChain));
}
if (System.currentTimeMillis() > validUntil) {
throw new SecurityException("Certificates no longer valid");
}
String peer = getPeerPrincipalName(clientSubject);
X500PrivateCredential pc =
getPrivateCredential(cert, peer, permissionAction);
if (pc == null) {
throw new SecurityException("Missing private credentials");
} else if (!equalPrivateCredentials(cred, pc)) {
throw new SecurityException("Wrong private credential");
}
return validUntil;
}
/**
* Returns the name of the principal for the peer subject, which should be
* read-only if it is not null.
*/
private String getPeerPrincipalName(Subject peerSubject) {
if (peerSubject == null) {
return null;
}
assert peerSubject.isReadOnly();
Principal p =
(Principal) peerSubject.getPrincipals().iterator().next();
return p.getName();
}
/** Returns the server logger */
Logger getLogger() {
return logger;
}
/**
* Gets the private credential for the specified X.509 certificate,
* checking for AuthenticationPermission to listen for the specified local
* principal and all peers.
*
* @param cert the certificate for the local principal
* @return the associated private credential or null if not found
* @throws SecurityException if the current access control context does not
* have the proper AuthenticationPermission
*/
X500PrivateCredential getPrivateCredential(X509Certificate cert) {
return getPrivateCredential(cert, (String) null, "listen");
}
/**
* Checks for AuthenticationPermission to accept for the specified local
* and peer principals. The peer is specified as a String to avoid needing
* to use the separate X.509 certificate type that JSSE uses for peer
* certificate chains.
*
* @param cert the certificate for the local principal
* @param peer the name of the peer principal or null if not known
* @param permissionAction the AuthenticationPermission action
* @return the associated private credential or null if not found
* @throws SecurityException if the current access control context does not
* have the proper AuthenticationPermission
*/
private X500PrivateCredential getPrivateCredential(X509Certificate cert,
String peer,
String permissionAction)
{
Subject subject = getSubject();
if (subject == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
getAuthPermission(cert, peer, permissionAction));
}
if (subjectIsReadOnly) {
for (int i = readOnlyPrivateCredentials.length; --i >= 0; ) {
X500PrivateCredential xpc = readOnlyPrivateCredentials[i];
if (cert.equals(xpc.getCertificate())) {
return xpc;
}
}
return null;
}
return (X500PrivateCredential) AccessController.doPrivileged(
new SubjectCredentials.GetPrivateCredentialAction(
subject, cert));
}
/**
* Returns the authentication permission for the specified principals and
* action.
*/
private AuthenticationPermission getAuthPermission(X509Certificate cert,
String peer,
String action)
{
Set server = Collections.singleton(cert.getSubjectX500Principal());
Set client = (peer == null)
? null : Collections.singleton(new X500Principal(peer));
return new AuthenticationPermission(server, client, action);
}
/* -- Implement X509KeyManager -- */
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
public String[] getServerAliases(String keyType, Principal[] issuers) {
String[] result = getAliases(keyType, issuers);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"get server aliases for key type {0}\n" +
"and issuers {1}\nreturns {2}",
new Object[] {
keyType, toString(issuers), toString(result)
});
}
return result;
}
public String chooseClientAlias(
String[] keyTypes, Principal[] issuers, Socket socket)
{
return null;
}
/**
* Returns the last server credential selected for this key type, if still
* usable. If not, then invalidate all sessions with the same key type and
* attempt to find another key.
*/
public String chooseServerAlias(
String keyType, Principal[] issuers, Socket socket)
{
X500PrivateCredential cred = null;
synchronized (credentialCache) {
Object val = credentialCache.get(keyType);
if (val instanceof X500PrivateCredential) {
cred = (X500PrivateCredential) val;
try {
checkCredentials(cred, null, "listen");
} catch (SecurityException e) {
if (logger.isLoggable(Levels.HANDLED)) {
logThrow(logger, Levels.HANDLED,
ServerAuthManager.class, "chooseServerAlias",
"choose server alias for key type {0}\n" +
"and issuers {1}\ncaught exception",
new Object[] { keyType, toString(issuers) },
e);
}
/*
* This credential is no longer present or we don't have
* permission to use it. Clear the cache and invalidate
* sessions with this key type.
*/
cred = null;
credentialCache.remove(keyType);
for (Enumeration en = sslSessionContext.getIds();
en.hasMoreElements(); )
{
SSLSession session =
sslSessionContext.getSession(
(byte[]) en.nextElement());
if (session != null) {
String suite = session.getCipherSuite();
if (keyType.equals(getKeyAlgorithm(suite))) {
session.invalidate();
}
}
}
}
}
if (cred == null) {
/* Try to select a new alias */
Exception exception = null;
try {
cred = chooseCredential(keyType, issuers);
if (cred != null) {
credentialCache.put(keyType, cred);
}
} catch (GeneralSecurityException e) {
exception = e;
} catch (SecurityException e) {
exception = e;
}
if (exception != null) {
credentialCache.put(keyType, exception.getMessage());
return null;
}
}
}
String result = (cred == null)
? null
: SubjectCredentials.getCertificateName(cred.getCertificate());
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"choose server alias for key type {0}\nissuers {1}\n" +
"returns {2}",
new Object[] { keyType, toString(issuers), result });
}
return result;
}
}