changeset 57428:a2b03207a7f9

8005819: Support cross-realm MSSFU Reviewed-by: weijun
author mbalao
date Wed, 11 Dec 2019 15:43:42 -0300
parents 2b0185471185
children d6a38e8f7389
files src/java.security.jgss/share/classes/sun/security/krb5/KrbTgsRep.java src/java.security.jgss/share/classes/sun/security/krb5/internal/CredentialsUtil.java src/java.security.jgss/share/classes/sun/security/krb5/internal/Krb5.java src/java.security.jgss/share/classes/sun/security/krb5/internal/PAForUserEnc.java src/java.security.jgss/share/classes/sun/security/krb5/internal/PaPacOptions.java test/jdk/sun/security/krb5/auto/KDC.java test/jdk/sun/security/krb5/auto/ReferralsTest.java
diffstat 7 files changed, 489 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.security.jgss/share/classes/sun/security/krb5/KrbTgsRep.java	Fri Dec 13 02:49:52 2019 +0100
+++ b/src/java.security.jgss/share/classes/sun/security/krb5/KrbTgsRep.java	Wed Dec 11 15:43:42 2019 -0300
@@ -95,9 +95,16 @@
             }
         }
 
+        PrincipalName clientAlias = null;
+        if (rep.cname.equals(req.reqBody.cname)) {
+            // Only propagate the client alias if it is not an
+            // impersonation ticket (S4U2Self or S4U2Proxy).
+            clientAlias = tgsReq.getClientAlias();
+        }
+
         this.creds = new Credentials(rep.ticket,
                                 rep.cname,
-                                tgsReq.getClientAlias(),
+                                clientAlias,
                                 enc_part.sname,
                                 serverAlias,
                                 enc_part.key,
--- a/src/java.security.jgss/share/classes/sun/security/krb5/internal/CredentialsUtil.java	Fri Dec 13 02:49:52 2019 +0100
+++ b/src/java.security.jgss/share/classes/sun/security/krb5/internal/CredentialsUtil.java	Wed Dec 11 15:43:42 2019 -0300
@@ -32,6 +32,8 @@
 package sun.security.krb5.internal;
 
 import sun.security.krb5.*;
+import sun.security.util.DerValue;
+
 import java.io.IOException;
 import java.util.LinkedList;
 import java.util.List;
@@ -45,6 +47,10 @@
 
     private static boolean DEBUG = sun.security.krb5.internal.Krb5.DEBUG;
 
+    private static enum S4U2Type {
+        NONE, SELF, PROXY
+    }
+
     /**
      * Used by a middle server to acquire credentials on behalf of a
      * client to itself using the S4U2self extension.
@@ -54,20 +60,44 @@
      */
     public static Credentials acquireS4U2selfCreds(PrincipalName client,
             Credentials ccreds) throws KrbException, IOException {
+        if (!ccreds.isForwardable()) {
+            throw new KrbException("S4U2self needs a FORWARDABLE ticket");
+        }
+        PrincipalName sname = ccreds.getClient();
         String uRealm = client.getRealmString();
         String localRealm = ccreds.getClient().getRealmString();
         if (!uRealm.equals(localRealm)) {
-            // TODO: we do not support kerberos referral now
-            throw new KrbException("Cross realm impersonation not supported");
+            // Referrals will be required because the middle service
+            // and the client impersonated are on different realms.
+            if (Config.DISABLE_REFERRALS) {
+                throw new KrbException("Cross-realm S4U2Self request not" +
+                        " possible when referrals are disabled.");
+            }
+            if (ccreds.getClientAlias() != null) {
+                // If the name was canonicalized, the user pick
+                // has preference. This gives the possibility of
+                // using FQDNs that KDCs may use to return referrals.
+                // I.e.: a SVC/host.realm-2.com@REALM-1.COM name
+                // may be used by REALM-1.COM KDC to return a
+                // referral to REALM-2.COM.
+                sname = ccreds.getClientAlias();
+            }
+            sname = new PrincipalName(sname.getNameType(),
+                    sname.getNameStrings(), new Realm(uRealm));
         }
-        if (!ccreds.isForwardable()) {
-            throw new KrbException("S4U2self needs a FORWARDABLE ticket");
-        }
-        Credentials creds = serviceCreds(KDCOptions.with(KDCOptions.FORWARDABLE),
-                ccreds, ccreds.getClient(), ccreds.getClient(), null,
-                new PAData[] {new PAData(Krb5.PA_FOR_USER,
-                        new PAForUserEnc(client,
-                            ccreds.getSessionKey()).asn1Encode())});
+        Credentials creds = serviceCreds(
+                KDCOptions.with(KDCOptions.FORWARDABLE),
+                ccreds, ccreds.getClient(), sname, null,
+                new PAData[] {
+                        new PAData(Krb5.PA_FOR_USER,
+                                new PAForUserEnc(client,
+                                        ccreds.getSessionKey()).asn1Encode()),
+                        new PAData(Krb5.PA_PAC_OPTIONS,
+                                new PaPacOptions()
+                                        .setResourceBasedConstrainedDelegation(true)
+                                        .setClaims(true)
+                                        .asn1Encode())
+                        }, S4U2Type.SELF);
         if (!creds.getClient().equals(client)) {
             throw new KrbException("S4U2self request not honored by KDC");
         }
@@ -89,10 +119,31 @@
                 String backend, Ticket second,
                 PrincipalName client, Credentials ccreds)
             throws KrbException, IOException {
+        PrincipalName backendPrincipal = new PrincipalName(backend);
+        String backendRealm = backendPrincipal.getRealmString();
+        String localRealm = ccreds.getClient().getRealmString();
+        if (!backendRealm.equals(localRealm)) {
+            // The middle service and the backend service are on
+            // different realms, so referrals will be required.
+            if (Config.DISABLE_REFERRALS) {
+                throw new KrbException("Cross-realm S4U2Proxy request not" +
+                        " possible when referrals are disabled.");
+            }
+            backendPrincipal = new PrincipalName(
+                    backendPrincipal.getNameType(),
+                    backendPrincipal.getNameStrings(),
+                    new Realm(localRealm));
+        }
         Credentials creds = serviceCreds(KDCOptions.with(
                 KDCOptions.CNAME_IN_ADDL_TKT, KDCOptions.FORWARDABLE),
-                ccreds, ccreds.getClient(), new PrincipalName(backend),
-                new Ticket[] {second}, null);
+                ccreds, ccreds.getClient(), backendPrincipal,
+                new Ticket[] {second}, new PAData[] {
+                        new PAData(Krb5.PA_PAC_OPTIONS,
+                                new PaPacOptions()
+                                        .setResourceBasedConstrainedDelegation(true)
+                                        .setClaims(true)
+                                        .asn1Encode())
+                        }, S4U2Type.PROXY);
         if (!creds.getClient().equals(client)) {
             throw new KrbException("S4U2proxy request not honored by KDC");
         }
@@ -261,7 +312,8 @@
             PrincipalName service, Credentials ccreds)
             throws KrbException, IOException {
         return serviceCreds(new KDCOptions(), ccreds,
-                ccreds.getClient(), service, null, null);
+                ccreds.getClient(), service, null, null,
+                S4U2Type.NONE);
     }
 
     /*
@@ -273,20 +325,21 @@
     private static Credentials serviceCreds(
             KDCOptions options, Credentials asCreds,
             PrincipalName cname, PrincipalName sname,
-            Ticket[] additionalTickets, PAData[] extraPAs)
+            Ticket[] additionalTickets, PAData[] extraPAs,
+            S4U2Type s4u2Type)
             throws KrbException, IOException {
         if (!Config.DISABLE_REFERRALS) {
             try {
-                return serviceCredsReferrals(options, asCreds,
-                        cname, sname, additionalTickets, extraPAs);
+                return serviceCredsReferrals(options, asCreds, cname, sname,
+                        s4u2Type, additionalTickets, extraPAs);
             } catch (KrbException e) {
                 // Server may raise an error if CANONICALIZE is true.
                 // Try CANONICALIZE false.
             }
         }
         return serviceCredsSingle(options, asCreds, cname,
-                asCreds.getClientAlias(), sname, sname, additionalTickets,
-                extraPAs);
+                asCreds.getClientAlias(), sname, sname, s4u2Type,
+                additionalTickets, extraPAs);
     }
 
     /*
@@ -296,8 +349,9 @@
     private static Credentials serviceCredsReferrals(
             KDCOptions options, Credentials asCreds,
             PrincipalName cname, PrincipalName sname,
-            Ticket[] additionalTickets, PAData[] extraPAs)
-            throws KrbException, IOException {
+            S4U2Type s4u2Type, Ticket[] additionalTickets,
+            PAData[] extraPAs)
+                    throws KrbException, IOException {
         options = new KDCOptions(options.toBooleanArray());
         options.set(KDCOptions.CANONICALIZE, true);
         PrincipalName cSname = sname;
@@ -312,34 +366,58 @@
             String toRealm = null;
             if (ref == null) {
                 creds = serviceCredsSingle(options, asCreds, cname,
-                        clientAlias, refSname, cSname, additionalTickets,
-                        extraPAs);
+                        clientAlias, refSname, cSname, s4u2Type,
+                        additionalTickets, extraPAs);
                 PrincipalName server = creds.getServer();
                 if (!refSname.equals(server)) {
                     String[] serverNameStrings = server.getNameStrings();
                     if (serverNameStrings.length == 2 &&
                         serverNameStrings[0].equals(
                                 PrincipalName.TGS_DEFAULT_SRV_NAME) &&
-                        !refSname.getRealmAsString().equals(serverNameStrings[1])) {
+                        !refSname.getRealmAsString().equals(
+                                serverNameStrings[1])) {
                         // Server Name (sname) has the following format:
                         //      krbtgt/TO-REALM.COM@FROM-REALM.COM
-                        ReferralsCache.put(cname, sname, server.getRealmString(),
-                                serverNameStrings[1], creds);
+                        if (s4u2Type == S4U2Type.NONE) {
+                            // Do not store S4U2Self or S4U2Proxy referral
+                            // TGTs in the cache. Caching such tickets is not
+                            // defined in MS-SFU and may cause unexpected
+                            // results when using them in a different context.
+                            ReferralsCache.put(cname, sname,
+                                    server.getRealmString(),
+                                    serverNameStrings[1], creds);
+                        }
                         toRealm = serverNameStrings[1];
                         isReferral = true;
-                        asCreds = creds;
                     }
                 }
             } else {
+                creds = ref.getCreds();
                 toRealm = ref.getToRealm();
-                asCreds = ref.getCreds();
                 isReferral = true;
             }
             if (isReferral) {
+                if (s4u2Type == S4U2Type.PROXY) {
+                    Credentials[] credsInOut =
+                            new Credentials[] {creds, null};
+                    toRealm = handleS4U2ProxyReferral(asCreds,
+                            credsInOut, sname);
+                    creds = credsInOut[0];
+                    if (additionalTickets == null ||
+                            additionalTickets.length == 0 ||
+                            credsInOut[1] == null) {
+                        throw new KrbException("Additional tickets expected" +
+                                " for S4U2Proxy.");
+                    }
+                    additionalTickets[0] = credsInOut[1].getTicket();
+                } else if (s4u2Type == S4U2Type.SELF) {
+                    handleS4U2SelfReferral(extraPAs, asCreds, creds);
+                }
                 if (referrals.contains(toRealm)) {
                     // Referrals loop detected
                     return null;
                 }
+                asCreds = creds;
                 refSname = new PrincipalName(refSname.getNameString(),
                         refSname.getNameType(), toRealm);
                 referrals.add(toRealm);
@@ -362,8 +440,9 @@
             KDCOptions options, Credentials asCreds,
             PrincipalName cname, PrincipalName clientAlias,
             PrincipalName refSname, PrincipalName sname,
-            Ticket[] additionalTickets, PAData[] extraPAs)
-            throws KrbException, IOException {
+            S4U2Type s4u2Type, Ticket[] additionalTickets,
+            PAData[] extraPAs)
+                    throws KrbException, IOException {
         Credentials theCreds = null;
         boolean[] okAsDelegate = new boolean[]{true};
         String[] serverAsCredsNames = asCreds.getServer().getNameStrings();
@@ -389,6 +468,9 @@
                         " serviceCredsSingle: ");
                 Credentials.printDebug(newTgt);
             }
+            if (s4u2Type == S4U2Type.SELF) {
+                handleS4U2SelfReferral(extraPAs, asCreds, newTgt);
+            }
             asCreds = newTgt;
             cname = asCreds.getClient();
         } else if (DEBUG) {
@@ -409,4 +491,79 @@
         }
         return theCreds;
     }
+
+    /**
+     * PA-FOR-USER may need to be regenerated if credentials
+     * change. This may happen when obtaining a TGT for a
+     * different realm or when using a referral TGT.
+     */
+    private static void handleS4U2SelfReferral(PAData[] pas,
+            Credentials oldCeds, Credentials newCreds)
+                    throws Asn1Exception, KrbException, IOException {
+        if (DEBUG) {
+            System.out.println(">>> Handling S4U2Self referral");
+        }
+        for (int i = 0; i < pas.length; i++) {
+            PAData pa = pas[i];
+            if (pa.getType() == Krb5.PA_FOR_USER) {
+                PAForUserEnc paForUser = new PAForUserEnc(
+                        new DerValue(pa.getValue()),
+                        oldCeds.getSessionKey());
+                pas[i] = new PAData(Krb5.PA_FOR_USER,
+                        new PAForUserEnc(paForUser.getName(),
+                                newCreds.getSessionKey()).asn1Encode());
+                break;
+            }
+        }
+    }
+
+    /**
+     * This method is called after receiving the first realm referral for
+     * a S4U2Proxy request. The credentials and tickets needed for the
+     * final S4U2Proxy request (in the referrals chain) are returned.
+     *
+     * Referrals are handled as described by MS-SFU (section 3.1.5.2.2
+     * Receives Referral).
+     *
+     * @param asCreds middle service credentials used for the first S4U2Proxy
+     *        request
+     * @param credsInOut (in/out parameter):
+     *         * input: first S4U2Proxy referral TGT received, null
+     *         * output: referral TGT for final S4U2Proxy service request,
+     *                   client referral TGT for final S4U2Proxy service request
+     *                   (to be sent as additional-ticket)
+     * @param sname the backend service name
+     * @param additionalTickets (out parameter): the additional ticket for the
+     *        last S4U2Proxy request is returned
+     * @return the backend realm for the last S4U2Proxy request
+     */
+    private static String handleS4U2ProxyReferral(Credentials asCreds,
+            Credentials[] credsInOut, PrincipalName sname)
+                    throws KrbException, IOException {
+        if (DEBUG) {
+            System.out.println(">>> Handling S4U2Proxy referral");
+        }
+        Credentials refTGT = null;
+        // Get a credential for the middle service to the backend so we know
+        // the backend realm, as described in MS-SFU (section 3.1.5.2.2).
+        Credentials middleSvcCredsInBackendRealm =
+                serviceCreds(sname, asCreds);
+        String backendRealm =
+                middleSvcCredsInBackendRealm.getServer().getRealmString();
+        String toRealm = credsInOut[0].getServer().getNameStrings()[1];
+        if (!toRealm.equals(backendRealm)) {
+            // More than 1 hop. Follow the referrals chain and obtain a
+            // TGT for the backend realm.
+            refTGT = getTGTforRealm(toRealm, backendRealm, credsInOut[0],
+                    new boolean[1]);
+        } else {
+            // There was only 1 hop. The referral TGT received is already
+            // for the backend realm.
+            refTGT = credsInOut[0];
+        }
+        credsInOut[0] = getTGTforRealm(asCreds.getClient().getRealmString(),
+                backendRealm, asCreds, new boolean[1]);
+        credsInOut[1] = refTGT;
+        return backendRealm;
+    }
 }
--- a/src/java.security.jgss/share/classes/sun/security/krb5/internal/Krb5.java	Fri Dec 13 02:49:52 2019 +0100
+++ b/src/java.security.jgss/share/classes/sun/security/krb5/internal/Krb5.java	Wed Dec 11 15:43:42 2019 -0300
@@ -165,6 +165,7 @@
 
     // S4U2user info
     public static final int PA_FOR_USER      = 129;
+    public static final int PA_PAC_OPTIONS   = 167;
 
     // FAST (RFC 6806)
     public static final int PA_REQ_ENC_PA_REP = 149;
--- a/src/java.security.jgss/share/classes/sun/security/krb5/internal/PAForUserEnc.java	Fri Dec 13 02:49:52 2019 +0100
+++ b/src/java.security.jgss/share/classes/sun/security/krb5/internal/PAForUserEnc.java	Wed Dec 11 15:43:42 2019 -0300
@@ -181,6 +181,10 @@
         return output;
     }
 
+    public PrincipalName getName() {
+        return name;
+    }
+
     public String toString() {
         return "PA-FOR-USER: " + name;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.security.jgss/share/classes/sun/security/krb5/internal/PaPacOptions.java	Wed Dec 11 15:43:42 2019 -0300
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc.
+ * 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 sun.security.krb5.internal;
+
+import java.io.IOException;
+import sun.security.krb5.Asn1Exception;
+import sun.security.krb5.internal.util.KerberosFlags;
+import sun.security.util.DerOutputStream;
+import sun.security.util.DerValue;
+
+/**
+ * Implements the ASN.1 PA-PAC-OPTIONS type.
+ *
+ * <pre>{@code
+ * PA-PAC-OPTIONS ::= SEQUENCE {
+ * KerberosFlags
+ *   -- Claims (0)
+ *   -- Branch Aware (1)
+ *   -- Forward to Full DC (2)
+ * }
+ * Note: KerberosFlags   ::= BIT STRING (SIZE (32..MAX))
+ *         -- minimum number of bits shall be sent, but no fewer than 32
+ *
+ * PA-PAC-OPTIONS ::= KerberosFlags
+ *   -- resource-based constrained delegation (3)
+ * }</pre>
+ *
+ * This definition reflects MS-KILE (section 2.2.10)
+ * and MS-SFU (section 2.2.5).
+ */
+
+public class PaPacOptions {
+
+    private static final int CLAIMS = 0;
+    private static final int BRANCH_AWARE = 1;
+    private static final int FORWARD_TO_FULL_DC = 2;
+    private static final int RESOURCE_BASED_CONSTRAINED_DELEGATION = 3;
+
+    private KerberosFlags flags;
+
+    public PaPacOptions() {
+        this.flags = new KerberosFlags(Krb5.AP_OPTS_MAX + 1);
+    }
+
+    /**
+     * Constructs a PA-PAC-OPTIONS object from a DER encoding.
+     * @param encoding the ASN.1 encoded input
+     * @throws Asn1Exception if invalid DER
+     * @throws IOException if there is an error reading the DER value
+     */
+    public PaPacOptions(DerValue encoding) throws Asn1Exception, IOException {
+        if (encoding.getTag() != DerValue.tag_Sequence) {
+            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
+        }
+
+        DerValue der = encoding.getData().getDerValue();
+        if ((der.getTag() & 0x1F) == 0x00) {
+            flags = new KDCOptions(
+                    der.getData().getDerValue());
+        } else {
+            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
+        }
+    }
+
+    /**
+     * Setter for the claims flag
+     * @param value whether the claims flag is set or not
+     * @return the same PaPacOptions instance
+     */
+    public PaPacOptions setClaims(boolean value) {
+        flags.set(CLAIMS, value);
+        return this;
+    }
+
+    /**
+     * Getter for the claims flag
+     * @return the claims flag value
+     */
+    public boolean getClaims() {
+        return flags.get(CLAIMS);
+    }
+
+    /**
+     * Setter for the branch-aware flag
+     * @param value whether the branch-aware flag is set or not
+     * @return the same PaPacOptions instance
+     */
+    public PaPacOptions setBranchAware(boolean value) {
+        flags.set(BRANCH_AWARE, value);
+        return this;
+    }
+
+    /**
+     * Getter for the branch-aware flag
+     * @return the branch-aware flag value
+     */
+    public boolean getBranchAware() {
+        return flags.get(BRANCH_AWARE);
+    }
+
+    /**
+     * Setter for the forward-to-full-DC flag
+     * @param value whether the forward-to-full-DC flag is set or not
+     * @return the same PaPacOptions instance
+     */
+    public PaPacOptions setForwardToFullDC(boolean value) {
+        flags.set(FORWARD_TO_FULL_DC, value);
+        return this;
+    }
+
+    /**
+     * Getter for the forward-to-full-DC flag
+     * @return the forward-to-full-DC flag value
+     */
+    public boolean getForwardToFullDC() {
+        return flags.get(FORWARD_TO_FULL_DC);
+    }
+
+    /**
+     * Setter for the resource-based-constrained-delegation flag
+     * @param value whether the resource-based-constrained-delegation
+     *        is set or not
+     * @return the same PaPacOptions instance
+     */
+    public PaPacOptions setResourceBasedConstrainedDelegation(boolean value) {
+        flags.set(RESOURCE_BASED_CONSTRAINED_DELEGATION, value);
+        return this;
+    }
+
+    /**
+     * Getter for the resource-based-constrained-delegation flag
+     * @return the resource-based-constrained-delegation flag value
+     */
+    public boolean getResourceBasedConstrainedDelegation() {
+        return flags.get(RESOURCE_BASED_CONSTRAINED_DELEGATION);
+    }
+
+    /**
+     * Encodes this PaPacOptions instance.
+     * @return an ASN.1 encoded PaPacOptions byte array
+     * @throws IOException if an I/O error occurs while encoding this
+     *         PaPacOptions instance
+     */
+    public byte[] asn1Encode() throws IOException {
+        byte[] bytes = null;
+        try(DerOutputStream temp = new DerOutputStream()) {
+            temp.write(
+                    DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)0x00),
+                    flags.asn1Encode());
+            bytes = temp.toByteArray();
+        }
+        try(DerOutputStream temp = new DerOutputStream()) {
+            temp.write(DerValue.tag_Sequence, bytes);
+            return temp.toByteArray();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return flags.toString();
+    }
+}
--- a/test/jdk/sun/security/krb5/auto/KDC.java	Fri Dec 13 02:49:52 2019 +0100
+++ b/test/jdk/sun/security/krb5/auto/KDC.java	Wed Dec 11 15:43:42 2019 -0300
@@ -923,8 +923,7 @@
             if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) {
                 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true;
             }
-            if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT) &&
-                    !isReferral) {
+            if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT)) {
                 if (!options.containsKey(Option.ALLOW_S4U2PROXY)) {
                     // Don't understand CNAME_IN_ADDL_TKT
                     throw new KrbException(Krb5.KDC_ERR_BADOPTION);
--- a/test/jdk/sun/security/krb5/auto/ReferralsTest.java	Fri Dec 13 02:49:52 2019 +0100
+++ b/test/jdk/sun/security/krb5/auto/ReferralsTest.java	Wed Dec 11 15:43:42 2019 -0300
@@ -53,9 +53,13 @@
 
     // Names
     private static final String clientName = "test";
+    private static final String userName = "user";
     private static final String serviceName = "http" +
             PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
             "server.dev.rabbit.hole";
+    private static final String backendServiceName = "cifs" +
+            PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
+            "backend.rabbit.hole";
 
     // Alias
     private static final String clientAlias = clientName +
@@ -68,14 +72,34 @@
             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
     private static final String clientKDC2Name = clientName +
             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
+    private static final String userKDC1Name = userName +
+            PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
     private static final String serviceKDC2Name = serviceName +
             PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
+    private static final String backendKDC1Name = backendServiceName +
+            PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
+    private static final String krbtgtKDC1 =
+            PrincipalName.TGS_DEFAULT_SRV_NAME +
+            PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1;
+    private static final String krbtgtKDC2 =
+            PrincipalName.TGS_DEFAULT_SRV_NAME +
+            PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2;
+    private static final String krbtgtKDC1toKDC2 =
+            PrincipalName.TGS_DEFAULT_SRV_NAME +
+            PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2 +
+            PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;
+    private static final String krbtgtKDC2toKDC1 =
+            PrincipalName.TGS_DEFAULT_SRV_NAME +
+            PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1 +
+            PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2;
 
     public static void main(String[] args) throws Exception {
         try {
             initializeKDCs();
             testSubjectCredentials();
-            testDelegated();
+            testDelegation();
+            testImpersonation();
+            testDelegationWithReferrals();
         } finally {
             cleanup();
         }
@@ -83,36 +107,35 @@
 
     private static void initializeKDCs() throws Exception {
         KDC kdc1 = KDC.create(realmKDC1, "localhost", 0, true);
-        kdc1.addPrincipalRandKey(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1);
-        kdc1.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1 +
-                PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2,
-                password);
-        kdc1.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2,
-                password);
+        kdc1.addPrincipalRandKey(krbtgtKDC1);
+        kdc1.addPrincipal(krbtgtKDC2toKDC1, password);
+        kdc1.addPrincipal(krbtgtKDC2, password);
+        kdc1.addPrincipal(userKDC1Name, password);
+        kdc1.addPrincipal(backendServiceName, password);
 
         KDC kdc2 = KDC.create(realmKDC2, "localhost", 0, true);
-        kdc2.addPrincipalRandKey(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2);
+        kdc2.addPrincipalRandKey(krbtgtKDC2);
         kdc2.addPrincipal(clientKDC2Name, password);
         kdc2.addPrincipal(serviceName, password);
-        kdc2.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC1,
-                password);
-        kdc2.addPrincipal(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                PrincipalName.NAME_COMPONENT_SEPARATOR_STR + realmKDC2 +
-                PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1,
-                password);
+        kdc2.addPrincipal(krbtgtKDC1, password);
+        kdc2.addPrincipal(krbtgtKDC1toKDC2, password);
 
         kdc1.registerAlias(clientAlias, kdc2);
         kdc1.registerAlias(serviceName, kdc2);
         kdc2.registerAlias(clientAlias, clientKDC2Name);
+        kdc2.registerAlias(backendServiceName, kdc1);
+
+        kdc1.setOption(KDC.Option.ALLOW_S4U2SELF, Arrays.asList(
+                new String[]{serviceName + "@" + realmKDC2}));
+        Map<String,List<String>> mapKDC1 = new HashMap<>();
+        mapKDC1.put(serviceName + "@" + realmKDC2, Arrays.asList(
+                new String[]{backendKDC1Name}));
+        kdc1.setOption(KDC.Option.ALLOW_S4U2PROXY, mapKDC1);
 
         Map<String,List<String>> mapKDC2 = new HashMap<>();
         mapKDC2.put(serviceName + "@" + realmKDC2, Arrays.asList(
-                new String[]{serviceName + "@" + realmKDC2}));
+                new String[]{serviceName + "@" + realmKDC2,
+                        krbtgtKDC2toKDC1}));
         kdc2.setOption(KDC.Option.ALLOW_S4U2PROXY, mapKDC2);
 
         KDC.saveConfig(krbConfigName, kdc1, kdc2,
@@ -171,10 +194,8 @@
             String cname = clientTicket.getClient().getName();
             String sname = clientTicket.getServer().getName();
             if (cname.equals(clientKDC2Name)) {
-                if (sname.equals(PrincipalName.TGS_DEFAULT_SRV_NAME +
-                        PrincipalName.NAME_COMPONENT_SEPARATOR_STR +
-                        realmKDC2 + PrincipalName.NAME_REALM_SEPARATOR_STR +
-                        realmKDC2)) {
+                if (sname.equals(krbtgtKDC2 +
+                        PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2)) {
                     tgtFound = true;
                 } else if (sname.equals(serviceKDC2Name)) {
                     tgsFound = true;
@@ -213,7 +234,7 @@
      * when requesting different TGTs and TGSs (including the
      * request for delegated credentials).
      */
-    private static void testDelegated() throws Exception {
+    private static void testDelegation() throws Exception {
         Context c = Context.fromUserPass(clientKDC2Name,
                 password, false);
         c.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
@@ -246,4 +267,62 @@
             throw new Exception("Unexpected initiator or acceptor names");
         }
     }
+
+    /*
+     * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
+     * will get a TGS ticket for itself on behalf of the client
+     * (user@RABBIT.HOLE). Cross-realm referrals will be handled
+     * in S4U2Self requests because the user and the server are
+     * on different realms.
+     */
+    private static void testImpersonation() throws Exception {
+        Context s = Context.fromUserPass(serviceKDC2Name, password, true);
+        s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
+        GSSName impName = s.impersonate(userKDC1Name).cred().getName();
+        if (DEBUG) {
+            System.out.println("Impersonated name: " + impName);
+        }
+        if (!impName.toString().equals(userKDC1Name)) {
+            throw new Exception("Unexpected impersonated name");
+        }
+    }
+
+    /*
+     * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
+     * will use delegated credentials (user@RABBIT.HOLE) to
+     * authenticate in the backend (cifs/backend.rabbit.hole@RABBIT.HOLE).
+     * Cross-realm referrals will be handled in S4U2Proxy requests
+     * because the server and the backend are on different realms.
+     */
+    private static void testDelegationWithReferrals() throws Exception {
+        Context c = Context.fromUserPass(userKDC1Name, password, false);
+        c.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
+        Context s = Context.fromUserPass(serviceKDC2Name, password, true);
+        s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
+        Context.handshake(c, s);
+        Context delegatedContext = s.delegated();
+        delegatedContext.startAsClient(backendServiceName,
+                GSSUtil.GSS_KRB5_MECH_OID);
+        delegatedContext.x().requestMutualAuth(false);
+        Context b = Context.fromUserPass(backendKDC1Name, password, true);
+        b.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
+
+        // Test authentication
+        Context.handshake(delegatedContext, b);
+        if (!delegatedContext.x().isEstablished() || !b.x().isEstablished()) {
+            throw new Exception("Delegated authentication failed");
+        }
+
+        // Test identities
+        GSSName contextInitiatorName = delegatedContext.x().getSrcName();
+        GSSName contextAcceptorName = delegatedContext.x().getTargName();
+        if (DEBUG) {
+            System.out.println("Context initiator: " + contextInitiatorName);
+            System.out.println("Context acceptor: " + contextAcceptorName);
+        }
+        if (!contextInitiatorName.toString().equals(userKDC1Name) ||
+                !contextAcceptorName.toString().equals(backendServiceName)) {
+            throw new Exception("Unexpected initiator or acceptor names");
+        }
+    }
 }