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 serviceName = "http" +
57             PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
58             "server.dev.rabbit.hole";
59 
60     // Alias
61     private static final String clientAlias = clientName +
62             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
63 
64     // Names + realms
65     private static final String clientKDC1Name = clientAlias.replaceAll(
66             PrincipalName.NAME_REALM_SEPARATOR_STR, "\\\\" +
67             PrincipalName.NAME_REALM_SEPARATOR_STR) +
68             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
69     private static final String clientKDC2Name = clientName +
70             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
71     private static final String serviceKDC2Name = serviceName +
72             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
73 
main(String[] args)74     public static void main(String[] args) throws Exception {
75         try {
76             initializeKDCs();
77             testSubjectCredentials();
78             testDelegated();
79         } finally {
80             cleanup();
81         }
82     }
83 
initializeKDCs()84     private static void initializeKDCs() throws Exception {
85         KDC kdc1 = KDC.create(realmKDC1, "localhost", 0, true);
86         kdc1.addPrincipalRandKey(PrincipalName.TGS_DEFAULT_SRV_NAME +
87                 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1);
88         kdc1.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
89                 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1 +
90                 PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2,
91                 password);
92         kdc1.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
93                 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2,
94                 password);
95 
96         KDC kdc2 = KDC.create(realmKDC2, "localhost", 0, true);
97         kdc2.addPrincipalRandKey(PrincipalName.TGS_DEFAULT_SRV_NAME +
98                 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2);
99         kdc2.addPrincipal(clientKDC2Name, password);
100         kdc2.addPrincipal(serviceName, password);
101         kdc2.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
102                 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1,
103                 password);
104         kdc2.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
105                 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2 +
106                 PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1,
107                 password);
108 
109         kdc1.registerAlias(clientAlias, kdc2);
110         kdc1.registerAlias(serviceName, kdc2);
111         kdc2.registerAlias(clientAlias, clientKDC2Name);
112 
113         Map<String,List<String>> mapKDC2 = new HashMap<>();
114         mapKDC2.put(serviceName + "@" + realmKDC2, Arrays.asList(
115                 new String[]{serviceName + "@" + realmKDC2}));
116         kdc2.setOption(KDC.Option.ALLOW_S4U2PROXY, mapKDC2);
117 
118         KDC.saveConfig(krbConfigName, kdc1, kdc2,
119                     "forwardable=true");
120         System.setProperty("java.security.krb5.conf", krbConfigName);
121     }
122 
cleanup()123     private static void cleanup() {
124         File f = new File(krbConfigName);
125         if (f.exists()) {
126             f.delete();
127         }
128     }
129 
130     /*
131      * The client subject (whose principal is
132      * test@RABBIT.HOLE@RABBIT.HOLE) will obtain a TGT after
133      * realm referral and name canonicalization (TGT cname
134      * will be test@DEV.RABBIT.HOLE). With this TGT, the client will request
135      * a TGS for service http/server.dev.rabbit.hole@RABBIT.HOLE. After
136      * realm referral, a http/server.dev.rabbit.hole@DEV.RABBIT.HOLE TGS
137      * will be obtained.
138      *
139      * Assert that we get the proper TGT and TGS tickets, and that they are
140      * associated to the client subject.
141      *
142      * Assert that if we request a TGS for the same service again (based on the
143      * original service name), we don't get a new one but the previous,
144      * already in the subject credentials.
145      */
testSubjectCredentials()146     private static void testSubjectCredentials() throws Exception {
147         Subject clientSubject = new Subject();
148         Context clientContext = Context.fromUserPass(clientSubject,
149                 clientKDC1Name, password, false);
150 
151         Set<Principal> clientPrincipals = clientSubject.getPrincipals();
152         if (clientPrincipals.size() != 1) {
153             throw new Exception("Only one client subject principal expected");
154         }
155         Principal clientPrincipal = clientPrincipals.iterator().next();
156         if (DEBUG) {
157             System.out.println("Client subject principal: " +
158                     clientPrincipal.getName());
159         }
160         if (!clientPrincipal.getName().equals(clientKDC1Name)) {
161             throw new Exception("Unexpected client subject principal.");
162         }
163 
164         clientContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
165         clientContext.take(new byte[0]);
166         Set<KerberosTicket> clientTickets =
167                 clientSubject.getPrivateCredentials(KerberosTicket.class);
168         boolean tgtFound = false;
169         boolean tgsFound = false;
170         for (KerberosTicket clientTicket : clientTickets) {
171             String cname = clientTicket.getClient().getName();
172             String sname = clientTicket.getServer().getName();
173             if (cname.equals(clientKDC2Name)) {
174                 if (sname.equals(PrincipalName.TGS_DEFAULT_SRV_NAME +
175                         PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
176                         realmKDC2 + PrincipalName.NAME_REALM_SEPARATOR_STR +
177                         realmKDC2)) {
178                     tgtFound = true;
179                 } else if (sname.equals(serviceKDC2Name)) {
180                     tgsFound = true;
181                 }
182             }
183             if (DEBUG) {
184                 System.out.println("Client subject KerberosTicket:");
185                 System.out.println(clientTicket);
186             }
187         }
188         if (!tgtFound || !tgsFound) {
189             throw new Exception("client subject tickets (TGT/TGS) not found.");
190         }
191         int numOfTickets = clientTickets.size();
192         clientContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
193         clientContext.take(new byte[0]);
194         clientContext.status();
195         int newNumOfTickets =
196                 clientSubject.getPrivateCredentials(KerberosTicket.class).size();
197         if (DEBUG) {
198             System.out.println("client subject number of tickets: " +
199                     numOfTickets);
200             System.out.println("client subject new number of tickets: " +
201                     newNumOfTickets);
202         }
203         if (numOfTickets != newNumOfTickets) {
204             throw new Exception("Useless client subject TGS request because" +
205                     " TGS was not found in private credentials.");
206         }
207     }
208 
209     /*
210      * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
211      * will authenticate on itself on behalf of the client
212      * (test@DEV.RABBIT.HOLE). Cross-realm referrals will occur
213      * when requesting different TGTs and TGSs (including the
214      * request for delegated credentials).
215      */
testDelegated()216     private static void testDelegated() throws Exception {
217         Context c = Context.fromUserPass(clientKDC2Name,
218                 password, false);
219         c.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
220         Context s = Context.fromUserPass(serviceKDC2Name,
221                 password, true);
222         s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
223         Context.handshake(c, s);
224         Context delegatedContext = s.delegated();
225         delegatedContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
226         delegatedContext.x().requestMutualAuth(false);
227         Context s2 = Context.fromUserPass(serviceKDC2Name,
228                 password, true);
229         s2.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
230 
231         // Test authentication
232         Context.handshake(delegatedContext, s2);
233         if (!delegatedContext.x().isEstablished() || !s2.x().isEstablished()) {
234             throw new Exception("Delegated authentication failed");
235         }
236 
237         // Test identities
238         GSSName contextInitiatorName = delegatedContext.x().getSrcName();
239         GSSName contextAcceptorName = delegatedContext.x().getTargName();
240         if (DEBUG) {
241             System.out.println("Context initiator: " + contextInitiatorName);
242             System.out.println("Context acceptor: " + contextAcceptorName);
243         }
244         if (!contextInitiatorName.toString().equals(clientKDC2Name) ||
245                 !contextAcceptorName.toString().equals(serviceName)) {
246             throw new Exception("Unexpected initiator or acceptor names");
247         }
248     }
249 }
250