I have a simple string of text that needs to be signed inside a Javascript and verified on the server (PHP). To start my test I first created a key pair:
// 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,
};
}
I obtained a private and public key:
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"
This is the script I’m using to sign the data (string) using the privatekey:
// 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;
}
Then I created a script to verify the data, still in Javascript, to validate what I was doing:
// 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;
}
The script returned true
so I moved on the next step, verifying the data in 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)
}
This script is never returning 1 indicating ‘valid’. I’m not sure if the problem is how the keys are generated. I can generate the pair in PHP if this helps. @Topaco this is the whole question I was talking about.
edit: I added return openssl_error_string();
in case of 0
and here is the result: error:02000077:rsa routines::wrong signature length
on a second run I got error:0480006C:PEM routines::no start line
2
Answers
It seems like there might be an issue with how the signature is encoded or decoded between JavaScript and PHP. Let’s ensure consistency in how the data is processed.
In your JavaScript signRequestData function, the signature is converted to base64 using btoa. In your PHP code, you’re using openssl_verify with the raw signature data. The signature should be base64-decoded before being passed to openssl_verify
Update the verifySignature function in JavaScript to directly use the raw signature bytes:
Update your PHP code to base64-decode the signature before verifying:
In the JavaScript code, hashing is performed twice, once explicitly with the
digest()
function and onceimplicitly
by thesign()
function. As the double hashing is performed on the JavaScript side during both signing and verification, verification works on the JavaScript side.In contrast, the PHP code hashes once, namely implicitly in
openssl_verify()
, so that as a result of this different hashing strategy both codes are incompatible. For this reason, verification with the PHP code fails. To eliminate this incompatability, hashing must be carried out consistently.Since the double hashing is unnecessary, the JavaScript side should be adapted and the double hashing removed, i.e. in the JavaScript code
dataBuffer
should be passed directly tosign()
andverify()
.In addition to this hashing issue, as already mentioned in the other answer, the Base64 encoded signature in the JavaScript code must be Base64 decoded in the PHP code.
The fixed JavaScript code (with only single hashing) is:
For the private key you specified and the message The quick brown fox jumps over the lazy dog, the following Base64 encoded signature results:
The PHP code remains unchanged apart from the Base64 decoding of the signature:
With these changes, verification with the PHP code is now successful.
For the sake of completeness: If the JavaScript code is the reference and the double hashing is to be kept, the PHP code must also hash twice, e.g. by replacing
$data
withhash('sha256', $data, true)
.