1/*
2 * Copyright (c) 2000, 2013, 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
26package com.sun.security.sasl.gsskerb;
27
28import java.io.IOException;
29import java.util.Map;
30import java.util.logging.Level;
31import javax.security.sasl.*;
32
33// JAAS
34import javax.security.auth.callback.CallbackHandler;
35
36// JGSS
37import org.ietf.jgss.*;
38
39/**
40  * Implements the GSSAPI SASL client mechanism for Kerberos V5.
41  * (<A HREF="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222</A>,
42  * <a HREF="http://www.ietf.org/internet-drafts/draft-ietf-cat-sasl-gssapi-04.txt">draft-ietf-cat-sasl-gssapi-04.txt</a>).
43  * It uses the Java Bindings for GSSAPI
44  * (<A HREF="http://www.ietf.org/rfc/rfc2853.txt">RFC 2853</A>)
45  * for getting GSSAPI/Kerberos V5 support.
46  *
47  * The client/server interactions are:
48  * C0: bind (GSSAPI, initial response)
49  * S0: sasl-bind-in-progress, challenge 1 (output of accept_sec_context or [])
50  * C1: bind (GSSAPI, response 1 (output of init_sec_context or []))
51  * S1: sasl-bind-in-progress challenge 2 (security layer, server max recv size)
52  * C2: bind (GSSAPI, response 2 (security layer, client max recv size, authzid))
53  * S2: bind success response
54  *
55  * Expects the client's credentials to be supplied from the
56  * javax.security.sasl.credentials property or from the thread's Subject.
57  * Otherwise the underlying KRB5 mech will attempt to acquire Kerberos creds
58  * by logging into Kerberos (via default TextCallbackHandler).
59  * These creds will be used for exchange with server.
60  *
61  * Required callbacks: none.
62  *
63  * Environment properties that affect behavior of implementation:
64  *
65  * javax.security.sasl.qop
66  * - quality of protection; list of auth, auth-int, auth-conf; default is "auth"
67  * javax.security.sasl.maxbuf
68  * - max receive buffer size; default is 65536
69  * javax.security.sasl.sendmaxbuffer
70  * - max send buffer size; default is 65536; (min with server max recv size)
71  *
72  * javax.security.sasl.server.authentication
73  * - "true" means require mutual authentication; default is "false"
74  *
75  * javax.security.sasl.credentials
76  * - an {@link org.ietf.jgss.GSSCredential} used for delegated authentication.
77  *
78  * @author Rosanna Lee
79  */
80
81final class GssKrb5Client extends GssKrb5Base implements SaslClient {
82    // ---------------- Constants -----------------
83    private static final String MY_CLASS_NAME = GssKrb5Client.class.getName();
84
85    private boolean finalHandshake = false;
86    private boolean mutual = false;       // default false
87    private byte[] authzID;
88
89    /**
90     * Creates a SASL mechanism with client credentials that it needs
91     * to participate in GSS-API/Kerberos v5 authentication exchange
92     * with the server.
93     */
94    GssKrb5Client(String authzID, String protocol, String serverName,
95        Map<String, ?> props, CallbackHandler cbh) throws SaslException {
96
97        super(props, MY_CLASS_NAME);
98
99        String service = protocol + "@" + serverName;
100        logger.log(Level.FINE, "KRB5CLNT01:Requesting service name: {0}",
101            service);
102
103        try {
104            GSSManager mgr = GSSManager.getInstance();
105
106            // Create the name for the requested service entity for Krb5 mech
107            GSSName acceptorName = mgr.createName(service,
108                GSSName.NT_HOSTBASED_SERVICE, KRB5_OID);
109
110            // Parse properties to check for supplied credentials
111            GSSCredential credentials = null;
112            if (props != null) {
113                Object prop = props.get(Sasl.CREDENTIALS);
114                if (prop != null && prop instanceof GSSCredential) {
115                    credentials = (GSSCredential) prop;
116                    logger.log(Level.FINE,
117                        "KRB5CLNT01:Using the credentials supplied in " +
118                        "javax.security.sasl.credentials");
119                }
120            }
121
122            // Create a context using credentials for Krb5 mech
123            secCtx = mgr.createContext(acceptorName,
124                KRB5_OID,   /* mechanism */
125                credentials, /* credentials */
126                GSSContext.INDEFINITE_LIFETIME);
127
128            // Request credential delegation when credentials have been supplied
129            if (credentials != null) {
130                secCtx.requestCredDeleg(true);
131            }
132
133            // Parse properties  to set desired context options
134            if (props != null) {
135                // Mutual authentication
136                String prop = (String)props.get(Sasl.SERVER_AUTH);
137                if (prop != null) {
138                    mutual = "true".equalsIgnoreCase(prop);
139                }
140            }
141            secCtx.requestMutualAuth(mutual);
142
143            // Always specify potential need for integrity and confidentiality
144            // Decision will be made during final handshake
145            secCtx.requestConf(true);
146            secCtx.requestInteg(true);
147
148        } catch (GSSException e) {
149            throw new SaslException("Failure to initialize security context", e);
150        }
151
152        if (authzID != null && authzID.length() > 0) {
153            try {
154                this.authzID = authzID.getBytes("UTF8");
155            } catch (IOException e) {
156                throw new SaslException("Cannot encode authorization ID", e);
157            }
158        }
159    }
160
161    public boolean hasInitialResponse() {
162        return true;
163    }
164
165    /**
166     * Processes the challenge data.
167     *
168     * The server sends a challenge data using which the client must
169     * process using GSS_Init_sec_context.
170     * As per RFC 2222, when GSS_S_COMPLETE is returned, we do
171     * an extra handshake to determine the negotiated security protection
172     * and buffer sizes.
173     *
174     * @param challengeData A non-null byte array containing the
175     * challenge data from the server.
176     * @return A non-null byte array containing the response to be
177     * sent to the server.
178     */
179    public byte[] evaluateChallenge(byte[] challengeData) throws SaslException {
180        if (completed) {
181            throw new IllegalStateException(
182                "GSSAPI authentication already complete");
183        }
184
185        if (finalHandshake) {
186            return doFinalHandshake(challengeData);
187        } else {
188
189            // Security context not established yet; continue with init
190
191            try {
192                byte[] gssOutToken = secCtx.initSecContext(challengeData,
193                    0, challengeData.length);
194                if (logger.isLoggable(Level.FINER)) {
195                    traceOutput(MY_CLASS_NAME, "evaluteChallenge",
196                        "KRB5CLNT02:Challenge: [raw]", challengeData);
197                    traceOutput(MY_CLASS_NAME, "evaluateChallenge",
198                        "KRB5CLNT03:Response: [after initSecCtx]", gssOutToken);
199                }
200
201                if (secCtx.isEstablished()) {
202                    finalHandshake = true;
203                    if (gssOutToken == null) {
204                        // RFC 2222 7.2.1:  Client responds with no data
205                        return EMPTY;
206                    }
207                }
208
209                return gssOutToken;
210            } catch (GSSException e) {
211                throw new SaslException("GSS initiate failed", e);
212            }
213        }
214    }
215
216    private byte[] doFinalHandshake(byte[] challengeData) throws SaslException {
217        try {
218            // Security context already established. challengeData
219            // should contain security layers and server's maximum buffer size
220
221            if (logger.isLoggable(Level.FINER)) {
222                traceOutput(MY_CLASS_NAME, "doFinalHandshake",
223                    "KRB5CLNT04:Challenge [raw]:", challengeData);
224            }
225
226            if (challengeData.length == 0) {
227                // Received S0, should return []
228                return EMPTY;
229            }
230
231            // Received S1 (security layer, server max recv size)
232
233            byte[] gssOutToken = secCtx.unwrap(challengeData, 0,
234                challengeData.length, new MessageProp(0, false));
235
236            // First octet is a bit-mask specifying the protections
237            // supported by the server
238            if (logger.isLoggable(Level.FINE)) {
239                if (logger.isLoggable(Level.FINER)) {
240                    traceOutput(MY_CLASS_NAME, "doFinalHandshake",
241                        "KRB5CLNT05:Challenge [unwrapped]:", gssOutToken);
242                }
243                logger.log(Level.FINE, "KRB5CLNT06:Server protections: {0}",
244                    gssOutToken[0]);
245            }
246
247            // Client selects preferred protection
248            // qop is ordered list of qop values
249            byte selectedQop = findPreferredMask(gssOutToken[0], qop);
250            if (selectedQop == 0) {
251                throw new SaslException(
252                    "No common protection layer between client and server");
253            }
254
255            if ((selectedQop&PRIVACY_PROTECTION) != 0) {
256                privacy = true;
257                integrity = true;
258            } else if ((selectedQop&INTEGRITY_ONLY_PROTECTION) != 0) {
259                integrity = true;
260            }
261
262            // 2nd-4th octets specifies maximum buffer size expected by
263            // server (in network byte order)
264            int srvMaxBufSize = networkByteOrderToInt(gssOutToken, 1, 3);
265
266            // Determine the max send buffer size based on what the
267            // server is able to receive and our specified max
268            sendMaxBufSize = (sendMaxBufSize == 0) ? srvMaxBufSize :
269                Math.min(sendMaxBufSize, srvMaxBufSize);
270
271            // Update context to limit size of returned buffer
272            rawSendSize = secCtx.getWrapSizeLimit(JGSS_QOP, privacy,
273                sendMaxBufSize);
274
275            if (logger.isLoggable(Level.FINE)) {
276                logger.log(Level.FINE,
277"KRB5CLNT07:Client max recv size: {0}; server max recv size: {1}; rawSendSize: {2}",
278                    new Object[] {recvMaxBufSize,
279                                  srvMaxBufSize,
280                                  rawSendSize});
281            }
282
283            // Construct negotiated security layers and client's max
284            // receive buffer size and authzID
285            int len = 4;
286            if (authzID != null) {
287                len += authzID.length;
288            }
289
290            byte[] gssInToken = new byte[len];
291            gssInToken[0] = selectedQop;
292
293            if (logger.isLoggable(Level.FINE)) {
294                logger.log(Level.FINE,
295            "KRB5CLNT08:Selected protection: {0}; privacy: {1}; integrity: {2}",
296                    new Object[]{selectedQop,
297                                 Boolean.valueOf(privacy),
298                                 Boolean.valueOf(integrity)});
299            }
300
301            if (privacy || integrity) {
302                // Last paragraph of RFC 4752 3.1: size ... MUST be 0 if the
303                // client does not support any security layer
304                intToNetworkByteOrder(recvMaxBufSize, gssInToken, 1, 3);
305            }
306            if (authzID != null) {
307                // copy authorization id
308                System.arraycopy(authzID, 0, gssInToken, 4, authzID.length);
309                logger.log(Level.FINE, "KRB5CLNT09:Authzid: {0}", authzID);
310            }
311
312            if (logger.isLoggable(Level.FINER)) {
313                traceOutput(MY_CLASS_NAME, "doFinalHandshake",
314                    "KRB5CLNT10:Response [raw]", gssInToken);
315            }
316
317            gssOutToken = secCtx.wrap(gssInToken,
318                0, gssInToken.length,
319                new MessageProp(0 /* qop */, false /* privacy */));
320
321            if (logger.isLoggable(Level.FINER)) {
322                traceOutput(MY_CLASS_NAME, "doFinalHandshake",
323                    "KRB5CLNT11:Response [after wrap]", gssOutToken);
324            }
325
326            completed = true;  // server authenticated
327
328            return gssOutToken;
329        } catch (GSSException e) {
330            throw new SaslException("Final handshake failed", e);
331        }
332    }
333}
334