/*
 * Copyright 2016 Analytical Graphics, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * 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 org.keycloak.crypto.def;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.RevokedStatus;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.cert.ocsp.UnknownStatus;
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.models.KeycloakSession;
import org.keycloak.utils.OCSPProvider;

import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.cert.CRLReason;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
 * @version $Revision: 1 $
 * @since 10/29/2016
 */

public class BCOCSPProvider extends OCSPProvider {

    private final static Logger logger = Logger.getLogger(BCOCSPProvider.class.getName());

    protected OCSPResp getResponse(KeycloakSession session, OCSPReq ocspReq, URI responderUri) throws IOException {
        byte[] data = getEncodedOCSPResponse(session, ocspReq.getEncoded(), responderUri);
        return new OCSPResp(data);
    }

    /**
     * Requests certificate revocation status using OCSP.
     * @param cert the certificate to be checked
     * @param issuerCertificate the issuer certificate
     * @param responderURIs the OCSP responder URIs
     * @param responderCert the OCSP responder certificate
     * @param date if null, the current time is used.
     * @return a revocation status
     * @throws CertPathValidatorException
     */
    @Override
    protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException {
        if (responderURIs == null || responderURIs.size() == 0)
            throw new IllegalArgumentException("Need at least one responder");
        try {

            DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().build();

            DigestCalculator digCalc = dcp.get(CertificateID.HASH_SHA1);

            JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber());

            // Create a nounce extension to protect against replay attacks
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            BigInteger nounce = BigInteger.valueOf(Math.abs(random.nextInt()));

            DEROctetString derString = new DEROctetString(nounce.toByteArray());
            Extension nounceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derString);
            Extensions extensions = new Extensions(nounceExtension);

            OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID, extensions).build();

            URI responderURI = responderURIs.get(0);
            logger.log(Level.INFO, "OCSP Responder {0}", responderURI);

            try {
                OCSPResp resp = getResponse(session, ocspReq, responderURI);
                logger.log(Level.FINE, "Received a response from OCSP responder {0}, the response status is {1}", new Object[]{responderURI, resp.getStatus()});
                switch (resp.getStatus()) {
                    case OCSPResp.SUCCESSFUL:
                        if (resp.getResponseObject() instanceof BasicOCSPResp) {
                            return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, nounce, (BasicOCSPResp)resp.getResponseObject());
                        } else {
                            throw new CertPathValidatorException("OCSP responder returned an invalid or unknown OCSP response.");
                        }

                    case OCSPResp.INTERNAL_ERROR:
                    case OCSPResp.TRY_LATER:
                        throw new CertPathValidatorException("Internal error/try later. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS);

                    case OCSPResp.SIG_REQUIRED:
                        throw new CertPathValidatorException("Invalid or missing signature. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.INVALID_SIGNATURE);

                    case OCSPResp.UNAUTHORIZED:
                        throw new CertPathValidatorException("Unauthorized request. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED);

                    case OCSPResp.MALFORMED_REQUEST:
                    default:
                        throw new CertPathValidatorException("OCSP request is malformed. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED);
                }
            }
            catch(IOException e) {
                logger.log(Level.FINE, "OCSP Responder \"{0}\" failed to return a valid OCSP response\n{1}",
                        new Object[] {responderURI, e.getMessage()});
                throw new CertPathValidatorException("OCSP check failed", e);
            }
        }
        catch(CertificateNotYetValidException | CertificateExpiredException | OperatorCreationException | OCSPException |
              CertificateEncodingException | NoSuchAlgorithmException | NoSuchProviderException e) {
            logger.log(Level.FINE, e.getMessage());
            throw new CertPathValidatorException(e.getMessage(), e);
        }
    }

    private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse)
            throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
        SingleResp expectedResponse = null;
        for (SingleResp singleResponse : basicOcspResponse.getResponses()) {
            if (compareCertIDs(certificateID, singleResponse.getCertID())) {
                expectedResponse = singleResponse;
                break;
            }
        }

        if (expectedResponse != null) {
            verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, nounce.toByteArray(), date);
            return singleResponseToRevocationStatus(expectedResponse);
        } else {
            throw new CertPathValidatorException("OCSP response does not include a response for a certificate supplied in the OCSP request");
        }
    }

    private boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) {
        if (idLeft == idRight)
            return true;
        if (idLeft == null || idRight == null)
            return false;

        return Arrays.equals(idLeft.getIssuerKeyHash(), idRight.getIssuerKeyHash()) &&
                Arrays.equals(idLeft.getIssuerNameHash(), idRight.getIssuerNameHash()) &&
                idLeft.getSerialNumber().equals(idRight.getSerialNumber());
    }

    private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {

        List<X509CertificateHolder> certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts()));
        X509Certificate signingCert = null;

        try {
            certs.add(new JcaX509CertificateHolder(issuerCertificate));
            if (responderCertificate != null) {
                certs.add(new JcaX509CertificateHolder(responderCertificate));
            }
        } catch (CertificateEncodingException e) {
            e.printStackTrace();
        }
        if (certs.size() > 0) {

            X500Name responderName = basicOcspResponse.getResponderId().toASN1Primitive().getName();
            byte[] responderKey = basicOcspResponse.getResponderId().toASN1Primitive().getKeyHash();

            if (responderName != null) {
                logger.log(Level.INFO, "Responder Name: {0}", responderName.toString());
                for (X509CertificateHolder certHolder : certs) {
                    try {
                        X509Certificate tempCert = new JcaX509CertificateConverter()
                                .setProvider(BouncyIntegration.PROVIDER).getCertificate(certHolder);
                        X500Name respName = new X500Name(tempCert.getSubjectX500Principal().getName());
                        if (responderName.equals(respName)) {
                            signingCert = tempCert;
                            logger.log(Level.INFO, "Found a certificate whose principal \"{0}\" matches the responder name \"{1}\"",
                                    new Object[] {tempCert.getSubjectDN().getName(), responderName.toString()});
                            break;
                        }
                    } catch (CertificateException e) {
                        logger.log(Level.FINE, e.getMessage());
                    }
                }
            } else if (responderKey != null) {
                SubjectKeyIdentifier responderSubjectKey = new SubjectKeyIdentifier(responderKey);
                logger.log(Level.INFO, "Responder Key: {0}", Arrays.toString(responderKey));
                for (X509CertificateHolder certHolder : certs) {
                    try {
                        X509Certificate tempCert = new JcaX509CertificateConverter()
                                .setProvider(BouncyIntegration.PROVIDER).getCertificate(certHolder);

                        SubjectKeyIdentifier subjectKeyIdentifier = null;
                        if (certHolder.getExtensions() != null) {
                            subjectKeyIdentifier = SubjectKeyIdentifier.fromExtensions(certHolder.getExtensions());
                        }

                        if (subjectKeyIdentifier != null) {
                            logger.log(Level.INFO, "Certificate: {0}\nSubject Key Id: {1}",
                                    new Object[] {tempCert.getSubjectDN().getName(), Arrays.toString(subjectKeyIdentifier.getKeyIdentifier())});
                        }

                        if (subjectKeyIdentifier != null && responderSubjectKey.equals(subjectKeyIdentifier)) {
                            signingCert = tempCert;
                            logger.log(Level.INFO, "Found a signer certificate \"{0}\" with the subject key extension value matching the responder key",
                                    signingCert.getSubjectDN().getName());

                            break;
                        }

                        subjectKeyIdentifier = new JcaX509ExtensionUtils().createSubjectKeyIdentifier(tempCert.getPublicKey());
                        if (responderSubjectKey.equals(subjectKeyIdentifier)) {
                            signingCert = tempCert;
                            logger.log(Level.INFO, "Found a certificate \"{0}\" with the subject key matching the OCSP responder key", signingCert.getSubjectDN().getName());
                            break;
                        }

                    } catch (CertificateException e) {
                        logger.log(Level.FINE, e.getMessage());
                    }
                }
            }
        }
        if (signingCert != null) {
            if (signingCert.equals(issuerCertificate)) {
                logger.log(Level.INFO, "OCSP response is signed by the target''s Issuing CA");
            } else if (responderCertificate != null && signingCert.equals(responderCertificate)) {
                // https://www.ietf.org/rfc/rfc2560.txt
                // 2.6  OCSP Signature Authority Delegation
                // - The responder certificate is issued to the responder by CA
                logger.log(Level.INFO, "OCSP response is signed by an authorized responder certificate");
            } else {
                // 4.2.2.2  Authorized Responders
                // 3. Includes a value of id-ad-ocspSigning in an ExtendedKeyUsage
                // extension and is issued by the CA that issued the certificate in
                // question."
                if (!signingCert.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) {
                    logger.log(Level.INFO, "Signer certificate''s Issuer: {0}\nIssuer certificate''s Subject: {1}",
                            new Object[] {signingCert.getIssuerX500Principal().getName(), issuerCertificate.getSubjectX500Principal().getName()});
                    throw new CertPathValidatorException("Responder\'s certificate is not authorized to sign OCSP responses");
                }
                try {
                    List<String> purposes = signingCert.getExtendedKeyUsage();
                    if (purposes == null || !purposes.contains(KeyPurposeId.id_kp_OCSPSigning.getId())) {
                        logger.log(Level.INFO, "OCSPSigning extended usage is not set");
                        throw new CertPathValidatorException("Responder\'s certificate not valid for signing OCSP responses");
                    }
                } catch (CertificateParsingException e) {
                    logger.log(Level.FINE, "Failed to get certificate''s extended key usage extension\n{0}", e.getMessage());
                }
                if (date == null) {
                    signingCert.checkValidity();
                } else {
                    signingCert.checkValidity(date);
                }
                try {
                    Extension noOCSPCheck = new JcaX509CertificateHolder(signingCert).getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck);
                    // TODO If the extension is present, the OCSP client can trust the
                    // responder's certificate for the lifetime of the certificate.
                    logger.log(Level.INFO, "OCSP no-check extension is {0} present", noOCSPCheck == null ? "not" : "");
                } catch (CertificateEncodingException e) {
                    logger.log(Level.FINE, "Certificate encoding exception: {0}", e.getMessage());
                }

                try {
                    signingCert.verify(issuerCertificate.getPublicKey());
                    logger.log(Level.INFO, "OCSP response is signed by an Authorized Responder");

                } catch (GeneralSecurityException ex) {
                    signingCert = null;
                }
            }
        }
        if (signingCert == null) {
            throw new CertPathValidatorException("Unable to verify OCSP Response\'s signature");
        } else {
            if (!verifySignature(basicOcspResponse, signingCert)) {
                throw new CertPathValidatorException("Error verifying OCSP Response\'s signature");
            } else {
                Extension responseNonce = basicOcspResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
                if (responseNonce != null && requestNonce != null && !Arrays.equals(requestNonce, responseNonce.getExtnValue().getOctets())) {
                    throw new CertPathValidatorException("Nonces do not match.");
                } else {
                    // See Sun's OCSP implementation.
                    // https://www.ietf.org/rfc/rfc2560.txt, if nextUpdate is not set,
                    // the responder is indicating that newer update is avilable all the time
                    long current = date == null ? System.currentTimeMillis() : date.getTime();
                    Date stop = new Date(current + (long) TIME_SKEW);
                    Date start = new Date(current - (long) TIME_SKEW);

                    Iterator<SingleResp> iter = Arrays.asList(basicOcspResponse.getResponses()).iterator();
                    SingleResp singleRes = null;
                    do {
                        if (!iter.hasNext()) {
                            return;
                        }
                        singleRes = iter.next();
                    }
                    while (!stop.before(singleRes.getThisUpdate()) &&
                            !start.after(singleRes.getNextUpdate() != null ? singleRes.getNextUpdate() : singleRes.getThisUpdate()));

                    throw new CertPathValidatorException("Response is unreliable: its validity interval is out-of-date");
                }
            }
        }
    }

    private boolean verifySignature(BasicOCSPResp basicOcspResponse, X509Certificate cert) {
        try {
            ContentVerifierProvider contentVerifier = new JcaContentVerifierProviderBuilder()
                    .setProvider(BouncyIntegration.PROVIDER).build(cert.getPublicKey());
            return basicOcspResponse.isSignatureValid(contentVerifier);
        } catch (OperatorCreationException e) {
            logger.log(Level.FINE, "Unable to construct OCSP content signature verifier\n{0}", e.getMessage());
        } catch (OCSPException e) {
            logger.log(Level.FINE, "Unable to validate OCSP response signature\n{0}", e.getMessage());
        }
        return false;
    }

    private OCSPRevocationStatus singleResponseToRevocationStatus(final SingleResp singleResponse) throws CertPathValidatorException {
        final CertificateStatus certStatus = singleResponse.getCertStatus();

        CRLReason revocationReason = CRLReason.UNSPECIFIED;
        Date revocationTime = null;
        RevocationStatus status = RevocationStatus.UNKNOWN;
        if (certStatus == CertificateStatus.GOOD) {
            status = RevocationStatus.GOOD;
        } else if (certStatus instanceof RevokedStatus) {
            RevokedStatus revoked = (RevokedStatus)certStatus;
            revocationTime = revoked.getRevocationTime();
            status = RevocationStatus.REVOKED;
            if (revoked.hasRevocationReason()) {
                revocationReason = CRLReason.values()[revoked.getRevocationReason()];
            }
        } else if (certStatus instanceof UnknownStatus) {
            status = RevocationStatus.UNKNOWN;
        } else {
            throw new CertPathValidatorException("Unrecognized revocation status received from OCSP.");
        }

        final RevocationStatus finalStatus = status;
        final Date finalRevocationTime = revocationTime;
        final CRLReason finalRevocationReason = revocationReason;
        return new OCSPRevocationStatus() {
            @Override
            public RevocationStatus getRevocationStatus() {
                return finalStatus;
            }

            @Override
            public Date getRevocationTime() {
                return finalRevocationTime;
            }

            @Override
            public CRLReason getRevocationReason() {
                return finalRevocationReason;
            }
        };
    }


    /**
     * Extracts OCSP responder URI from X509 AIA v3 extension, if available. There can be
     * multiple responder URIs encoded in the certificate.
     * @param cert
     * @return a list of available responder URIs.
     * @throws CertificateEncodingException
     */
    @Override
    protected List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException {

        LinkedList<String> responderURIs = new LinkedList<>();
        JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert);
        Extension aia = holder.getExtension(Extension.authorityInfoAccess);
        if (aia != null) {
            try {
                ASN1InputStream in = new ASN1InputStream(aia.getExtnValue().getOctetStream());
                ASN1Sequence seq = (ASN1Sequence)in.readObject();
                AuthorityInformationAccess authorityInfoAccess = AuthorityInformationAccess.getInstance(seq);
                for (AccessDescription ad : authorityInfoAccess.getAccessDescriptions()) {
                    if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) {
                        // See https://www.ietf.org/rfc/rfc2560.txt, 3.1 Certificate Content
                        if (ad.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) {
                            DERIA5String value = DERIA5String.getInstance(ad.getAccessLocation().getName());
                            responderURIs.add(value.getString());
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return responderURIs;
    }
}
