Path: blob/master/src/java.security.jgss/share/classes/sun/security/krb5/KrbAsReqBuilder.java
41159 views
/*1* Copyright (c) 2010, 2020, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package sun.security.krb5;2627import java.io.IOException;28import java.util.Arrays;29import javax.security.auth.kerberos.KeyTab;30import sun.security.jgss.krb5.Krb5Util;31import sun.security.krb5.internal.HostAddresses;32import sun.security.krb5.internal.KDCOptions;33import sun.security.krb5.internal.KRBError;34import sun.security.krb5.internal.KerberosTime;35import sun.security.krb5.internal.Krb5;36import sun.security.krb5.internal.PAData;37import sun.security.krb5.internal.crypto.EType;3839/**40* A manager class for AS-REQ communications.41*42* This class does:43* 1. Gather information to create AS-REQ44* 2. Create and send AS-REQ45* 3. Receive AS-REP and KRB-ERROR (-KRB_ERR_RESPONSE_TOO_BIG) and parse them46* 4. Emit credentials and secret keys (for JAAS storeKey=true with password)47*48* This class does not:49* 1. Deal with real communications (KdcComm does it, and TGS-REQ)50* a. Name of KDCs for a realm51* b. Server availability, timeout, UDP or TCP52* d. KRB_ERR_RESPONSE_TOO_BIG53* 2. Stores its own copy of password, this means:54* a. Do not change/wipe it before Builder finish55* b. Builder will not wipe it for you56*57* With this class:58* 1. KrbAsReq has only one constructor59* 2. Krb5LoginModule and Kinit call a single builder60* 3. Better handling of sensitive info61*62* @since 1.763*/6465public final class KrbAsReqBuilder {6667// Common data for AS-REQ fields68private KDCOptions options;69private PrincipalName cname;70private PrincipalName refCname; // May be changed by referrals71private PrincipalName sname;72private KerberosTime from;73private KerberosTime till;74private KerberosTime rtime;75private HostAddresses addresses;7677// Secret source: can't be changed once assigned, only one (of the two78// sources) can be set to non-null79private final char[] password;80private final KeyTab ktab;8182// Used to create a ENC-TIMESTAMP in the 2nd AS-REQ83private PAData[] paList; // PA-DATA from both KRB-ERROR and AS-REP.84// Used by getKeys() only.85// Only AS-REP should be enough per RFC,86// combined in case etypes are different.8788// The generated and received:89private KrbAsReq req;90private KrbAsRep rep;9192private static enum State {93INIT, // Initialized, can still add more initialization info94REQ_OK, // AS-REQ performed95DESTROYED, // Destroyed, not usable anymore96}97private State state;9899// Called by other constructors100private void init(PrincipalName cname)101throws KrbException {102this.cname = cname;103this.refCname = cname;104state = State.INIT;105}106107/**108* Creates a builder to be used by {@code cname} with existing keys.109*110* @param cname the client of the AS-REQ. Must not be null. Might have no111* realm, where default realm will be used. This realm will be the target112* realm for AS-REQ. I believe a client should only get initial TGT from113* its own realm.114* @param ktab must not be null. If empty, might be quite useless.115* This argument will neither be modified nor stored by the method.116* @throws KrbException117*/118public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)119throws KrbException {120init(cname);121this.ktab = ktab;122this.password = null;123}124125/**126* Creates a builder to be used by {@code cname} with a known password.127*128* @param cname the client of the AS-REQ. Must not be null. Might have no129* realm, where default realm will be used. This realm will be the target130* realm for AS-REQ. I believe a client should only get initial TGT from131* its own realm.132* @param pass must not be null. This argument will neither be modified133* nor stored by the method.134* @throws KrbException135*/136public KrbAsReqBuilder(PrincipalName cname, char[] pass)137throws KrbException {138init(cname);139this.password = pass.clone();140this.ktab = null;141}142143/**144* Retrieves an array of secret keys for the client. This is used when145* the client supplies password but need keys to act as an acceptor. For146* an initiator, it must be called after AS-REQ is performed (state is OK).147* For an acceptor, it can be called when this KrbAsReqBuilder object is148* constructed (state is INIT).149* @param isInitiator if the caller is an initiator150* @return generated keys from password. PA-DATA from server might be used.151* All "default_tkt_enctypes" keys will be generated, Never null.152* @throws IllegalStateException if not constructed from a password153* @throws KrbException154*/155public EncryptionKey[] getKeys(boolean isInitiator) throws KrbException {156checkState(isInitiator?State.REQ_OK:State.INIT, "Cannot get keys");157if (password != null) {158int[] eTypes = EType.getDefaults("default_tkt_enctypes");159EncryptionKey[] result = new EncryptionKey[eTypes.length];160161/*162* Returns an array of keys. Before KrbAsReqBuilder, all etypes163* use the same salt which is either the default one or a new salt164* coming from PA-DATA. After KrbAsReqBuilder, each etype uses its165* own new salt from PA-DATA. For an etype with no PA-DATA new salt166* at all, what salt should it use?167*168* Commonly, the stored keys are only to be used by an acceptor to169* decrypt service ticket in AP-REQ. Most impls only allow keys170* from a keytab on acceptor, but unfortunately (?) Java supports171* acceptor using password. In this case, if the service ticket is172* encrypted using an etype which we don't have PA-DATA new salt,173* using the default salt might be wrong (say, case-insensitive174* user name). Instead, we would use the new salt of another etype.175*/176177String salt = null; // the saved new salt178try {179for (int i=0; i<eTypes.length; i++) {180// First round, only calculate those have a PA entry181PAData.SaltAndParams snp =182PAData.getSaltAndParams(eTypes[i], paList);183if (snp != null) {184// Never uses a salt for rc4-hmac, it does not use185// a salt at all186if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC &&187snp.salt != null) {188salt = snp.salt;189}190result[i] = EncryptionKey.acquireSecretKey(cname,191password,192eTypes[i],193snp);194}195}196// No new salt from PA, maybe empty, maybe only rc4-hmac197if (salt == null) salt = cname.getSalt();198for (int i=0; i<eTypes.length; i++) {199// Second round, calculate those with no PA entry200if (result[i] == null) {201result[i] = EncryptionKey.acquireSecretKey(password,202salt,203eTypes[i],204null);205}206}207} catch (IOException ioe) {208KrbException ke = new KrbException(Krb5.ASN1_PARSE_ERROR);209ke.initCause(ioe);210throw ke;211}212return result;213} else {214throw new IllegalStateException("Required password not provided");215}216}217218/**219* Sets or clears options. If cleared, default options will be used220* at creation time.221* @param options222*/223public void setOptions(KDCOptions options) {224checkState(State.INIT, "Cannot specify options");225this.options = options;226}227228public void setTill(KerberosTime till) {229checkState(State.INIT, "Cannot specify till");230this.till = till;231}232233public void setRTime(KerberosTime rtime) {234checkState(State.INIT, "Cannot specify rtime");235this.rtime = rtime;236}237238/**239* Sets or clears target. If cleared, KrbAsReq might choose krbtgt240* for cname realm241* @param sname242*/243public void setTarget(PrincipalName sname) {244checkState(State.INIT, "Cannot specify target");245this.sname = sname;246}247248/**249* Adds or clears addresses. KrbAsReq might add some if empty250* field not allowed251* @param addresses252*/253public void setAddresses(HostAddresses addresses) {254checkState(State.INIT, "Cannot specify addresses");255this.addresses = addresses;256}257258/**259* Build a KrbAsReq object from all info fed above. Normally this method260* will be called twice: initial AS-REQ and second with pakey261* @param key null (initial AS-REQ) or pakey (with preauth)262* @return the KrbAsReq object263* @throws KrbException264* @throws IOException265*/266private KrbAsReq build(EncryptionKey key, ReferralsState referralsState)267throws KrbException, IOException {268PAData[] extraPAs = null;269int[] eTypes;270if (password != null) {271eTypes = EType.getDefaults("default_tkt_enctypes");272} else {273EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);274eTypes = EType.getDefaults("default_tkt_enctypes",275ks);276for (EncryptionKey k: ks) k.destroy();277}278options = (options == null) ? new KDCOptions() : options;279if (referralsState.isEnabled()) {280if (referralsState.sendCanonicalize()) {281options.set(KDCOptions.CANONICALIZE, true);282}283extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP,284new byte[]{}) };285} else {286options.set(KDCOptions.CANONICALIZE, false);287}288return new KrbAsReq(key,289options,290refCname,291sname,292from,293till,294rtime,295eTypes,296addresses,297extraPAs);298}299300/**301* Parses AS-REP, decrypts enc-part, retrieves ticket and session key302* @throws KrbException303* @throws Asn1Exception304* @throws IOException305*/306private KrbAsReqBuilder resolve()307throws KrbException, Asn1Exception, IOException {308if (ktab != null) {309rep.decryptUsingKeyTab(ktab, req, cname);310} else {311rep.decryptUsingPassword(password, req, cname);312}313if (rep.getPA() != null) {314if (paList == null || paList.length == 0) {315paList = rep.getPA();316} else {317int extraLen = rep.getPA().length;318if (extraLen > 0) {319int oldLen = paList.length;320paList = Arrays.copyOf(paList, paList.length + extraLen);321System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen);322}323}324}325return this;326}327328/**329* Communication until AS-REP or non preauth-related KRB-ERROR received330* @throws KrbException331* @throws IOException332*/333private KrbAsReqBuilder send() throws KrbException, IOException {334boolean preAuthFailedOnce = false;335KdcComm comm = null;336EncryptionKey pakey = null;337ReferralsState referralsState = new ReferralsState(this);338while (true) {339if (referralsState.refreshComm()) {340comm = new KdcComm(refCname.getRealmAsString());341}342try {343req = build(pakey, referralsState);344rep = new KrbAsRep(comm.send(req.encoding()));345return this;346} catch (KrbException ke) {347if (!preAuthFailedOnce && (348ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED ||349ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) {350if (Krb5.DEBUG) {351System.out.println("KrbAsReqBuilder: " +352"PREAUTH FAILED/REQ, re-send AS-REQ");353}354preAuthFailedOnce = true;355KRBError kerr = ke.getError();356int paEType = PAData.getPreferredEType(kerr.getPA(),357EType.getDefaults("default_tkt_enctypes")[0]);358if (password == null) {359EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);360pakey = EncryptionKey.findKey(paEType, ks);361if (pakey != null) pakey = (EncryptionKey)pakey.clone();362for (EncryptionKey k: ks) k.destroy();363} else {364pakey = EncryptionKey.acquireSecretKey(cname,365password,366paEType,367PAData.getSaltAndParams(368paEType, kerr.getPA()));369}370paList = kerr.getPA(); // Update current paList371} else {372if (referralsState.handleError(ke)) {373pakey = null;374preAuthFailedOnce = false;375continue;376}377throw ke;378}379}380}381}382383static final class ReferralsState {384private static boolean canonicalizeConfig;385private boolean enabled;386private boolean sendCanonicalize;387private boolean isEnterpriseCname;388private int count;389private boolean refreshComm;390private KrbAsReqBuilder reqBuilder;391392static {393initStatic();394}395396// Config may be refreshed while running so the setting397// value may need to be updated. See Config::refresh.398static void initStatic() {399canonicalizeConfig = false;400try {401canonicalizeConfig = Config.getInstance()402.getBooleanObject("libdefaults", "canonicalize") ==403Boolean.TRUE;404} catch (KrbException e) {405if (Krb5.DEBUG) {406System.out.println("Exception in getting canonicalize," +407" using default value " +408Boolean.valueOf(canonicalizeConfig) + ": " +409e.getMessage());410}411}412}413414ReferralsState(KrbAsReqBuilder reqBuilder) throws KrbException {415this.reqBuilder = reqBuilder;416sendCanonicalize = canonicalizeConfig;417isEnterpriseCname = reqBuilder.refCname.getNameType() ==418PrincipalName.KRB_NT_ENTERPRISE;419updateStatus();420if (!enabled && isEnterpriseCname) {421throw new KrbException("NT-ENTERPRISE principals only" +422" allowed when referrals are enabled.");423}424refreshComm = true;425}426427private void updateStatus() {428enabled = !Config.DISABLE_REFERRALS &&429(isEnterpriseCname || sendCanonicalize);430}431432boolean handleError(KrbException ke) throws RealmException {433if (enabled) {434if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) {435Realm referredRealm = ke.getError().getClientRealm();436if (referredRealm != null &&437!referredRealm.toString().isEmpty() &&438count < Config.MAX_REFERRALS) {439// A valid referral was received while referrals440// were enabled. Change the cname realm to the referred441// realm and set refreshComm to send a new request.442reqBuilder.refCname = new PrincipalName(443reqBuilder.refCname.getNameType(),444reqBuilder.refCname.getNameStrings(),445referredRealm);446refreshComm = true;447count++;448return true;449}450}451if (count < Config.MAX_REFERRALS && sendCanonicalize) {452if (Krb5.DEBUG) {453System.out.println("KrbAsReqBuilder: AS-REQ failed." +454" Retrying with CANONICALIZE false.");455}456457// Server returned an unexpected error with458// CANONICALIZE true. Retry with false.459sendCanonicalize = false;460461// Setting CANONICALIZE to false may imply that referrals462// are now disabled (if cname is not of NT-ENTERPRISE type).463updateStatus();464465return true;466}467}468return false;469}470471boolean refreshComm() {472boolean retRefreshComm = refreshComm;473refreshComm = false;474return retRefreshComm;475}476477boolean isEnabled() {478return enabled;479}480481boolean sendCanonicalize() {482return sendCanonicalize;483}484}485486/**487* Performs AS-REQ send and AS-REP receive.488* Maybe a state is needed here, to divide prepare process and getCreds.489* @throws KrbException490* @throws Asn1Exception491* @throws IOException492*/493public KrbAsReqBuilder action()494throws KrbException, Asn1Exception, IOException {495checkState(State.INIT, "Cannot call action");496state = State.REQ_OK;497return send().resolve();498}499500/**501* Gets Credentials object after action502*/503public Credentials getCreds() {504checkState(State.REQ_OK, "Cannot retrieve creds");505return rep.getCreds();506}507508/**509* Gets another type of Credentials after action510*/511public sun.security.krb5.internal.ccache.Credentials getCCreds() {512checkState(State.REQ_OK, "Cannot retrieve CCreds");513return rep.getCCreds();514}515516/**517* Destroys the object and clears keys and password info.518*/519public void destroy() {520state = State.DESTROYED;521if (password != null) {522Arrays.fill(password, (char)0);523}524}525526/**527* Checks if the current state is the specified one.528* @param st the expected state529* @param msg error message if state is not correct530* @throws IllegalStateException if state is not correct531*/532private void checkState(State st, String msg) {533if (state != st) {534throw new IllegalStateException(msg + " at " + st + " state");535}536}537}538539540