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