1 /*
2  * Copyright (c) 2010, 2020, 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 package sun.security.krb5;
27 
28 import java.io.IOException;
29 import java.util.Arrays;
30 import javax.security.auth.kerberos.KeyTab;
31 import sun.security.jgss.krb5.Krb5Util;
32 import sun.security.krb5.internal.HostAddresses;
33 import sun.security.krb5.internal.KDCOptions;
34 import sun.security.krb5.internal.KRBError;
35 import sun.security.krb5.internal.KerberosTime;
36 import sun.security.krb5.internal.Krb5;
37 import sun.security.krb5.internal.PAData;
38 import sun.security.krb5.internal.crypto.EType;
39 
40 /**
41  * A manager class for AS-REQ communications.
42  *
43  * This class does:
44  * 1. Gather information to create AS-REQ
45  * 2. Create and send AS-REQ
46  * 3. Receive AS-REP and KRB-ERROR (-KRB_ERR_RESPONSE_TOO_BIG) and parse them
47  * 4. Emit credentials and secret keys (for JAAS storeKey=true with password)
48  *
49  * This class does not:
50  * 1. Deal with real communications (KdcComm does it, and TGS-REQ)
51  *    a. Name of KDCs for a realm
52  *    b. Server availability, timeout, UDP or TCP
53  *    d. KRB_ERR_RESPONSE_TOO_BIG
54  * 2. Stores its own copy of password, this means:
55  *    a. Do not change/wipe it before Builder finish
56  *    b. Builder will not wipe it for you
57  *
58  * With this class:
59  * 1. KrbAsReq has only one constructor
60  * 2. Krb5LoginModule and Kinit call a single builder
61  * 3. Better handling of sensitive info
62  *
63  * @since 1.7
64  */
65 
66 public final class KrbAsReqBuilder {
67 
68     // Common data for AS-REQ fields
69     private KDCOptions options;
70     private PrincipalName cname;
71     private PrincipalName refCname; // May be changed by referrals
72     private PrincipalName sname;
73     private KerberosTime from;
74     private KerberosTime till;
75     private KerberosTime rtime;
76     private HostAddresses addresses;
77 
78     // Secret source: can't be changed once assigned, only one (of the two
79     // sources) can be set to non-null
80     private final char[] password;
81     private final KeyTab ktab;
82 
83     // Used to create a ENC-TIMESTAMP in the 2nd AS-REQ
84     private PAData[] paList;        // PA-DATA from both KRB-ERROR and AS-REP.
85                                     // Used by getKeys() only.
86                                     // Only AS-REP should be enough per RFC,
87                                     // combined in case etypes are different.
88 
89     // The generated and received:
90     private KrbAsReq req;
91     private KrbAsRep rep;
92 
93     private static enum State {
94         INIT,       // Initialized, can still add more initialization info
95         REQ_OK,     // AS-REQ performed
96         DESTROYED,  // Destroyed, not usable anymore
97     }
98     private State state;
99 
100     // Called by other constructors
init(PrincipalName cname)101     private void init(PrincipalName cname)
102             throws KrbException {
103         this.cname = cname;
104         this.refCname = cname;
105         state = State.INIT;
106     }
107 
108     /**
109      * Creates a builder to be used by {@code cname} with existing keys.
110      *
111      * @param cname the client of the AS-REQ. Must not be null. Might have no
112      * realm, where default realm will be used. This realm will be the target
113      * realm for AS-REQ. I believe a client should only get initial TGT from
114      * its own realm.
115      * @param ktab must not be null. If empty, might be quite useless.
116      * This argument will neither be modified nor stored by the method.
117      * @throws KrbException
118      */
KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)119     public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)
120             throws KrbException {
121         init(cname);
122         this.ktab = ktab;
123         this.password = null;
124     }
125 
126     /**
127      * Creates a builder to be used by {@code cname} with a known password.
128      *
129      * @param cname the client of the AS-REQ. Must not be null. Might have no
130      * realm, where default realm will be used. This realm will be the target
131      * realm for AS-REQ. I believe a client should only get initial TGT from
132      * its own realm.
133      * @param pass must not be null. This argument will neither be modified
134      * nor stored by the method.
135      * @throws KrbException
136      */
KrbAsReqBuilder(PrincipalName cname, char[] pass)137     public KrbAsReqBuilder(PrincipalName cname, char[] pass)
138             throws KrbException {
139         init(cname);
140         this.password = pass.clone();
141         this.ktab = null;
142     }
143 
144     /**
145      * Retrieves an array of secret keys for the client. This is used when
146      * the client supplies password but need keys to act as an acceptor. For
147      * an initiator, it must be called after AS-REQ is performed (state is OK).
148      * For an acceptor, it can be called when this KrbAsReqBuilder object is
149      * constructed (state is INIT).
150      * @param isInitiator if the caller is an initiator
151      * @return generated keys from password. PA-DATA from server might be used.
152      * All "default_tkt_enctypes" keys will be generated, Never null.
153      * @throws IllegalStateException if not constructed from a password
154      * @throws KrbException
155      */
getKeys(boolean isInitiator)156     public EncryptionKey[] getKeys(boolean isInitiator) throws KrbException {
157         checkState(isInitiator?State.REQ_OK:State.INIT, "Cannot get keys");
158         if (password != null) {
159             int[] eTypes = EType.getDefaults("default_tkt_enctypes");
160             EncryptionKey[] result = new EncryptionKey[eTypes.length];
161 
162             /*
163              * Returns an array of keys. Before KrbAsReqBuilder, all etypes
164              * use the same salt which is either the default one or a new salt
165              * coming from PA-DATA. After KrbAsReqBuilder, each etype uses its
166              * own new salt from PA-DATA. For an etype with no PA-DATA new salt
167              * at all, what salt should it use?
168              *
169              * Commonly, the stored keys are only to be used by an acceptor to
170              * decrypt service ticket in AP-REQ. Most impls only allow keys
171              * from a keytab on acceptor, but unfortunately (?) Java supports
172              * acceptor using password. In this case, if the service ticket is
173              * encrypted using an etype which we don't have PA-DATA new salt,
174              * using the default salt might be wrong (say, case-insensitive
175              * user name). Instead, we would use the new salt of another etype.
176              */
177 
178             String salt = null;     // the saved new salt
179             try {
180                 for (int i=0; i<eTypes.length; i++) {
181                     // First round, only calculate those have a PA entry
182                     PAData.SaltAndParams snp =
183                             PAData.getSaltAndParams(eTypes[i], paList);
184                     if (snp != null) {
185                         // Never uses a salt for rc4-hmac, it does not use
186                         // a salt at all
187                         if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC &&
188                                 snp.salt != null) {
189                             salt = snp.salt;
190                         }
191                         result[i] = EncryptionKey.acquireSecretKey(cname,
192                                 password,
193                                 eTypes[i],
194                                 snp);
195                     }
196                 }
197                 // No new salt from PA, maybe empty, maybe only rc4-hmac
198                 if (salt == null) salt = cname.getSalt();
199                 for (int i=0; i<eTypes.length; i++) {
200                     // Second round, calculate those with no PA entry
201                     if (result[i] == null) {
202                         result[i] = EncryptionKey.acquireSecretKey(password,
203                                 salt,
204                                 eTypes[i],
205                                 null);
206                     }
207                 }
208             } catch (IOException ioe) {
209                 KrbException ke = new KrbException(Krb5.ASN1_PARSE_ERROR);
210                 ke.initCause(ioe);
211                 throw ke;
212             }
213             return result;
214         } else {
215             throw new IllegalStateException("Required password not provided");
216         }
217     }
218 
219     /**
220      * Sets or clears options. If cleared, default options will be used
221      * at creation time.
222      * @param options
223      */
setOptions(KDCOptions options)224     public void setOptions(KDCOptions options) {
225         checkState(State.INIT, "Cannot specify options");
226         this.options = options;
227     }
228 
setTill(KerberosTime till)229     public void setTill(KerberosTime till) {
230         checkState(State.INIT, "Cannot specify till");
231         this.till = till;
232     }
233 
setRTime(KerberosTime rtime)234     public void setRTime(KerberosTime rtime) {
235         checkState(State.INIT, "Cannot specify rtime");
236         this.rtime = rtime;
237     }
238 
239     /**
240      * Sets or clears target. If cleared, KrbAsReq might choose krbtgt
241      * for cname realm
242      * @param sname
243      */
setTarget(PrincipalName sname)244     public void setTarget(PrincipalName sname) {
245         checkState(State.INIT, "Cannot specify target");
246         this.sname = sname;
247     }
248 
249     /**
250      * Adds or clears addresses. KrbAsReq might add some if empty
251      * field not allowed
252      * @param addresses
253      */
setAddresses(HostAddresses addresses)254     public void setAddresses(HostAddresses addresses) {
255         checkState(State.INIT, "Cannot specify addresses");
256         this.addresses = addresses;
257     }
258 
259     /**
260      * Build a KrbAsReq object from all info fed above. Normally this method
261      * will be called twice: initial AS-REQ and second with pakey
262      * @param key null (initial AS-REQ) or pakey (with preauth)
263      * @return the KrbAsReq object
264      * @throws KrbException
265      * @throws IOException
266      */
build(EncryptionKey key, ReferralsState referralsState)267     private KrbAsReq build(EncryptionKey key, ReferralsState referralsState)
268             throws KrbException, IOException {
269         PAData[] extraPAs = null;
270         int[] eTypes;
271         if (password != null) {
272             eTypes = EType.getDefaults("default_tkt_enctypes");
273         } else {
274             EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
275             eTypes = EType.getDefaults("default_tkt_enctypes",
276                     ks);
277             for (EncryptionKey k: ks) k.destroy();
278         }
279         options = (options == null) ? new KDCOptions() : options;
280         if (referralsState.isEnabled()) {
281             if (referralsState.sendCanonicalize()) {
282                 options.set(KDCOptions.CANONICALIZE, true);
283             }
284             extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP,
285                     new byte[]{}) };
286         } else {
287             options.set(KDCOptions.CANONICALIZE, false);
288         }
289         return new KrbAsReq(key,
290             options,
291             refCname,
292             sname,
293             from,
294             till,
295             rtime,
296             eTypes,
297             addresses,
298             extraPAs);
299     }
300 
301     /**
302      * Parses AS-REP, decrypts enc-part, retrieves ticket and session key
303      * @throws KrbException
304      * @throws Asn1Exception
305      * @throws IOException
306      */
resolve()307     private KrbAsReqBuilder resolve()
308             throws KrbException, Asn1Exception, IOException {
309         if (ktab != null) {
310             rep.decryptUsingKeyTab(ktab, req, cname);
311         } else {
312             rep.decryptUsingPassword(password, req, cname);
313         }
314         if (rep.getPA() != null) {
315             if (paList == null || paList.length == 0) {
316                 paList = rep.getPA();
317             } else {
318                 int extraLen = rep.getPA().length;
319                 if (extraLen > 0) {
320                     int oldLen = paList.length;
321                     paList = Arrays.copyOf(paList, paList.length + extraLen);
322                     System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen);
323                 }
324             }
325         }
326         return this;
327     }
328 
329     /**
330      * Communication until AS-REP or non preauth-related KRB-ERROR received
331      * @throws KrbException
332      * @throws IOException
333      */
send()334     private KrbAsReqBuilder send() throws KrbException, IOException {
335         boolean preAuthFailedOnce = false;
336         KdcComm comm = null;
337         EncryptionKey pakey = null;
338         ReferralsState referralsState = new ReferralsState(this);
339         while (true) {
340             if (referralsState.refreshComm()) {
341                 comm = new KdcComm(refCname.getRealmAsString());
342             }
343             try {
344                 req = build(pakey, referralsState);
345                 rep = new KrbAsRep(comm.send(req.encoding()));
346                 return this;
347             } catch (KrbException ke) {
348                 if (!preAuthFailedOnce && (
349                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED ||
350                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) {
351                     if (Krb5.DEBUG) {
352                         System.out.println("KrbAsReqBuilder: " +
353                                 "PREAUTH FAILED/REQ, re-send AS-REQ");
354                     }
355                     preAuthFailedOnce = true;
356                     KRBError kerr = ke.getError();
357                     int paEType = PAData.getPreferredEType(kerr.getPA(),
358                             EType.getDefaults("default_tkt_enctypes")[0]);
359                     if (password == null) {
360                         EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
361                         pakey = EncryptionKey.findKey(paEType, ks);
362                         if (pakey != null) pakey = (EncryptionKey)pakey.clone();
363                         for (EncryptionKey k: ks) k.destroy();
364                     } else {
365                         pakey = EncryptionKey.acquireSecretKey(cname,
366                                 password,
367                                 paEType,
368                                 PAData.getSaltAndParams(
369                                     paEType, kerr.getPA()));
370                     }
371                     paList = kerr.getPA();  // Update current paList
372                 } else {
373                     if (referralsState.handleError(ke)) {
374                         pakey = null;
375                         preAuthFailedOnce = false;
376                         continue;
377                     }
378                     throw ke;
379                 }
380             }
381         }
382     }
383 
384     static final class ReferralsState {
385         private static boolean canonicalizeConfig;
386         private boolean enabled;
387         private boolean sendCanonicalize;
388         private boolean isEnterpriseCname;
389         private int count;
390         private boolean refreshComm;
391         private KrbAsReqBuilder reqBuilder;
392 
393         static {
initStatic()394             initStatic();
395         }
396 
397         // Config may be refreshed while running so the setting
398         // value may need to be updated. See Config::refresh.
initStatic()399         static void initStatic() {
400             canonicalizeConfig = false;
401             try {
402                 canonicalizeConfig = Config.getInstance()
403                         .getBooleanObject("libdefaults", "canonicalize") ==
404                         Boolean.TRUE;
405             } catch (KrbException e) {
406                 if (Krb5.DEBUG) {
407                     System.out.println("Exception in getting canonicalize," +
408                             " using default value " +
409                             Boolean.valueOf(canonicalizeConfig) + ": " +
410                             e.getMessage());
411                 }
412             }
413         }
414 
ReferralsState(KrbAsReqBuilder reqBuilder)415         ReferralsState(KrbAsReqBuilder reqBuilder) throws KrbException {
416             this.reqBuilder = reqBuilder;
417             sendCanonicalize = canonicalizeConfig;
418             isEnterpriseCname = reqBuilder.refCname.getNameType() ==
419                     PrincipalName.KRB_NT_ENTERPRISE;
420             updateStatus();
421             if (!enabled && isEnterpriseCname) {
422                 throw new KrbException("NT-ENTERPRISE principals only" +
423                         " allowed when referrals are enabled.");
424             }
425             refreshComm = true;
426         }
427 
updateStatus()428         private void updateStatus() {
429             enabled = !Config.DISABLE_REFERRALS &&
430                     (isEnterpriseCname || sendCanonicalize);
431         }
432 
handleError(KrbException ke)433         boolean handleError(KrbException ke) throws RealmException {
434             if (enabled) {
435                 if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) {
436                     Realm referredRealm = ke.getError().getClientRealm();
437                     if (referredRealm != null &&
438                             !referredRealm.toString().isEmpty() &&
439                             count < Config.MAX_REFERRALS) {
440                         // A valid referral was received while referrals
441                         // were enabled. Change the cname realm to the referred
442                         // realm and set refreshComm to send a new request.
443                         reqBuilder.refCname = new PrincipalName(
444                                 reqBuilder.refCname.getNameType(),
445                                 reqBuilder.refCname.getNameStrings(),
446                                 referredRealm);
447                         refreshComm = true;
448                         count++;
449                         return true;
450                     }
451                 }
452                 if (count < Config.MAX_REFERRALS && sendCanonicalize) {
453                     if (Krb5.DEBUG) {
454                         System.out.println("KrbAsReqBuilder: AS-REQ failed." +
455                                 " Retrying with CANONICALIZE false.");
456                     }
457 
458                     // Server returned an unexpected error with
459                     // CANONICALIZE true. Retry with false.
460                     sendCanonicalize = false;
461 
462                     // Setting CANONICALIZE to false may imply that referrals
463                     // are now disabled (if cname is not of NT-ENTERPRISE type).
464                     updateStatus();
465 
466                     return true;
467                 }
468             }
469             return false;
470         }
471 
refreshComm()472         boolean refreshComm() {
473             boolean retRefreshComm = refreshComm;
474             refreshComm = false;
475             return retRefreshComm;
476         }
477 
isEnabled()478         boolean isEnabled() {
479             return enabled;
480         }
481 
sendCanonicalize()482         boolean sendCanonicalize() {
483             return sendCanonicalize;
484         }
485     }
486 
487     /**
488      * Performs AS-REQ send and AS-REP receive.
489      * Maybe a state is needed here, to divide prepare process and getCreds.
490      * @throws KrbException
491      * @throws Asn1Exception
492      * @throws IOException
493      */
action()494     public KrbAsReqBuilder action()
495             throws KrbException, Asn1Exception, IOException {
496         checkState(State.INIT, "Cannot call action");
497         state = State.REQ_OK;
498         return send().resolve();
499     }
500 
501     /**
502      * Gets Credentials object after action
503      */
getCreds()504     public Credentials getCreds() {
505         checkState(State.REQ_OK, "Cannot retrieve creds");
506         return rep.getCreds();
507     }
508 
509     /**
510      * Gets another type of Credentials after action
511      */
getCCreds()512     public sun.security.krb5.internal.ccache.Credentials getCCreds() {
513         checkState(State.REQ_OK, "Cannot retrieve CCreds");
514         return rep.getCCreds();
515     }
516 
517     /**
518      * Destroys the object and clears keys and password info.
519      */
destroy()520     public void destroy() {
521         state = State.DESTROYED;
522         if (password != null) {
523             Arrays.fill(password, (char)0);
524         }
525     }
526 
527     /**
528      * Checks if the current state is the specified one.
529      * @param st the expected state
530      * @param msg error message if state is not correct
531      * @throws IllegalStateException if state is not correct
532      */
checkState(State st, String msg)533     private void checkState(State st, String msg) {
534         if (state != st) {
535             throw new IllegalStateException(msg + " at " + st + " state");
536         }
537     }
538 }
539