I try to make the app with PHP that creates ECDSA signature for some document and that signature is verified with Golang app.
I use private keys generated with openssl tool. It is prime256v1 curve key. Created with the command:
openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-key.pem
In PHP i create signature using openssl_sign function.
And all my attempts to verify the signature with Golang fail. In Golang use the crypto/ecdsa, crypto/elliptic packages.
There is my code.
PHP
<?php
$stringtosign = "my test string to sign";
// Privcate key was geerated with openssl tool with the command
// openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-key.pem
$cert = file_get_contents('prime256v1-key.pem');
$prkey = openssl_pkey_get_private($cert);
// we sign only hashes, because Golang lib can wok with hashes only
$stringtosign = md5($stringtosign);
// we generate 64 length signature (r and s 32 bytes length)
while(1) {
openssl_sign($stringtosign, $signature, $prkey, OPENSSL_ALGO_SHA256);
$rlen = ord(substr($signature,3,1));
$slen = ord(substr($signature,5+$rlen,1));
if ($slen != 32 || $rlen != 32) {
// try other signature if length is not 32 for both parts
continue;
}
$r = substr($signature,4,$rlen);
$s = substr($signature,6+$rlen,$slen);
$signature = $r.$s;
break;
}
openssl_free_key($prkey);
$signature = bin2hex($signature);
echo $signature."\n";
Golang
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"math/big"
"crypto/x509"
"encoding/pem"
)
func main() {
stringtosign := "my test string to sign"
// This is outpur of PHP app. Signature generated by PHP openssl_sign
signature := "18d5c1d044a4a752ad91bc06499c72a590b2842b3d3b4c4b1086bfd0eea3e7eb5c06b77e15542e5ba944f3a1a613c24eabaefa4e2b2251bd8c9355bba4d14640"
// NOTE . Error verificaion is skipped here
// Privcate key was geerated with openssl tool with the command
// openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-key.pem
prikeybytes, _ := ioutil.ReadFile("prime256v1-key.pem")
p, _ := pem.Decode(prikeybytes)
prikey, _ := x509.ParseECPrivateKey(p.Bytes)
signatureBytes, _ := hex.DecodeString(signature)
// make MD5 hash
h := md5.New()
io.WriteString(h, stringtosign)
data := h.Sum(nil)
// build key and verify data
r := big.Int{}
s := big.Int{}
// make signature numbers
sigLen := len(signatureBytes)
r.SetBytes(signatureBytes[:(sigLen / 2)])
s.SetBytes(signatureBytes[(sigLen / 2):])
curve := elliptic.P256()
// make public key from private key
x := big.Int{}
y := big.Int{}
x.SetBytes(prikey.PublicKey.X.Bytes())
y.SetBytes(prikey.PublicKey.Y.Bytes())
rawPubKey := ecdsa.PublicKey{Curve: curve, X: &x, Y: &y}
v := ecdsa.Verify(&rawPubKey, data, &r, &s)
if v {
fmt.Println("Success verify!")
return
}
fmt.Println(fmt.Sprintf("Signatire doed not match"))
}
What do i do wrong? Can anyone show me working example where Golang verifies signatre created with PHP?
I tried to use different versions in openssl_sign instead of OPENSSL_ALGO_SHA256 . Tried OPENSSL_ALGO_SHA1, OPENSSL_ALGO_SHA512
The problem with your code seems to be, that you hash the string in PHP using MD5 before signing it using OPENSSL_ALGO_SHA256, which hashes what you sign (the MD5 hash) again, while in your Go program, you only have the first of these 2 hashes. To fix this, I would remove the MD5 step in the PHP code and replace the h := md5.New() line in your code with the hash used by your signature algorithm (h := sha256.New() in your example).
To elaborate a bit more on what theses signing functions do, I would first like to break signing and verifying down into the following steps:
Signing:
Hash the message
Encrypt the message's hash using the private key (this encrypted hash is the signature)
Verifying:
Hash the message
Decrypt the signature using the public key (this yields the hash which was encrypted while signing).
Compare the calculated and decrypted hashes. If they match, then the signature is correct.
Now the call to openssl_sign in your PHP code, does all the signing steps, while the call to ecdsa.Verify in Go, only does the second and third step of the verification process. And this is why it takes a hash as the second argument. So to verify a signature, you must implement the first verification step yourself, namely generating the hash.
You must use the same hashing algorithm while signing and verifying, therefore you must use SHA256, not MD5, in your Go code (as you sign using OPENSSL_ALGO_SHA256), otherwise the hashes will (generally) not match.
Also, I would recommend to not use MD5 for signatures, as it is no longer considered collision resistant (a hash collision is, when you have 2 different strings/files/... with the same hash). For more details about that, you can check the Wikipedia article on MD5, specifically the section "Collision vulnerabilities". This is a problem, as 2 messages with the same MD5 hash, will also have the same signature and an attacker could use the signature generated for one of the strings to trick you into thinking the other was signed (and therefore trust it).
Additionally, ecdsa.PrivateKey can give you the corresponding public key, and you can call ecdsa.Verify like this:
ecdsa.Verify(&prikey.PublicKey, data, &r, &s)
This saves you the trouble of copying all the data from the private key to a new object.
Related
In the frontend I'm using PHP (Symfony). In a PHP form the password is stored in the database, there I want to encrypt it. That same password is used in the backend for an API that in programmed in Go, there I need to decrypt it.
I like to use AES encryption, but if something better is proposed than that is also fine.
In backend I'm using the following GO function to decrypt it:
func DecryptAES(key []byte, ct string) string {
ciphertext, _ := hex.DecodeString(ct)
c, _ := aes.NewCipher(key)
pt := make([]byte, len(ciphertext))
c.Decrypt(pt, ciphertext)
s := string(pt[:])
return s
}
I use for example the following input:
// cipher key
key := "1234567890123456"
// password
pt := "-This is secret-" // needs to be 16 chars
To decrypt with the key above I need to following Ciphertext: 55c6ca0c634fb5cc7d7a287d30285f09
How can I generate this Ciphertext with PHP?
I did try the following, but the outcome if different:
$ct = openssl_encrypt('-This is secret-', 'AES-128-ECB', "1234567890123456");
I am trying to send a data to an API. They wants it as encrypted before send it over.
The documentation says:
Generate a private key using AES algorithm(with SHA-1 HMac function by PKCS 5 V2.0 Scheme - I DON'T know what this means.
Encrypt the data with this key (data is json_string) (PADDING_PKCS1 = 11 and key length is 2048bit)
Encrypt the private_key using RSA algorithm with the public key(they gave us a pem file)
Then send this encrypted datas in a json format.
They also gives a dll file. So I decompiled it, and it uses a library called BouncyCastle.
Their function is basicly does this;
this.aesService.GenerateKey(password_variable, salt_file_contents_as_bytes); (they gives that both)
byte[] data1 = this.aesService.Encode(json_string);
byte[] data2 = this.rsaService.Encode(this.aesService.GetAesKey(), reads_public_keys_data_as_byte);
// and some http requests nothing special
What I tried is;
1- generated a key using openssl_pbkdf2 function with their password and salt
$key = openssl_pbkdf2($password, $salt, 32, 20000, 'sha1');
2- encrypted the data with this key with openssl_encrypt function
$encryptedJson = openssl_encrypt($json_string, "AES256", $key);
3- encrypted the key that I generate with their public key with openssl_public_encrypt function
openssl_public_encrypt($key, $encryptedAes, $public_key, OPENSSL_PKCS1_PADDING);
4- convert this results to hex with bin2hex function and send it to them
$jsonData = bin2hex($encryptedJson);
$keyData = bin2hex($encryptedAes);
But it returns error (says invalid object -I don't know what this means). I asked them about it but they didn't reply yet.
What I want to ask you is:
Am I doing it right? Am I using the right functions for this operations?
EDIT:
They still didn't reply back. But I just added the IV to the the start of the encrypted data returned by the openssl_encrypt function and convert it to hex. Voilà! It worked.
I am trying to do the equivalent of the php seal function.
What I have is a string to encrypt, a public key and a randomly generated secret key, and I have to encode the string using the 'rs4' algorithm.
So far I managed to encode the string with the crypto functions:
var password = crypto.randomBytes(128);
var cipher = crypto.createCipher('rc4', password);
var crypted = cipher.update(text,'utf8','base64');
crypted += cipher.final('base64');
But somehow, I need to include in the encryption the public x509 certificate.
Can someone point me in the right direction?
RC4 is not a public key encryption system. You're looking for (in order of preference):
node-sodium
RSAES-OAEP with MGF1+SHA256 and e = 65537
Also, for secret-key cryptography, don't use RC4.
In the end I didn't find any way of achieving the same encryption in node, so I just called a php cli script from node that sent the data to be encrypted, and read the stdout for the encrypted base64 result :(
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?
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.
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.
I am required to send a query to the bank which contains a verification code $vk_mac in a specified string format. The code has to be a SHA1 hash and RSA encrypted with my public key and presented in base64 format. Unfortunately, so far, I have been unsuccessful - the bank gives me "Wrong signature" and that all the info I'm getting.
What I have is this:
$rsa = new Crypt_RSA();
$rsa->loadKey(file_get_contents("private_key.pem"));
$rsa->loadKey($rsa->getPublicKey());
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
$encrypted = $rsa->encrypt(sha1($vk_mac));
$vk_mac = base64_encode($encrypted);
private_key.pem here is my private key in plain text. I tried setting the encryption mode to CRYPT_RSA_ENCRYPTION_OAEP without luck. I am 99.9% sure, that the starting $vk_mac string is formatted correctly and contains all the required details.
Does anybody have any idea what can I be doing wrong? Thank you.
Edit:
I've changed the code to this (where vk_mac is the starting formatted string that needs to be signed and private_key.pem is my the decoded private key):
$rsa = new Crypt_RSA();
$rsa->loadKey(file_get_contents("private_key.pem"));
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
$hashed = $rsa->hash->hash($vk_mac);
$encrypted = $rsa->sign($hashed);
$signature = base64_encode($encrypted);
I can tell the generated signature is correct, since when I do this:
$rsa->loadKey($rsa->getPublicKey());
$verified = $rsa->verify($hashed, base64_decode($signature));
$verified returns TRUE.
The bank though, responds "Incorrect signature". Any more ideas?
Edit:
Specification
VK_MAC control code calculation
VK_MAC, for electronic signature, using in the request, for checking and confirming used version of the algorithm, indicated in the parameter VK_VERSION. In this time version 008 is used. VK_MAC is presented as a request parameter in BASE64 coding.
Version 008
The value of the MAC008 function is calculated using the public key algorithm RSA. Values of empty fields are taken into account as well – “000”.
MAC008(x1,x2,…,xn) := RSA(SHA-1(p(x1)|| x1|| p(x2 )|| x2 || … ||p(xn)||xn),d,n)
Where:
|| is an operation of adding the string
x1, x2, …, xn are the query parameters
p is a function of the parameter length. The length is a number in the form of a three-digit string
d is the RSA secret exponent
n is the RSA modulus
The signature is calculated in accordance with the PKCS1 standard (RFC 2437).
What if you try $rsa->sign()? PKCS#1 doesn't do signing by simply encrypting the hash and if your bank is using an interoperable RSA solution they're probably not doing that either.
The code was almost correct - I did not need to hash it again though (thanks #Accipitridae).
The solution was that the merchant's ID had to be uppercase, and not lowercase as provided. It does not say anywhere in the spec that it has to be uppercase as well. Nice.
As mentioned above you can do this easily with openssl. Below is how I would do so.
$hashed = sha1($vk_mac);
openssl_public_encrypt($vk_mac, $encrypted, ($pubkey));
$vk_mac = base6$_encode($encrypted);
Read the documentation on openssl_public_encrypt for more.