I'm working on an xcode automated build system. When performing some pre-build validation I would like to check if the specified certificate file has been revoked. I understand that security verify-cert
verifies other cert properties but not revokation. How can I check for revokation?
I'm writing the build system in Ruby but am really open to ideas in any language.
I read this answer (Openssl - How to check if a certificate is revoked or not) but the link towards the bottom (Does OpenSSL automatically handle CRLs (Certificate Revocation Lists) now?) gets into material that's a bit too involved for my purposes (a user uploading a revoked cert is a far out edge case). Is there a simpler / ruby oriented method for checking for revokation?
Thanks in advance!
Checking if a certificate is revoked can be a complex process. First you have to look for a CDP or OCSP AIA, then make a request, parse the response, and check that the response is signed against by a CA that is authorized to respond for the certificate in question. If it is a CRL you then need to see if the serial number of the certificate you're checking is present in the list. If it is OCSP then you need to see if you've received a "good" response (as opposed to unknown, revoked, or any of the various OCSP responder errors like unauthorized). Additionally you may want to verify that the certificate is within its validity period and chains to a trusted root. Finally, you should do revocation checks against every intermediate as well and check the certificate's fingerprint against the explicit blacklists that Mozilla/Apple/Google/Microsoft maintain.
I'm unaware of any Ruby libraries that automate the revocation checking process for you (eventually I hope to add it to r509), but given your more specific use case here's some untested code that should point you in the right direction.
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
Unfortunately, due to the enormous size (~680k entries) of the Apple WWDRCA CRL this check can be quite slow with r509's current hash map model.
If you're interested in going down the OCSP path I can write up how to generate OCSP requests/parse responses in Ruby as well.
Edit: It appears the iPhone developer certificates I have do not contain an embedded OCSP AIA so the only option for revocation checking will be via CRL distribution point as presented above.
Edit2: Oh why not, let's do an OCSP check in Ruby! For this we'll need the certificate and its issuing certificate. You can't use a WWDRCA certificate for this so just grab one from your favorite website. I'm using my own website.
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
The example code above neglects to handle many possible edge cases, so it should be considered a starting point only. Good luck!
Paul's example has not worked with my local server, made by OpenSSL Cookbook, but have worked with post request
# 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)
P.S. For now it requesting status of ocsp certificate (like in book), wanted to request server/end-entity status, but at first I have to try it with openssl cli, and here I have stumbled
P.S.S
done this, thanks 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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With