phpseclib 未验证 window.subtlecrypto 中生成的签名

phpseclib not verifying a signature generated in window.subtlecrypto

所以是的,我的老板想在他的系统中加密,他希望消息在 js 中签名并在 php 中验证。目前,我正在使用 mozilla 的 subtlecrypto api 生成 RSA-PSS 密钥并签名,并使用 phpseclib 进行验证。事实是,事实并非如此。

使用js密钥,phpseclib可以很好地进行签名验证,但是无法处理js签名。

这是我的代码。 JS:

    function keys(){
        var cryptoObj = window.crypto || window.msCrypto;
        let msg = '///';
        if(!cryptoObj)
        {
            alert("Crypto API is not supported by the Browser");
            return;
        }

        window.crypto.subtle.generateKey({
            name: "RSA-PSS",
            modulusLength: 2048, //can be 1024, 2048, or 4096
            publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
            hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
        },
        true, //whether the key is extractable (i.e. can be used in exportKey)
        ["sign", "verify"] //can be any combination of "sign" and "verify"
    )
        .then(function(key) {
            publicKey = key.publicKey;
            privateKey = key.privateKey;
            // For Demo Purpos Only Exported in JWK format
            if (document.getElementById('public').value == "") {
                window.crypto.subtle.exportKey("spki", key.publicKey).then(
                function (keydata) {
                    publicKeyhold = keydata;
                    let exported = publicKeyhold;
                    const exportedAsString = ab2str(exported);
                    const exportedAsBase64 = window.btoa(exportedAsString);
                    const pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;
                    document.getElementById('public').value = pemExported;
                }
            );
            }

            if (document.getElementById('private').value == "" ) {
                msg = document.getElementById('msg').value;
                window.crypto.subtle.exportKey("pkcs8", key.privateKey).then(
                function (keydata) {
                    privateKeyhold = keydata;
                    const priv = privateKeyhold;
                    const privExportedAsString = ab2str(priv);
                    const privExportedAsBase64 = window.btoa(privExportedAsString);
                    const privPemExported = `-----BEGIN RSA PRIVATE KEY-----\n${privExportedAsBase64}\n-----END RSA PRIVATE KEY-----`;
                    document.getElementById('privJSON').data = key.privateKey;
                    document.getElementById('private').value = privPemExported;
                }
            );
        }})
            window.crypto.subtle.sign({
                    name: "RSA-PSS",
                    saltLength: 128, //the length of the salt
                },
                //from generateKey or importKey above
                document.getElementById('privJSON').data,
                getMessageEncoding())
    //ArrayBuffer of data you want to sign
            .then(function(signature) {
                    //returns an ArrayBuffer containing the signature
                    console.dir(ab2str(signature));
                    document.getElementById("cryptmsg").value = window.btoa(ab2str(signature)) ;
                })


}
function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
}


function asciiToUint8Array(str) {
    var chars = [];
    for (var i = 0; i < str.length; ++i)
        chars.push(str.charCodeAt(i));
    return new Uint8Array(chars);
}

function bytesToHexString(bytes) {
    if (!bytes)
        return null;

    bytes = new Uint8Array(bytes);
    var hexBytes = [];

    for (var i = 0; i < bytes.length; ++i) {
        var byteString = bytes[i].toString(16);
        if (byteString.length < 2)
            byteString = "0" + byteString;
        hexBytes.push(byteString);
    }

    return hexBytes.join("");
}
function getMessageEncoding() {
    const messageBox = document.getElementById('msg');
    let message = messageBox.value;
    let enc = new TextEncoder();
    return enc.encode(message);
}

php:

<?php
include('Crypt/RSA.php');
define('CRYPT_RSA_PKCS15_COMPAT', true);

$pubK = $_POST['public'];
$privK = $_POST['private'];
echo $pubK."<br><br>";
echo $privK."<br><br>";

$sign = ($_POST['cryptmsg']);
$txt = $_POST['msg'];

$rsa = new Crypt_RSA();

$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PSS);
$rsa->setSaltLength(128);
$rsa->setMGFHash("SHA256");
$rsa->setHash("SHA256");

$rsa->loadKey($privK);
$serverSign = $rsa->sign($txt);

$rsa->loadKey($pubK);
echo "text: ".$txt;
echo "<br><br>js sign: <br>".($sign)."<br><br>";
echo "seclib sign: <br>".base64_encode($serverSign);
echo "<br><br>";

$jsTest = $rsa->verify($txt, base64_decode($sign)) ? 'verified' : 'unverified';
$selfTest = $rsa->verify($txt, $serverSign) ? 'verified' : 'unverified';

echo 'js signature: '. $jsTest;
echo'<br> phpseclib signature (same keys): '. $selfTest;

在 PHP 代码中,摘要必须用 sha256 指定,即用小写字母代替大写字母:

$rsa->setMGFHash("sha256");
$rsa->setHash("sha256");

phpseclib 不知道大写名称,因此使用默认摘要 sha1 而不是 [1]。这可以通过输出与 $rsa->hashName 一起使用的摘要名称来轻松测试。

该错误并不那么容易找到,如果 phpseclib 显示错误消息或警告可能比默默地使用默认值更好。

附带说明:在 JavaScript 代码中,私钥以 PKCS8 格式导出,但使用 PKCS1 格式的页眉和页脚,。但是,phpseclib 容忍这种不一致。