Verifying a Detached S/MIME Signature in Python
I was recently experimenting some more with my iOS MDM server, and found that I needed to verify inbound signatures on the messages the clients send to the server. It took some doing, but eventually I found the right way to handle it at the command line.
I had to take the signature (in this case, provided as a base-64 string in the HTTP header), decode it, and save it to a file. Then I needed a copy of the public key for the certificate used to sign the message, and finally, I had to copy the text of the message itself to another file. Once all that was done, it was something like this:
openssl smime -verify -in <sig> -inform der -content <msg> -certfile <signer cert> -CAfile <ca cert>
This then spits out a copy of the text, and the message “Verification successful.” Very simple, though it took a while to get it exactly right. (I may have been using the wrong signer certificate for some time, though….)
So this is all well and good, but how do I do this programatically? In my server? Should be easy, right? Just find a good OpenSSL library for Python, and Bob’s your Uncle!
Turns out, that’s the hard part–finding a good OpenSSL library for Python. PyOpenSSL was abandoned for a while, but recently revived. The other library I found, M2Crypto, was a bit more arcane and also fairly dated. But in the end, I did figure out how to get M2Crypto to work. Here’s the code, in a nutshell:
from M2Crypto import SMIME, X509, BIO
raw_sig = ""
msg = """"""
sm_obj = SMIME.SMIME()
x509 = X509.load_cert('signer.crt') # public key cert used by the remote
# client when signing the message
sk = X509.X509_Stack()
sk.push(x509)
sm_obj.set_x509_stack(sk)
st = X509.X509_Store()
st.load_info('CA.crt') # Public cert for the CA which signed
# the above certificate
sm_obj.set_x509_store(st)
# re-wrap signature so that it fits base64 standards
cooked_sig = '\n'.join(raw_sig[pos:pos+76] for pos in xrange(0, len(raw_sig), 76))
# now, wrap the signature in a PKCS7 block
sig = """
-----BEGIN PKCS7-----
%s
-----END PKCS7-----
""" % cooked_sig
# and load it into an SMIME p7 object through the BIO I/O buffer:
buf = BIO.MemoryBuffer(sig)
p7 = SMIME.load_pkcs7_bio(buf)
# do the same for the message text
data_bio = BIO.MemoryBuffer(msg)
# finally, try to verify it:
try:
v = sm_obj.verify(p7, data_bio)
if v:
print "Client signature verified."
except:
print "*** INVALID CLIENT MESSAGE SIGNATURE ***"
This seemed to work just fine, though it did take a few tweaks to get exactly right. One thing that I found useful was to verify that I was at least getting the same message hash. Adding the following code to my test script (in the appropriate places), I could then see what it thought my message hashed to:
import binascii
from M2Crypto import EVP
md = EVP.MessageDigest('sha1')
md.update(msg)
print binascii.b2a_hex(md.digest())
Then, I’d look at the .der signature for the hash inside it:
openssl asn1parse -in sig.der -inform der
and look for the “HEX DUMP” string right after the “:messageDigest” header. If that matched what the script showed, then I knew I at least had the content properly processed, and any remaining problems were probably with the certificate. If they didn’t match, then I apparently hadn’t properly read the content in the first place, and no amount of fiddling with PKI would yield a positive result.
In the end, though it wasn’t nearly as simple as “verify(sig, text, ca)” I was still able to get it working reliably, and eventually added it to my server’s bag of tricks.