Jochen Woidich
Jochen Woidich

Reputation: 23

Avoid SHA1 hashing in openssl_sign / sign given hash

I'm working on replacing a legacy system that (among other things) receives SHA1 hashes of arbitrary files and signs them using a private key with a simple PHP web service.

It should look something like that:

$providedInput = '13A0227580C5DE137C2EBB2907A3F2D7F00CA71D'; 
// pseudo "= sha1(somefile.txt); file not available server side!
$expectedOutput = 'DBC9CC4CB0BECEE313BB100DD1AD39AEC045714D72767211FD574E3E3546EB55E77D2EBFE33BA2974BB74CE051608BFF45A73A52612C5FC418DD3A76CAC0AE0C8FB3FC6CE4F7A516013A9743A36424DDACFE889B3D45E86E6853FD9A55B5B4F0F0D8A574A0B244C0946A99B81CCBD1A7AF7C11072745B11C06AD680BE8AC4CB4'; 
// pseudo: "= openssl_sign(file_get_contents(somefile.txt), signature, privateKeID);

For the sake of simplicity I'm using PHP's built in openssl extention. The problem I'm running into is that openssl_sign seems to SHA1 hash the input data again internally according to this German manual entry on openssl_sign. The English entry is missing that info for some reason.

This produces the expected output ...

$privateKeyID = openssl_get_privatekey(file_get_contents($privateKey));
openssl_sign(file_get_contents("x.txt"), $signature, $privateKeyID);
var_dump(bin2hex($signature));

... but since I don't have access to the actual input files on server side it's not very helpful.

Is there a way around the additional hashing without 3rd party libs? I already tried to simply encrypt the hash received, but from How to compute RSA-SHA1(sha1WithRSAEncryption) value I understand encrypting and signing produce different output.

Update to make things more clear:

I'm recieving an SHA1 hash as input and the service has to convert it to a valid signature (using a private key) that can simply be verified using openssl_verify. The clients are out of reach, so changing their implementation is not possible.

From How to compute RSA-SHA1(sha1WithRSAEncryption) value:

If you reproduce this EM and use RSA_private_encrypt, then you will get the correct PKCS#1 v1.5 signature encoding, the same you would get with RSA_sign or even better, using the generic EVP_PKEY_sign.

I figured I could simply implement the DER encoding myself according to this specification, but the result (EM) seems too long to be encrypted with my key

// 1. Apply the hash function to the message M to produce a hash value H
$H     = hex2bin($input); // web service receives sha1 hash of an arbitrary file as input
$emLen = 128;             // 1024 rsa key

// 2. Encode the algorithm ID for the hash function and the hash value into 
// an ASN.1 value of type DigestInfo
$algorithmIdentifier = pack('H*', '3021300906052b0e03021a05000414');
$digest              = $H; 
$digestInfo          = $algorithmIdentifier.$digest;
$tLen                = strlen($digestInfo);

// 3. error checks omitted ...

// 4. Generate an octet string PS consisting of emLen - tLen - 3 octets
//    with hexadecimal value 0xff.  The length of PS will be at least 8
//    octets.
$ps   = str_repeat(chr(0xFF), $emLen - $tLen - 3);

//5. Concatenate PS, the DER encoding T, and other padding to form the
//   encoded message EM as
$em = "\0\1$ps\0$digestInfo";

if(!openssl_private_encrypt($em, $signature, $privateKeyID)) {
    echo openssl_error_string();
 }
else {
    echo bin2hex($signature);
}

Output:

Error:0406C06E:rsa routines:RSA_padding_add_PKCS1_type_1:data too large for key size

Any hints?

Upvotes: 2

Views: 5542

Answers (2)

kba
kba

Reputation: 4310

UPDATE

As you can see in code below openssl_verify return 1 for result of openssl_sign and even for openssl_private_encrypt result. I tested it on my machine. This solution will work only if sha1 digest in digital signature is used.

// Content of file
$data = 'content of file somewhere far away';

// SHA1 hash from file - input data
$digest = hash('sha1', $data);

// private and public keys used for signing
$private_key = openssl_pkey_get_private('file://mykey.pem');
$public_key = openssl_pkey_get_public('file://mykey.pub');

// Encoded ASN1 structure for encryption
$der = pack('H*', '3021300906052b0e03021a05000414') . pack('H*', $digest);

// Signature without openssl_sign()
openssl_private_encrypt($der, $signature, $private_key);

// Signature with openssl_sign (from original data)
openssl_sign($data, $opensslSignature, $private_key);

// Verifying - both should return 1
var_dump(openssl_verify($data, $signature, $public_key));
var_dump(openssl_verify($data, $opensslSignature, $public_key));

I just captured DER encoded structure by decrypting openssl_sign() result.

ORIGINAL ANSWER

openssl_sign() creates digest from data because this is how digital signature works. Digital signature is always encrypted digest from data.

You can use openssl_private_encrypt() and openssl_public_decrypt() on your sha1 digest with no fear. In general, it is the same thing but yes, there is a difference. If you encrypt something on your own, the encryption process does not care about data and just encrypts them. It is on you to know that what you will decrypt later is sha1 digest for some data. In fact, it is just data encryption with private key, not true digital signature.

openssl_sign() creates digest from data and encrypts information about kind of digest and digest itself (this is ASN.1 DER structure from your link). This is because openssl_verify() needs to know what kind of digest was used when signing.

Upvotes: 3

jww
jww

Reputation: 102296

According to the English page of openssl_sign:

bool openssl_sign ( string $data , string &$signature , mixed $priv_key_id [, mixed $signature_alg = OPENSSL_ALGO_SHA1 ] )

I think the obvious suggestion is to use OPENSSL_ALGO_SHA256. See openssl_get_md_methods for a list of the supported algorithms.

Upvotes: 1

Related Questions