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:
- 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. - 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.
- 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?
- Feel free to change Exported format(pem), Public key format(‘spki’), Algorithm format(RSA-PSS) and so on.
Advertisement
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