OpenPGP mail encryption with Ruby
TL;DR
This is going to be a very long post, so if you’re in a hurry, already know what GnuPG is and just want to send encrypted mail from your Ruby app now, here’s the short version:
After a lot of googling and not finding much on the topic in a Ruby / Rails context I built the mail-gpg gem, which basically adds PGP encryption through the GPGME library to Ruby’s Mail gem and, for your convenience, also to ActionMailer. The github page has basic usage info, fork it, open tickets, send pull requests, have fun!
Oh, before you leave - the OpenPGP best practices guide I found while working on that is really worth a read.
Why?
Why not? Unless you’ve been living under a rock the last few months you should know by now that anything you send over the wires that make up the internet will be stored and possibly looked at by, ehm, let’s call them interested third parties that only want your best - literally any piece of information they can get hold of. Be it the cat pix you send to colleagues or the construction plans for the nuclear fusion power plant you’ve been working on in your spare time for the last 20 years - just to be on the safe side they will store whatever they can, for as long as they can.
But we use HTTPS everwhere!
Go and check if you (or the services you use) have Perfect Forward Secrecy properly configured. And while you are at it, google BREACH, BEAST and CRIME.
Done? Good. Back to email encryption…
This quite interesting topic recently came up at Planio: while any reasonable web application nowadays uses HTTPS at least for sign in and anything happening after that, we were not aware of a single one that would sign or even encrypt their email communication with their users.
This not only potentially sends a lot of data unencrypted over the air that you otherwise try to carefully protect by using HTTPS, it also makes your users more vulnerable to phishing attacks through fake emails. So why not generally sign any outgoing emails, and give your users the choice to opt in for encrypted communication by uploading their public key?
Some background on email encryption
Today there are two competing systems for encrypting / signing email: S/MIME and PGP/MIME. Both are asymmetric systems, meaning you use the recipient’s public key for encryption, and your private key for signing. The recipient uses his private key for decryption and your public key for verifying the signature. Signing and encryption can be used together or on their own.
The biggest difference between the two systems is in how trust is established between the sender and receiver of a message. The concept of trust is important for verifying signatures – only with a certain level of trust in the key that has been used for signing a message you can say yes, this message is indeed from ACME Corp. Without trust you only know for sure that the message has been signed by a key claiming to belong to ACME Corp. That means that you need to have a way to verify whether a given key really belongs to the entity claiming it is their’s.
S/MIME is based on certificates issued by trusted certificate authorities (just like SSL certificates for web servers). How much you really trust those centralized CAs is up to you and your personal level of paranoia. Personally I think that guys from any Land of the Free 3 letter agency walking into some CA and leaving with a signed certificate for whatever identity they want to incorporate is not a very unlikely scenario. If they even need to walk in there in person at all…
PGP on the other hand relies on a decentralized Web of Trust, which means that anybody can publicly declare that she believes that a given key belongs to a given identity by signing that key and publishing that signed version. The theory is that if you trust A, and A trusts B, you can trust B as well.
There are different ways to establish trust in the PGP world - it all boils down to making sure a given key really belongs to the entity claiming it is their key. Wether you do that by meeting someone in person, verifying their identity and getting their key fingerprint from them in written form (a good idea might be to have your key fingerprint printed on your business cards) or you go to that persons or companies website (in case you trust their CA, see above…) to get hold of their key fingerprint depends again on your level of paranoia.
In the end you compare the fingerprint of the key in question against the fingerprint from the business card or website and if they match, sign the key either locally for your own further usage or make your trust public by publishing the signed key.
The rest of this post will stick to the PGP/MIME variant of email encryption as defined in RFC 3156. More specifically we’ll use GnuPG, which is an open source implementation of the OpenPGP standard. I won’t go into detail here on how to setup GnuPG on your machine, but I want to point you to this OpenPGP best practices guide which might also be helpful if you’ve already used OpenPGP before.
PGP/MIME with GPGME and the Mail gem
GPGME or GnuPG Made Easy is a C library for accessing GnuPG functionality from within any application. We’ll use the gpgme gem which nicely wraps that library and even adds some higher level API on top of it.
The Mail gem is what’s being used by Rails for composing and sending mail since I think 3.0. You never really get in touch with it there because it’s completely hidden by ActionMailer, but it’s very easy to use on it’s own, too:
message = Mail.new do
to 'john@foo.bar'
from 'jane@doe.com'
subject 'whatever'
body 'test'
end
In order to send such a message in an encrypted way we need to encrypt it as a whole and send the result as part of a multipart mail. The exact layout of that outer email is laid out in RFC 3156. Given the plain text mail we created above it comes down to this:
1) create another Mail::Message using the headers from the plain text mail
encrypted_mail = Mail.new do
to message.to
from message.from
subject message.subject
end
2) create a part holding a very tiny bit of meta data and add it to the encrypted message
part1 = Mail::Part.new do
body 'Version: 1'
content_type 'application/pgp-encrypted'
content_description 'PGP/MIME Versions Identification'
end
encrypted_mail.add_part part1
3) create another part, holding the encrypted content:
part2 = Mail::Part.new do
content_type 'application/octet-stream; name="encrypted.asc"'
content_disposition 'inline; filename="encrypted.asc"'
content_description 'OpenPGP encrypted message'
body encrypt(message.encoded.to_s, recipients: mail.to)
end
encrypted_mail.add_part part2
4) set the content type and preamble of the encrypted message
encrypted_mail.content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{mail.boundary}"
encrypted_mail.body.preamble = "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
The content type re-uses the MIME boundary the Mail gem already has computed. The preamble is what people whose mail client cannot handle the PGP/MIME format will see.
That’s it! Well, except of the implementation of the encrypt
method… Thankfully the gpgme gem makes this part quite easy:
def encrypt(text, *recipients)
GPGME::Crypto.new.encrypt(text, recipients: recipients).to_s
end
That was too easy…
Well, while this code will actually work if you have the reipient’s public key in your gpg keychain things will most likely become a little more complex in reality:
- you may want to copy over more headers to the corresponding header fields of the encrypted mail (think of
cc
,bcc
,reply_to
and any custom header fields) - you need to encrypt for all receivers, which means you need to include
cc
andbcc
recipients as well in the call toencrypt
- you need to have the public keys of all recipients in your keychain in order to encrypt for them.
- signing in addition to encryption requires you to specify the passphrase for the private key. And it requires you to have a private key for signing set up in the first place.
- the GPGME lib will by default use the keychain located in
~/.gnupg
of the user that’s running it, so in the case of a web application you might want to configure a different GnuPG home directory by setting theGNUPGHOME
environment variable to the place where you set up the keychain for your web application to use.
Wrapping it up
Given all of the above I think it’s a good idea to have a library helping to reduce the complexity to a minimum. Something like this:
message = Mail.new do
to 'john@foo.bar'
from 'jane@doe.com'
subject 'whatever'
body 'test'
gpg encrypt: true, sign: true
end
Or this, for your Rails app:
class MyMailer < ActionMailer::Base
default from: 'joe@foo.bar'
def top_secret
mail to: 'jane@doe.com', body: 'encrypted mail', gpg: { encrypt: true }
end
end
Neat, isn’t it? It’s there! Please play around with it and tell me what you think! Until now the only client I tested with is Mutt, so feedback regarding different mail clients will be highly appreciated.