diff --git a/src/main/java/org/jruby/ext/openssl/OCSPBasicResponse.java b/src/main/java/org/jruby/ext/openssl/OCSPBasicResponse.java index e8b25f95..8064938c 100644 --- a/src/main/java/org/jruby/ext/openssl/OCSPBasicResponse.java +++ b/src/main/java/org/jruby/ext/openssl/OCSPBasicResponse.java @@ -122,7 +122,6 @@ public static void createBasicResponse(final Ruby runtime, final RubyModule OCSP BasicResponse.defineAnnotatedMethods(OCSPBasicResponse.class); } - private byte[] nonce; private List singleResponses = new ArrayList<>(); private BasicOCSPResponse asn1BCBasicOCSPResp; private List extensions = new ArrayList<>(); @@ -155,29 +154,27 @@ public IRubyObject initialize_copy(IRubyObject obj) { if ( this == obj ) return this; checkFrozen(); - this.asn1BCBasicOCSPResp = ((OCSPBasicResponse)obj).getASN1BCOCSPResp(); + OCSPBasicResponse other = (OCSPBasicResponse) obj; + this.asn1BCBasicOCSPResp = other.getASN1BCOCSPResp(); + this.extensions = new ArrayList<>(other.extensions); + this.singleResponses = new ArrayList<>(other.singleResponses); return this; } @JRubyMethod(name = "add_nonce", rest = true) public OCSPBasicResponse add_nonce(ThreadContext context, IRubyObject... args) { - byte[] tmpNonce; - if ( Arity.checkArgumentCount(context.runtime, args, 0, 1) == 0 ) { - try { - tmpNonce = generateNonce(context); - } - catch (NoSuchAlgorithmException e) { - throw newOCSPError(context.runtime, e); - } + final boolean random = Arity.checkArgumentCount(context.runtime, args, 0, 1) == 0 || args[0].isNil(); + final Extension nonceExt; + try { + byte[] nonce = random ? generateNonce(context) : args[0].convertToString().getBytes(); + nonceExt = OCSPRequest.createNonceExtension(nonce); } - else { - RubyString input = (RubyString) args[0]; - tmpNonce = input.getBytes(); + catch (NoSuchAlgorithmException|IOException e) { + throw newOCSPError(context.runtime, e); } - extensions.add(new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, tmpNonce)); - nonce = tmpNonce; - + extensions.removeIf(ext -> OCSPObjectIdentifiers.id_pkix_ocsp_nonce.equals(ext.getExtnId())); + extensions.add(nonceExt); return this; } @@ -232,7 +229,14 @@ public OCSPBasicResponse add_status(final ThreadContext context, IRubyObject[] a @JRubyMethod(name = "copy_nonce") public IRubyObject copy_nonce(final ThreadContext context, IRubyObject request) { - add_nonce(context, RubyString.newString(context.runtime, ((OCSPRequest) request).getNonce())); + if (!(request instanceof OCSPRequest)) { + throw context.runtime.newTypeError(request, (RubyClass) _OCSP(context.runtime).getConstantAt("Request")); + } + byte[] reqNonce = ((OCSPRequest) request).getNonce(); + if (reqNonce == null) { + return RubyFixnum.two(context.runtime); + } + add_nonce(context, RubyString.newString(context.runtime, reqNonce)); return RubyFixnum.one(context.runtime); } @@ -593,7 +597,11 @@ public BasicOCSPResponse getASN1BCOCSPResp() { } public byte[] getNonce() { - return this.nonce; + for (Extension ext : extensions) { + if (OCSPObjectIdentifiers.id_pkix_ocsp_nonce.equals(ext.getExtnId())) return OCSPRequest.extractNonce(ext); + } + if (asn1BCBasicOCSPResp == null) return null; + return OCSPRequest.extractNonce(asn1BCBasicOCSPResp.getTbsResponseData().getResponseExtensions()); } private ASN1GeneralizedTime rubyIntOrTimeToGenTime(IRubyObject intOrTime) { diff --git a/src/main/java/org/jruby/ext/openssl/OCSPCertificateId.java b/src/main/java/org/jruby/ext/openssl/OCSPCertificateId.java index 8995d371..1f8bcf33 100644 --- a/src/main/java/org/jruby/ext/openssl/OCSPCertificateId.java +++ b/src/main/java/org/jruby/ext/openssl/OCSPCertificateId.java @@ -87,6 +87,11 @@ public OCSPCertificateId(Ruby runtime, RubyClass metaClass) { public OCSPCertificateId(Ruby runtime) { this(runtime, (RubyClass) _OCSP(runtime).getConstantAt("CertificateId")); } + + OCSPCertificateId(Ruby runtime, CertID bcCertId) { + this(runtime); + this.bcCertId = bcCertId; + } @JRubyMethod(name = "initialize", visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, IRubyObject subject, IRubyObject issuer, IRubyObject digest) { diff --git a/src/main/java/org/jruby/ext/openssl/OCSPRequest.java b/src/main/java/org/jruby/ext/openssl/OCSPRequest.java index fb779388..5689d5de 100644 --- a/src/main/java/org/jruby/ext/openssl/OCSPRequest.java +++ b/src/main/java/org/jruby/ext/openssl/OCSPRequest.java @@ -46,11 +46,16 @@ import org.jruby.ext.openssl.log.Logger; import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.ocsp.Request; import org.bouncycastle.asn1.ocsp.Signature; import org.bouncycastle.asn1.ocsp.TBSRequest; import org.bouncycastle.asn1.x500.X500Name; @@ -82,6 +87,7 @@ import org.jruby.runtime.Visibility; import org.jruby.runtime.builtin.IRubyObject; +import static org.jruby.ext.openssl.OCSP.generateNonce; import static org.jruby.ext.openssl.OpenSSL.throwOperationErrorOrOriginal; import static org.jruby.ext.openssl.OCSP._OCSP; import static org.jruby.ext.openssl.OCSP.newOCSPError; @@ -114,8 +120,6 @@ public static void createRequest(final Ruby runtime, final RubyModule _OCSP) { private final static String OCSP_TRUSTOTHER = "TRUSTOTHER"; private final static String OCSP_NOCHAIN = "NOCHAIN"; private org.bouncycastle.asn1.ocsp.OCSPRequest asn1bcReq; - private List certificateIds = new ArrayList(); - private byte[] nonce; @JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, IRubyObject[] args) { @@ -125,83 +129,98 @@ public IRubyObject initialize(final ThreadContext context, IRubyObject[] args) { 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(); + + GeneralName requestorName = null; + Extensions requestExts = null; // optional + Signature signature = null; // optional + ASN1EncodableVector requests = new ASN1EncodableVector(); + + if (asn1bcReq != null) { + TBSRequest currentTbsReq = asn1bcReq.getTbsRequest(); + requestorName = currentTbsReq.getRequestorName(); + requestExts = currentTbsReq.getRequestExtensions(); + ASN1Sequence reqList = currentTbsReq.getRequestList(); + for (int i = 0; i < reqList.size(); i++) { + requests.add(reqList.getObjectAt(i)); + } + signature = asn1bcReq.getOptionalSignature(); } + + requests.add(new Request(rubyCertId.getCertID(), null)); + TBSRequest newTbsReq = new TBSRequest(requestorName, new DERSequence(requests), requestExts); + asn1bcReq = new org.bouncycastle.asn1.ocsp.OCSPRequest(newTbsReq, signature); + return this; } @JRubyMethod(name = "add_nonce", rest = true) public IRubyObject add_nonce(ThreadContext context, IRubyObject[] args) { - if ( Arity.checkArgumentCount(context.runtime, args, 0, 1) == 0 ) { - try { - nonce = OCSP.generateNonce(context); - } - catch (NoSuchAlgorithmException e) { - throw newOCSPError(context.runtime, e); - } + final boolean random = Arity.checkArgumentCount(context.runtime, args, 0, 1) == 0 || args[0].isNil(); + final Extension nonceExt; + try { + byte[] nonce = random ? OCSP.generateNonce(context) : args[0].convertToString().getBytes(); + nonceExt = createNonceExtension(nonce); } - else { - RubyString input = (RubyString) args[0]; - nonce = input.getBytes(); + catch (NoSuchAlgorithmException|IOException e) { + throw newOCSPError(context.runtime, e); } - - addNonceImpl(); + addNonceImpl(nonceExt); return this; } - - // BC doesn't have support for nonces... gotta do things manually - private void addNonceImpl() { + + private void addNonceImpl(final Extension nonceExt) { GeneralName requestorName = null; ASN1Sequence requestList = new DERSequence(); - Extensions extensions; - Signature sig = null; - List tmpExtensions = new ArrayList(); + Signature signature = null; + List tmpExtensions = new ArrayList<>(); if (asn1bcReq != null) { TBSRequest currentTbsReq = asn1bcReq.getTbsRequest(); - extensions = currentTbsReq.getRequestExtensions(); - sig = asn1bcReq.getOptionalSignature(); - Enumeration oids = extensions.oids(); - while (oids.hasMoreElements()) { - tmpExtensions.add(extensions.getExtension(oids.nextElement())); + requestorName = currentTbsReq.getRequestorName(); + requestList = currentTbsReq.getRequestList(); + Extensions extensions = currentTbsReq.getRequestExtensions(); // optional + if (extensions != null) { + Enumeration oids = extensions.oids(); + while (oids.hasMoreElements()) { + ASN1ObjectIdentifier oid = oids.nextElement(); + if (!OCSPObjectIdentifiers.id_pkix_ocsp_nonce.equals(oid)) { + tmpExtensions.add(extensions.getExtension(oid)); + } + } } + signature = asn1bcReq.getOptionalSignature(); } - - tmpExtensions.add(new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, nonce)); - Extension[] exts = new Extension[tmpExtensions.size()]; - Extensions newExtensions = new Extensions(tmpExtensions.toArray(exts)); + + tmpExtensions.add(nonceExt); + + Extensions newExtensions = new Extensions(tmpExtensions.toArray(new Extension[tmpExtensions.size()])); TBSRequest newTbsReq = new TBSRequest(requestorName, requestList, newExtensions); - asn1bcReq = new org.bouncycastle.asn1.ocsp.OCSPRequest(newTbsReq, sig); + asn1bcReq = new org.bouncycastle.asn1.ocsp.OCSPRequest(newTbsReq, signature); + } + + static Extension createNonceExtension(final byte[] nonce) throws IOException { + final byte[] nonceOctet = new DEROctetString(nonce).getEncoded(); + return new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, nonceOctet); } @JRubyMethod(name = "certid") - public IRubyObject certid() { - Ruby runtime = getRuntime(); - return RubyArray.newArray(runtime, certificateIds); + public IRubyObject certid(ThreadContext context) { + if (asn1bcReq == null) return context.nil; + ASN1Sequence reqList = asn1bcReq.getTbsRequest().getRequestList(); + if (reqList.size() == 0) return context.nil; + + RubyArray certIds = RubyArray.newArray(context.runtime, reqList.size()); + for (int i = 0; i < reqList.size(); i++) { + certIds.append(new OCSPCertificateId(context.runtime, Request.getInstance(reqList.getObjectAt(i)).getReqCert())); + } + return certIds; } @JRubyMethod(name = "signed?") @@ -212,16 +231,17 @@ public RubyBoolean signed_p() { @JRubyMethod(name = "check_nonce") public IRubyObject check_nonce(ThreadContext context, IRubyObject response) { final Ruby runtime = context.runtime; + final byte[] reqNonce = getNonce(); if (response instanceof OCSPBasicResponse) { OCSPBasicResponse rubyBasicRes = (OCSPBasicResponse) response; - return checkNonceImpl(runtime, this.nonce, rubyBasicRes.getNonce()); + return checkNonceImpl(runtime, reqNonce, rubyBasicRes.getNonce()); } else if (response instanceof OCSPResponse) { OCSPResponse rubyResp = (OCSPResponse) response; - return checkNonceImpl(runtime, this.nonce, ((OCSPBasicResponse)rubyResp.basic(context)).getNonce()); + return checkNonceImpl(runtime, reqNonce, ((OCSPBasicResponse)rubyResp.basic(context)).getNonce()); } else { - return checkNonceImpl(runtime, this.nonce, null); + return checkNonceImpl(runtime, reqNonce, null); } } @@ -271,8 +291,16 @@ public IRubyObject sign(final ThreadContext context, IRubyObject[] args) { OCSPReqBuilder builder = new OCSPReqBuilder(); builder.setRequestorName(signer.getSubject().getX500Name()); - for (OCSPCertificateId certId : certificateIds) { - builder.addRequest(new CertificateID(certId.getCertID())); + if (asn1bcReq != null) { + ASN1Sequence reqList = asn1bcReq.getTbsRequest().getRequestList(); + for (int i = 0; i < reqList.size(); i++) { + Request req = Request.getInstance(reqList.getObjectAt(i)); + builder.addRequest(new CertificateID(req.getReqCert()), req.getSingleRequestExtensions()); + } + Extensions exts = asn1bcReq.getTbsRequest().getRequestExtensions(); + if (exts != null) { + builder.setRequestExtensions(exts); + } } List certChain = new ArrayList<>(); @@ -304,8 +332,6 @@ public IRubyObject sign(final ThreadContext context, IRubyObject[] args) { // handlePotentialOperationError(ex, e -> newSecurityError(runtime, e)); //} - if (nonce != null) addNonceImpl(); - return this; } catch (Throwable ex) { @@ -410,7 +436,8 @@ public IRubyObject initialize_copy(IRubyObject obj) { if ( this == obj ) return this; checkFrozen(); - this.asn1bcReq = ((OCSPRequest)obj).asn1bcReq; + OCSPRequest other = (OCSPRequest) obj; + this.asn1bcReq = other.asn1bcReq; return this; } @@ -437,7 +464,25 @@ private java.security.cert.Certificate findCertByName(ASN1Encodable genX500Name, } public byte[] getNonce() { - return this.nonce; + if (asn1bcReq == null) return null; + return extractNonce(asn1bcReq.getTbsRequest().getRequestExtensions()); + } + + static byte[] extractNonce(final Extensions exts) { + return exts == null ? null : extractNonce(exts.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce)); + } + + static byte[] extractNonce(final Extension ext) { + if (ext == null) return null; + + final byte[] value = ext.getExtnValue().getOctets(); + try { + ASN1Primitive primitive = ASN1Primitive.fromByteArray(value); + if (primitive instanceof ASN1OctetString) { + return ((ASN1OctetString) primitive).getOctets(); + } + } catch (IOException ignored) {} + return value; } private IRubyObject checkNonceImpl(Ruby runtime, byte[] reqNonce, byte[] respNonce) { diff --git a/test/ssl/test_ocsp.rb b/test/ssl/test_ocsp.rb index 24211328..822b37d4 100644 --- a/test/ssl/test_ocsp.rb +++ b/test/ssl/test_ocsp.rb @@ -155,7 +155,8 @@ def test_request_nonce req2 = OpenSSL::OCSP::Request.new.add_nonce("ABCDE") bres = OpenSSL::OCSP::BasicResponse.new assert_equal 2, req0.check_nonce(bres) - bres.copy_nonce(req1) + assert_equal 2, bres.copy_nonce(req0) + assert_equal 1, bres.copy_nonce(req1) assert_equal 3, req0.check_nonce(bres) assert_equal 1, req1.check_nonce(bres) bres.add_nonce("NONCE") @@ -163,6 +164,126 @@ def test_request_nonce assert_equal 0, req2.check_nonce(bres) end + def test_request_nonce_with_certid # GH-396 + cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert) + + # add_certid then add_nonce + req1 = OpenSSL::OCSP::Request.new + req1.add_certid(cid) + req1.add_nonce("NONCE") + assert_equal 1, req1.certid.size + assert_equal cid.to_der, req1.certid.first.to_der + + # add_nonce then add_certid + req2 = OpenSSL::OCSP::Request.new + req2.add_nonce("NONCE") + req2.add_certid(cid) + assert_equal 1, req2.certid.size + assert_equal cid.to_der, req2.certid.first.to_der + + # preserved after to_der and add_nonce + req_parsed = OpenSSL::OCSP::Request.new(req1.to_der) + assert_equal 1, req_parsed.certid.size + assert_equal cid.to_der, req_parsed.certid.first.to_der + req_parsed.add_nonce("NEWNONCE") + assert_equal 1, req_parsed.certid.size + + # check nonce preserved in DER + bres = OpenSSL::OCSP::BasicResponse.new + bres.add_nonce("NONCE") + assert_equal 1, req1.check_nonce(bres) + assert_equal 1, OpenSSL::OCSP::Request.new(req1.to_der).check_nonce(bres) + end + + def test_request_nonce_der # MRI encodes nonce as an OCTET STRING inside extnValue + req = OpenSSL::OCSP::Request.new.add_nonce("NONCE") + assert_equal [["Nonce", "\x04\x05NONCE"]], request_extensions(req) + + req.add_nonce("ABC") # replaces existing nonce + assert_equal [["Nonce", "\x04\x03ABC"]], request_extensions(req) + + req = OpenSSL::OCSP::Request.new.add_nonce(nil) + nonce = request_extensions(req).first + assert_equal "Nonce", nonce[0] + assert_equal 18, nonce[1].bytesize # 16 random bytes + + assert_raise(TypeError) { OpenSSL::OCSP::Request.new.add_nonce(1) } + end + + def test_basic_response_nonce_der + cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert) + bres = OpenSSL::OCSP::BasicResponse.new + bres.add_nonce("NONCE") + bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, []) + bres.sign(@ocsp_cert, @ocsp_key, [], 0, OpenSSL::Digest::SHA256.new) + + resp_exts = OpenSSL::ASN1.decode(bres.to_der).value[0].value.last + assert_equal 1, resp_exts.tag # responseExtensions [1] + ext = OpenSSL::X509::Extension.new(resp_exts.value[0].value[0].to_der) + assert_equal ["Nonce", "\x04\x05NONCE"], [ext.oid, ext.value_der] + + parsed = OpenSSL::OCSP::BasicResponse.new(bres.to_der) + assert_equal 1, OpenSSL::OCSP::Request.new.add_nonce("NONCE").check_nonce(parsed) + req = OpenSSL::OCSP::Request.new(OpenSSL::OCSP::Request.new.add_nonce("NONCE").to_der) + assert_equal 1, req.check_nonce(parsed) + assert_equal 0, OpenSSL::OCSP::Request.new.add_nonce("OTHER").check_nonce(parsed) + end + + def test_basic_response_copy_nonce + bres = OpenSSL::OCSP::BasicResponse.new + assert_equal 2, bres.copy_nonce(OpenSSL::OCSP::Request.new) + assert_equal 2, OpenSSL::OCSP::Request.new.check_nonce(bres) + + req = OpenSSL::OCSP::Request.new.add_nonce("NONCE") + assert_equal 1, bres.copy_nonce(req) + assert_equal 1, req.check_nonce(bres) + assert_equal 1, OpenSSL::OCSP::Request.new(req.to_der).check_nonce(bres) + + assert_raise(TypeError) { bres.copy_nonce("NONCE") } + end + + def test_request_sign_with_nonce + cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert) + store = OpenSSL::X509::Store.new.add_cert(@ca_cert) + + req = OpenSSL::OCSP::Request.new.add_certid(cid) + req.add_nonce("NONCE") + req.sign(@cert, @cert_key, [], 0, OpenSSL::Digest::SHA256.new) + assert_equal true, req.verify([], store) + + req_parsed = OpenSSL::OCSP::Request.new(req.to_der) + assert_equal true, req_parsed.verify([], store) + assert_equal 1, req_parsed.certid.size + end + + def test_request_certid + assert_nil OpenSSL::OCSP::Request.new.certid + assert_nil OpenSSL::OCSP::Request.new(OpenSSL::OCSP::Request.new.add_nonce.to_der).certid + + cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert) + req = OpenSSL::OCSP::Request.new.add_certid(cid) + assert_not_same req.certid.first, req.certid.first + assert_equal cid.to_der, req.dup.certid.first.to_der + end + + def test_request_sign_keeps_single_request_extensions + cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert) + ext = OpenSSL::X509::Extension.new("1.3.6.1.5.5.7.48.1.7", OpenSSL::ASN1::Null.new(nil).to_der) + single = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1.decode(cid.to_der), + OpenSSL::ASN1::Sequence([OpenSSL::ASN1.decode(ext.to_der)], 0, :EXPLICIT) + ]) + der = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Sequence([single])])]).to_der + + req = OpenSSL::OCSP::Request.new(der) + req.sign(@cert, @cert_key, [], 0, OpenSSL::Digest::SHA256.new) + assert_equal true, req.verify([], OpenSSL::X509::Store.new.add_cert(@ca_cert)) + + tbs = OpenSSL::ASN1.decode(req.to_der).value[0] + request_list = tbs.value.find { |v| v.is_a?(OpenSSL::ASN1::Sequence) } + assert_equal single.to_der, request_list.value[0].to_der + end + def test_request_is_signed cid = OpenSSL::OCSP::CertificateId.new(@cert, @ca_cert) req = OpenSSL::OCSP::Request.new.add_certid(cid) @@ -373,4 +494,14 @@ def test_response_dup res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres) assert_equal res.to_der, res.dup.to_der end + + private + + def request_extensions(req) + tbs = OpenSSL::ASN1.decode(req.to_der).value[0] + exts = tbs.value.find { |v| v.tag_class == :CONTEXT_SPECIFIC && v.tag == 2 } # requestExtensions [2] + return [] unless exts + exts.value[0].value.map { |e| ext = OpenSSL::X509::Extension.new(e.to_der); [ext.oid, ext.value_der] } + end + end