Unable to verify RSA-PSS signature in Node.js

Tags: , , , ,



I have a client in JavaScript and a server in Node.JS. I’m trying to sign a simple text in client and send the signature along with publicKey to the server then server can verify the publicKey.

Anything in client-side is OK! but I’m unable to verify the signature in server-side. I think there is no need for you to read the client code but just for assurance I’ll provide it too.

Client code:

let privateKey = 0;
let publicKey = 0;
let encoded = '';
let signatureAsBase64 = '';
let pemExported = ''
function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
}

function str2ab(str) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}
let keygen = crypto.subtle.generateKey({
  name: 'RSA-PSS',
  modulusLength: 4096,
  publicExponent: new Uint8Array([1,0,1]),
  hash: 'SHA-256'
  }, true, ['sign', 'verify']);

keygen.then((value)=>{
    publicKey = value.publicKey;
    privateKey = value.privateKey;
    let exported = crypto.subtle.exportKey('spki', publicKey);
    return  exported
}).then((value)=>{
    console.log('successful');
    const exportedAsString = ab2str(value);
    const exportedAsBase64 = btoa(exportedAsString);
    pemExported = `-----BEGIN PUBLIC KEY-----n${exportedAsBase64}n-----END PUBLIC KEY-----`;
    //signing:
    encoded = new TextEncoder().encode('test');
    let signing = crypto.subtle.sign({
          name: "RSA-PSS",
          saltLength: 32
      },
      privateKey,
      encoded);
    return signing;
}).then((signature)=>{
    const signatureAsString = ab2str(signature);
    signatureAsBase64 = btoa(signatureAsString);
    //verifying just to be sure everything is OK:
    return crypto.subtle.verify({
          name: 'RSA-PSS',
          saltLength: 32
      },
      publicKey,
      signature,
      encoded)
}).then((result)=>{
    console.log(result);
    
    //send information to server:
    let toSend = new XMLHttpRequest();
    toSend.onreadystatechange = ()=>{
       console.log(this.status);
    };
    toSend.open("POST", "http://127.0.0.1:3000/authentication", true);
    let data = {
      signature: signatureAsBase64,
      publicKey: pemExported
    };
    toSend.setRequestHeader('Content-Type', 'application/json');
    toSend.send(JSON.stringify(data));
    
    //to let you see the values, I'll print them to console in result:
    console.log("signature is:n", signatureAsBase64);
    console.log("publicKey is:n", pemExported);
}).catch((error)=>{
  console.log("error",error.message);
})

Server Code(I use express for this purpose):

const express = require('express');
const crypto = require('crypto');
const router = express.Router(); 

function str2ab(str) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

router.post('/authentication',  async (req, res)=>{
    try{
        const publicKey = crypto.createPublicKey({
            key: req.body.publicKey,
            format: 'pem',
            type: 'spki'
        });
        console.log(publicKey.asymmetricKeyType, publicKey.asymmetricKeySize, publicKey.type);
        let signature = Buffer.from(req.body.signature, 'base64').toString();
        signature = str2ab(signature);
        const result = crypto.verify('rsa-sha256', new TextEncoder().encode('test'),
                        publicKey, new Uint8Array(signature));
        console.log(result);
    }catch(error){
        console.log('Error when autheticating user: ', error.message);
    }
})

Server Console Log:

rsa undefined public
false

NOTE:

  1. I think the public key is imported correctly in server because when I export the public key again in server, the pem formats of both sides(client & server) are completely equal. so I think the problem is associated with ‘verification’ or ‘converting signature’ in server.
  2. I prefer to use the built-in crypto module if it’s possible, so other libraries such as subtle-crypto are my second options and I’m here to see if this can be done with crypto or not.
  3. I want to learn how to verify a signature that is signed by JavaScript SubtleCrypto, due to this, Please don’t ask some questions such as:

Why do you want to verify the public key in server?

Why don’t you use ‘X’ library in client?

  1. Feel free to change Exported format(pem), Public key format(‘spki’), Algorithm format(RSA-PSS) and so on.

Answer

The failed verification has two reasons:

  • The PSS padding must be specified explicitly, since PKCS#1 v1.5 padding is the default, s. here.

  • The conversion of the signature corrupts the data: The line:

    let signature = Buffer.from(req.body.signature, 'base64').toString();
    

    performs a UTF8 decoding, s. here, which irreversibly changes the data, s. here. The signature consists of binary data that is generally UTF8 incompatible. A conversion to a string is only possible with suitable binary-to-text encodings (like Base64, hex etc.), s. here.
    But apart from that a conversion is actually not necessary at all, because the signature can be passed directly as a buffer, s. here.

The following NodeJS code performs a successful verification (for a signature and public key produced with the client code):

const publicKey = crypto.createPublicKey(
    {
        key: req.body.publicKey,
        format: 'pem',
        type: 'spki'
    });

const result = crypto.verify(
    'rsa-sha256', 
    new TextEncoder().encode('test'), 
    {
        key: publicKey, 
        padding: crypto.constants.RSA_PKCS1_PSS_PADDING
    }, 
    Buffer.from(req.body.signature, 'base64'));

console.log(result); // true


Source: stackoverflow