Java签名/验证密钥和Javascript WebCrypto验证失败

4
我正在尝试:
  1. 生成签名/验证密钥(RSA)
  2. 在Java Web应用程序上使用这些密钥对一个值进行签名(我们称之为服务器端)
  3. 以便Web客户端验证-公钥导入为RSASSA-PKCS1-v1_5+SHA-256,(在浏览器中使用WebCrypto API/客户端)
虽然在客户端成功地将公共签名/验证密钥作为JWK导入,但我在验证由Java服务器端签署的值时遇到了问题。
我想知道是否在任何步骤(OpenSSL、Java或JavaScript)中存在任何算法兼容性问题。以下是用于生成密钥的OpenSSL命令:
openssl genrsa -out privatekey.pem 2048
openssl rsa -in privatekey.pem -pubout > publickey.pub
openssl pkcs8 -topk8 -inform PEM -outform DER -in privatekey.pem -out privatekey-pkcs8.pem

使用Java(服务器端)导入密钥

public static KeyPair generateSignKeyPair() throws ... {
    byte[] privBytes = b64ToByteArray(PRIVATE_KEY_PEM_VALUE);
    byte[] pubBytes = b64ToByteArray(PUBLIC_KEY_PEM_VALUE);

    // private key
    KeySpec keySpec = new PKCS8EncodedKeySpec(privBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

    // public key (javaPubSignKey)
    X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(pubBytes);
    PublicKey publicKey = keyFactory.generatePublic(X509publicKey);

    return new KeyPair(publicKey, privateKey);
}

使用Java(服务器端)对值进行签名

 public static byte[] generateSignature(PrivateKey signPrivateKey, byte[] data) throws ... {
    Signature dsa = Signature.getInstance("SHA256withRSA");
    dsa.initSign(signPrivateKey);
    dsa.update(data);
    return dsa.sign();
}

将它们发送到 WebCrypto API 的 Web 应用程序以作为客户端/浏览器进行验证(客户端知道在第一步生成的 publicKey)。
// Import public sign/verify key (javaPubSignVerifyKey)
var signatureAlgorithm = {
    name: 'RSASSA-PKCS1-v1_5',
    modulusLength: 2048,
    publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
    hash: {
      name: 'SHA-256'
   }
};
// JWK format (1)
crypto.subtle.importKey(
    'jwk', javaPubSignVerifyKey, signatureAlgorithm, false, ['verify']
).then(success, error);

function success(key) {
    signatureVerifyPublicKey = key;
}

注意(1):在Java端,我使用com.nimbusds.jose.jwk.JWK将publicKey导出为JWK格式。
签名密钥已成功导入WebCrypto。但是在验证时失败了(验证布尔值为false)。
crypto.subtle.verify(
      signatureAlgorithm,
      signatureVerifyPublicKey,
      signature,               // bytes in Int8Array format (2)
      data                     // bytes in Int8Array format
    ).then(
       function (valid) {
           // valid === false
       }
    )

注意(2):还要注意到,我在WebCrypto上找到的每个示例都使用Uint8Array表示字节数组,但由于Java生成的是有符号字节数组,因此我需要使用Int8Array,以便签名值不被污染(也许这也是一个问题)。 编辑:供参考,结果证明这是另一个无关的问题——我在Javascript中将期望数据从base64转换了两次,而没有注意到它;自然会导致验证失败。

Java的byte[]存储来自0..255的值。我认为您需要使用Uint8ArrayArrayBuffer。您可以尝试使用此处记录的stringToArrayBuffer函数(https://dev59.com/vVoV5IYBdhLWcg3wnf7r#37407739)来表示`data`和`signature`。 - pedrofb
谢谢,还是不起作用。也许我需要改变实现的基本原理:考虑在Java中导入X509证书,并使用pki.js + asn1.js从中(证书)提取公钥,在JavaScript中使用WebCrypto进行验证(示例在这里)。 - nuno
我猜测公钥应该没问题,因为当您导入JWK时已经引发了异常。但是为了确保,您可以使用spki格式(将二进制RSA密钥转换为ArrayBuffer)进行导入。 - pedrofb
当在Java中使用Base64.encode(pubKey.getEncoded())生成spki,并在Javascript中使用str2ab(atob(spkiBase64))导入时,出现的症状是相同的:验证失败。 - nuno
1个回答

3

请检查这段基于你的简单代码,用于导入RSA公钥(spki)并验证签名。我使用类似的Java代码生成了密钥和签名。

var publicKeyB64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVdZDEs6htb3oxWstz7q+e5IwIRcptMNJiyemuoNyyjtiOy+0tEodjgo7RVoyUcGU3MysEivqvKdswQZ4KfwQCBLAR8DRzp3biAge5utZcKsQoQaC1rCEplfmzEo5ovIlBcMq5x1BxnrnlwEPRmM7MefRa+OeAOQJcstHcrJFO7QIDAQAB";
var dataB64 = "aGVsbG8=";
var signatureB64 = "aEOmUA7YC5gvF6QgH+TMg0erY5pzr83nykZGFtyGOOe+6ld+MC4/Qdb608XiNud+pBpzh0wqd6aajOtJim5XEfCH8vUPsv45aSPtukUIQTX00Oc1frIFDQI6jGJ4Q8dQYIwpqsyE2rkGwTDzt1fTTGiw54pLsJXjtL/D5hUEKL8=";
var signatureAlgorithm = {name: 'RSASSA-PKCS1-v1_5',modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]),hash: { name: 'SHA-256'  }};

//convert public key, data and signature to ArrayBuffer. 
var publicKey = str2ab(atob(publicKeyB64)); 
var data = str2ab(atob(dataB64));
var signature = str2ab(atob(signatureB64));            

crypto.subtle.importKey("spki", publicKey, signatureAlgorithm, false,["verify"]).
    then(function(key){
        console.log(key);
        return crypto.subtle.verify( signatureAlgorithm, key, signature, data);                    
}).then( function (valid) {
    console.log("Signature valid: "+valid);
}).catch(function(err) {
    alert("Verification failed " + err );
});

我无法完全复现这个问题。使用你提供的str2ab实用函数,代码可以完美运行。

//Utility function
function str2ab(str) {
  var arrBuff = new ArrayBuffer(str.length);
  var bytes = new Uint8Array(arrBuff);
  for (var iii = 0; iii < str.length; iii++) {
    bytes[iii] = str.charCodeAt(iii);
  }
  return bytes;
}

我建议比较这两个代码,找出它们的不同之处。


嗨@pedrofb,感谢您的反馈 - 结果是另一个无关的问题:我在Javascript中将预期数据从base64转换了两次而没有注意到;自然地,验证失败了。供参考,使用Uint8Array一点问题都没有,正如我最初怀疑的那样。 - nuno
另外,我使用的 str2abab2str 方法如下:链接 - nuno
也许你使用 Uint16Array 时遇到了问题? - nuno
我已经使用您提供的str2ab函数检查了代码,并且运行良好。实际上,这个函数的内容和我之前在stringToArrayBuffer中使用的代码是一样的。我认为之前的str2ab问题出在Uint16Array上。我已经根据正确的链接更新了答案。 - pedrofb

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