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

import java.nio.file.Files;
import java.nio.file.Path;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.CertificateFactory;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertPathValidatorException.BasicReason;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.cert.X509CRL;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import sun.security.x509.AuthorityKeyIdentifierExtension;
import sun.security.x509.CRLDistributionPointsExtension;
import sun.security.x509.CRLExtensions;
import sun.security.x509.CRLNumberExtension;
import sun.security.x509.DistributionPoint;
import sun.security.x509.Extension;
import sun.security.x509.GeneralName;
import sun.security.x509.GeneralNames;
import sun.security.x509.KeyIdentifier;
import sun.security.x509.URIName;
import sun.security.x509.X500Name;
import sun.security.x509.X509CRLEntryImpl;
import sun.security.x509.X509CRLImpl;
import static sun.security.x509.X509CRLImpl.TBSCertList;
import jdk.test.lib.security.CertificateBuilder;

/*
 * @test
 * @bug 8200566
 * @summary Check that CRL validation continues to check other CRLs in
 *          CRLDP extension after CRL fetching errors and exhibits same
 *          behavior (fails because cert is revoked) whether CRL cache is
 *          fresh or stale.
 * @modules java.base/sun.security.x509
 *          java.base/sun.security.util
 * @library /test/lib
 * @run main/othervm -Dcom.sun.security.enableCRLDP=true CheckAllCRLs
 */
public class CheckAllCRLs {

    public static void main(String[] args) throws Exception {

        CertificateBuilder cb = new CertificateBuilder();

        // Create CA cert
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        KeyPair rootKeyPair = kpg.genKeyPair();
        X509Certificate rootCert = createCert(cb, "CN=Root CA",
            rootKeyPair, rootKeyPair, null, "SHA384withRSA", true, false);

        // Create EE cert. This EE cert will contain a CRL Distribution
        // Points extension with two DistributionPoints - one will be a HTTP
        // URL to a non-existant HTTP server, and the other will be a File
        // URL to a file containing the CRL.
        KeyPair eeKeyPair = kpg.genKeyPair();
        X509Certificate eeCert1 = createCert(cb, "CN=End Entity",
            rootKeyPair, eeKeyPair, rootCert, "SHA384withRSA", false, true);

        // Create another EE cert. This EE cert is similar in that it contains
        // a CRL Distribution Points extension but with one DistributionPoint
        // containing 2 GeneralName URLs as above.
        X509Certificate eeCert2 = createCert(cb, "CN=End Entity",
            rootKeyPair, eeKeyPair, rootCert, "SHA384withRSA", false, false);

        // Create a CRL with no revoked certificates and store it in a file
        X509CRL crl = createCRL(new X500Name("CN=Root CA"), rootKeyPair,
            "SHA384withRSA");
        Files.write(Path.of("root.crl"), crl.getEncoded());

        // Validate path containing eeCert1
        System.out.println("Validating cert with CRLDP containing one "
            + "DistributionPoint with 2 entries, the first non-existent");
        validatePath(eeCert1, rootCert);

        // Validate path containing eeCert2
        System.out.println("Validating cert with CRLDP containing two "
            + "DistributionPoints with 1 entry each, the first non-existent");
        validatePath(eeCert2, rootCert);
    }

    private static X509Certificate createCert(CertificateBuilder cb,
            String subjectDN, KeyPair issuerKeyPair, KeyPair subjectKeyPair,
            X509Certificate issuerCert, String sigAlg, boolean isCA,
            boolean twoDPs) throws Exception {
        cb.setSubjectName(subjectDN);
        cb.setPublicKey(subjectKeyPair.getPublic());
        cb.setSerialNumber(new BigInteger("1"));

        if (isCA) {
            // Make a 3 year validity starting from 60 days ago
            long start = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60);
            long end = start + TimeUnit.DAYS.toMillis(1085);
            cb.setValidity(new Date(start), new Date(end));
            cb.addBasicConstraintsExt(true, true, -1);
            cb.addKeyUsageExt(new boolean[]
                {false, false, false, false, false, true, true, false, false});
        } else {
            // Make a 1 year validity starting from 7 days ago
            long start = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
            long end = start + TimeUnit.DAYS.toMillis(365);
            cb.setValidity(new Date(start), new Date(end));
            cb.addAuthorityKeyIdExt(issuerKeyPair.getPublic());
            cb.addKeyUsageExt(new boolean[]
                {true, false, false, false, false, false, false, false, false});
            cb.addExtendedKeyUsageExt(List.of("1.3.6.1.5.5.7.3.1"));
            GeneralName first = new GeneralName(new URIName(
                    "http://127.0.0.1:48180/crl/will/always/fail/root.crl"));
            GeneralName second = new GeneralName(new URIName("file:./root.crl"));
            if (twoDPs) {
                GeneralNames gn1 = new GeneralNames().add(first);
                DistributionPoint dp1 = new DistributionPoint(gn1, null, null);
                GeneralNames gn2 = new GeneralNames().add(second);
                DistributionPoint dp2 = new DistributionPoint(gn2, null, null);
                cb.addExtension(new CRLDistributionPointsExtension(List.of(dp1, dp2)));
            } else {
                GeneralNames gn = new GeneralNames().add(first).add(second);
                DistributionPoint dp = new DistributionPoint(gn, null, null);
                cb.addExtension(new CRLDistributionPointsExtension(List.of(dp)));
            }
        }
        cb.addSubjectKeyIdExt(subjectKeyPair.getPublic());

        // return signed cert
        return cb.build(issuerCert, issuerKeyPair.getPrivate(), sigAlg);
    }

    private static X509CRL createCRL(X500Name caIssuer, KeyPair caKeyPair,
            String sigAlg) throws Exception {

        CRLExtensions crlExts = new CRLExtensions();

        // add AuthorityKeyIdentifier extension
        KeyIdentifier kid = new KeyIdentifier(caKeyPair.getPublic());
        Extension ext = new AuthorityKeyIdentifierExtension(kid, null, null);
        crlExts.setExtension(ext.getId(),
            new AuthorityKeyIdentifierExtension(kid, null, null));

        // add CRLNumber extension
        ext = new CRLNumberExtension(1);
        crlExts.setExtension(ext.getId(), ext);

        // revoke cert
        X509CRLEntryImpl crlEntry =
            new X509CRLEntryImpl(new BigInteger("1"), new Date());

        // Create a 1 year validity CRL starting from 7 days ago
        long start = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7);
        long end = start + TimeUnit.DAYS.toMillis(365);
        TBSCertList tcl = new TBSCertList(caIssuer, new Date(start),
            new Date(end), new X509CRLEntryImpl[]{ crlEntry }, crlExts);

        // return signed CRL
        return X509CRLImpl.newSigned(tcl, caKeyPair.getPrivate(), sigAlg);
    }

    private static void validatePath(X509Certificate eeCert,
            X509Certificate rootCert) throws Exception {

        // Create certification path and set up PKIXParameters.
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        CertPath cp = cf.generateCertPath(List.of(eeCert));
        PKIXParameters pp =
            new PKIXParameters(Set.of(new TrustAnchor(rootCert, null)));
        pp.setRevocationEnabled(true);

        CertPathValidator cpv = CertPathValidator.getInstance("PKIX");

        // Validate path twice in succession, making sure we get consistent
        // results the second time when the CRL cache is fresh.
        System.out.println("First time validating path");
        validate(cpv, cp, pp);
        System.out.println("Second time validating path");
        validate(cpv, cp, pp);

        // CRL lookup cache time is 30s. Sleep for 35 seconds to ensure
        // cache is stale, and validate one more time to ensure we get
        // consistent results.
        System.out.println("Waiting for CRL cache to be cleared");
        Thread.sleep(30500);

        System.out.println("Third time validating path");
        validate(cpv, cp, pp);
    }

    private static void validate(CertPathValidator cpv, CertPath cp,
            PKIXParameters pp) throws Exception {

        try {
            cpv.validate(cp, pp);
            throw new Exception("Validation passed unexpectedly");
        } catch (CertPathValidatorException cpve) {
            if (cpve.getReason() != BasicReason.REVOKED) {
                throw new Exception("Validation failed with unexpected reason", cpve);
            }
            System.out.println("Validation failed as expected: " + cpve);
        }
    }
}
