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.