I have a Java code given to me by a vendor where we generate signature of the payload and send the signature along with the payload in the request. The signature is the same for the same payload no matter how many times the code is run. I converted the same to Golang as well. The problem is I am unable to replicate the same in Python.
Basically the algorithm is:
- Generate SHA-1 digest of the payload
- Base16 encode the generated SHA-1 digest
- Encrypt the encoded digest with RSAPrivateKey
- Base16 encode the encrypted digest
My java snippet:
private static String encryptDigest(String raw, String privateKeyString) throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
Key privatekey = null;
StringReader reader = new StringReader(privateKeyString);
try {
PEMReader pemReader = new PEMReader(reader);
KeyPair keyPair = (KeyPair) pemReader.readObject();
privateKey = keyPair.getPrivate();
pemReader.close();
} catch (IOException i) {
log.error("Error while initializing Private Key", i);
}
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(raw.getBytes());
byte[] encodedBase16 = Hex.encode(digest);
String encodedDigest = new String(encodedBase16);
String strEncrypted = "";
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
cipher.init(Cipher.ENCRYPT_MODE, privatekey);
byte[] encrypted = cipher.doFinal(encodedDigest);
byte[] encoded = Hex.encode(encrypted);
strEncrypted = new String(encoded);
return strEncrypted;
}
My Python snippet:
import hashlib
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
import binascii
import os
private_key_pem = """-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----"""
def get_signature(payload):
"""Replicates the Go code's signature generation."""
# 1. Load the private key
private_key = serialization.load_pem_private_key(
private_key_pem.encode("utf-8"),
password=None,
backend=default_backend()
)
private_numbers = private_key.private_numbers()
public_numbers = private_key.public_key().public_numbers()
# Print key parameters to compare with OpenSSL & Java
print("Modulus (n):", hex(public_numbers.n))
print("Private Exponent (d):", hex(private_numbers.d))
print("Public Exponent (e):", hex(public_numbers.e)) # Remove '0x' prefix
# 2. Payload processing
escaped_payload = payload.replace("\n", "").replace(" ", "").replace('<?xmlversion="1.0"encoding="UTF-8"?>', "")
# 3. SHA-1 hashing
sha1_hash = hashlib.sha1(escaped_payload.encode("utf-8")).digest()
sha1_hash_hex = binascii.hexlify(sha1_hash).decode("utf-8")
sha1_hash_bytes = binascii.unhexlify(sha1_hash_hex)
# 4. RSA signing (crucial part)
signature = private_key.sign(
sha1_hash_bytes,
padding.PKCS1v15(),
hashes.SHA1(),
)
# 5. Hex encoding
hex_signature = binascii.hexlify(signature).decode("utf-8")
return hex_signature
print(get_signature('helloworld'))
Debugging Steps taken by me to verify:
- SHA-1 hash is matching in Java and Python
- hex of SHA-1 hash is matching in Java and Python
- the private key is read properly and matching. I have matched the modulus, private and public exponent in Java and Python.
From what I got from LLM's and the web is there are subtle difference in how PKCS#1 v1.5 padding is appled in Java and Python. But I don't think that is the problem. Can anyone point me where I am going wrong.
sample private key:
-----BEGIN RSA PRIVATE KEY-----
MIIBOQIBAAJAXWRPQyGlEY+SXz8Uslhe+MLjTgWd8lf/nA0hgCm9JFKC1tq1S73c
Q9naClNXsMqY7pwPt1bSY8jYRqHHbdoUvwIDAQABAkAfJkz1pCwtfkig8iZSEf2j
VUWBiYgUA9vizdJlsAZBLceLrdk8RZF2YOYCWHrpUtZVea37dzZJe99Dr53K0UZx
AiEAtyHQBGoCVHfzPM//a+4tv2ba3tx9at+3uzGR86YNMzcCIQCCjWHcLW/+sQTW
OXeXRrtxqHPp28ir8AVYuNX0nT1+uQIgJm158PMtufvRlpkux78a6mby1oD98Ecx
jp5AOhhF/NECICyHsQN69CJ5mt6/R01wMOt5u9/eubn76rbyhPgk0h7xAiEAjn6m
EmLwkIYD9VnZfp9+2UoWSh0qZiTIHyNwFpJH78o=
-----END RSA PRIVATE KEY-----
Signature generated by java
01540a01e7c372f4d1395221ec90f68a0f4dbc123af9d032b768fd141c0b0a6420e0dcf903739dd2729cfdbf81bcd9512cc39ad4bd26239eab23069fdaf4e6fe
Signature generated by python
252a29b2d5459f4506ab3bb143f24adc60c41a10afb6d7557a1c98cfc244a002eb1490d84780d40233420ef1bc83e005ab7cdb390809c06a757bd84dd790cb2b
0x01540a...you posted cannot have been generated with the Java code you posted. The Java code generates the SHA-1 hash and signs it. The hash is not hex encoded before signing. On the other hand, your description states that the hash is hex encoded (step 2), and indeed the signature0x01540a...can only be reproduced with the hex encoded hash. What is correct now?