/*
 * Copyright (c) 2000, 2015, 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.
 *
 * 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 test.java.security.cert;

import static org.testng.Assert.assertEquals;
import static sun.security.x509.GeneralNameInterface.NAME_DIRECTORY;
import static sun.security.x509.NameConstraintsExtension.EXCLUDED_SUBTREES;
import static sun.security.x509.NameConstraintsExtension.PERMITTED_SUBTREES;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.Principal;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.testng.annotations.Test;

import sun.security.util.DerInputStream;
import sun.security.util.DerOutputStream;
import sun.security.util.DerValue;
import sun.security.util.ObjectIdentifier;
import sun.security.x509.AlgorithmId;
import sun.security.x509.AuthorityKeyIdentifierExtension;
import sun.security.x509.CertificatePoliciesExtension;
import sun.security.x509.DNSName;
import sun.security.x509.GeneralName;
import sun.security.x509.GeneralNameInterface;
import sun.security.x509.GeneralNames;
import sun.security.x509.GeneralSubtree;
import sun.security.x509.GeneralSubtrees;
import sun.security.x509.KeyIdentifier;
import sun.security.x509.NameConstraintsExtension;
import sun.security.x509.PolicyInformation;
import sun.security.x509.PrivateKeyUsageExtension;
import sun.security.x509.SubjectAlternativeNameExtension;
import sun.security.x509.X500Name;

/*
 * @test
 * @bug 8074931
 * @summary This class tests the X509CertSelector. The tests check particular criteria
 *          by setting them to a value that should match our test certificate and
 *          ensuring that they do match, then setting them to a value that should not
 *          match our test certificate and ensuring that they do not match.
 * @modules java.base/sun.security.x509
 *          java.base/sun.security.util
 */
public class X509CertSelectorTest {
    /*
            Certificate:
                Data:
                    Version: 3 (0x2)
                    Serial Number: 954172088 (0x38df82b8)
                Signature Algorithm: dsaWithSHA1
                    Issuer: C=us, O=sun, OU=testing
                    Validity
                        Not Before: Mar 27 15:48:08 2000 GMT
                        Not After : Jun 25 14:48:08 2000 GMT
                    Subject: C=us, O=sun, OU=testing, CN=mullan
                    Subject Public Key Info:
                        Public Key Algorithm: dsaEncryption
                            pub:  0
                            P:    0
                            Q:    0
                            G:    0
                    X509v3 extensions:
                        X509v3 Name Constraints: critical
                            0D.B0@.>1.0...U....us1.0
            ..U.
            ..sun1.0...U....testing1.0
            ..U....mullan
                        X509v3 Subject Key Identifier:
                            56:E8:88:AE:9D:B5:3F:2B:CB:A0:4C:4B:E2:87:53:07:33:77:1B:DF
                        X509v3 Authority Key Identifier:
                            keyid:8E:DD:AF:6F:EE:02:12:F4:61:E9:2F:E3:64:1A:6F:71:32:25:20:C0

                        X509v3 Subject Alternative Name:
                            email:mullan@east.sun.com
                        X509v3 Private Key Usage Period:
                            Not Before: Jan  1 05:00:00 2000 GMT, Not After: Jan  1 05:00:00 2001 GMT
                        X509v3 Key Usage: critical
                            Digital Signature
                        X509v3 Certificate Policies:
                            0$0\..*...0.0...+.......0..
            Testing...
                Signature Algorithm: dsaWithSHA1
                     r:
                         44:c7:35:40:5d:6c:28:75:7f:73:b2:f8:0d:72:6c:
                         09:65:b8:81:14
                     s:
                         76:79:f5:c7:37:3b:0d:9b:db:70:2f:20:80:36:e3:
                         80:e8:a6:c6:71
    */
    private static final String testCert =
            "-----BEGIN CERTIFICATE-----\n" +
            "MIICLjCCAeygAwIBAgIEON+CuDALBgcqhkjOOAQDBQAwLTELMAkGA1UEBhMCdXMx\n" +
            "DDAKBgNVBAoTA3N1bjEQMA4GA1UECxMHdGVzdGluZzAeFw0wMDAzMjcxNTQ4MDha\n" +
            "Fw0wMDA2MjUxNDQ4MDhaMD4xCzAJBgNVBAYTAnVzMQwwCgYDVQQKEwNzdW4xEDAO\n" +
            "BgNVBAsTB3Rlc3RpbmcxDzANBgNVBAMTBm11bGxhbjAcMBQGByqGSM44BAEwCQIB\n" +
            "AAIBAAIBAAMEAAIBAKOCASMwggEfMFAGA1UdHgEB/wRGMESgQjBApD4xCzAJBgNV\n" +
            "BAYTAnVzMQwwCgYDVQQKEwNzdW4xEDAOBgNVBAsTB3Rlc3RpbmcxDzANBgNVBAMT\n" +
            "Bm11bGxhbjAdBgNVHQ4EFgQUVuiIrp21PyvLoExL4odTBzN3G98wHwYDVR0jBBgw\n" +
            "FoAUjt2vb+4CEvRh6S/jZBpvcTIlIMAwHgYDVR0RBBcwFYETbXVsbGFuQGVhc3Qu\n" +
            "c3VuLmNvbTArBgNVHRAEJDAigA8yMDAwMDEwMTA1MDAwMFqBDzIwMDEwMTAxMDUw\n" +
            "MDAwWjAPBgNVHQ8BAf8EBQMDB4AAMC0GA1UdIAQmMCQwIgYEKoSAADAaMBgGCCsG\n" +
            "AQUFBwICMAwSClRlc3RpbmcuLi4wCwYHKoZIzjgEAwUAAy8AMCwCFETHNUBdbCh1\n" +
            "f3Oy+A1ybAlluIEUAhR2efXHNzsNm9twLyCANuOA6KbGcQ==\n" +
            "-----END CERTIFICATE-----\n" +
            "";

    private static final String testKey =
            "MIIBtjCCASsGByqGSM44BAEwggEeAoGBAIVWPEkcxbxhQRCqVzg55tNqbP5j0K4kdu4bkmXvfqC5\n" +
            "+qA75DvnfzsOJseb+9AuKXWk/DvCzFDmrY1YaU3scZC3OQEO9lEO3F4VDKOaudY6OT1SI22pAIwz\n" +
            "j5pvq+i7zOp4xUqkQUeh/4iQSfxOT5UrFGjkcbnbpVkCXD/GxAz7AhUAjtnm3dVIddUUHl6wxpZ7\n" +
            "GcA6gSsCgYAf/PXzQtemgIDjpFrNNSgTEKkLposBXKatAM+gUKlMUjf8SQvquqPxDtRrscGjXkoL\n" +
            "oTkaR7/akULYFpBvUcFkeIFiCnJg8M9XhCWdLvn9MPt+jR2oxookvCb9xLtD6WvIM/wd/nZ1iK4u\n" +
            "iY1+q85xvns/Awbtwl7oZDAwE2TUKAOBhAACgYBDc9UZ+3xsZubUZvRG5cpyJceYpJp2exOPVJXn\n" +
            "jR4CcR+cT9bAJpFSxqE/8KtNHXxHdu4f3DU67IMOVDpugzihyzXJvNm3w2H9x+6xczHG2wjvAJeh\n" +
            "X62EWbUatxPXFAoVKZWuUbaYaZzdWBDtNRrCuKKsLo0GFy8g2BZISuD3jw==\n" +
            "";

    // Certificate to run tests on
    private final X509Certificate cert;

    // BEGIN Android-removed: Not needed with testng
    /*
    public static void main(String[] args) throws Exception {
        X509CertSelectorTest test = new X509CertSelectorTest();
        test.doTest();
    }
    */
    // END Android-removed: Not needed with testng

    public X509CertSelectorTest() throws CertificateException, IOException {
        cert = (X509Certificate) CertificateFactory.getInstance("X.509")
                .generateCertificate(new ByteArrayInputStream(testCert.getBytes()));
    }

    // Runs the test.
    // BEGIN Android-removed: Not needed with testng
    /*
    private void doTest() throws Exception {
        System.out.println("START OF TESTS FOR " + "X509CertSelector");

        testSerialNumber();
        testIssuer();
        testSubjectKeyIdentifier();
        testAuthorityKeyIdentifier();
        testCertificateValid();
        testPrivateKeyValid();
        testSubjectPublicKeyAlgID();
        testKeyUsage();
        testSubjectAltName();
        testPolicy();
        testPathToName();
        testSubject();
        testSubjectPublicKey();
        testNameConstraints();
        testBasicConstraints();
        testCertificate();
    }
    */
    // END Android-removed: Not needed with testng

    // Tests matching on the serial number contained in the certificate.
    @Test
    public void testSerialNumber() {
        System.out.println("X.509 Certificate Match on serialNumber");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        selector.setSerialNumber(new BigInteger("999999999"));
        checkMatch(selector, cert, false);

        // good match
        selector.setSerialNumber(cert.getSerialNumber());
        checkMatch(selector, cert, true);
    }

    // Tests matching on the issuer name contained in the certificate.
    @Test
    public void testIssuer() throws IOException {
        System.out.println("X.509 Certificate Match on issuer");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        selector.setIssuer("ou=bogus,ou=east,o=sun,c=us");
        checkMatch(selector, cert, false);

        // good match
        selector.setIssuer((cert.getIssuerX500Principal()).getName("RFC2253"));
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the subject key identifier contained in the
     * certificate.
     */
    @Test
    public void testSubjectKeyIdentifier() throws IOException {
        System.out.println("X.509 Certificate Match on subjectKeyIdentifier");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        byte[] b = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        selector.setSubjectKeyIdentifier(b);
        checkMatch(selector, cert, false);

        // good match
        DerInputStream in = new DerInputStream(cert.getExtensionValue("2.5.29.14"));
        byte[] encoded = in.getOctetString();
        selector.setSubjectKeyIdentifier(encoded);
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the authority key identifier contained in the
     * certificate.
     */
    @Test
    public void testAuthorityKeyIdentifier() throws IOException {
        System.out.println("X.509 Certificate Match on authorityKeyIdentifier");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        byte[] b = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        AuthorityKeyIdentifierExtension a = new AuthorityKeyIdentifierExtension(new KeyIdentifier(b), null, null);
        selector.setAuthorityKeyIdentifier(a.getExtensionValue());
        checkMatch(selector, cert, false);

        // good match
        DerInputStream in = new DerInputStream(cert.getExtensionValue("2.5.29.35"));
        byte[] encoded = in.getOctetString();
        selector.setAuthorityKeyIdentifier(encoded);
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the certificate validity component contained in the
     * certificate.
     */
    @Test
    public void testCertificateValid() {
        System.out.println("X.509 Certificate Match on certificateValid");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        Calendar cal = Calendar.getInstance();
        cal.set(1968, 12, 31);
        selector.setCertificateValid(cal.getTime());
        checkMatch(selector, cert, false);

        // good match
        selector.setCertificateValid(cert.getNotBefore());
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the private key validity component contained in the
     * certificate.
     */
    @Test
    public void testPrivateKeyValid() throws IOException, CertificateException {
        System.out.println("X.509 Certificate Match on privateKeyValid");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        Calendar cal = Calendar.getInstance();
        cal.set(1968, 12, 31);
        selector.setPrivateKeyValid(cal.getTime());
        checkMatch(selector, cert, false);

        // good match
        DerInputStream in = new DerInputStream(cert.getExtensionValue("2.5.29.16"));
        byte[] encoded = in.getOctetString();
        PrivateKeyUsageExtension ext = new PrivateKeyUsageExtension(false, encoded);
        Date validDate = (Date) ext.get(PrivateKeyUsageExtension.NOT_BEFORE);
        selector.setPrivateKeyValid(validDate);
        checkMatch(selector, cert, true);

    }

    private ObjectIdentifier getCertPubKeyAlgOID(X509Certificate xcert) throws IOException {
        byte[] encodedKey = xcert.getPublicKey().getEncoded();
        DerValue val = new DerValue(encodedKey);
        if (val.tag != DerValue.tag_Sequence) {
            throw new RuntimeException("invalid key format");
        }

        return AlgorithmId.parse(val.data.getDerValue()).getOID();
    }

    /*
     * Tests matching on the subject public key algorithm ID component contained
     * in the certificate.
     */
    @Test
    public void testSubjectPublicKeyAlgID() throws IOException {
        System.out.println("X.509 Certificate Match on subjectPublicKeyAlgID");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        selector.setSubjectPublicKeyAlgID("2.5.29.14");
        checkMatch(selector, cert, false);

        // good match
        selector.setSubjectPublicKeyAlgID(getCertPubKeyAlgOID(cert).toString());
        checkMatch(selector, cert, true);

    }

    // Tests matching on the key usage extension contained in the certificate.
    @Test
    public void testKeyUsage() {
        System.out.println("X.509 Certificate Match on keyUsage");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        boolean[] keyUsage = { true, false, true, false, true, false, true, false };
        selector.setKeyUsage(keyUsage);
        System.out.println("Selector = " + selector.toString());
        checkMatch(selector, cert, false);

        // good match
        selector.setKeyUsage(cert.getKeyUsage());
        System.out.println("Selector = " + selector.toString());
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the subject alternative name extension contained in the
     * certificate.
     */
    @Test
    public void testSubjectAltName() throws IOException {
        System.out.println("X.509 Certificate Match on subjectAltName");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        GeneralNameInterface dnsName = new DNSName("foo.com");
        DerOutputStream tmp = new DerOutputStream();
        dnsName.encode(tmp);
        selector.addSubjectAlternativeName(2, tmp.toByteArray());
        checkMatch(selector, cert, false);

        // good match
        DerInputStream in = new DerInputStream(cert.getExtensionValue("2.5.29.17"));
        byte[] encoded = in.getOctetString();
        SubjectAlternativeNameExtension ext = new SubjectAlternativeNameExtension(false, encoded);
        GeneralNames names = (GeneralNames) ext.get(SubjectAlternativeNameExtension.SUBJECT_NAME);
        GeneralName name = (GeneralName) names.get(0);
        selector.setSubjectAlternativeNames(null);
        DerOutputStream tmp2 = new DerOutputStream();
        name.getName().encode(tmp2);
        selector.addSubjectAlternativeName(name.getType(), tmp2.toByteArray());
        checkMatch(selector, cert, true);

        // good match 2 (matches at least one)
        selector.setMatchAllSubjectAltNames(false);
        selector.addSubjectAlternativeName(2, "foo.com");
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the policy constraints extension contained in the
     * certificate.
     */
    @Test
    public void testPolicy() throws IOException {
        System.out.println("X.509 Certificate Match on certificatePolicies");
        // test encoding of CertificatePoliciesExtension because we wrote the
        // code
        // bad match
        X509CertSelector selector = new X509CertSelector();
        Set<String> s = new HashSet<>();
        s.add(new String("1.2.5.7.68"));
        selector.setPolicy(s);
        checkMatch(selector, cert, false);

        // good match
        DerInputStream in = new DerInputStream(cert.getExtensionValue("2.5.29.32"));
        CertificatePoliciesExtension ext = new CertificatePoliciesExtension(false, in.getOctetString());
        List<PolicyInformation> policies = ext.get(CertificatePoliciesExtension.POLICIES);
        // match on the first policy id
        PolicyInformation policyInfo = (PolicyInformation) policies.get(0);
        s.clear();
        s.add(policyInfo.getPolicyIdentifier().getIdentifier().toString());
        selector.setPolicy(s);
        checkMatch(selector, cert, true);
    }

    /*
     * Tests matching on the name constraints extension contained in the
     * certificate.
     */
    @Test
    public void testPathToName() throws IOException {
        System.out.println("X.509 Certificate Match on pathToName");

        X509CertSelector selector = null;
        DerInputStream in = new DerInputStream(cert.getExtensionValue("2.5.29.30"));
        byte[] encoded = in.getOctetString();
        NameConstraintsExtension ext = new NameConstraintsExtension(false, encoded);
        GeneralSubtrees permitted = (GeneralSubtrees) ext.get(PERMITTED_SUBTREES);
        GeneralSubtrees excluded = (GeneralSubtrees) ext.get(EXCLUDED_SUBTREES);

        // bad matches on pathToName within excluded subtrees
        if (excluded != null) {
            Iterator<GeneralSubtree> e = excluded.iterator();
            while (e.hasNext()) {
                GeneralSubtree tree = e.next();
                if (tree.getName().getType() == NAME_DIRECTORY) {
                    X500Name excludedDN1 = new X500Name(tree.getName().toString());
                    X500Name excludedDN2 = new X500Name("CN=Bogus, " + tree.getName().toString());
                    DerOutputStream derDN1 = new DerOutputStream();
                    DerOutputStream derDN2 = new DerOutputStream();
                    excludedDN1.encode(derDN1);
                    excludedDN2.encode(derDN2);
                    selector = new X509CertSelector();
                    selector.addPathToName(NAME_DIRECTORY, derDN1.toByteArray());
                    checkMatch(selector, cert, false);
                    selector.setPathToNames(null);
                    selector.addPathToName(NAME_DIRECTORY, derDN2.toByteArray());
                    checkMatch(selector, cert, false);
                }
            }
        }

        // good matches on pathToName within permitted subtrees
        if (permitted != null) {
            Iterator<GeneralSubtree> e = permitted.iterator();
            while (e.hasNext()) {
                GeneralSubtree tree = e.next();
                if (tree.getName().getType() == NAME_DIRECTORY) {
                    X500Name permittedDN1 = new X500Name(tree.getName().toString());
                    X500Name permittedDN2 = new X500Name("CN=good, " + tree.getName().toString());
                    DerOutputStream derDN1 = new DerOutputStream();
                    DerOutputStream derDN2 = new DerOutputStream();
                    permittedDN1.encode(derDN1);
                    permittedDN2.encode(derDN2);
                    selector = new X509CertSelector();
                    selector.addPathToName(NAME_DIRECTORY, derDN1.toByteArray());
                    checkMatch(selector, cert, true);
                    selector.setPathToNames(null);
                    selector.addPathToName(NAME_DIRECTORY, derDN2.toByteArray());
                    checkMatch(selector, cert, true);
                }
            }
        }
    }

    // Tests matching on the subject name contained in the certificate.
    @Test
    public void testSubject() throws IOException {
        System.out.println("X.509 Certificate Match on subject");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        selector.setSubject("ou=bogus,ou=east,o=sun,c=us");
        checkMatch(selector, cert, false);

        // good match
        selector.setSubject(cert.getSubjectX500Principal().getName("RFC2253"));
        checkMatch(selector, cert, true);
    }

    // Tests matching on the subject public key contained in the certificate.
    @Test
    public void testSubjectPublicKey() throws IOException, GeneralSecurityException {
        System.out.println("X.509 Certificate Match on subject public key");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
                Base64.getMimeDecoder().decode(testKey.getBytes()));
        KeyFactory keyFactory = KeyFactory.getInstance("DSA");
        PublicKey pubKey = keyFactory.generatePublic(keySpec);
        selector.setSubjectPublicKey(pubKey);
        checkMatch(selector, cert, false);

        // good match
        selector.setSubjectPublicKey(cert.getPublicKey());
        checkMatch(selector, cert, true);
    }

    // Tests matching on the name constraints contained in the certificate.
    // TODO(b/203769066): Depends on updated X509Certificate implementation
    private void testNameConstraints() throws IOException {
        System.out.println("X.509 Certificate Match on name constraints");
        // bad match
        GeneralSubtrees subjectTree = new GeneralSubtrees();
        subjectTree.add(getGeneralSubtree((X500Name) cert.getSubjectDN()));
        NameConstraintsExtension ext = new NameConstraintsExtension((GeneralSubtrees) null, subjectTree);
        X509CertSelector selector = new X509CertSelector();
        selector.setNameConstraints(ext.getExtensionValue());
        checkMatch(selector, cert, false);

        // good match
        ext = new NameConstraintsExtension(subjectTree, null);
        selector.setNameConstraints(ext.getExtensionValue());
        checkMatch(selector, cert, true);
    }

    // Tests matching on basic constraints.
    @Test
    public void testBasicConstraints() {
        System.out.println("X.509 Certificate Match on basic constraints");
        // bad match
        X509CertSelector selector = new X509CertSelector();
        int mpl = cert.getBasicConstraints();
        selector.setBasicConstraints(0);
        checkMatch(selector, cert, false);

        // good match
        selector.setBasicConstraints(mpl);
        checkMatch(selector, cert, true);
    }

    // Tests certificateEquals criterion
    @Test
    public void testCertificate() {
        System.out.println("X.509 Certificate Match on certificateEquals criterion");

        X509CertSelector selector = new X509CertSelector();
        // good match
        selector.setCertificate(cert);
        checkMatch(selector, cert, true);
    }

    private void checkMatch(X509CertSelector selector, X509Certificate cert, boolean match) {
        boolean result = selector.match(cert);
        assertEquals(result, match,
            selector + " match " + cert + " is " + result + ", but expect " + match);
    }

    private static GeneralSubtree getGeneralSubtree(GeneralNameInterface gni) {
        // Create a new GeneralSubtree with the specified name, 0 base, and
        // unlimited length
        GeneralName gn = new GeneralName(gni);
        GeneralSubtree subTree = new GeneralSubtree(gn, 0, -1);
        return subTree;
    }
}
