/*
* The contents of this file are subject to the Common Public License Version 1.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.eclipse.org/legal/cpl-v10.html
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR APARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*  Copyright (C) 2017 Donovan Lampa <donovan.lampa@gmail.com>
*  Copyright (C) 2009-2017 The JRuby Team
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
*
*
* JRuby-OpenSSL includes software by The Legion of the Bouncy Castle Inc.
* Please, visit (http://bouncycastle.org/license.html) for licensing details.
*/
package org.jruby.ext.openssl;

import static org.jruby.ext.openssl.Digest._Digest;
import static org.jruby.ext.openssl.OCSP._OCSP;
import static org.jruby.ext.openssl.OCSP.newOCSPError;
import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
import static org.jruby.ext.openssl.X509._X509;

import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.Signature;
import org.bouncycastle.asn1.ocsp.TBSRequest;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.openssl.x509store.X509AuxCertificate;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

/*
 * An OpenSSL::OCSP::Request contains the certificate information for determining 
 * if a certificate has been revoked or not. A Request can be created for a 
 * certificate or from a DER-encoded request created elsewhere.
 * 
 * @author lampad
 */
public class OCSPRequest extends RubyObject {
    private static final long serialVersionUID = -4020616730425816999L;

    private static ObjectAllocator REQUEST_ALLOCATOR = new ObjectAllocator() {
        public IRubyObject allocate(Ruby runtime, RubyClass klass) {
            return new OCSPRequest(runtime, klass);
        }
    };
    
    public OCSPRequest(Ruby runtime, RubyClass metaClass) {
        super(runtime, metaClass);
    }
    
    public static void createRequest(final Ruby runtime, final RubyModule _OCSP) {
        RubyClass _request = _OCSP.defineClassUnder("Request", runtime.getObject(), REQUEST_ALLOCATOR);
        _request.defineAnnotatedMethods(OCSPRequest.class);
    }
    
    private final static String OCSP_NOCERTS = "NOCERTS";
    private final static String OCSP_NOSIGS = "NOSIGS";
    private final static String OCSP_NOINTERN = "NOINTERN";
    private final static String OCSP_NOVERIFY = "NOVERIFY";
    private final static String OCSP_TRUSTOTHER = "TRUSTOTHER";
    private final static String OCSP_NOCHAIN = "NOCHAIN";
    private org.bouncycastle.asn1.ocsp.OCSPRequest asn1bcReq;
    private List<OCSPCertificateId> certificateIds = new ArrayList<OCSPCertificateId>();
    private byte[] nonce;
    
    @JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, IRubyObject[] args) {
        Ruby runtime = context.getRuntime();
        
        if ( Arity.checkArgumentCount(runtime, args, 0, 1) == 0 ) return this;
        
        RubyString derString = StringHelper.readPossibleDERInput(context, args[0]);
        asn1bcReq = org.bouncycastle.asn1.ocsp.OCSPRequest.getInstance(derString.getBytes());
        
        return this;
    }
    
    @JRubyMethod(name = "add_certid")
    public IRubyObject add_certid(IRubyObject certId) {
        Ruby runtime = getRuntime();
        OCSPCertificateId rubyCertId = (OCSPCertificateId) certId;
        certificateIds.add(rubyCertId);
        
        OCSPReqBuilder builder = new OCSPReqBuilder();
        for (OCSPCertificateId certificateId : certificateIds) {
            builder.addRequest(new CertificateID(certificateId.getCertID()));
        }
        
        try {
            asn1bcReq = org.bouncycastle.asn1.ocsp.OCSPRequest.getInstance(builder.build().getEncoded());
        }
        catch (Exception e) {
            throw newOCSPError(runtime, e);
        }
        
        if (nonce != null) {
            addNonceImpl();
        }
        return this;
    }

    @JRubyMethod(name = "add_nonce", rest = true)
    public IRubyObject add_nonce(IRubyObject[] args) {
        Ruby runtime = getRuntime();
        
        if ( Arity.checkArgumentCount(runtime, args, 0, 1) == 0 ) {
            nonce = generateNonce();
        }
        else {
            RubyString input = (RubyString)args[0];
            nonce = input.getBytes();
        }
        
        addNonceImpl();
        return this;
    }
    
    // BC doesn't have support for nonces... gotta do things manually
    private void addNonceImpl() {
        GeneralName requestorName = null;
        ASN1Sequence requestList = new DERSequence();
        Extensions extensions = null;
        Signature sig = null;
        List<Extension> tmpExtensions = new ArrayList<Extension>();
        
        if (asn1bcReq != null) {
            TBSRequest currentTbsReq = asn1bcReq.getTbsRequest();
            extensions = currentTbsReq.getRequestExtensions();
            sig = asn1bcReq.getOptionalSignature();        
            Enumeration<ASN1ObjectIdentifier> oids = extensions.oids();
            while (oids.hasMoreElements()) {
                tmpExtensions.add(extensions.getExtension(oids.nextElement()));
            }
        }
        
        tmpExtensions.add(new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, nonce));
        Extension[] exts = new Extension[tmpExtensions.size()];
        Extensions newExtensions = new Extensions(tmpExtensions.toArray(exts));
        TBSRequest newTbsReq = new TBSRequest(requestorName, requestList, newExtensions);

        asn1bcReq = new org.bouncycastle.asn1.ocsp.OCSPRequest(newTbsReq, sig);
    }
    
    @JRubyMethod(name = "certid")
    public IRubyObject certid() {
        Ruby runtime = getRuntime();
        return RubyArray.newArray(runtime, certificateIds);
    }
    
    @JRubyMethod(name = "check_nonce")
    public IRubyObject check_nonce(ThreadContext context, IRubyObject response) {
        final Ruby runtime = context.runtime;
        if (response instanceof OCSPBasicResponse) {
            OCSPBasicResponse rubyBasicRes = (OCSPBasicResponse) response;
            return checkNonceImpl(runtime, this.nonce, rubyBasicRes.getNonce());
        }
        else if (response instanceof OCSPResponse) {
            OCSPResponse rubyResp = (OCSPResponse) response;
            return checkNonceImpl(runtime, this.nonce, ((OCSPBasicResponse)rubyResp.basic(context)).getNonce());
        }
        else {
            return checkNonceImpl(runtime, this.nonce, null);
        }
    }
    
    @JRubyMethod(name = "sign", rest = true)
    public IRubyObject sign(final ThreadContext context, IRubyObject[] args) {
        final Ruby runtime = context.runtime;
                
        int flag = 0;
        IRubyObject additionalCerts = context.nil;
        IRubyObject flags = context.nil;
        IRubyObject digest = context.nil;
        Digest digestInstance = new Digest(runtime, _Digest(runtime));
        IRubyObject nocerts = (RubyFixnum)_OCSP(runtime).getConstant(OCSP_NOCERTS);
        
        switch (Arity.checkArgumentCount(runtime, args, 2, 5)) {
            case 3 :
                additionalCerts = args[2];
                break;
            case 4 :
                additionalCerts = args[2];
                flags = args[3];
                break;
            case 5 :
                additionalCerts = args[2];
                flags = args[3];
                digest = args[4];
                break;
            default :
                break;
                    
        }
                
        if (digest.isNil()) digest = digestInstance.initialize(context, new IRubyObject[] { RubyString.newString(runtime, "SHA1") });
        if (additionalCerts.isNil()) flag |= RubyFixnum.fix2int(nocerts);
        if (!flags.isNil()) flag = RubyFixnum.fix2int(flags);
                
        X509Cert signer = (X509Cert) args[0];
        PKey signerKey = (PKey) args[1];
        
        String keyAlg = signerKey.getAlgorithm();
        String digAlg = ((Digest) digest).getShortAlgorithm();
        
        JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(digAlg + "with" + keyAlg);
        signerBuilder.setProvider("BC");
        ContentSigner contentSigner = null;
        try {
            contentSigner = signerBuilder.build(signerKey.getPrivateKey());
        }
        catch (OperatorCreationException e) {
            throw newOCSPError(runtime, e);
        }

        OCSPReqBuilder builder = new OCSPReqBuilder();
        builder.setRequestorName(signer.getSubject().getX500Name());
        for (OCSPCertificateId certId : certificateIds) {
            builder.addRequest(new CertificateID(certId.getCertID()));
        }
        
        List<X509CertificateHolder> certChain = new ArrayList<X509CertificateHolder>();
        if (flag != RubyFixnum.fix2int(nocerts)) {
            try {
                certChain.add(new X509CertificateHolder(signer.getAuxCert().getEncoded()));
                if (!additionalCerts.isNil()) {
                    Iterator<java.security.cert.Certificate> certIt = ((RubyArray)additionalCerts).iterator();
                    while (certIt.hasNext()) {
                        certChain.add(new X509CertificateHolder(certIt.next().getEncoded()));
                    }
                }
            }
            catch (Exception e) {
                throw newOCSPError(runtime, e);
            }
        }
        
        X509CertificateHolder[] chain = new X509CertificateHolder[certChain.size()];
        certChain.toArray(chain);
        
        try {
            asn1bcReq = org.bouncycastle.asn1.ocsp.OCSPRequest.getInstance(builder.build(contentSigner, chain).getEncoded());
        }
        catch (Exception e) {
            throw newOCSPError(runtime, e);
        }

        if (nonce != null) {
            addNonceImpl();
        }
                
        return this;
    }
        
    @JRubyMethod(name = "verify", rest = true)
    public IRubyObject verify(IRubyObject[] args) {
        Ruby runtime = getRuntime();
        ThreadContext context = runtime.getCurrentContext();
        int flags = 0;
        boolean ret = false;
        
        if (Arity.checkArgumentCount(runtime, args, 2, 3) == 3) {
            flags = RubyFixnum.fix2int((RubyFixnum)args[2]);
        }
        
        IRubyObject certificates = args[0];
        IRubyObject store = args[1];
        
        OCSPReq bcOCSPReq = getBCOCSPReq();  
        if (bcOCSPReq == null) {
            throw newOCSPError(runtime, new NullPointerException("Missing BC asn1bcReq. Missing certIDs or signature?"));
        }
        
        if (!bcOCSPReq.isSigned()) {
            return RubyBoolean.newBoolean(runtime, ret);
        }
              
        GeneralName genName = bcOCSPReq.getRequestorName();
        if (genName.getTagNo() != 4) {
            return RubyBoolean.newBoolean(runtime, ret);
        }
        
        X500Name genX500Name = X500Name.getInstance(genName.getName());
        X509StoreContext storeContext = null;
        JcaContentVerifierProviderBuilder jcacvpb = new JcaContentVerifierProviderBuilder();
        jcacvpb.setProvider("BC");
        
        try {
           java.security.cert.Certificate signer = findCertByName(genX500Name, certificates, flags);
           
           if (signer == null) return RubyBoolean.newBoolean(runtime, ret);
           if ((flags & RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_NOINTERN))) > 0 &&
                   ((flags & RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_TRUSTOTHER))) > 0))
               flags |= RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_NOVERIFY));
           if ((flags & RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_NOSIGS))) == 0) {
               PublicKey signerPubKey = signer.getPublicKey();
               ContentVerifierProvider cvp = jcacvpb.build(signerPubKey);
               ret = bcOCSPReq.isSignatureValid(cvp);
               if (!ret) {
                   return RubyBoolean.newBoolean(runtime, ret);
               }
           }
           if ((flags & RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_NOVERIFY))) == 0) {
               if ((flags & RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_NOCHAIN))) > 0) {
                   storeContext = X509StoreContext.newStoreContext(context, (X509Store)store, X509Cert.wrap(runtime, signer), context.nil); 
               }
               else {
                   RubyArray certs = RubyArray.newEmptyArray(runtime);

                   ASN1Sequence bcCerts = asn1bcReq.getOptionalSignature().getCerts();
                   if (bcCerts != null) {
                       Iterator<ASN1Encodable> it = bcCerts.iterator();
                       while (it.hasNext()) {
                           Certificate cert = Certificate.getInstance(it.next());
                           certs.add(X509Cert.wrap(runtime, new X509AuxCertificate(cert)));
                       }
                   }
                   storeContext = X509StoreContext.newStoreContext(context, (X509Store)store, X509Cert.wrap(runtime, signer), certs); 
               }
               
               storeContext.set_purpose(context, _X509(runtime).getConstant("PURPOSE_OCSP_HELPER"));
               storeContext.set_trust(context, _X509(runtime).getConstant("TRUST_OCSP_REQUEST"));
               ret = storeContext.verify(context).isTrue();
               if (!ret) return RubyBoolean.newBoolean(runtime, false);
           }
        }
        catch (Exception e) {
            debugStackTrace(e);
            throw newOCSPError(runtime, e);
        }
        
        return RubyBoolean.newBoolean(getRuntime(), ret);
    }

    @JRubyMethod(name = "to_der")
    public IRubyObject to_der() {
        Ruby runtime = getRuntime();
        try {
            return RubyString.newString(runtime, this.asn1bcReq.getEncoded(ASN1Encoding.DER));
        }
        catch (IOException e) {
            throw newOCSPError(runtime, e);
        }
    }
    
    @Override
    @JRubyMethod(visibility = Visibility.PRIVATE)
    public IRubyObject initialize_copy(IRubyObject obj) {
        if ( this == obj ) return this;

        checkFrozen();
        this.asn1bcReq = ((OCSPRequest)obj).getBCRequest();
        return this;
    }
    
    private java.security.cert.Certificate findCertByName(ASN1Encodable genX500Name, IRubyObject certificates, int flags) throws CertificateException, IOException {
        Ruby runtime = getRuntime();
        if ((flags & RubyFixnum.fix2int(_OCSP(runtime).getConstant(OCSP_NOINTERN))) == 0) {
            ASN1Sequence certs = asn1bcReq.getOptionalSignature().getCerts();
            if (certs != null) {
                Iterator<ASN1Encodable> it = certs.iterator();
                while (it.hasNext()) {
                    Certificate cert = Certificate.getInstance(it.next());
                    if (genX500Name.equals(cert.getSubject())) return new X509AuxCertificate(cert);
                }
            }
        }
        
        @SuppressWarnings("unchecked")
        List<X509Certificate> certList = (RubyArray)certificates;
        for (X509Certificate cert : certList) {
            if (genX500Name.equals(X500Name.getInstance(cert.getSubjectX500Principal().getEncoded()))) return new X509AuxCertificate(cert);
        }
        
        return null;
    }

    public byte[] getNonce() {
        return this.nonce;
    }
    
    private IRubyObject checkNonceImpl(Ruby runtime, byte[] reqNonce, byte[] respNonce) {
        if (reqNonce != null && respNonce != null) {
            if (Arrays.equals(reqNonce, respNonce)) {
                return RubyFixnum.one(runtime);
            }
            else {
                return RubyFixnum.zero(runtime);
            }
        }
        else if (reqNonce == null && respNonce == null) {
            return RubyFixnum.two(runtime);
        }
        else if (reqNonce != null && respNonce == null) {
            return RubyFixnum.newFixnum(runtime, -1);
        }
        else {
            return RubyFixnum.three(runtime);
        }
    }

    private byte[] generateNonce() {
        // OSSL currently generates 16 byte nonce by default
        return generateNonce(new byte[16]);
    }
    
    private byte[] generateNonce(byte[] bytes) {
        OpenSSL.getSecureRandom(getRuntime()).nextBytes(bytes);
        return bytes;
    }
    
    public org.bouncycastle.asn1.ocsp.OCSPRequest getBCRequest() {
        return asn1bcReq;
    }
    
    public OCSPReq getBCOCSPReq() {
        if (asn1bcReq == null) return null;
        return new OCSPReq(asn1bcReq);
    }

}
