/*
 * Copyright (c) 2019, 2021, 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.
 *
 * 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.
 */

/*
 * @test
 * @bug 8215032
 * @run main/othervm/timeout=120 -Dsun.security.krb5.debug=true ReferralsTest
 * @summary Test Kerberos cross-realm referrals (RFC 6806)
 */

import java.io.File;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;

import org.ietf.jgss.GSSName;

import sun.security.jgss.GSSUtil;
import sun.security.krb5.Config;
import sun.security.krb5.PrincipalName;

public class ReferralsTest {
    private static final boolean DEBUG = true;
    private static final String krbConfigName = "krb5-localkdc.conf";
    private static final String krbConfigNameNoCanonicalize =
            "krb5-localkdc-nocanonicalize.conf";
    private static final String realmKDC1 = "RABBIT.HOLE";
    private static final String realmKDC2 = "DEV.RABBIT.HOLE";
    private static final char[] password = "123qwe@Z".toCharArray();

    // 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 +
            PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC1;

    // Names + realms
    private static final String clientKDC1Name = clientAlias.replaceAll(
            PrincipalName.NAME_REALM_SEPARATOR_STR, "\\\\" +
            PrincipalName.NAME_REALM_SEPARATOR_STR) +
            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();
            testDelegation();
            testImpersonation();
            testDelegationWithReferrals();
            testNoCanonicalize();
        } finally {
            cleanup();
        }
    }

    private static void initializeKDCs() throws Exception {
        KDC kdc1 = KDC.create(realmKDC1, "localhost", 0, true);
        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(krbtgtKDC2);
        kdc2.addPrincipal(clientKDC2Name, password);
        kdc2.addPrincipal(serviceName, 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,
                        krbtgtKDC2toKDC1}));
        kdc2.setOption(KDC.Option.ALLOW_S4U2PROXY, mapKDC2);

        KDC.saveConfig(krbConfigName, kdc1, kdc2,
                "forwardable=true", "canonicalize=true");
        KDC.saveConfig(krbConfigNameNoCanonicalize, kdc1, kdc2,
                "forwardable=true");
        System.setProperty("java.security.krb5.conf", krbConfigName);
    }

    private static void cleanup() {
        String[] configFiles = new String[]{krbConfigName,
                krbConfigNameNoCanonicalize};
        for (String configFile : configFiles) {
            File f = new File(configFile);
            if (f.exists()) {
                f.delete();
            }
        }
    }

    /*
     * The client subject (whose principal is
     * test@RABBIT.HOLE@RABBIT.HOLE) will obtain a TGT after
     * realm referral and name canonicalization (TGT cname
     * will be test@DEV.RABBIT.HOLE). With this TGT, the client will request
     * a TGS for service http/server.dev.rabbit.hole@RABBIT.HOLE. After
     * realm referral, a http/server.dev.rabbit.hole@DEV.RABBIT.HOLE TGS
     * will be obtained.
     *
     * Assert that we get the proper TGT and TGS tickets, and that they are
     * associated to the client subject.
     *
     * Assert that if we request a TGS for the same service again (based on the
     * original service name), we don't get a new one but the previous,
     * already in the subject credentials.
     */
    private static void testSubjectCredentials() throws Exception {
        Subject clientSubject = new Subject();
        Context clientContext = Context.fromUserPass(clientSubject,
                clientKDC1Name, password, false);

        Set<Principal> clientPrincipals = clientSubject.getPrincipals();
        if (clientPrincipals.size() != 1) {
            throw new Exception("Only one client subject principal expected");
        }
        Principal clientPrincipal = clientPrincipals.iterator().next();
        if (DEBUG) {
            System.out.println("Client subject principal: " +
                    clientPrincipal.getName());
        }
        if (!clientPrincipal.getName().equals(clientKDC1Name)) {
            throw new Exception("Unexpected client subject principal.");
        }

        clientContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
        clientContext.take(new byte[0]);
        Set<KerberosTicket> clientTickets =
                clientSubject.getPrivateCredentials(KerberosTicket.class);
        boolean tgtFound = false;
        boolean tgsFound = false;
        for (KerberosTicket clientTicket : clientTickets) {
            String cname = clientTicket.getClient().getName();
            String sname = clientTicket.getServer().getName();
            if (cname.equals(clientKDC2Name)) {
                if (sname.equals(krbtgtKDC2 +
                        PrincipalName.NAME_REALM_SEPARATOR_STR + realmKDC2)) {
                    tgtFound = true;
                } else if (sname.equals(serviceKDC2Name)) {
                    tgsFound = true;
                }
            }
            if (DEBUG) {
                System.out.println("Client subject KerberosTicket:");
                System.out.println(clientTicket);
            }
        }
        if (!tgtFound || !tgsFound) {
            throw new Exception("client subject tickets (TGT/TGS) not found.");
        }
        int numOfTickets = clientTickets.size();
        clientContext.startAsClient(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
        clientContext.take(new byte[0]);
        clientContext.status();
        int newNumOfTickets =
                clientSubject.getPrivateCredentials(KerberosTicket.class).size();
        if (DEBUG) {
            System.out.println("client subject number of tickets: " +
                    numOfTickets);
            System.out.println("client subject new number of tickets: " +
                    newNumOfTickets);
        }
        if (numOfTickets != newNumOfTickets) {
            throw new Exception("Useless client subject TGS request because" +
                    " TGS was not found in private credentials.");
        }
    }

    /*
     * The server (http/server.dev.rabbit.hole@DEV.RABBIT.HOLE)
     * will authenticate on itself on behalf of the client
     * (test@DEV.RABBIT.HOLE). Cross-realm referrals will occur
     * when requesting different TGTs and TGSs (including the
     * request for delegated credentials).
     */
    private static void testDelegation() throws Exception {
        Context c = Context.fromUserPass(clientKDC2Name,
                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(serviceName, GSSUtil.GSS_KRB5_MECH_OID);
        delegatedContext.x().requestMutualAuth(false);
        Context s2 = Context.fromUserPass(serviceKDC2Name,
                password, true);
        s2.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);

        // Test authentication
        Context.handshake(delegatedContext, s2);
        if (!delegatedContext.x().isEstablished() || !s2.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(clientKDC2Name) ||
                !contextAcceptorName.toString().equals(serviceName)) {
            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 {
        testImpersonationSingle();

        // Try a second time to force the use of the Referrals Cache.
        // During this execution, the referral ticket from RABBIT.HOLE
        // to DEV.RABBIT.HOLE (upon the initial S4U2Self message) will
        // be obtained from the Cache.
        testImpersonationSingle();
    }

    private static void testImpersonationSingle() 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 {
        testDelegationWithReferralsSingle();

        // Try a second time to force the use of the Referrals Cache.
        // During this execution, the referral ticket from RABBIT.HOLE
        // to DEV.RABBIT.HOLE (upon the initial S4U2Proxy message) will
        // be obtained from the Cache.
        testDelegationWithReferralsSingle();
    }

    private static void testDelegationWithReferralsSingle() 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");
        }
    }

    /*
     * The client tries to get a TGT (AS protocol) as in testSubjectCredentials
     * but without the canonicalize setting in krb5.conf. The KDC
     * must not return a referral but a failure because the client
     * is not in the local database.
     */
    private static void testNoCanonicalize() throws Exception {
        System.setProperty("java.security.krb5.conf",
                krbConfigNameNoCanonicalize);
        Config.refresh();
        try {
            Context.fromUserPass(new Subject(),
                    clientKDC1Name, password, false);
            throw new Exception("should not succeed");
        } catch (LoginException e) {
            // expected
        }
    }
}
