How to programmatically check if a certificate has been revoked? - ruby

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)

For the record, largely inspired from Paul Kehrer answer (Thanks!) I wrote a small ruby gem to check the validity and revocation of a certificate (it is used in my product updown.io): https://github.com/jarthod/ssl-test
# Gemfile
gem 'ssl-test'
Here is an example:
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...>
Since 1.4 it supports both OCSP and CRL.

Related

OpenSSL::SSL::SSLError: Ruby client's server ca certificate does not work while it worked with curl

I got certificate from customer to connect with their VPN, but it does not work with ruby code while it worked with curl command. Curl command is as follows:
curl --cacert cert.cer -d '{"acb": 123 }' -H 'Content-Type: application/json' 'https://demo.com'
In ruby, I am trying to do the following to connect the client APIs provided to us for transactions.
require 'net/http'
require 'json'
require 'uri'
full_url = "https://demo.com"
uri = URI.parse(full_url)
data = { "acb": 123 }
headers = { 'Content-Type' => "application/json" }
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
raw = File.read("path_to_the_certificate")
http.cert = OpenSSL::X509::Certificate.new(raw)
request = Net::HTTP::Post.new(uri.request_uri, headers)
request.body = data.to_json
response = http.request(request)
puts response.code
puts response.body
We also tried to pass our server's certificate as follows, but that doesn't work either
http.ca_path='/etc/pki/tls/certs'
http.ca_file='/etc/pki/tls/certs/cert.cer'
http.cert = OpenSSL::X509::Certificate.new(File.read("/path/client.crt"))
http.key = OpenSSL::PKey::RSA.new(File.read("/path/client.key"))
Getting the following error while
OpenSSL::SSL::SSLError (SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get local issuer certificate))
I think the issue with their self-signed certificate. It fails verification.
However, you can manually disable it with
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
verify_mode[RW]
Sets the flags for server the certification verification at beginning of SSL/TLS session.
OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
from https://ruby-doc.org/stdlib-2.7.0/libdoc/net/http/rdoc/Net/HTTP.html
I tried to replicate it locally and it worked with this fix.
It should be that the vpn certificate is self-signed, you need to specify your own cacert, so you specify cacert file as the file used by the curl above, not the cacert file that comes with the system
add this line:
http.ca_file = "cacert filename"
Like this:
require 'net/http'
require 'json'
require 'uri'
full_url = "https://localhost/test.html"
uri = URI.parse(full_url)
data = { "acb": 123 }
headers = { 'Content-Type' => "application/json" }
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# You need to specify the cacert file used for curl above (filename: cert.cer)
http.ca_file = "/root/myca/cacert.crt"
request = Net::HTTP::Post.new(uri.request_uri, headers)
request.body = data.to_json
response = http.request(request)
puts response.code
puts response.body
you should add your certificate in .pem format to (depending on the version) either:
C:\Ruby{version number}{-x64 - if 64 bit operating system}\ssl
e.g. C:\Ruby25-x64\ssl
or to
C:\Ruby{version number}{-x64 - if 64 bit operating system}\lib\ruby{version number}\rubygems\ssl_certs{your cn}
e.g. C:\Ruby25-x64\lib\ruby\2.5.0\rubygems\ssl_certs\client.cn
and then in the C:\Ruby{The version number}{-x64 - If it's a 64-bit operating system}\ssl\certs run the c_rehash.r script
For Apps using PayPal::SDK.configure(...)
PayPal::SDK.configure(
mode: ...,
client_id: ...,
client_secret: ...,
# Deliberately set ca_file to nil so the system's Cert Authority is used,
# instead of the bundled paypal.crt file which is out-of-date due to:
# https://www.paypal.com/va/smarthelp/article/discontinue-use-of-verisign-g5-root-certificates-ts2240
ssl_options: { ca_file: nil }
)
For apps using a YAML config file
ssl_options:
ca_file: null

SSO Auth flow w/ DocuSign_eSign gem: "The specified Integrator Key was not found or is disabled."

I am using the DocuSign_eSign ruby gem version 1.0.2 because version 2.0.0 does not handle JWT auth. The eg-01-ruby-jwt example provided by DocuSign uses v1.0.2 as well, so I just pinned this version in my Gemfile.
I am getting the following error when I attempt to make a call using the DocuSign_eSign::EnvelopesApi.new#create_envelope method:
docusign_esign-1.0.2/lib/docusign_esign/api_client.rb:66:in `call_api': Bad Request (DocuSign_eSign::ApiError)
For good measure, I walked through the code line by line to get a better idea of what was happening, and I ended up with this error:
irb(main):101:0> response.code
=> 401
irb(main):103:0> response.body
=> "{\r\n \"errorCode\": \"PARTNER_AUTHENTICATION_FAILED\",\r\n \"message\": \"The specified Integrator Key was not found or is disabled. An Integrator key was not specified.\"\r\n}"
irb(main):104:0> response.status_message
=> "Unauthorized"
Here is the ruby code (minus logging/error handling) from my DocuSignWebClient where I trigger the call (send_envelope). This is where the auth happens:
TOKEN_REPLACEMENT_IN_SECONDS = 10.minutes.seconds
TOKEN_EXPIRATION_IN_SECONDS = 1.hour.seconds
def initialize(options = {})
#docusign_config = Padrino.config.docusign
#api_client = DocuSignWebClient.setup_api_client
#s3_client = options[:s3_client] || Aws::S3::Client.new(region: Padrino.config.aws.region)
#token_expire_in = options[:token_expire_in] || 0
#account_id = options[:account_id]
end
def self.setup_api_client
configuration = DocuSign_eSign::Configuration.new
DocuSign_eSign::ApiClient.new(configuration)
end
def send_envelope(details)
authorize
envelope = DocuSign::EnvelopeBuilder.new(details, #s3_client).build_envelope
sender = DocuSign::EnvelopeSender.new(#api_client, #account_id)
response = sender.send_envelope(envelope)
update_document_status(details, response)
end
def authorize
check_token
#account_id ||= update_account_id
end
def check_token
if no_token? || token_near_expiration?
update_token
end
end
def no_token?
!#api_client.default_headers['Authorization']
end
def token_near_expiration?
now = Time.now.to_f
(now + TOKEN_REPLACEMENT_IN_SECONDS) > #token_expire_in
end
def update_token
configure_jwt_authorization_flow
#token_expire_in = Time.now.to_f + TOKEN_EXPIRATION_IN_SECONDS
end
def configure_jwt_authorization_flow
#api_client.configure_jwt_authorization_flow(#docusign_config[:private_key_file],
#docusign_config[:auth_server],
#docusign_config[:integrator_key],
#docusign_config[:user_id],
TOKEN_EXPIRATION_IN_SECONDS)
end
def update_account_id
account = fetch_account_info
#api_client.config.host = account[:base_uri]
account[:account_id]
end
def fetch_account_info
response = DocuSignResponse.new(#api_client.call_api('GET', "https://#{#docusign_config[:auth_server]}/oauth/userinfo", return_type: 'Object'))
if response.ok?
response.data[:accounts].detect { |acct| acct[:is_default] }
end
end
I am getting a token back from DocuSign after I go through the authorization flow, but it says that the token is not valid once I try to use it.
irb(main):033:0> account_id = client.authorize
=> "386...a24"
irb(main):036:0> client.api_client
=> #<DocuSign_eSign::ApiClient:0x00007ff6d243c8c8 #config=#<DocuSign_eSign::Configuration:0x00007ff6d243d2f0 #scheme="https", #host="demo.docusign.net", #base_path="/restapi", #api_key={}, #api_key_prefix={}, #timeout=0, #verify_ssl=true, #verify_ssl_host=true, #params_encoding=nil, #cert_file=nil, #key_file=nil, #debugging=false, #inject_format=false, #force_ending_format=false, #logger=#<Logger:0x00007ff6d243d110 #level=0, #progname=nil, #default_formatter=#<Logger::Formatter:0x00007ff6d243d098 #datetime_format=nil>, #formatter=nil, #logdev=#<Logger::LogDevice:0x00007ff6d243cfa8 #shift_period_suffix=nil, #shift_size=nil, #shift_age=nil, #filename=nil, #dev=#<IO:<STDOUT>>, #mon_mutex=#<Thread::Mutex:0x00007ff6d243c8f0>, #mon_mutex_owner_object_id=70349033170900, #mon_owner=nil, #mon_count=0>>>, #user_agent="Swagger-Codegen/1.0.2/ruby", #default_headers={"Content-Type"=>"application/json", "User-Agent"=>"Swagger-Codegen/1.0.2/ruby", "Authorization"=>"Bearer eyJ0eXAiOiJNVCIsImFsZyI6IlJTMjU2Iiwia2lkIjoiNjgxODVmZjEtNGU1MS00Y2U5LWFmMWMtNjg5ODEyMjAzMzE3In0.AQkAAAABAAUABwAACzCudSXXSAgAAHP0D34l10gCABIJ3tlGgoJMvWi9_zzeFocVAAEAAAAYAAEAAAAFAAAADQAkAAAAZTVjOTIwMTItMWI0ZC00ZTgzLTgzNjYtNjgzNDQ0NjQyNjc0IwAkAAAAZTVjOTIwMTItMWI0ZC00ZTgzLTgzNjYtNjgzNDQ0NjQyNjc0EgABAAAABgAAAGp3dF9icg.yt_0QtjwAcL1dosfVuaNoKoM3Yzq9DK4MUf6lx3Sp5EYy0OdeSaKt6TgsAujUNQQoQH4e_IZFGtVXxBjFXzP2hh9EB2nsdwKAzi5EZJcOIp1wEfzKjEllUnOXOkEoUwcmHHCSN1j4LfNF8olRTDJnaSDB9A5TbumLURC_-FkttxHitFVpt39Fvl85VtSlIsQxU544SRjeJGJMl_BDwRmu0JrUSawc5LSUF9ET9SVTBGEjS_vZMz92hdiFM2x4qZqupeSXLrQ92bhzjEXHOH7kmKbE-iKDTH_TOln0rhhqLXq25yOTBJ_yUWqhvYaxpct9GRuPo6IIZCDDv0Of7k-xQ"}>
Does anything in that API instance look wrong? I can't figure out why my token is not working.
UPDATE: I re-attempted the same flow with a token using the OAuth Token Generator and that token also fails with the same error. Additionaly, I am able to send a token using the eg-01-ruby-jwt example code with the SAME EXACT inputs when calling DocuSign_eSign::ApiClient#configure_jwt_authorization_flow. The Envelopes API takes the API client and the account ID, which are identical upon inspection between the example code and my code (besides the auth token).
Try to use the token generator (https://developers.docusign.com/oauth-token-generator) and see if that token works for you. That would isolate the issue to obtaining the token.
Make sure in the sandbox admin, you configured your IK correctly, and that you use the exact same one.
You need an RSA key generated and make sure to use it for JWT.
Make sure you point to account-d.docusign.com and not account.docusign.com (so you use our developer sandbox and not production)

Make FTPS connection in Ruby with double-bag-ftps gem

I am trying to get a native FTP connection work to an odd FTP server in ruby. It requires TLS and implicit SSL. I have a FileZilla client configured and working. Here's my code:
require 'double_bag_ftps'
DoubleBagFTPS.open(ftp_host, ftp_user, passwd, nil, DoubleBagFTPS::IMPLICIT, :verify_mode => OpenSSL::SSL::VERIFY_NONE) do |ftp|
...
files = ftp.list(file_path)
STDOUT.write files
end
I get the following runtime error when I run the above:
bunches of traceback lines
<path_to_gems>/double-bag-ftps-0.1.4/lib/double_bag_ftps.rb:160:in `initialize': wrong argument type nil (expected OpenSSL/SSL/CTX) (TypeError)
I can't seem to get anything out of the server with Ruby and the traditional net/ftp gem (various errors related to TLS/SSL problems). DoubleBagFTPS seems to be the most promising gem, but I still get an error. It may be the case that I am not calling the open function correctly. The only nil is the fourth parameter, but that's clearly spelled out in the DooubleBagFTPS example.
Can someone help?
Update
Per the suggestion, here's my new code
class MyFTP < Net::FTP
FTP_PORT = 990
def connect(host, port = FTP_PORT)
synchronize do
#host = host
#bare_sock = open_socket(host, port)
begin
ssl_sock = start_tls_session(Socket.tcp(host, port))
#sock = BufferedSSLSocket.new(ssl_sock, read_timeout: #read_timeout)
voidresp
if #private_data_connection
voidcmd("PBSZ 0")
voidcmd("PROT P")
end
rescue OpenSSL::SSL::SSLError, Net::OpenTimeout
#sock.close
raise
end
end
end
end
def ftp_options
{
username: 'user',
password: 'password',
ssl: true,
passive: true
}
end
MyFTP.open(ftp_host, ftp_options) do |ftp|
ftp.login
files = ftp.chdir(file_path)
files = ftp.list
STDOUT.write files
end
I'm still getting an error as follows:
---stack-trace---
<path_to_gem>/ruby/2.5.0/net/protocol.rb:52:in `connect': SSL_connect returned=1 errno=0 state=SSLv2/v3 read server hello A: unknown protocol (OpenSSL::SSL::SSLError)
So I got it working with regular old Net::FTP as follows:
def ftp_options
{
username: '<username>',
password: '<password>',
ssl: {
verify_mode: OpenSSL::SSL::VERIFY_NONE
}
}
end
Net::FTP.open(ftp_host, ftp_options) do |ftp|
ftp.login(ftp_options[:username], ftp_options[:password])
files = ftp.list
STDOUT.write files
puts "\n"
end
The one thing I don't understand is why I am forced to pass the username and password to the ftp.login method, since it's already defined in ftp_options, which was passed to Net::FTP.open(). As far as I can tell everything is set up correctly in ftp_options. For the particular server I'm connecting to, TLS/SSL is required, and that's working, so that parameter variable is being picked up... why not user/password?
Anyway, case closed for me at least. I can confirm that regular Net::FTP seems to work with at least one of these non-vanilla FTP servers requiring TLS and implicit SSL.

Why can I capture rb_warn messages on $stderr only once?

Due to a bad decision on OpenSSL's part that affected the Ruby bindings, the only way to check if an OCSP request is signed is by parsing the warning from OpenSSL::OCSP::Request#verify. I intercept $stderr and read the warning, but if I repeat this process in multiple unit tests, the first error message gets captured every time, even though each intercept uses a new buffer.
As an example, I created this script: test.rb
#!/usr/bin/env ruby
require 'openssl'
require 'stringio'
def main
if ARGV[0]
puts "signed: \n#{signed}"
puts "unsigned: \n#{unsigned}"
else
puts "unsigned: \n#{unsigned}"
puts "signed: \n#{signed}"
end
end
def unsigned
cert = OpenSSL::X509::Certificate.new
certid = OpenSSL::OCSP::CertificateId.new cert, cert
request = OpenSSL::OCSP::Request.new.add_certid certid
store = OpenSSL::X509::Store.new
capture_stderr { request.verify([], store) }
end
def signed
key = OpenSSL::PKey::RSA.generate(2048)
cert = OpenSSL::X509::Certificate.new
cert.public_key = key.public_key
cert.sign(key, OpenSSL::Digest::SHA1.new)
certid = OpenSSL::OCSP::CertificateId.new OpenSSL::X509::Certificate.new, cert
store = OpenSSL::X509::Store.new
store.add_cert cert
request = OpenSSL::OCSP::Request.new.add_certid certid
request.sign(cert, key)
capture_stderr { request.verify([], store) }
end
def capture_stderr
$stderr = StringIO.new
result = yield
[result, $stderr.string]
ensure
$stderr = STDERR
end
# try `./test.rb` and `./test.rb 1`
main
By flipping the order of function calls, I get different results.
$ ./test.rb
unsigned:
[false, "./test.rb:22: warning: error:27074080:OCSP routines:OCSP_request_verify:request not signed\n"]
signed:
[false, "./test.rb:38: warning: error:27074080:OCSP routines:OCSP_request_verify:request not signed\n"]
and
$ ./test.rb 1
signed:
[false, "./test.rb:38: warning: error:27074065:OCSP routines:OCSP_request_verify:certificate verify error\n"]
unsigned:
[false, "./test.rb:22: warning: error:27074065:OCSP routines:OCSP_request_verify:certificate verify error\n"]
I imagine that the explanation for this weird behavior probably goes into the C language part of the Ruby stdlib codebase.
It turned out to be a bug in Ruby's OpenSSL library. It was fixed in the Gem version 2.0.0-beta2: https://github.com/ruby/openssl/commit/9af69abcec15b114d9a0ec3811983fc1d7b5a1dc
Now OpenSSL messages aren't repeatedly leaked to stderr. However, it is now impossible to differentiate between untrusted signatures and missing signatures. Thankfully, I was wrong before and verification does indeed fail for missing signatures.

Insert digital signature into existing pdf file

I need to insert a digital signature into already existing pdf files, using a rails application server. (Basically, clients upload pdf files and the server signs them with a local certificate)
I've been using JSignpdf to insert digital signatures into pdf files, and started probing for gems for ruby...
I've found another portable file to do this job on rubypdf site http://soft.rubypdf.com/software/pdf-digital-signe, but cannot find any gem or even example code to do this in ruby.
I've looked also at Digital signature verification with OpenSSL, but couldn't understand how to actually sign an already existing document, with a local certificate file.
I also took a peak at http://code.google.com/p/origami-pdf/ , but this seems a bit harsh for a supposingly "simple" (at least in concept) task.
Any ideas/suggestions?
Thank you
After some research, recurring to the OpenSSL documentation and exploring the Origami solution, i built the code below, and managed to insert a locally generated signature/certificate into a pdf document. Now I just need to figure out how to use this with an external generated certificate (check version 2 below, where i solved it). I've opened a new question where you can find some details on a difficulty i had with OpenSSL and DER encoded certificates.
To develop version 2, i also spent some time wondering how to add an annotation - so the signature becomes visible in Adobe reader - without adding a new page to the document. From origami documentation, i found the get_page method, which solved my last problem on this. I'm using Adobe Reader X, for the record.
Hope you find this useful as I will ;-).
VERSION 1 - Generate certificate and key file, and insert them directly into the document
require 'openssl'
begin
require 'origami'
rescue LoadError
ORIGAMIDIR = "C:\RailsInstaller\Ruby1.9.3\lib\ruby\gems\1.9.1\gems\origami-1.2.4\lib"
$: << ORIGAMIDIR
require 'origami'
end
include Origami
# Code below is based on documentation available on
# http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL.html
key = OpenSSL::PKey::RSA.new 2048
open 'private_key.pem', 'w' do |io| io.write key.to_pem end
open 'public_key.pem', 'w' do |io| io.write key.public_key.to_pem end
cipher = OpenSSL::Cipher::Cipher.new 'AES-128-CBC'
pass_phrase = 'Origami rocks'
key_secure = key.export cipher, pass_phrase
open 'private_key.pem', 'w' do |io|
io.write key_secure
end
#Create the certificate
name = OpenSSL::X509::Name.parse 'CN=nobody/DC=example'
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 0
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.public_key = key.public_key
cert.subject = name
OUTPUTFILE = "test.pdf"
contents = ContentStream.new.setFilter(:FlateDecode)
contents.write OUTPUTFILE,
:x => 350, :y => 750, :rendering => Text::Rendering::STROKE, :size => 30
pdf = PDF.read('Sample.pdf')
# Open certificate files
#sigannot = Annotation::Widget::Signature.new
#sigannot.Rect = Rectangle[:llx => 89.0, :lly => 386.0, :urx => 190.0, :ury => 353.0]
#page.add_annot(sigannot)
# Sign the PDF with the specified keys
pdf.sign(cert, key,
:method => 'adbe.pkcs7.sha1',
#:annotation => sigannot,
:location => "Portugal",
:contact => "myemail#email.tt",
:reason => "Proof of Concept"
)
# Save the resulting file
pdf.save(OUTPUTFILE)
VERSION 2 - Use existing certificates to sign a pdf document
require 'openssl'
begin
require 'origami'
rescue LoadError
ORIGAMIDIR = "C:\RailsInstaller\Ruby1.9.3\lib\ruby\gems\1.9.1\gems\origami-1.2.4\lib"
$: << ORIGAMIDIR
require 'origami'
end
include Origami
INPUTFILE = "Sample.pdf"
#inputfile = String.new(INPUTFILE)
OUTPUTFILE = #inputfile.insert(INPUTFILE.rindex("."),"_signed")
CERTFILE = "certificate.pem"
RSAKEYFILE = "private_key.pem"
passphrase = "your passphrase"
key4pem=File.read RSAKEYFILE
key = OpenSSL::PKey::RSA.new key4pem, passphrase
cert = OpenSSL::X509::Certificate.new(File.read CERTFILE)
pdf = PDF.read(INPUTFILE)
page = pdf.get_page(1)
# Add signature annotation (so it becomes visibles in pdf document)
sigannot = Annotation::Widget::Signature.new
sigannot.Rect = Rectangle[:llx => 89.0, :lly => 386.0, :urx => 190.0, :ury => 353.0]
page.add_annot(sigannot)
# Sign the PDF with the specified keys
pdf.sign(cert, key,
:method => 'adbe.pkcs7.sha1',
:annotation => sigannot,
:location => "Portugal",
:contact => "myemail#email.tt",
:reason => "Proof of Concept"
)
# Save the resulting file
pdf.save(OUTPUTFILE)
If you're working on a project for pay, you might want to consider jPDFSecure, a commercial Java library built for developers to digitally sign PDF documents and change security settings on PDF Documents. With jPDFSecure, your application or java applet can encrypt PDF documents, set permissions and passwords, and create and apply digital signatures. jPDFSecure is optimized for performance and is built on top of Qoppa's proprietary PDF technology so there is no need for any third party software or drivers.
jPDFSecure has a simple interface to load PDF documents from files, network drives,URLs and even input streams, which can be generated runtime or come directly from a database. After changing security settings, jPDFSecure can save the document to a file, a java.io.OutputStream or a javax.servlet.ServletOutputStream when running in a Java EE application server to output the file directly to a browser.
jPDFSecure is platform independent and can be used in any environment that supports Java, including Windows, Mac OSX and Linux.

Resources