Comme il est parfois difficile de faire le lien entre les formats, les algorithmes et les divers paramètres, cette page à pour but de montrer le lien qu'il existe entre des opérations avec l'API WebCrypto et d'autres outils et langages. Elle ne reflète en aucun cas des méthodes devant être appliquée mais bien des exemples comme générer un clé privée avec OpenSSL et l'utiliser avec WebCrypto.
$ openssl pkey -pubin -in rsapub.pem -out rsapub.der -outform DER
Pour convertir une clé SSH vers RSA (pour ensuite être utilisée avec OpenSSL)
$ ssh-keygen -p -m PEM -f ssh-rsa.key
Ensuite on extrait sa clé publique et on la converti en PKCS#8
$ openssl rsa -in ssh-rsa.key -pubout > ssh-rsa.pub $ openssl pkcs8 -topk8 -nocrypt -in ssh-rsa.key -out ssh-rsa.pkcs8
Pour importer une clé en JavaScript, le format PKCS#8 est préféré
$ openssl pkcs8 -topk8 -nocrypt -in rsakey.pem -out rsakey.pkcs8 # converti pkcs8
Convertir une clé PKCS#8 format PEM vers un ArrayBuffer utilisable avec crypto.subtle.importKey
function b64Decode (text) { let s = atob(text.trim().replace(/\r?\n|\r/g, '')) const buffer = new ArrayBuffer(s.length) const bview = new Uint8Array(buffer) for (let i = 0; i < s.length; i++) { bview[i] = s.charCodeAt(i) } return buffer } function readPEM (text) { const pemRegex = /^-----BEGIN (?:PRIVATE|PUBLIC) KEY-----$([a-zA-Z0-9\/\+=\r\n]*)^-----END (?:PRIVATE|PUBLIC) KEY-----$/mg let m = pemRegex.exec(text) if (m && m.length > 1) { return b64Decode(m[1]) } return undefined }
Convertir un ArrayBuffer obtenu avec crypto.subtle.exportKey (avec une mise en forme similaire à OpenSSL)
function b64Encode (buffer) { let txt = '' let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))) for (let i = 0; i < (Math.ceil(b64.length / 64)); i++) { txt += b64.substring(i * 64, (i + 1) * 64) + "\n" } return txt } function writePEM (buffer, priv = false) { let txt = `-----BEGIN ${priv ? 'PRIVATE' : 'PUBLIC'} KEY-----` + "\n" txt += b64Encode(buffer) return txt + `-----END ${priv ? 'PRIVATE' : 'PUBLIC'} KEY-----` }
OpenSSL génère, avec genrsa, une clé au format pkcs#1 codé avec PEM
$ openssl genrsa -F4 2048 > rsakey.pem # clé privée 2048bits pkcs1 $ openssl rsa -in rsakey.pem -pubout > rsapub.pem # clé publique
OpenSSH génère des clé au format RFC4716.
$ ssh-keygen -t rsa -b 2048 -f ssh-rsa.key
Avec l'option “-m PEM” on peut avoir directement une clé PKCS#1
function genKeyPair() { return new Promise((resolve, reject) => { /* same as openssl genrsa -F4 2048 */ crypto.subtle.generateKey( { name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) }, true, ['sign', 'verify']).then((key) => { Promise.all([ crypto.subtle.exportKey('pkcs8', key.privateKey), crypto.subtle.exportKey('spki', key.publicKey) ]).then((keys) => { resolve([writePEM(keys[0], true), writePEM(keys[1], false)]) }) }) }) }
echo -n "Texte à signer" | openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -sign fromjs.key -out - - | base64
function signFromPKCS8 (text, privkey) { return new Promise((resolve, reject) => { const key = readPEM(privkey) if (key) { crypto.subtle.importKey( 'pkcs8', key, { name: 'RSA-PSS', hash: { name: 'SHA-256' } }, false, [ 'sign' ] ).then((key) => { crypto.subtle.sign( { name: 'RSA-PSS', saltLength: 0 }, key, (new TextEncoder).encode(text) ).then((signed) => { resolve(b64Encode(signed)) }) }) } }) }
Signature avec phpseclib
<?PHP require('phpseclib/autoload.php'); use phpseclib\Crypt\RSA; $rsa = new RSA(); $rsa->loadKey(file_get_contents('fromjs.key')); $rsa->setHash('sha256'); $rsa->setMGFHash('sha256'); $rsa->setSaltLength(0); $rsa->setSignatureMode(RSA::SIGNATURE_PSS); $text = 'Texte à signer'; $signature = $rsa->sign($text); echo base64_encode($signature); ?>
/* Pour lire la clé : * * FILE * fp = fopen("pkey.pem", "r"); * sign(PEM_read_PrivateKey(fp, NULL, NULL, NULL), ...); */ void sign (EVP_PKEY * pkey, const char * text, unsigned char ** stext, size_t * slen) { size_t siglen = 0; EVP_PKEY_CTX * kctx = NULL; EVP_MD_CTX * mctx = NULL; mctx = EVP_MD_CTX_new(); if (!mctx) { return; } kctx = EVP_PKEY_CTX_new(pkey, NULL); if (kctx) { /* ordre important ici */ EVP_MD_CTX_set_pkey_ctx(mctx, kctx); EVP_DigestSignInit(mctx, NULL, EVP_sha256(), NULL, NULL); EVP_PKEY_CTX_set_rsa_padding(kctx, RSA_PKCS1_PSS_PADDING); EVP_PKEY_CTX_set_signature_md(kctx, EVP_sha256()); EVP_PKEY_CTX_set_rsa_mgf1_md(kctx, EVP_sha256()); EVP_PKEY_CTX_set_rsa_pss_saltlen(kctx, 0); EVP_DigestSignUpdate(mctx, text, strlen(text)); EVP_DigestSignFinal(mctx, NULL, &siglen); *stext = calloc(siglen, sizeof(**stext)); if (*stext == NULL) { EVP_MD_CTX_free(mctx); return; } EVP_DigestSignFinal(mctx, *stext, &siglen); *slen = siglen; } EVP_MD_CTX_free(mctx); return; }
$ base64 -d < fromjs.sig - > fromjs.bin.sig $ echo -n "Texte à signer" | openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -verify fromjs.pem -signature fromjs.bin.sig -
function verifyFromPKCS8 (text, signature, pubkey) { return new Promise((resolve, reject) => { const cert = readPEM(pubkey) if (cert) { crypto.subtle.importKey( 'spki', cert, { name: 'RSA-PSS', hash: { name: 'SHA-256' } }, false, [ 'verify' ] ).then((key) => { crypto.subtle.verify( { name: 'RSA-PSS', saltLength: 0 }, key, b64Decode(signature), (new TextEncoder).encode(text) ).then((verified) => { resolve(verified) }) }) } }) }
L'ordre des opérations (setHash, setMGFHash, loadKey, …) n'est pas déterminant.
<?PHP $rsa = new RSA(); $rsa->loadKey(file_get_contents('fromjs.pem')); $rsa->setHash('sha256'); $rsa->setMGFHash('sha256'); $rsa->setSaltLength(0); $rsa->setSignatureMode(RSA::SIGNATURE_PSS); $text = 'Texte à signer'; echo $rsa->verify($text, base64_decode(file_get_contents('fromjs.sig'))) ? 'Valide' : 'Invalide'; ?>