Я пытаюсь создать приложение с PHP, которое создает подпись ECDSA для некоторого документа, и эта подпись проверяется приложением Golang.
Я использую закрытые ключи, сгенерированные с помощью инструмента openssl. Это простой ключ кривой 256v1. Создано с помощью команды:
openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-key.pem
В PHP я создаю подпись, используя функцию openssl_sign.
И все мои попытки проверить подпись с Голангом провалились. В Голанге используются пакеты crypto / ecdsa, crypto / elliptic.
Вот мой код.
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"))
}
Что я делаю не так? Может кто-нибудь показать мне рабочий пример, где Golang проверяет подпись, созданную с помощью PHP?
Я попытался использовать разные версии в openssl_sign вместо OPENSSL_ALGO_SHA256. Пробовал OPENSSL_ALGO_SHA1, OPENSSL_ALGO_SHA512
Проблема с вашим кодом заключается в том, что вы хэшируете строку в PHP, используя MD5, прежде чем подписывать ее, используя OPENSSL_ALGO_SHA256
, который снова хэширует то, что вы подписываете (хэш MD5), в то время как в вашей программе Go у вас есть только первый из этих 2 хэшей. Чтобы это исправить, я бы удалил MD5
шаг в коде PHP и заменить h := md5.New()
строка в вашем коде с хешем, используемым вашим алгоритмом подписи (h := sha256.New()
в твоем примере).
Чтобы более подробно рассказать о том, что делают функции подписи этих тезисов, я сначала хотел бы разбить подписывание и проверку на следующие шаги:
Теперь вызов openssl_sign
в вашем PHP-коде выполняет все шаги подписи, в то время как вызов ecdsa.Verify
в Go, только делает второй и третий шаг процесса проверки. И именно поэтому он принимает хеш в качестве второго аргумента. Таким образом, чтобы проверить подпись, вы должны самостоятельно выполнить первый шаг проверки, а именно, создать хеш.
Вы должны использовать один и тот же алгоритм хеширования при подписании и проверке, поэтому вы должны использовать SHA256, а не MD5, в своем коде Go (когда вы подписываете, используя OPENSSL_ALGO_SHA256
), иначе хеши будут (вообще) не совпадать.
Кроме того, я бы рекомендовал не использовать MD5 для подписей, так как он больше не считается устойчивым к коллизиям (коллизия хэшей возникает, когда у вас есть 2 разные строки / файлы / … с одинаковым хешем). Для получения более подробной информации об этом, вы можете проверить Статья в Википедии о MD5, в частности, раздел «Уязвимости при столкновениях». Это проблема, поскольку 2 сообщения с одинаковым хешем MD5 также будут иметь одинаковую подпись, и злоумышленник может использовать подпись, сгенерированную для одной из строк, чтобы заставить вас думать, что другая подписана (и, следовательно, доверять ей).
Дополнительно, ecdsa.PrivateKey
может дать вам соответствующий открытый ключ, и вы можете позвонить ecdsa.Verify
как это:
ecdsa.Verify(&prikey.PublicKey, data, &r, &s)
Это избавляет вас от необходимости копировать все данные из закрытого ключа в новый объект.
Других решений пока нет …