1 /* 2 * Copyright (c) 2008, 2020, 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 java.lang.reflect.Constructor; 25 import java.lang.reflect.Field; 26 import java.lang.reflect.InvocationTargetException; 27 import java.net.*; 28 import java.io.*; 29 import java.lang.reflect.Method; 30 import java.nio.file.Files; 31 import java.nio.file.Paths; 32 import java.util.*; 33 import java.util.concurrent.*; 34 import java.util.stream.Collectors; 35 import java.util.stream.Stream; 36 37 import sun.net.spi.nameservice.NameService; 38 import sun.net.spi.nameservice.NameServiceDescriptor; 39 import sun.security.krb5.*; 40 import sun.security.krb5.internal.*; 41 import sun.security.krb5.internal.ccache.CredentialsCache; 42 import sun.security.krb5.internal.crypto.EType; 43 import sun.security.krb5.internal.crypto.KeyUsage; 44 import sun.security.krb5.internal.ktab.KeyTab; 45 import sun.security.util.DerInputStream; 46 import sun.security.util.DerOutputStream; 47 import sun.security.util.DerValue; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 /** 52 * A KDC server. 53 * 54 * Note: By setting the system property native.kdc.path to a native 55 * krb5 installation, this class starts a native KDC with the 56 * given realm and host. It can also add new principals and save keytabs. 57 * Other features might not be available. 58 * <p> 59 * Features: 60 * <ol> 61 * <li> Supports TCP and UDP 62 * <li> Supports AS-REQ and TGS-REQ 63 * <li> Principal db and other settings hard coded in application 64 * <li> Options, say, request preauth or not 65 * </ol> 66 * Side effects: 67 * <ol> 68 * <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a 69 * singleton and initialized according to Kerberos settings (krb5.conf and 70 * java.security.krb5.* system properties). This means once it's initialized 71 * it will not automatically notice any changes to these settings (or file 72 * changes of krb5.conf). The KDC class normally does not touch these 73 * settings (except for the <code>writeKtab()</code> method). However, to make 74 * sure nothing ever goes wrong, if you want to make any changes to these 75 * settings after calling a KDC method, call <code>Config.refresh()</code> to 76 * make sure your changes are reflected in the <code>Config</code> object. 77 * </ol> 78 * System properties recognized: 79 * <ul> 80 * <li>test.kdc.save.ccache 81 * </ul> 82 * Issues and TODOs: 83 * <ol> 84 * <li> Generates krb5.conf to be used on another machine, currently the kdc is 85 * always localhost 86 * <li> More options to KDC, say, error output, say, response nonce != 87 * request nonce 88 * </ol> 89 * Note: This program uses internal krb5 classes (including reflection to 90 * access private fields and methods). 91 * <p> 92 * Usages: 93 * <p> 94 * 1. Init and start the KDC: 95 * <pre> 96 * KDC kdc = KDC.create("REALM.NAME", port, isDaemon); 97 * KDC kdc = KDC.create("REALM.NAME"); 98 * </pre> 99 * Here, <code>port</code> is the UDP and TCP port number the KDC server 100 * listens on. If zero, a random port is chosen, which you can use getPort() 101 * later to retrieve the value. 102 * <p> 103 * If <code>isDaemon</code> is true, the KDC worker threads will be daemons. 104 * <p> 105 * The shortcut <code>KDC.create("REALM.NAME")</code> has port=0 and 106 * isDaemon=false, and is commonly used in an embedded KDC. 107 * <p> 108 * 2. Adding users: 109 * <pre> 110 * kdc.addPrincipal(String principal_name, char[] password); 111 * kdc.addPrincipalRandKey(String principal_name); 112 * </pre> 113 * A service principal's name should look like "host/f.q.d.n". The second form 114 * generates a random key. To expose this key, call <code>writeKtab()</code> to 115 * save the keys into a keytab file. 116 * <p> 117 * Note that you need to add the principal name krbtgt/REALM.NAME yourself. 118 * <p> 119 * Note that you can safely add a principal at any time after the KDC is 120 * started and before a user requests info on this principal. 121 * <p> 122 * 3. Other public methods: 123 * <ul> 124 * <li> <code>getPort</code>: Returns the port number the KDC uses 125 * <li> <code>getRealm</code>: Returns the realm name 126 * <li> <code>writeKtab</code>: Writes all principals' keys into a keytab file 127 * <li> <code>saveConfig</code>: Saves a krb5.conf file to access this KDC 128 * <li> <code>setOption</code>: Sets various options 129 * </ul> 130 * Read the javadoc for details. Lazy developer can use <code>OneKDC</code> 131 * directly. 132 */ 133 public class KDC { 134 135 public static final int DEFAULT_LIFETIME = 39600; 136 public static final int DEFAULT_RENEWTIME = 86400; 137 138 // What etypes the KDC supports. Comma-separated strings. Null for all. 139 // Please note native KDCs might use different names. 140 private static final String SUPPORTED_ETYPES 141 = System.getProperty("kdc.supported.enctypes"); 142 143 // The native KDC 144 private final NativeKdc nativeKdc; 145 146 // The native KDC process 147 private Process kdcProc = null; 148 149 // Under the hood. 150 151 // Principal db. principal -> pass. A case-insensitive TreeMap is used 152 // so that even if the client provides a name with different case, the KDC 153 // can still locate the principal and give back correct salt. 154 private TreeMap<String,char[]> passwords = new TreeMap<> 155 (String.CASE_INSENSITIVE_ORDER); 156 157 // Non default salts. Precisely, there should be different salts for 158 // different etypes, pretend they are the same at the moment. 159 private TreeMap<String,String> salts = new TreeMap<> 160 (String.CASE_INSENSITIVE_ORDER); 161 162 // Non default s2kparams for newer etypes. Precisely, there should be 163 // different s2kparams for different etypes, pretend they are the same 164 // at the moment. 165 private TreeMap<String,byte[]> s2kparamses = new TreeMap<> 166 (String.CASE_INSENSITIVE_ORDER); 167 168 // Alias for referrals. 169 private TreeMap<String,KDC> aliasReferrals = new TreeMap<> 170 (String.CASE_INSENSITIVE_ORDER); 171 172 // Alias for local resolution. 173 private TreeMap<String,PrincipalName> alias2Principals = new TreeMap<> 174 (String.CASE_INSENSITIVE_ORDER); 175 176 // Realm name 177 private String realm; 178 // KDC 179 private String kdc; 180 // Service port number 181 private int port; 182 // The request/response job queue 183 private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100); 184 // Options 185 private Map<Option,Object> options = new HashMap<>(); 186 // Realm-specific krb5.conf settings 187 private List<String> conf = new ArrayList<>(); 188 189 private Thread thread1, thread2, thread3; 190 private volatile boolean udpConsumerReady = false; 191 private volatile boolean tcpConsumerReady = false; 192 private volatile boolean dispatcherReady = false; 193 DatagramSocket u1 = null; 194 ServerSocket t1 = null; 195 196 public static enum KtabMode { APPEND, EXISTING }; 197 198 /** 199 * Option names, to be expanded forever. 200 */ 201 public static enum Option { 202 /** 203 * Whether pre-authentication is required. Default Boolean.TRUE 204 */ 205 PREAUTH_REQUIRED, 206 /** 207 * Only issue TGT in RC4 208 */ 209 ONLY_RC4_TGT, 210 /** 211 * Use RC4 as the first in preauth 212 */ 213 RC4_FIRST_PREAUTH, 214 /** 215 * Use only one preauth, so that some keys are not easy to generate 216 */ 217 ONLY_ONE_PREAUTH, 218 /** 219 * Set all name-type to a value in response 220 */ 221 RESP_NT, 222 /** 223 * Multiple ETYPE-INFO-ENTRY with same etype but different salt 224 */ 225 DUP_ETYPE, 226 /** 227 * What backend server can be delegated to 228 */ 229 OK_AS_DELEGATE, 230 /** 231 * Allow S4U2self, List<String> of middle servers. 232 * If not set, means KDC does not understand S4U2self at all, therefore 233 * would ignore any PA-FOR-USER request and send a ticket using the 234 * cname of teh requestor. If set, it returns FORWARDABLE tickets to 235 * a server with its name in the list 236 */ 237 ALLOW_S4U2SELF, 238 /** 239 * Allow S4U2proxy, Map<String,List<String>> of middle servers to 240 * backends. If not set or a backend not in a server's list, 241 * Krb5.KDC_ERR_POLICY will be send for S4U2proxy request. 242 */ 243 ALLOW_S4U2PROXY, 244 /** 245 * Sensitive accounts can never be delegated. 246 */ 247 SENSITIVE_ACCOUNTS, 248 /** 249 * If true, will check if TGS-REQ contains a non-null addresses field. 250 */ 251 CHECK_ADDRESSES, 252 }; 253 254 //static { 255 // System.setProperty("sun.net.spi.nameservice.provider.1", "ns,mock"); 256 //} 257 258 /** 259 * A standalone KDC server. 260 */ main(String[] args)261 public static void main(String[] args) throws Exception { 262 int port = args.length > 0 ? Integer.parseInt(args[0]) : 0; 263 KDC kdc = create("RABBIT.HOLE", "kdc.rabbit.hole", port, false); 264 kdc.addPrincipal("dummy", "bogus".toCharArray()); 265 kdc.addPrincipal("foo", "bar".toCharArray()); 266 kdc.addPrincipalRandKey("krbtgt/RABBIT.HOLE"); 267 kdc.addPrincipalRandKey("server/host.rabbit.hole"); 268 kdc.addPrincipalRandKey("backend/host.rabbit.hole"); 269 KDC.saveConfig("krb5.conf", kdc, "forwardable = true"); 270 } 271 272 /** 273 * Creates and starts a KDC running as a daemon on a random port. 274 * @param realm the realm name 275 * @return the running KDC instance 276 * @throws java.io.IOException for any socket creation error 277 */ create(String realm)278 public static KDC create(String realm) throws IOException { 279 return create(realm, "kdc." + realm.toLowerCase(), 0, true); 280 } 281 existing(String realm, String kdc, int port)282 public static KDC existing(String realm, String kdc, int port) { 283 KDC k = new KDC(realm, kdc); 284 k.port = port; 285 return k; 286 } 287 288 /** 289 * Creates and starts a KDC server. 290 * @param realm the realm name 291 * @param port the TCP and UDP port to listen to. A random port will to 292 * chosen if zero. 293 * @param asDaemon if true, KDC threads will be daemons. Otherwise, not. 294 * @return the running KDC instance 295 * @throws java.io.IOException for any socket creation error 296 */ create(String realm, String kdc, int port, boolean asDaemon)297 public static KDC create(String realm, String kdc, int port, 298 boolean asDaemon) throws IOException { 299 return new KDC(realm, kdc, port, asDaemon); 300 } 301 302 /** 303 * Sets an option 304 * @param key the option name 305 * @param value the value 306 */ setOption(Option key, Object value)307 public void setOption(Option key, Object value) { 308 if (value == null) { 309 options.remove(key); 310 } else { 311 options.put(key, value); 312 } 313 } 314 315 /** 316 * Writes or appends keys into a keytab. 317 * <p> 318 * Attention: This is the most basic one of a series of methods below on 319 * keytab creation or modification. All these methods reference krb5.conf 320 * settings. If you need to modify krb5.conf or switch to another krb5.conf 321 * later, please call <code>Config.refresh()</code> again. For example: 322 * <pre> 323 * kdc.writeKtab("/etc/kdc/ktab", true); // Config is initialized, 324 * System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf"); 325 * Config.refresh(); 326 * </pre> 327 * Inside this method there are 2 places krb5.conf is used: 328 * <ol> 329 * <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys 330 * <li> (Has workaround) Creating PrincipalName 331 * </ol> 332 * @param tab the keytab file name 333 * @param append true if append, otherwise, overwrite. 334 * @param names the names to write into, write all if names is empty 335 */ writeKtab(String tab, boolean append, String... names)336 public void writeKtab(String tab, boolean append, String... names) 337 throws IOException, KrbException { 338 KeyTab ktab = null; 339 if (nativeKdc == null) { 340 ktab = append ? KeyTab.getInstance(tab) : KeyTab.create(tab); 341 } 342 Iterable<String> entries = 343 (names.length != 0) ? Arrays.asList(names): passwords.keySet(); 344 for (String name : entries) { 345 if (name.indexOf('@') < 0) { 346 name = name + "@" + realm; 347 } 348 if (nativeKdc == null) { 349 char[] pass = passwords.get(name); 350 int kvno = 0; 351 if (Character.isDigit(pass[pass.length - 1])) { 352 kvno = pass[pass.length - 1] - '0'; 353 } 354 PrincipalName pn = new PrincipalName(name, 355 name.indexOf('/') < 0 ? 356 PrincipalName.KRB_NT_UNKNOWN : 357 PrincipalName.KRB_NT_SRV_HST); 358 ktab.addEntry(pn, 359 getSalt(pn), 360 pass, 361 kvno, 362 true); 363 } else { 364 nativeKdc.ktadd(name, tab); 365 } 366 } 367 if (nativeKdc == null) { 368 ktab.save(); 369 } 370 } 371 372 /** 373 * Writes all principals' keys from multiple KDCs into one keytab file. 374 * @throws java.io.IOException for any file output error 375 * @throws sun.security.krb5.KrbException for any realm and/or principal 376 * name error. 377 */ writeMultiKtab(String tab, KDC... kdcs)378 public static void writeMultiKtab(String tab, KDC... kdcs) 379 throws IOException, KrbException { 380 KeyTab.create(tab).save(); // Empty the old keytab 381 appendMultiKtab(tab, kdcs); 382 } 383 384 /** 385 * Appends all principals' keys from multiple KDCs to one keytab file. 386 */ appendMultiKtab(String tab, KDC... kdcs)387 public static void appendMultiKtab(String tab, KDC... kdcs) 388 throws IOException, KrbException { 389 for (KDC kdc: kdcs) { 390 kdc.writeKtab(tab, true); 391 } 392 } 393 394 /** 395 * Write a ktab for this KDC. 396 */ writeKtab(String tab)397 public void writeKtab(String tab) throws IOException, KrbException { 398 writeKtab(tab, false); 399 } 400 401 /** 402 * Appends keys in this KDC to a ktab. 403 */ appendKtab(String tab)404 public void appendKtab(String tab) throws IOException, KrbException { 405 writeKtab(tab, true); 406 } 407 408 /** 409 * Adds a new principal to this realm with a given password. 410 * @param user the principal's name. For a service principal, use the 411 * form of host/f.q.d.n 412 * @param pass the password for the principal 413 */ addPrincipal(String user, char[] pass)414 public void addPrincipal(String user, char[] pass) { 415 addPrincipal(user, pass, null, null); 416 } 417 418 /** 419 * Adds a new principal to this realm with a given password. 420 * @param user the principal's name. For a service principal, use the 421 * form of host/f.q.d.n 422 * @param pass the password for the principal 423 * @param salt the salt, or null if a default value will be used 424 * @param s2kparams the s2kparams, or null if a default value will be used 425 */ addPrincipal( String user, char[] pass, String salt, byte[] s2kparams)426 public void addPrincipal( 427 String user, char[] pass, String salt, byte[] s2kparams) { 428 if (user.indexOf('@') < 0) { 429 user = user + "@" + realm; 430 } 431 if (nativeKdc != null) { 432 if (!user.equals("krbtgt/" + realm)) { 433 nativeKdc.addPrincipal(user, new String(pass)); 434 } 435 passwords.put(user, new char[0]); 436 } else { 437 passwords.put(user, pass); 438 if (salt != null) { 439 salts.put(user, salt); 440 } 441 if (s2kparams != null) { 442 s2kparamses.put(user, s2kparams); 443 } 444 } 445 } 446 447 /** 448 * Adds a new principal to this realm with a random password 449 * @param user the principal's name. For a service principal, use the 450 * form of host/f.q.d.n 451 */ addPrincipalRandKey(String user)452 public void addPrincipalRandKey(String user) { 453 addPrincipal(user, randomPassword()); 454 } 455 456 /** 457 * Returns the name of this realm 458 * @return the name of this realm 459 */ getRealm()460 public String getRealm() { 461 return realm; 462 } 463 464 /** 465 * Returns the name of kdc 466 * @return the name of kdc 467 */ getKDC()468 public String getKDC() { 469 return kdc; 470 } 471 472 /** 473 * Add realm-specific krb5.conf setting 474 */ addConf(String s)475 public void addConf(String s) { 476 conf.add(s); 477 } 478 479 /** 480 * Writes a krb5.conf for one or more KDC that includes KDC locations for 481 * each realm and the default realm name. You can also add extra strings 482 * into the file. The method should be called like: 483 * <pre> 484 * KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...); 485 * </pre> 486 * Here you can provide one or more kdc# and zero or more line# arguments. 487 * The line# will be put after [libdefaults] and before [realms]. Therefore 488 * you can append new lines into [libdefaults] and/or create your new 489 * stanzas as well. Note that a newline character will be appended to 490 * each line# argument. 491 * <p> 492 * For example: 493 * <pre> 494 * KDC.saveConfig("krb5.conf", this); 495 * </pre> 496 * generates: 497 * <pre> 498 * [libdefaults] 499 * default_realm = REALM.NAME 500 * 501 * [realms] 502 * REALM.NAME = { 503 * kdc = host:port_number 504 * # realm-specific settings 505 * } 506 * </pre> 507 * 508 * Another example: 509 * <pre> 510 * KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "", 511 * "[domain_realm]", 512 * ".kdc1.com = KDC1.NAME"); 513 * </pre> 514 * generates: 515 * <pre> 516 * [libdefaults] 517 * default_realm = KDC1.NAME 518 * forwardable = true 519 * 520 * [domain_realm] 521 * .kdc1.com = KDC1.NAME 522 * 523 * [realms] 524 * KDC1.NAME = { 525 * kdc = host:port1 526 * } 527 * KDC2.NAME = { 528 * kdc = host:port2 529 * } 530 * </pre> 531 * @param file the name of the file to write into 532 * @param kdc the first (and default) KDC 533 * @param more more KDCs or extra lines (in their appearing order) to 534 * insert into the krb5.conf file. This method reads each argument's type 535 * to determine what it's for. This argument can be empty. 536 * @throws java.io.IOException for any file output error 537 */ saveConfig(String file, KDC kdc, Object... more)538 public static void saveConfig(String file, KDC kdc, Object... more) 539 throws IOException { 540 StringBuffer sb = new StringBuffer(); 541 sb.append("[libdefaults]\ndefault_realm = "); 542 sb.append(kdc.realm); 543 sb.append("\n"); 544 for (Object o : more) { 545 if (o instanceof String) { 546 sb.append(o); 547 sb.append("\n"); 548 } 549 } 550 sb.append("\n[realms]\n"); 551 sb.append(kdc.realmLine()); 552 for (Object o : more) { 553 if (o instanceof KDC) { 554 sb.append(((KDC) o).realmLine()); 555 } 556 } 557 Files.write(Paths.get(file), sb.toString().getBytes()); 558 } 559 560 /** 561 * Returns the service port of the KDC server. 562 * @return the KDC service port 563 */ getPort()564 public int getPort() { 565 return port; 566 } 567 568 /** 569 * Register an alias name to be referred to a different KDC for 570 * resolution, according to RFC 6806. 571 * @param alias Alias name (i.e. user@REALM.COM). 572 * @param referredKDC KDC to which the alias is referred for resolution. 573 */ registerAlias(String alias, KDC referredKDC)574 public void registerAlias(String alias, KDC referredKDC) { 575 aliasReferrals.remove(alias); 576 aliasReferrals.put(alias, referredKDC); 577 } 578 579 /** 580 * Register an alias to be resolved to a Principal Name locally, 581 * according to RFC 6806. 582 * @param alias Alias name (i.e. user@REALM.COM). 583 * @param user Principal Name to which the alias is resolved. 584 */ registerAlias(String alias, String user)585 public void registerAlias(String alias, String user) 586 throws RealmException { 587 alias2Principals.remove(alias); 588 alias2Principals.put(alias, new PrincipalName(user)); 589 } 590 591 // Private helper methods 592 593 /** 594 * Private constructor, cannot be called outside. 595 * @param realm 596 */ KDC(String realm, String kdc)597 private KDC(String realm, String kdc) { 598 this.realm = realm; 599 this.kdc = kdc; 600 this.nativeKdc = null; 601 } 602 603 /** 604 * A constructor that starts the KDC service also. 605 */ KDC(String realm, String kdc, int port, boolean asDaemon)606 protected KDC(String realm, String kdc, int port, boolean asDaemon) 607 throws IOException { 608 this.realm = realm; 609 this.kdc = kdc; 610 this.nativeKdc = NativeKdc.get(this); 611 startServer(port, asDaemon); 612 } 613 /** 614 * Generates a 32-char random password 615 * @return the password 616 */ randomPassword()617 private static char[] randomPassword() { 618 char[] pass = new char[32]; 619 Random r = new Random(); 620 for (int i=0; i<31; i++) 621 pass[i] = (char)('a' + r.nextInt(26)); 622 // The last char cannot be a number, otherwise, keyForUser() 623 // believes it's a sign of kvno 624 pass[31] = 'Z'; 625 return pass; 626 } 627 628 /** 629 * Generates a random key for the given encryption type. 630 * @param eType the encryption type 631 * @return the generated key 632 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 633 */ generateRandomKey(int eType)634 private static EncryptionKey generateRandomKey(int eType) 635 throws KrbException { 636 // Is 32 enough for AES256? I should have generated the keys directly 637 // but different cryptos have different rules on what keys are valid. 638 char[] pass = randomPassword(); 639 String algo; 640 switch (eType) { 641 case EncryptedData.ETYPE_DES_CBC_MD5: algo = "DES"; break; 642 case EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD: algo = "DESede"; break; 643 case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: algo = "AES128"; break; 644 case EncryptedData.ETYPE_ARCFOUR_HMAC: algo = "ArcFourHMAC"; break; 645 case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: algo = "AES256"; break; 646 default: algo = "DES"; break; 647 } 648 return new EncryptionKey(pass, "NOTHING", algo); // Silly 649 } 650 651 /** 652 * Returns the password for a given principal 653 * @param p principal 654 * @return the password 655 * @throws sun.security.krb5.KrbException when the principal is not inside 656 * the database. 657 */ getPassword(PrincipalName p, boolean server)658 private char[] getPassword(PrincipalName p, boolean server) 659 throws KrbException { 660 String pn = p.toString(); 661 if (p.getRealmString() == null) { 662 pn = pn + "@" + getRealm(); 663 } 664 char[] pass = passwords.get(pn); 665 if (pass == null) { 666 throw new KrbException(server? 667 Krb5.KDC_ERR_S_PRINCIPAL_UNKNOWN: 668 Krb5.KDC_ERR_C_PRINCIPAL_UNKNOWN, pn.toString()); 669 } 670 return pass; 671 } 672 673 /** 674 * Returns the salt string for the principal. 675 * @param p principal 676 * @return the salt 677 */ getSalt(PrincipalName p)678 protected String getSalt(PrincipalName p) { 679 String pn = p.toString(); 680 if (p.getRealmString() == null) { 681 pn = pn + "@" + getRealm(); 682 } 683 if (salts.containsKey(pn)) { 684 return salts.get(pn); 685 } 686 if (passwords.containsKey(pn)) { 687 try { 688 // Find the principal name with correct case. 689 p = new PrincipalName(passwords.ceilingEntry(pn).getKey()); 690 } catch (RealmException re) { 691 // Won't happen 692 } 693 } 694 String s = p.getRealmString(); 695 if (s == null) s = getRealm(); 696 for (String n: p.getNameStrings()) { 697 s += n; 698 } 699 return s; 700 } 701 702 /** 703 * Returns the s2kparams for the principal given the etype. 704 * @param p principal 705 * @param etype encryption type 706 * @return the s2kparams, might be null 707 */ getParams(PrincipalName p, int etype)708 protected byte[] getParams(PrincipalName p, int etype) { 709 switch (etype) { 710 case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: 711 case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: 712 String pn = p.toString(); 713 if (p.getRealmString() == null) { 714 pn = pn + "@" + getRealm(); 715 } 716 if (s2kparamses.containsKey(pn)) { 717 return s2kparamses.get(pn); 718 } 719 return new byte[] {0, 0, 0x10, 0}; 720 default: 721 return null; 722 } 723 } 724 725 /** 726 * Returns the key for a given principal of the given encryption type 727 * @param p the principal 728 * @param etype the encryption type 729 * @param server looking for a server principal? 730 * @return the key 731 * @throws sun.security.krb5.KrbException for unknown/unsupported etype 732 */ keyForUser(PrincipalName p, int etype, boolean server)733 EncryptionKey keyForUser(PrincipalName p, int etype, boolean server) 734 throws KrbException { 735 try { 736 // Do not call EncryptionKey.acquireSecretKeys(), otherwise 737 // the krb5.conf config file would be loaded. 738 Integer kvno = null; 739 // For service whose password ending with a number, use it as kvno. 740 // Kvno must be postive. 741 if (p.toString().indexOf('/') > 0) { 742 char[] pass = getPassword(p, server); 743 if (Character.isDigit(pass[pass.length-1])) { 744 kvno = pass[pass.length-1] - '0'; 745 } 746 } 747 return new EncryptionKey(EncryptionKeyDotStringToKey( 748 getPassword(p, server), getSalt(p), getParams(p, etype), etype), 749 etype, kvno); 750 } catch (KrbException ke) { 751 throw ke; 752 } catch (Exception e) { 753 throw new RuntimeException(e); // should not happen 754 } 755 } 756 757 /** 758 * Returns a KerberosTime. 759 * 760 * @param offset offset from NOW in seconds 761 */ timeAfter(int offset)762 private static KerberosTime timeAfter(int offset) { 763 return new KerberosTime(new Date().getTime() + offset * 1000L); 764 } 765 766 /** 767 * Processes an incoming request and generates a response. 768 * @param in the request 769 * @return the response 770 * @throws java.lang.Exception for various errors 771 */ processMessage(byte[] in)772 protected byte[] processMessage(byte[] in) throws Exception { 773 if ((in[0] & 0x1f) == Krb5.KRB_AS_REQ) 774 return processAsReq(in); 775 else 776 return processTgsReq(in); 777 } 778 779 /** 780 * Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR) 781 * @param in the request 782 * @return the response 783 * @throws java.lang.Exception for various errors 784 */ processTgsReq(byte[] in)785 protected byte[] processTgsReq(byte[] in) throws Exception { 786 TGSReq tgsReq = new TGSReq(in); 787 PrincipalName service = tgsReq.reqBody.sname; 788 if (options.containsKey(KDC.Option.RESP_NT)) { 789 service = new PrincipalName((int)options.get(KDC.Option.RESP_NT), 790 service.getNameStrings(), service.getRealm()); 791 } 792 try { 793 System.out.println(realm + "> " + tgsReq.reqBody.cname + 794 " sends TGS-REQ for " + 795 service + ", " + tgsReq.reqBody.kdcOptions); 796 KDCReqBody body = tgsReq.reqBody; 797 int[] eTypes = filterSupported(KDCReqBodyDotEType(body)); 798 if (eTypes.length == 0) { 799 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 800 } 801 int e2 = eTypes[0]; // etype for outgoing session key 802 int e3 = eTypes[0]; // etype for outgoing ticket 803 804 PAData[] pas = tgsReq.pAData; 805 806 Ticket tkt = null; 807 EncTicketPart etp = null; 808 809 PrincipalName cname = null; 810 boolean allowForwardable = true; 811 boolean isReferral = false; 812 if (body.kdcOptions.get(KDCOptions.CANONICALIZE)) { 813 System.out.println(realm + "> verifying referral for " + 814 body.sname.getNameString()); 815 KDC referral = aliasReferrals.get(body.sname.getNameString()); 816 if (referral != null) { 817 service = new PrincipalName( 818 PrincipalName.TGS_DEFAULT_SRV_NAME + 819 PrincipalName.NAME_COMPONENT_SEPARATOR_STR + 820 referral.getRealm(), PrincipalName.KRB_NT_SRV_INST, 821 this.getRealm()); 822 System.out.println(realm + "> referral to " + 823 referral.getRealm()); 824 isReferral = true; 825 } 826 } 827 828 if (pas == null || pas.length == 0) { 829 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 830 } else { 831 PrincipalName forUserCName = null; 832 for (PAData pa: pas) { 833 if (pa.getType() == Krb5.PA_TGS_REQ) { 834 APReq apReq = new APReq(pa.getValue()); 835 tkt = apReq.ticket; 836 int te = tkt.encPart.getEType(); 837 EncryptionKey kkey = keyForUser(tkt.sname, te, true); 838 byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET); 839 DerInputStream derIn = new DerInputStream(bb); 840 DerValue der = derIn.getDerValue(); 841 etp = new EncTicketPart(der.toByteArray()); 842 // Finally, cname will be overwritten by PA-FOR-USER 843 // if it exists. 844 cname = etp.cname; 845 System.out.println(realm + "> presenting a ticket of " 846 + etp.cname + " to " + tkt.sname); 847 } else if (pa.getType() == Krb5.PA_FOR_USER) { 848 if (options.containsKey(Option.ALLOW_S4U2SELF)) { 849 PAForUserEnc p4u = new PAForUserEnc( 850 new DerValue(pa.getValue()), null); 851 forUserCName = p4u.name; 852 System.out.println(realm + "> See PA_FOR_USER " 853 + " in the name of " + p4u.name); 854 } 855 } 856 } 857 if (forUserCName != null) { 858 List<String> names = (List<String>) 859 options.get(Option.ALLOW_S4U2SELF); 860 if (!names.contains(cname.toString())) { 861 // Mimic the normal KDC behavior. When a server is not 862 // allowed to send S4U2self, do not send an error. 863 // Instead, send a ticket which is useless later. 864 allowForwardable = false; 865 } 866 cname = forUserCName; 867 } 868 if (tkt == null) { 869 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); 870 } 871 } 872 873 // Session key for original ticket, TGT 874 EncryptionKey ckey = etp.key; 875 876 // Session key for session with the service 877 EncryptionKey key = generateRandomKey(e2); 878 879 // Check time, TODO 880 KerberosTime from = body.from; 881 KerberosTime till = body.till; 882 if (from == null || from.isZero()) { 883 from = timeAfter(0); 884 } 885 KerberosTime rtime = body.rtime; 886 if (till == null) { 887 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 888 } else if (till.isZero()) { 889 till = timeAfter(DEFAULT_LIFETIME); 890 } 891 if (rtime == null && body.kdcOptions.get(KDCOptions.RENEWABLE)) { 892 rtime = timeAfter(DEFAULT_RENEWTIME); 893 } 894 895 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 896 if (body.kdcOptions.get(KDCOptions.FORWARDABLE) 897 && allowForwardable) { 898 List<String> sensitives = (List<String>) 899 options.get(Option.SENSITIVE_ACCOUNTS); 900 if (sensitives != null && sensitives.contains(cname.toString())) { 901 // Cannot make FORWARDABLE 902 } else { 903 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 904 } 905 } 906 // We do not request for addresses for FORWARDED tickets 907 if (options.containsKey(Option.CHECK_ADDRESSES) 908 && body.kdcOptions.get(KDCOptions.FORWARDED) 909 && body.addresses != null) { 910 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 911 } 912 if (body.kdcOptions.get(KDCOptions.FORWARDED) || 913 etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) { 914 bFlags[Krb5.TKT_OPTS_FORWARDED] = true; 915 } 916 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 917 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 918 //renew = timeAfter(3600 * 24 * 7); 919 } 920 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 921 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 922 } 923 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 924 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 925 } 926 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 927 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 928 } 929 if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT)) { 930 if (!options.containsKey(Option.ALLOW_S4U2PROXY)) { 931 // Don't understand CNAME_IN_ADDL_TKT 932 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 933 } else { 934 Map<String,List<String>> map = (Map<String,List<String>>) 935 options.get(Option.ALLOW_S4U2PROXY); 936 Ticket second = KDCReqBodyDotFirstAdditionalTicket(body); 937 EncryptionKey key2 = keyForUser( 938 second.sname, second.encPart.getEType(), true); 939 byte[] bb = second.encPart.decrypt(key2, KeyUsage.KU_TICKET); 940 DerInputStream derIn = new DerInputStream(bb); 941 DerValue der = derIn.getDerValue(); 942 EncTicketPart tktEncPart = new EncTicketPart(der.toByteArray()); 943 if (!tktEncPart.flags.get(Krb5.TKT_OPTS_FORWARDABLE)) { 944 //throw new KrbException(Krb5.KDC_ERR_BADOPTION); 945 } 946 PrincipalName client = tktEncPart.cname; 947 System.out.println(realm + "> and an additional ticket of " 948 + client + " to " + second.sname); 949 if (map.containsKey(cname.toString())) { 950 if (map.get(cname.toString()).contains(service.toString())) { 951 System.out.println(realm + "> S4U2proxy OK"); 952 } else { 953 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 954 } 955 } else { 956 throw new KrbException(Krb5.KDC_ERR_BADOPTION); 957 } 958 cname = client; 959 } 960 } 961 962 String okAsDelegate = (String)options.get(Option.OK_AS_DELEGATE); 963 if (okAsDelegate != null && ( 964 okAsDelegate.isEmpty() || 965 okAsDelegate.contains(service.getNameString()))) { 966 bFlags[Krb5.TKT_OPTS_DELEGATE] = true; 967 } 968 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 969 970 KerberosTime renewTill = etp.renewTill; 971 if (renewTill != null && body.kdcOptions.get(KDCOptions.RENEW)) { 972 // till should never pass renewTill 973 if (till.greaterThan(renewTill)) { 974 till = renewTill; 975 } 976 if (System.getProperty("test.set.null.renew") != null) { 977 // Testing 8186576, see NullRenewUntil.java. 978 renewTill = null; 979 } 980 } 981 982 TicketFlags tFlags = new TicketFlags(bFlags); 983 EncTicketPart enc = new EncTicketPart( 984 tFlags, 985 key, 986 cname, 987 new TransitedEncoding(1, new byte[0]), // TODO 988 timeAfter(0), 989 from, 990 till, renewTill, 991 body.addresses != null ? body.addresses 992 : etp.caddr, 993 null); 994 EncryptionKey skey = keyForUser(service, e3, true); 995 if (skey == null) { 996 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 997 } 998 Ticket t = new Ticket( 999 System.getProperty("test.kdc.diff.sname") != null ? 1000 new PrincipalName("xx" + service.toString()) : 1001 service, 1002 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 1003 ); 1004 EncTGSRepPart enc_part = new EncTGSRepPart( 1005 key, 1006 new LastReq(new LastReqEntry[] { 1007 new LastReqEntry(0, timeAfter(-10)) 1008 }), 1009 body.getNonce(), // TODO: detect replay 1010 timeAfter(3600 * 24), 1011 // Next 5 and last MUST be same with ticket 1012 tFlags, 1013 timeAfter(0), 1014 from, 1015 till, renewTill, 1016 service, 1017 body.addresses, 1018 null 1019 ); 1020 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), 1021 KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY); 1022 TGSRep tgsRep = new TGSRep(null, 1023 cname, 1024 t, 1025 edata); 1026 System.out.println(" Return " + tgsRep.cname 1027 + " ticket for " + tgsRep.ticket.sname + ", flags " 1028 + tFlags); 1029 1030 DerOutputStream out = new DerOutputStream(); 1031 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 1032 true, (byte)Krb5.KRB_TGS_REP), tgsRep.asn1Encode()); 1033 return out.toByteArray(); 1034 } catch (KrbException ke) { 1035 ke.printStackTrace(System.out); 1036 KRBError kerr = ke.getError(); 1037 KDCReqBody body = tgsReq.reqBody; 1038 System.out.println(" Error " + ke.returnCode() 1039 + " " +ke.returnCodeMessage()); 1040 if (kerr == null) { 1041 kerr = new KRBError(null, null, null, 1042 timeAfter(0), 1043 0, 1044 ke.returnCode(), 1045 body.cname, 1046 service, 1047 KrbException.errorMessage(ke.returnCode()), 1048 null); 1049 } 1050 return kerr.asn1Encode(); 1051 } 1052 } 1053 1054 /** 1055 * Processes a AS_REQ and generates a AS_REP (or KRB_ERROR) 1056 * @param in the request 1057 * @return the response 1058 * @throws java.lang.Exception for various errors 1059 */ processAsReq(byte[] in)1060 protected byte[] processAsReq(byte[] in) throws Exception { 1061 ASReq asReq = new ASReq(in); 1062 byte[] asReqbytes = asReq.asn1Encode(); 1063 int[] eTypes = null; 1064 List<PAData> outPAs = new ArrayList<>(); 1065 1066 PrincipalName service = asReq.reqBody.sname; 1067 if (options.containsKey(KDC.Option.RESP_NT)) { 1068 service = new PrincipalName((int)options.get(KDC.Option.RESP_NT), 1069 service.getNameStrings(), 1070 Realm.getDefault()); 1071 } 1072 try { 1073 System.out.println(realm + "> " + asReq.reqBody.cname + 1074 " sends AS-REQ for " + 1075 service + ", " + asReq.reqBody.kdcOptions); 1076 1077 KDCReqBody body = asReq.reqBody; 1078 1079 eTypes = filterSupported(KDCReqBodyDotEType(body)); 1080 if (eTypes.length == 0) { 1081 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 1082 } 1083 int eType = eTypes[0]; 1084 1085 if (body.kdcOptions.get(KDCOptions.CANONICALIZE)) { 1086 PrincipalName principal = alias2Principals.get( 1087 body.cname.getNameString()); 1088 if (principal != null) { 1089 body.cname = principal; 1090 } else { 1091 KDC referral = aliasReferrals.get(body.cname.getNameString()); 1092 if (referral != null) { 1093 body.cname = new PrincipalName( 1094 PrincipalName.TGS_DEFAULT_SRV_NAME, 1095 PrincipalName.KRB_NT_SRV_INST, 1096 referral.getRealm()); 1097 throw new KrbException(Krb5.KRB_ERR_WRONG_REALM); 1098 } 1099 } 1100 } 1101 1102 EncryptionKey ckey = keyForUser(body.cname, eType, false); 1103 EncryptionKey skey = keyForUser(service, eType, true); 1104 1105 if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) { 1106 int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC; 1107 boolean found = false; 1108 for (int i=0; i<eTypes.length; i++) { 1109 if (eTypes[i] == tgtEType) { 1110 found = true; 1111 break; 1112 } 1113 } 1114 if (!found) { 1115 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 1116 } 1117 skey = keyForUser(service, tgtEType, true); 1118 } 1119 if (ckey == null) { 1120 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); 1121 } 1122 if (skey == null) { 1123 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO 1124 } 1125 1126 // Session key 1127 EncryptionKey key = generateRandomKey(eType); 1128 // Check time, TODO 1129 KerberosTime from = body.from; 1130 KerberosTime till = body.till; 1131 KerberosTime rtime = body.rtime; 1132 if (from == null || from.isZero()) { 1133 from = timeAfter(0); 1134 } 1135 if (till == null) { 1136 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO 1137 } else if (till.isZero()) { 1138 String ttlsVal = System.getProperty("test.kdc.ttl.value"); 1139 if (ttlsVal != null){ 1140 till = timeAfter(duration(ttlsVal)); 1141 if (till.greaterThan(timeAfter(24 * 3600)) && 1142 (System.getProperty("test.kdc.force.till") == null)) { 1143 till = timeAfter(DEFAULT_LIFETIME); 1144 body.kdcOptions.set(KDCOptions.RENEWABLE, true); 1145 } 1146 } else { 1147 till = timeAfter(DEFAULT_LIFETIME); 1148 } 1149 } 1150 1151 if (rtime == null && body.kdcOptions.get(KDCOptions.RENEWABLE)) { 1152 rtime = timeAfter(DEFAULT_RENEWTIME); 1153 } 1154 1155 //body.from 1156 boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; 1157 if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { 1158 List<String> sensitives = (List<String>) 1159 options.get(Option.SENSITIVE_ACCOUNTS); 1160 if (sensitives != null 1161 && sensitives.contains(body.cname.toString())) { 1162 // Cannot make FORWARDABLE 1163 } else { 1164 bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; 1165 } 1166 } 1167 if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { 1168 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; 1169 //renew = timeAfter(3600 * 24 * 7); 1170 } 1171 if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { 1172 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; 1173 } 1174 if (body.kdcOptions.get(KDCOptions.POSTDATED)) { 1175 bFlags[Krb5.TKT_OPTS_POSTDATED] = true; 1176 } 1177 if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { 1178 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; 1179 } 1180 bFlags[Krb5.TKT_OPTS_INITIAL] = true; 1181 if (System.getProperty("test.kdc.always.enc.pa.rep") != null) { 1182 bFlags[Krb5.TKT_OPTS_ENC_PA_REP] = true; 1183 } 1184 1185 // Creating PA-DATA 1186 DerValue[] pas2 = null, pas = null; 1187 if (options.containsKey(KDC.Option.DUP_ETYPE)) { 1188 int n = (Integer)options.get(KDC.Option.DUP_ETYPE); 1189 switch (n) { 1190 case 1: // customer's case in 7067974 1191 pas2 = new DerValue[] { 1192 new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), 1193 new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), 1194 new DerValue(new ETypeInfo2( 1195 1, realm, new byte[]{1}).asn1Encode()), 1196 }; 1197 pas = new DerValue[] { 1198 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1199 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1200 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1201 }; 1202 break; 1203 case 2: // we still reject non-null s2kparams and prefer E2 over E 1204 pas2 = new DerValue[] { 1205 new DerValue(new ETypeInfo2( 1206 1, realm, new byte[]{1}).asn1Encode()), 1207 new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), 1208 new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), 1209 }; 1210 pas = new DerValue[] { 1211 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1212 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1213 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1214 }; 1215 break; 1216 case 3: // but only E is wrong 1217 pas = new DerValue[] { 1218 new DerValue(new ETypeInfo(1, realm).asn1Encode()), 1219 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1220 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1221 }; 1222 break; 1223 case 4: // we also ignore rc4-hmac 1224 pas = new DerValue[] { 1225 new DerValue(new ETypeInfo(23, "ANYTHING").asn1Encode()), 1226 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1227 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1228 }; 1229 break; 1230 case 5: // "" should be wrong, but we accept it now 1231 // See s.s.k.internal.PAData$SaltAndParams 1232 pas = new DerValue[] { 1233 new DerValue(new ETypeInfo(1, "").asn1Encode()), 1234 new DerValue(new ETypeInfo(1, null).asn1Encode()), 1235 }; 1236 break; 1237 } 1238 } else { 1239 int[] epas = eTypes; 1240 if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) { 1241 for (int i=1; i<epas.length; i++) { 1242 if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) { 1243 epas[i] = epas[0]; 1244 epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC; 1245 break; 1246 } 1247 }; 1248 } else if (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) { 1249 epas = new int[] { eTypes[0] }; 1250 } 1251 pas2 = new DerValue[epas.length]; 1252 for (int i=0; i<epas.length; i++) { 1253 pas2[i] = new DerValue(new ETypeInfo2( 1254 epas[i], 1255 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 1256 null : getSalt(body.cname), 1257 getParams(body.cname, epas[i])).asn1Encode()); 1258 } 1259 boolean allOld = true; 1260 for (int i: eTypes) { 1261 if (i == EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 || 1262 i == EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96) { 1263 allOld = false; 1264 break; 1265 } 1266 } 1267 if (allOld) { 1268 pas = new DerValue[epas.length]; 1269 for (int i=0; i<epas.length; i++) { 1270 pas[i] = new DerValue(new ETypeInfo( 1271 epas[i], 1272 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? 1273 null : getSalt(body.cname) 1274 ).asn1Encode()); 1275 } 1276 } 1277 } 1278 1279 DerOutputStream eid; 1280 if (pas2 != null) { 1281 eid = new DerOutputStream(); 1282 eid.putSequence(pas2); 1283 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray())); 1284 } 1285 if (pas != null) { 1286 eid = new DerOutputStream(); 1287 eid.putSequence(pas); 1288 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray())); 1289 } 1290 1291 PAData[] inPAs = asReq.pAData; 1292 List<PAData> enc_outPAs = new ArrayList<>(); 1293 1294 byte[] paEncTimestamp = null; 1295 if (inPAs != null) { 1296 for (PAData inPA : inPAs) { 1297 if (inPA.getType() == Krb5.PA_ENC_TIMESTAMP) { 1298 paEncTimestamp = inPA.getValue(); 1299 } 1300 } 1301 } 1302 1303 if (paEncTimestamp == null) { 1304 Object preauth = options.get(Option.PREAUTH_REQUIRED); 1305 if (preauth == null || preauth.equals(Boolean.TRUE)) { 1306 throw new KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED); 1307 } 1308 } else { 1309 EncryptionKey pakey = null; 1310 try { 1311 EncryptedData data = newEncryptedData( 1312 new DerValue(paEncTimestamp)); 1313 pakey = keyForUser(body.cname, data.getEType(), false); 1314 data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS); 1315 } catch (Exception e) { 1316 KrbException ke = new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED); 1317 ke.initCause(e); 1318 throw ke; 1319 } 1320 bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true; 1321 for (PAData pa : inPAs) { 1322 if (pa.getType() == Krb5.PA_REQ_ENC_PA_REP) { 1323 Checksum ckSum = new Checksum( 1324 Checksum.CKSUMTYPE_HMAC_SHA1_96_AES128, 1325 asReqbytes, ckey, KeyUsage.KU_AS_REQ); 1326 enc_outPAs.add(new PAData(Krb5.PA_REQ_ENC_PA_REP, 1327 ckSum.asn1Encode())); 1328 bFlags[Krb5.TKT_OPTS_ENC_PA_REP] = true; 1329 break; 1330 } 1331 } 1332 } 1333 1334 TicketFlags tFlags = new TicketFlags(bFlags); 1335 EncTicketPart enc = new EncTicketPart( 1336 tFlags, 1337 key, 1338 body.cname, 1339 new TransitedEncoding(1, new byte[0]), 1340 timeAfter(0), 1341 from, 1342 till, rtime, 1343 body.addresses, 1344 null); 1345 Ticket t = new Ticket( 1346 service, 1347 new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) 1348 ); 1349 EncASRepPart enc_part = new EncASRepPart( 1350 key, 1351 new LastReq(new LastReqEntry[]{ 1352 new LastReqEntry(0, timeAfter(-10)) 1353 }), 1354 body.getNonce(), // TODO: detect replay? 1355 timeAfter(3600 * 24), 1356 // Next 5 and last MUST be same with ticket 1357 tFlags, 1358 timeAfter(0), 1359 from, 1360 till, rtime, 1361 service, 1362 body.addresses, 1363 enc_outPAs.toArray(new PAData[enc_outPAs.size()]) 1364 ); 1365 EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), 1366 KeyUsage.KU_ENC_AS_REP_PART); 1367 ASRep asRep = new ASRep( 1368 outPAs.toArray(new PAData[outPAs.size()]), 1369 body.cname, 1370 t, 1371 edata); 1372 1373 System.out.println(" Return " + asRep.cname 1374 + " ticket for " + asRep.ticket.sname + ", flags " 1375 + tFlags); 1376 1377 DerOutputStream out = new DerOutputStream(); 1378 out.write(DerValue.createTag(DerValue.TAG_APPLICATION, 1379 true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode()); 1380 byte[] result = out.toByteArray(); 1381 1382 // Added feature: 1383 // Write the current issuing TGT into a ccache file specified 1384 // by the system property below. 1385 String ccache = System.getProperty("test.kdc.save.ccache"); 1386 if (ccache != null) { 1387 asRep.encKDCRepPart = enc_part; 1388 sun.security.krb5.internal.ccache.Credentials credentials = 1389 new sun.security.krb5.internal.ccache.Credentials(asRep); 1390 CredentialsCache cache = 1391 CredentialsCache.create(asReq.reqBody.cname, ccache); 1392 if (cache == null) { 1393 throw new IOException("Unable to create the cache file " + 1394 ccache); 1395 } 1396 cache.update(credentials); 1397 cache.save(); 1398 } 1399 1400 return result; 1401 } catch (KrbException ke) { 1402 ke.printStackTrace(System.out); 1403 KRBError kerr = ke.getError(); 1404 KDCReqBody body = asReq.reqBody; 1405 System.out.println(" Error " + ke.returnCode() 1406 + " " +ke.returnCodeMessage()); 1407 byte[] eData = null; 1408 if (kerr == null) { 1409 if (ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED || 1410 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED) { 1411 outPAs.add(new PAData(Krb5.PA_ENC_TIMESTAMP, new byte[0])); 1412 } 1413 if (outPAs.size() > 0) { 1414 DerOutputStream bytes = new DerOutputStream(); 1415 for (PAData p: outPAs) { 1416 bytes.write(p.asn1Encode()); 1417 } 1418 DerOutputStream temp = new DerOutputStream(); 1419 temp.write(DerValue.tag_Sequence, bytes); 1420 eData = temp.toByteArray(); 1421 } 1422 kerr = new KRBError(null, null, null, 1423 timeAfter(0), 1424 0, 1425 ke.returnCode(), 1426 body.cname, 1427 service, 1428 KrbException.errorMessage(ke.returnCode()), 1429 eData); 1430 } 1431 return kerr.asn1Encode(); 1432 } 1433 } 1434 1435 /** 1436 * Translates a duration value into seconds. 1437 * 1438 * The format can be one of "h:m[:s]", "NdNhNmNs", and "N". See 1439 * http://web.mit.edu/kerberos/krb5-devel/doc/basic/date_format.html#duration 1440 * for definitions. 1441 * 1442 * @param s the string duration 1443 * @return time in seconds 1444 * @throw KrbException if format is illegal 1445 */ duration(String s)1446 public static int duration(String s) throws KrbException { 1447 1448 if (s.isEmpty()) { 1449 throw new KrbException("Duration cannot be empty"); 1450 } 1451 1452 // N 1453 if (s.matches("\\d+")) { 1454 return Integer.parseInt(s); 1455 } 1456 1457 // h:m[:s] 1458 Matcher m = Pattern.compile("(\\d+):(\\d+)(:(\\d+))?").matcher(s); 1459 if (m.matches()) { 1460 int hr = Integer.parseInt(m.group(1)); 1461 int min = Integer.parseInt(m.group(2)); 1462 if (min >= 60) { 1463 throw new KrbException("Illegal duration format " + s); 1464 } 1465 int result = hr * 3600 + min * 60; 1466 if (m.group(4) != null) { 1467 int sec = Integer.parseInt(m.group(4)); 1468 if (sec >= 60) { 1469 throw new KrbException("Illegal duration format " + s); 1470 } 1471 result += sec; 1472 } 1473 return result; 1474 } 1475 1476 // NdNhNmNs 1477 // 120m allowed. Maybe 1h120m is not good, but still allowed 1478 m = Pattern.compile( 1479 "((\\d+)d)?\\s*((\\d+)h)?\\s*((\\d+)m)?\\s*((\\d+)s)?", 1480 Pattern.CASE_INSENSITIVE).matcher(s); 1481 if (m.matches()) { 1482 int result = 0; 1483 if (m.group(2) != null) { 1484 result += 86400 * Integer.parseInt(m.group(2)); 1485 } 1486 if (m.group(4) != null) { 1487 result += 3600 * Integer.parseInt(m.group(4)); 1488 } 1489 if (m.group(6) != null) { 1490 result += 60 * Integer.parseInt(m.group(6)); 1491 } 1492 if (m.group(8) != null) { 1493 result += Integer.parseInt(m.group(8)); 1494 } 1495 return result; 1496 } 1497 1498 throw new KrbException("Illegal duration format " + s); 1499 } 1500 filterSupported(int[] input)1501 private int[] filterSupported(int[] input) { 1502 int count = 0; 1503 for (int i = 0; i < input.length; i++) { 1504 if (!EType.isSupported(input[i])) { 1505 continue; 1506 } 1507 if (SUPPORTED_ETYPES != null) { 1508 boolean supported = false; 1509 for (String se : SUPPORTED_ETYPES.split(",")) { 1510 if (Config.getType(se) == input[i]) { 1511 supported = true; 1512 break; 1513 } 1514 } 1515 if (!supported) { 1516 continue; 1517 } 1518 } 1519 if (count != i) { 1520 input[count] = input[i]; 1521 } 1522 count++; 1523 } 1524 if (count != input.length) { 1525 input = Arrays.copyOf(input, count); 1526 } 1527 return input; 1528 } 1529 1530 /** 1531 * Generates a line for a KDC to put inside [realms] of krb5.conf 1532 * @return REALM.NAME = { kdc = host:port etc } 1533 */ realmLine()1534 private String realmLine() { 1535 StringBuilder sb = new StringBuilder(); 1536 sb.append(realm).append(" = {\n kdc = ") 1537 .append(kdc).append(':').append(port).append('\n'); 1538 for (String s: conf) { 1539 sb.append(" ").append(s).append('\n'); 1540 } 1541 return sb.append("}\n").toString(); 1542 } 1543 1544 /** 1545 * Start the KDC service. This server listens on both UDP and TCP using 1546 * the same port number. It uses three threads to deal with requests. 1547 * They can be set to daemon threads if requested. 1548 * @param port the port number to listen to. If zero, a random available 1549 * port no less than 8000 will be chosen and used. 1550 * @param asDaemon true if the KDC threads should be daemons 1551 * @throws java.io.IOException for any communication error 1552 */ startServer(int port, boolean asDaemon)1553 protected void startServer(int port, boolean asDaemon) throws IOException { 1554 if (nativeKdc != null) { 1555 startNativeServer(port, asDaemon); 1556 } else { 1557 startJavaServer(port, asDaemon); 1558 } 1559 } 1560 startNativeServer(int port, boolean asDaemon)1561 private void startNativeServer(int port, boolean asDaemon) throws IOException { 1562 nativeKdc.prepare(); 1563 nativeKdc.init(); 1564 kdcProc = nativeKdc.kdc(); 1565 } 1566 startJavaServer(int port, boolean asDaemon)1567 private void startJavaServer(int port, boolean asDaemon) throws IOException { 1568 if (port > 0) { 1569 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1570 t1 = new ServerSocket(port); 1571 } else { 1572 while (true) { 1573 // Try to find a port number that's both TCP and UDP free 1574 try { 1575 port = 8000 + new java.util.Random().nextInt(10000); 1576 u1 = null; 1577 u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); 1578 t1 = new ServerSocket(port); 1579 break; 1580 } catch (Exception e) { 1581 if (u1 != null) u1.close(); 1582 } 1583 } 1584 } 1585 final DatagramSocket udp = u1; 1586 final ServerSocket tcp = t1; 1587 System.out.println("Start KDC on " + port); 1588 1589 this.port = port; 1590 1591 // The UDP consumer 1592 thread1 = new Thread() { 1593 public void run() { 1594 udpConsumerReady = true; 1595 while (true) { 1596 try { 1597 byte[] inbuf = new byte[8192]; 1598 DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); 1599 udp.receive(p); 1600 System.out.println("-----------------------------------------------"); 1601 System.out.println(">>>>> UDP packet received"); 1602 q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); 1603 } catch (Exception e) { 1604 e.printStackTrace(); 1605 } 1606 } 1607 } 1608 }; 1609 thread1.setDaemon(asDaemon); 1610 thread1.start(); 1611 1612 // The TCP consumer 1613 thread2 = new Thread() { 1614 public void run() { 1615 tcpConsumerReady = true; 1616 while (true) { 1617 try { 1618 Socket socket = tcp.accept(); 1619 System.out.println("-----------------------------------------------"); 1620 System.out.println(">>>>> TCP connection established"); 1621 DataInputStream in = new DataInputStream(socket.getInputStream()); 1622 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 1623 byte[] token = new byte[in.readInt()]; 1624 in.readFully(token); 1625 q.put(new Job(processMessage(token), socket, out)); 1626 } catch (Exception e) { 1627 e.printStackTrace(); 1628 } 1629 } 1630 } 1631 }; 1632 thread2.setDaemon(asDaemon); 1633 thread2.start(); 1634 1635 // The dispatcher 1636 thread3 = new Thread() { 1637 public void run() { 1638 dispatcherReady = true; 1639 while (true) { 1640 try { 1641 q.take().send(); 1642 } catch (Exception e) { 1643 } 1644 } 1645 } 1646 }; 1647 thread3.setDaemon(true); 1648 thread3.start(); 1649 1650 // wait for the KDC is ready 1651 try { 1652 while (!isReady()) { 1653 Thread.sleep(100); 1654 } 1655 } catch(InterruptedException e) { 1656 throw new IOException(e); 1657 } 1658 } 1659 kinit(String user, String ccache)1660 public void kinit(String user, String ccache) throws Exception { 1661 if (user.indexOf('@') < 0) { 1662 user = user + "@" + realm; 1663 } 1664 if (nativeKdc != null) { 1665 nativeKdc.kinit(user, ccache); 1666 } else { 1667 Context.fromUserPass(user, passwords.get(user), false) 1668 .ccache(ccache); 1669 } 1670 } 1671 isReady()1672 boolean isReady() { 1673 return udpConsumerReady && tcpConsumerReady && dispatcherReady; 1674 } 1675 terminate()1676 public void terminate() { 1677 if (nativeKdc != null) { 1678 System.out.println("Killing kdc..."); 1679 kdcProc.destroyForcibly(); 1680 System.out.println("Done"); 1681 } else { 1682 try { 1683 thread1.stop(); 1684 thread2.stop(); 1685 thread3.stop(); 1686 u1.close(); 1687 t1.close(); 1688 } catch (Exception e) { 1689 // OK 1690 } 1691 } 1692 } 1693 startKDC(final String host, final String krbConfFileName, final String realm, final Map<String, String> principals, final String ktab, final KtabMode mode)1694 public static KDC startKDC(final String host, final String krbConfFileName, 1695 final String realm, final Map<String, String> principals, 1696 final String ktab, final KtabMode mode) { 1697 1698 KDC kdc; 1699 try { 1700 kdc = KDC.create(realm, host, 0, true); 1701 kdc.setOption(KDC.Option.PREAUTH_REQUIRED, Boolean.FALSE); 1702 if (krbConfFileName != null) { 1703 KDC.saveConfig(krbConfFileName, kdc); 1704 } 1705 1706 // Add principals 1707 if (principals != null) { 1708 principals.forEach((name, password) -> { 1709 if (password == null || password.isEmpty()) { 1710 System.out.println(String.format( 1711 "KDC:add a principal '%s' with a random " + 1712 "password", name)); 1713 kdc.addPrincipalRandKey(name); 1714 } else { 1715 System.out.println(String.format( 1716 "KDC:add a principal '%s' with '%s' password", 1717 name, password)); 1718 kdc.addPrincipal(name, password.toCharArray()); 1719 } 1720 }); 1721 } 1722 1723 // Create or append keys to existing keytab file 1724 if (ktab != null) { 1725 File ktabFile = new File(ktab); 1726 switch(mode) { 1727 case APPEND: 1728 if (ktabFile.exists()) { 1729 System.out.println(String.format( 1730 "KDC:append keys to an exising keytab " 1731 + "file %s", ktab)); 1732 kdc.appendKtab(ktab); 1733 } else { 1734 System.out.println(String.format( 1735 "KDC:create a new keytab file %s", ktab)); 1736 kdc.writeKtab(ktab); 1737 } 1738 break; 1739 case EXISTING: 1740 System.out.println(String.format( 1741 "KDC:use an existing keytab file %s", ktab)); 1742 break; 1743 default: 1744 throw new RuntimeException(String.format( 1745 "KDC:unsupported keytab mode: %s", mode)); 1746 } 1747 } 1748 1749 System.out.println(String.format( 1750 "KDC: started on %s:%s with '%s' realm", 1751 host, kdc.getPort(), realm)); 1752 } catch (Exception e) { 1753 throw new RuntimeException("KDC: unexpected exception", e); 1754 } 1755 1756 return kdc; 1757 } 1758 1759 /** 1760 * Helper class to encapsulate a job in a KDC. 1761 */ 1762 private static class Job { 1763 byte[] token; // The received request at creation time and 1764 // the response at send time 1765 Socket s; // The TCP socket from where the request comes 1766 DataOutputStream out; // The OutputStream of the TCP socket 1767 DatagramSocket s2; // The UDP socket from where the request comes 1768 DatagramPacket dp; // The incoming UDP datagram packet 1769 boolean useTCP; // Whether TCP or UDP is used 1770 1771 // Creates a job object for TCP Job(byte[] token, Socket s, DataOutputStream out)1772 Job(byte[] token, Socket s, DataOutputStream out) { 1773 useTCP = true; 1774 this.token = token; 1775 this.s = s; 1776 this.out = out; 1777 } 1778 1779 // Creates a job object for UDP Job(byte[] token, DatagramSocket s2, DatagramPacket dp)1780 Job(byte[] token, DatagramSocket s2, DatagramPacket dp) { 1781 useTCP = false; 1782 this.token = token; 1783 this.s2 = s2; 1784 this.dp = dp; 1785 } 1786 1787 // Sends the output back to the client send()1788 void send() { 1789 try { 1790 if (useTCP) { 1791 System.out.println(">>>>> TCP request honored"); 1792 out.writeInt(token.length); 1793 out.write(token); 1794 s.close(); 1795 } else { 1796 System.out.println(">>>>> UDP request honored"); 1797 s2.send(new DatagramPacket(token, token.length, dp.getAddress(), dp.getPort())); 1798 } 1799 } catch (Exception e) { 1800 e.printStackTrace(); 1801 } 1802 } 1803 } 1804 1805 public static class KDCNameService implements NameServiceDescriptor { 1806 1807 public static String NOT_EXISTING_HOST = "not.existing.host"; 1808 1809 @Override createNameService()1810 public NameService createNameService() throws Exception { 1811 NameService ns = new NameService() { 1812 @Override 1813 public InetAddress[] lookupAllHostAddr(String host) 1814 throws UnknownHostException { 1815 // Everything is localhost except NOT_EXISTING_HOST 1816 if (NOT_EXISTING_HOST.equals(host)) { 1817 throw new UnknownHostException("Unknown host name: " 1818 + NOT_EXISTING_HOST); 1819 } 1820 return new InetAddress[]{ 1821 InetAddress.getByAddress(host, new byte[]{127,0,0,1}) 1822 }; 1823 } 1824 @Override 1825 public String getHostByAddr(byte[] addr) 1826 throws UnknownHostException { 1827 // No reverse lookup, PrincipalName use original string 1828 throw new UnknownHostException(); 1829 } 1830 }; 1831 return ns; 1832 } 1833 1834 @Override getProviderName()1835 public String getProviderName() { 1836 return "mock"; 1837 } 1838 1839 @Override getType()1840 public String getType() { 1841 return "ns"; 1842 } 1843 } 1844 1845 /** 1846 * A native KDC using the binaries in nativePath. Attention: 1847 * this is using binaries, not an existing KDC instance. 1848 * An implementation of this takes care of configuration, 1849 * principal db managing and KDC startup. 1850 */ 1851 static abstract class NativeKdc { 1852 1853 protected Map<String,String> env; 1854 protected String nativePath; 1855 protected String base; 1856 protected String realm; 1857 protected int port; 1858 NativeKdc(String nativePath, KDC kdc)1859 NativeKdc(String nativePath, KDC kdc) { 1860 if (kdc.port == 0) { 1861 kdc.port = 8000 + new java.util.Random().nextInt(10000); 1862 } 1863 this.nativePath = nativePath; 1864 this.realm = kdc.realm; 1865 this.port = kdc.port; 1866 this.base = Paths.get("" + port).toAbsolutePath().toString(); 1867 } 1868 1869 // Add a new principal addPrincipal(String user, String pass)1870 abstract void addPrincipal(String user, String pass); 1871 // Add a keytab entry ktadd(String user, String ktab)1872 abstract void ktadd(String user, String ktab); 1873 // Initialize KDC init()1874 abstract void init(); 1875 // Start kdc kdc()1876 abstract Process kdc(); 1877 // Configuration prepare()1878 abstract void prepare(); 1879 // Fill ccache kinit(String user, String ccache)1880 abstract void kinit(String user, String ccache); 1881 get(KDC kdc)1882 static NativeKdc get(KDC kdc) { 1883 String prop = System.getProperty("native.kdc.path"); 1884 if (prop == null) { 1885 return null; 1886 } else if (Files.exists(Paths.get(prop, "sbin/krb5kdc"))) { 1887 return new MIT(true, prop, kdc); 1888 } else if (Files.exists(Paths.get(prop, "kdc/krb5kdc"))) { 1889 return new MIT(false, prop, kdc); 1890 } else if (Files.exists(Paths.get(prop, "libexec/kdc"))) { 1891 return new Heimdal(prop, kdc); 1892 } else { 1893 throw new IllegalArgumentException("Strange " + prop); 1894 } 1895 } 1896 run(boolean wait, String... cmd)1897 Process run(boolean wait, String... cmd) { 1898 try { 1899 System.out.println("Running " + cmd2str(env, cmd)); 1900 ProcessBuilder pb = new ProcessBuilder(); 1901 pb.inheritIO(); 1902 pb.environment().putAll(env); 1903 Process p = pb.command(cmd).start(); 1904 if (wait) { 1905 if (p.waitFor() < 0) { 1906 throw new RuntimeException("exit code is not null"); 1907 } 1908 return null; 1909 } else { 1910 return p; 1911 } 1912 } catch (Exception e) { 1913 throw new RuntimeException(e); 1914 } 1915 } 1916 cmd2str(Map<String,String> env, String... cmd)1917 private String cmd2str(Map<String,String> env, String... cmd) { 1918 return env.entrySet().stream().map(e -> e.getKey()+"="+e.getValue()) 1919 .collect(Collectors.joining(" ")) + " " + 1920 Stream.of(cmd).collect(Collectors.joining(" ")); 1921 } 1922 } 1923 1924 // Heimdal KDC. Build your own and run "make install" to nativePath. 1925 static class Heimdal extends NativeKdc { 1926 Heimdal(String nativePath, KDC kdc)1927 Heimdal(String nativePath, KDC kdc) { 1928 super(nativePath, kdc); 1929 Map<String, String> environment = new HashMap<>(); 1930 environment.put("KRB5_CONFIG", base + "/krb5.conf"); 1931 environment.put("KRB5_TRACE", "/dev/stderr"); 1932 environment.put("DYLD_LIBRARY_PATH", nativePath + "/lib"); 1933 environment.put("LD_LIBRARY_PATH", nativePath + "/lib"); 1934 this.env = Collections.unmodifiableMap(environment); 1935 } 1936 1937 @Override addPrincipal(String user, String pass)1938 public void addPrincipal(String user, String pass) { 1939 run(true, nativePath + "/bin/kadmin", "-l", "-r", realm, 1940 "add", "-p", pass, "--use-defaults", user); 1941 } 1942 1943 @Override ktadd(String user, String ktab)1944 public void ktadd(String user, String ktab) { 1945 run(true, nativePath + "/bin/kadmin", "-l", "-r", realm, 1946 "ext_keytab", "-k", ktab, user); 1947 } 1948 1949 @Override init()1950 public void init() { 1951 run(true, nativePath + "/bin/kadmin", "-l", "-r", realm, 1952 "init", "--realm-max-ticket-life=1day", 1953 "--realm-max-renewable-life=1month", realm); 1954 } 1955 1956 @Override kdc()1957 public Process kdc() { 1958 return run(false, nativePath + "/libexec/kdc", 1959 "--addresses=127.0.0.1", "-P", "" + port); 1960 } 1961 1962 @Override prepare()1963 public void prepare() { 1964 try { 1965 Files.createDirectory(Paths.get(base)); 1966 Files.write(Paths.get(base + "/krb5.conf"), Arrays.asList( 1967 "[libdefaults]", 1968 "default_realm = " + realm, 1969 "default_keytab_name = FILE:" + base + "/krb5.keytab", 1970 "forwardable = true", 1971 "dns_lookup_kdc = no", 1972 "dns_lookup_realm = no", 1973 "dns_canonicalize_hostname = false", 1974 "\n[realms]", 1975 realm + " = {", 1976 " kdc = localhost:" + port, 1977 "}", 1978 "\n[kdc]", 1979 "db-dir = " + base, 1980 "database = {", 1981 " label = {", 1982 " dbname = " + base + "/current-db", 1983 " realm = " + realm, 1984 " mkey_file = " + base + "/mkey.file", 1985 " acl_file = " + base + "/heimdal.acl", 1986 " log_file = " + base + "/current.log", 1987 " }", 1988 "}", 1989 SUPPORTED_ETYPES == null ? "" 1990 : ("\n[kadmin]\ndefault_keys = " 1991 + (SUPPORTED_ETYPES + ",") 1992 .replaceAll(",", ":pw-salt ")), 1993 "\n[logging]", 1994 "kdc = 0-/FILE:" + base + "/messages.log", 1995 "krb5 = 0-/FILE:" + base + "/messages.log", 1996 "default = 0-/FILE:" + base + "/messages.log" 1997 )); 1998 } catch (IOException e) { 1999 throw new UncheckedIOException(e); 2000 } 2001 } 2002 2003 @Override kinit(String user, String ccache)2004 void kinit(String user, String ccache) { 2005 String tmpName = base + "/" + user + "." + 2006 System.identityHashCode(this) + ".keytab"; 2007 ktadd(user, tmpName); 2008 run(true, nativePath + "/bin/kinit", 2009 "-f", "-t", tmpName, "-c", ccache, user); 2010 } 2011 } 2012 2013 // MIT krb5 KDC. Make your own exploded (install == false), or 2014 // "make install" into nativePath (install == true). 2015 static class MIT extends NativeKdc { 2016 2017 private boolean install; // "make install" or "make" 2018 MIT(boolean install, String nativePath, KDC kdc)2019 MIT(boolean install, String nativePath, KDC kdc) { 2020 super(nativePath, kdc); 2021 this.install = install; 2022 Map<String, String> environment = new HashMap<>(); 2023 environment.put("KRB5_KDC_PROFILE", base + "/kdc.conf"); 2024 environment.put("KRB5_CONFIG", base + "/krb5.conf"); 2025 environment.put("KRB5_TRACE", "/dev/stderr"); 2026 environment.put("DYLD_LIBRARY_PATH", nativePath + "/lib"); 2027 environment.put("LD_LIBRARY_PATH", nativePath + "/lib"); 2028 this.env = Collections.unmodifiableMap(environment); 2029 } 2030 2031 @Override addPrincipal(String user, String pass)2032 public void addPrincipal(String user, String pass) { 2033 run(true, nativePath + 2034 (install ? "/sbin/" : "/kadmin/cli/") + "kadmin.local", 2035 "-q", "addprinc -pw " + pass + " " + user); 2036 } 2037 2038 @Override ktadd(String user, String ktab)2039 public void ktadd(String user, String ktab) { 2040 run(true, nativePath + 2041 (install ? "/sbin/" : "/kadmin/cli/") + "kadmin.local", 2042 "-q", "ktadd -k " + ktab + " -norandkey " + user); 2043 } 2044 2045 @Override init()2046 public void init() { 2047 run(true, nativePath + 2048 (install ? "/sbin/" : "/kadmin/dbutil/") + "kdb5_util", 2049 "create", "-s", "-W", "-P", "olala"); 2050 } 2051 2052 @Override kdc()2053 public Process kdc() { 2054 return run(false, nativePath + 2055 (install ? "/sbin/" : "/kdc/") + "krb5kdc", 2056 "-n"); 2057 } 2058 2059 @Override prepare()2060 public void prepare() { 2061 try { 2062 Files.createDirectory(Paths.get(base)); 2063 Files.write(Paths.get(base + "/kdc.conf"), Arrays.asList( 2064 "[kdcdefaults]", 2065 "\n[realms]", 2066 realm + "= {", 2067 " kdc_listen = " + this.port, 2068 " kdc_tcp_listen = " + this.port, 2069 " database_name = " + base + "/principal", 2070 " key_stash_file = " + base + "/.k5.ATHENA.MIT.EDU", 2071 SUPPORTED_ETYPES == null ? "" 2072 : (" supported_enctypes = " 2073 + (SUPPORTED_ETYPES + ",") 2074 .replaceAll(",", ":normal ")), 2075 "}" 2076 )); 2077 Files.write(Paths.get(base + "/krb5.conf"), Arrays.asList( 2078 "[libdefaults]", 2079 "default_realm = " + realm, 2080 "default_keytab_name = FILE:" + base + "/krb5.keytab", 2081 "forwardable = true", 2082 "dns_lookup_kdc = no", 2083 "dns_lookup_realm = no", 2084 "dns_canonicalize_hostname = false", 2085 "\n[realms]", 2086 realm + " = {", 2087 " kdc = localhost:" + port, 2088 "}", 2089 "\n[logging]", 2090 "kdc = FILE:" + base + "/krb5kdc.log" 2091 )); 2092 } catch (IOException e) { 2093 throw new UncheckedIOException(e); 2094 } 2095 } 2096 2097 @Override kinit(String user, String ccache)2098 void kinit(String user, String ccache) { 2099 String tmpName = base + "/" + user + "." + 2100 System.identityHashCode(this) + ".keytab"; 2101 ktadd(user, tmpName); 2102 run(true, nativePath + 2103 (install ? "/bin/" : "/clients/kinit/") + "kinit", 2104 "-f", "-t", tmpName, "-c", ccache, user); 2105 } 2106 } 2107 2108 // Calling private methods thru reflections 2109 private static final Field getEType; 2110 private static final Constructor<EncryptedData> ctorEncryptedData; 2111 private static final Method stringToKey; 2112 private static final Field getAddlTkt; 2113 2114 static { 2115 try { 2116 ctorEncryptedData = EncryptedData.class.getDeclaredConstructor(DerValue.class); 2117 ctorEncryptedData.setAccessible(true); 2118 getEType = KDCReqBody.class.getDeclaredField("eType"); 2119 getEType.setAccessible(true); 2120 stringToKey = EncryptionKey.class.getDeclaredMethod( 2121 "stringToKey", 2122 char[].class, String.class, byte[].class, Integer.TYPE); 2123 stringToKey.setAccessible(true); 2124 getAddlTkt = KDCReqBody.class.getDeclaredField("additionalTickets"); 2125 getAddlTkt.setAccessible(true); 2126 } catch (NoSuchFieldException nsfe) { 2127 throw new AssertionError(nsfe); 2128 } catch (NoSuchMethodException nsme) { 2129 throw new AssertionError(nsme); 2130 } 2131 } newEncryptedData(DerValue der)2132 private EncryptedData newEncryptedData(DerValue der) { 2133 try { 2134 return ctorEncryptedData.newInstance(der); 2135 } catch (Exception e) { 2136 throw new AssertionError(e); 2137 } 2138 } KDCReqBodyDotEType(KDCReqBody body)2139 private static int[] KDCReqBodyDotEType(KDCReqBody body) { 2140 try { 2141 return (int[]) getEType.get(body); 2142 } catch (Exception e) { 2143 throw new AssertionError(e); 2144 } 2145 } EncryptionKeyDotStringToKey(char[] password, String salt, byte[] s2kparams, int keyType)2146 private static byte[] EncryptionKeyDotStringToKey(char[] password, String salt, 2147 byte[] s2kparams, int keyType) throws KrbCryptoException { 2148 try { 2149 return (byte[])stringToKey.invoke( 2150 null, password, salt, s2kparams, keyType); 2151 } catch (InvocationTargetException ex) { 2152 throw (KrbCryptoException)ex.getCause(); 2153 } catch (Exception e) { 2154 throw new AssertionError(e); 2155 } 2156 } KDCReqBodyDotFirstAdditionalTicket(KDCReqBody body)2157 private static Ticket KDCReqBodyDotFirstAdditionalTicket(KDCReqBody body) { 2158 try { 2159 return ((Ticket[])getAddlTkt.get(body))[0]; 2160 } catch (Exception e) { 2161 throw new AssertionError(e); 2162 } 2163 } 2164 } 2165