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