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