我有一个简单的文本字符串,需要在JavaScript中签名并在服务器(PHP)上验证.为了开始我的测试,我首先创建了一个密钥对:

// Function to generate a new RSA key pair
async function generateKeyPair() {
    const keyPair = await window.crypto.subtle.generateKey(
        {
            name: "RSASSA-PKCS1-v1_5",
            modulusLength: 2048,
            publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
            hash: { name: "SHA-256" },
        },
        true,
        ["sign", "verify"]
    );

    const publicKey = await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
    const privateKey = await window.crypto.subtle.exportKey("pkcs8", keyPair.privateKey);

    return {
        privateKey: privateKey,
        publicKey: publicKey,
    };
}

我获得了私钥和公钥:

privateKey: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDw4Ees4C+vsTUQodgWKIsj3Ni67RG3ny9xY1KjCaatu2o/ev5hS4yrxxWLZAFU9mt/rfNmzzby3mqlWPWm8Df91Mue6wNTsN2yMnHw+XvcvovCngSTH4H2zY+uAhEiG+u+vzGqbzxm0JB3ybX5kYEMK2iKoALq1ASJ781gyy7AsCf/Ck+OvIE4in1kNm4a5NUgbyuflWerMIB7FUQ7h/+XlLn3F2bvC1SWWxKsmQ/dF5fYpZaAV2KvVw2LMnkWdU536an9vxj5LZIJyzfNQv/foNGcUh8iT1tLe8jV4eYrAcqiLnG+iZFFc3X5F33WUILmRvCg11bec6ic1NwuY8eDAgMBAAECggEAQq7kSNCbgvj85sjXSHMa6ee2vDDrKblQ6gQEGYyPbyMmK8LB72951wg7R6Z80+eQJP2kF38gCCZYwcOZ5gg0h/nEEQ+gkSeyiCV886gtiRPbHxqdy5j6YrfPoe2Cjr3KCrllZ3h58UCl7fOShC+q2RKfU1ku1ZGyW/leEwDMxZy1PISHFHtmd43LdrkWgyNk4TIpNRzizx+gxNeyQUEZDfkUu4mFP/weWM26lLyaE+RkPqvFnLjXckvgno1bY8Hq2yywVkyvfBo0tQvVBtNP5WTEyOvNGylc46pnVBODrSUn5q4ZNdi56fd7WFPBFySAVQiA0uLgaWYOuBUGMyc5CQKBgQD+wcyYL2IgA5O2c6Jp9cJV9xQVvWEQ/sVUDJgRVhOxAqw9r2LmUe7tRnWYEI4Sz9g/ejFq8fkL+h2lQbGB+ZKlu5EVHrmfuYf3zo80QKMWC1XKbYnw0HKkMlOqxiMYyu6PqFX59icmcZ58k1m9h2br5f7GGGWAFY8yFgRUIUR6FwKBgQDyDSSbH7WoqUUhYNvY9wKUUYM8uZSAC1TPfuR/ZvAec3cZxMJyOnY88MPOh63vUMzTUt6AAyps2EFPa0UGuysevMaXSL+MAQQzDfnEC2KfeRqkVOKYPrjjjxIl5mQJCacpB7rdzLszmtJJ9G99lTqeGuVa3mhlJupqckYbbdO9dQKBgQD9zf4TMEHGO0oSX6nTfvCZzIrKDd6CnA/j6JgnzWXY2BzZZ75UUBSFd8j4MqYYv9FljEtnjKLd99VJKuW54/bh/rhQHkg4hRKdI8EwAaV49NoHzpG6xTExvKH2ZWfZ73M01DSzzzS57EBFRFgHpro3EvB8UxnsPY5oC99MIcijCQKBgBn16OguVXh6dyymS84QaBlqSK4ZpWC6VmVO0ckMTFKnxa1g2g4QUSAmHoonKTOSsfU0XSLTtBgqdY7EDYo0RuKsEoylQ84LSd0D8bbiFbjO71mStR7pE0Fs1eB0vmPtwhz3dEZXr/hP8Z/29II+oCPW9KRzWDUJIHk8OmK0u9IFAoGAEOWhm/zaXMNJ+oBcvBbCKTZ0XInzvV4SqhC6Bj9aC8wqCe5QKyKl9HglG9J+o3D+hIEcMXGvIv1KB3xDStCQQKcDOrD/8tGZtstSONaNzeGg0hUY9SKd7R2wMPEWufzccFE+zVG5hHUg+eQrnzXdXkG8hW1QxQxgoDkC3DNVsnE="
publicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8OBHrOAvr7E1EKHYFiiLI9zYuu0Rt58vcWNSowmmrbtqP3r+YUuMq8cVi2QBVPZrf63zZs828t5qpVj1pvA3/dTLnusDU7DdsjJx8Pl73L6Lwp4Ekx+B9s2PrgIRIhvrvr8xqm88ZtCQd8m1+ZGBDCtoiqAC6tQEie/NYMsuwLAn/wpPjryBOIp9ZDZuGuTVIG8rn5VnqzCAexVEO4f/l5S59xdm7wtUllsSrJkP3ReX2KWWgFdir1cNizJ5FnVOd+mp/b8Y+S2SCcs3zUL/36DRnFIfIk9bS3vI1eHmKwHKoi5xvomRRXN1+Rd91lCC5kbwoNdW3nOonNTcLmPHgwIDAQAB"

这是我用来使用私有密钥对数据(字符串)进行签名的脚本:

// Function to sign the data
async function signRequestData(data, privateKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);

    const signatureBuffer = await window.crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        hashBuffer
    );

    const signatureArray = new Uint8Array(signatureBuffer);
    const signatureBase64 = btoa(String.fromCharCode.apply(null, signatureArray));

    return signatureBase64;
}

然后,我创建了一个脚本来验证数据,该数据仍然是使用Java脚本的,以验证我正在做的事情:

// Function to verify the signature
async function verifySignature(data, signatureBase64, publicKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const signatureArray = new Uint8Array(atob(signatureBase64).split("").map((c) => c.charCodeAt(0)));
    const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
    const isSignatureValid = await window.crypto.subtle.verify(
        { name: "RSASSA-PKCS1-v1_5" },
        publicKey,
        signatureArray,
        hashBuffer
    );

    return isSignatureValid;
}

脚本返回true,因此我继续下一步,验证PHP中的数据:

    // Function to verify the signature
    public function verifySignature($data, $signatureBase64, $publicKey) {      
        // Import public key
        $publicKeyResource = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----" . "\n" . $publicKey . "\n" . "-----END PUBLIC KEY-----");

        if ($publicKeyResource === false) {
            // Handle error (unable to import public key)
            die("Error importing public key");
        }

        // Verify the signature
        $isSignatureValid = openssl_verify($data, $signatureBase64, $publicKeyResource, OPENSSL_ALGO_SHA256);

        // Free the public key resource
        openssl_free_key($publicKeyResource);

        return ($isSignatureValid === 1)
    }   

此脚本永远不会返回1,表示"有效".我不确定问题是否在于密钥是如何生成的.如果有帮助,我可以在PHP中生成这对代码.@Topaco这就是我所说的全部问题.

edit:我在0的情况下加了return openssl_error_string();,结果是:第二次我得了error:0480006C:PEM routines::no start line

推荐答案

在JavaScript代码中,散列被执行twice,一次explicitly是利用digest()函数,一次implicitly是利用sign()函数.由于双重散列在签名和验证期间都在JavaScript侧执行,因此验证在JavaScript侧工作.

相反,PHP代码散列once,即openssl_verify()中的implicitly,因此作为该不同散列策略的结果,两个代码是不兼容的.因此,使用PHP代码进行验证失败.要消除这种不兼容性,必须一致地执行散列.

因为双重散列是不必要的,所以应该适配JAVASCRIPT端,并且移除双重散列,即在JavaScript代码dataBuffer中,应该将directly传递到sign()verify().

除了这个散列问题,正如在另一个答案中已经提到的,JavaScript代码中的Base64编码签名必须在PHP代码中进行Base64解码.


已修复的JavaScript代码(仅使用单一散列)为:

(async () => {

var pkcs8DerB64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDw4Ees4C+vsTUQodgWKIsj3Ni67RG3ny9xY1KjCaatu2o/ev5hS4yrxxWLZAFU9mt/rfNmzzby3mqlWPWm8Df91Mue6wNTsN2yMnHw+XvcvovCngSTH4H2zY+uAhEiG+u+vzGqbzxm0JB3ybX5kYEMK2iKoALq1ASJ781gyy7AsCf/Ck+OvIE4in1kNm4a5NUgbyuflWerMIB7FUQ7h/+XlLn3F2bvC1SWWxKsmQ/dF5fYpZaAV2KvVw2LMnkWdU536an9vxj5LZIJyzfNQv/foNGcUh8iT1tLe8jV4eYrAcqiLnG+iZFFc3X5F33WUILmRvCg11bec6ic1NwuY8eDAgMBAAECggEAQq7kSNCbgvj85sjXSHMa6ee2vDDrKblQ6gQEGYyPbyMmK8LB72951wg7R6Z80+eQJP2kF38gCCZYwcOZ5gg0h/nEEQ+gkSeyiCV886gtiRPbHxqdy5j6YrfPoe2Cjr3KCrllZ3h58UCl7fOShC+q2RKfU1ku1ZGyW/leEwDMxZy1PISHFHtmd43LdrkWgyNk4TIpNRzizx+gxNeyQUEZDfkUu4mFP/weWM26lLyaE+RkPqvFnLjXckvgno1bY8Hq2yywVkyvfBo0tQvVBtNP5WTEyOvNGylc46pnVBODrSUn5q4ZNdi56fd7WFPBFySAVQiA0uLgaWYOuBUGMyc5CQKBgQD+wcyYL2IgA5O2c6Jp9cJV9xQVvWEQ/sVUDJgRVhOxAqw9r2LmUe7tRnWYEI4Sz9g/ejFq8fkL+h2lQbGB+ZKlu5EVHrmfuYf3zo80QKMWC1XKbYnw0HKkMlOqxiMYyu6PqFX59icmcZ58k1m9h2br5f7GGGWAFY8yFgRUIUR6FwKBgQDyDSSbH7WoqUUhYNvY9wKUUYM8uZSAC1TPfuR/ZvAec3cZxMJyOnY88MPOh63vUMzTUt6AAyps2EFPa0UGuysevMaXSL+MAQQzDfnEC2KfeRqkVOKYPrjjjxIl5mQJCacpB7rdzLszmtJJ9G99lTqeGuVa3mhlJupqckYbbdO9dQKBgQD9zf4TMEHGO0oSX6nTfvCZzIrKDd6CnA/j6JgnzWXY2BzZZ75UUBSFd8j4MqYYv9FljEtnjKLd99VJKuW54/bh/rhQHkg4hRKdI8EwAaV49NoHzpG6xTExvKH2ZWfZ73M01DSzzzS57EBFRFgHpro3EvB8UxnsPY5oC99MIcijCQKBgBn16OguVXh6dyymS84QaBlqSK4ZpWC6VmVO0ckMTFKnxa1g2g4QUSAmHoonKTOSsfU0XSLTtBgqdY7EDYo0RuKsEoylQ84LSd0D8bbiFbjO71mStR7pE0Fs1eB0vmPtwhz3dEZXr/hP8Z/29II+oCPW9KRzWDUJIHk8OmK0u9IFAoGAEOWhm/zaXMNJ+oBcvBbCKTZ0XInzvV4SqhC6Bj9aC8wqCe5QKyKl9HglG9J+o3D+hIEcMXGvIv1KB3xDStCQQKcDOrD/8tGZtstSONaNzeGg0hUY9SKd7R2wMPEWufzccFE+zVG5hHUg+eQrnzXdXkG8hW1QxQxgoDkC3DNVsnE=";
var pkcs8Der = Uint8Array.from(window.atob(pkcs8DerB64), c => c.charCodeAt(0));

var data = "The quick brown fox jumps over the lazy dog";
var privateKey = await window.crypto.subtle.importKey("pkcs8", pkcs8Der, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"]);
var signature = await signRequestData(data, privateKey);
console.log(signature);

// Function to sign the data
async function signRequestData(data, privateKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const signatureBuffer = await window.crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        dataBuffer  // Fix: apply the unhashed data
    );
    const signatureArray = new Uint8Array(signatureBuffer);
    const signatureBase64 = btoa(String.fromCharCode.apply(null, signatureArray));
    return signatureBase64;
}

})();

对于您指定的私钥和消息The quick brown fox jumps over the lazy dog,将产生以下Base64编码的签名:

Gd8BrZtcq54CZY6gwmvZpoazHzJiEQ8xOd6hNIHLC7o9NscZDyJ3XjFgpUG3WKZ6uBuuJpPl3GNS++VDcQqV3cBGh3mS6WNnQehnO6JnnDxvFb4FF8xJzvD87g9m2xHgS3XFnNtE+zNS0sKRPKgAhQY/T6FCYWDX0yWAGTxuuDd2kUB4XWQhQmH5/iMIsF+gpbwxUegtaj8R1fkL++np3cSGLQ9lMbDSPY7h8Fq1d98fSVQZ1ludKJpGY42l1U9z4Vg3xU5rP0wtSzcYhQUit+ZCtKINhU8RbZxkwMUVFpEoVONeRCYfBpMZB6VNYp0hHX8qqZLbki3QDdey52rQ7g==

PHP代码保持不变,除了对签名进行Base64解码:

<?php
// Function to verify the signature
function verifySignature($data, $signatureBase64, $publicKey) {      
    // Import public key
    $publicKeyResource = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----" . "\n" . $publicKey . "\n" . "-----END PUBLIC KEY-----");

    if ($publicKeyResource === false) {
        // Handle error (unable to import public key)
        die("Error importing public key");
    }

    // Verify the signature
    $isSignatureValid = openssl_verify($data, base64_decode($signatureBase64), $publicKeyResource, OPENSSL_ALGO_SHA256); // Fix: Base64 decode the signature

    // Free the public key resource
    openssl_free_key($publicKeyResource);

    return ($isSignatureValid === 1);
}   
    
$publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8OBHrOAvr7E1EKHYFiiLI9zYuu0Rt58vcWNSowmmrbtqP3r+YUuMq8cVi2QBVPZrf63zZs828t5qpVj1pvA3/dTLnusDU7DdsjJx8Pl73L6Lwp4Ekx+B9s2PrgIRIhvrvr8xqm88ZtCQd8m1+ZGBDCtoiqAC6tQEie/NYMsuwLAn/wpPjryBOIp9ZDZuGuTVIG8rn5VnqzCAexVEO4f/l5S59xdm7wtUllsSrJkP3ReX2KWWgFdir1cNizJ5FnVOd+mp/b8Y+S2SCcs3zUL/36DRnFIfIk9bS3vI1eHmKwHKoi5xvomRRXN1+Rd91lCC5kbwoNdW3nOonNTcLmPHgwIDAQAB";
$signatureBase64 = "Gd8BrZtcq54CZY6gwmvZpoazHzJiEQ8xOd6hNIHLC7o9NscZDyJ3XjFgpUG3WKZ6uBuuJpPl3GNS++VDcQqV3cBGh3mS6WNnQehnO6JnnDxvFb4FF8xJzvD87g9m2xHgS3XFnNtE+zNS0sKRPKgAhQY/T6FCYWDX0yWAGTxuuDd2kUB4XWQhQmH5/iMIsF+gpbwxUegtaj8R1fkL++np3cSGLQ9lMbDSPY7h8Fq1d98fSVQZ1ludKJpGY42l1U9z4Vg3xU5rP0wtSzcYhQUit+ZCtKINhU8RbZxkwMUVFpEoVONeRCYfBpMZB6VNYp0hHX8qqZLbki3QDdey52rQ7g==";
$data = "The quick brown fox jumps over the lazy dog";
print(verifySignature($data, $signatureBase64, $publicKey));
?>

有了这些更改,现在可以成功地使用PHP代码进行验证.


出于完整性的考虑:如果引用的是JavaScript代码,并且要保留双重散列,则PHP代码还必须进行两次散列,例如,用hash('sha256', $data, true)替换$data.

Javascript相关问答推荐

对象和数字减法会抵消浏览器js中的数字

过滤对象数组并动态将属性放入新数组

为什么这个JS模块在TypeScript中使用默认属性导入?""

用JavaScript复制C#CRC 32生成器

如何根据当前打开的BottomTab Screeb动态加载React组件?

如何解决useState错误—setSelect Image不是函数''

在forEach循环中获取目标而不是父对象的属性

连接到游戏的玩家不会在浏览器在线游戏中呈现

400 bad request error posting through node-fetch

Angular 形式,从DOM中删除不会删除指定索引处的内容,但会删除最后一项

如何在 Select 文本时停止Click事件?

TypeError:无法读取未定义的属性(正在读取';宽度';)

打字脚本中方括号符号属性访问和拾取实用程序的区别

将相关数据组合到两个不同的数组中

未找到用于 Select 器的元素:in( puppeteer 师错误)

调用特定数组索引时,为什么类型脚本不判断未定义

JavaScript:多个图像错误处理程序

如何将值从后端传递到前端

如何从Reaction-Redux中来自API调用的数据中筛选值

在JS/TS中构造RSA公钥