Rails: Digitally sign outgoing emails (S/MIME)
In this article, I will introduce one method to digitally sign outgoing emails with S/MIME using Ruby on Rails.
require ‘openssl’ vs. Kernel.system openssl
At first, I tried to sign mails using the Ruby OpenSSL library which is basically a lightweight wrapper for libopenssl. However, I was not successful; I found out how to create PKCS7 signatures in general, but not especially for S/MIME mails.
So I decided to use the openssl command line tool that can be invoked like this:
openssl smime -sign -signer $CERT_FILE -passin pass:$CERT_PASS -in $UNSIGNED_MAIL -out $SIGNED_MAIL -certfile $CERT_CA_FILE -from 'your' -to 'recipients <email@address>' -subject 'The Subject'
This command takes an unsigned MIME mail (located in a file whose path is stored in $UNSIGNED_MAIL), signs it with $CERT_FILE (protected by $CERT_PASS), attaches the Certification Authority’s certificate ($CERT_CA_FILE) and stores the resulting signed mail (which is in multipart/signed format) into $SIGNED_MAIL, setting the From:, To: and Subject: headers appropriately.
Invoking openssl from your mailer
To sign all emails that go out from your mailer (which is probably derived from ActionMailer::Base), you can use this code:
CERT_DIR = ‘…’
CERT_FILE = ‘cert.pem’
CERT_PASS = ‘passphrase for cert.pem’
CERT_CA_FILE = ‘your-ca.pem’
#… your other mailer code …
# overload deliver! to sign outgoing emails
def deliver!
unsigned = Tempfile.new ‘notification-unsigned’
unsigned.write mail
unsigned.close
signed = Tempfile.new ‘notification-signed’
signed.close
Kernel.system ‘openssl’, ‘smime’, ‘-sign’, ‘-signer’, CERT_DIR+CERT_FILE, ‘-passin’, ‘pass:’+CERT_PASS,
‘-in’, unsigned.path, ‘-out’, signed.path, ‘-certfile’, CERT_DIR+CERT_CA_FILE,
‘-from’, ‘YOUR NAME <EMAIL@ADDRESS>’, ‘-to’, mail.to.join(’,’), ‘-subject’, mail.subject
unsigned.close!
mail.instance_variable_set ‘@smime_encoded’, File::read(signed.path)
signed.unlink
class << mail
define_method :encoded do
self.instance_variable_get ‘@smime_encoded’
end
end
super mail
end
end
This is a bit dirty because the first parameter for “deliver!” is normally a TMail::Mail object whose “encoded” method is called by the delivery method. In the code above, I have replaced the “mail” object (first parameter for “deliver!”) by a String with an attached “encoded” method that returns the string itself. However, it was the most simple working solution I could think of. Please tell me if you know a cleaner solution (re-parsing the signed email into a TMail::Mail object doesn’t work because it changes the signed mail by a few bytes and makes the signature invalid).
The above code assumes that both public (signature) and private key are stored in the $CERT_FILE. If this is not the case for you, you’d have to modify the command line slightly.