/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */
package org.elasticsearch.xpack.ssl;

import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.test.ESTestCase;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigInteger;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * Unit tests for cert utils
 */
public class CertUtilsTests extends ESTestCase {

    public void testSerialNotRepeated() {
        int iterations = scaledRandomIntBetween(10, 100);
        List<BigInteger> list = new ArrayList<>(iterations);
        for (int i = 0; i < iterations; i++) {
            BigInteger serial = CertUtils.getSerial();
            assertThat(list.contains(serial), is(false));
            list.add(serial);
        }
    }

    public void testGenerateKeyPair() throws Exception {
        KeyPair keyPair = CertUtils.generateKeyPair(randomFrom(1024, 2048));
        assertThat(keyPair.getPrivate().getAlgorithm(), is("RSA"));
        assertThat(keyPair.getPublic().getAlgorithm(), is("RSA"));
    }

    public void testReadKeysCorrectly() throws Exception {
        // read in keystore version
        Path keystorePath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks");
        Key key;
        try (InputStream in = Files.newInputStream(keystorePath)) {
            KeyStore keyStore = KeyStore.getInstance("jks");
            keyStore.load(in, "testnode".toCharArray());
            key = keyStore.getKey("testnode", "testnode".toCharArray());
        }
        assertThat(key, notNullValue());
        assertThat(key, instanceOf(PrivateKey.class));

        PrivateKey privateKey;
        try (Reader reader =
                     Files.newBufferedReader(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.pem"),
                             StandardCharsets.UTF_8)) {
            privateKey = CertUtils.readPrivateKey(reader, "testnode"::toCharArray);
        }
        assertThat(privateKey, notNullValue());
        assertThat(privateKey, equalTo(key));
    }

    public void testReadCertsCorrectly() throws Exception {
        // read in keystore version
        Path keystorePath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks");
        Certificate certificate;
        try (InputStream in = Files.newInputStream(keystorePath)) {
            KeyStore keyStore = KeyStore.getInstance("jks");
            keyStore.load(in, "testnode".toCharArray());
            certificate = keyStore.getCertificate("testnode");
        }
        assertThat(certificate, notNullValue());
        assertThat(certificate, instanceOf(X509Certificate.class));

        Certificate pemCert;
        try (Reader reader =
                     Files.newBufferedReader(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"),
                             StandardCharsets.UTF_8)) {
            List<Certificate> certificateList = new ArrayList<>(1);
            CertUtils.readCertificates(reader, certificateList, CertificateFactory.getInstance("X.509"));
            assertThat(certificateList.size(), is(1));
            pemCert = certificateList.get(0);
        }
        assertThat(pemCert, notNullValue());
        assertThat(pemCert, equalTo(certificate));
    }

    public void testSubjectAlternativeNames() throws Exception {
        final boolean resolveName = randomBoolean();
        InetAddress address = InetAddresses.forString("127.0.0.1");

        GeneralNames generalNames = CertUtils.getSubjectAlternativeNames(resolveName, Collections.singleton(address));
        assertThat(generalNames, notNullValue());
        GeneralName[] generalNameArray = generalNames.getNames();
        assertThat(generalNameArray, notNullValue());

        logger.info("resolve name [{}], address [{}], subject alt names [{}]", resolveName, NetworkAddress.format(address), generalNames);
        if (resolveName && isResolvable(address)) {
            assertThat(generalNameArray.length, is(2));
            int firstType = generalNameArray[0].getTagNo();
            if (firstType == GeneralName.iPAddress) {
                assertThat(generalNameArray[1].getTagNo(), is(GeneralName.dNSName));
            } else if (firstType == GeneralName.dNSName) {
                assertThat(generalNameArray[1].getTagNo(), is(GeneralName.iPAddress));
            } else {
                fail("unknown tag value: " + firstType);
            }
        } else {
            assertThat(generalNameArray.length, is(1));
            assertThat(generalNameArray[0].getTagNo(), is(GeneralName.iPAddress));
        }
    }

    @SuppressForbidden(reason = "need to use getHostName to resolve DNS name and getHostAddress to ensure we resolved the name")
    private boolean isResolvable(InetAddress inetAddress) {
        String hostname = inetAddress.getHostName();
        return hostname.equals(inetAddress.getHostAddress()) == false;
    }

    public void testIsAnyLocalAddress() throws Exception {
        InetAddress address = mock(InetAddress.class);
        when(address.isAnyLocalAddress()).thenReturn(true);

        GeneralNames generalNames = CertUtils.getSubjectAlternativeNames(randomBoolean(), Collections.singleton(address));
        assertThat(generalNames, notNullValue());
        GeneralName[] generalNameArray = generalNames.getNames();
        assertThat(generalNameArray, notNullValue());

        verify(address).isAnyLocalAddress();
        verifyNoMoreInteractions(address);
    }

    public void testReadEllipticCurveCertificateAndKey() throws Exception {
        Path keyPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/prime256v1-key.pem");
        try (Reader reader = Files.newBufferedReader(keyPath)) {
            verifyPrime256v1ECKey(reader);
        }

        Path keyNoSpecPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/prime256v1-key-noparam.pem");
        try (Reader reader = Files.newBufferedReader(keyNoSpecPath)) {
            verifyPrime256v1ECKey(reader);
        }

        Path certPath = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/prime256v1-cert.pem");
        Certificate[] certs = CertUtils.readCertificates(Collections.singletonList(certPath.toString()), null);
        assertEquals(1, certs.length);
        Certificate cert = certs[0];
        assertNotNull(cert);
        assertEquals("EC", cert.getPublicKey().getAlgorithm());
    }

    private void verifyPrime256v1ECKey(Reader reader) throws Exception {
        PrivateKey privateKey = CertUtils.readPrivateKey(reader, () -> null);
        assertNotNull(privateKey);
        assertEquals("ECDSA", privateKey.getAlgorithm());
        assertThat(privateKey, instanceOf(ECPrivateKey.class));
        ECPrivateKey ecPrivateKey = (ECPrivateKey) privateKey;
        assertThat(ecPrivateKey.getParams(), instanceOf(ECNamedCurveSpec.class));
        ECNamedCurveSpec namedCurveSpec = (ECNamedCurveSpec) ecPrivateKey.getParams();
        assertEquals("prime256v1", namedCurveSpec.getName());
    }
}
