I recently ran into a bug with verification of OCSP responses that appears to be in all versions of OpenSSL (including current 1.1.0 builds). RFC 6960 and its predecessor 2560 allow that the response may be signed by the key for "the CA who issued the certificate in question? (section 2.2). In this case it should not be necessary to include any certs in the basicResponse, as the signature can be validated using the public key used to validate the certificate whose status is being checked. While this is contemplated by the RFCs, OpenSSL fails in certain cases if no certificates are provided in the response. If there are at least two intermediate CAs in the certificate chain between a trust anchor and end entity certificate, then OCSP_basic_verify will return a certificate verify error. The code below reproduces the failure and demonstrates that reducing the path length from anchor to end entity certificate resolves the issue as does adding a certificate from the trust anchor to the first CA to the response. I think the correct behaviour would be to check if the issuer of the certificate matches the signer of the OCSP response and, if so, simply skip the X509_verify_cert check. Thanks, Peter #!/usr/bin/env ruby # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the OpenSSL license (the "License"). You may not use # this file except in compliance with the License. You can obtain a copy # in the file LICENSE in the source distribution or at # https://www.openssl.org/source/license.html require 'openssl' require 'base64' require 'open3' # Ruby has no to_text method for OCSP responses, so shell out def ocsp_to_text(ocsp_der) # Figure out if our base64 uses -D or -d to decode %x/echo | base64 -D/ if $? == 0 dparam="-D" else dparam="-d" end out = "" Open3::popen2("/bin/bash -t") do |i, o, t| i.puts("openssl ocsp -noverify -text -respin <(echo #{Base64.strict_encode64(ocsp_der)} | base64 #{dparam})") o.each{|l|out += l} end out end # Set up Names and Keys for all the certs nodes = { :root => { :name => OpenSSL::X509::Name.new([["C", "US", OpenSSL::ASN1::PRINTABLESTRING], ["O", "Beyond Hypersecure Inc", OpenSSL::ASN1::PRINTABLESTRING], ["CN", "Beyond Hypersecure Root CA", OpenSSL::ASN1::PRINTABLESTRING]]), :key => OpenSSL::PKey::RSA.new(2048) }, :ca1 => { :name => OpenSSL::X509::Name.new([["C", "US", OpenSSL::ASN1::PRINTABLESTRING], ["O", "Beyond Hypersecure Inc", OpenSSL::ASN1::PRINTABLESTRING], ["CN", "Beyond Hypersecure Partner CA", OpenSSL::ASN1::PRINTABLESTRING]]), :key => OpenSSL::PKey::RSA.new(2048) }, :ca2 => { :name => OpenSSL::X509::Name.new([["C", "US", OpenSSL::ASN1::PRINTABLESTRING], ["O", "HyperCyberHost LLC", OpenSSL::ASN1::PRINTABLESTRING], ["CN", "HyperCyberHost Server CA", OpenSSL::ASN1::PRINTABLESTRING]]), :key => OpenSSL::PKey::RSA.new(2048) }, :ee => { :name => OpenSSL::X509::Name.new([["C", "US", OpenSSL::ASN1::PRINTABLESTRING], ["CN", "localdemo.sslmap.com", OpenSSL::ASN1::PRINTABLESTRING]]), :key => OpenSSL::PKey::RSA.new(2048), :sans => "DNS:localdemo.sslmap.com" } } # Generate all the certs root_cert = OpenSSL::X509::Certificate.new root_cert.version = 0x2 root_cert.serial = 0x0 root_cert.not_before = Time.new(2004,01,01,00,00,01) root_cert.not_after = Time.new(2028,12,31,23,59,59) root_cert.subject = nodes[:root][:name] root_cert.issuer = root_cert.subject root_cert.public_key = nodes[:root][:key] ef = OpenSSL::X509::ExtensionFactory.new ef.subject_certificate = root_cert ef.issuer_certificate = root_cert root_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) root_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true)) root_cert.add_extension(ef.create_extension("keyUsage","digitalSignature, keyCertSign, cRLSign", true)) root_cert.sign(nodes[:root][:key], OpenSSL::Digest::SHA256.new) puts root_cert.to_text puts root_cert.to_pem ca1_cert = OpenSSL::X509::Certificate.new ca1_cert.version = 0x2 ca1_cert.serial = 0xa ca1_cert.not_before = Time.new(2011,01,01,00,00,01) ca1_cert.not_after = Time.new(2020,12,31,23,59,59) ca1_cert.subject = nodes[:ca1][:name] ca1_cert.issuer = root_cert.subject ca1_cert.public_key = nodes[:ca1][:key] ef = OpenSSL::X509::ExtensionFactory.new ef.config = OpenSSL::Config.parse(' [polsect] policyIdentifier = 2.5.29.32.0 CPS.1="http://beyondhypersecure.example.com/cps" ') ef.subject_certificate = ca1_cert ef.issuer_certificate = root_cert ca1_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) ca1_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false)) ca1_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true)) ca1_cert.add_extension(ef.create_extension("certificatePolicies","2.5.29.32.0")) ca1_cert.add_extension(ef.create_extension("keyUsage","digitalSignature, keyCertSign, cRLSign", true)) ca1_cert.add_extension(ef.create_extension("authorityInfoAccess","caIssuers;URI:http://beyondhypersecure.example.com/root.cer,OCSP;URI:http://ocsp.beyondhypersecure.example.com")) ca1_cert.add_extension(ef.create_extension("crlDistributionPoints","URI:http://beyondhypersecure.example.com/root.crl")) ca1_cert.sign(nodes[:root][:key], OpenSSL::Digest::SHA256.new) puts ca1_cert.to_text puts ca1_cert.to_pem ca2_cert = OpenSSL::X509::Certificate.new ca2_cert.version = 0x2 ca2_cert.serial = 0xfb ca2_cert.not_before = Time.new(2012,01,01,00,00,01) ca2_cert.not_after = Time.new(2018,12,31,23,59,59) ca2_cert.subject = nodes[:ca2][:name] ca2_cert.issuer = ca1_cert.subject ca2_cert.public_key = nodes[:ca2][:key] ef = OpenSSL::X509::ExtensionFactory.new ef.config = OpenSSL::Config.parse(' [polsect] policyIdentifier = 2.5.29.32.0 CPS.1="http://beyondhypersecure.example.com/cps" ') ef.subject_certificate = ca2_cert ef.issuer_certificate = ca1_cert ca2_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) ca2_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false)) ca2_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true)) ca2_cert.add_extension(ef.create_extension("certificatePolicies","2.5.29.32.0")) ca2_cert.add_extension(ef.create_extension("keyUsage","digitalSignature, keyCertSign, cRLSign", true)) ca2_cert.add_extension(ef.create_extension("authorityInfoAccess","caIssuers;URI:http://beyondhypersecure.example.com/ca1.cer,OCSP;URI:http://ocsp.beyondhypersecure.example.com")) ca2_cert.add_extension(ef.create_extension("crlDistributionPoints","URI:http://beyondhypersecure.example.com/ca1.crl")) ca2_cert.sign(nodes[:ca1][:key], OpenSSL::Digest::SHA256.new) puts ca2_cert.to_text puts ca2_cert.to_pem ee_cert = OpenSSL::X509::Certificate.new ee_cert.version = 0x2 ee_cert.serial = OpenSSL::BN.rand(64, -1, 0) ee_cert.not_before = Time.new(2015,02,26,16,00,00) ee_cert.not_after = Time.new(2016,02,28,15,59,59) ee_cert.subject = nodes[:ee][:name] ee_cert.issuer = ca2_cert.subject ee_cert.public_key = nodes[:ee][:key] ef = OpenSSL::X509::ExtensionFactory.new ef.config = OpenSSL::Config.parse(' [polsect] policyIdentifier = 2.23.140.1.2.1 CPS.1="http://beyondhypersecure.example.com/cps" ') ef.subject_certificate = ee_cert ef.issuer_certificate = ca2_cert ee_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false)) ee_cert.add_extension(ef.create_extension("certificatePolicies","@polsect")) ee_cert.add_extension(ef.create_extension("subjectAltName",nodes[:ee][:sans])) ee_cert.add_extension(ef.create_extension("keyUsage","digitalSignature,keyEncipherment", true)) ee_cert.add_extension(ef.create_extension("extendedKeyUsage", "serverAuth, clientAuth")) ee_cert.add_extension(ef.create_extension("crlDistributionPoints","URI:http://beyondhypersecure.example.com/ca2.crl")) ee_cert.add_extension(ef.create_extension("authorityInfoAccess","caIssuers;URI:http://beyondhypersecure.example.com/ca2.cer,OCSP;URI:http://ocsp.beyondhypersecure.example.com")) ee_cert.sign(nodes[:ca2][:key], OpenSSL::Digest::SHA256.new) puts nodes[:ee][:key].to_text puts nodes[:ee][:key].to_pem puts ee_cert.to_text puts ee_cert.to_pem bres = OpenSSL::OCSP::BasicResponse.new cid = OpenSSL::OCSP::CertificateId.new(ee_cert, ca2_cert, OpenSSL::Digest::SHA1.new) bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -1*60*60*6, 60*60*24*3, []) bres.sign(ca2_cert, nodes[:ca2][:key], nil, OpenSSL::OCSP::RESPID_KEY) ocsp_res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres) ocsp_res_1_der = ocsp_res.to_der puts ocsp_to_text(ocsp_res_1_der) puts "-----BEGIN OCSP RESPONSE 1-----" puts Base64.encode64(ocsp_res_1_der) puts "-----END OCSP RESPONSE 1-----" bres.sign(ca2_cert, nodes[:ca2][:key], [ca1_cert], OpenSSL::OCSP::RESPID_KEY) ocsp_res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres) ocsp_res_2_der = ocsp_res.to_der # OCSP_basic_sign adds the signer cert unless OCSP_NOCERTS is passed # Ruby doesn't expose OCSP_basic_add1_cert, so instead we rip out the signer # cert, just leaving the certs we explicitly passed to OCSP_basic_sign # (Yes, this is a horrible hack) a = OpenSSL::ASN1.decode(ocsp_res_2_der) bres_a = OpenSSL::ASN1.decode(a.value[1].value[0].value[1].value) bres_a.value[3].value[0].value.delete_at(0) a.value[1].value[0].value[1].value = bres_a.to_der ocsp_res_2_der = a.to_der puts ocsp_to_text(ocsp_res_2_der) puts "-----BEGIN OCSP RESPONSE 2-----" puts Base64.encode64(ocsp_res_2_der) puts "-----END OCSP RESPONSE 2-----" ## Now test # Encode/decode loops to ensure the client test objects are clean trust_store_1 = OpenSSL::X509::Store.new [root_cert].each do |c| trust_store_1.add_cert(OpenSSL::X509::Certificate.new(c.to_der)) end trust_store_2 = OpenSSL::X509::Store.new [root_cert, ca1_cert].each do |c| trust_store_2.add_cert(OpenSSL::X509::Certificate.new(c.to_der)) end server_chain = [ee_cert, ca2_cert, ca1_cert].map{|c|OpenSSL::X509::Certificate.new(c.to_der)} ocsp_res_1 = OpenSSL::OCSP::Response.new(ocsp_res_1_der) ocsp_res_2 = OpenSSL::OCSP::Response.new(ocsp_res_2_der) puts "\nTrying with only root cert in trust store" puts ocsp_res_1.basic.verify(server_chain, trust_store_1) puts "\nTrying with root and issuer of CA cert in trust store" puts ocsp_res_1.basic.verify(server_chain, trust_store_2) puts "\nTrying with only root cert in trust store and with CA1 cert in response" puts ocsp_res_2.basic.verify(server_chain, trust_store_1) puts "\nTrying with root and issuer of CA cert in trust store and with CA1 cert in response" puts ocsp_res_2.basic.verify(server_chain, trust_store_2)