View Javadoc

1   /* 
2    * Copyright (c) 2007, Fraunhofer-Gesellschaft
3    * All rights reserved.
4    * 
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions are
7    * met:
8    * 
9    * (1) Redistributions of source code must retain the above copyright
10   *     notice, this list of conditions and the disclaimer at the end.
11   *     Redistributions in binary form must reproduce the above copyright
12   *     notice, this list of conditions and the following disclaimer in
13   *     the documentation and/or other materials provided with the
14   *     distribution.
15   * 
16   * (2) Neither the name of Fraunhofer nor the names of its
17   *     contributors may be used to endorse or promote products derived
18   *     from this software without specific prior written permission.
19   * 
20   * DISCLAIMER
21   * 
22   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26   * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33   *  
34   */
35  package org.ogf.graap.wsag.client.wsrf.security;
36  
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.security.KeyStore;
40  import java.security.KeyStoreException;
41  import java.security.NoSuchAlgorithmException;
42  import java.security.PrivateKey;
43  import java.security.cert.Certificate;
44  import java.security.cert.CertificateException;
45  import java.security.cert.X509Certificate;
46  import java.text.MessageFormat;
47  import java.util.Map;
48  import java.util.Properties;
49  
50  import javax.security.auth.Subject;
51  import javax.security.auth.callback.Callback;
52  import javax.security.auth.callback.CallbackHandler;
53  import javax.security.auth.callback.UnsupportedCallbackException;
54  import javax.security.auth.login.LoginException;
55  import javax.security.auth.spi.LoginModule;
56  import javax.security.auth.x500.X500Principal;
57  import javax.security.auth.x500.X500PrivateCredential;
58  
59  import org.apache.log4j.Logger;
60  import org.apache.ws.security.WSSecurityException;
61  import org.apache.ws.security.components.crypto.Crypto;
62  import org.apache.ws.security.components.crypto.CryptoBase;
63  import org.ogf.graap.wsag.api.configuration.WSAG4JConfiguration;
64  import org.ogf.graap.wsag.api.security.KeystoreCallback;
65  import org.ogf.graap.wsag.api.security.SecurityConstants;
66  
67  /**
68   * KeystoreLoginModule
69   * 
70   * @author Oliver Waeldrich
71   * 
72   */
73  public class KeystoreLoginModule
74      implements LoginModule
75  {
76  
77      private static final Logger LOG = Logger.getLogger( KeystoreLoginModule.class );
78  
79      private Subject klmSubject;
80  
81      private CallbackHandler cbHandler;
82  
83      // private Map sharedState;
84  
85      @SuppressWarnings( "rawtypes" )
86      private Map klmOptions;
87  
88      // variables related to access the keystore
89      private KeyStore keystore;
90  
91      private String keystoreType;
92  
93      private String keystoreFile;
94  
95      private String keystorePassword;
96  
97      private String alias;
98  
99      private String privateKeyPassword;
100 
101     private String truststoreType;
102 
103     private String truststoreFile;
104 
105     private String truststorePassword;
106 
107     // variables related to user authentication
108     private Crypto userCrypto;
109 
110     private X500Principal userPrincipal;
111 
112     // variables related to login module steering
113     private boolean login = false;
114 
115     private boolean commit = false;
116 
117     /**
118      * {@inheritDoc}
119      * 
120      * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject,
121      *      javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
122      */
123     @Override
124     @SuppressWarnings( "rawtypes" )
125     public void initialize( Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options )
126     {
127         this.klmSubject = subject;
128         this.cbHandler = callbackHandler;
129         // this.sharedState = sharedState;
130         this.klmOptions = options;
131 
132         initializeOptions();
133     }
134 
135     private void initializeOptions()
136     {
137 
138         keystoreFile = (String) klmOptions.get( "keyStoreURL" );
139         keystoreType = (String) klmOptions.get( "keyStoreType" );
140         alias = (String) klmOptions.get( "keyStoreAlias" );
141 
142         truststoreFile = (String) klmOptions.get( "trustStoreURL" );
143         truststoreType = (String) klmOptions.get( "trustStoreType" );
144 
145         //
146         // processing of the options
147         //
148         // keystoreFile = resolveKeystoreURL(keystoreFile);
149         // truststoreFile = resolveKeystoreURL(truststoreFile);
150 
151         keystoreType = ( keystoreType == null ) ? "JKS" : keystoreType;
152         truststoreType = ( truststoreType == null ) ? "JKS" : truststoreType;
153     }
154 
155     /**
156      * {@inheritDoc}
157      * 
158      * @see javax.security.auth.spi.LoginModule#login()
159      */
160     @Override
161     public boolean login() throws LoginException
162     {
163         KeystoreCallback ksCallback = new KeystoreCallback();
164         Callback[] callbacks = new Callback[] { ksCallback };
165 
166         // handle login callbacks
167         try
168         {
169             cbHandler.handle( callbacks );
170         }
171         catch ( IOException e )
172         {
173             String message = "IO error during login";
174             LoginException le = new LoginException( message );
175             le.initCause( e );
176             throw le;
177         }
178         catch ( UnsupportedCallbackException e )
179         {
180             String message = "Invalid callback handler. Callback not supported.";
181             LoginException le = new LoginException( message );
182             le.initCause( e );
183             throw le;
184         }
185 
186         keystorePassword = ksCallback.getKeystorePassword();
187 
188         truststorePassword = ksCallback.getTruststorePassword();
189 
190         privateKeyPassword = ksCallback.getPrivateKeyPassword();
191 
192         //
193         // if an empty alias is supplied, we set the alias to null
194         // this is for treating PKCS12 files, where certificates
195         // do not have a alias
196         //
197         // if ("".equals(alias)) alias = null;
198 
199         if ( ( keystoreFile == null ) || ( keystorePassword == null ) || ( privateKeyPassword == null ) )
200         {
201 
202             String message =
203                 "Missing required parameter. "
204                     + "The KeystoreLoginModule requires the following parameters: "
205                     + "[keystoreFilename, keystorePassword, alias, privateKeyPassword]";
206 
207             throw new LoginException( message );
208         }
209 
210         loadKeyStore();
211 
212         login = true;
213 
214         return true;
215     }
216 
217     /**
218      * {@inheritDoc}
219      * 
220      * @see javax.security.auth.spi.LoginModule#commit()
221      */
222     @Override
223     public boolean commit() throws LoginException
224     {
225         if ( !login )
226         {
227             return false;
228         }
229 
230         PrivateKey userKey;
231         X500PrivateCredential userCredential;
232         X509Certificate[] userCertificateChain;
233 
234         try
235         {
236             userCertificateChain = getCertificates( alias );
237             userKey = (PrivateKey) keystore.getKey( alias, privateKeyPassword.toCharArray() );
238         }
239         catch ( KeyStoreException e )
240         {
241             // thrown by keystoreManager.getCertificateByAlias(defaultAlias)[0]
242             String message = "Could not get default certificate from KeyStoreManager";
243             LoginException le = new LoginException( message );
244             le.initCause( e );
245             throw le;
246         }
247         catch ( Exception e )
248         {
249             // thrown by keystoreManager.getKeyEntry(defaultAlias)
250             String message = "Could not get private key from KeyStoreManager";
251             LoginException le = new LoginException( message );
252             le.initCause( e );
253             throw le;
254         }
255 
256         if ( userCertificateChain == null )
257         {
258             Object[] filler = new Object[] { alias };
259             String message = MessageFormat.format( "No certificates found for user {0}", filler );
260             throw new LoginException( message );
261         }
262 
263         userCredential = new X500PrivateCredential( userCertificateChain[0], userKey );
264 
265         userCrypto = loadUserCrypto( userCredential, userCertificateChain, keystore );
266         userPrincipal =
267             new X500Principal( userCredential.getCertificate().getSubjectX500Principal().getName() );
268 
269         klmSubject.getPrivateCredentials().add( userCrypto );
270         klmSubject.getPrivateCredentials().add( userCredential );
271         klmSubject.getPrincipals().add( userPrincipal );
272 
273         commit = true;
274 
275         return true;
276     }
277 
278     /**
279      * {@inheritDoc}
280      * 
281      * @see javax.security.auth.spi.LoginModule#abort()
282      */
283     @Override
284     public boolean abort() throws LoginException
285     {
286         if ( !login )
287         {
288             return false;
289         }
290         if ( ( login ) && ( !commit ) )
291         {
292             // login succeeded, but overall authentication failed
293             login = false;
294 
295             klmSubject.getPrincipals().remove( userPrincipal );
296             klmSubject.getPrivateCredentials().remove( userCrypto );
297 
298             userCrypto = null;
299             userPrincipal = null;
300 
301             keystore = null;
302             keystoreFile = null;
303             keystorePassword = null;
304             keystoreType = null;
305 
306             alias = null;
307             privateKeyPassword = null;
308         }
309         else
310         {
311             // overall authentication succeeded and commit succeeded,
312             // but someone else's commit failed
313             logout();
314         }
315 
316         return true;
317     }
318 
319     /**
320      * {@inheritDoc}
321      * 
322      * @see javax.security.auth.spi.LoginModule#logout()
323      */
324     @Override
325     public boolean logout() throws LoginException
326     {
327         klmSubject.getPrivateCredentials().remove( userCrypto );
328         klmSubject.getPrincipals().remove( userPrincipal );
329 
330         login = false;
331         commit = false;
332 
333         userCrypto = null;
334         userPrincipal = null;
335 
336         keystore = null;
337         keystoreFile = null;
338         keystorePassword = null;
339         keystoreType = null;
340 
341         alias = null;
342         privateKeyPassword = null;
343 
344         return true;
345     }
346 
347     private synchronized KeyStore getKeystore() throws LoginException
348     {
349         if ( keystore == null )
350         {
351             loadKeyStore();
352         }
353         return keystore;
354     }
355 
356     private void loadKeyStore() throws LoginException
357     {
358         try
359         {
360             String actualKSType = ( keystoreType == null ) ? KeyStore.getDefaultType() : keystoreType;
361 
362             keystore = KeyStore.getInstance( actualKSType );
363 
364             if ( keystoreFile == null )
365             {
366                 throw new IOException( "No keystore specified by user." );
367             }
368 
369             InputStream ksInput = WSAG4JConfiguration.findResource( keystoreFile );
370             keystore.load( ksInput, keystorePassword.toCharArray() );
371 
372         }
373         catch ( KeyStoreException e )
374         {
375             throw new LoginException( e.getMessage() );
376         }
377         catch ( IOException e )
378         {
379             throw new LoginException( e.getMessage() );
380         }
381         catch ( CertificateException e )
382         {
383             throw new LoginException( e.getMessage() );
384         }
385         catch ( NoSuchAlgorithmException e )
386         {
387             throw new LoginException( e.getMessage() );
388         }
389 
390     }
391 
392     /**
393      * Gets the list of certificates for a given alias.
394      * <p/>
395      * 
396      * @param ksAlias
397      *            Lookup certificate chain for this alias
398      * 
399      * @return Array of X509 certificates for this alias name, or null if this alias does not exist in the
400      *         keystore
401      * 
402      * @throws KeyStoreException
403      *             error accessing the keystore
404      * 
405      * @throws LoginException
406      *             error logging into the keystore
407      */
408     private X509Certificate[] getCertificates( String ksAlias ) throws KeyStoreException, LoginException
409     {
410         Certificate[] certs = null;
411         Certificate cert = null;
412 
413         KeyStore store = getKeystore();
414 
415         if ( store != null )
416         {
417             // There's a chance that there can only be a set of trust stores
418             certs = store.getCertificateChain( ksAlias );
419             if ( certs == null || certs.length == 0 )
420             {
421                 // no cert chain, so lets check if getCertificate gives us a
422                 // result.
423                 cert = store.getCertificate( ksAlias );
424             }
425         }
426 
427         if ( cert != null )
428         {
429             certs = new Certificate[] { cert };
430         }
431         else if ( certs == null )
432         {
433             // At this pont we don't have certs or a cert
434             return null;
435         }
436 
437         X509Certificate[] x509certs = new X509Certificate[certs.length];
438         for ( int i = 0; i < certs.length; i++ )
439         {
440             x509certs[i] = (X509Certificate) certs[i];
441         }
442         return x509certs;
443     }
444 
445     /**
446      * This method returns a new instance of org.apache.ws.security.components.crypto.Merlin.
447      * 
448      * @param privCredential
449      * @param pubCredential
450      * @param store
451      * @return
452      */
453     private Crypto loadUserCrypto( final X500PrivateCredential privCredential,
454                                    final X509Certificate[] pubCredential, final KeyStore store )
455     {
456 
457         //
458         // create the Merlin properties
459         //
460         Properties properties = new Properties();
461         properties.setProperty( SecurityConstants.PROP_CRYPTO_PROVIDER,
462             org.apache.ws.security.components.crypto.Merlin.class.getName() );
463 
464         properties.setProperty( SecurityConstants.PROP_KEYSTORE_TYPE, keystoreType );
465         properties.setProperty( SecurityConstants.PROP_KEYSTORE_PASS, keystorePassword );
466         properties.setProperty( SecurityConstants.PROP_KEYSTORE_ALIAS, alias );
467         properties.setProperty( SecurityConstants.PROP_KEYSTORE_ALIAS_PASS, privateKeyPassword );
468 
469         if ( keystoreFile.startsWith( "/" ) )
470         {
471             properties.setProperty( SecurityConstants.PROP_KEYSTORE_FILE, keystoreFile.substring( 1 ) );
472         }
473         else
474         {
475             properties.setProperty( SecurityConstants.PROP_KEYSTORE_FILE, keystoreFile );
476         }
477 
478         if ( truststoreFile != null )
479         {
480             properties.setProperty( SecurityConstants.PROP_TRUSTSTORE_FILE, truststoreFile );
481         }
482         if ( truststorePassword != null )
483         {
484             properties.setProperty( SecurityConstants.PROP_TRUSTSTORE_PASS, truststorePassword );
485         }
486         if ( truststoreType != null )
487         {
488             properties.setProperty( SecurityConstants.PROP_TRUSTSTORE_TYPE, truststoreType );
489         }
490 
491         try
492         {
493             ClassLoader classLoader = getClass().getClassLoader();
494             return new org.apache.ws.security.components.crypto.Merlin( properties, classLoader )
495             {
496                 /**
497                  * Change the alias name provided in the AXIS2 service configuration to the defaultAlias if
498                  * necessary and change the password accordingly.
499                  * 
500                  * {@inheritDoc}
501                  */
502                 @Override
503                 public PrivateKey getPrivateKey( String myAlias, String myPassword ) throws Exception
504                 {
505                     if ( myAlias.equals( SecurityConstants.DEFAULT_ALIAS )
506                         && myPassword.equals( SecurityConstants.DEFAULT_ALIAS_PASSWORD ) )
507                     {
508                         myAlias = getDefaultX509Alias();
509                         myPassword =
510                             properties.getProperty( "org.apache.ws.security.crypto.merlin.alias.password" );
511                     }
512                     return super.getPrivateKey( myAlias, myPassword );
513                 }
514 
515                 /**
516                  * Change the alias name provided in the AXIS2 service configuration to the defaultAlias if
517                  * necessary.
518                  * 
519                  * {@inheritDoc}
520                  */
521                 @Override
522                 public X509Certificate[] getCertificates( String myAlias ) throws WSSecurityException
523                 {
524                     myAlias =
525                         ( myAlias.equals( SecurityConstants.DEFAULT_ALIAS ) ) ? getDefaultX509Alias()
526                                         : myAlias;
527                     return super.getCertificates( myAlias );
528                 }
529             };
530         }
531         catch ( Exception e )
532         {
533             Object[] filler = new Object[] { e.getMessage() };
534             String message = MessageFormat.format( "Could not load user crypto. Reason: {0}", filler );
535             LOG.error( message );
536             LOG.error( "Try fallback... Does eventually not work for PKCS12 keystore." );
537         }
538 
539         //
540         // Fallback in case Merlin was not loaded
541         //
542         CryptoBase crypto = getFallbackCrypto( privCredential, pubCredential, store );
543 
544         return crypto;
545     }
546 
547     /**
548      * @param privCredential
549      * @param pubCredential
550      * @param store
551      * @return
552      */
553     private CryptoBase getFallbackCrypto( final X500PrivateCredential privCredential,
554                                           final X509Certificate[] pubCredential, final KeyStore store )
555     {
556         CryptoBase crypto = new CryptoBase()
557         {
558 
559             @Override
560             protected String getCryptoProvider()
561             {
562                 return null;
563             }
564 
565             @Override
566             public String getDefaultX509Alias()
567             {
568                 /*
569                  * The user name for signing a SOAP message is configured statically in the AXIS2
570                  * configuration of the Rampart module. By default we set it to
571                  * SecurityConstants.DEFAULT_ALIAS.
572                  */
573                 return SecurityConstants.DEFAULT_ALIAS;
574             }
575 
576             /**
577              * {@inheritDoc}
578              */
579             @Override
580             public PrivateKey getPrivateKey( String ksAlias, String password ) throws Exception
581             {
582                 /*
583                  * We return the private credential for requests with the SecurityConstants.DEFAULT_ALIAS,
584                  * otherwise we delegate the request to the keystore object.
585                  */
586                 if ( SecurityConstants.DEFAULT_ALIAS.equals( ksAlias ) )
587                 {
588                     return privCredential.getPrivateKey();
589                 }
590                 return (PrivateKey) keystore.getKey( ksAlias, password.toCharArray() );
591             }
592 
593             /**
594              * {@inheritDoc}
595              */
596             @Override
597             public X509Certificate[] getCertificates( String ksAlias ) throws WSSecurityException
598             {
599                 /*
600                  * We return the public credential for requests with the SecurityConstants.DEFAULT_ALIAS,
601                  * otherwise we delegate the request to the keystore object.
602                  */
603                 if ( SecurityConstants.DEFAULT_ALIAS.equals( ksAlias ) )
604                 {
605                     if ( pubCredential == null )
606                     {
607                         LOG.warn( "No certificate chain not provided in the login context." );
608                         return new X509Certificate[] { privCredential.getCertificate() };
609                     }
610 
611                     return pubCredential;
612                 }
613                 else
614                 {
615                     return super.getCertificates( ksAlias );
616                 }
617             }
618 
619         };
620 
621         crypto.setKeyStore( store );
622         return crypto;
623     }
624 
625 }