使用PHP创建ECDSA签名并用Golang进行验证

7

我尝试使用PHP制作一个应用程序,为某些文档创建ECDSA签名,并且该签名可以由Golang应用程序验证。

我使用openssl工具生成的私钥。这是prime256v1曲线密钥。使用以下命令创建:

openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-key.pem

我使用openssl_sign函数在PHP中创建签名。

然而,我在Golang中使用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";

Go语言

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"))

}

我做错了什么?有人能给我展示一个验证使用PHP创建的签名的工作示例吗?

我尝试在openssl_sign中使用不同版本而不是OPENSSL_ALGO_SHA256。尝试过OPENSSL_ALGO_SHA1,OPENSSL_ALGO_SHA512。

1个回答

2
你的代码问题似乎在于,在使用OPENSSL_ALGO_SHA256签名之前,你使用MD5在PHP中哈希了字符串,而在你的Go程序中,你只有这两个哈希中的第一个。为解决该问题,建议将PHP代码中的MD5步骤移除,并将代码中的h := md5.New()替换为签名算法所需的哈希(例如你的示例中的h := sha256.New())。
更进一步地说明,签名和验证可以分解为以下步骤:
签名:
1. 哈希消息 2. 使用私钥加密消息的哈希(这个加密后的哈希就是签名)
验证:
1. 哈希消息 2. 使用公钥解密签名(会得到被加密的哈希) 3. 将计算出的哈希与解密后的哈希进行比较,如果相同,则签名正确
在你的PHP代码中,调用openssl_sign函数执行所有签名步骤,而在Go中调用ecdsa.Verify仅执行验证过程的第二步和第三步。因此,它需要一个哈希作为第二个参数。为了验证签名,你必须自己实现第一个验证步骤,即生成哈希。
在签名和验证过程中必须使用相同的哈希算法,因此在Go代码中必须使用SHA256而不是MD5(因为你使用OPENSSL_ALGO_SHA256进行签名),否则哈希通常不匹配。另外,建议不要使用MD5进行签名,因为它已不再被认为是抗碰撞的(哈希冲突是指具有相同哈希的两个不同的字符串/文件)。关于这方面的更多细节,可以查看MD5的Wikipedia文章,特别是“碰撞漏洞”部分。如果两个消息具有相同的MD5哈希,则它们将具有相同的签名,并且攻击者可以使用为其中一个字符串生成的签名来欺骗你认为另一个字符串也是签名的(从而信任它)。
此外,ecdsa.PrivateKey可以提供相应的公钥,可以像这样调用ecdsa.Verify
ecdsa.Verify(&prikey.PublicKey, data, &r, &s)

这样可以避免您将私钥的所有数据复制到一个新对象中的麻烦。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接