/* * 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); } }