Path: blob/master/src/java.security.jgss/share/classes/javax/security/auth/kerberos/KerberosTicket.java
41161 views
/*1* Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package javax.security.auth.kerberos;2627import java.io.*;28import java.util.Date;29import java.util.Arrays;30import java.net.InetAddress;31import java.util.Objects;32import javax.crypto.SecretKey;33import javax.security.auth.Refreshable;34import javax.security.auth.Destroyable;35import javax.security.auth.RefreshFailedException;36import javax.security.auth.DestroyFailedException;3738import sun.security.util.HexDumpEncoder;3940/**41* This class encapsulates a Kerberos ticket and associated42* information as viewed from the client's point of view. It captures all43* information that the Key Distribution Center (KDC) sends to the client44* in the reply message KDC-REP defined in the Kerberos Protocol45* Specification (<a href=http://www.ietf.org/rfc/rfc4120.txt>RFC 4120</a>).46* <p>47* All Kerberos JAAS login modules that authenticate a user to a KDC should48* use this class. Where available, the login module might even read this49* information from a ticket cache in the operating system instead of50* directly communicating with the KDC. During the commit phase of the JAAS51* authentication process, the JAAS login module should instantiate this52* class and store the instance in the private credential set of a53* {@link javax.security.auth.Subject Subject}.<p>54*55* It might be necessary for the application to be granted a56* {@link javax.security.auth.PrivateCredentialPermission57* PrivateCredentialPermission} if it needs to access a {@code KerberosTicket}58* instance from a {@code Subject}. This permission is not needed when the59* application depends on the default JGSS Kerberos mechanism to access the60* {@code KerberosTicket}. In that case, however, the application will need an61* appropriate62* {@link javax.security.auth.kerberos.ServicePermission ServicePermission}.63* <p>64* Note that this class is applicable to both ticket granting tickets and65* other regular service tickets. A ticket granting ticket is just a66* special case of a more generalized service ticket.67*68* @implNote The JAAS login module in the JDK reference implementation destroys69* all tickets after logout.70*71* @see javax.security.auth.Subject72* @see javax.security.auth.PrivateCredentialPermission73* @see javax.security.auth.login.LoginContext74* @see org.ietf.jgss.GSSCredential75* @see org.ietf.jgss.GSSManager76*77* @author Mayank Upadhyay78* @since 1.479*/80public class KerberosTicket implements Destroyable, Refreshable,81java.io.Serializable {8283private static final long serialVersionUID = 7395334370157380539L;8485// XXX Make these flag indices public86private static final int FORWARDABLE_TICKET_FLAG = 1;87private static final int FORWARDED_TICKET_FLAG = 2;88private static final int PROXIABLE_TICKET_FLAG = 3;89private static final int PROXY_TICKET_FLAG = 4;90private static final int POSTDATED_TICKET_FLAG = 6;91private static final int RENEWABLE_TICKET_FLAG = 8;92private static final int INITIAL_TICKET_FLAG = 9;9394private static final int NUM_FLAGS = 32;9596/**97*98* ASN.1 DER Encoding of the Ticket as defined in the99* Kerberos Protocol Specification RFC4120.100*101* @serial102*/103104private byte[] asn1Encoding;105106/**107*{@code KeyImpl} is serialized by writing out the ASN1 Encoded bytes108* of the encryption key. The ASN1 encoding is defined in RFC4120 and as109* follows:110* <pre>111* EncryptionKey ::= SEQUENCE {112* keytype [0] Int32 -- actually encryption type --,113* keyvalue [1] OCTET STRING114* }115* </pre>116*117* @serial118*/119120private KeyImpl sessionKey;121122/**123*124* Ticket Flags as defined in the Kerberos Protocol Specification RFC4120.125*126* @serial127*/128129private boolean[] flags;130131/**132*133* Time of initial authentication134*135* @serial136*/137138private Date authTime;139140/**141*142* Time after which the ticket is valid.143* @serial144*/145private Date startTime;146147/**148*149* Time after which the ticket will not be honored. (its expiration time).150*151* @serial152*/153154private Date endTime;155156/**157*158* For renewable Tickets it indicates the maximum endtime that may be159* included in a renewal. It can be thought of as the absolute expiration160* time for the ticket, including all renewals. This field may be null161* for tickets that are not renewable.162*163* @serial164*/165166private Date renewTill;167168/**169*170* Client that owns the service ticket171*172* @serial173*/174175private KerberosPrincipal client;176177/**178*179* The service for which the ticket was issued.180*181* @serial182*/183184private KerberosPrincipal server;185186/**187*188* The addresses from where the ticket may be used by the client.189* This field may be null when the ticket is usable from any address.190*191* @serial192*/193194private InetAddress[] clientAddresses;195196/**197* Evidence ticket if proxy_impersonator. This field can be accessed198* by KerberosSecrets. It's serialized.199*/200KerberosTicket proxy = null;201202private transient boolean destroyed = false;203204transient KerberosPrincipal clientAlias = null;205206transient KerberosPrincipal serverAlias = null;207208/**209* Constructs a {@code KerberosTicket} using credentials information that a210* client either receives from a KDC or reads from a cache.211*212* @param asn1Encoding the ASN.1 encoding of the ticket as defined by213* the Kerberos protocol specification.214* @param client the client that owns this service215* ticket216* @param server the service that this ticket is for217* @param sessionKey the raw bytes for the session key that must be218* used to encrypt the authenticator that will be sent to the server219* @param keyType the key type for the session key as defined by the220* Kerberos protocol specification.221* @param flags the ticket flags. Each element in this array indicates222* the value for the corresponding bit in the ASN.1 BitString that223* represents the ticket flags. If the number of elements in this array224* is less than the number of flags used by the Kerberos protocol,225* then the missing flags will be filled in with false.226* @param authTime the time of initial authentication for the client227* @param startTime the time after which the ticket will be valid. This228* may be null in which case the value of authTime is treated as the229* startTime.230* @param endTime the time after which the ticket will no longer be231* valid232* @param renewTill an absolute expiration time for the ticket,233* including all renewal that might be possible. This field may be null234* for tickets that are not renewable.235* @param clientAddresses the addresses from where the ticket may be236* used by the client. This field may be null when the ticket is usable237* from any address.238*/239public KerberosTicket(byte[] asn1Encoding,240KerberosPrincipal client,241KerberosPrincipal server,242byte[] sessionKey,243int keyType,244boolean[] flags,245Date authTime,246Date startTime,247Date endTime,248Date renewTill,249InetAddress[] clientAddresses) {250251init(asn1Encoding, client, server, sessionKey, keyType, flags,252authTime, startTime, endTime, renewTill, clientAddresses);253}254255private void init(byte[] asn1Encoding,256KerberosPrincipal client,257KerberosPrincipal server,258byte[] sessionKey,259int keyType,260boolean[] flags,261Date authTime,262Date startTime,263Date endTime,264Date renewTill,265InetAddress[] clientAddresses) {266if (sessionKey == null) {267throw new IllegalArgumentException("Session key for ticket"268+ " cannot be null");269}270init(asn1Encoding, client, server,271new KeyImpl(sessionKey, keyType), flags, authTime,272startTime, endTime, renewTill, clientAddresses);273}274275private void init(byte[] asn1Encoding,276KerberosPrincipal client,277KerberosPrincipal server,278KeyImpl sessionKey,279boolean[] flags,280Date authTime,281Date startTime,282Date endTime,283Date renewTill,284InetAddress[] clientAddresses) {285if (asn1Encoding == null) {286throw new IllegalArgumentException("ASN.1 encoding of ticket"287+ " cannot be null");288}289this.asn1Encoding = asn1Encoding.clone();290291if (client == null) {292throw new IllegalArgumentException("Client name in ticket"293+ " cannot be null");294}295this.client = client;296297if (server == null) {298throw new IllegalArgumentException("Server name in ticket"299+ " cannot be null");300}301this.server = server;302303// Caller needs to make sure `sessionKey` will not be null304this.sessionKey = sessionKey;305306if (flags != null) {307if (flags.length >= NUM_FLAGS) {308this.flags = flags.clone();309} else {310this.flags = new boolean[NUM_FLAGS];311// Fill in whatever we have312for (int i = 0; i < flags.length; i++) {313this.flags[i] = flags[i];314}315}316} else {317this.flags = new boolean[NUM_FLAGS];318}319320if (this.flags[RENEWABLE_TICKET_FLAG] && renewTill != null) {321this.renewTill = new Date(renewTill.getTime());322}323324if (authTime != null) {325this.authTime = new Date(authTime.getTime());326}327if (startTime != null) {328this.startTime = new Date(startTime.getTime());329} else {330this.startTime = this.authTime;331}332333if (endTime == null) {334throw new IllegalArgumentException("End time for ticket validity"335+ " cannot be null");336}337this.endTime = new Date(endTime.getTime());338339if (clientAddresses != null) {340this.clientAddresses = clientAddresses.clone();341}342}343344/**345* Returns the client principal associated with this ticket.346*347* @return the client principal, or {@code null} if destroyed.348*/349public final KerberosPrincipal getClient() {350return client;351}352353/**354* Returns the service principal associated with this ticket.355*356* @return the service principal, or {@code null} if destroyed.357*/358public final KerberosPrincipal getServer() {359return server;360}361362/**363* Returns the session key associated with this ticket. The return value364* is always a {@link EncryptionKey} object.365*366* @return the session key.367* @throws IllegalStateException if this ticket is destroyed368*/369public final SecretKey getSessionKey() {370if (destroyed) {371throw new IllegalStateException("This ticket is no longer valid");372}373return new EncryptionKey(374sessionKey.getEncoded(), sessionKey.getKeyType());375}376377/**378* Returns the key type of the session key associated with this379* ticket as defined by the Kerberos Protocol Specification.380*381* @return the key type of the session key associated with this382* ticket.383* @throws IllegalStateException if this ticket is destroyed384*385* @see #getSessionKey()386*/387public final int getSessionKeyType() {388if (destroyed) {389throw new IllegalStateException("This ticket is no longer valid");390}391return sessionKey.getKeyType();392}393394/**395* Determines if this ticket is forwardable.396*397* @return true if this ticket is forwardable, or false if not forwardable398* or destroyed.399*/400public final boolean isForwardable() {401return flags == null? false: flags[FORWARDABLE_TICKET_FLAG];402}403404/**405* Determines if this ticket had been forwarded or was issued based on406* authentication involving a forwarded ticket-granting ticket.407*408* @return true if this ticket had been forwarded or was issued based on409* authentication involving a forwarded ticket-granting ticket,410* or false otherwise or destroyed.411*/412public final boolean isForwarded() {413return flags == null? false: flags[FORWARDED_TICKET_FLAG];414}415416/**417* Determines if this ticket is proxiable.418*419* @return true if this ticket is proxiable, or false if not proxiable420* or destroyed.421*/422public final boolean isProxiable() {423return flags == null? false: flags[PROXIABLE_TICKET_FLAG];424}425426/**427* Determines is this ticket is a proxy-ticket.428*429* @return true if this ticket is a proxy-ticket, or false if not430* a proxy-ticket or destroyed.431*/432public final boolean isProxy() {433return flags == null? false: flags[PROXY_TICKET_FLAG];434}435436437/**438* Determines is this ticket is post-dated.439*440* @return true if this ticket is post-dated, or false if not post-dated441* or destroyed.442*/443public final boolean isPostdated() {444return flags == null? false: flags[POSTDATED_TICKET_FLAG];445}446447/**448* Determines is this ticket is renewable. If so, the {@link #refresh()449* refresh} method can be called, assuming the validity period for450* renewing is not already over.451*452* @return true if this ticket is renewable, or false if not renewable453* or destroyed.454*/455public final boolean isRenewable() {456return flags == null? false: flags[RENEWABLE_TICKET_FLAG];457}458459/**460* Determines if this ticket was issued using the Kerberos AS-Exchange461* protocol, and not issued based on some ticket-granting ticket.462*463* @return true if this ticket was issued using the Kerberos AS-Exchange464* protocol, or false if not issued this way or destroyed.465*/466public final boolean isInitial() {467return flags == null? false: flags[INITIAL_TICKET_FLAG];468}469470/**471* Returns the flags associated with this ticket. Each element in the472* returned array indicates the value for the corresponding bit in the473* ASN.1 BitString that represents the ticket flags.474*475* @return the flags associated with this ticket, or {@code null}476* if destroyed.477*/478public final boolean[] getFlags() {479return (flags == null? null: flags.clone());480}481482/**483* Returns the time that the client was authenticated.484*485* @return the time that the client was authenticated486* or {@code null} if the field is not set or487* this ticket is destroyed.488*/489public final java.util.Date getAuthTime() {490return (authTime == null) ? null : (Date)authTime.clone();491}492493/**494* Returns the start time for this ticket's validity period.495*496* @return the start time for this ticket's validity period497* or {@code null} if the field is not set or498* this ticket is destroyed.499*/500public final java.util.Date getStartTime() {501return (startTime == null) ? null : (Date)startTime.clone();502}503504/**505* Returns the expiration time for this ticket's validity period.506*507* @return the expiration time for this ticket's validity period,508* or {@code null} if destroyed.509*/510public final java.util.Date getEndTime() {511return (endTime == null) ? null : (Date) endTime.clone();512}513514/**515* Returns the latest expiration time for this ticket, including all516* renewals. This will return a null value for non-renewable tickets.517*518* @return the latest expiration time for this ticket, or {@code null}519* if destroyed.520*/521public final java.util.Date getRenewTill() {522return (renewTill == null) ? null: (Date)renewTill.clone();523}524525/**526* Returns a list of addresses from where the ticket can be used.527*528* @return the list of addresses, or {@code null} if the field was not529* provided or this ticket is destroyed.530*/531public final java.net.InetAddress[] getClientAddresses() {532return (clientAddresses == null) ? null: clientAddresses.clone();533}534535/**536* Returns an ASN.1 encoding of the entire ticket.537*538* @return an ASN.1 encoding of the entire ticket. A new byte539* array is returned each time this method is called.540* @throws IllegalStateException if this ticket is destroyed541*/542public final byte[] getEncoded() {543if (destroyed) {544throw new IllegalStateException("This ticket is no longer valid");545}546return asn1Encoding.clone();547}548549/**550* Determines if this ticket is still current.551*552* @return true if this ticket is still current, or false if not current553* or destroyed.554*/555public boolean isCurrent() {556return endTime == null? false: (System.currentTimeMillis() <= endTime.getTime());557}558559/**560* Extends the validity period of this ticket. The ticket will contain561* a new session key if the refresh operation succeeds. The refresh562* operation will fail if the ticket is not renewable or the latest563* allowable renew time has passed. Any other error returned by the564* KDC will also cause this method to fail.565*566* Note: This method is not synchronized with the accessor567* methods of this object. Hence callers need to be aware of multiple568* threads that might access this and try to renew it at the same569* time.570*571* @throws IllegalStateException if this ticket is destroyed572* @throws RefreshFailedException if the ticket is not renewable, or573* the latest allowable renew time has passed, or the KDC returns some574* error.575*576* @see #isRenewable()577* @see #getRenewTill()578*/579public void refresh() throws RefreshFailedException {580581if (destroyed) {582throw new RefreshFailedException("A destroyed ticket "583+ "cannot be renewd.");584}585if (!isRenewable()) {586throw new RefreshFailedException("This ticket is not renewable");587}588589if (getRenewTill() == null) {590// Renewable ticket without renew-till. Illegal and ignored.591return;592}593594if (System.currentTimeMillis() > getRenewTill().getTime()) {595throw new RefreshFailedException("This ticket is past "596+ "its last renewal time.");597}598Throwable e = null;599sun.security.krb5.Credentials krb5Creds = null;600601try {602krb5Creds = new sun.security.krb5.Credentials(asn1Encoding,603client.getName(),604(clientAlias != null ?605clientAlias.getName() : null),606server.getName(),607(serverAlias != null ?608serverAlias.getName() : null),609sessionKey.getEncoded(),610sessionKey.getKeyType(),611flags,612authTime,613startTime,614endTime,615renewTill,616clientAddresses);617krb5Creds = krb5Creds.renew();618} catch (sun.security.krb5.KrbException krbException) {619e = krbException;620} catch (java.io.IOException ioException) {621e = ioException;622}623624if (e != null) {625RefreshFailedException rfException626= new RefreshFailedException("Failed to renew Kerberos Ticket "627+ "for client " + client628+ " and server " + server629+ " - " + e.getMessage());630rfException.initCause(e);631throw rfException;632}633634/*635* In case multiple threads try to refresh it at the same time.636*/637synchronized (this) {638try {639this.destroy();640} catch (DestroyFailedException dfException) {641// Squelch it since we don't care about the old ticket.642}643init(krb5Creds.getEncoded(),644new KerberosPrincipal(krb5Creds.getClient().getName()),645new KerberosPrincipal(krb5Creds.getServer().getName(),646KerberosPrincipal.KRB_NT_SRV_INST),647krb5Creds.getSessionKey().getBytes(),648krb5Creds.getSessionKey().getEType(),649krb5Creds.getFlags(),650krb5Creds.getAuthTime(),651krb5Creds.getStartTime(),652krb5Creds.getEndTime(),653krb5Creds.getRenewTill(),654krb5Creds.getClientAddresses());655destroyed = false;656}657}658659/**660* Destroys the ticket and destroys any sensitive information stored in661* it.662*/663public void destroy() throws DestroyFailedException {664if (!destroyed) {665Arrays.fill(asn1Encoding, (byte) 0);666client = null;667server = null;668sessionKey.destroy();669flags = null;670authTime = null;671startTime = null;672endTime = null;673renewTill = null;674clientAddresses = null;675destroyed = true;676}677}678679/**680* Determines if this ticket has been destroyed.681*/682public boolean isDestroyed() {683return destroyed;684}685686/**687* Returns an informative textual representation of this {@code KerberosTicket}.688*689* @return an informative textual representation of this {@code KerberosTicket}.690*/691public String toString() {692if (destroyed) {693return "Destroyed KerberosTicket";694}695StringBuilder caddrString = new StringBuilder();696if (clientAddresses != null) {697for (int i = 0; i < clientAddresses.length; i++) {698caddrString.append("clientAddresses[" + i + "] = " +699clientAddresses[i].toString());700}701}702return ("Ticket (hex) = " + "\n" +703(new HexDumpEncoder()).encodeBuffer(asn1Encoding) + "\n" +704"Client Principal = " + client.toString() + "\n" +705"Server Principal = " + server.toString() + "\n" +706"Session Key = " + sessionKey.toString() + "\n" +707"Forwardable Ticket " + flags[FORWARDABLE_TICKET_FLAG] + "\n" +708"Forwarded Ticket " + flags[FORWARDED_TICKET_FLAG] + "\n" +709"Proxiable Ticket " + flags[PROXIABLE_TICKET_FLAG] + "\n" +710"Proxy Ticket " + flags[PROXY_TICKET_FLAG] + "\n" +711"Postdated Ticket " + flags[POSTDATED_TICKET_FLAG] + "\n" +712"Renewable Ticket " + flags[RENEWABLE_TICKET_FLAG] + "\n" +713"Initial Ticket " + flags[INITIAL_TICKET_FLAG] + "\n" +714"Auth Time = " + String.valueOf(authTime) + "\n" +715"Start Time = " + String.valueOf(startTime) + "\n" +716"End Time = " + endTime.toString() + "\n" +717"Renew Till = " + String.valueOf(renewTill) + "\n" +718"Client Addresses " +719(clientAddresses == null ? " Null " : caddrString.toString() +720(proxy == null ? "" : "\nwith a proxy ticket") +721"\n"));722}723724/**725* Returns a hash code for this {@code KerberosTicket}.726*727* @return a hash code for this {@code KerberosTicket}.728* @since 1.6729*/730public int hashCode() {731int result = 17;732if (isDestroyed()) {733return result;734}735result = result * 37 + Arrays.hashCode(getEncoded());736result = result * 37 + endTime.hashCode();737result = result * 37 + client.hashCode();738result = result * 37 + server.hashCode();739result = result * 37 + sessionKey.hashCode();740741// authTime may be null742if (authTime != null) {743result = result * 37 + authTime.hashCode();744}745746// startTime may be null747if (startTime != null) {748result = result * 37 + startTime.hashCode();749}750751// renewTill may be null752if (renewTill != null) {753result = result * 37 + renewTill.hashCode();754}755756// clientAddress may be null, the array's hashCode is 0757result = result * 37 + Arrays.hashCode(clientAddresses);758759if (proxy != null) {760result = result * 37 + proxy.hashCode();761}762return result * 37 + Arrays.hashCode(flags);763}764765/**766* Compares the specified object with this {@code KerberosTicket} for equality.767* Returns true if the given object is also a768* {@code KerberosTicket} and the two769* {@code KerberosTicket} instances are equivalent.770* A destroyed {@code KerberosTicket} object is only equal to itself.771*772* @param other the object to compare to773* @return true if the specified object is equal to this {@code KerberosTicket},774* false otherwise.775* @since 1.6776*/777public boolean equals(Object other) {778779if (other == this) {780return true;781}782783if (! (other instanceof KerberosTicket)) {784return false;785}786787KerberosTicket otherTicket = ((KerberosTicket) other);788if (isDestroyed() || otherTicket.isDestroyed()) {789return false;790}791792if (!Arrays.equals(getEncoded(), otherTicket.getEncoded()) ||793!endTime.equals(otherTicket.getEndTime()) ||794!server.equals(otherTicket.getServer()) ||795!client.equals(otherTicket.getClient()) ||796!sessionKey.equals(otherTicket.sessionKey) ||797!Arrays.equals(clientAddresses, otherTicket.getClientAddresses()) ||798!Arrays.equals(flags, otherTicket.getFlags())) {799return false;800}801802// authTime may be null803if (authTime == null) {804if (otherTicket.getAuthTime() != null) {805return false;806}807} else {808if (!authTime.equals(otherTicket.getAuthTime())) {809return false;810}811}812813// startTime may be null814if (startTime == null) {815if (otherTicket.getStartTime() != null) {816return false;817}818} else {819if (!startTime.equals(otherTicket.getStartTime())) {820return false;821}822}823824if (renewTill == null) {825if (otherTicket.getRenewTill() != null) {826return false;827}828} else {829if (!renewTill.equals(otherTicket.getRenewTill())) {830return false;831}832}833834if (!Objects.equals(proxy, otherTicket.proxy)) {835return false;836}837838return true;839}840841/**842* Restores the state of this object from the stream.843*844* @param s the {@code ObjectInputStream} from which data is read845* @throws IOException if an I/O error occurs846* @throws ClassNotFoundException if a serialized class cannot be loaded847*/848private void readObject(ObjectInputStream s)849throws IOException, ClassNotFoundException {850s.defaultReadObject();851if (sessionKey == null) {852throw new InvalidObjectException("Session key cannot be null");853}854try {855init(asn1Encoding, client, server, sessionKey,856flags, authTime, startTime, endTime,857renewTill, clientAddresses);858} catch (IllegalArgumentException iae) {859throw (InvalidObjectException)860new InvalidObjectException(iae.getMessage()).initCause(iae);861}862}863}864865866