1 /* |
|
2 * Copyright (c) 2003, 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 |
|
26 package sun.security.ssl.krb5; |
|
27 |
|
28 import java.io.IOException; |
|
29 import java.io.PrintStream; |
|
30 import java.security.AccessController; |
|
31 import java.security.AccessControlContext; |
|
32 import java.security.PrivilegedExceptionAction; |
|
33 import java.security.PrivilegedActionException; |
|
34 import java.security.SecureRandom; |
|
35 import java.net.InetAddress; |
|
36 import java.security.PrivilegedAction; |
|
37 |
|
38 import javax.security.auth.kerberos.KerberosTicket; |
|
39 import javax.security.auth.kerberos.KerberosKey; |
|
40 import javax.security.auth.kerberos.KerberosPrincipal; |
|
41 import javax.security.auth.kerberos.ServicePermission; |
|
42 import sun.security.jgss.GSSCaller; |
|
43 |
|
44 import sun.security.krb5.EncryptionKey; |
|
45 import sun.security.krb5.EncryptedData; |
|
46 import sun.security.krb5.PrincipalName; |
|
47 import sun.security.krb5.internal.Ticket; |
|
48 import sun.security.krb5.internal.EncTicketPart; |
|
49 import sun.security.krb5.internal.crypto.KeyUsage; |
|
50 |
|
51 import sun.security.jgss.krb5.Krb5Util; |
|
52 import sun.security.jgss.krb5.ServiceCreds; |
|
53 import sun.security.krb5.KrbException; |
|
54 import sun.security.krb5.internal.Krb5; |
|
55 |
|
56 import sun.security.ssl.Debug; |
|
57 import sun.security.ssl.HandshakeInStream; |
|
58 import sun.security.ssl.HandshakeOutStream; |
|
59 import sun.security.ssl.Krb5Helper; |
|
60 import sun.security.ssl.ProtocolVersion; |
|
61 |
|
62 /** |
|
63 * This is Kerberos option in the client key exchange message |
|
64 * (CLIENT -> SERVER). It holds the Kerberos ticket and the encrypted |
|
65 * premaster secret encrypted with the session key sealed in the ticket. |
|
66 * From RFC 2712: |
|
67 * struct |
|
68 * { |
|
69 * opaque Ticket; |
|
70 * opaque authenticator; // optional |
|
71 * opaque EncryptedPreMasterSecret; // encrypted with the session key |
|
72 * // which is sealed in the ticket |
|
73 * } KerberosWrapper; |
|
74 * |
|
75 * |
|
76 * Ticket and authenticator are encrypted as per RFC 1510 (in ASN.1) |
|
77 * Encrypted pre-master secret has the same structure as it does for RSA |
|
78 * except for Kerberos, the encryption key is the session key instead of |
|
79 * the RSA public key. |
|
80 * |
|
81 * XXX authenticator currently ignored |
|
82 * |
|
83 */ |
|
84 public final class KerberosClientKeyExchangeImpl |
|
85 extends sun.security.ssl.KerberosClientKeyExchange { |
|
86 |
|
87 private KerberosPreMasterSecret preMaster; |
|
88 private byte[] encodedTicket; |
|
89 private KerberosPrincipal peerPrincipal; |
|
90 private KerberosPrincipal localPrincipal; |
|
91 |
|
92 public KerberosClientKeyExchangeImpl() { |
|
93 } |
|
94 |
|
95 /** |
|
96 * Creates an instance of KerberosClientKeyExchange consisting of the |
|
97 * Kerberos service ticket, authenticator and encrypted premaster secret. |
|
98 * Called by client handshaker. |
|
99 * |
|
100 * @param serverName name of server with which to do handshake; |
|
101 * this is used to get the Kerberos service ticket |
|
102 * @param protocolVersion Maximum version supported by client (i.e, |
|
103 * version it requested in client hello) |
|
104 * @param rand random number generator to use for generating pre-master |
|
105 * secret |
|
106 */ |
|
107 @Override |
|
108 public void init(String serverName, |
|
109 AccessControlContext acc, ProtocolVersion protocolVersion, |
|
110 SecureRandom rand) throws IOException { |
|
111 |
|
112 // Get service ticket |
|
113 KerberosTicket ticket = getServiceTicket(serverName, acc); |
|
114 encodedTicket = ticket.getEncoded(); |
|
115 |
|
116 // Record the Kerberos principals |
|
117 peerPrincipal = ticket.getServer(); |
|
118 localPrincipal = ticket.getClient(); |
|
119 |
|
120 // Optional authenticator, encrypted using session key, |
|
121 // currently ignored |
|
122 |
|
123 // Generate premaster secret and encrypt it using session key |
|
124 EncryptionKey sessionKey = new EncryptionKey( |
|
125 ticket.getSessionKeyType(), |
|
126 ticket.getSessionKey().getEncoded()); |
|
127 |
|
128 preMaster = new KerberosPreMasterSecret(protocolVersion, |
|
129 rand, sessionKey); |
|
130 } |
|
131 |
|
132 /** |
|
133 * Creates an instance of KerberosClientKeyExchange from its ASN.1 encoding. |
|
134 * Used by ServerHandshaker to verify and obtain premaster secret. |
|
135 * |
|
136 * @param protocolVersion current protocol version |
|
137 * @param clientVersion version requested by client in its ClientHello; |
|
138 * used by premaster secret version check |
|
139 * @param rand random number generator used for generating random |
|
140 * premaster secret if ticket and/or premaster verification fails |
|
141 * @param input inputstream from which to get ASN.1-encoded KerberosWrapper |
|
142 * @param acc the AccessControlContext of the handshaker |
|
143 * @param serviceCreds server's creds |
|
144 */ |
|
145 @Override |
|
146 public void init(ProtocolVersion protocolVersion, |
|
147 ProtocolVersion clientVersion, |
|
148 SecureRandom rand, HandshakeInStream input, AccessControlContext acc, Object serviceCreds) |
|
149 throws IOException { |
|
150 |
|
151 // Read ticket |
|
152 encodedTicket = input.getBytes16(); |
|
153 |
|
154 if (debug != null && Debug.isOn("verbose")) { |
|
155 Debug.println(System.out, |
|
156 "encoded Kerberos service ticket", encodedTicket); |
|
157 } |
|
158 |
|
159 EncryptionKey sessionKey = null; |
|
160 |
|
161 try { |
|
162 Ticket t = new Ticket(encodedTicket); |
|
163 |
|
164 EncryptedData encPart = t.encPart; |
|
165 PrincipalName ticketSname = t.sname; |
|
166 |
|
167 final ServiceCreds creds = (ServiceCreds)serviceCreds; |
|
168 final KerberosPrincipal princ = |
|
169 new KerberosPrincipal(ticketSname.toString()); |
|
170 |
|
171 // For bound service, permission already checked at setup |
|
172 if (creds.getName() == null) { |
|
173 SecurityManager sm = System.getSecurityManager(); |
|
174 try { |
|
175 if (sm != null) { |
|
176 // Eliminate dependency on ServicePermission |
|
177 sm.checkPermission(Krb5Helper.getServicePermission( |
|
178 ticketSname.toString(), "accept"), acc); |
|
179 } |
|
180 } catch (SecurityException se) { |
|
181 serviceCreds = null; |
|
182 // Do not destroy keys. Will affect Subject |
|
183 if (debug != null && Debug.isOn("handshake")) { |
|
184 System.out.println("Permission to access Kerberos" |
|
185 + " secret key denied"); |
|
186 } |
|
187 throw new IOException("Kerberos service not allowedy"); |
|
188 } |
|
189 } |
|
190 KerberosKey[] serverKeys = AccessController.doPrivileged( |
|
191 new PrivilegedAction<KerberosKey[]>() { |
|
192 @Override |
|
193 public KerberosKey[] run() { |
|
194 return creds.getKKeys(princ); |
|
195 } |
|
196 }); |
|
197 if (serverKeys.length == 0) { |
|
198 throw new IOException("Found no key for " + princ + |
|
199 (creds.getName() == null ? "" : |
|
200 (", this keytab is for " + creds.getName() + " only"))); |
|
201 } |
|
202 |
|
203 /* |
|
204 * permission to access and use the secret key of the Kerberized |
|
205 * "host" service is done in ServerHandshaker.getKerberosKeys() |
|
206 * to ensure server has the permission to use the secret key |
|
207 * before promising the client |
|
208 */ |
|
209 |
|
210 // See if we have the right key to decrypt the ticket to get |
|
211 // the session key. |
|
212 int encPartKeyType = encPart.getEType(); |
|
213 Integer encPartKeyVersion = encPart.getKeyVersionNumber(); |
|
214 KerberosKey dkey = null; |
|
215 try { |
|
216 dkey = findKey(encPartKeyType, encPartKeyVersion, serverKeys); |
|
217 } catch (KrbException ke) { // a kvno mismatch |
|
218 throw new IOException( |
|
219 "Cannot find key matching version number", ke); |
|
220 } |
|
221 if (dkey == null) { |
|
222 // %%% Should print string repr of etype |
|
223 throw new IOException("Cannot find key of appropriate type" + |
|
224 " to decrypt ticket - need etype " + encPartKeyType); |
|
225 } |
|
226 |
|
227 EncryptionKey secretKey = new EncryptionKey( |
|
228 encPartKeyType, |
|
229 dkey.getEncoded()); |
|
230 |
|
231 // Decrypt encPart using server's secret key |
|
232 byte[] bytes = encPart.decrypt(secretKey, KeyUsage.KU_TICKET); |
|
233 |
|
234 // Reset data stream after decryption, remove redundant bytes |
|
235 byte[] temp = encPart.reset(bytes); |
|
236 EncTicketPart encTicketPart = new EncTicketPart(temp); |
|
237 |
|
238 // Record the Kerberos Principals |
|
239 peerPrincipal = |
|
240 new KerberosPrincipal(encTicketPart.cname.getName()); |
|
241 localPrincipal = new KerberosPrincipal(ticketSname.getName()); |
|
242 |
|
243 sessionKey = encTicketPart.key; |
|
244 |
|
245 if (debug != null && Debug.isOn("handshake")) { |
|
246 System.out.println("server principal: " + ticketSname); |
|
247 System.out.println("cname: " + encTicketPart.cname.toString()); |
|
248 } |
|
249 } catch (IOException e) { |
|
250 throw e; |
|
251 } catch (Exception e) { |
|
252 if (debug != null && Debug.isOn("handshake")) { |
|
253 System.out.println("KerberosWrapper error getting session key," |
|
254 + " generating random secret (" + e.getMessage() + ")"); |
|
255 } |
|
256 sessionKey = null; |
|
257 } |
|
258 |
|
259 input.getBytes16(); // XXX Read and ignore authenticator |
|
260 |
|
261 if (sessionKey != null) { |
|
262 preMaster = new KerberosPreMasterSecret(protocolVersion, |
|
263 clientVersion, rand, input, sessionKey); |
|
264 } else { |
|
265 // Generate bogus premaster secret |
|
266 preMaster = new KerberosPreMasterSecret(clientVersion, rand); |
|
267 } |
|
268 } |
|
269 |
|
270 @Override |
|
271 public int messageLength() { |
|
272 return (6 + encodedTicket.length + preMaster.getEncrypted().length); |
|
273 } |
|
274 |
|
275 @Override |
|
276 public void send(HandshakeOutStream s) throws IOException { |
|
277 s.putBytes16(encodedTicket); |
|
278 s.putBytes16(null); // XXX no authenticator |
|
279 s.putBytes16(preMaster.getEncrypted()); |
|
280 } |
|
281 |
|
282 @Override |
|
283 public void print(PrintStream s) throws IOException { |
|
284 s.println("*** ClientKeyExchange, Kerberos"); |
|
285 |
|
286 if (debug != null && Debug.isOn("verbose")) { |
|
287 Debug.println(s, "Kerberos service ticket", encodedTicket); |
|
288 Debug.println(s, "Random Secret", preMaster.getUnencrypted()); |
|
289 Debug.println(s, "Encrypted random Secret", |
|
290 preMaster.getEncrypted()); |
|
291 } |
|
292 } |
|
293 |
|
294 // Similar to sun.security.jgss.krb5.Krb5InitCredenetial/Krb5Context |
|
295 private static KerberosTicket getServiceTicket(String serverName, |
|
296 final AccessControlContext acc) throws IOException { |
|
297 |
|
298 if ("localhost".equals(serverName) || |
|
299 "localhost.localdomain".equals(serverName)) { |
|
300 |
|
301 if (debug != null && Debug.isOn("handshake")) { |
|
302 System.out.println("Get the local hostname"); |
|
303 } |
|
304 String localHost = java.security.AccessController.doPrivileged( |
|
305 new java.security.PrivilegedAction<String>() { |
|
306 public String run() { |
|
307 try { |
|
308 return InetAddress.getLocalHost().getHostName(); |
|
309 } catch (java.net.UnknownHostException e) { |
|
310 if (debug != null && Debug.isOn("handshake")) { |
|
311 System.out.println("Warning," |
|
312 + " cannot get the local hostname: " |
|
313 + e.getMessage()); |
|
314 } |
|
315 return null; |
|
316 } |
|
317 } |
|
318 }); |
|
319 if (localHost != null) { |
|
320 serverName = localHost; |
|
321 } |
|
322 } |
|
323 |
|
324 // Resolve serverName (possibly in IP addr form) to Kerberos principal |
|
325 // name for service with hostname |
|
326 String serviceName = "host/" + serverName; |
|
327 PrincipalName principal; |
|
328 try { |
|
329 principal = new PrincipalName(serviceName, |
|
330 PrincipalName.KRB_NT_SRV_HST); |
|
331 } catch (SecurityException se) { |
|
332 throw se; |
|
333 } catch (Exception e) { |
|
334 IOException ioe = new IOException("Invalid service principal" + |
|
335 " name: " + serviceName); |
|
336 ioe.initCause(e); |
|
337 throw ioe; |
|
338 } |
|
339 String realm = principal.getRealmAsString(); |
|
340 |
|
341 final String serverPrincipal = principal.toString(); |
|
342 final String tgsPrincipal = "krbtgt/" + realm + "@" + realm; |
|
343 final String clientPrincipal = null; // use default |
|
344 |
|
345 |
|
346 // check permission to obtain a service ticket to initiate a |
|
347 // context with the "host" service |
|
348 SecurityManager sm = System.getSecurityManager(); |
|
349 if (sm != null) { |
|
350 sm.checkPermission(new ServicePermission(serverPrincipal, |
|
351 "initiate"), acc); |
|
352 } |
|
353 |
|
354 try { |
|
355 KerberosTicket ticket = AccessController.doPrivileged( |
|
356 new PrivilegedExceptionAction<KerberosTicket>() { |
|
357 public KerberosTicket run() throws Exception { |
|
358 return Krb5Util.getTicketFromSubjectAndTgs( |
|
359 GSSCaller.CALLER_SSL_CLIENT, |
|
360 clientPrincipal, serverPrincipal, |
|
361 tgsPrincipal, acc); |
|
362 }}); |
|
363 |
|
364 if (ticket == null) { |
|
365 throw new IOException("Failed to find any kerberos service" + |
|
366 " ticket for " + serverPrincipal); |
|
367 } |
|
368 return ticket; |
|
369 } catch (PrivilegedActionException e) { |
|
370 IOException ioe = new IOException( |
|
371 "Attempt to obtain kerberos service ticket for " + |
|
372 serverPrincipal + " failed!"); |
|
373 ioe.initCause(e); |
|
374 throw ioe; |
|
375 } |
|
376 } |
|
377 |
|
378 @Override |
|
379 public byte[] getUnencryptedPreMasterSecret() { |
|
380 return preMaster.getUnencrypted(); |
|
381 } |
|
382 |
|
383 @Override |
|
384 public KerberosPrincipal getPeerPrincipal() { |
|
385 return peerPrincipal; |
|
386 } |
|
387 |
|
388 @Override |
|
389 public KerberosPrincipal getLocalPrincipal() { |
|
390 return localPrincipal; |
|
391 } |
|
392 |
|
393 /** |
|
394 * Determines if a kvno matches another kvno. Used in the method |
|
395 * findKey(etype, version, keys). Always returns true if either input |
|
396 * is null or zero, in case any side does not have kvno info available. |
|
397 * |
|
398 * Note: zero is included because N/A is not a legal value for kvno |
|
399 * in javax.security.auth.kerberos.KerberosKey. Therefore, the info |
|
400 * that the kvno is N/A might be lost when converting between |
|
401 * EncryptionKey and KerberosKey. |
|
402 */ |
|
403 private static boolean versionMatches(Integer v1, int v2) { |
|
404 if (v1 == null || v1 == 0 || v2 == 0) { |
|
405 return true; |
|
406 } |
|
407 return v1.equals(v2); |
|
408 } |
|
409 |
|
410 private static KerberosKey findKey(int etype, Integer version, |
|
411 KerberosKey[] keys) throws KrbException { |
|
412 int ktype; |
|
413 boolean etypeFound = false; |
|
414 |
|
415 // When no matched kvno is found, returns tke key of the same |
|
416 // etype with the highest kvno |
|
417 int kvno_found = 0; |
|
418 KerberosKey key_found = null; |
|
419 |
|
420 for (int i = 0; i < keys.length; i++) { |
|
421 ktype = keys[i].getKeyType(); |
|
422 if (etype == ktype) { |
|
423 int kv = keys[i].getVersionNumber(); |
|
424 etypeFound = true; |
|
425 if (versionMatches(version, kv)) { |
|
426 return keys[i]; |
|
427 } else if (kv > kvno_found) { |
|
428 key_found = keys[i]; |
|
429 kvno_found = kv; |
|
430 } |
|
431 } |
|
432 } |
|
433 // Key not found. |
|
434 // %%% kludge to allow DES keys to be used for diff etypes |
|
435 if ((etype == EncryptedData.ETYPE_DES_CBC_CRC || |
|
436 etype == EncryptedData.ETYPE_DES_CBC_MD5)) { |
|
437 for (int i = 0; i < keys.length; i++) { |
|
438 ktype = keys[i].getKeyType(); |
|
439 if (ktype == EncryptedData.ETYPE_DES_CBC_CRC || |
|
440 ktype == EncryptedData.ETYPE_DES_CBC_MD5) { |
|
441 int kv = keys[i].getVersionNumber(); |
|
442 etypeFound = true; |
|
443 if (versionMatches(version, kv)) { |
|
444 return new KerberosKey(keys[i].getPrincipal(), |
|
445 keys[i].getEncoded(), |
|
446 etype, |
|
447 kv); |
|
448 } else if (kv > kvno_found) { |
|
449 key_found = new KerberosKey(keys[i].getPrincipal(), |
|
450 keys[i].getEncoded(), |
|
451 etype, |
|
452 kv); |
|
453 kvno_found = kv; |
|
454 } |
|
455 } |
|
456 } |
|
457 } |
|
458 if (etypeFound) { |
|
459 return key_found; |
|
460 } |
|
461 return null; |
|
462 } |
|
463 } |
|