1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.server.ssl;
21  
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.File;
25  import java.io.FileOutputStream;
26  import java.security.KeyStore;
27  import java.security.cert.Certificate;
28  import java.security.cert.CertificateFactory;
29  import java.util.ArrayList;
30  import java.util.HashSet;
31  import java.util.Hashtable;
32  import java.util.List;
33  import java.util.Set;
34  
35  import javax.naming.AuthenticationNotSupportedException;
36  import javax.naming.Context;
37  import javax.naming.NameNotFoundException;
38  import javax.naming.NamingEnumeration;
39  import javax.naming.directory.Attributes;
40  import javax.naming.directory.BasicAttribute;
41  import javax.naming.directory.BasicAttributes;
42  import javax.naming.directory.DirContext;
43  import javax.naming.directory.ModificationItem;
44  import javax.naming.directory.SearchControls;
45  import javax.naming.directory.SearchResult;
46  import javax.naming.ldap.InitialLdapContext;
47  import javax.naming.ldap.LdapContext;
48  import javax.naming.ldap.StartTlsRequest;
49  import javax.naming.ldap.StartTlsResponse;
50  import javax.net.ssl.HostnameVerifier;
51  import javax.net.ssl.SSLSession;
52  
53  import org.apache.directory.server.core.CoreSession;
54  import org.apache.directory.server.core.entry.ClonedServerEntry;
55  import org.apache.directory.server.core.integ.Level;
56  import org.apache.directory.server.core.integ.annotations.CleanupLevel;
57  import org.apache.directory.server.integ.ServerIntegrationUtils;
58  import org.apache.directory.server.integ.SiRunner;
59  import org.apache.directory.server.ldap.LdapService;
60  import org.apache.directory.shared.ldap.name.LdapDN;
61  import org.junit.After;
62  import org.junit.Before;
63  import org.junit.Test;
64  import org.junit.runner.RunWith;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  
69  import static org.junit.Assert.assertNotNull;
70  import static org.junit.Assert.assertEquals;
71  import static org.junit.Assert.assertTrue;
72  import static org.junit.Assert.fail;
73  
74  
75  /**
76   * Test case to verify proper operation of confidentiality requirements as 
77   * specified in https://issues.apache.org/jira/browse/DIRSERVER-1189.  
78   * 
79   * Starts up the server binds via SUN JNDI provider to perform various 
80   * operations on entries which should be rejected when a TLS secured 
81   * connection is not established.
82   * 
83   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
84   * @version $Rev: 639006 $
85   */
86  @RunWith ( SiRunner.class ) 
87  @CleanupLevel ( Level.SUITE )
88  public class StartTlsIT
89  {
90      private static final Logger LOG = LoggerFactory.getLogger( StartTlsIT.class );
91      private static final String[] CERT_IDS = new String[] { "userCertificate" };
92      private static final int CONNECT_ITERATIONS = 10;
93      private static final boolean VERBOSE = false;
94      private File ksFile;
95  
96      
97      public static LdapService ldapService;
98      boolean oldConfidentialityRequiredValue;
99      
100     
101     /**
102      * Sets up the key store and installs the self signed certificate for the 
103      * server (created on first startup) which is to be used by the StartTLS 
104      * JDNDI client that will connect.  The key store is created from scratch
105      * programmatically and whipped on each run.  The certificate is acquired 
106      * by pulling down the bytes for administrator's userCertificate from 
107      * uid=admin,ou=system.  We use sysRoot direct context instead of one over
108      * the wire since the server is configured to prevent connections without
109      * TLS secured connections.
110      */
111     @Before
112     public void installKeyStoreWithCertificate() throws Exception
113     {
114     	if ( ksFile != null && ksFile.exists() )
115     	{
116     		ksFile.delete();
117     	}
118     	
119     	ksFile = File.createTempFile( "testStore", "ks" );
120     	
121     	CoreSession session = ldapService.getDirectoryService().getAdminSession();
122     	ClonedServerEntry entry = session.lookup( new LdapDN( "uid=admin,ou=system" ), CERT_IDS );
123     	byte[] userCertificate = entry.get( CERT_IDS[0] ).getBytes();
124     	assertNotNull( userCertificate );
125 
126     	ByteArrayInputStream in = new ByteArrayInputStream( userCertificate );
127     	CertificateFactory factory = CertificateFactory.getInstance( "X.509" );
128     	Certificate cert = factory.generateCertificate( in );
129     	KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );
130     	ks.load( null, null );
131     	ks.setCertificateEntry( "apacheds", cert );
132     	ks.store( new FileOutputStream( ksFile ), "changeit".toCharArray() );
133     	LOG.debug( "Keystore file installed: {}", ksFile.getAbsolutePath() );
134     	
135         oldConfidentialityRequiredValue = ldapService.isConfidentialityRequired();
136     }
137     
138     
139     /**
140      * Just deletes the generated key store file.
141      */
142     @After
143     public void deleteKeyStore() throws Exception
144     {
145     	if ( ksFile != null && ksFile.exists() )
146     	{
147     		ksFile.delete();
148     	}
149     	
150     	LOG.debug( "Keystore file deleted: {}", ksFile.getAbsolutePath() );
151     	ldapService.setConfidentialityRequired( oldConfidentialityRequiredValue );
152     }
153     
154 
155     private LdapContext getSecuredContext() throws Exception
156     {
157     	System.setProperty ( "javax.net.ssl.trustStore", ksFile.getAbsolutePath() );
158     	System.setProperty ( "javax.net.ssl.keyStore", ksFile.getAbsolutePath() );
159     	System.setProperty ( "javax.net.ssl.keyStorePassword", "changeit" );
160     	LOG.debug( "testStartTls() test starting ... " );
161     	
162     	// Set up environment for creating initial context
163     	Hashtable<String, Object> env = new Hashtable<String,Object>();
164         env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
165         
166         // Must use the name of the server that is found in its certificate?
167         env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapService.getIpPort() );
168 
169         // Create initial context
170         LOG.debug( "About to get initial context" );
171         LdapContext ctx = new InitialLdapContext( env, null );
172 
173         // Start TLS
174         LOG.debug( "About send startTls extended operation" );
175         StartTlsResponse tls = ( StartTlsResponse ) ctx.extendedOperation( new StartTlsRequest() );
176         LOG.debug( "Extended operation issued" );
177         tls.setHostnameVerifier( new HostnameVerifier() {
178             public boolean verify( String hostname, SSLSession session )
179             {
180                 return true;
181             } 
182         } );
183         LOG.debug( "TLS negotion about to begin" );
184         tls.negotiate();
185         return ctx;
186     }
187     
188 
189     /**
190      * Checks to make sure insecure binds fail while secure binds succeed.
191      */
192     @Test
193     public void testConfidentiality() throws Exception
194     {
195         ldapService.setConfidentialityRequired( true );
196 
197         // -------------------------------------------------------------------
198     	// Unsecured bind should fail
199     	// -------------------------------------------------------------------
200 
201     	try
202     	{
203     		ServerIntegrationUtils.getWiredContext( ldapService );
204     		fail( "Should not get here due to violation of confidentiality requirements" );
205     	}
206     	catch( AuthenticationNotSupportedException e )
207     	{
208     	}
209     	
210     	// -------------------------------------------------------------------
211     	// get anonymous connection with StartTLS (no bind request sent)
212     	// -------------------------------------------------------------------
213 
214     	LdapContext ctx = getSecuredContext();
215     	assertNotNull( ctx );
216     	
217     	// -------------------------------------------------------------------
218     	// upgrade connection via bind request (same physical connection - TLS)
219     	// -------------------------------------------------------------------
220 
221     	ctx.addToEnvironment( Context.SECURITY_PRINCIPAL, "uid=admin,ou=system" );
222     	ctx.addToEnvironment( Context.SECURITY_CREDENTIALS, "secret" );
223     	ctx.addToEnvironment( Context.SECURITY_AUTHENTICATION, "simple" );
224     	ctx.reconnect( null );
225     	
226     	// -------------------------------------------------------------------
227     	// do a search and confirm
228     	// -------------------------------------------------------------------
229 
230     	NamingEnumeration<SearchResult> results = ctx.search( "ou=system", "(objectClass=*)", new SearchControls() );
231     	Set<String> names = new HashSet<String>();
232     	while( results.hasMore() )
233     	{
234     		names.add( results.next().getName() );
235     	}
236     	results.close();
237     	assertTrue( names.contains( "prefNodeName=sysPrefRoot" ) );
238     	assertTrue( names.contains( "ou=users" ) );
239     	assertTrue( names.contains( "ou=configuration" ) );
240     	assertTrue( names.contains( "uid=admin" ) );
241     	assertTrue( names.contains( "ou=groups" ) );
242     	
243     	// -------------------------------------------------------------------
244     	// do add and confirm
245     	// -------------------------------------------------------------------
246 
247     	Attributes attrs = new BasicAttributes( "objectClass", "person", true );
248     	attrs.put( "sn", "foo" );
249     	attrs.put( "cn", "foo bar" );
250     	ctx.createSubcontext( "cn=foo bar,ou=system", attrs );
251     	assertNotNull( ctx.lookup( "cn=foo bar,ou=system" ) );
252     	
253     	// -------------------------------------------------------------------
254     	// do modify and confirm
255     	// -------------------------------------------------------------------
256 
257     	ModificationItem[] mods = new ModificationItem[] {
258     			new ModificationItem( DirContext.ADD_ATTRIBUTE, new BasicAttribute( "cn", "fbar" ) )
259     	};
260     	ctx.modifyAttributes( "cn=foo bar,ou=system", mods );
261     	Attributes reread = ( Attributes ) ctx.getAttributes( "cn=foo bar,ou=system" );
262     	assertTrue( reread.get( "cn" ).contains( "fbar" ) );
263     	
264     	// -------------------------------------------------------------------
265     	// do rename and confirm 
266     	// -------------------------------------------------------------------
267 
268     	ctx.rename( "cn=foo bar,ou=system", "cn=fbar,ou=system" );
269     	try
270     	{
271     		ctx.getAttributes( "cn=foo bar,ou=system" );
272     		fail( "old name of renamed entry should not be found" );
273     	}
274     	catch ( NameNotFoundException e )
275     	{
276     	}
277     	reread = ( Attributes ) ctx.getAttributes( "cn=fbar,ou=system" );
278     	assertTrue( reread.get( "cn" ).contains( "fbar" ) );
279     	
280     	// -------------------------------------------------------------------
281     	// do delete and confirm
282     	// -------------------------------------------------------------------
283 
284     	ctx.destroySubcontext( "cn=fbar,ou=system" );
285     	try
286     	{
287     		ctx.getAttributes( "cn=fbar,ou=system" );
288     		fail( "deleted entry should not be found" );
289     	}
290     	catch ( NameNotFoundException e )
291     	{
292     	}
293     	
294     	ctx.close();
295     }
296 
297 
298     private void search( int ii, LdapContext securedContext ) throws Exception
299     {
300         SearchControls controls = new SearchControls();
301         controls.setSearchScope( SearchControls.SUBTREE_SCOPE );
302         
303         if ( VERBOSE )
304         {
305             System.out.println( "Searching on " + ii + "-th iteration:" );
306         }
307         
308         List<String> results = new ArrayList<String>();
309         NamingEnumeration<SearchResult> ne = securedContext.search( "ou=system", "(objectClass=*)", controls );
310         while ( ne.hasMore() )
311         {
312             String dn = ne.next().getNameInNamespace();
313             results.add( dn );
314             
315             if ( VERBOSE )
316             {
317                 System.out.println( "\tSearch Result = " + dn );
318             }
319         }
320         ne.close();
321         
322         assertEquals( "ou=system", results.get( 0 ) );
323         assertEquals( "uid=admin,ou=system", results.get( 1 ) );
324         assertEquals( "ou=users,ou=system", results.get( 2 ) );
325         assertEquals( "ou=groups,ou=system", results.get( 3 ) );
326         assertEquals( "cn=Administrators,ou=groups,ou=system", results.get( 4 ) );
327         assertEquals( "ou=configuration,ou=system", results.get( 5 ) );
328         assertEquals( "ou=partitions,ou=configuration,ou=system", results.get( 6 ) );
329         assertEquals( "ou=services,ou=configuration,ou=system", results.get( 7 ) );
330         assertEquals( "ou=interceptors,ou=configuration,ou=system", results.get( 8 ) );
331         assertEquals( "prefNodeName=sysPrefRoot,ou=system", results.get( 9 ) );
332     }
333     
334     
335     /**
336      * Tests StartTLS by creating a JNDI connection using the generated key 
337      * store with the installed self signed certificate.  It then searches 
338      * the server and verifies the presence of the expected entries and closes
339      * the connection.  This process repeats for a number of iterations.  
340      * Modify the CONNECT_ITERATIONS constant to change the number of 
341      * iterations.  Modify the VERBOSE constant to print out information while
342      * performing searches.
343      */
344     @Test
345     public void testStartTls() throws Exception
346     {
347         for ( int ii = 0; ii < CONNECT_ITERATIONS; ii++ )
348         {
349             if ( VERBOSE )
350             {
351                 System.out.println( "Performing " + ii + "-th iteration to connect via StartTLS." );
352             }
353 
354             System.setProperty ( "javax.net.ssl.trustStore", ksFile.getAbsolutePath() );
355             System.setProperty ( "javax.net.ssl.keyStore", ksFile.getAbsolutePath() );
356             System.setProperty ( "javax.net.ssl.keyStorePassword", "changeit" );
357             LOG.debug( "testStartTls() test starting ... " );
358             
359             // Set up environment for creating initial context
360             Hashtable<String, Object> env = new Hashtable<String,Object>();
361             env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
362             env.put( "java.naming.security.principal", "uid=admin,ou=system" );
363             env.put( "java.naming.security.credentials", "secret" );
364             env.put( "java.naming.security.authentication", "simple" );
365             
366             // Must use the name of the server that is found in its certificate?
367             env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapService.getIpPort() );
368     
369             // Create initial context
370             LOG.debug( "About to get initial context" );
371             LdapContext ctx = new InitialLdapContext( env, null );
372     
373             // Start TLS
374             LOG.debug( "About send startTls extended operation" );
375             StartTlsResponse tls = ( StartTlsResponse ) ctx.extendedOperation( new StartTlsRequest() );
376             LOG.debug( "Extended operation issued" );
377             tls.setHostnameVerifier( new HostnameVerifier() {
378                 public boolean verify( String hostname, SSLSession session )
379                 {
380                     return true;
381                 } 
382             } );
383             LOG.debug( "TLS negotion about to begin" );
384             tls.negotiate();
385 
386             search( ii, ctx );
387             
388             tls.close();
389             ctx.close();
390         }
391     }
392 }