1 /*
2  * Copyright (c) 2019, 2021, Red Hat, Inc.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 /*
25  * @test
26  * @bug 8215032
27  * @run main/othervm/timeout=120 -Dsun.security.krb5.debug=true ReferralsTest
28  * @summary Test Kerberos cross-realm referrals (RFC 6806)
29  */
30 
31 import java.io.File;
32 import java.security.Principal;
33 import java.util.Arrays;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 import javax.security.auth.kerberos.KerberosTicket;
39 import javax.security.auth.Subject;
40 import javax.security.auth.login.LoginException;
41 
42 import org.ietf.jgss.GSSName;
43 
44 import sun.security.jgss.GSSUtil;
45 import sun.security.krb5.Config;
46 import sun.security.krb5.PrincipalName;
47 
48 public class ReferralsTest {
49     private static final boolean DEBUG = true;
50     private static final String krbConfigName = "krb5-localkdc.conf";
51     private static final String krbConfigNameNoCanonicalize =
52             "krb5-localkdc-nocanonicalize.conf";
53     private static final String realmKDC1 = "RABBIT.HOLE";
54     private static final String realmKDC2 = "DEV.RABBIT.HOLE";
55     private static final char[] password = "123qwe@Z".toCharArray();
56 
57     // Names
58     private static final String clientName = "test";
59     private static final String userName = "user";
60     private static final String serviceName = "http" +
61             PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
62             "server.dev.rabbit.hole";
63     private static final String backendServiceName = "cifs" +
64             PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
65             "backend.rabbit.hole";
66 
67     // Alias
68     private static final String clientAlias = clientName +
69             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
70 
71     // Names + realms
72     private static final String clientKDC1Name = clientAlias.replaceAll(
73             PrincipalName.NAME_REALM_SEPARATOR_STR, "\\\\" +
74             PrincipalName.NAME_REALM_SEPARATOR_STR) +
75             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
76     private static final String clientKDC2Name = clientName +
77             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
78     private static final String userKDC1Name = userName +
79             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
80     private static final String serviceKDC2Name = serviceName +
81             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
82     private static final String backendKDC1Name = backendServiceName +
83             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
84     private static final String krbtgtKDC1 =
85             PrincipalName.TGS_DEFAULT_SRV_NAME +
86             PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1;
87     private static final String krbtgtKDC2 =
88             PrincipalName.TGS_DEFAULT_SRV_NAME +
89             PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2;
90     private static final String krbtgtKDC1toKDC2 =
91             PrincipalName.TGS_DEFAULT_SRV_NAME +
92             PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2 +
93             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
94     private static final String krbtgtKDC2toKDC1 =
95             PrincipalName.TGS_DEFAULT_SRV_NAME +
96             PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1 +
97             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
98 
main(String[] args)99     public static void main(String[] args) throws Exception {
100         try {
101             initializeKDCs();
102             testSubjectCredentials();
103             testDelegation();
104             testImpersonation();
105             testDelegationWithReferrals();
106             testNoCanonicalize();
107         } finally {
108             cleanup();
109         }
110     }
111 
initializeKDCs()112     private static void initializeKDCs() throws Exception {
113         KDC kdc1 = KDC.create(realmKDC1, "localhost", 0, true);
114         kdc1.addPrincipalRandKey(krbtgtKDC1);
115         kdc1.addPrincipal(krbtgtKDC2toKDC1, password);
116         kdc1.addPrincipal(krbtgtKDC2, password);
117         kdc1.addPrincipal(userKDC1Name, password);
118         kdc1.addPrincipal(backendServiceName, password);
119 
120         KDC kdc2 = KDC.create(realmKDC2, "localhost", 0, true);
121         kdc2.addPrincipalRandKey(krbtgtKDC2);
122         kdc2.addPrincipal(clientKDC2Name, password);
123         kdc2.addPrincipal(serviceName, password);
124         kdc2.addPrincipal(krbtgtKDC1, password);
125         kdc2.addPrincipal(krbtgtKDC1toKDC2, password);
126 
127         kdc1.registerAlias(clientAlias, kdc2);
128         kdc1.registerAlias(serviceName, kdc2);
129         kdc2.registerAlias(clientAlias, clientKDC2Name);
130         kdc2.registerAlias(backendServiceName, kdc1);
131 
132         kdc1.setOption(KDC.Option.ALLOW_S4U2SELF, Arrays.asList(
133                 new String[]{serviceName + "@" + realmKDC2}));
134         Map<String,List<String>> mapKDC1 = new HashMap<>();
135         mapKDC1.put(serviceName + "@" + realmKDC2, Arrays.asList(
136                 new String[]{backendKDC1Name}));
137         kdc1.setOption(KDC.Option.ALLOW_S4U2PROXY, mapKDC1);
138 
139         Map<String,List<String>> mapKDC2 = new HashMap<>();
140         mapKDC2.put(serviceName + "@" + realmKDC2, Arrays.asList(
141                 new String[]{serviceName + "@" + realmKDC2,
142                         krbtgtKDC2toKDC1}));
143         kdc2.setOption(KDC.Option.ALLOW_S4U2PROXY, mapKDC2);
144 
145         KDC.saveConfig(krbConfigName, kdc1, kdc2,
146                 "forwardable=true", "canonicalize=true");
147         KDC.saveConfig(krbConfigNameNoCanonicalize, kdc1, kdc2,
148                 "forwardable=true");
149         System.setProperty("java.security.krb5.conf", krbConfigName);
150     }
151 
cleanup()152     private static void cleanup() {
153         String[] configFiles = new String[]{krbConfigName,
154                 krbConfigNameNoCanonicalize};
155         for (String configFile : configFiles) {
156             File f = new File(configFile);
157             if (f.exists()) {
158                 f.delete();
159             }
160         }
161     }
162 
163     /*
164      * The client subject (whose principal is
165      * test@RABBIT.HOLE@RABBIT.HOLE) will obtain a TGT after
166      * realm referral and name canonicalization (TGT cname
167      * will be test@DEV.RABBIT.HOLE). With this TGT, the client will request
168      * a TGS for service http/server.dev.rabbit.hole@RABBIT.HOLE. After
169      * realm referral, a http/server.dev.rabbit.hole@DEV.RABBIT.HOLE TGS
170      * will be obtained.
171      *
172      * Assert that we get the proper TGT and TGS tickets, and that they are
173      * associated to the client subject.
174      *
175      * Assert that if we request a TGS for the same service again (based on the
176      * original service name), we don't get a new one but the previous,
177      * already in the subject credentials.
178      */
testSubjectCredentials()179     private static void testSubjectCredentials() throws Exception {
180         Subject clientSubject = new Subject();
181         Context clientContext = Context.fromUserPass(clientSubject,
182                 clientKDC1Name, password, false);
183 
184         Set<Principal> clientPrincipals = clientSubject.getPrincipals();
185         if (clientPrincipals.size() != 1) {
186             throw new Exception("Only one client subject principal expected");
187         }
188         Principal clientPrincipal = clientPrincipals.iterator().next();
189         if (DEBUG) {
190             System.out.println("Client subject principal: " +
191                     clientPrincipal.getName());
192         }
193         if (!clientPrincipal.getName().equals(clientKDC1Name)) {
194             throw new Exception("Unexpected client subject principal.");
195         }
196 
197         clientContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
198         clientContext.take(new byte[0]);
199         Set<KerberosTicket> clientTickets =
200                 clientSubject.getPrivateCredentials(KerberosTicket.class);
201         boolean tgtFound = false;
202         boolean tgsFound = false;
203         for (KerberosTicket clientTicket : clientTickets) {
204             String cname = clientTicket.getClient().getName();
205             String sname = clientTicket.getServer().getName();
206             if (cname.equals(clientKDC2Name)) {
207                 if (sname.equals(krbtgtKDC2 +
208                         PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2)) {
209                     tgtFound = true;
210                 } else if (sname.equals(serviceKDC2Name)) {
211                     tgsFound = true;
212                 }
213             }
214             if (DEBUG) {
215                 System.out.println("Client subject KerberosTicket:");
216                 System.out.println(clientTicket);
217             }
218         }
219         if (!tgtFound || !tgsFound) {
220             throw new Exception("client subject tickets (TGT/TGS) not found.");
221         }
222         int numOfTickets = clientTickets.size();
223         clientContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
224         clientContext.take(new byte[0]);
225         clientContext.status();
226         int newNumOfTickets =
227                 clientSubject.getPrivateCredentials(KerberosTicket.class).size();
228         if (DEBUG) {
229             System.out.println("client subject number of tickets: " +
230                     numOfTickets);
231             System.out.println("client subject new number of tickets: " +
232                     newNumOfTickets);
233         }
234         if (numOfTickets != newNumOfTickets) {
235             throw new Exception("Useless client subject TGS request because" +
236                     " TGS was not found in private credentials.");
237         }
238     }
239 
240     /*
241      * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
242      * will authenticate on itself on behalf of the client
243      * (test@DEV.RABBIT.HOLE). Cross-realm referrals will occur
244      * when requesting different TGTs and TGSs (including the
245      * request for delegated credentials).
246      */
testDelegation()247     private static void testDelegation() throws Exception {
248         Context c = Context.fromUserPass(clientKDC2Name,
249                 password, false);
250         c.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
251         Context s = Context.fromUserPass(serviceKDC2Name,
252                 password, true);
253         s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
254         Context.handshake(c, s);
255         Context delegatedContext = s.delegated();
256         delegatedContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
257         delegatedContext.x().requestMutualAuth(false);
258         Context s2 = Context.fromUserPass(serviceKDC2Name,
259                 password, true);
260         s2.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
261 
262         // Test authentication
263         Context.handshake(delegatedContext, s2);
264         if (!delegatedContext.x().isEstablished() || !s2.x().isEstablished()) {
265             throw new Exception("Delegated authentication failed");
266         }
267 
268         // Test identities
269         GSSName contextInitiatorName = delegatedContext.x().getSrcName();
270         GSSName contextAcceptorName = delegatedContext.x().getTargName();
271         if (DEBUG) {
272             System.out.println("Context initiator: " + contextInitiatorName);
273             System.out.println("Context acceptor: " + contextAcceptorName);
274         }
275         if (!contextInitiatorName.toString().equals(clientKDC2Name) ||
276                 !contextAcceptorName.toString().equals(serviceName)) {
277             throw new Exception("Unexpected initiator or acceptor names");
278         }
279     }
280 
281     /*
282      * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
283      * will get a TGS ticket for itself on behalf of the client
284      * (user@RABBIT.HOLE). Cross-realm referrals will be handled
285      * in S4U2Self requests because the user and the server are
286      * on different realms.
287      */
testImpersonation()288     private static void testImpersonation() throws Exception {
289         testImpersonationSingle();
290 
291         // Try a second time to force the use of the Referrals Cache.
292         // During this execution, the referral ticket from RABBIT.HOLE
293         // to DEV.RABBIT.HOLE (upon the initial S4U2Self message) will
294         // be obtained from the Cache.
295         testImpersonationSingle();
296     }
297 
testImpersonationSingle()298     private static void testImpersonationSingle() throws Exception {
299         Context s = Context.fromUserPass(serviceKDC2Name, password, true);
300         s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
301         GSSName impName = s.impersonate(userKDC1Name).cred().getName();
302         if (DEBUG) {
303             System.out.println("Impersonated name: " + impName);
304         }
305         if (!impName.toString().equals(userKDC1Name)) {
306             throw new Exception("Unexpected impersonated name");
307         }
308     }
309 
310     /*
311      * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
312      * will use delegated credentials (user@RABBIT.HOLE) to
313      * authenticate in the backend (cifs/backend.rabbit.hole@RABBIT.HOLE).
314      * Cross-realm referrals will be handled in S4U2Proxy requests
315      * because the server and the backend are on different realms.
316      */
testDelegationWithReferrals()317     private static void testDelegationWithReferrals() throws Exception {
318         testDelegationWithReferralsSingle();
319 
320         // Try a second time to force the use of the Referrals Cache.
321         // During this execution, the referral ticket from RABBIT.HOLE
322         // to DEV.RABBIT.HOLE (upon the initial S4U2Proxy message) will
323         // be obtained from the Cache.
324         testDelegationWithReferralsSingle();
325     }
326 
testDelegationWithReferralsSingle()327     private static void testDelegationWithReferralsSingle() throws Exception {
328         Context c = Context.fromUserPass(userKDC1Name, password, false);
329         c.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
330         Context s = Context.fromUserPass(serviceKDC2Name, password, true);
331         s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
332         Context.handshake(c, s);
333         Context delegatedContext = s.delegated();
334         delegatedContext.startAsClient(backendServiceName,
335                 GSSUtil.GSS_KRB5_MECH_OID);
336         delegatedContext.x().requestMutualAuth(false);
337         Context b = Context.fromUserPass(backendKDC1Name, password, true);
338         b.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
339 
340         // Test authentication
341         Context.handshake(delegatedContext, b);
342         if (!delegatedContext.x().isEstablished() || !b.x().isEstablished()) {
343             throw new Exception("Delegated authentication failed");
344         }
345 
346         // Test identities
347         GSSName contextInitiatorName = delegatedContext.x().getSrcName();
348         GSSName contextAcceptorName = delegatedContext.x().getTargName();
349         if (DEBUG) {
350             System.out.println("Context initiator: " + contextInitiatorName);
351             System.out.println("Context acceptor: " + contextAcceptorName);
352         }
353         if (!contextInitiatorName.toString().equals(userKDC1Name) ||
354                 !contextAcceptorName.toString().equals(backendServiceName)) {
355             throw new Exception("Unexpected initiator or acceptor names");
356         }
357     }
358 
359     /*
360      * The client tries to get a TGT (AS protocol) as in testSubjectCredentials
361      * but without the canonicalize setting in krb5.conf. The KDC
362      * must not return a referral but a failure because the client
363      * is not in the local database.
364      */
testNoCanonicalize()365     private static void testNoCanonicalize() throws Exception {
366         System.setProperty("java.security.krb5.conf",
367                 krbConfigNameNoCanonicalize);
368         Config.refresh();
369         try {
370             Context.fromUserPass(new Subject(),
371                     clientKDC1Name, password, false);
372             throw new Exception("should not succeed");
373         } catch (LoginException e) {
374             // expected
375         }
376     }
377 }
378