1 /* |
|
2 * Copyright (c) 2015, 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.internal.ssl; |
|
27 |
|
28 import sun.security.ssl.ClientKeyExchange; |
|
29 import sun.security.ssl.Debug; |
|
30 import sun.security.ssl.ClientKeyExchangeService; |
|
31 import sun.security.ssl.HandshakeOutStream; |
|
32 |
|
33 import sun.security.jgss.GSSCaller; |
|
34 import sun.security.jgss.krb5.Krb5Util; |
|
35 import sun.security.jgss.krb5.ServiceCreds; |
|
36 import sun.security.krb5.EncryptedData; |
|
37 import sun.security.krb5.EncryptionKey; |
|
38 import sun.security.krb5.KrbException; |
|
39 import sun.security.krb5.PrincipalName; |
|
40 import sun.security.krb5.internal.EncTicketPart; |
|
41 import sun.security.krb5.internal.Ticket; |
|
42 import sun.security.krb5.internal.crypto.KeyUsage; |
|
43 import sun.security.ssl.ProtocolVersion; |
|
44 |
|
45 import javax.crypto.SecretKey; |
|
46 import javax.crypto.spec.SecretKeySpec; |
|
47 import javax.security.auth.Subject; |
|
48 import javax.security.auth.kerberos.KerberosKey; |
|
49 import javax.security.auth.kerberos.KerberosPrincipal; |
|
50 import javax.security.auth.kerberos.KerberosTicket; |
|
51 import javax.security.auth.kerberos.KeyTab; |
|
52 import javax.security.auth.kerberos.ServicePermission; |
|
53 import java.io.IOException; |
|
54 import java.io.PrintStream; |
|
55 import java.net.InetAddress; |
|
56 import java.security.AccessControlContext; |
|
57 import java.security.AccessController; |
|
58 import java.security.Principal; |
|
59 import java.security.PrivilegedAction; |
|
60 import java.security.PrivilegedActionException; |
|
61 import java.security.PrivilegedExceptionAction; |
|
62 import java.security.SecureRandom; |
|
63 import java.util.Set; |
|
64 |
|
65 /** |
|
66 * The provider for TLS_KRB_ cipher suites. |
|
67 * |
|
68 * @since 9 |
|
69 */ |
|
70 public class Krb5KeyExchangeService implements ClientKeyExchangeService { |
|
71 |
|
72 public static final Debug debug = Debug.getInstance("ssl"); |
|
73 |
|
74 @Override |
|
75 public String[] supported() { |
|
76 return new String[] { "KRB5", "KRB5_EXPORT" }; |
|
77 } |
|
78 |
|
79 @Override |
|
80 public Object getServiceCreds(AccessControlContext acc) { |
|
81 try { |
|
82 ServiceCreds serviceCreds = AccessController.doPrivileged( |
|
83 (PrivilegedExceptionAction<ServiceCreds>) |
|
84 () -> Krb5Util.getServiceCreds( |
|
85 GSSCaller.CALLER_SSL_SERVER, null, acc)); |
|
86 if (serviceCreds == null) { |
|
87 if (debug != null && Debug.isOn("handshake")) { |
|
88 System.out.println("Kerberos serviceCreds not available"); |
|
89 } |
|
90 return null; |
|
91 } |
|
92 if (debug != null && Debug.isOn("handshake")) { |
|
93 System.out.println("Using Kerberos creds"); |
|
94 } |
|
95 String serverPrincipal = serviceCreds.getName(); |
|
96 if (serverPrincipal != null) { |
|
97 // When service is bound, we check ASAP. Otherwise, |
|
98 // will check after client request is received |
|
99 // in in Kerberos ClientKeyExchange |
|
100 SecurityManager sm = System.getSecurityManager(); |
|
101 try { |
|
102 if (sm != null) { |
|
103 // Eliminate dependency on ServicePermission |
|
104 sm.checkPermission(new ServicePermission( |
|
105 serverPrincipal, "accept"), acc); |
|
106 } |
|
107 } catch (SecurityException se) { |
|
108 if (debug != null && Debug.isOn("handshake")) { |
|
109 System.out.println("Permission to access Kerberos" |
|
110 + " secret key denied"); |
|
111 } |
|
112 return null; |
|
113 } |
|
114 } |
|
115 return serviceCreds; |
|
116 } catch (PrivilegedActionException e) { |
|
117 // Likely exception here is LoginException |
|
118 if (debug != null && Debug.isOn("handshake")) { |
|
119 System.out.println("Attempt to obtain Kerberos key failed: " |
|
120 + e.toString()); |
|
121 } |
|
122 return null; |
|
123 } |
|
124 } |
|
125 |
|
126 @Override |
|
127 public String getServiceHostName(Principal principal) { |
|
128 if (principal == null) { |
|
129 return null; |
|
130 } |
|
131 String hostName = null; |
|
132 try { |
|
133 PrincipalName princName = |
|
134 new PrincipalName(principal.getName(), |
|
135 PrincipalName.KRB_NT_SRV_HST); |
|
136 String[] nameParts = princName.getNameStrings(); |
|
137 if (nameParts.length >= 2) { |
|
138 hostName = nameParts[1]; |
|
139 } |
|
140 } catch (Exception e) { |
|
141 // ignore |
|
142 } |
|
143 return hostName; |
|
144 } |
|
145 |
|
146 |
|
147 @Override |
|
148 public boolean isRelated(boolean isClient, |
|
149 AccessControlContext acc, Principal p) { |
|
150 |
|
151 if (p == null) return false; |
|
152 try { |
|
153 Subject subject = AccessController.doPrivileged( |
|
154 (PrivilegedExceptionAction<Subject>) |
|
155 () -> Krb5Util.getSubject( |
|
156 isClient ? GSSCaller.CALLER_SSL_CLIENT |
|
157 : GSSCaller.CALLER_SSL_SERVER, |
|
158 acc)); |
|
159 if (subject == null) { |
|
160 if (debug != null && Debug.isOn("session")) { |
|
161 System.out.println("Kerberos credentials are" + |
|
162 " not present in the current Subject;" + |
|
163 " check if " + |
|
164 " javax.security.auth.useSubjectAsCreds" + |
|
165 " system property has been set to false"); |
|
166 } |
|
167 return false; |
|
168 } |
|
169 Set<Principal> principals = |
|
170 subject.getPrincipals(Principal.class); |
|
171 if (principals.contains(p)) { |
|
172 // bound to this principal |
|
173 return true; |
|
174 } else { |
|
175 if (isClient) { |
|
176 return false; |
|
177 } else { |
|
178 for (KeyTab pc : subject.getPrivateCredentials(KeyTab.class)) { |
|
179 if (!pc.isBound()) { |
|
180 return true; |
|
181 } |
|
182 } |
|
183 return false; |
|
184 } |
|
185 } |
|
186 } catch (PrivilegedActionException pae) { |
|
187 if (debug != null && Debug.isOn("session")) { |
|
188 System.out.println("Attempt to obtain" + |
|
189 " subject failed! " + pae); |
|
190 } |
|
191 return false; |
|
192 } |
|
193 |
|
194 } |
|
195 |
|
196 public ClientKeyExchange createClientExchange( |
|
197 String serverName, AccessControlContext acc, |
|
198 ProtocolVersion protocolVerson, SecureRandom rand) throws IOException { |
|
199 return new ExchangerImpl(serverName, acc, protocolVerson, rand); |
|
200 } |
|
201 |
|
202 public ClientKeyExchange createServerExchange( |
|
203 ProtocolVersion protocolVersion, ProtocolVersion clientVersion, |
|
204 SecureRandom rand, byte[] encodedTicket, byte[] encrypted, |
|
205 AccessControlContext acc, Object serviceCreds) throws IOException { |
|
206 return new ExchangerImpl(protocolVersion, clientVersion, rand, |
|
207 encodedTicket, encrypted, acc, serviceCreds); |
|
208 } |
|
209 |
|
210 static class ExchangerImpl extends ClientKeyExchange { |
|
211 |
|
212 final private KerberosPreMasterSecret preMaster; |
|
213 final private byte[] encodedTicket; |
|
214 final private KerberosPrincipal peerPrincipal; |
|
215 final private KerberosPrincipal localPrincipal; |
|
216 |
|
217 @Override |
|
218 public int messageLength() { |
|
219 return encodedTicket.length + preMaster.getEncrypted().length + 6; |
|
220 } |
|
221 |
|
222 @Override |
|
223 public void send(HandshakeOutStream s) throws IOException { |
|
224 s.putBytes16(encodedTicket); |
|
225 s.putBytes16(null); |
|
226 s.putBytes16(preMaster.getEncrypted()); |
|
227 } |
|
228 |
|
229 @Override |
|
230 public void print(PrintStream s) throws IOException { |
|
231 s.println("*** ClientKeyExchange, Kerberos"); |
|
232 |
|
233 if (debug != null && Debug.isOn("verbose")) { |
|
234 Debug.println(s, "Kerberos service ticket", encodedTicket); |
|
235 Debug.println(s, "Random Secret", preMaster.getUnencrypted()); |
|
236 Debug.println(s, "Encrypted random Secret", preMaster.getEncrypted()); |
|
237 } |
|
238 } |
|
239 |
|
240 ExchangerImpl(String serverName, AccessControlContext acc, |
|
241 ProtocolVersion protocolVersion, SecureRandom rand) throws IOException { |
|
242 |
|
243 // Get service ticket |
|
244 KerberosTicket ticket = getServiceTicket(serverName, acc); |
|
245 encodedTicket = ticket.getEncoded(); |
|
246 |
|
247 // Record the Kerberos principals |
|
248 peerPrincipal = ticket.getServer(); |
|
249 localPrincipal = ticket.getClient(); |
|
250 |
|
251 // Optional authenticator, encrypted using session key, |
|
252 // currently ignored |
|
253 |
|
254 // Generate premaster secret and encrypt it using session key |
|
255 EncryptionKey sessionKey = new EncryptionKey( |
|
256 ticket.getSessionKeyType(), |
|
257 ticket.getSessionKey().getEncoded()); |
|
258 |
|
259 preMaster = new KerberosPreMasterSecret(protocolVersion, |
|
260 rand, sessionKey); |
|
261 } |
|
262 |
|
263 ExchangerImpl( |
|
264 ProtocolVersion protocolVersion, ProtocolVersion clientVersion, SecureRandom rand, |
|
265 byte[] encodedTicket, byte[] encrypted, |
|
266 AccessControlContext acc, Object serviceCreds) throws IOException { |
|
267 |
|
268 // Read ticket |
|
269 this.encodedTicket = encodedTicket; |
|
270 |
|
271 if (debug != null && Debug.isOn("verbose")) { |
|
272 Debug.println(System.out, |
|
273 "encoded Kerberos service ticket", encodedTicket); |
|
274 } |
|
275 |
|
276 EncryptionKey sessionKey = null; |
|
277 KerberosPrincipal tmpPeer = null; |
|
278 KerberosPrincipal tmpLocal = null; |
|
279 |
|
280 try { |
|
281 Ticket t = new Ticket(encodedTicket); |
|
282 |
|
283 EncryptedData encPart = t.encPart; |
|
284 PrincipalName ticketSname = t.sname; |
|
285 |
|
286 final ServiceCreds creds = (ServiceCreds)serviceCreds; |
|
287 final KerberosPrincipal princ = |
|
288 new KerberosPrincipal(ticketSname.toString()); |
|
289 |
|
290 // For bound service, permission already checked at setup |
|
291 if (creds.getName() == null) { |
|
292 SecurityManager sm = System.getSecurityManager(); |
|
293 try { |
|
294 if (sm != null) { |
|
295 // Eliminate dependency on ServicePermission |
|
296 sm.checkPermission(new ServicePermission( |
|
297 ticketSname.toString(), "accept"), acc); |
|
298 } |
|
299 } catch (SecurityException se) { |
|
300 serviceCreds = null; |
|
301 // Do not destroy keys. Will affect Subject |
|
302 if (debug != null && Debug.isOn("handshake")) { |
|
303 System.out.println("Permission to access Kerberos" |
|
304 + " secret key denied"); |
|
305 se.printStackTrace(System.out); |
|
306 } |
|
307 throw new IOException("Kerberos service not allowedy"); |
|
308 } |
|
309 } |
|
310 KerberosKey[] serverKeys = AccessController.doPrivileged( |
|
311 new PrivilegedAction<KerberosKey[]>() { |
|
312 @Override |
|
313 public KerberosKey[] run() { |
|
314 return creds.getKKeys(princ); |
|
315 } |
|
316 }); |
|
317 if (serverKeys.length == 0) { |
|
318 throw new IOException("Found no key for " + princ + |
|
319 (creds.getName() == null ? "" : |
|
320 (", this keytab is for " + creds.getName() + " only"))); |
|
321 } |
|
322 |
|
323 /* |
|
324 * permission to access and use the secret key of the Kerberized |
|
325 * "host" service is done in ServerHandshaker.getKerberosKeys() |
|
326 * to ensure server has the permission to use the secret key |
|
327 * before promising the client |
|
328 */ |
|
329 |
|
330 // See if we have the right key to decrypt the ticket to get |
|
331 // the session key. |
|
332 int encPartKeyType = encPart.getEType(); |
|
333 Integer encPartKeyVersion = encPart.getKeyVersionNumber(); |
|
334 KerberosKey dkey = null; |
|
335 try { |
|
336 dkey = findKey(encPartKeyType, encPartKeyVersion, serverKeys); |
|
337 } catch (KrbException ke) { // a kvno mismatch |
|
338 throw new IOException( |
|
339 "Cannot find key matching version number", ke); |
|
340 } |
|
341 if (dkey == null) { |
|
342 // %%% Should print string repr of etype |
|
343 throw new IOException("Cannot find key of appropriate type" + |
|
344 " to decrypt ticket - need etype " + encPartKeyType); |
|
345 } |
|
346 |
|
347 EncryptionKey secretKey = new EncryptionKey( |
|
348 encPartKeyType, |
|
349 dkey.getEncoded()); |
|
350 |
|
351 // Decrypt encPart using server's secret key |
|
352 byte[] bytes = encPart.decrypt(secretKey, KeyUsage.KU_TICKET); |
|
353 |
|
354 // Reset data stream after decryption, remove redundant bytes |
|
355 byte[] temp = encPart.reset(bytes); |
|
356 EncTicketPart encTicketPart = new EncTicketPart(temp); |
|
357 |
|
358 // Record the Kerberos Principals |
|
359 tmpPeer = new KerberosPrincipal(encTicketPart.cname.getName()); |
|
360 tmpLocal = new KerberosPrincipal(ticketSname.getName()); |
|
361 |
|
362 sessionKey = encTicketPart.key; |
|
363 |
|
364 if (debug != null && Debug.isOn("handshake")) { |
|
365 System.out.println("server principal: " + ticketSname); |
|
366 System.out.println("cname: " + encTicketPart.cname.toString()); |
|
367 } |
|
368 } catch (IOException e) { |
|
369 throw e; |
|
370 } catch (Exception e) { |
|
371 if (debug != null && Debug.isOn("handshake")) { |
|
372 System.out.println("KerberosWrapper error getting session key," |
|
373 + " generating random secret (" + e.getMessage() + ")"); |
|
374 } |
|
375 sessionKey = null; |
|
376 } |
|
377 |
|
378 //input.getBytes16(); // XXX Read and ignore authenticator |
|
379 |
|
380 if (sessionKey != null) { |
|
381 preMaster = new KerberosPreMasterSecret(protocolVersion, |
|
382 clientVersion, rand, encrypted, sessionKey); |
|
383 } else { |
|
384 // Generate bogus premaster secret |
|
385 preMaster = new KerberosPreMasterSecret(clientVersion, rand); |
|
386 } |
|
387 |
|
388 peerPrincipal = tmpPeer; |
|
389 localPrincipal = tmpLocal; |
|
390 } |
|
391 |
|
392 // Similar to sun.security.jgss.krb5.Krb5InitCredenetial/Krb5Context |
|
393 private static KerberosTicket getServiceTicket(String serverName, |
|
394 final AccessControlContext acc) throws IOException { |
|
395 |
|
396 if ("localhost".equals(serverName) || |
|
397 "localhost.localdomain".equals(serverName)) { |
|
398 |
|
399 if (debug != null && Debug.isOn("handshake")) { |
|
400 System.out.println("Get the local hostname"); |
|
401 } |
|
402 String localHost = java.security.AccessController.doPrivileged( |
|
403 new java.security.PrivilegedAction<String>() { |
|
404 public String run() { |
|
405 try { |
|
406 return InetAddress.getLocalHost().getHostName(); |
|
407 } catch (java.net.UnknownHostException e) { |
|
408 if (debug != null && Debug.isOn("handshake")) { |
|
409 System.out.println("Warning," |
|
410 + " cannot get the local hostname: " |
|
411 + e.getMessage()); |
|
412 } |
|
413 return null; |
|
414 } |
|
415 } |
|
416 }); |
|
417 if (localHost != null) { |
|
418 serverName = localHost; |
|
419 } |
|
420 } |
|
421 |
|
422 // Resolve serverName (possibly in IP addr form) to Kerberos principal |
|
423 // name for service with hostname |
|
424 String serviceName = "host/" + serverName; |
|
425 PrincipalName principal; |
|
426 try { |
|
427 principal = new PrincipalName(serviceName, |
|
428 PrincipalName.KRB_NT_SRV_HST); |
|
429 } catch (SecurityException se) { |
|
430 throw se; |
|
431 } catch (Exception e) { |
|
432 IOException ioe = new IOException("Invalid service principal" + |
|
433 " name: " + serviceName); |
|
434 ioe.initCause(e); |
|
435 throw ioe; |
|
436 } |
|
437 String realm = principal.getRealmAsString(); |
|
438 |
|
439 final String serverPrincipal = principal.toString(); |
|
440 final String tgsPrincipal = "krbtgt/" + realm + "@" + realm; |
|
441 final String clientPrincipal = null; // use default |
|
442 |
|
443 |
|
444 // check permission to obtain a service ticket to initiate a |
|
445 // context with the "host" service |
|
446 SecurityManager sm = System.getSecurityManager(); |
|
447 if (sm != null) { |
|
448 sm.checkPermission(new ServicePermission(serverPrincipal, |
|
449 "initiate"), acc); |
|
450 } |
|
451 |
|
452 try { |
|
453 KerberosTicket ticket = AccessController.doPrivileged( |
|
454 new PrivilegedExceptionAction<KerberosTicket>() { |
|
455 public KerberosTicket run() throws Exception { |
|
456 return Krb5Util.getTicketFromSubjectAndTgs( |
|
457 GSSCaller.CALLER_SSL_CLIENT, |
|
458 clientPrincipal, serverPrincipal, |
|
459 tgsPrincipal, acc); |
|
460 }}); |
|
461 |
|
462 if (ticket == null) { |
|
463 throw new IOException("Failed to find any kerberos service" + |
|
464 " ticket for " + serverPrincipal); |
|
465 } |
|
466 return ticket; |
|
467 } catch (PrivilegedActionException e) { |
|
468 IOException ioe = new IOException( |
|
469 "Attempt to obtain kerberos service ticket for " + |
|
470 serverPrincipal + " failed!"); |
|
471 ioe.initCause(e); |
|
472 throw ioe; |
|
473 } |
|
474 } |
|
475 |
|
476 @Override |
|
477 public SecretKey clientKeyExchange() { |
|
478 byte[] secretBytes = preMaster.getUnencrypted(); |
|
479 return new SecretKeySpec(secretBytes, "TlsPremasterSecret"); |
|
480 } |
|
481 |
|
482 @Override |
|
483 public Principal getPeerPrincipal() { |
|
484 return peerPrincipal; |
|
485 } |
|
486 |
|
487 @Override |
|
488 public Principal getLocalPrincipal() { |
|
489 return localPrincipal; |
|
490 } |
|
491 |
|
492 /** |
|
493 * Determines if a kvno matches another kvno. Used in the method |
|
494 * findKey(etype, version, keys). Always returns true if either input |
|
495 * is null or zero, in case any side does not have kvno info available. |
|
496 * |
|
497 * Note: zero is included because N/A is not a legal value for kvno |
|
498 * in javax.security.auth.kerberos.KerberosKey. Therefore, the info |
|
499 * that the kvno is N/A might be lost when converting between |
|
500 * EncryptionKey and KerberosKey. |
|
501 */ |
|
502 private static boolean versionMatches(Integer v1, int v2) { |
|
503 if (v1 == null || v1 == 0 || v2 == 0) { |
|
504 return true; |
|
505 } |
|
506 return v1.equals(v2); |
|
507 } |
|
508 |
|
509 private static KerberosKey findKey(int etype, Integer version, |
|
510 KerberosKey[] keys) throws KrbException { |
|
511 int ktype; |
|
512 boolean etypeFound = false; |
|
513 |
|
514 // When no matched kvno is found, returns tke key of the same |
|
515 // etype with the highest kvno |
|
516 int kvno_found = 0; |
|
517 KerberosKey key_found = null; |
|
518 |
|
519 for (int i = 0; i < keys.length; i++) { |
|
520 ktype = keys[i].getKeyType(); |
|
521 if (etype == ktype) { |
|
522 int kv = keys[i].getVersionNumber(); |
|
523 etypeFound = true; |
|
524 if (versionMatches(version, kv)) { |
|
525 return keys[i]; |
|
526 } else if (kv > kvno_found) { |
|
527 key_found = keys[i]; |
|
528 kvno_found = kv; |
|
529 } |
|
530 } |
|
531 } |
|
532 // Key not found. |
|
533 // %%% kludge to allow DES keys to be used for diff etypes |
|
534 if ((etype == EncryptedData.ETYPE_DES_CBC_CRC || |
|
535 etype == EncryptedData.ETYPE_DES_CBC_MD5)) { |
|
536 for (int i = 0; i < keys.length; i++) { |
|
537 ktype = keys[i].getKeyType(); |
|
538 if (ktype == EncryptedData.ETYPE_DES_CBC_CRC || |
|
539 ktype == EncryptedData.ETYPE_DES_CBC_MD5) { |
|
540 int kv = keys[i].getVersionNumber(); |
|
541 etypeFound = true; |
|
542 if (versionMatches(version, kv)) { |
|
543 return new KerberosKey(keys[i].getPrincipal(), |
|
544 keys[i].getEncoded(), |
|
545 etype, |
|
546 kv); |
|
547 } else if (kv > kvno_found) { |
|
548 key_found = new KerberosKey(keys[i].getPrincipal(), |
|
549 keys[i].getEncoded(), |
|
550 etype, |
|
551 kv); |
|
552 kvno_found = kv; |
|
553 } |
|
554 } |
|
555 } |
|
556 } |
|
557 if (etypeFound) { |
|
558 return key_found; |
|
559 } |
|
560 return null; |
|
561 } |
|
562 } |
|
563 } |
|