/* * @(#)KeyStoreLoginModule.java 1.18 04/05/05 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package com.sun.security.auth.module; import javax.security.auth.x500.X500Principal; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.net.MalformedURLException; import java.net.URL; import java.security.AuthProvider; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Principal; import java.security.PrivateKey; import java.security.Provider; import java.security.UnrecoverableKeyException; import java.security.cert.*; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.ResourceBundle; import javax.security.auth.Destroyable; import javax.security.auth.DestroyFailedException; import javax.security.auth.Subject; import javax.security.auth.x500.*; import javax.security.auth.Subject; import javax.security.auth.x500.*; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.ConfirmationCallback; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.TextOutputCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import sun.security.util.AuthResources; import sun.security.util.Password; /** * Provides a JAAS login module that prompts for a key store alias and * populates the subject with the alias's principal and credentials. Stores * an X500Principal for the subject distinguished name of the * first certificate in the alias's credentials in the subject's principals, * the alias's certificate path in the subject's public credentials, and a * X500PrivateCredential whose certificate is the first * certificate in the alias's certificate path and whose private key is the * alias's private key in the subject's private credentials.

* * Recognizes the following options in the configuration file: *

* *
keyStoreURL
*
A URL that specifies the location of the key store. Defaults to * a URL pointing to the .keystore file in the directory specified by the * user.home system property. The input stream from this * URL is passed to the KeyStore.load method. * "NONE" may be specified if a null stream must be * passed to the KeyStore.load method. * "NONE" should be specified if the KeyStore resides * on a hardware token device, for example.
* *
keyStoreType
*
The key store type. If not specified, defaults to the result of * calling KeyStore.getDefaultType(). * If the type is "PKCS11", then keyStoreURL must be "NONE" * and privateKeyPasswordURL must not be specified.
* *
keyStoreProvider
*
The key store provider. If not specified, uses the standard search * order to find the provider.
* *
keyStoreAlias
*
The alias in the key store to login as. Required when no callback * handler is provided. No default value.
* *
keyStorePasswordURL
*
A URL that specifies the location of the key store password. Required * when no callback handler is provided and * protected is false. * No default value.
* *
privateKeyPasswordURL
*
A URL that specifies the location of the specific private key password * needed to access the private key for this alias. * The keystore password * is used if this value is needed and not specified.
* *
protected
*
This value should be set to "true" if the KeyStore * has a separate, protected authentication path * (for example, a dedicated PIN-pad attached to a smart card). * Defaults to "false". If "true" keyStorePasswordURL and * privateKeyPasswordURL must not be specified.
* *
*/ public class KeyStoreLoginModule implements LoginModule { static final java.util.ResourceBundle rb = java.util.ResourceBundle.getBundle("sun.security.util.AuthResources"); /* -- Fields -- */ private static final int UNINITIALIZED = 0; private static final int INITIALIZED = 1; private static final int AUTHENTICATED = 2; private static final int LOGGED_IN = 3; private static final int PROTECTED_PATH = 0; private static final int TOKEN = 1; private static final int NORMAL = 2; private static final String NONE = "NONE"; private static final String P11KEYSTORE = "PKCS11"; private static final TextOutputCallback bannerCallback = new TextOutputCallback (TextOutputCallback.INFORMATION, rb.getString("Please enter keystore information")); private final ConfirmationCallback confirmationCallback = new ConfirmationCallback (ConfirmationCallback.INFORMATION, ConfirmationCallback.OK_CANCEL_OPTION, ConfirmationCallback.OK); private Subject subject; private CallbackHandler callbackHandler; private Map sharedState; private Map options; private char[] keyStorePassword; private char[] privateKeyPassword; private KeyStore keyStore; private String keyStoreURL; private String keyStoreType; private String keyStoreProvider; private String keyStoreAlias; private String keyStorePasswordURL; private String privateKeyPasswordURL; private boolean debug; private javax.security.auth.x500.X500Principal principal; private Certificate[] fromKeyStore; private java.security.cert.CertPath certP = null; private X500PrivateCredential privateCredential; private int status = UNINITIALIZED; private boolean nullStream = false; private boolean token = false; private boolean protectedPath = false; /* -- Methods -- */ /** * Initialize this LoginModule. * *

* * @param subject the Subject to be authenticated.

* * @param callbackHandler a CallbackHandler for communicating * with the end user (prompting for usernames and * passwords, for example), * which may be null.

* * @param sharedState shared LoginModule state.

* * @param options options specified in the login * Configuration for this particular * LoginModule. */ public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = sharedState; this.options = options; processOptions(); status = INITIALIZED; } private void processOptions() { keyStoreURL = (String) options.get("keyStoreURL"); if (keyStoreURL == null) { keyStoreURL = "file:" + System.getProperty("user.home").replace( File.separatorChar, '/') + '/' + ".keystore"; } else if (NONE.equals(keyStoreURL)) { nullStream = true; } keyStoreType = (String) options.get("keyStoreType"); if (keyStoreType == null) { keyStoreType = KeyStore.getDefaultType(); } if (P11KEYSTORE.equalsIgnoreCase(keyStoreType)) { token = true; } keyStoreProvider = (String) options.get("keyStoreProvider"); keyStoreAlias = (String) options.get("keyStoreAlias"); keyStorePasswordURL = (String) options.get("keyStorePasswordURL"); privateKeyPasswordURL = (String) options.get("privateKeyPasswordURL"); protectedPath = "true".equalsIgnoreCase((String)options.get ("protected")); debug = "true".equalsIgnoreCase((String) options.get("debug")); if (debug) { debugPrint(null); debugPrint("keyStoreURL=" + keyStoreURL); debugPrint("keyStoreType=" + keyStoreType); debugPrint("keyStoreProvider=" + keyStoreProvider); debugPrint("keyStoreAlias=" + keyStoreAlias); debugPrint("keyStorePasswordURL=" + keyStorePasswordURL); debugPrint("privateKeyPasswordURL=" + privateKeyPasswordURL); debugPrint("protectedPath=" + protectedPath); debugPrint(null); } } /** * Authenticate the user. * *

Get the Keystore alias and relevant passwords. * Retrieve the alias's principal and credentials from the Keystore. * *

* * @exception FailedLoginException if the authentication fails.

* * @return true in all cases (this LoginModule * should not be ignored). */ public boolean login() throws LoginException { switch (status) { case UNINITIALIZED: default: throw new LoginException("The login module is not initialized"); case INITIALIZED: case AUTHENTICATED: if (token && !nullStream) { throw new LoginException ("if keyStoreType is " + P11KEYSTORE + " then keyStoreURL must be " + NONE); } if (token && privateKeyPasswordURL != null) { throw new LoginException ("if keyStoreType is " + P11KEYSTORE + " then privateKeyPasswordURL must not be specified"); } if (protectedPath && (keyStorePasswordURL != null || privateKeyPasswordURL != null)) { throw new LoginException ("if protected is true then keyStorePasswordURL and " + "privateKeyPasswordURL must not be specified"); } // get relevant alias and password info if (protectedPath) { getAliasAndPasswords(PROTECTED_PATH); } else if (token) { getAliasAndPasswords(TOKEN); } else { getAliasAndPasswords(NORMAL); } // log into KeyStore to retrieve data, // then clear passwords try { getKeyStoreInfo(); } finally { if (privateKeyPassword != null && privateKeyPassword != keyStorePassword) { Arrays.fill(privateKeyPassword, '\0'); privateKeyPassword = null; } if (keyStorePassword != null) { Arrays.fill(keyStorePassword, '\0'); keyStorePassword = null; } } status = AUTHENTICATED; return true; case LOGGED_IN: return true; } } /** Get the alias and passwords to use for looking up in the KeyStore. */ private void getAliasAndPasswords(int env) throws LoginException { if (callbackHandler == null) { // No callback handler - check for alias and password options switch (env) { case PROTECTED_PATH: checkAlias(); break; case TOKEN: checkAlias(); checkStorePass(); break; case NORMAL: checkAlias(); checkStorePass(); checkKeyPass(); break; } } else { // Callback handler available - prompt for alias and passwords NameCallback aliasCallback; if (keyStoreAlias == null || keyStoreAlias.length() == 0) { aliasCallback = new NameCallback( rb.getString("Keystore alias: ")); } else { aliasCallback = new NameCallback(rb.getString("Keystore alias: "), keyStoreAlias); } PasswordCallback storePassCallback = null; PasswordCallback keyPassCallback = null; switch (env) { case PROTECTED_PATH: break; case NORMAL: keyPassCallback = new PasswordCallback (rb.getString("Private key password (optional): "), false); // fall thru case TOKEN: storePassCallback = new PasswordCallback (rb.getString("Keystore password: "), false); break; } prompt(aliasCallback, storePassCallback, keyPassCallback); } if (debug) { debugPrint("alias=" + keyStoreAlias); } } private void checkAlias() throws LoginException { if (keyStoreAlias == null) { throw new LoginException ("Need to specify an alias option to use " + "KeyStoreLoginModule non-interactively."); } } private void checkStorePass() throws LoginException { if (keyStorePasswordURL == null) { throw new LoginException ("Need to specify keyStorePasswordURL option to use " + "KeyStoreLoginModule non-interactively."); } try { InputStream in = new URL(keyStorePasswordURL).openStream(); keyStorePassword = Password.readPassword(in); in.close(); } catch (IOException e) { LoginException le = new LoginException ("Problem accessing keystore password \"" + keyStorePasswordURL + "\""); le.initCause(e); throw le; } } private void checkKeyPass() throws LoginException { if (privateKeyPasswordURL == null) { privateKeyPassword = keyStorePassword; } else { try { InputStream in = new URL(privateKeyPasswordURL).openStream(); privateKeyPassword = Password.readPassword(in); in.close(); } catch (IOException e) { LoginException le = new LoginException ("Problem accessing private key password \"" + privateKeyPasswordURL + "\""); le.initCause(e); throw le; } } } private void prompt(NameCallback aliasCallback, PasswordCallback storePassCallback, PasswordCallback keyPassCallback) throws LoginException { if (storePassCallback == null) { // only prompt for alias try { callbackHandler.handle( new Callback[] { bannerCallback, aliasCallback, confirmationCallback }); } catch (IOException e) { LoginException le = new LoginException ("Problem retrieving keystore alias"); le.initCause(e); throw le; } catch (UnsupportedCallbackException e) { throw new LoginException( "Error: " + e.getCallback().toString() + " is not available to retrieve authentication " + " information from the user"); } int confirmationResult = confirmationCallback.getSelectedIndex(); if (confirmationResult == ConfirmationCallback.CANCEL) { throw new LoginException("Login cancelled"); } saveAlias(aliasCallback); } else if (keyPassCallback == null) { // prompt for alias and key store password try { callbackHandler.handle( new Callback[] { bannerCallback, aliasCallback, storePassCallback, confirmationCallback }); } catch (IOException e) { LoginException le = new LoginException ("Problem retrieving keystore alias and password"); le.initCause(e); throw le; } catch (UnsupportedCallbackException e) { throw new LoginException( "Error: " + e.getCallback().toString() + " is not available to retrieve authentication " + " information from the user"); } int confirmationResult = confirmationCallback.getSelectedIndex(); if (confirmationResult == ConfirmationCallback.CANCEL) { throw new LoginException("Login cancelled"); } saveAlias(aliasCallback); saveStorePass(storePassCallback); } else { // prompt for alias, key store password, and key password try { callbackHandler.handle( new Callback[] { bannerCallback, aliasCallback, storePassCallback, keyPassCallback, confirmationCallback }); } catch (IOException e) { LoginException le = new LoginException ("Problem retrieving keystore alias and passwords"); le.initCause(e); throw le; } catch (UnsupportedCallbackException e) { throw new LoginException( "Error: " + e.getCallback().toString() + " is not available to retrieve authentication " + " information from the user"); } int confirmationResult = confirmationCallback.getSelectedIndex(); if (confirmationResult == ConfirmationCallback.CANCEL) { throw new LoginException("Login cancelled"); } saveAlias(aliasCallback); saveStorePass(storePassCallback); saveKeyPass(keyPassCallback); } } private void saveAlias(NameCallback cb) { keyStoreAlias = cb.getName(); } private void saveStorePass(PasswordCallback c) { keyStorePassword = c.getPassword(); if (keyStorePassword == null) { /* Treat a NULL password as an empty password */ keyStorePassword = new char[0]; } c.clearPassword(); } private void saveKeyPass(PasswordCallback c) { privateKeyPassword = c.getPassword(); if (privateKeyPassword == null || privateKeyPassword.length == 0) { /* * Use keystore password if no private key password is * specified. */ privateKeyPassword = keyStorePassword; } c.clearPassword(); } /** Get the credentials from the KeyStore. */ private void getKeyStoreInfo() throws LoginException { /* Get KeyStore instance */ try { if (keyStoreProvider == null) { keyStore = KeyStore.getInstance(keyStoreType); } else { keyStore = KeyStore.getInstance(keyStoreType, keyStoreProvider); } } catch (KeyStoreException e) { LoginException le = new LoginException ("The specified keystore type was not available"); le.initCause(e); throw le; } catch (NoSuchProviderException e) { LoginException le = new LoginException ("The specified keystore provider was not available"); le.initCause(e); throw le; } /* Load KeyStore contents from file */ try { if (nullStream) { // if using protected auth path, keyStorePassword will be null keyStore.load(null, keyStorePassword); } else { InputStream in = new URL(keyStoreURL).openStream(); keyStore.load(in, keyStorePassword); in.close(); } } catch (MalformedURLException e) { LoginException le = new LoginException ("Incorrect keyStoreURL option"); le.initCause(e); throw le; } catch (GeneralSecurityException e) { LoginException le = new LoginException ("Error initializing keystore"); le.initCause(e); throw le; } catch (IOException e) { LoginException le = new LoginException ("Error initializing keystore"); le.initCause(e); throw le; } /* Get certificate chain and create a certificate path */ try { fromKeyStore = keyStore.getCertificateChain(keyStoreAlias); if (fromKeyStore == null || fromKeyStore.length == 0 || !(fromKeyStore[0] instanceof X509Certificate)) { throw new FailedLoginException( "Unable to find X.509 certificate chain in keystore"); } else { LinkedList certList = new LinkedList(); for (int i=0; i < fromKeyStore.length; i++) { certList.add(fromKeyStore[i]); } CertificateFactory certF= CertificateFactory.getInstance("X.509"); certP = certF.generateCertPath(certList); } } catch (KeyStoreException e) { LoginException le = new LoginException("Error using keystore"); le.initCause(e); throw le; } catch (CertificateException ce) { LoginException le = new LoginException ("Error: X.509 Certificate type unavailable"); le.initCause(ce); throw le; } /* Get principal and keys */ try { X509Certificate certificate = (X509Certificate)fromKeyStore[0]; principal = new javax.security.auth.x500.X500Principal (certificate.getSubjectDN().getName()); // if token, privateKeyPassword will be null Key privateKey = keyStore.getKey(keyStoreAlias, privateKeyPassword); if (privateKey == null || !(privateKey instanceof PrivateKey)) { throw new FailedLoginException( "Unable to recover key from keystore"); } privateCredential = new X500PrivateCredential( certificate, (PrivateKey) privateKey, keyStoreAlias); } catch (KeyStoreException e) { LoginException le = new LoginException("Error using keystore"); le.initCause(e); throw le; } catch (NoSuchAlgorithmException e) { LoginException le = new LoginException("Error using keystore"); le.initCause(e); throw le; } catch (UnrecoverableKeyException e) { FailedLoginException fle = new FailedLoginException ("Unable to recover key from keystore"); fle.initCause(e); throw fle; } if (debug) { debugPrint("principal=" + principal + "\n certificate=" + privateCredential.getCertificate() + "\n alias =" + privateCredential.getAlias()); } } /** * Abstract method to commit the authentication process (phase 2). * *

This method is called if the LoginContext's * overall authentication succeeded * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules * succeeded). * *

If this LoginModule's own authentication attempt * succeeded (checked by retrieving the private state saved by the * login method), then this method associates a * X500Principal for the subject distinguished name of the * first certificate in the alias's credentials in the subject's * principals,the alias's certificate path in the subject's public * credentials, and aX500PrivateCredential whose certificate * is the first certificate in the alias's certificate path and whose * private key is the alias's private key in the subject's private * credentials. If this LoginModule's own * authentication attempted failed, then this method removes * any state that was originally saved. * *

* * @exception LoginException if the commit fails * * @return true if this LoginModule's own login and commit * attempts succeeded, or false otherwise. */ public boolean commit() throws LoginException { switch (status) { case UNINITIALIZED: default: throw new LoginException("The login module is not initialized"); case INITIALIZED: logoutInternal(); throw new LoginException("Authentication failed"); case AUTHENTICATED: if (commitInternal()) { return true; } else { logoutInternal(); throw new LoginException("Unable to retrieve certificates"); } case LOGGED_IN: return true; } } private boolean commitInternal() throws LoginException { /* If the subject is not readonly add to the principal and credentials * set; otherwise just return true */ if (subject.isReadOnly()) { throw new LoginException ("Subject is set readonly"); } else { subject.getPrincipals().add(principal); subject.getPublicCredentials().add(certP); subject.getPrivateCredentials().add(privateCredential); status = LOGGED_IN; return true; } } /** *

This method is called if the LoginContext's * overall authentication failed. * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules * did not succeed). * *

If this LoginModule's own authentication attempt * succeeded (checked by retrieving the private state saved by the * login and commit methods), * then this method cleans up any state that was originally saved. * *

If the loaded KeyStore's provider extends * java.security.AuthProvider, * then the provider's logout method is invoked. * *

* * @exception LoginException if the abort fails. * * @return false if this LoginModule's own login and/or commit attempts * failed, and true otherwise. */ public boolean abort() throws LoginException { switch (status) { case UNINITIALIZED: default: return false; case INITIALIZED: return false; case AUTHENTICATED: logoutInternal(); return true; case LOGGED_IN: logoutInternal(); return true; } } /** * Logout a user. * *

This method removes the Principals, public credentials and the * private credentials that were added by the commit method. * *

If the loaded KeyStore's provider extends * java.security.AuthProvider, * then the provider's logout method is invoked. * *

* * @exception LoginException if the logout fails. * * @return true in all cases since this LoginModule * should not be ignored. */ public boolean logout() throws LoginException { if (debug) debugPrint("Entering logout " + status); switch (status) { case UNINITIALIZED: throw new LoginException ("The login module is not initialized"); case INITIALIZED: case AUTHENTICATED: default: // impossible for LoginModule to be in AUTHENTICATED // state // assert status != AUTHENTICATED; return false; case LOGGED_IN: logoutInternal(); return true; } } private void logoutInternal() throws LoginException { if (debug) { debugPrint("Entering logoutInternal"); } // assumption is that KeyStore.load did a login - // perform explicit logout if possible LoginException logoutException = null; Provider provider = keyStore.getProvider(); if (provider instanceof AuthProvider) { AuthProvider ap = (AuthProvider)provider; try { ap.logout(); if (debug) { debugPrint("logged out of KeyStore AuthProvider"); } } catch (LoginException le) { // save but continue below logoutException = le; } } if (subject.isReadOnly()) { // attempt to destroy the private credential // even if the Subject is read-only principal = null; certP = null; status = INITIALIZED; // destroy the private credential Iterator it = subject.getPrivateCredentials().iterator(); while (it.hasNext()) { Object obj = it.next(); if (privateCredential.equals(obj)) { privateCredential = null; try { ((Destroyable)obj).destroy(); if (debug) debugPrint("Destroyed private credential, " + obj.getClass().getName()); break; } catch (DestroyFailedException dfe) { LoginException le = new LoginException ("Unable to destroy private credential, " + obj.getClass().getName()); le.initCause(dfe); throw le; } } } // throw an exception because we can not remove // the principal and public credential from this // read-only Subject throw new LoginException ("Unable to remove Principal (" + "X500Principal " + ") and public credential (certificatepath) " + "from read-only Subject"); } if (principal != null) { subject.getPrincipals().remove(principal); principal = null; } if (certP != null) { subject.getPublicCredentials().remove(certP); certP = null; } if (privateCredential != null) { subject.getPrivateCredentials().remove(privateCredential); privateCredential = null; } // throw pending logout exception if there is one if (logoutException != null) { throw logoutException; } status = INITIALIZED; } private void debugPrint(String message) { // we should switch to logging API if (message == null) { System.err.println(); } else { System.err.println("Debug KeyStoreLoginModule: " + message); } } }