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

/*
 * @test
 * @bug 6802846 8172529 8227758 8260960
 * @summary jarsigner needs enhanced cert validation(options)
 * @library /test/lib
 * @run main/timeout=240 ConciseJarsigner
 */

import jdk.test.lib.Asserts;
import jdk.test.lib.SecurityTools;
import jdk.test.lib.process.OutputAnalyzer;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Calendar;
import java.util.List;

public class ConciseJarsigner {

    static OutputAnalyzer kt(String cmd) throws Exception {
        // Choose 2048-bit RSA to make sure it runs fine and fast. In
        // fact, every keyalg/keysize combination is OK for this test.
        // The start date is set to -1M to prevent the certificate not yet valid during fast enough execution.
        // If -startdate is specified in cmd, cmd version will be used.
        if (cmd.contains("-startdate")) {
            return SecurityTools.keytool("-storepass changeit -keypass changeit "
                    + "-keystore ks -keyalg rsa -keysize 2048 " + cmd);
        } else {
            return SecurityTools.keytool("-storepass changeit -keypass changeit "
                    + "-keystore ks -keyalg rsa -keysize 2048 -startdate -1M " + cmd);
        }
    }

    static void gencert(String owner, String cmd) throws Exception {
        kt("-certreq -alias " + owner + " -file tmp.req");
        kt("-gencert -infile tmp.req -outfile tmp.cert " + cmd);
        kt("-import -alias " + owner + " -file tmp.cert");
    }

    static OutputAnalyzer js(String cmd) throws Exception {
        return SecurityTools.jarsigner("-debug " + cmd);
    }

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

        Files.write(Path.of("A1"), List.of("a1"));
        Files.write(Path.of("A2"), List.of("a2"));
        Files.write(Path.of("A3"), List.of("a3"));
        Files.write(Path.of("A4"), List.of("a4"));
        Files.write(Path.of("A5"), List.of("a5"));
        Files.write(Path.of("A6"), List.of("a6"));

        String year = "" + Calendar.getInstance().get(Calendar.YEAR);

        // ==========================================================
        // First part: output format
        // ==========================================================

        kt("-genkeypair -alias a1 -dname CN=a1 -validity 366");
        kt("-genkeypair -alias a2 -dname CN=a2 -validity 366");

        // a.jar includes 8 unsigned, 2 signed by a1 and a2, 2 signed by a3
        SecurityTools.jar("cvf a.jar A1 A2");
        js("-keystore ks -storepass changeit a.jar a1");
        SecurityTools.jar("uvf a.jar A3 A4");
        js("-keystore ks -storepass changeit a.jar a2");
        SecurityTools.jar("uvf a.jar A5 A6");

        // Verify OK
        js("-verify a.jar").shouldHaveExitValue(0);

        // 4(chainNotValidated)+16(hasUnsignedEntry)
        js("-verify a.jar -strict").shouldHaveExitValue(20);

        // 16(hasUnsignedEntry)
        js("-verify a.jar -strict -keystore ks -storepass changeit")
                .shouldHaveExitValue(16);

        // 16(hasUnsignedEntry)+32(notSignedByAlias)
        js("-verify a.jar a1 -strict -keystore ks -storepass changeit")
                .shouldHaveExitValue(48);

        // 16(hasUnsignedEntry)
        js("-verify a.jar a1 a2 -strict -keystore ks -storepass changeit")
                .shouldHaveExitValue(16);

        // 12 entries all together
        Asserts.assertTrue(js("-verify a.jar -verbose")
                .asLines().stream()
                .filter(s -> s.contains(year))
                .count() == 12);

        // 12 entries all listed
        Asserts.assertTrue(js("-verify a.jar -verbose:grouped")
                .asLines().stream()
                .filter(s -> s.contains(year))
                .count() == 12);

        // 5 groups: MANIFEST, signature related entries, directory entries,
        // signed entries, and unsigned entries.
        Asserts.assertTrue(js("-verify a.jar -verbose:summary")
                .asLines().stream()
                .filter(s -> s.contains(year))
                .count() == 5);

        // still 5 groups, but MANIFEST group and directiry entry group
        // have no other file
        Asserts.assertTrue(js("-verify a.jar -verbose:summary")
                .asLines().stream()
                .filter(s -> s.contains("more)"))
                .count() == 3);

        // 6 groups: MANIFEST, signature related entries, directory entries,
        // signed entries by a1/a2, signed entries by a2, and unsigned entries.
        Asserts.assertTrue(js("-verify a.jar -verbose:summary -certs")
                .asLines().stream()
                .filter(s -> s.contains(year))
                .count() == 6);

        // 2 for MANIFEST, 2*2 for A1/A2, 2 for A3/A4
        Asserts.assertTrue(js("-verify a.jar -verbose -certs")
                .asLines().stream()
                .filter(s -> s.contains("[certificate"))
                .count() == 8);

        // a1,a2 for MANIFEST, a1,a2 for A1/A2, a2 for A3/A4
        Asserts.assertTrue(js("-verify a.jar -verbose:grouped -certs")
                .asLines().stream()
                .filter(s -> s.contains("[certificate"))
                .count() == 5);

        // a1,a2 for MANIFEST, a1,a2 for A1/A2, a2 for A3/A4
        Asserts.assertTrue(js("-verify a.jar -verbose:summary -certs")
                .asLines().stream()
                .filter(s -> s.contains("[certificate"))
                .count() == 5);

        // still 6 groups, but MANIFEST group and directory entry group
        // have no other file
        Asserts.assertTrue(js("-verify a.jar -verbose:summary -certs")
                .asLines().stream()
                .filter(s -> s.contains("more)"))
                .count() == 4);

        // ==========================================================
        // Second part: exit code 2, 4, 8.
        // 16 and 32 already covered in the first part
        // ==========================================================

        kt("-genkeypair -alias ca -dname CN=ca -ext bc -validity 365");
        kt("-genkeypair -alias expired -dname CN=expired");
        gencert("expired", "-alias ca -startdate -10m");
        kt("-genkeypair -alias notyetvalid -dname CN=notyetvalid");
        gencert("notyetvalid", "-alias ca -startdate +1m");
        kt("-genkeypair -alias badku -dname CN=badku");
        gencert("badku", "-alias ca -ext KU=cRLSign -validity 365");
        kt("-genkeypair -alias badeku -dname CN=badeku");
        gencert("badeku", "-alias ca -ext EKU=sa -validity 365");
        kt("-genkeypair -alias goodku -dname CN=goodku");
        gencert("goodku", "-alias ca -ext KU=dig -validity 365");
        kt("-genkeypair -alias goodeku -dname CN=goodeku");
        gencert("goodeku", "-alias ca -ext EKU=codesign -validity 365");

        js("-strict -keystore ks -storepass changeit a.jar expired")
                .shouldHaveExitValue(4);

        js("-strict -keystore ks -storepass changeit a.jar notyetvalid")
                .shouldHaveExitValue(4);

        js("-strict -keystore ks -storepass changeit a.jar badku")
                .shouldHaveExitValue(8);

        js("-strict -keystore ks -storepass changeit a.jar badeku")
                .shouldHaveExitValue(8);

        js("-strict -keystore ks -storepass changeit a.jar goodku")
                .shouldHaveExitValue(0);

        js("-strict -keystore ks -storepass changeit a.jar goodeku")
                .shouldHaveExitValue(0);

        // badchain signed by ca1, but ca1 is removed later
        kt("-genkeypair -alias badchain -dname CN=badchain -validity 365");
        kt("-genkeypair -alias ca1 -dname CN=ca1 -ext bc -validity 365");
        gencert("badchain", "-alias ca1 -validity 365");

        // save ca1.cert for easy replay
        kt("-exportcert -file ca1.cert -alias ca1");
        kt("-delete -alias ca1");

        js("-strict -keystore ks -storepass changeit a.jar badchain")
                .shouldHaveExitValue(4);

        js("-verify a.jar").shouldHaveExitValue(0);

        // ==========================================================
        // Third part: -certchain test
        // ==========================================================

        // altchain signed by ca2
        kt("-genkeypair -alias altchain -dname CN=altchain -validity 365");
        kt("-genkeypair -alias ca2 -dname CN=ca2 -ext bc -validity 365");
        kt("-certreq -alias altchain -file altchain.req");
        Files.write(Path.of("certchain"), List.of(
                kt("-gencert -alias ca2 -validity 365 -rfc -infile altchain.req")
                        .getOutput(),
                kt("-exportcert -alias ca2 -rfc").getOutput()));

        // Self-signed cert does not work
        js("-strict -keystore ks -storepass changeit a.jar altchain")
                .shouldHaveExitValue(4);

        // -certchain works
        js("-strict -keystore ks -storepass changeit -certchain certchain "
                + "a.jar altchain")
                .shouldHaveExitValue(0);

        // if ca2 is removed and cert is imported, -certchain won't work
        // because this certificate entry is not trusted
        // save ca2.cert for easy replay
        kt("-exportcert -file ca2.cert -alias ca2");
        kt("-delete -alias ca2");
        kt("-importcert -file certchain -alias altchain -noprompt");
        js("-strict -keystore ks -storepass changeit "
                + "-certchain certchain a.jar altchain")
                .shouldHaveExitValue(4);

        js("-verify a.jar").shouldHaveExitValue(0);

        // ==========================================================
        // 8172529
        // ==========================================================

        kt("-genkeypair -alias ee -dname CN=ee");
        kt("-genkeypair -alias caone -dname CN=caone -ext bc:c");
        kt("-genkeypair -alias catwo -dname CN=catwo -ext bc:c");

        kt("-certreq -alias ee -file ee.req");
        kt("-certreq -alias catwo -file catwo.req");

        // This certchain contains a cross-signed weak catwo.cert
        Files.write(Path.of("ee2"), List.of(
                kt("-gencert -alias catwo -rfc -infile ee.req").getOutput(),
                kt("-gencert -alias caone -sigalg MD5withRSA -rfc "
                        + "-infile catwo.req").getOutput()));

        kt("-importcert -alias ee -file ee2");

        SecurityTools.jar("cvf a.jar A1");
        js("-strict -keystore ks -storepass changeit a.jar ee")
                .shouldHaveExitValue(0);
        js("-strict -keystore ks -storepass changeit -verify a.jar")
                .shouldHaveExitValue(0);
    }
}
