2

Hi I'm having old legacy code (python2) , that creates a signature file for a digest. (in my case this digest is always a 40 character string with only 0-9a-f characters).

It uses m2crypto

I'm also having a function, that verifies whether the digest and a signature file do match.

Now I have to reimplement the signing code using cryptography instead of m2crypto (and switch to python3 lateron) However the resulting signature must be verifiable with the old m2crypto code. (The code doing the verification runs on machines. that I cannot update now, the signing however has to be migrated to cryptography (and python3)

Attached I have a full self explaining example. I guess the issue lies somewhere in the padding, which is not identical with the m2crypto and cryptography solution. but I don't know how to proceed.

The old signing code is:

def old_sign(digest):
    import M2Crypto
    rsa = M2Crypto.RSA.load_key("k.key")
    return rsa.sign(digest, "sha1")

The code to verify the signature is:

def old_check_signature(digest, signature):
    import M2Crypto
    rsa = M2Crypto.RSA.load_pub_key("k.pub")
    try:
        rsa.verify(digest, signature, algo="sha1")
        return True
    except M2Crypto.RSA.RSAError as exc:
        args = exc.args
        if len(args) < 1:
            raise
        return False

I don't know what kind of padding has been used by m2crypto and I don't know all possible variations of padding.

I tried two different paddings with cryptography

First attempt:

def new_sign1(digest):
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    from cryptography.hazmat.primitives.asymmetric import rsa

    key_data = open("k.key").read()
    key =  serialization.load_pem_private_key(
        key_data, password=None, backend=default_backend())
    return key.sign(
        digest,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA1()),
            salt_length=padding.PSS.MAX_LENGTH,
            ),
        hashes.SHA1())

second attempt

def new_sign2(digest):
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    from cryptography.hazmat.primitives.asymmetric import rsa

    key_data = open("k.key").read()
    key =  serialization.load_pem_private_key(
        key_data, password=None, backend=default_backend())

    return key.sign(digest, padding.PKCS1v15(), hashes.SHA1())


After Paul's feedback I also tried

def new_sign3(digest):
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.hazmat.primitives.asymmetric import utils

    key_data = open("k.key", "rb").read()
    key =  serialization.load_pem_private_key(
        key_data, password=None, backend=default_backend())

    return key.sign(
        digest,
        padding.PKCS1v15(),
         utils.Prehashed(hashes.SHA1()),
        )

which fails with ValueError: The provided data must be the same length as the hash algorithm's digest size.

Following code can reproduce the error if all above functions are declared:

dig = "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0"
sigs = [old_sign(dig), new_sign1(dig), new_sign2(dig)]
print([old_check_signature(dig, sig) for sig in sigs])

The output is [True, False, False], which means, that only the signature created with m2crypto is correct and the signatures created with cryptography cannot be verified with m2crypto.

If you want to test the code and don't have a key file and a public key file you can use following snippet to create one:

KEY = """
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAusZYCLS8RPUJ3fhod3cr2foK++t3R30Eiqstq+B5vIIrcJN1
2HnKfU1o3m9cwEJOezHxuI7Ks8/YVt/bGy6HyY4i3bk5AS9BK4gEptmUlldsvV+l
hOLel884dwhUk2ZrEpCq7YvSpMBb7ORKcUwcTu9tyuSHTlVpabYADTStebDWwp0+
heeRWN/zG5CN0zO4BScRNTpnw/TAqBTytuWgo/YFhBX6a4U3fKKASqynn5ZJYAg3
CDqdAe4BM2OZUv73UHWQsltC3xyKqsN6+gH7O7WoetxdMZsvYtEp4hvhUjgCrDSU
gSvtZD8BMxcPPO59w670DdVa3EPLWZzhGel18wIDAQABAoIBAB+DrAL8C/BOsDWF
3oqZzwpeiE/tcRjc3VFQhMpFfAT0qcO6/d1i32m5EALII4xFI9zhlnmfjlA8t7Ig
32V8umil1Pg4cofio0pnDvHgMJQVeEGTy+faJ9jRnCNpgmvEkjh1tIGUYBxwYJJe
CrmHMBeZipr7aGEtRDYUAXo48zRe+mO41ICkPeVRRSK0b5F9P7SOm+QnkN8rHmtm
MsCJ0jPJ1fG5Xi5BVVdjhQ6+S42tbm/A68Yx/uWSC3pt+ScIklgYF2owawVWDbWJ
Eekl0sNUTm1j1OlnyAEbb8R/VkYsLYfEwpgULTzkokBRq4NZxjCKY7VJRZ05neev
4JbdcOECgYEA46e+L/uvHUUzyhWgrNgHAdT7Eff4Imu7WdG+KQ7d7kNazMJFyMrk
1Ln66i/oPyiMwMXcACmEoA5Jl/wBk3sjKjLno9dvMM3amfwkBZav4FirQS+9Xk/l
EQAoc/WxsxlVywM00c20CgGqNu0hRBlM5C8zCoIFlt6G2Mwv58yd8hkCgYEA0geU
7FXh/xVnq4wU9bnTF0laPd4HlVYYNpRMjTdRwwIycrCynAlfEF+keUyOXhgYw5vm
loyICCqsNi5pw8tRPFyzBiESkLUL+ZTcfxswfgmhlTs71Zk72b8LCM3sRPeuUg2a
6GrNvycWRLfc+toq5NvSKnfVQS2tgMKJ0CtPIesCgYB+Za0H8SKaCsklY3qxXMQP
NVQs9tOTMON1jCmbnECGQGlSlG6wfE4u+g+hJPY60uXLRk/O2z5iq2wa8XVikBTH
IjpQUpXOsAy2QDMz0yVVV4XGDJ6ElbFmDgNn1rtR6DglHmOeNSrH/4KlOmWk7LMv
YjFhnS1DRcvy5POYLJhpSQKBgEILIEkwuF/92xuWcQDT7gzkg/vwVXIgIH0JJQlC
2/L2PebSqVdnmv0LFi0OZbYw3Zik7V1p01y+Dmj7L0biKClS/PhwbeYTCDDzHmLZ
qeX4IVdLyQThqnBOIqoiFqmZOLeUj6GF9Cynndj99/7pm5NbjDrOc8CLHIPgqHVN
KRUBAoGBAKEr1mg+Hjf+M/5ru1tQT6xTvlKW1rS4ioGB35XQf+n/OAO98SPQkwPQ
XfX5nKHif/TmDZygGeHYm96qCceOmzCL7LqaYOb1qTZLEk6L18eEtHwG+hXyMKuA
t2XLK89blAoPT+9x10KKp27IWe/W+QCD6LnrdjoN9BQ7fCnV5v3Q
-----END RSA PRIVATE KEY-----
"""

open("k.key", "w").write(KEY)


PUB_KEY = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAusZYCLS8RPUJ3fhod3cr
2foK++t3R30Eiqstq+B5vIIrcJN12HnKfU1o3m9cwEJOezHxuI7Ks8/YVt/bGy6H
yY4i3bk5AS9BK4gEptmUlldsvV+lhOLel884dwhUk2ZrEpCq7YvSpMBb7ORKcUwc
Tu9tyuSHTlVpabYADTStebDWwp0+heeRWN/zG5CN0zO4BScRNTpnw/TAqBTytuWg
o/YFhBX6a4U3fKKASqynn5ZJYAg3CDqdAe4BM2OZUv73UHWQsltC3xyKqsN6+gH7
O7WoetxdMZsvYtEp4hvhUjgCrDSUgSvtZD8BMxcPPO59w670DdVa3EPLWZzhGel1
8wIDAQAB
-----END PUBLIC KEY-----
"""
open("k.pub", "w").write(PUB_KEY)


Thanks in advance for any suggestions.

I don't use SO that often, I have also a full blown python file (102 lines), that is completely self contained. How to share such files on SO in a question

Addendum 2019-06-13 23:15:00 UTC

It seems. that M2Crypto.RSA.sign()

calls finally following C code. (from the m2crypto git repository in the file SWIG/_m2crypto_wrap.c.

I'm not really good with libssl, but perhaps somebody else understands this and can tell me how to 'reproduce the calculation' (perhaps with openssl commandline or with ctypes) without using M2Crypto.

PyObject *rsa_sign(RSA *rsa, PyObject *py_digest_string, int method_type) {
    int digest_len = 0;
    int buf_len = 0;
    int ret = 0;
    unsigned int real_buf_len = 0;
    char *digest_string = NULL;
    unsigned char * sign_buf = NULL;
    PyObject *signature;

    ret = m2_PyString_AsStringAndSizeInt(py_digest_string, &digest_string,
                                         &digest_len);
    if (ret == -1) {
        /* PyString_AsStringAndSize raises the correct exceptions. */
        return NULL;
    }

    buf_len = RSA_size(rsa);
    sign_buf = (unsigned char *)PyMem_Malloc(buf_len);
    ret = RSA_sign(method_type, (const unsigned char *)digest_string, digest_len,
                   sign_buf, &real_buf_len, rsa);

    if (!ret) {
        m2_PyErr_Msg(_rsa_err);
        PyMem_Free(sign_buf);
        return NULL;
    }

    signature =  PyBytes_FromStringAndSize((const char*) sign_buf, buf_len);

    PyMem_Free(sign_buf);
    return signature;
}

A workaround using subprocess.Popen or ctypes (directly attacking libssl) is (in my context) acceptable.

gelonida
  • 5,327
  • 2
  • 23
  • 41
  • 1
    [M2Crypto RSA.sign vs OpenSSL rsautl -sign](https://stackoverflow.com/q/11221898/608639), [M2crypto Signature vs OpenSSL Signature](https://stackoverflow.com/q/41290549/608639), [RSA Signature is different generated from rsa module and m2crypto](https://stackoverflow.com/q/45320041/608639), etc. – jww Jun 14 '19 at 11:39
  • Thanks. Well whatever I try: I do not manage to get the same signature with any other code (I tried cryptography and command line openssl rsautl -sign so far) I think the original code is kind of bugged, but that is what is currently used on some machines. Will try some more things, but in the end I might just give up to have an intermediate code without m2crypto, that can create these old signatures and lateron switch to a new signing and a new signature verification algorithm. It just makes deployment and updating a little more complicated. – gelonida Jun 15 '19 at 00:51
  • 1
    SHA-1 vs SHA-256? OpenSSL changed the default a couple of years ago. But Mcrypt is so old it is likely using SHA-1. – jww Jun 15 '19 at 00:52
  • precisely m2crypto uses sha-1 and I did not find any command line version, that yields the same result. you might look at the code, that I posted to pastebin and seeh whether you find out any command line version giving the same result – gelonida Jun 15 '19 at 00:56
  • If you look at the source for [`rsautil.c`](https://github.com/openssl/openssl/blob/master/apps/rsautl.c) it calls `RSA_private_encrypt`. It does not digest the message, and it does not format the message. You can sign with SHA-256 using `openssl digest` and piping it to `rsautil`. I don't know how to format the message with padding like PKCS #1, however. I believe you use another utility for that. (I can't keep up with OpenSSL utilities anymore). – jww Jun 15 '19 at 01:04

1 Answers1

1

M2Crypto is probably using PKCS1 padding when calling sign so the PKCS1v15 example is close to what you want. Assuming it defaults to PKCS1 (and not no padding, which is wildly insecure and cryptography does not support) then the digest is likely the problem.

Is digest already the hash you want signed or is it the data to be hashed? Cryptography, by default, hashes the provided data for you. If the data has already been hashed then you need to use Prehashed to avoid that step.

Paul Kehrer
  • 13,466
  • 4
  • 40
  • 57
  • Paul, I tried already something like that, but got stuck as well. The digest in my example is a string like: dig = b"f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0" If I used: `return key.sign(digest, padding.PKCS1v15(), utils.Prehashed(hashes.SHA1()),)` I get following error: ValueError: The provided data must be the same length as the hash algorithm's digest size. – gelonida Jun 13 '19 at 08:20
  • The digest in your example is encoded in hex and needs to be decoded to binary to use with Prehashed. `binascii.unhexlify()` will do it for you. – Paul Kehrer Jun 13 '19 at 15:46
  • Yes I can do that and will try it However I have my doubts, that the signature will be compatible with the existing m2crypto code `old_sign()`, that is at the moment used on different machines and that tries to verify the signature. The whole objective is to have code that is interoperable with the currently existing solution and whose signatures validate with old_check_signature() – gelonida Jun 13 '19 at 18:14
  • 1
    I think to concretely answer the possibility of that you need to determine what M2Crypto's rsa.sign does by default. If it is unpadded RSA then cryptography does not support that, but you should IMMEDIATELY fix it as it's a major security issue. – Paul Kehrer Jun 13 '19 at 18:58
  • Agreed the main question is what m2crypto is doing (I did not find details about it) and to see whether cryptography is able to do the same. For future versions (whenever I can update all hosts, that verify the signature and the host, that signs) I will definitely use smarter / safer padding. – gelonida Jun 13 '19 at 19:43
  • unhexlify gets rid of the ValueError. However the results don't match. Not sure how to debug that issue and find out what m2crypto is doing. I'm quite lousy with reverse engineering. :-( But will give it a shot. – gelonida Jun 13 '19 at 21:56
  • I identified the C code, that is finally called. Hope now to find a way to reproduce the M2crypto behavior without m2crypto (perhaps with subprocess or ctypes if cryptography refuses to excute such a weird / weak algorithm) – gelonida Jun 13 '19 at 22:19