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