1 /* 2 * Copyright (c) 2008, 2018, 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. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import com.sun.security.auth.module.Krb5LoginModule; 25 26 import java.io.IOException; 27 import java.lang.reflect.InvocationTargetException; 28 import java.lang.reflect.Method; 29 import java.security.PrivilegedActionException; 30 import java.security.PrivilegedExceptionAction; 31 import java.util.Arrays; 32 import java.util.HashMap; 33 import java.util.Map; 34 import javax.security.auth.Subject; 35 import javax.security.auth.callback.Callback; 36 import javax.security.auth.callback.CallbackHandler; 37 import javax.security.auth.callback.NameCallback; 38 import javax.security.auth.callback.PasswordCallback; 39 import javax.security.auth.callback.UnsupportedCallbackException; 40 import javax.security.auth.kerberos.KerberosKey; 41 import javax.security.auth.kerberos.KerberosTicket; 42 import javax.security.auth.login.LoginContext; 43 import org.ietf.jgss.GSSContext; 44 import org.ietf.jgss.GSSCredential; 45 import org.ietf.jgss.GSSException; 46 import org.ietf.jgss.GSSManager; 47 import org.ietf.jgss.GSSName; 48 import org.ietf.jgss.MessageProp; 49 import org.ietf.jgss.Oid; 50 import sun.security.jgss.krb5.Krb5Util; 51 import sun.security.krb5.Credentials; 52 import sun.security.krb5.internal.ccache.CredentialsCache; 53 54 import java.io.ByteArrayInputStream; 55 import java.io.ByteArrayOutputStream; 56 import java.security.Principal; 57 import java.util.Set; 58 59 /** 60 * Context of a JGSS subject, encapsulating Subject and GSSContext. 61 * 62 * Three "constructors", which acquire the (private) credentials and fill 63 * it into the Subject: 64 * 65 * 1. static fromJAAS(): Creates a Context using a JAAS login config entry 66 * 2. static fromUserPass(): Creates a Context using a username and a password 67 * 3. delegated(): A new context which uses the delegated credentials from a 68 * previously established acceptor Context 69 * 70 * Two context initiators, which create the GSSContext object inside: 71 * 72 * 1. startAsClient() 73 * 2. startAsServer() 74 * 75 * Privileged action: 76 * doAs(): Performs an action in the name of the Subject 77 * 78 * Handshake process: 79 * static handShake(initiator, acceptor) 80 * 81 * A four-phase typical data communication which includes all four GSS 82 * actions (wrap, unwrap, getMic and veryfyMiC): 83 * static transmit(message, from, to) 84 */ 85 public class Context { 86 87 private Subject s; 88 private GSSContext x; 89 private String name; 90 private GSSCredential cred; // see static method delegated(). 91 92 static boolean usingStream = false; 93 Context()94 private Context() {} 95 96 /** 97 * Using the delegated credentials from a previous acceptor 98 */ delegated()99 public Context delegated() throws Exception { 100 Context out = new Context(); 101 out.s = s; 102 try { 103 out.cred = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() { 104 @Override 105 public GSSCredential run() throws Exception { 106 GSSCredential cred = x.getDelegCred(); 107 if (cred == null && x.getCredDelegState() || 108 cred != null && !x.getCredDelegState()) { 109 throw new Exception("getCredDelegState not match"); 110 } 111 return cred; 112 } 113 }); 114 } catch (PrivilegedActionException pae) { 115 throw pae.getException(); 116 } 117 out.name = name + " as " + out.cred.getName().toString(); 118 return out; 119 } 120 121 /** 122 * No JAAS login at all, can be used to test JGSS without JAAS 123 */ fromThinAir()124 public static Context fromThinAir() throws Exception { 125 Context out = new Context(); 126 out.s = new Subject(); 127 return out; 128 } 129 130 /** 131 * Logins with a JAAS login config entry name 132 */ fromJAAS(final String name)133 public static Context fromJAAS(final String name) throws Exception { 134 Context out = new Context(); 135 out.name = name; 136 LoginContext lc = new LoginContext(name); 137 lc.login(); 138 out.s = lc.getSubject(); 139 return out; 140 } 141 142 /** 143 * Logins with username/password as a new Subject 144 */ fromUserPass( String user, char[] pass, boolean storeKey)145 public static Context fromUserPass( 146 String user, char[] pass, boolean storeKey) throws Exception { 147 return fromUserPass(new Subject(), user, pass, storeKey); 148 } 149 150 /** 151 * Logins with username/password as an existing Subject. The 152 * same subject can be used multiple times to simulate multiple logins. 153 */ fromUserPass(Subject s, String user, char[] pass, boolean storeKey)154 public static Context fromUserPass(Subject s, 155 String user, char[] pass, boolean storeKey) throws Exception { 156 Context out = new Context(); 157 out.name = user; 158 out.s = s; 159 Krb5LoginModule krb5 = new Krb5LoginModule(); 160 Map<String, String> map = new HashMap<>(); 161 Map<String, Object> shared = new HashMap<>(); 162 163 if (storeKey) { 164 map.put("storeKey", "true"); 165 } 166 167 if (pass != null) { 168 krb5.initialize(out.s, new CallbackHandler() { 169 @Override 170 public void handle(Callback[] callbacks) 171 throws IOException, UnsupportedCallbackException { 172 for (Callback cb: callbacks) { 173 if (cb instanceof NameCallback) { 174 ((NameCallback)cb).setName(user); 175 } else if (cb instanceof PasswordCallback) { 176 ((PasswordCallback)cb).setPassword(pass); 177 } 178 } 179 } 180 }, shared, map); 181 } else { 182 map.put("doNotPrompt", "true"); 183 map.put("useTicketCache", "true"); 184 if (user != null) { 185 map.put("principal", user); 186 } 187 krb5.initialize(out.s, null, shared, map); 188 } 189 190 krb5.login(); 191 krb5.commit(); 192 193 return out; 194 } 195 196 /** 197 * Logins with username/keytab as an existing Subject. The 198 * same subject can be used multiple times to simulate multiple logins. 199 */ fromUserKtab( String user, String ktab, boolean storeKey)200 public static Context fromUserKtab( 201 String user, String ktab, boolean storeKey) throws Exception { 202 return fromUserKtab(new Subject(), user, ktab, storeKey); 203 } 204 205 /** 206 * Logins with username/keytab as a new subject, 207 */ fromUserKtab(Subject s, String user, String ktab, boolean storeKey)208 public static Context fromUserKtab(Subject s, 209 String user, String ktab, boolean storeKey) throws Exception { 210 Context out = new Context(); 211 out.name = user; 212 out.s = s; 213 Krb5LoginModule krb5 = new Krb5LoginModule(); 214 Map<String, String> map = new HashMap<>(); 215 216 map.put("isInitiator", "false"); 217 map.put("doNotPrompt", "true"); 218 map.put("useTicketCache", "false"); 219 map.put("useKeyTab", "true"); 220 map.put("keyTab", ktab); 221 map.put("principal", user); 222 if (storeKey) { 223 map.put("storeKey", "true"); 224 } 225 226 krb5.initialize(out.s, null, null, map); 227 krb5.login(); 228 krb5.commit(); 229 return out; 230 } 231 232 /** 233 * Starts as a client 234 * @param target communication peer 235 * @param mech GSS mech 236 * @throws java.lang.Exception 237 */ startAsClient(final String target, final Oid mech)238 public void startAsClient(final String target, final Oid mech) throws Exception { 239 doAs(new Action() { 240 @Override 241 public byte[] run(Context me, byte[] dummy) throws Exception { 242 GSSManager m = GSSManager.getInstance(); 243 me.x = m.createContext( 244 target.indexOf('@') < 0 ? 245 m.createName(target, null) : 246 m.createName(target, GSSName.NT_HOSTBASED_SERVICE), 247 mech, 248 cred, 249 GSSContext.DEFAULT_LIFETIME); 250 return null; 251 } 252 }, null); 253 } 254 255 /** 256 * Starts as a server 257 * @param mech GSS mech 258 * @throws java.lang.Exception 259 */ startAsServer(final Oid mech)260 public void startAsServer(final Oid mech) throws Exception { 261 startAsServer(null, mech, false); 262 } 263 startAsServer(final String name, final Oid mech)264 public void startAsServer(final String name, final Oid mech) throws Exception { 265 startAsServer(name, mech, false); 266 } 267 /** 268 * Starts as a server with the specified service name 269 * @param name the service name 270 * @param mech GSS mech 271 * @throws java.lang.Exception 272 */ startAsServer(final String name, final Oid mech, final boolean asInitiator)273 public void startAsServer(final String name, final Oid mech, final boolean asInitiator) throws Exception { 274 doAs(new Action() { 275 @Override 276 public byte[] run(Context me, byte[] dummy) throws Exception { 277 GSSManager m = GSSManager.getInstance(); 278 me.cred = m.createCredential( 279 name == null ? null : 280 (name.indexOf('@') < 0 ? 281 m.createName(name, null) : 282 m.createName(name, GSSName.NT_HOSTBASED_SERVICE)), 283 GSSCredential.INDEFINITE_LIFETIME, 284 mech, 285 asInitiator? 286 GSSCredential.INITIATE_AND_ACCEPT: 287 GSSCredential.ACCEPT_ONLY); 288 me.x = m.createContext(me.cred); 289 return null; 290 } 291 }, null); 292 } 293 294 /** 295 * Accesses the internal GSSContext object. Currently it's used for -- 296 * 297 * 1. calling requestXXX() before handshake 298 * 2. accessing source name 299 * 300 * Note: If the application needs to do any privileged call on this 301 * object, please use doAs(). Otherwise, it can be done directly. The 302 * methods listed above are all non-privileged calls. 303 * 304 * @return the GSSContext object 305 */ x()306 public GSSContext x() { 307 return x; 308 } 309 310 /** 311 * Accesses the internal subject. 312 * @return the subject 313 */ s()314 public Subject s() { 315 return s; 316 } 317 318 /** 319 * Returns the cred inside, if there is one 320 */ cred()321 public GSSCredential cred() { 322 return cred; 323 } 324 325 /** 326 * Disposes the GSSContext within 327 * @throws org.ietf.jgss.GSSException 328 */ dispose()329 public void dispose() throws GSSException { 330 x.dispose(); 331 } 332 333 /** 334 * Does something using the Subject inside 335 * @param action the action 336 * @param in the input byte 337 * @return the output byte 338 * @throws java.lang.Exception 339 */ doAs(final Action action, final byte[] in)340 public byte[] doAs(final Action action, final byte[] in) throws Exception { 341 try { 342 return Subject.doAs(s, new PrivilegedExceptionAction<byte[]>() { 343 344 @Override 345 public byte[] run() throws Exception { 346 return action.run(Context.this, in); 347 } 348 }); 349 } catch (PrivilegedActionException pae) { 350 throw pae.getException(); 351 } 352 } 353 354 /** 355 * Prints status of GSSContext and Subject 356 * @throws java.lang.Exception 357 */ 358 public void status() throws Exception { 359 System.out.println("STATUS OF " + name.toUpperCase()); 360 if (x != null) { 361 StringBuffer sb = new StringBuffer(); 362 if (x.getAnonymityState()) { 363 sb.append("anon, "); 364 } 365 if (x.getConfState()) { 366 sb.append("conf, "); 367 } 368 if (x.getCredDelegState()) { 369 sb.append("deleg, "); 370 } 371 if (x.getIntegState()) { 372 sb.append("integ, "); 373 } 374 if (x.getMutualAuthState()) { 375 sb.append("mutual, "); 376 } 377 if (x.getReplayDetState()) { 378 sb.append("rep det, "); 379 } 380 if (x.getSequenceDetState()) { 381 sb.append("seq det, "); 382 } 383 System.out.println(" Context status of " + name + ": " + sb.toString()); 384 if (x.isProtReady() || x.isEstablished()) { 385 System.out.println(" " + x.getSrcName() + " -> " + x.getTargName()); 386 } 387 } 388 xstatus(); 389 if (s != null) { 390 System.out.println("====== START SUBJECT CONTENT ====="); 391 for (Principal p : s.getPrincipals()) { 392 System.out.println(" Principal: " + p); 393 } 394 for (Object o : s.getPublicCredentials()) { 395 System.out.println(" " + o.getClass()); 396 System.out.println(" " + o); 397 } 398 System.out.println("====== Private Credentials Set ======"); 399 for (Object o : s.getPrivateCredentials()) { 400 System.out.println(" " + o.getClass()); 401 if (o instanceof KerberosTicket) { 402 KerberosTicket kt = (KerberosTicket) o; 403 System.out.println(" " + kt.getServer() + " for " + kt.getClient()); 404 } else if (o instanceof KerberosKey) { 405 KerberosKey kk = (KerberosKey) o; 406 System.out.print(" " + kk.getKeyType() + " " + kk.getVersionNumber() + " " + kk.getAlgorithm() + " "); 407 for (byte b : kk.getEncoded()) { 408 System.out.printf("%02X", b & 0xff); 409 } 410 System.out.println(); 411 } else if (o instanceof Map) { 412 Map map = (Map) o; 413 for (Object k : map.keySet()) { 414 System.out.println(" " + k + ": " + map.get(k)); 415 } 416 } else { 417 System.out.println(" " + o); 418 } 419 } 420 System.out.println("====== END SUBJECT CONTENT ====="); 421 } 422 } 423 424 public void xstatus() throws Exception { 425 System.out.println(" Extended context status:"); 426 if (x != null) { 427 try { 428 Class<?> clazz = Class.forName("com.sun.security.jgss.ExtendedGSSContext"); 429 if (clazz.isAssignableFrom(x.getClass())) { 430 if (clazz.getMethod("getDelegPolicyState").invoke(x) == Boolean.TRUE) { 431 System.out.println(" deleg policy"); 432 } 433 if (x.isEstablished()) { 434 Class<?> inqType = Class.forName("com.sun.security.jgss.InquireType"); 435 Method inqMethod = clazz.getMethod("inquireSecContext", inqType); 436 for (Object o : inqType.getEnumConstants()) { 437 System.out.println(" " + o + ":"); 438 try { 439 System.out.println(" " + inqMethod.invoke(x, o)); 440 } catch (Exception e) { 441 System.out.println(e.getCause()); 442 } 443 } 444 } 445 } 446 } catch (ClassNotFoundException cnfe) { 447 System.out.println(" -- ExtendedGSSContext not available"); 448 } 449 } 450 if (cred != null) { 451 try { 452 Class<?> clazz2 = Class.forName("com.sun.security.jgss.ExtendedGSSCredential"); 453 if (!clazz2.isAssignableFrom(cred.getClass())) { 454 throw new Exception("cred is not extended"); 455 } 456 } catch (ClassNotFoundException cnfe) { 457 System.out.println(" -- ExtendedGSSCredential not available"); 458 } 459 } 460 } 461 462 public byte[] wrap(byte[] t, final boolean privacy) 463 throws Exception { 464 return doAs(new Action() { 465 @Override 466 public byte[] run(Context me, byte[] input) throws Exception { 467 System.out.printf("wrap %s privacy from %s: ", privacy?"with":"without", me.name); 468 MessageProp p1 = new MessageProp(0, privacy); 469 byte[] out; 470 if (usingStream) { 471 ByteArrayOutputStream os = new ByteArrayOutputStream(); 472 me.x.wrap(new ByteArrayInputStream(input), os, p1); 473 out = os.toByteArray(); 474 } else { 475 out = me.x.wrap(input, 0, input.length, p1); 476 } 477 System.out.println(printProp(p1)); 478 if ((x.getConfState() && privacy) != p1.getPrivacy()) { 479 throw new Exception("unexpected privacy status"); 480 } 481 return out; 482 } 483 }, t); 484 } 485 486 public byte[] unwrap(byte[] t, final boolean privacyExpected) 487 throws Exception { 488 return doAs(new Action() { 489 @Override 490 public byte[] run(Context me, byte[] input) throws Exception { 491 System.out.printf("unwrap from %s", me.name); 492 MessageProp p1 = new MessageProp(0, true); 493 byte[] bytes; 494 if (usingStream) { 495 ByteArrayOutputStream os = new ByteArrayOutputStream(); 496 me.x.unwrap(new ByteArrayInputStream(input), os, p1); 497 bytes = os.toByteArray(); 498 } else { 499 bytes = me.x.unwrap(input, 0, input.length, p1); 500 } 501 System.out.println(printProp(p1)); 502 if (p1.getPrivacy() != privacyExpected) { 503 throw new Exception("Unexpected privacy: " + p1.getPrivacy()); 504 } 505 return bytes; 506 } 507 }, t); 508 } 509 510 public byte[] getMic(byte[] t) throws Exception { 511 return doAs(new Action() { 512 @Override 513 public byte[] run(Context me, byte[] input) throws Exception { 514 MessageProp p1 = new MessageProp(0, true); 515 byte[] bytes; 516 p1 = new MessageProp(0, true); 517 System.out.printf("getMic from %s: ", me.name); 518 if (usingStream) { 519 ByteArrayOutputStream os = new ByteArrayOutputStream(); 520 me.x.getMIC(new ByteArrayInputStream(input), os, p1); 521 bytes = os.toByteArray(); 522 } else { 523 bytes = me.x.getMIC(input, 0, input.length, p1); 524 } 525 System.out.println(printProp(p1)); 526 return bytes; 527 } 528 }, t); 529 } 530 531 public void verifyMic(byte[] t, final byte[] msg) throws Exception { 532 doAs(new Action() { 533 @Override 534 public byte[] run(Context me, byte[] input) throws Exception { 535 MessageProp p1 = new MessageProp(0, true); 536 System.out.printf("verifyMic from %s: ", me.name); 537 if (usingStream) { 538 me.x.verifyMIC(new ByteArrayInputStream(input), 539 new ByteArrayInputStream(msg), p1); 540 } else { 541 me.x.verifyMIC(input, 0, input.length, 542 msg, 0, msg.length, 543 p1); 544 } 545 System.out.println(printProp(p1)); 546 if (p1.isUnseqToken() || p1.isOldToken() 547 || p1.isDuplicateToken() || p1.isGapToken()) { 548 throw new Exception("Wrong sequence number detected"); 549 } 550 return null; 551 } 552 }, t); 553 } 554 555 /** 556 * Transmits a message from one Context to another. The sender wraps the 557 * message and sends it to the receiver. The receiver unwraps it, creates 558 * a MIC of the clear text and sends it back to the sender. The sender 559 * verifies the MIC against the message sent earlier. 560 * @param message the message 561 * @param s1 the sender 562 * @param s2 the receiver 563 * @throws java.lang.Exception If anything goes wrong 564 */ 565 static public void transmit(String message, final Context s1, 566 final Context s2) throws Exception { 567 transmit(message.getBytes(), s1, s2); 568 } 569 570 /** 571 * Transmits a message from one Context to another. The sender wraps the 572 * message and sends it to the receiver. The receiver unwraps it, creates 573 * a MIC of the clear text and sends it back to the sender. The sender 574 * verifies the MIC against the message sent earlier. 575 * @param messageBytes the message 576 * @param s1 the sender 577 * @param s2 the receiver 578 * @throws java.lang.Exception If anything goes wrong 579 */ 580 static public void transmit(byte[] messageBytes, final Context s1, 581 final Context s2) throws Exception { 582 System.out.printf("-------------------- TRANSMIT from %s to %s------------------------\n", 583 s1.name, s2.name); 584 byte[] wrapped = s1.wrap(messageBytes, true); 585 byte[] unwrapped = s2.unwrap(wrapped, s2.x.getConfState()); 586 if (!Arrays.equals(messageBytes, unwrapped)) { 587 throw new Exception("wrap/unwrap mismatch"); 588 } 589 byte[] mic = s2.getMic(unwrapped); 590 s1.verifyMic(mic, messageBytes); 591 } 592 593 /** 594 * Returns a string description of a MessageProp object 595 * @param prop the object 596 * @return the description 597 */ 598 static public String printProp(MessageProp prop) { 599 StringBuffer sb = new StringBuffer(); 600 sb.append("MessagePop: "); 601 sb.append("QOP="+ prop.getQOP() + ", "); 602 sb.append(prop.getPrivacy()?"privacy, ":""); 603 sb.append(prop.isDuplicateToken()?"dup, ":""); 604 sb.append(prop.isGapToken()?"gap, ":""); 605 sb.append(prop.isOldToken()?"old, ":""); 606 sb.append(prop.isUnseqToken()?"unseq, ":""); 607 if (prop.getMinorStatus() != 0) { 608 sb.append(prop.getMinorString()+ "(" + prop.getMinorStatus()+")"); 609 } 610 return sb.toString(); 611 } 612 613 public Context impersonate(final String someone) throws Exception { 614 try { 615 GSSCredential creds = Subject.doAs(s, new PrivilegedExceptionAction<GSSCredential>() { 616 @Override 617 public GSSCredential run() throws Exception { 618 GSSManager m = GSSManager.getInstance(); 619 GSSName other = m.createName(someone, GSSName.NT_USER_NAME); 620 if (Context.this.cred == null) { 621 Context.this.cred = m.createCredential(GSSCredential.INITIATE_ONLY); 622 } 623 return (GSSCredential) 624 Class.forName("com.sun.security.jgss.ExtendedGSSCredential") 625 .getMethod("impersonate", GSSName.class) 626 .invoke(Context.this.cred, other); 627 } 628 }); 629 Context out = new Context(); 630 out.s = s; 631 out.cred = creds; 632 out.name = name + " as " + out.cred.getName().toString(); 633 return out; 634 } catch (PrivilegedActionException pae) { 635 Exception e = pae.getException(); 636 if (e instanceof InvocationTargetException) { 637 throw (Exception)((InvocationTargetException) e).getTargetException(); 638 } else { 639 throw e; 640 } 641 } 642 } 643 644 public byte[] take(final byte[] in) throws Exception { 645 return doAs(new Action() { 646 @Override 647 public byte[] run(Context me, byte[] input) throws Exception { 648 if (me.x.isEstablished()) { 649 System.out.println(name + " side established"); 650 if (input != null) { 651 throw new Exception("Context established but " + 652 "still receive token at " + name); 653 } 654 return null; 655 } else { 656 if (me.x.isInitiator()) { 657 System.out.println(name + " call initSecContext"); 658 return me.x.initSecContext(input, 0, input.length); 659 } else { 660 System.out.println(name + " call acceptSecContext"); 661 return me.x.acceptSecContext(input, 0, input.length); 662 } 663 } 664 } 665 }, in); 666 } 667 668 /** 669 * Saves the tickets to a ccache file. 670 * 671 * @param file pathname of the ccache file 672 * @return true if created, false otherwise. 673 */ 674 public boolean ccache(String file) throws Exception { 675 Set<KerberosTicket> tickets 676 = s.getPrivateCredentials(KerberosTicket.class); 677 if (tickets != null && !tickets.isEmpty()) { 678 CredentialsCache cc = null; 679 for (KerberosTicket t : tickets) { 680 Credentials cred = Krb5Util.ticketToCreds(t); 681 if (cc == null) { 682 cc = CredentialsCache.create(cred.getClient(), file); 683 } 684 cc.update(cred.toCCacheCreds()); 685 } 686 if (cc != null) { 687 cc.save(); 688 return true; 689 } 690 } 691 return false; 692 } 693 694 /** 695 * Handshake (security context establishment process) between two Contexts 696 * @param c the initiator 697 * @param s the acceptor 698 * @throws java.lang.Exception 699 */ 700 static public void handshake(final Context c, final Context s) throws Exception { 701 byte[] t = new byte[0]; 702 while (true) { 703 if (t != null || !c.x.isEstablished()) t = c.take(t); 704 if (t != null || !s.x.isEstablished()) t = s.take(t); 705 if (c.x.isEstablished() && s.x.isEstablished()) break; 706 } 707 } 708 } 709