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

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.

Related

`[]': no implicit conversion of Symbol into Integer (TypeError) for a simple ruby Hash

In my ruby code, I define a new hash:
options = {
host: 'localhost',
user: nil,
file: nil,
}
I then parse the hash using the OptionsParser
OptionParser.new do |opts|
opts.banner = 'Usage: ruby-remote-tailer.rb [options]'
opts.on('-h', '--host', 'The host to run ssh session with') do |host|
options[:host] = "#{host}"
end
opts.on('-h', '--user', 'The user account that will run the session') do |user|
options[:user] = "#{user}"
end
opts.on('-f', '--file', 'The file to run the tail on') do |file|
options[:file] = "#{file}"
end
end
And run:
options = ARGV.parse!
puts options[:host]
The last puts line results in an error no implicit conversion of Symbol into Integer (TypeError). I know that the input I put in is correct as doing p options works. Any ideas on how to fix this?
(Note I would prefer not to use a .each loop as suggested in other answers to get the single values I need).
Thanks.
You are not using OptionParser correctly in several ways.
When defining the options you have to provide a placeholder for the actual value of the option. If you do not, it will be interpreted as a switch, returning either true or false depending on whether the switch was set.
opts.on('-h', '--host HOST', 'The host to run ssh session with') do |host|
options[:host] = "#{host}"
end
Another mistake is that you defined -h for both the host and the user option. You better define a different letter to each of them, you probably intended to have -u for the user anyway.
But the main problem, the one causing the error is that you treat the return value of the #parse! method as if it would return the parsed values. But the return value is actually the remainder of the ARGV array that was not matched by the parser. And because you try to access it like a Hash by asking for an element by Symbol, it complains because array elements are accessed only by Integer values. To fix it, just keep the options reference from before and don't assign the return value to it.
ARGV.parse!
From here on, I will give you some further criticism, but the things I will recommend should not be the reason of any errors:
Additionally you might skip the default nil values you provided in the Hash in the beginning, if you ask a Hash for an undefined key, you will provide you with a nil anyway.
options = {
host: 'localhost'
}
I'd say calling #parse! on the the command line argument array ARGV, while it seems to be an option to do so, is not very obvious. I'd recommend saving a reference to the parser to a variable and call the method on the parser.
parser = OptionParser.new do |opts|
…
end
parser.parse!(ARGV)
If you want you can even call it without an argument, it will use ARGV by default. But this would again make your code harder to understand in my opinion.
And you can also get rid of the string conversion of the values through interpolation. At least when you are actually getting your values from the command line arguments array ARGV you can be quite sure that all elements will be String objects. However if you intend to feed the parser with other arrays which are not entirely built with string elements, you should keep the conversion.
opts.on('-h', '--host HOST', 'The host to run ssh session with') do |host|
options[:host] = host
end
Also please note that there is a very, very widespread convention that you use an of exactly two spaces for each level of indentation in Ruby code. In your examples you use four and eight spaces, which a lot of Rubyists would dislike very much. See the The Ruby styleguide for more information.
Here is a fixed-up version of your code:
#!/usr/bin/env ruby
require 'optionparser'
options = {
host: 'localhost'
}
parser = OptionParser.new do |opts|
opts.banner = 'Usage: ruby-remote-tailer.rb [options]'
opts.on('-h', '--host HOST', 'The host to run ssh session with') do |host|
options[:host] = host
end
opts.on('-u', '--user USER', 'The user account that will run the session') do |user|
options[:user] = user
end
opts.on('-f', '--file FILE', 'The file to run the tail on') do |file|
options[:file] = file
end
end
parser.parse!(ARGV)
puts options[:host]

Zlib data error while opening file in Ruby - works when a string is specified

I wrote this bit of code that reverses XOR-based encryption in Ruby. The chipertext is XORed with 'key' and the output is passed to Zlib.deflate.
require 'zlib'
def bin_to_hex(s)
s.unpack('H*').first
end
def hex(s)
s.scan(/../).map { |x| x.hex }.pack('c*')
end
chipertext = "Encrypted data"
key = "Some encryption key"
puts hex((bin_to_hex(Zlib::Inflate.inflate(code)).to_i(16) ^ ((bin_to_hex(key) * (bin_to_hex(Zlib::Inflate.inflate(chipertext)).length/bin_to_hex(key).length)) + bin_to_hex(key)[0, bin_to_hex(Zlib::Inflate.inflate(chipertext)).length%bin_to_hex(key).length]).to_i(16)).to_s(16))
The code runs perfectly when I specify chipertext as a string, in the example above. But when I use code like chipertext = File.open(ARGV[0], 'rb') { |f| f.read }, I get a inflate: incorrect header check (Zlib::DataError).
How can I prevent this from happening?

How to programmatically check if a certificate has been revoked?

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.

Ruby: Parse API Response

I am trying to geht this script to run: http://dysinger.net/2008/10/13/using-amazon-ec2-metadata-as-a-simple-dns but dosnt work because it is using an old amazon sdk version, i rewrote it to use the new one:
#!/usr/bin/env ruby
require "rubygems"
require "aws-sdk"
%w(optparse rubygems aws-sdk resolv pp).each {|l| require l}
options = {}
parser = OptionParser.new do |p|
p.banner = "Usage: hosts [options]"
p.on("-a", "--access-key USER", "The user's AWS access key ID.") do |aki|
options[:access_key_id] = aki
end
p.on("-s",
"--secret-key PASSWORD",
"The user's AWS secret access key.") do |sak|
options[:secret_access_key] = sak
end
p.on_tail("-h", "--help", "Show this message") {
puts(p)
exit
}
p.parse!(ARGV) rescue puts(p)
end
if options.key?(:access_key_id) and options.key?(:secret_access_key)
puts "127.0.0.1 localhost"
AWS.config(options)
AWS::EC2.new(options)
answer = AWS::EC2::Client.new.describe_instances
answer.reservationSet.item.each do |r|
r.instancesSet.item.each do |i|
if i.instanceState.name =~ /running/
puts(Resolv::DNS.new.getaddress(i.privateDnsName).to_s +
" #{i.keyName}.ec2 #{i.keyName}")
end
end
end
else
puts(parser)
exit(1)
end
What this should do is outputing a new /etc/hosts file with my ec2 instances in it.
And i get a response =D, but answer is a hash and therefore i get the
error undefined method `reservationSet' for #<Hash:0x7f7573b27880>.
And this is my problem, since i dont know Ruby at all ( All I was doing was reading Amazon Documentation and playing around so i get an answer ). Somehow in the original example this seemed to work. I suppose that back then, the API did not return a hash, anyway...how can i iterate through a hash like above, to get this to work?
This code may help you:
answer = AWS::EC2::Client.new.describe_instances
reservations = answer[:reservation_set]
reservations.each do |reservation|
instances = reservation[:instances_set]
instances.each do |instance|
if instance[:instance_state][:name] == "running"
private_dns_name = instance[:private_dns_name]
key_name = instance[:key_name]
address = Resolv::DNS.new.getaddress(private_dns_name)
puts "{address} #{key_name}.ec2 #{key_name}"
end
end
end
Generally change your code from using methods with names e.g. item.fooBarBaz to using a hash e.g. item[:foo_bar_baz]
When you're learning Ruby the "pp" command is very useful for pretty-printing variables as you go, such as:
pp reservations
pp instances
pp private_dns_name

Reading/Writing password protected and encrypted file in ruby

I want to encrypt a file that a ruby program will be loading data from.
In addition, I need the program to prompt for a password on startup that will be used to decrypt the file.
In other words, the file needs to reside encrypted on the machine and only users with passwords will be able to run the app.
I have started to look at openpgp but as far as I understand, this does still not solve the password problem.
There's two easy ways to go about doing this. One is to shell out to openssl to do your encryption / decryption there. The arguably better way would be to use the Ruby Crypto gem.
Program to encrypt:
require 'rubygems'
require 'crypt/blowfish';
puts "Password? "
pw = gets
puts "Secret data? "
data = gets
blowfish = Crypt::Blowfish.new(pw)
r = StringIO.new(data);
File.open('data', 'w') do |f|
while l = r.read(8) do
while l.size < 8 do l += "\0" end
f.print blowfish.encrypt_block(l)
end
end
Program to decrypt:
require 'rubygems'
require 'crypt/blowfish';
puts "Password? "
pw = gets
blowfish = Crypt::Blowfish.new(pw)
r = StringIO.new();
File.open('data', 'r') do |f|
while l = f.read(8) do
r << blowfish.decrypt_block(l)
end
end
puts "Secret data:"
puts r.string
This example uses the Blowfish symmetric block cypher. Other cyphers could be used. Also, you would probably want to concatenate a fixed string to the password, to make the key longer and to help tie the encryption/decryption to your application.
Try the encrypted strings gem. Works like a charm.

Resources