/*
 * Copyright (C) 2017 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.locksettings.recoverablekeystore.certificate;

import static com.google.common.truth.Truth.assertThat;

import static java.nio.charset.StandardCharsets.UTF_8;

import static org.testng.Assert.assertThrows;
import static org.testng.Assert.expectThrows;

import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import java.io.InputStream;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.cert.CertPath;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.w3c.dom.Element;

@SmallTest
@RunWith(AndroidJUnit4.class)
public final class CertUtilsTest {

    private static final String XML_STR = ""
            + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
            + "<!-- comment 1 -->"
            + "<root>\n\n\n\r\r\r"
            + "  <node1>\r\n\r\n"
            + "    node1-1</node1>"
            + "  <!-- comment 2 -->"
            + "  <node1>node1-2"
            + "    \n\r\n\r</node1>"
            + "  <node2>"
            + "    <node1>   node2-node1-1</node1>"
            + "    <node1>node2-node1-2   </node1>"
            + "    <!-- comment 3 -->"
            + "    <node1>   node2-node1-3   </node1>"
            + "  </node2>"
            + "</root>";

    private static final String SIGNED_STR = "abcdefg\n";
    private static final String SIGNATURE_BASE64 = ""
            + "KxBt9B3pwL3/59SrjTJTpuhc9JRxLOUNwNr3J4EEdXj5BqkYOUeXIOjyBGp8XaOnmuW8WmBxhko3"
            + "yTR3/M9x0/pJuKDgqQSSFG+I56O/IAri7DmMBfY8QqcgiF8RaR86G7mWXUIdu8ixEtpKa//T4bN7"
            + "c8Txvt96ApAcW0wJDihfCqDEXyi56pFCp+qEZuL4fS8iZtZTUkvxim1tb2/IsZ9OyDd9BWxp+JTs"
            + "zihzH6xqnUCa1aELSUZnU8OzWGeuKpVDQDbDMtQpcxJ9o+6L6wO5vmQutZAulgw5gRPGhYWVs8+0"
            + "ATdNEbv8TSomkXkZ3/lMYnmPXKmaHxcP4330DA==";
    private static final PublicKey SIGNER_PUBLIC_KEY = TestData.INTERMEDIATE_CA_2.getPublicKey();

    @Test
    public void decodeCert_readPemFile_succeeds_singleBlock() throws Exception {
        InputStream f = TestData.openTestFile("pem/valid-cert.pem");
        X509Certificate cert = CertUtils.decodeCert(f);
        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
    }

    @Test
    public void decodeCert_readPemFile_succeeds_multipleBlocks() throws Exception {
        InputStream in = TestData.openTestFile("pem/valid-cert-multiple-blocks.pem");
        X509Certificate cert = CertUtils.decodeCert(in);
        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
    }

    @Test
    public void decodeCert_readPemFile_throwsIfNoBeginEndLines() throws Exception {
        InputStream in = TestData.openTestFile("pem/invalid-cert-1-no-begin-end.pem");
        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
    }

    @Test
    public void decodeCert_readPemFile_throwsIfEmptyBlock() throws Exception {
        InputStream in = TestData.openTestFile("pem/invalid-cert-2-empty-block.pem");
        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
    }

    @Test
    public void decodeCert_readPemFile_throwsIfInvalidCert() throws Exception {
        InputStream in = TestData.openTestFile("pem/invalid-cert-3-invalid-key.pem");
        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
    }

    @Test
    public void decodeCert_readBytes_succeeds() throws Exception {
        X509Certificate cert = CertUtils.decodeCert(TestData.INTERMEDIATE_CA_2.getEncoded());
        assertThat(cert.getIssuerX500Principal().getName())
                .isEqualTo("CN=Google CryptAuthVault Intermediate");
    }

    @Test
    public void decodeCert_readBytes_throwsIfInvalidCert() throws Exception {
        byte[] modifiedCertBytes = TestData.INTERMEDIATE_CA_1.getEncoded();
        modifiedCertBytes[0] ^= (byte) 1;
        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(modifiedCertBytes));
    }

    @Test
    public void decodeBase64_succeeds() throws Exception {
        assertThat(CertUtils.decodeBase64("VEVTVA==")).isEqualTo("TEST".getBytes(UTF_8));
    }

    @Test
    public void decodeBase64_succeedsIfEmptyInput() throws Exception {
        assertThat(CertUtils.decodeBase64("")).hasLength(0);
    }

    @Test
    public void decodeBase64_throwsIfInvalidInput() throws Exception {
        assertThrows(CertParsingException.class, () -> CertUtils.decodeBase64("EVTVA=="));
    }

    @Test
    public void getXmlRootNode_succeeds() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        assertThat(root.getTagName()).isEqualTo("root");
    }

    @Test
    public void getXmlRootNode_throwsIfEmptyInput() throws Exception {
        assertThrows(CertParsingException.class, () -> CertUtils.getXmlRootNode(new byte[0]));
    }

    @Test
    public void getXmlNodeContents_singleLevel_succeeds() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        assertThat(CertUtils.getXmlNodeContents(CertUtils.MUST_EXIST_UNENFORCED, root, "node1"))
                .containsExactly("node1-1", "node1-2");
    }

    @Test
    public void getXmlNodeContents_multipleLevels_succeeds() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        assertThat(CertUtils.getXmlNodeContents(CertUtils.MUST_EXIST_UNENFORCED, root, "node2", "node1"))
                .containsExactly("node2-node1-1", "node2-node1-2", "node2-node1-3");
    }

    @Test
    public void getXmlNodeContents_mustExistFalse_succeedsIfNotExist() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        assertThat(
                CertUtils.getXmlNodeContents(
                        CertUtils.MUST_EXIST_UNENFORCED, root, "node2", "node-not-exist"))
                .isEmpty();
    }

    @Test
    public void getXmlNodeContents_mustExistAtLeastOne_throwsIfNotExist() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        CertParsingException expected =
                expectThrows(
                        CertParsingException.class,
                        () ->
                                CertUtils.getXmlNodeContents(
                                        CertUtils.MUST_EXIST_AT_LEAST_ONE, root, "node2",
                                        "node-not-exist"));
        assertThat(expected.getMessage()).contains("must contain at least one");
    }

    @Test
    public void getXmlNodeContents_mustExistExactlyOne_throwsIfNotExist() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        CertParsingException expected =
                expectThrows(
                        CertParsingException.class,
                        () ->
                                CertUtils.getXmlNodeContents(
                                        CertUtils.MUST_EXIST_EXACTLY_ONE, root, "node-not-exist",
                                        "node1"));
        assertThat(expected.getMessage()).contains("must contain exactly one");
    }

    @Test
    public void getXmlNodeContents_mustExistExactlyOne_throwsIfMultipleExist() throws Exception {
        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
        CertParsingException expected =
                expectThrows(
                        CertParsingException.class,
                        () ->
                                CertUtils.getXmlNodeContents(
                                        CertUtils.MUST_EXIST_EXACTLY_ONE, root, "node2", "node1"));
        assertThat(expected.getMessage()).contains("must contain exactly one");
    }

    @Test
    public void verifyRsaSha256Signature_succeeds() throws Exception {
        CertUtils.verifyRsaSha256Signature(
                SIGNER_PUBLIC_KEY,
                Base64.getDecoder().decode(SIGNATURE_BASE64),
                SIGNED_STR.getBytes(UTF_8));
    }

    @Test
    public void verifyRsaSha256Signature_throwsIfMismatchSignature() throws Exception {
        byte[] modifiedBytes = SIGNED_STR.getBytes(UTF_8);
        modifiedBytes[0] ^= (byte) 1;
        assertThrows(
                CertValidationException.class,
                () ->
                        CertUtils.verifyRsaSha256Signature(
                                SIGNER_PUBLIC_KEY, Base64.getDecoder().decode(SIGNATURE_BASE64),
                                modifiedBytes));
    }

    @Test
    public void verifyRsaSha256Signature_throwsIfWrongKeyType() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
        keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
        PublicKey publicKey = keyPairGenerator.generateKeyPair().getPublic();
        assertThrows(
                CertValidationException.class,
                () ->
                        CertUtils.verifyRsaSha256Signature(
                                publicKey,
                                Base64.getDecoder().decode(SIGNATURE_BASE64),
                                SIGNED_STR.getBytes(UTF_8)));
    }

    @Test
    public void buildCertPath_succeedsWithoutIntermediates() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
        X509Certificate leafCert = TestData.INTERMEDIATE_CA_1;
        CertPath certPath = CertUtils.buildCertPath(
                CertUtils.buildPkixParams(
                        TestData.DATE_ALL_CERTS_VALID, rootCert, Collections.emptyList(),
                        leafCert));
        assertThat(certPath.getCertificates()).containsExactly(
                TestData.INTERMEDIATE_CA_1).inOrder();
    }

    @Test
    public void buildCertPath_succeedsWithIntermediates() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
        List<X509Certificate> intermediateCerts =
                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
        X509Certificate leafCert = TestData.LEAF_CERT_2;
        CertPath certPath =
                CertUtils.buildCertPath(
                        CertUtils.buildPkixParams(
                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
                                leafCert));
        assertThat(certPath.getCertificates())
                .containsExactly(
                        TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
                        TestData.INTERMEDIATE_CA_1)
                .inOrder();
    }

    @Test
    public void buildCertPath_succeedsWithIntermediates_ignoreUnrelatedIntermedateCert()
            throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
        List<X509Certificate> intermediateCerts =
                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
        X509Certificate leafCert = TestData.LEAF_CERT_1;
        CertPath certPath =
                CertUtils.buildCertPath(
                        CertUtils.buildPkixParams(
                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
                                leafCert));
        assertThat(certPath.getCertificates())
                .containsExactly(TestData.LEAF_CERT_1, TestData.INTERMEDIATE_CA_1)
                .inOrder();
    }

    @Test
    public void buildCertPath_throwsIfWrongRootCommonName() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
        List<X509Certificate> intermediateCerts =
                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
        X509Certificate leafCert = TestData.LEAF_CERT_1;

        assertThrows(
                CertValidationException.class,
                () ->
                        CertUtils.buildCertPath(
                                CertUtils.buildPkixParams(
                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
                                        leafCert)));
    }

    @Test
    public void buildCertPath_throwsIfMissingIntermediateCert() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
        List<X509Certificate> intermediateCerts = Collections.emptyList();
        X509Certificate leafCert = TestData.LEAF_CERT_1;

        assertThrows(
                CertValidationException.class,
                () ->
                        CertUtils.buildCertPath(
                                CertUtils.buildPkixParams(
                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
                                        leafCert)));
    }

    @Test
    public void validateCert_succeeds() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
        List<X509Certificate> intermediateCerts =
                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
        X509Certificate leafCert = TestData.LEAF_CERT_2;
        CertUtils.validateCert(TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
                leafCert);
    }

    @Test
    public void validateCert_throwsIfExpired() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
        List<X509Certificate> intermediateCerts =
                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
        X509Certificate leafCert = TestData.LEAF_CERT_2;
        assertThrows(
                CertValidationException.class,
                () ->
                        CertUtils.validateCert(
                                TestData.DATE_LEAF_CERT_2_EXPIRED, rootCert, intermediateCerts,
                                leafCert));
    }

    @Test
    public void validateCert_throwsIfWrongRootWithTheSameCommonName() throws Exception {
        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_KEY;
        List<X509Certificate> intermediateCerts =
                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
        X509Certificate leafCert = TestData.LEAF_CERT_2;
        assertThrows(
                CertValidationException.class,
                () ->
                        CertUtils.validateCert(
                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
                                leafCert));
    }
}
