{"id":332,"date":"2011-03-13T12:20:59","date_gmt":"2011-03-13T11:20:59","guid":{"rendered":"http:\/\/locallost.net\/?p=332"},"modified":"2011-03-24T19:11:46","modified_gmt":"2011-03-24T18:11:46","slug":"signing-large-files-with-php-openssl-extension","status":"publish","type":"post","link":"https:\/\/locallost.net\/?p=332","title":{"rendered":"Signing large files with PHP openssl extension"},"content":{"rendered":"<p>The openssl extension for PHP provides the openssl_sign() function to sign data.<\/p>\n<p>The drawback of this function is that the data is fed with a single variable holding the entire data to be signed. This implies that all your data must fit in memory. This is inadequate if you want to sign large files and cannot\/dont want to load the whole file in memory with a get_file_contents() for example.<\/p>\n<p>Here is a way to circumvent this limitation by implementing your own signing process (bonus point for being compatible with the output from the original openssl_sign()\/openssl_verify() functions).<\/p>\n<p><!--more--><\/p>\n<p>Basically, signing a file consist in:<\/p>\n<ol>\n<li>Compute a digest of the file with a hashing function like SHA1<\/li>\n<li>Encrypt this digest with your private key<\/li>\n<\/ol>\n<p>This gives you a signature data that you can be verified by doing the reverse:<\/p>\n<ol>\n<li>Decrypt the signature with the signer public key<\/li>\n<li>Compute a digest of the file with the same hashing function used by the signer<\/li>\n<li>Compare your digest with the decrypted one. If they are \u00a0different, then your file is not the original one<\/li>\n<\/ol>\n<p>So, based on this principles, here are a sign and verify function that do not requires you to load the entire file into memory:<\/p>\n<ul>\n<li>sign_file()<\/li>\n<\/ul>\n<pre>$privKeyfile = '\/path\/to\/your\/private_key.pem';\r\n\r\nfunction sign_file($privKeyfile, $file) {\r\n  $digest = sha1_file($file, true);\r\n\r\n  $privkey = openssl_get_privatekey(file_get_contents($privKeyfile));\r\n  openssl_private_encrypt($digest, $signature, $privkey);\r\n\r\n  return $signature;\r\n}\r\n\r\n$signature = sign_file($privKeyfile, 'large_file.iso');<\/pre>\n<ul>\n<li>verify_file()<\/li>\n<\/ul>\n<pre>$pubKeyfile = '\/path\/to\/your\/public_key.pem';\r\n\r\nfunction verify_file($pubKeyfile, $file, $signature) {\r\n  $digest = sha1_file($file, true);\r\n\r\n  $pubkey = openssl_get_publickey(file_get_contents($pubKeyfile));\r\n  openssl_public_decrypt($signature, $decrypted_digest, $pubkey);\r\n\r\n  if( $digest == $decrypted_digest ) {\r\n    return true;\r\n  }\r\n\r\n  return false;\r\n}\r\n\r\n$isValid = verify_file($pubKeyfile, 'large_file.iso', $signature);\r\nif( $isValid ) {\r\n  print \"Valid signature.\\n\";\r\n} else {\r\n  print \"Invalid signature!\\n\";\r\n}<\/pre>\n<p>But there is still something wrong (the bonus point).<\/p>\n<p>If you generate a signature with sign_file() and try to verify the signature with openssl_verify() you&rsquo;ll notice that it will fail. The same happens if you sign with openssl_sign() and verify with verify_file().<\/p>\n<p>Despite the fact that openssl_sign() seems to do the same operations (compute digest, then encrypt the digest with the priv key), the resulting signature is not the same.<\/p>\n<p>Afer grepping into the openssl source code for a while, I finally found that openssl does not encrypt the \u00ab\u00a0raw\u00a0\u00bb digest, but encrypt the digest in its ASN1 form.<\/p>\n<p>So, if you want to generate, or verify an openssl signature, you&rsquo;ll have to also encode\/decode your digests in ASN1.<\/p>\n<p>Here is a rewrite of the sign_file() and verify_file() functions that is compatible with the signature from openssl_sign() and openssl_verify() :<\/p>\n<ul>\n<li>sign_file() with \u00ab\u00a0hardcoded\u00a0\u00bb ASN1 encoding<\/li>\n<\/ul>\n<pre>$privKeyfile = '\/path\/to\/your\/private_key.pem';\r\n\r\nfunction sign_file($privKeyfile, $file) {\r\n  $digest = sha1_file($file, true);\r\n\r\n  $asn1  = chr(0x30).chr(0x21); \/\/ SEQUENCE, 33\r\n  $asn1 .= chr(0x30).chr(0x09); \/\/ SEQUENCE, 9\r\n  $asn1 .= chr(0x06).chr(0x05); \/\/ OBJECT IDENTIFIER, 5\r\n  $asn1 .= chr(0x2b).chr(0x0e).chr(0x03).chr(0x02).chr(0x1a); \/\/ 1.3.14.3.2.26 (SHA1)\r\n  $asn1 .= chr(0x05).chr(0x00); \/\/ NULL\r\n  $asn1 .= chr(0x04).chr(0x14); \/\/ OCTET STRING, 20\r\n  $asn1 .= $digest;\r\n\r\n  $privkey = openssl_get_privatekey(file_get_contents($privKeyfile));\r\n  openssl_private_encrypt($asn1, $signature, $privkey);\r\n\r\n  return $signature;\r\n}\r\n\r\n$signature = sign_file($privKeyfile, 'large_file.iso');<\/pre>\n<ul>\n<li>verify_file() with \u00ab\u00a0hardcoded\u00a0\u00bb ASN1 decoding<\/li>\n<\/ul>\n<pre>$pubKeyfile = '\/path\/to\/your\/public_key.pem';\r\n\r\nfunction verify_file($pubKeyfile, $file, $signature) {\r\n  $digest = sha1_file($file, true);\r\n\r\n  $pubkey = openssl_get_publickey(file_get_contents($pubKeyfile));\r\n  openssl_public_decrypt($signature, $asn1, $pubkey);\r\n\r\n  $decrypted_digest = substr($asn1, 15); \/\/ Blindly strip the ASN1 header\r\n\r\n  if( $digest == $decrypted_digest ) {\r\n    return true;\r\n  }\r\n\r\n  return false;\r\n}\r\n\r\n$isValid = verify_file($pubKeyfile, 'large_file.iso', $signature);\r\nif( $isValid ) {\r\n  print \"Valid signature.\\n\";\r\n} else {\r\n  print \"Invalid signature!\\n\";\r\n}<\/pre>\n<p>Now, you can sign with openssl_sign() and verify with verify_file(), or sign with sign_file() and verify with openssl_verify().<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The openssl extension for PHP provides the openssl_sign() function to sign data. The drawback of this function is that the data is fed with a single variable holding the entire data to be signed. This implies that all your data &hellip; <a href=\"https:\/\/locallost.net\/?p=332\">Continuer la lecture <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[4],"tags":[],"class_list":["post-332","post","type-post","status-publish","format-standard","hentry","category-code"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p2Bei9-5m","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/locallost.net\/index.php?rest_route=\/wp\/v2\/posts\/332","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/locallost.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/locallost.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/locallost.net\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/locallost.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=332"}],"version-history":[{"count":9,"href":"https:\/\/locallost.net\/index.php?rest_route=\/wp\/v2\/posts\/332\/revisions"}],"predecessor-version":[{"id":347,"href":"https:\/\/locallost.net\/index.php?rest_route=\/wp\/v2\/posts\/332\/revisions\/347"}],"wp:attachment":[{"href":"https:\/\/locallost.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=332"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/locallost.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=332"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/locallost.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=332"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}