javascriptphpasn.1ecdsawebcrypto

OpenSSL ECDSA ASN.1 Signature vs WebCrypto ECDSA


Let's use a test key (don't use it, it's just a test, I don't use it either):

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkeS6LTDov1L2oShp6PgM2STESyMD1YTQkxUtxSJc1PahRANCAAR+uKNDd0l2odP4excxvTBY5bQxQBufCYb2TkVgp1NBefyO603LHzOelGVnnIFfamtJvv55AwVxt0nUlS0z450U
-----END PRIVATE KEY-----

I want to sign in Javascript and verify in PHP but I don't get the same result so I tried to get at first same result for signing. Here is my code:

PHP:

$challenge=file_get_contents("/tmp/challenge-XIABFf");
$key=openssl_pkey_get_private(file_get_contents("/tmp/private-key"));
$response="";
openssl_sign($challenge,$response, $key, OPENSSL_ALGO_SHA384);
var_dump(base64_encode($response));

I get : MEYCIQCQt9+reL0WRzi26ft8CEUMRDhnd4vh50F3sYL9BA2x0QIhAJCD7bakD6jXIfYTNpEoGPTR9guusC7CFxbbFcKPvroq which is 96 characters.

Now in JS :

function arrayBufferToBase64(buffer) {
        var binary = '';
        var bytes = new Uint8Array(buffer);
        for (var i = 0; i < bytes.byteLength; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }
function base64ToArrayBuffer(base64) {
    var binary_string = atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}

key=await window.crypto.subtle.importKey("pkcs8",base64ToArrayBuffer("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkeS6LTDov1L2oShp6PgM2STESyMD1YTQkxUtxSJc1PahRANCAAR+uKNDd0l2odP4excxvTBY5bQxQBufCYb2TkVgp1NBefyO603LHzOelGVnnIFfamtJvv55AwVxt0nUlS0z450U"), {name: "ECDSA", namedCurve: "P-256"},true,["sign"]);
message=await window.crypto.subtle.sign({"name": "ECDSA", "hash": "SHA-384"},key,base64ToArrayBuffer("4BRYPzkacla7x5uR0bXtkQXC18odd9yCfcefjIrh")); console.log(arrayBufferToBase64(message));

The 4BRY... is the base64 of the challenge. And the result is : ZDDWSkpkwS8N8LYhHib/Qeb+P8DMRtKIT925AhO8m6bD/K5wAYEYa3Fq89Ik8z6t62a0dtuN8kydxNWttgFcdA== (88 characters) which is not the same as PHP, why?


Solution

  • After some deep and help from @Dhaval-purohit, I identified why there is a 8 characters difference between my signature.

    I found this library (called jsrsassign) on Github and they do import functions to do the trick such as :

    They need the hexadecimal form so using this function, you can sign and get a result that OpenSSL can match :

    /* Stackoverflow DorianCoding */
    async function signchallenge(key, challenge) {
                var response = await window.crypto.subtle.sign({
                    name: "ECDSA",
                    hash: "SHA-384"
                }, key, base64ToArrayBuffer(challenge));
                if (!response) return Promise.reject();
                return arrayBufferToBase64(hextoArrayBuffer(KJUR.crypto.ECDSA.concatSigToASN1Sig(ArrayBuffertohex(response))));
            }
    

    You need to implement arraybuffer <=> base64 and hex <=> base64 functions (there is example in my question).

    Then a signature like : 3d282410b1f47bfb0a4aae0caf7bef2a59d62b4ecd41037ff913cd71e922a26843c390e1a042cf6199421e55f12b4983845f5c503fcac1b70e3f8e13ec39a8bb

    turns into :

    3045022100e28ba263bfa655a5bbe2b84d6ef2cdcb821d941f60e375c17b016030eb10e97e0220219927e83f702626ef00dcd0714c232cb5299730b29082e3ec72e94c0b776da2

    and as first byte are often the same (3045, ASN.1 format should start by MEU/QCI... in base64) :

    MEQCID0oJBCx9Hv7CkquDK977ypZ1itOzUEDf/kTzXHpIqJoAiBDw5DhoELPYZlCHlXxK0mDhF9cUD/KwbcOP44T7Dmouw==
    

    And now OpenSSL is happy.