1 /*
2  * Copyright (c) 2001, 2021, Oracle and/or its affiliates. All rights reserved.
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.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 /*
27  *
28  *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
29  *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
30  */
31 
32 package sun.security.krb5.internal;
33 
34 import sun.security.krb5.*;
35 import sun.security.util.DerValue;
36 
37 import java.io.IOException;
38 import java.util.LinkedList;
39 import java.util.List;
40 
41 /**
42  * This class is a utility that contains much of the TGS-Exchange
43  * protocol. It is used by ../Credentials.java for service ticket
44  * acquisition in both the normal and the x-realm case.
45  */
46 public class CredentialsUtil {
47 
48     private static boolean DEBUG = sun.security.krb5.internal.Krb5.DEBUG;
49 
50     private static enum S4U2Type {
51         NONE, SELF, PROXY
52     }
53 
54     /**
55      * Used by a middle server to acquire credentials on behalf of a
56      * user to itself using the S4U2self extension.
57      * @param user the user to impersonate
58      * @param ccreds the TGT of the middle service
59      * @return the new creds (cname=user, sname=middle)
60      */
acquireS4U2selfCreds(PrincipalName user, Credentials ccreds)61     public static Credentials acquireS4U2selfCreds(PrincipalName user,
62             Credentials ccreds) throws KrbException, IOException {
63         if (!ccreds.isForwardable()) {
64             throw new KrbException("S4U2self needs a FORWARDABLE ticket");
65         }
66         PrincipalName sname = ccreds.getClient();
67         String uRealm = user.getRealmString();
68         String localRealm = ccreds.getClient().getRealmString();
69         if (!uRealm.equals(localRealm)) {
70             // Referrals will be required because the middle service
71             // and the user impersonated are on different realms.
72             if (Config.DISABLE_REFERRALS) {
73                 throw new KrbException("Cross-realm S4U2Self request not" +
74                         " possible when referrals are disabled.");
75             }
76             if (ccreds.getClientAlias() != null) {
77                 // If the name was canonicalized, the user pick
78                 // has preference. This gives the possibility of
79                 // using FQDNs that KDCs may use to return referrals.
80                 // I.e.: a SVC/host.realm-2.com@REALM-1.COM name
81                 // may be used by REALM-1.COM KDC to return a
82                 // referral to REALM-2.COM.
83                 sname = ccreds.getClientAlias();
84             }
85             sname = new PrincipalName(sname.getNameType(),
86                     sname.getNameStrings(), new Realm(uRealm));
87         }
88         Credentials creds = serviceCreds(
89                 KDCOptions.with(KDCOptions.FORWARDABLE),
90                 ccreds, ccreds.getClient(), sname, user,
91                 null, new PAData[] {
92                         new PAData(Krb5.PA_FOR_USER,
93                                 new PAForUserEnc(user,
94                                         ccreds.getSessionKey()).asn1Encode()),
95                         new PAData(Krb5.PA_PAC_OPTIONS,
96                                 new PaPacOptions()
97                                         .setResourceBasedConstrainedDelegation(true)
98                                         .setClaims(true)
99                                         .asn1Encode())
100                         }, S4U2Type.SELF);
101         if (!creds.getClient().equals(user)) {
102             throw new KrbException("S4U2self request not honored by KDC");
103         }
104         if (!creds.isForwardable()) {
105             throw new KrbException("S4U2self ticket must be FORWARDABLE");
106         }
107         return creds;
108     }
109 
110     /**
111      * Used by a middle server to acquire a service ticket to a backend
112      * server using the S4U2proxy extension.
113      * @param backend the name of the backend service
114      * @param second the client's service ticket to the middle server
115      * @param ccreds the TGT of the middle server
116      * @return the creds (cname=client, sname=backend)
117      */
acquireS4U2proxyCreds( String backend, Ticket second, PrincipalName client, Credentials ccreds)118     public static Credentials acquireS4U2proxyCreds(
119                 String backend, Ticket second,
120                 PrincipalName client, Credentials ccreds)
121             throws KrbException, IOException {
122         PrincipalName backendPrincipal = new PrincipalName(backend);
123         String backendRealm = backendPrincipal.getRealmString();
124         String localRealm = ccreds.getClient().getRealmString();
125         if (!backendRealm.equals(localRealm)) {
126             // The middle service and the backend service are on
127             // different realms, so referrals will be required.
128             if (Config.DISABLE_REFERRALS) {
129                 throw new KrbException("Cross-realm S4U2Proxy request not" +
130                         " possible when referrals are disabled.");
131             }
132             backendPrincipal = new PrincipalName(
133                     backendPrincipal.getNameType(),
134                     backendPrincipal.getNameStrings(),
135                     new Realm(localRealm));
136         }
137         Credentials creds = serviceCreds(KDCOptions.with(
138                 KDCOptions.CNAME_IN_ADDL_TKT, KDCOptions.FORWARDABLE),
139                 ccreds, ccreds.getClient(), backendPrincipal, null,
140                 new Ticket[] {second}, new PAData[] {
141                         new PAData(Krb5.PA_PAC_OPTIONS,
142                                 new PaPacOptions()
143                                         .setResourceBasedConstrainedDelegation(true)
144                                         .setClaims(true)
145                                         .asn1Encode())
146                         }, S4U2Type.PROXY);
147         if (!creds.getClient().equals(client)) {
148             throw new KrbException("S4U2proxy request not honored by KDC");
149         }
150         return creds;
151     }
152 
153     /**
154      * Acquires credentials for a specified service using initial
155      * credential. When the service has a different realm from the initial
156      * credential, we do cross-realm authentication - first, we use the
157      * current credential to get a cross-realm credential from the local KDC,
158      * then use that cross-realm credential to request service credential
159      * from the foreign KDC.
160      *
161      * @param service the name of service principal
162      * @param ccreds client's initial credential
163      */
acquireServiceCreds( String service, Credentials ccreds)164     public static Credentials acquireServiceCreds(
165                 String service, Credentials ccreds)
166             throws KrbException, IOException {
167         PrincipalName sname = new PrincipalName(service,
168                 PrincipalName.KRB_NT_UNKNOWN);
169         return serviceCreds(sname, ccreds);
170     }
171 
172     /**
173      * Gets a TGT to another realm
174      * @param localRealm this realm
175      * @param serviceRealm the other realm, cannot equals to localRealm
176      * @param ccreds TGT in this realm
177      * @param okAsDelegate an [out] argument to receive the okAsDelegate
178      * property. True only if all realms allow delegation.
179      * @return the TGT for the other realm, null if cannot find a path
180      * @throws KrbException if something goes wrong
181      */
getTGTforRealm(String localRealm, String serviceRealm, Credentials ccreds, boolean[] okAsDelegate)182     private static Credentials getTGTforRealm(String localRealm,
183             String serviceRealm, Credentials ccreds, boolean[] okAsDelegate)
184             throws KrbException {
185 
186         // Get a list of realms to traverse
187         String[] realms = Realm.getRealmsList(localRealm, serviceRealm);
188 
189         int i = 0, k = 0;
190         Credentials cTgt = null, newTgt = null, theTgt = null;
191         PrincipalName tempService = null;
192         String newTgtRealm = null;
193 
194         okAsDelegate[0] = true;
195         for (cTgt = ccreds, i = 0; i < realms.length;) {
196             tempService = PrincipalName.tgsService(serviceRealm, realms[i]);
197 
198             if (DEBUG) {
199                 System.out.println(
200                         ">>> Credentials acquireServiceCreds: main loop: ["
201                         + i +"] tempService=" + tempService);
202             }
203 
204             try {
205                 newTgt = serviceCreds(tempService, cTgt);
206             } catch (Exception exc) {
207                 newTgt = null;
208             }
209 
210             if (newTgt == null) {
211                 if (DEBUG) {
212                     System.out.println(">>> Credentials acquireServiceCreds: "
213                             + "no tgt; searching thru capath");
214                 }
215 
216                 /*
217                  * No tgt found. Let's go thru the realms list one by one.
218                  */
219                 for (newTgt = null, k = i+1;
220                         newTgt == null && k < realms.length; k++) {
221                     tempService = PrincipalName.tgsService(realms[k], realms[i]);
222                     if (DEBUG) {
223                         System.out.println(
224                                 ">>> Credentials acquireServiceCreds: "
225                                 + "inner loop: [" + k
226                                 + "] tempService=" + tempService);
227                     }
228                     try {
229                         newTgt = serviceCreds(tempService, cTgt);
230                     } catch (Exception exc) {
231                         newTgt = null;
232                     }
233                 }
234             } // Ends 'if (newTgt == null)'
235 
236             if (newTgt == null) {
237                 if (DEBUG) {
238                     System.out.println(">>> Credentials acquireServiceCreds: "
239                             + "no tgt; cannot get creds");
240                 }
241                 break;
242             }
243 
244             /*
245              * We have a tgt. It may or may not be for the target.
246              * If it's for the target realm, we're done looking for a tgt.
247              */
248             newTgtRealm = newTgt.getServer().getInstanceComponent();
249             if (okAsDelegate[0] && !newTgt.checkDelegate()) {
250                 if (DEBUG) {
251                     System.out.println(">>> Credentials acquireServiceCreds: " +
252                             "global OK-AS-DELEGATE turned off at " +
253                             newTgt.getServer());
254                 }
255                 okAsDelegate[0] = false;
256             }
257 
258             if (DEBUG) {
259                 System.out.println(">>> Credentials acquireServiceCreds: "
260                         + "got tgt");
261             }
262 
263             if (newTgtRealm.equals(serviceRealm)) {
264                 /* We got the right tgt */
265                 theTgt = newTgt;
266                 break;
267             }
268 
269             /*
270              * The new tgt is not for the target realm.
271              * See if the realm of the new tgt is in the list of realms
272              * and continue looking from there.
273              */
274             for (k = i+1; k < realms.length; k++) {
275                 if (newTgtRealm.equals(realms[k])) {
276                     break;
277                 }
278             }
279 
280             if (k < realms.length) {
281                 /*
282                  * (re)set the counter so we start looking
283                  * from the realm we just obtained a tgt for.
284                  */
285                 i = k;
286                 cTgt = newTgt;
287 
288                 if (DEBUG) {
289                     System.out.println(">>> Credentials acquireServiceCreds: "
290                             + "continuing with main loop counter reset to " + i);
291                 }
292                 continue;
293             }
294             else {
295                 /*
296                  * The new tgt's realm is not in the hierarchy of realms.
297                  * It's probably not safe to get a tgt from
298                  * a tgs that is outside the known list of realms.
299                  * Give up now.
300                  */
301                 break;
302             }
303         } // Ends outermost/main 'for' loop
304 
305         return theTgt;
306     }
307 
308    /*
309     * This method does the real job to request the service credential.
310     */
serviceCreds( PrincipalName service, Credentials ccreds)311     private static Credentials serviceCreds(
312             PrincipalName service, Credentials ccreds)
313             throws KrbException, IOException {
314         return serviceCreds(new KDCOptions(), ccreds,
315                 ccreds.getClient(), service, null, null,
316                 null, S4U2Type.NONE);
317     }
318 
319     /*
320      * Obtains credentials for a service (TGS).
321      * Cross-realm referrals are handled if enabled. A fallback scheme
322      * without cross-realm referrals supports is used in case of server
323      * error to maintain backward compatibility.
324      */
serviceCreds( KDCOptions options, Credentials asCreds, PrincipalName cname, PrincipalName sname, PrincipalName user, Ticket[] additionalTickets, PAData[] extraPAs, S4U2Type s4u2Type)325     private static Credentials serviceCreds(
326             KDCOptions options, Credentials asCreds,
327             PrincipalName cname, PrincipalName sname,
328             PrincipalName user, Ticket[] additionalTickets,
329             PAData[] extraPAs, S4U2Type s4u2Type)
330             throws KrbException, IOException {
331         if (!Config.DISABLE_REFERRALS) {
332             try {
333                 return serviceCredsReferrals(options, asCreds, cname, sname,
334                         s4u2Type, user, additionalTickets, extraPAs);
335             } catch (KrbException e) {
336                 // Server may raise an error if CANONICALIZE is true.
337                 // Try CANONICALIZE false.
338             }
339         }
340         return serviceCredsSingle(options, asCreds, cname,
341                 asCreds.getClientAlias(), sname, sname, s4u2Type,
342                 user, additionalTickets, extraPAs);
343     }
344 
345     /*
346      * Obtains credentials for a service (TGS).
347      * May handle and follow cross-realm referrals as defined by RFC 6806.
348      */
serviceCredsReferrals( KDCOptions options, Credentials asCreds, PrincipalName cname, PrincipalName sname, S4U2Type s4u2Type, PrincipalName user, Ticket[] additionalTickets, PAData[] extraPAs)349     private static Credentials serviceCredsReferrals(
350             KDCOptions options, Credentials asCreds,
351             PrincipalName cname, PrincipalName sname,
352             S4U2Type s4u2Type, PrincipalName user,
353             Ticket[] additionalTickets, PAData[] extraPAs)
354                     throws KrbException, IOException {
355         options = new KDCOptions(options.toBooleanArray());
356         options.set(KDCOptions.CANONICALIZE, true);
357         PrincipalName cSname = sname;
358         PrincipalName refSname = sname; // May change with referrals
359         Credentials creds = null;
360         boolean isReferral = false;
361         List<String> referrals = new LinkedList<>();
362         PrincipalName clientAlias = asCreds.getClientAlias();
363         while (referrals.size() <= Config.MAX_REFERRALS) {
364             ReferralsCache.ReferralCacheEntry ref =
365                     ReferralsCache.get(cname, sname, user,
366                             additionalTickets, refSname.getRealmString());
367             String toRealm = null;
368             if (ref == null) {
369                 creds = serviceCredsSingle(options, asCreds, cname,
370                         clientAlias, refSname, cSname, s4u2Type,
371                         user, additionalTickets, extraPAs);
372                 PrincipalName server = creds.getServer();
373                 if (!refSname.equals(server)) {
374                     String[] serverNameStrings = server.getNameStrings();
375                     if (serverNameStrings.length == 2 &&
376                         serverNameStrings[0].equals(
377                                 PrincipalName.TGS_DEFAULT_SRV_NAME) &&
378                         !refSname.getRealmAsString().equals(
379                                 serverNameStrings[1])) {
380                         // Server Name (sname) has the following format:
381                         //      krbtgt/TO-REALM.COM@FROM-REALM.COM
382                         ReferralsCache.put(cname, sname, user,
383                                 additionalTickets, server.getRealmString(),
384                                 serverNameStrings[1], creds);
385                         toRealm = serverNameStrings[1];
386                         isReferral = true;
387                     }
388                 }
389             } else {
390                 creds = ref.getCreds();
391                 toRealm = ref.getToRealm();
392                 isReferral = true;
393             }
394             if (isReferral) {
395                 if (s4u2Type == S4U2Type.PROXY) {
396                     Credentials[] credsInOut =
397                             new Credentials[] {creds, null};
398                     toRealm = handleS4U2ProxyReferral(asCreds,
399                             credsInOut, sname);
400                     creds = credsInOut[0];
401                     if (additionalTickets == null ||
402                             additionalTickets.length == 0 ||
403                             credsInOut[1] == null) {
404                         throw new KrbException("Additional tickets expected" +
405                                 " for S4U2Proxy.");
406                     }
407                     additionalTickets[0] = credsInOut[1].getTicket();
408                 } else if (s4u2Type == S4U2Type.SELF) {
409                     handleS4U2SelfReferral(extraPAs, user, creds);
410                 }
411                 if (referrals.contains(toRealm)) {
412                     // Referrals loop detected
413                     return null;
414                 }
415                 asCreds = creds;
416                 refSname = new PrincipalName(refSname.getNameString(),
417                         refSname.getNameType(), toRealm);
418                 referrals.add(toRealm);
419                 isReferral = false;
420                 continue;
421             }
422             break;
423         }
424         return creds;
425     }
426 
427     /*
428      * Obtains credentials for a service (TGS).
429      * If the service realm is different than the one in the TGT, a new TGT for
430      * the service realm is obtained first (see getTGTforRealm call). This is
431      * not expected when following cross-realm referrals because the referral
432      * TGT realm matches the service realm.
433      */
serviceCredsSingle( KDCOptions options, Credentials asCreds, PrincipalName cname, PrincipalName clientAlias, PrincipalName refSname, PrincipalName sname, S4U2Type s4u2Type, PrincipalName user, Ticket[] additionalTickets, PAData[] extraPAs)434     private static Credentials serviceCredsSingle(
435             KDCOptions options, Credentials asCreds,
436             PrincipalName cname, PrincipalName clientAlias,
437             PrincipalName refSname, PrincipalName sname,
438             S4U2Type s4u2Type, PrincipalName user,
439             Ticket[] additionalTickets, PAData[] extraPAs)
440                     throws KrbException, IOException {
441         Credentials theCreds = null;
442         boolean[] okAsDelegate = new boolean[]{true};
443         String[] serverAsCredsNames = asCreds.getServer().getNameStrings();
444         String tgtRealm = serverAsCredsNames[1];
445         String serviceRealm = refSname.getRealmString();
446         if (!serviceRealm.equals(tgtRealm)) {
447             // This is a cross-realm service request
448             if (DEBUG) {
449                 System.out.println(">>> serviceCredsSingle:" +
450                         " cross-realm authentication");
451                 System.out.println(">>> serviceCredsSingle:" +
452                         " obtaining credentials from " + tgtRealm +
453                         " to " + serviceRealm);
454             }
455             Credentials newTgt = getTGTforRealm(tgtRealm, serviceRealm,
456                     asCreds, okAsDelegate);
457             if (newTgt == null) {
458                 throw new KrbApErrException(Krb5.KRB_AP_ERR_GEN_CRED,
459                         "No service creds");
460             }
461             if (DEBUG) {
462                 System.out.println(">>> Cross-realm TGT Credentials" +
463                         " serviceCredsSingle: ");
464                 Credentials.printDebug(newTgt);
465             }
466             if (s4u2Type == S4U2Type.SELF) {
467                 handleS4U2SelfReferral(extraPAs, user, newTgt);
468             }
469             asCreds = newTgt;
470             cname = asCreds.getClient();
471         } else if (DEBUG) {
472             System.out.println(">>> Credentials serviceCredsSingle:" +
473                     " same realm");
474         }
475         KrbTgsReq req = new KrbTgsReq(options, asCreds, cname, clientAlias,
476                 refSname, sname, additionalTickets, extraPAs);
477         theCreds = req.sendAndGetCreds();
478         if (theCreds != null) {
479             if (DEBUG) {
480                 System.out.println(">>> TGS credentials serviceCredsSingle:");
481                 Credentials.printDebug(theCreds);
482             }
483             if (!okAsDelegate[0]) {
484                 theCreds.resetDelegate();
485             }
486         }
487         return theCreds;
488     }
489 
490     /**
491      * PA-FOR-USER may need to be regenerated if credentials
492      * change. This may happen when obtaining a TGT for a
493      * different realm or when using a referral TGT.
494      */
handleS4U2SelfReferral(PAData[] pas, PrincipalName user, Credentials newCreds)495     private static void handleS4U2SelfReferral(PAData[] pas,
496             PrincipalName user, Credentials newCreds)
497                     throws Asn1Exception, KrbException, IOException {
498         if (DEBUG) {
499             System.out.println(">>> Handling S4U2Self referral");
500         }
501         for (int i = 0; i < pas.length; i++) {
502             PAData pa = pas[i];
503             if (pa.getType() == Krb5.PA_FOR_USER) {
504                 pas[i] = new PAData(Krb5.PA_FOR_USER,
505                         new PAForUserEnc(user,
506                                 newCreds.getSessionKey()).asn1Encode());
507                 break;
508             }
509         }
510     }
511 
512     /**
513      * This method is called after receiving the first realm referral for
514      * a S4U2Proxy request. The credentials and tickets needed for the
515      * final S4U2Proxy request (in the referrals chain) are returned.
516      *
517      * Referrals are handled as described by MS-SFU (section 3.1.5.2.2
518      * Receives Referral).
519      *
520      * @param asCreds middle service credentials used for the first S4U2Proxy
521      *        request
522      * @param credsInOut (in/out parameter):
523      *         * input: first S4U2Proxy referral TGT received, null
524      *         * output: referral TGT for final S4U2Proxy service request,
525      *                   client referral TGT for final S4U2Proxy service request
526      *                   (to be sent as additional-ticket)
527      * @param sname the backend service name
528      * @param additionalTickets (out parameter): the additional ticket for the
529      *        last S4U2Proxy request is returned
530      * @return the backend realm for the last S4U2Proxy request
531      */
handleS4U2ProxyReferral(Credentials asCreds, Credentials[] credsInOut, PrincipalName sname)532     private static String handleS4U2ProxyReferral(Credentials asCreds,
533             Credentials[] credsInOut, PrincipalName sname)
534                     throws KrbException, IOException {
535         if (DEBUG) {
536             System.out.println(">>> Handling S4U2Proxy referral");
537         }
538         Credentials refTGT = null;
539         // Get a credential for the middle service to the backend so we know
540         // the backend realm, as described in MS-SFU (section 3.1.5.2.2).
541         Credentials middleSvcCredsInBackendRealm =
542                 serviceCreds(sname, asCreds);
543         String backendRealm =
544                 middleSvcCredsInBackendRealm.getServer().getRealmString();
545         String toRealm = credsInOut[0].getServer().getNameStrings()[1];
546         if (!toRealm.equals(backendRealm)) {
547             // More than 1 hop. Follow the referrals chain and obtain a
548             // TGT for the backend realm.
549             refTGT = getTGTforRealm(toRealm, backendRealm, credsInOut[0],
550                     new boolean[1]);
551         } else {
552             // There was only 1 hop. The referral TGT received is already
553             // for the backend realm.
554             refTGT = credsInOut[0];
555         }
556         credsInOut[0] = getTGTforRealm(asCreds.getClient().getRealmString(),
557                 backendRealm, asCreds, new boolean[1]);
558         credsInOut[1] = refTGT;
559         return backendRealm;
560     }
561 }
562