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:
JavaScript121let signature = Buffer.from(req.body.signature, 'base64').toString();
2
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