Java tutorial
/* * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.security.auth.kerberos; import java.io.*; import java.util.Date; import java.util.Arrays; import java.net.InetAddress; import javax.crypto.SecretKey; import javax.security.auth.Refreshable; import javax.security.auth.Destroyable; import javax.security.auth.RefreshFailedException; import javax.security.auth.DestroyFailedException; import sun.security.util.HexDumpEncoder; /** * This class encapsulates a Kerberos ticket and associated * information as viewed from the client's point of view. It captures all * information that the Key Distribution Center (KDC) sends to the client * in the reply message KDC-REP defined in the Kerberos Protocol * Specification (<a href=http://www.ietf.org/rfc/rfc4120.txt>RFC 4120</a>). * <p> * All Kerberos JAAS login modules that authenticate a user to a KDC should * use this class. Where available, the login module might even read this * information from a ticket cache in the operating system instead of * directly communicating with the KDC. During the commit phase of the JAAS * authentication process, the JAAS login module should instantiate this * class and store the instance in the private credential set of a * {@link javax.security.auth.Subject Subject}.<p> * * It might be necessary for the application to be granted a * {@link javax.security.auth.PrivateCredentialPermission * PrivateCredentialPermission} if it needs to access a {@code KerberosTicket} * instance from a {@code Subject}. This permission is not needed when the * application depends on the default JGSS Kerberos mechanism to access the * {@code KerberosTicket}. In that case, however, the application will need an * appropriate * {@link javax.security.auth.kerberos.ServicePermission ServicePermission}. * <p> * Note that this class is applicable to both ticket granting tickets and * other regular service tickets. A ticket granting ticket is just a * special case of a more generalized service ticket. * * @implNote The JAAS login module in the JDK reference implementation destroys * all tickets after logout. * * @see javax.security.auth.Subject * @see javax.security.auth.PrivateCredentialPermission * @see javax.security.auth.login.LoginContext * @see org.ietf.jgss.GSSCredential * @see org.ietf.jgss.GSSManager * * @author Mayank Upadhyay * @since 1.4 */ public class KerberosTicket implements Destroyable, Refreshable, java.io.Serializable { private static final long serialVersionUID = 7395334370157380539L; // XXX Make these flag indices public private static final int FORWARDABLE_TICKET_FLAG = 1; private static final int FORWARDED_TICKET_FLAG = 2; private static final int PROXIABLE_TICKET_FLAG = 3; private static final int PROXY_TICKET_FLAG = 4; private static final int POSTDATED_TICKET_FLAG = 6; private static final int RENEWABLE_TICKET_FLAG = 8; private static final int INITIAL_TICKET_FLAG = 9; private static final int NUM_FLAGS = 32; /** * * ASN.1 DER Encoding of the Ticket as defined in the * Kerberos Protocol Specification RFC4120. * * @serial */ private byte[] asn1Encoding; /** *{@code KeyImpl} is serialized by writing out the ASN1 Encoded bytes * of the encryption key. The ASN1 encoding is defined in RFC4120 and as * follows: * <pre> * EncryptionKey ::= SEQUENCE { * keytype [0] Int32 -- actually encryption type --, * keyvalue [1] OCTET STRING * } * </pre> * * @serial */ private KeyImpl sessionKey; /** * * Ticket Flags as defined in the Kerberos Protocol Specification RFC4120. * * @serial */ private boolean[] flags; /** * * Time of initial authentication * * @serial */ private Date authTime; /** * * Time after which the ticket is valid. * @serial */ private Date startTime; /** * * Time after which the ticket will not be honored. (its expiration time). * * @serial */ private Date endTime; /** * * For renewable Tickets it indicates the maximum endtime that may be * included in a renewal. It can be thought of as the absolute expiration * time for the ticket, including all renewals. This field may be null * for tickets that are not renewable. * * @serial */ private Date renewTill; /** * * Client that owns the service ticket * * @serial */ private KerberosPrincipal client; /** * * The service for which the ticket was issued. * * @serial */ private KerberosPrincipal server; /** * * The addresses from where the ticket may be used by the client. * This field may be null when the ticket is usable from any address. * * @serial */ private InetAddress[] clientAddresses; private transient boolean destroyed = false; /** * Constructs a {@code KerberosTicket} using credentials information that a * client either receives from a KDC or reads from a cache. * * @param asn1Encoding the ASN.1 encoding of the ticket as defined by * the Kerberos protocol specification. * @param client the client that owns this service * ticket * @param server the service that this ticket is for * @param sessionKey the raw bytes for the session key that must be * used to encrypt the authenticator that will be sent to the server * @param keyType the key type for the session key as defined by the * Kerberos protocol specification. * @param flags the ticket flags. Each element in this array indicates * the value for the corresponding bit in the ASN.1 BitString that * represents the ticket flags. If the number of elements in this array * is less than the number of flags used by the Kerberos protocol, * then the missing flags will be filled in with false. * @param authTime the time of initial authentication for the client * @param startTime the time after which the ticket will be valid. This * may be null in which case the value of authTime is treated as the * startTime. * @param endTime the time after which the ticket will no longer be * valid * @param renewTill an absolute expiration time for the ticket, * including all renewal that might be possible. This field may be null * for tickets that are not renewable. * @param clientAddresses the addresses from where the ticket may be * used by the client. This field may be null when the ticket is usable * from any address. */ public KerberosTicket(byte[] asn1Encoding, KerberosPrincipal client, KerberosPrincipal server, byte[] sessionKey, int keyType, boolean[] flags, Date authTime, Date startTime, Date endTime, Date renewTill, InetAddress[] clientAddresses) { init(asn1Encoding, client, server, sessionKey, keyType, flags, authTime, startTime, endTime, renewTill, clientAddresses); } private void init(byte[] asn1Encoding, KerberosPrincipal client, KerberosPrincipal server, byte[] sessionKey, int keyType, boolean[] flags, Date authTime, Date startTime, Date endTime, Date renewTill, InetAddress[] clientAddresses) { if (sessionKey == null) { throw new IllegalArgumentException("Session key for ticket" + " cannot be null"); } init(asn1Encoding, client, server, new KeyImpl(sessionKey, keyType), flags, authTime, startTime, endTime, renewTill, clientAddresses); } private void init(byte[] asn1Encoding, KerberosPrincipal client, KerberosPrincipal server, KeyImpl sessionKey, boolean[] flags, Date authTime, Date startTime, Date endTime, Date renewTill, InetAddress[] clientAddresses) { if (asn1Encoding == null) { throw new IllegalArgumentException("ASN.1 encoding of ticket" + " cannot be null"); } this.asn1Encoding = asn1Encoding.clone(); if (client == null) { throw new IllegalArgumentException("Client name in ticket" + " cannot be null"); } this.client = client; if (server == null) { throw new IllegalArgumentException("Server name in ticket" + " cannot be null"); } this.server = server; // Caller needs to make sure `sessionKey` will not be null this.sessionKey = sessionKey; if (flags != null) { if (flags.length >= NUM_FLAGS) { this.flags = flags.clone(); } else { this.flags = new boolean[NUM_FLAGS]; // Fill in whatever we have for (int i = 0; i < flags.length; i++) { this.flags[i] = flags[i]; } } } else { this.flags = new boolean[NUM_FLAGS]; } if (this.flags[RENEWABLE_TICKET_FLAG] && renewTill != null) { this.renewTill = new Date(renewTill.getTime()); } if (authTime != null) { this.authTime = new Date(authTime.getTime()); } if (startTime != null) { this.startTime = new Date(startTime.getTime()); } else { this.startTime = this.authTime; } if (endTime == null) { throw new IllegalArgumentException("End time for ticket validity" + " cannot be null"); } this.endTime = new Date(endTime.getTime()); if (clientAddresses != null) { this.clientAddresses = clientAddresses.clone(); } } /** * Returns the client principal associated with this ticket. * * @return the client principal, or {@code null} if destroyed. */ public final KerberosPrincipal getClient() { return client; } /** * Returns the service principal associated with this ticket. * * @return the service principal, or {@code null} if destroyed. */ public final KerberosPrincipal getServer() { return server; } /** * Returns the session key associated with this ticket. The return value * is always a {@link EncryptionKey} object. * * @return the session key. * @throws IllegalStateException if this ticket is destroyed */ public final SecretKey getSessionKey() { if (destroyed) { throw new IllegalStateException("This ticket is no longer valid"); } return new EncryptionKey(sessionKey.getEncoded(), sessionKey.getKeyType()); } /** * Returns the key type of the session key associated with this * ticket as defined by the Kerberos Protocol Specification. * * @return the key type of the session key associated with this * ticket. * @throws IllegalStateException if this ticket is destroyed * * @see #getSessionKey() */ public final int getSessionKeyType() { if (destroyed) { throw new IllegalStateException("This ticket is no longer valid"); } return sessionKey.getKeyType(); } /** * Determines if this ticket is forwardable. * * @return true if this ticket is forwardable, or false if not forwardable * or destroyed. */ public final boolean isForwardable() { return flags == null ? false : flags[FORWARDABLE_TICKET_FLAG]; } /** * Determines if this ticket had been forwarded or was issued based on * authentication involving a forwarded ticket-granting ticket. * * @return true if this ticket had been forwarded or was issued based on * authentication involving a forwarded ticket-granting ticket, * or false otherwise or destroyed. */ public final boolean isForwarded() { return flags == null ? false : flags[FORWARDED_TICKET_FLAG]; } /** * Determines if this ticket is proxiable. * * @return true if this ticket is proxiable, or false if not proxiable * or destroyed. */ public final boolean isProxiable() { return flags == null ? false : flags[PROXIABLE_TICKET_FLAG]; } /** * Determines is this ticket is a proxy-ticket. * * @return true if this ticket is a proxy-ticket, or false if not * a proxy-ticket or destroyed. */ public final boolean isProxy() { return flags == null ? false : flags[PROXY_TICKET_FLAG]; } /** * Determines is this ticket is post-dated. * * @return true if this ticket is post-dated, or false if not post-dated * or destroyed. */ public final boolean isPostdated() { return flags == null ? false : flags[POSTDATED_TICKET_FLAG]; } /** * Determines is this ticket is renewable. If so, the {@link #refresh() * refresh} method can be called, assuming the validity period for * renewing is not already over. * * @return true if this ticket is renewable, or false if not renewable * or destroyed. */ public final boolean isRenewable() { return flags == null ? false : flags[RENEWABLE_TICKET_FLAG]; } /** * Determines if this ticket was issued using the Kerberos AS-Exchange * protocol, and not issued based on some ticket-granting ticket. * * @return true if this ticket was issued using the Kerberos AS-Exchange * protocol, or false if not issued this way or destroyed. */ public final boolean isInitial() { return flags == null ? false : flags[INITIAL_TICKET_FLAG]; } /** * Returns the flags associated with this ticket. Each element in the * returned array indicates the value for the corresponding bit in the * ASN.1 BitString that represents the ticket flags. * * @return the flags associated with this ticket, or {@code null} * if destroyed. */ public final boolean[] getFlags() { return (flags == null ? null : flags.clone()); } /** * Returns the time that the client was authenticated. * * @return the time that the client was authenticated * or {@code null} if the field is not set or * this ticket is destroyed. */ public final java.util.Date getAuthTime() { return (authTime == null) ? null : (Date) authTime.clone(); } /** * Returns the start time for this ticket's validity period. * * @return the start time for this ticket's validity period * or {@code null} if the field is not set or * this ticket is destroyed. */ public final java.util.Date getStartTime() { return (startTime == null) ? null : (Date) startTime.clone(); } /** * Returns the expiration time for this ticket's validity period. * * @return the expiration time for this ticket's validity period, * or {@code null} if destroyed. */ public final java.util.Date getEndTime() { return (endTime == null) ? null : (Date) endTime.clone(); } /** * Returns the latest expiration time for this ticket, including all * renewals. This will return a null value for non-renewable tickets. * * @return the latest expiration time for this ticket, or {@code null} * if destroyed. */ public final java.util.Date getRenewTill() { return (renewTill == null) ? null : (Date) renewTill.clone(); } /** * Returns a list of addresses from where the ticket can be used. * * @return the list of addresses, or {@code null} if the field was not * provided or this ticket is destroyed. */ public final java.net.InetAddress[] getClientAddresses() { return (clientAddresses == null) ? null : clientAddresses.clone(); } /** * Returns an ASN.1 encoding of the entire ticket. * * @return an ASN.1 encoding of the entire ticket. A new byte * array is returned each time this method is called. * @throws IllegalStateException if this ticket is destroyed */ public final byte[] getEncoded() { if (destroyed) { throw new IllegalStateException("This ticket is no longer valid"); } return asn1Encoding.clone(); } /** * Determines if this ticket is still current. * * @return true if this ticket is still current, or false if not current * or destroyed. */ public boolean isCurrent() { return endTime == null ? false : (System.currentTimeMillis() <= endTime.getTime()); } /** * Extends the validity period of this ticket. The ticket will contain * a new session key if the refresh operation succeeds. The refresh * operation will fail if the ticket is not renewable or the latest * allowable renew time has passed. Any other error returned by the * KDC will also cause this method to fail. * * Note: This method is not synchronized with the accessor * methods of this object. Hence callers need to be aware of multiple * threads that might access this and try to renew it at the same * time. * * @throws IllegalStateException if this ticket is destroyed * @throws RefreshFailedException if the ticket is not renewable, or * the latest allowable renew time has passed, or the KDC returns some * error. * * @see #isRenewable() * @see #getRenewTill() */ public void refresh() throws RefreshFailedException { if (destroyed) { throw new RefreshFailedException("A destroyed ticket " + "cannot be renewd."); } if (!isRenewable()) { throw new RefreshFailedException("This ticket is not renewable"); } if (getRenewTill() == null) { // Renewable ticket without renew-till. Illegal and ignored. return; } if (System.currentTimeMillis() > getRenewTill().getTime()) { throw new RefreshFailedException("This ticket is past " + "its last renewal time."); } Throwable e = null; sun.security.krb5.Credentials krb5Creds = null; try { krb5Creds = new sun.security.krb5.Credentials(asn1Encoding, client.getName(), server.getName(), sessionKey.getEncoded(), sessionKey.getKeyType(), flags, authTime, startTime, endTime, renewTill, clientAddresses); krb5Creds = krb5Creds.renew(); } catch (sun.security.krb5.KrbException krbException) { e = krbException; } catch (java.io.IOException ioException) { e = ioException; } if (e != null) { RefreshFailedException rfException = new RefreshFailedException("Failed to renew Kerberos Ticket " + "for client " + client + " and server " + server + " - " + e.getMessage()); rfException.initCause(e); throw rfException; } /* * In case multiple threads try to refresh it at the same time. */ synchronized (this) { try { this.destroy(); } catch (DestroyFailedException dfException) { // Squelch it since we don't care about the old ticket. } init(krb5Creds.getEncoded(), new KerberosPrincipal(krb5Creds.getClient().getName()), new KerberosPrincipal(krb5Creds.getServer().getName(), KerberosPrincipal.KRB_NT_SRV_INST), krb5Creds.getSessionKey().getBytes(), krb5Creds.getSessionKey().getEType(), krb5Creds.getFlags(), krb5Creds.getAuthTime(), krb5Creds.getStartTime(), krb5Creds.getEndTime(), krb5Creds.getRenewTill(), krb5Creds.getClientAddresses()); destroyed = false; } } /** * Destroys the ticket and destroys any sensitive information stored in * it. */ public void destroy() throws DestroyFailedException { if (!destroyed) { Arrays.fill(asn1Encoding, (byte) 0); client = null; server = null; sessionKey.destroy(); flags = null; authTime = null; startTime = null; endTime = null; renewTill = null; clientAddresses = null; destroyed = true; } } /** * Determines if this ticket has been destroyed. */ public boolean isDestroyed() { return destroyed; } /** * Returns an informative textual representation of this {@code KerberosTicket}. * * @return an informative textual representation of this {@code KerberosTicket}. */ public String toString() { if (destroyed) { return "Destroyed KerberosTicket"; } StringBuilder caddrString = new StringBuilder(); if (clientAddresses != null) { for (int i = 0; i < clientAddresses.length; i++) { caddrString.append("clientAddresses[" + i + "] = " + clientAddresses[i].toString()); } } return ("Ticket (hex) = " + "\n" + (new HexDumpEncoder()).encodeBuffer(asn1Encoding) + "\n" + "Client Principal = " + client.toString() + "\n" + "Server Principal = " + server.toString() + "\n" + "Session Key = " + sessionKey.toString() + "\n" + "Forwardable Ticket " + flags[FORWARDABLE_TICKET_FLAG] + "\n" + "Forwarded Ticket " + flags[FORWARDED_TICKET_FLAG] + "\n" + "Proxiable Ticket " + flags[PROXIABLE_TICKET_FLAG] + "\n" + "Proxy Ticket " + flags[PROXY_TICKET_FLAG] + "\n" + "Postdated Ticket " + flags[POSTDATED_TICKET_FLAG] + "\n" + "Renewable Ticket " + flags[RENEWABLE_TICKET_FLAG] + "\n" + "Initial Ticket " + flags[INITIAL_TICKET_FLAG] + "\n" + "Auth Time = " + String.valueOf(authTime) + "\n" + "Start Time = " + String.valueOf(startTime) + "\n" + "End Time = " + endTime.toString() + "\n" + "Renew Till = " + String.valueOf(renewTill) + "\n" + "Client Addresses " + (clientAddresses == null ? " Null " : caddrString.toString() + "\n")); } /** * Returns a hash code for this {@code KerberosTicket}. * * @return a hash code for this {@code KerberosTicket}. * @since 1.6 */ public int hashCode() { int result = 17; if (isDestroyed()) { return result; } result = result * 37 + Arrays.hashCode(getEncoded()); result = result * 37 + endTime.hashCode(); result = result * 37 + client.hashCode(); result = result * 37 + server.hashCode(); result = result * 37 + sessionKey.hashCode(); // authTime may be null if (authTime != null) { result = result * 37 + authTime.hashCode(); } // startTime may be null if (startTime != null) { result = result * 37 + startTime.hashCode(); } // renewTill may be null if (renewTill != null) { result = result * 37 + renewTill.hashCode(); } // clientAddress may be null, the array's hashCode is 0 result = result * 37 + Arrays.hashCode(clientAddresses); return result * 37 + Arrays.hashCode(flags); } /** * Compares the specified object with this {@code KerberosTicket} for equality. * Returns true if the given object is also a * {@code KerberosTicket} and the two * {@code KerberosTicket} instances are equivalent. * A destroyed {@code KerberosTicket} object is only equal to itself. * * @param other the object to compare to * @return true if the specified object is equal to this {@code KerberosTicket}, * false otherwise. * @since 1.6 */ public boolean equals(Object other) { if (other == this) { return true; } if (!(other instanceof KerberosTicket)) { return false; } KerberosTicket otherTicket = ((KerberosTicket) other); if (isDestroyed() || otherTicket.isDestroyed()) { return false; } if (!Arrays.equals(getEncoded(), otherTicket.getEncoded()) || !endTime.equals(otherTicket.getEndTime()) || !server.equals(otherTicket.getServer()) || !client.equals(otherTicket.getClient()) || !sessionKey.equals(otherTicket.sessionKey) || !Arrays.equals(clientAddresses, otherTicket.getClientAddresses()) || !Arrays.equals(flags, otherTicket.getFlags())) { return false; } // authTime may be null if (authTime == null) { if (otherTicket.getAuthTime() != null) { return false; } } else { if (!authTime.equals(otherTicket.getAuthTime())) { return false; } } // startTime may be null if (startTime == null) { if (otherTicket.getStartTime() != null) { return false; } } else { if (!startTime.equals(otherTicket.getStartTime())) { return false; } } if (renewTill == null) { if (otherTicket.getRenewTill() != null) { return false; } } else { if (!renewTill.equals(otherTicket.getRenewTill())) { return false; } } return true; } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); if (sessionKey == null) { throw new InvalidObjectException("Session key cannot be null"); } try { init(asn1Encoding, client, server, sessionKey, flags, authTime, startTime, endTime, renewTill, clientAddresses); } catch (IllegalArgumentException iae) { throw (InvalidObjectException) new InvalidObjectException(iae.getMessage()).initCause(iae); } } }