【问题标题】:How to programmatically check if a certificate has been revoked?如何以编程方式检查证书是否已被吊销?
【发布时间】:2013-04-21 01:53:56
【问题描述】:

我正在开发一个 xcode 自动构建系统。在执行一些预构建验证时,我想检查指定的证书文件是否已被吊销。我了解security verify-cert 会验证其他证书属性,但不会验证吊销。如何检查撤销?

我正在用 Ruby 编写构建系统,但我对任何语言的想法都持开放态度。

我阅读了此答案 (Openssl - How to check if a certificate is revoked or not),但指向底部的链接 (Does OpenSSL automatically handle CRLs (Certificate Revocation Lists) now?) 涉及的材料对我的目的来说有点过于复杂(上传已撤销证书的用户是一个非常极端的情况)。是否有更简单/面向 ruby​​ 的方法来检查吊销?

提前致谢!

【问题讨论】:

    标签: ruby xcode openssl certificate xcodebuild


    【解决方案1】:

    为了记录,很大程度上灵感来自Paul Kehrer answer(谢谢!)我写了一个小的ruby gem来检查证书的有效性和撤销(它在我的产品updown.io中使用):https://github.com/jarthod/ssl-test

    # Gemfile
    gem 'ssl-test'
    

    这是一个例子:

    valid, error, cert = SSLTest.test "https://revoked.badssl.com"
    valid # => false
    error # => "SSL certificate revoked: The certificate was revoked for an unknown reason (revocation date: 2019-10-07 20:30:39 UTC)"
    cert # => #<OpenSSL::X509::Certificate...>
    

    由于1.4,它同时支持OCSP和CRL。

    【讨论】:

      【解决方案2】:

      Paul 的示例不适用于我的本地服务器,由 OpenSSL Cookbook 制作,但适用于发布请求

      # openssl ocsp -port 9080 -index db/index -rsigner root-ocsp.crt -rkey private/root-ocsp.key -CA root-ca.crt -text
      # openssl ocsp -issuer root-ca.crt -CAfile root-ca.crt -cert root-ocsp.crt -url http://127.0.0.1:9080
      
      require 'net/http'
      require 'openssl'
      require 'base64'
      require 'test/unit'
      extend Test::Unit::Assertions
      
      def load_cert(name)
        OpenSSL::X509::Certificate.new(File.read(name))
      end
      
      ca_file = issuer = load_cert('root-ca.crt')
      cert = load_cert('root-ocsp.crt')
      
      cid = OpenSSL::OCSP::CertificateId.new(cert, issuer)
      request = OpenSSL::OCSP::Request.new.add_certid(cid)
      
      # with get, invalid, server responding with
      # Invalid request
      # Responder Error: malformedrequest (1)
      #
      # encoded_der = Base64.encode64(request.to_der)
      # request_uri = URI.parse('http://127.0.0.1/' + URI.encode_www_form_component(encoded_der.strip))
      # req = Net::HTTP::Get.new(request_uri.path, 'Content-Type' => 'application/ocsp-response')
      # http_resp = Net::HTTP.new(request_uri.host, '9080').request(req)
      
      # with post, work
      ocsp_uri = URI('http://127.0.0.1:9080/')
      http_resp = Net::HTTP.post(ocsp_uri, request.to_der, 'Content-Type' => 'application/ocsp-response')
      
      resp = OpenSSL::OCSP::Response.new(http_resp.body)
      
      assert_equal resp.status, OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
      assert resp.basic.is_a? OpenSSL::OCSP::BasicResponse
      
      current_time = Time.now
      resp.basic.status.each do |status_arr|
        certificate_id, status, reason, revocation_time, this_update, next_update, extensions = status_arr
        assert_equal status, 0 # 0 is good, 1 is revoked, 2 is unknown.
        assert this_update < current_time
        assert next_update.nil?
      end
      
      first_cert_id = resp.basic.status[0][0]
      assert first_cert_id.cmp(cid)
      assert first_cert_id.cmp_issuer(cid)
      assert_equal first_cert_id.serial, cert.serial
      
      resp.basic.responses.each do |resp|
        assert resp.is_a? OpenSSL::OCSP::SingleResponse
        assert resp.check_validity
      end
      
      store = OpenSSL::X509::Store.new
      store.add_cert(cert)
      store.add_cert(issuer) # assuming issuer is a trusted root here, but in reality you'll need at least one more certificate
      assert resp.basic.verify([], store)
      

      附: 现在它请求 ocsp 证书的状态(如书中),想要请求服务器/终端实体状态,但首先我必须使用 openssl cli 尝试它,并且here I have stumbled

      附言

      完成了,谢谢 Steffen Ullrich

      # openssl ocsp -port 9080 -index db/index -rsigner subca-ocsp.crt -rkey private/subca-ocsp.key -CA sub-ca.crt -text
      # cat sub-ca.crt root-ca.crt > sub-and-root.crt
      # openssl ocsp -issuer sub-ca.crt -CAfile sub-and-root.crt -cert server.crt -url http://127.0.0.1:9080
      
      require 'net/http'
      require 'openssl'
      require 'base64'
      require 'test/unit'
      extend Test::Unit::Assertions
      
      def load_cert(name)
        OpenSSL::X509::Certificate.new(File.read(name))
      end
      
      subca = load_cert('sub-ca.crt')
      root = load_cert('root-ca.crt')
      cert = load_cert('server.crt')
      
      cid = OpenSSL::OCSP::CertificateId.new(cert, subca)
      request = OpenSSL::OCSP::Request.new.add_certid(cid)
      
      # with post, work
      ocsp_uri = URI('http://127.0.0.1:9080/')
      http_resp = Net::HTTP.post(ocsp_uri, request.to_der, 'Content-Type' => 'application/ocsp-response')
      
      resp = OpenSSL::OCSP::Response.new(http_resp.body)
      
      assert_equal resp.status, OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
      assert resp.basic.is_a? OpenSSL::OCSP::BasicResponse
      
      first_cert_id = resp.basic.status[0][0]
      assert first_cert_id.cmp(cid)
      assert first_cert_id.cmp_issuer(cid)
      assert_equal first_cert_id.serial, cert.serial
      
      resp.basic.responses.each do |resp|
        assert resp.is_a? OpenSSL::OCSP::SingleResponse
        assert resp.check_validity
      end
      
      store = OpenSSL::X509::Store.new
      store.add_cert(cert)
      store.add_cert(subca)
      store.add_cert(root)
      assert resp.basic.verify([], store)
      

      【讨论】:

        【解决方案3】:

        检查证书是否被吊销可能是一个复杂的过程。首先,您必须查找 CDP 或 OCSP AIA,然后发出请求,解析响应,并检查响应是否由授权响应相关证书的 CA 签署。如果它是 CRL,那么您需要查看您正在检查的证书的序列号是否存在于列表中。如果是 OCSP,那么您需要查看是否收到“良好”响应(与未知、已撤销或任何各种 OCSP 响应者错误(如未经授权)相对)。此外,您可能希望验证证书是否在其有效期内并链接到受信任的根。最后,您还应该对每个中间人进行吊销检查,并根据 Mozilla/Apple/Google/Microsoft 维护的明确黑名单检查证书的指纹。

        我不知道有任何 Ruby 库可以为您自动执行吊销检查过程(最终我希望将其添加到 r509),但考虑到您更具体的用例,这里有一些未经测试的代码,应该可以为您指明正确的方向方向。

        require 'r509'
        require 'net/http'
        cert = R509::Cert.load_from_file("some_iphone_cert.pem")
        crl_uri = cert.crl_distribution_points.crl.uris[0]
        crl = Net::HTTP.get_response(URI(crl_uri)) # you may need to follow redirects here, but let's assume you got the CRL.
        # Also note that the Apple WWDRCA CRL is like 28MB so you may want to cache this damned thing. OCSP would be nicer but it's a bit trickier to validate.
        parsed_crl = R509::CRL::SignedList.new(crl)
        if not parsed_crl.verify(cert.public_key)
          raise StandardError, "Invalid CRL for certificate"
        end
        if parsed_crl.revoked?(cert.serial)
          puts 'revoked'
        end
        

        不幸的是,由于 Apple WWDRCA CRL 的庞大规模(约 680k 条目),对于 r509 的当前哈希映射模型,此检查可能会非常慢。

        如果您对 OCSP 路径感兴趣,我也可以写下如何在 Ruby 中生成 OCSP 请求/解析响应。

        编辑:我拥有的 iPhone 开发人员证书似乎不包含嵌入式 OCSP AIA,因此吊销检查的唯一选择是通过 CRL 分发点,如上所述。

        Edit2:哦,为什么不呢,让我们在 Ruby 中进行 OCSP 检查!为此,我们需要证书及其颁发证书。您不能为此使用 WWDRCA 证书,因此只需从您最喜欢的网站上获取一个。我正在使用我自己的网站。

        require 'net/http'
        require 'r509'
        cert = R509::Cert.load_from_file("my_website.pem")
        # get the first OCSP AIA URI. There can be more than one 
        # (degenerate example!)
        ocsp_uri = cert.aia.ocsp.uris[0]
        issuer = R509::Cert.load_from_file("my_issuer.pem")
        cert_id = OpenSSL::OCSP::CertificateId.new(cert.cert,issuer.cert)
        request = OpenSSL::OCSP::Request.new
        request.add_certid(cert_id)
        # we're going to make a GET request per RFC 5019. You can also POST the 
        # binary DER encoded version if you're more of an RFC 2560 partisan
        request_uri = URI(ocsp_uri+"/"+URI.encode_www_form_component(req_pem.strip)
        http_response = Net::HTTP.get_response(request_uri)
        if http_response.code != "200"
          raise StandardError, "Invalid response code from OCSP responder"
        end
        response = OpenSSL::OCSP::Response.new(http_response.body)
        if response.status != 0
          raise StandardError, "Not a successful status"
        end
        if response.basic[0][0].serial != cert.serial
          raise StandardError, "Not the same serial"
        end
        if response.basic[0][1] != 0 # 0 is good, 1 is revoked, 2 is unknown.
          raise StandardError, "Not a good status"
        end
        current_time = Time.now
        if response.basic[0][4] > current_time or response.basic[0][5] < current_time
          raise StandardError, "The response is not within its validity window"
        end
        # we also need to verify that the OCSP response is signed by 
        # a certificate that is allowed and chains up to a trusted root. 
        # To do this you'll need to build an OpenSSL::X509::Store object 
        # that contains the certificate you're checking + intermediates + root.
        store = OpenSSL::X509::Store.new
        store.add_cert(cert.cert)
        store.add_cert(issuer.cert) #assuming issuer is a trusted root here, but in reality you'll need at least one more certificate
        if response.basic.verify([],store) != true
          raise StandardError, "Certificate verification error"
        end
        

        上面的示例代码忽略了处理许多可能的边缘情况,所以它应该被认为只是一个起点。祝你好运!

        【讨论】:

        • @Alfie 没问题,很高兴它有帮助!
        • 除了使用您的代码之外,是否可以将 OpenSSL 设置为在接受连接时自动执行此操作?即@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER,但使用VERIFY_CRL_LIST(如果存在)。 c库里至少有X509_VERIFY_PARAM_set_flags(param, X509_V_FLAG_CRL_CHECK);
        • @MartinWickman 这确实存在于库本身中,但 Ruby 使用 SSL_CTX_set_verify 并且在我(诚然简短)对代码库的检查中,看起来没有办法找到 X509_VERIFY_PARAM 你d 需要修改。要正确执行此操作,您还需要使用X509_V_FLAG_CRL_CHECK_ALL,因为这将尝试检查整个链而不仅仅是叶子。
        • Paul - 我正在研究这个例子并随时调整它,但我不明白你从哪里得到 req_pem。这只是原始证书还是请求对象的操作?
        • 这看起来像是一个愚蠢的疏忽。我相信你需要做req_pem = Base64.encode64(request.to_der)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-09
        • 1970-01-01
        • 2010-10-27
        • 2010-12-01
        • 2012-03-26
        • 2019-04-23
        相关资源
        最近更新 更多