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