/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.pm;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.content.Context;
import android.content.pm.PackageParser;
import android.content.pm.Signature;
import android.util.Xml;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.util.HexDump;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;

import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
public class PackageSignaturesTest {
    private static final String TEST_RESOURCES_FOLDER = "PackageSignaturesTest";

    private Context mContext;

    private PackageSetting mPackageSetting;

    // These signatures are the DER encoding of the ec-p256[_X] X509 certificates in the certs/
    // directory. The apksigner tool was used to sign a test APK with these certificates and the
    // corresponding ec-p256{_X].pk8 private key file. For the lineage tests the
    // ec-p256-lineage-X-signers file was provided as the parameter to the --lineage option when
    // signing the APK. The APK was then installed on a test device, the packages.xml file was
    // pulled from the device, and the APK's <sig> tag was used as the basis for these tests.
    // For more details see the README under the xml/ directory.
    private static final String FIRST_EXPECTED_SIGNATURE =
            "3082016c30820111a003020102020900ca0fb64dfb66e772300a06082a8648ce3d04030230123110300e06"
            + "035504030c0765632d70323536301e170d3136303333313134353830365a170d34333038313731343538"
            + "30365a30123110300e06035504030c0765632d703235363059301306072a8648ce3d020106082a8648ce"
            + "3d03010703420004a65f113d22cb4913908307ac31ee2ba0e9138b785fac6536d14ea2ce90d2b4bfe194"
            + "b50cdc8e169f54a73a991ef0fa76329825be078cc782740703da44b4d7eba350304e301d0603551d0e04"
            + "160414d4133568b95b30158b322071ea8c43ff5b05ccc8301f0603551d23041830168014d4133568b95b"
            + "30158b322071ea8c43ff5b05ccc8300c0603551d13040530030101ff300a06082a8648ce3d0403020349"
            + "003046022100f504a0866caef029f417142c5cb71354c79ffcd1d640618dfca4f19e16db78d6022100f8"
            + "eea4829799c06cad08c6d3d2d2ec05e0574154e747ea0fdbb8042cb655aadd";
    private static final String SECOND_EXPECTED_SIGNATURE =
            "3082016d30820113a0030201020209008855bd1dd2b2b225300a06082a8648ce3d04030230123110300e06"
            + "035504030c0765632d70323536301e170d3138303731333137343135315a170d32383037313031373431"
            + "35315a30143112301006035504030c0965632d703235365f323059301306072a8648ce3d020106082a86"
            + "48ce3d030107034200041d4cca0472ad97ee3cecef0da93d62b450c6788333b36e7553cde9f74ab5df00"
            + "bbba6ba950e68461d70bbc271b62151dad2de2bf6203cd2076801c7a9d4422e1a350304e301d0603551d"
            + "0e041604147991d92b0208fc448bf506d4efc9fff428cb5e5f301f0603551d23041830168014d4133568"
            + "b95b30158b322071ea8c43ff5b05ccc8300c0603551d13040530030101ff300a06082a8648ce3d040302"
            + "034800304502202769abb1b49fc2f53479c4ae92a6631dabfd522c9acb0bba2b43ebeb99c63011022100"
            + "d260fb1d1f176cf9b7fa60098bfd24319f4905a3e5fda100a6fe1a2ab19ff09e";
    private static final String THIRD_EXPECTED_SIGNATURE =
            "3082016e30820115a0030201020209008394f5cad16a89a7300a06082a8648ce3d04030230143112301006"
            + "035504030c0965632d703235365f32301e170d3138303731343030303532365a170d3238303731313030"
            + "303532365a30143112301006035504030c0965632d703235365f333059301306072a8648ce3d02010608"
            + "2a8648ce3d03010703420004f31e62430e9db6fc5928d975fc4e47419bacfcb2e07c89299e6cd7e344dd"
            + "21adfd308d58cb49a1a2a3fecacceea4862069f30be1643bcc255040d8089dfb3743a350304e301d0603"
            + "551d0e041604146f8d0828b13efaf577fc86b0e99fa3e54bcbcff0301f0603551d230418301680147991"
            + "d92b0208fc448bf506d4efc9fff428cb5e5f300c0603551d13040530030101ff300a06082a8648ce3d04"
            + "030203470030440220256bdaa2784c273e4cc291a595a46779dee9de9044dc9f7ab820309567df9fe902"
            + "201a4ad8c69891b5a8c47434fe9540ed1f4979b5fad3483f3fa04d5677355a579e";

    // When running tests using the pastSigs tag / lineage the past signers and their capabilities
    // should be returned in the SigningDetails. The flags attribute of the cert tag under the
    // pastSigs tag contains these capabilities; for tests that verify the lineage the capabilities
    // of the signers should be set to the values in this Map.
    private static final Map<String, Integer> SIGNATURE_TO_CAPABILITY_MAP;

    static {
        SIGNATURE_TO_CAPABILITY_MAP = new HashMap<>();
        SIGNATURE_TO_CAPABILITY_MAP.put(FIRST_EXPECTED_SIGNATURE, 3);
        SIGNATURE_TO_CAPABILITY_MAP.put(SECOND_EXPECTED_SIGNATURE, 7);
        SIGNATURE_TO_CAPABILITY_MAP.put(THIRD_EXPECTED_SIGNATURE, 23);
    }

    private static final int[] CAPABILITIES =
            {PackageParser.SigningDetails.CertCapabilities.INSTALLED_DATA,
                    PackageParser.SigningDetails.CertCapabilities.SHARED_USER_ID,
                    PackageParser.SigningDetails.CertCapabilities.PERMISSION,
                    PackageParser.SigningDetails.CertCapabilities.ROLLBACK};

    @Before
    public void setUp() throws Exception {
        mContext = InstrumentationRegistry.getContext();
        mPackageSetting = createPackageSetting();
    }

    @Test
    public void testReadXmlWithOneSignerCompletesSuccessfully() throws Exception {
        // Verifies the good path of reading a single sigs tag with one signer returns the
        // expected signature and scheme version.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer.xml", 1, FIRST_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithTwoV1V2Signers() throws Exception {
        // Verifies the good path of reading a single sigs tag with multiple signers returns the
        // expected signatures and scheme version.
        verifyReadXmlReturnsExpectedSignatures("xml/two-signers-v1v2.xml", 2,
                FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlFromTwoSigsTagsWithSameSigner() throws Exception {
        // Verifies the good path of reading two separate packages tags from the same signer. The
        // first call to readXml should return the list with the expected signature, then the second
        // call should reference this signature and complete successfully with no new entries in the
        // List.
        XmlPullParser parser = getXMLFromResources("xml/one-signer.xml");
        ArrayList<Signature> signatures = new ArrayList<>();
        mPackageSetting.signatures.readXml(parser, signatures);
        Set<String> expectedSignatures = createSetOfSignatures(FIRST_EXPECTED_SIGNATURE);
        verifySignaturesContainExpectedValues(signatures, expectedSignatures);
        parser = getXMLFromResources("xml/one-signer-previous-cert.xml");
        mPackageSetting.signatures.readXml(parser, signatures);
        expectedSignatures = createSetOfSignatures(FIRST_EXPECTED_SIGNATURE);
        verifySignaturesContainExpectedValues(signatures, expectedSignatures);
    }

    @Test
    public void testReadXmlWithSigningLineage() throws Exception {
        // Verifies the good path of reading a single sigs tag including pastSigs with the
        // signing lineage returns the expected signatures and lineage for two and three signers
        // in the lineage.
        verifyReadXmlReturnsExpectedSignaturesAndLineage("xml/two-signers-in-lineage.xml", 3,
                FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
        verifyReadXmlReturnsExpectedSignaturesAndLineage("xml/three-signers-in-lineage.xml", 3,
                FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE, THIRD_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithInvalidPublicKeyInCertKey() throws Exception {
        // If the cert tag key attribute does not contain a valid public key then a
        // CertificateException should be thrown when attempting to build the SigningDetails; in
        // this case the signing details should be set to UNKNOWN.
        XmlPullParser parser = getXMLFromResources(
                "xml/one-signer-invalid-public-key-cert-key.xml");
        ArrayList<Signature> signatures = new ArrayList<>();
        mPackageSetting.signatures.readXml(parser, signatures);
        assertEquals(
                "The signing details was not UNKNOWN after parsing an invalid public key cert key"
                        + " attribute",
                PackageParser.SigningDetails.UNKNOWN, mPackageSetting.signatures.mSigningDetails);
    }

    @Test
    public void testReadXmlWithMissingSigsCount() throws Exception {
        // Verifies if the sigs count attribute is missing then the signature cannot be read but the
        // method does not throw an exception.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-missing-sigs-count.xml",
                PackageParser.SigningDetails.SignatureSchemeVersion.UNKNOWN);
    }

    @Test
    public void testReadXmlWithMissingSchemeVersion() throws Exception {
        // Verifies if the schemeVersion is an invalid value the signature can still be obtained.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-missing-scheme-version.xml",
                PackageParser.SigningDetails.SignatureSchemeVersion.UNKNOWN,
                FIRST_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithSigningLineageWithMissingSchemeVersion() throws Exception {
        // Verifies if the scheme version cannot be read the signers in the lineage can still be
        // obtained.
        verifyReadXmlReturnsExpectedSignaturesAndLineage(
                "xml/three-signers-in-lineage-missing-scheme-version.xml",
                PackageParser.SigningDetails.SignatureSchemeVersion.UNKNOWN,
                FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE, THIRD_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithInvalidCertIndex() throws Exception {
        // If the cert index attribute is invalid the signature will not be read but the call
        // should exit gracefully.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-invalid-cert-index.xml", 3);
    }

    @Test
    public void testReadXmlWithMissingCertIndex() throws Exception {
        // If the cert index attribute is missing the signature will not be read but the call should
        // exit gracefully.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-missing-cert-index.xml", 3);
    }

    @Test
    public void testReadXmlWithInvalidCertKey() throws Exception {
        // If the cert key value is invalid the signature cannot be read but the call should exit
        // gracefully.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-invalid-cert-key.xml", 3);
    }

    @Test
    public void testReadXmlWithMissingCertKey() throws Exception {
        // If the cert key is missing the signature cannot be read but the call should exit
        // gracefully.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-missing-cert-key.xml", 3);
    }

    @Test
    public void testReadXmlWithMissingCertTag() throws Exception {
        // If the cert tag is missing there is no signature to read but the call should exit
        // gracefully.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-missing-cert-tag.xml", 3);
    }

    @Test
    public void testReadXmlWithTooFewCertTags() throws Exception {
        // If the number of cert tags is less than that specified in the count attribute then the
        // signatures that could be read are copied to a smaller array to be used when building
        // the SigningDetails object. This test verifies if there are too few cert tags the
        // available signatures can still be obtained.
        verifyReadXmlReturnsExpectedSignatures("xml/two-signers-v1v2-missing-cert-tag.xml", 1,
                FIRST_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithExtraCertTag() throws Exception {
        // Verifies if there are more cert tags than specified by the count attribute the extra cert
        // tag is ignored and the expected signature from the first cert tag is returned.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-extra-cert-tag.xml", 3,
                FIRST_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithInvalidTag() throws Exception {
        // Verifies an invalid tag under sigs is ignored and the expected signature is returned.
        verifyReadXmlReturnsExpectedSignatures("xml/one-signer-invalid-tag.xml", 3,
                FIRST_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithInvalidPastSigsCount() throws Exception {
        // Verifies if the pastSigs tag contains an invalid count attribute the current signature
        // is still returned; in this case the third expected signature is the most recent signer.
        verifyReadXmlReturnsExpectedSignatures(
                "xml/three-signers-in-lineage-invalid-pastSigs-count.xml", 3,
                THIRD_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithMissingPastSigsCount() throws Exception {
        // Verifies if the pastSigs tag is missing the count attribute the current signature is
        // still returned; in this case the third expected signature is the most recent signer.
        verifyReadXmlReturnsExpectedSignaturesAndLineage(
                "xml/three-signers-in-lineage-missing-pastSigs-count.xml", 3,
                THIRD_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithInvalidCertFlags() throws Exception {
        // Verifies if the cert tag contains an invalid flags attribute the expected signatures
        // are still returned, although since the flags could not be read these signatures will not
        // include the capabilities of the previous signers in the lineage.
        verifyReadXmlReturnsExpectedSignatures("xml/two-signers-in-lineage-invalid-certs-flags.xml",
                3, FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithMissingCertFlags() throws Exception {
        // Verifies if the cert tag does not contain a flags attribute the expected signatures are
        // still returned, although since there are no flags to read these signatures will not
        // include the capabilities of the previous signers in the lineage.
        verifyReadXmlReturnsExpectedSignatures("xml/two-signers-in-lineage-missing-certs-flags.xml",
                3, FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithMultiplePastSigsTags() throws Exception {
        // Verifies if multiple pastSigs tags are found under the sigs tag the additional pastSigs
        // tag is ignored and the expected signatures are returned along with the previous signer in
        // the lineage.
        verifyReadXmlReturnsExpectedSignaturesAndLineage(
                "xml/two-signers-in-lineage-multiple-pastSigs-tags.xml", 3,
                FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithInvalidPastSigsCertIndex() throws Exception {
        // If the pastSigs cert tag contains an invalid index attribute that signature cannot be
        // read but the current signature should still be returned.
        verifyReadXmlReturnsExpectedSignaturesAndLineage(
                "xml/two-signers-in-lineage-invalid-pastSigs-cert-index.xml", 3,
                SECOND_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithMissingPastSigsCertIndex() throws Exception {
        // If the pastSigs cert tag does not contain an index attribute that signature cannot be
        // read but the current signature should still be returned.
        verifyReadXmlReturnsExpectedSignaturesAndLineage(
                "xml/two-signers-in-lineage-missing-pastSigs-cert-index.xml", 3,
                SECOND_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithUndefinedPastSigsIndex() throws Exception {
        // If a cert tag does not contain a key attribute it is assumed that the index attribute
        // refers to a previously seen signature. If a signature does not yet exist at this index
        // then the current signature cannot be read but any other signatures should still be
        // returned.
        verifyReadXmlReturnsExpectedSignatures(
                "xml/two-signers-in-lineage-undefined-pastSigs-index.xml", 3,
                FIRST_EXPECTED_SIGNATURE, null);
    }

    @Test
    public void testReadXmlWithTooFewPastSigsCertTags() throws Exception {
        // If the number of cert tags is less than that specified in the count attribute of the
        // pastSigs tag then the signatures that could be read are copied to a smaller array to be
        // used when building the SigningDetails object. This test verifies if there are too few
        // cert tags the available signatures and lineage can still be obtained.
        verifyReadXmlReturnsExpectedSignaturesAndLineage(
                "xml/three-signers-in-lineage-missing-pastSigs-cert-tag.xml", 3,
                FIRST_EXPECTED_SIGNATURE, THIRD_EXPECTED_SIGNATURE);
    }

    @Test
    public void testReadXmlWithPastSignerWithNoCapabilities() throws Exception {
        // When rotating the signing key a developer is able to specify the capabilities granted to
        // the apps signed with the previous key. This test verifies a previous signing certificate
        // with the flags set to 0 does not have any capabilities.
        XmlPullParser parser = getXMLFromResources("xml/two-signers-in-lineage-no-caps.xml");
        ArrayList<Signature> signatures = new ArrayList<>();
        mPackageSetting.signatures.readXml(parser, signatures);
        // obtain the Signature in the list matching the previous signing certificate
        Signature previousSignature = null;
        for (Signature signature : signatures) {
            String signatureValue = HexDump.toHexString(signature.toByteArray(), false);
            if (signatureValue.equals(FIRST_EXPECTED_SIGNATURE)) {
                previousSignature = signature;
                break;
            }
        }
        assertNotNull("Unable to find the expected previous signer", previousSignature);
        for (int capability : CAPABILITIES) {
            assertFalse("The previous signer should not have the " + capability + " capability",
                    mPackageSetting.signatures.mSigningDetails.hasCertificate(previousSignature,
                            capability));
        }
    }

    /**
     * Verifies reading the sigs tag of the provided XML file returns the specified signature scheme
     * version and the provided signatures.
     */
    private void verifyReadXmlReturnsExpectedSignatures(String xmlFile, int expectedSchemeVersion,
            String... expectedSignatureValues) throws Exception {
        XmlPullParser parser = getXMLFromResources(xmlFile);
        ArrayList<Signature> signatures = new ArrayList<>();
        mPackageSetting.signatures.readXml(parser, signatures);
        Set<String> expectedSignatures = createSetOfSignatures(expectedSignatureValues);
        verifySignaturesContainExpectedValues(signatures, expectedSignatures);
        assertEquals("The returned signature scheme is not the expected value",
                expectedSchemeVersion,
                mPackageSetting.signatures.mSigningDetails.signatureSchemeVersion);
    }

    /**
     * Verifies reading the sigs tag of the provided XML file returns the specified signature scheme
     * version, the provided signatures, and that the previous signers have the expected
     * capabilities.
     */
    private void verifyReadXmlReturnsExpectedSignaturesAndLineage(String xmlFile,
            int schemeVersion, String... expectedSignatureValues) throws Exception {
        XmlPullParser parser = getXMLFromResources(xmlFile);
        ArrayList<Signature> signatures = new ArrayList<>();
        mPackageSetting.signatures.readXml(parser, signatures);
        Set<String> expectedSignatures = createSetOfSignatures(expectedSignatureValues);
        verifySignaturesContainExpectedValues(signatures, expectedSignatures);
        assertEquals("The returned signature scheme is not the expected value", schemeVersion,
                mPackageSetting.signatures.mSigningDetails.signatureSchemeVersion);
        for (Signature signature : signatures) {
            String signatureValue = HexDump.toHexString(signature.toByteArray(), false);
            int expectedCapabilities = SIGNATURE_TO_CAPABILITY_MAP.get(signatureValue);
            assertTrue("The signature " + signatureValue
                            + " was not found with the expected capabilities of " +
                            expectedCapabilities
                            + " in the signing details",
                    mPackageSetting.signatures.mSigningDetails.hasCertificate(signature,
                            expectedCapabilities));
        }
    }

    /**
     * Verifies the provided {@code List} contains Signatures that match the provided hex encoded
     * signature values.
     *
     * The provided {@code Set} will be modified by this method as elements will be removed to
     * ensure duplicate expected Signatures are not in the {@code List}.
     */
    private static void verifySignaturesContainExpectedValues(ArrayList<Signature> signatures,
            Set<String> expectedSignatures) {
        assertEquals("The number of signatures does not equal the expected number of signatures",
                expectedSignatures.size(), signatures.size());
        for (Signature signature : signatures) {
            String signatureString = null;
            if (signature != null) {
                signatureString = HexDump.toHexString(signature.toByteArray(), false);
            }
            // If the signature is in the expected set then remove it so that duplicate matching
            // signatures are reported.
            if (expectedSignatures.contains(signatureString)) {
                expectedSignatures.remove(signatureString);
            } else {
                fail("The following unexpected signature was returned: " + signatureString);
            }
        }
    }

    private static Set<String> createSetOfSignatures(String... signatures) {
        Set<String> result = new HashSet<String>();
        for (String signature : signatures) {
            result.add(signature);
        }
        return result;
    }

    private XmlPullParser getXMLFromResources(String xmlFile) throws Exception {
        InputStream xmlStream = mContext.getResources().getAssets().open(
                TEST_RESOURCES_FOLDER + "/" + xmlFile);
        XmlPullParser result = Xml.newPullParser();
        result.setInput(xmlStream, StandardCharsets.UTF_8.name());
        int type;
        // advance the parser to the first tag
        while ((type = result.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            ;
        }
        return result;
    }

    private static PackageSetting createPackageSetting() {
        // Generic PackageSetting object with values from a test app installed on a device to be
        // used to test the methods under the PackageSignatures signatures data member.
        File appPath = new File("/data/app/app");
        PackageSetting result = new PackageSetting("test.app", null, appPath, appPath,
                "/data/app/app", null, null, null,
                1, 940097092, 0, 0 /*userId*/, null, null);
        return result;
    }
}
