/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.synapse.transport.utils.sslcert;

import junit.framework.TestCase;
import org.apache.synapse.transport.utils.sslcert.crl.CRLCache;
import org.apache.synapse.transport.utils.sslcert.crl.CRLVerifier;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.x509.X509V2CRLGenerator;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure;

import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;

public class CRLVerifierTest extends TestCase {

    /**
     * To test CRLVerifier behaviour when a revoked certificate is given, a fake certificate will
     * be created, signed by a fake root certificate. To make our life easy,
     * the CrlDistributionPoint extension will be extracted from the real peer certificate in
     * resources directory and copied to the fake certificate as a certificate extension. So the
     * criDistributionPointURL in the fake certificate will be the same as in the real certificate.
     * The created X509CRL object will be put to CRLCache against the criDistributionPointURL.
     * Since the crl is in the cache, there will NOT be a remote call to the CRL server at
     * criDistributionPointURL.
     *
     * @throws Exception
     */
    public void testRevokedCertificate() throws Exception {

        //Add BouncyCastle as Security Provider.
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        Utils utils = new Utils();
        //Create X509Certificate from the real certificate file in resource folder.
        X509Certificate realPeerCertificate = utils.getRealPeerCertificate();
        //Extract crlDistributionPointUrl from the real peer certificate.
        String crlDistributionPointUrl = getCRLDistributionPointUrl(realPeerCertificate);

        //Create fake CA certificate.
        KeyPair caKeyPair = utils.generateRSAKeyPair();
        X509Certificate fakeCACert = utils.generateFakeRootCert(caKeyPair);

        //Create fake peer certificate signed by the fake CA private key. This will be a revoked
        // certificate.
        KeyPair peerKeyPair = utils.generateRSAKeyPair();
        BigInteger revokedSerialNumber = BigInteger.valueOf(111);
        X509Certificate fakeRevokedCertificate = generateFakePeerCert(revokedSerialNumber,
                peerKeyPair.getPublic(),
                caKeyPair.getPrivate(), fakeCACert, realPeerCertificate);

        //Create a crl with fakeRevokedCertificate marked as revoked.
        X509CRL x509CRL = createCRL(fakeCACert, caKeyPair.getPrivate(), revokedSerialNumber);

        CRLCache cache = CRLCache.getCache();
        cache.init(5, 5);
        cache.setCacheValue(crlDistributionPointUrl, x509CRL);

        CRLVerifier crlVerifier = new CRLVerifier(cache);
        RevocationStatus status = crlVerifier.checkRevocationStatus(fakeRevokedCertificate, null);

        //the fake crl we created will be checked if the fake certificate is revoked. So the
        // status should be REVOKED.
        assertTrue(status == RevocationStatus.REVOKED);
    }

    /**
     * This will use Reflection to call getCrlDistributionPoints() private method in CRLVerifier.
     *
     * @param certificate is a certificate with a proper CRLDistributionPoints extension.
     * @return the extracted cRLDistributionPointUrl.
     * @throws Exception
     */
    private String getCRLDistributionPointUrl(X509Certificate certificate) throws Exception {

        CRLVerifier crlVerifier = new CRLVerifier(null);
        // use reflection since getCrlDistributionPoints() is private.
        Class<? extends CRLVerifier> crlVerifierClass = crlVerifier.getClass();
        Method getCrlDistributionPoints = crlVerifierClass.getDeclaredMethod
                ("getCrlDistributionPoints", X509Certificate.class);
        getCrlDistributionPoints.setAccessible(true);

        //getCrlDistributionPoints(..) returns a list of urls. Get the first one.
        List<String> distPoints = (List<String>) getCrlDistributionPoints.invoke(crlVerifier,
                certificate);
        return distPoints.get(0);
    }

    /**
     * Creates a fake CRL for the fake CA. The fake certificate with the given
     * revokedSerialNumber will be marked
     * as Revoked in the returned CRL.
     *
     * @param caCert              the fake CA certificate.
     * @param caPrivateKey        private key of the fake CA.
     * @param revokedSerialNumber the serial number of the fake peer certificate made to be
     *                            marked as revoked.
     * @return the created fake CRL
     * @throws Exception
     */
    public static X509CRL createCRL(X509Certificate caCert, PrivateKey caPrivateKey,
                                    BigInteger revokedSerialNumber)
            throws Exception {

        X509V2CRLGenerator crlGen = new X509V2CRLGenerator();
        Date now = new Date();
        crlGen.setIssuerDN(caCert.getSubjectX500Principal());
        crlGen.setThisUpdate(now);
        crlGen.setNextUpdate(new Date(now.getTime() + TestConstants.NEXT_UPDATE_PERIOD));
        crlGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
        crlGen.addCRLEntry(revokedSerialNumber, now, CRLReason.privilegeWithdrawn);
        crlGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
                new AuthorityKeyIdentifierStructure(caCert));
        crlGen.addExtension(X509Extensions.CRLNumber, false, new CRLNumber(BigInteger.valueOf(1)));

        return crlGen.generateX509CRL(caPrivateKey, "BC");
    }

    public X509Certificate generateFakePeerCert(BigInteger serialNumber, PublicKey entityKey,
                                                PrivateKey caKey, X509Certificate caCert,
                                                X509Certificate firstCertificate)
            throws Exception {

        Utils utils = new Utils();
        X509V3CertificateGenerator certGen = utils.getUsableCertificateGenerator(caCert,
                entityKey, serialNumber);
        certGen.copyAndAddExtension(new ASN1ObjectIdentifier(X509Extensions.CRLDistributionPoints
                .getId()), false, firstCertificate);

        return certGen.generateX509Certificate(caKey, "BC");
    }

}
