PageRenderTime 39ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/caelum/classes/toolbox/CertTest.class.php

https://github.com/nemiah/projectMankind
PHP | 343 lines | 210 code | 32 blank | 101 comment | 39 complexity | 6181b321bbdd3b69cf0c83fdd9abe7ae MD5 | raw file
Possible License(s): LGPL-2.1, GPL-3.0
  1. <?php
  2. /**
  3. * Is one pem encoded certificate the signer of another?
  4. *
  5. * The PHP openssl functionality is severely limited by the lack of a stable
  6. * api and documentation that might as well have been encrypted itself.
  7. * In particular the documention on openssl_verify() never explains where
  8. * to get the actual signature to verify. The isCertSigner() function below
  9. * will accept two PEM encoded certs as arguments and will return true if
  10. * one certificate was used to sign the other. It only relies on the
  11. * openssl_pkey_get_public() and openssl_public_decrypt() openssl functions,
  12. * which should stay fairly stable. The ASN parsing code snippets were mostly
  13. * borrowed from the horde project's smime.php.
  14. *
  15. * @author Mike Green <mikey at badpenguins dot com>
  16. * @copyright Copyright (c) 2010, Mike Green, "classified" by Rainer Furtmeier, 2011
  17. * @license http://opensource.org/licenses/gpl-2.0.php GPLv2
  18. */
  19. class CertTest {
  20. public static $FITCertificate = "-----BEGIN CERTIFICATE-----
  21. MIID0zCCAzygAwIBAgIBADANBgkqhkiG9w0BAQQFADCBqDELMAkGA1UEBhMCREUx
  22. DzANBgNVBAgTBkJheWVybjEVMBMGA1UEBxMMR2VuZGVya2luZ2VuMSUwIwYDVQQK
  23. ExxGdXJ0bWVpZXIgSGFyZC0gdW5kIFNvZnR3YXJlMQswCQYDVQQLEwJJVDEZMBcG
  24. A1UEAxMQUmFpbmVyIEZ1cnRtZWllcjEiMCAGCSqGSIb3DQEJARYTUmFpbmVyQEZ1
  25. cnRtZWllci5pdDAeFw0xMTExMjIxMDMyMjRaFw0xMjExMjExMDMyMjRaMIGoMQsw
  26. CQYDVQQGEwJERTEPMA0GA1UECBMGQmF5ZXJuMRUwEwYDVQQHEwxHZW5kZXJraW5n
  27. ZW4xJTAjBgNVBAoTHEZ1cnRtZWllciBIYXJkLSB1bmQgU29mdHdhcmUxCzAJBgNV
  28. BAsTAklUMRkwFwYDVQQDExBSYWluZXIgRnVydG1laWVyMSIwIAYJKoZIhvcNAQkB
  29. FhNSYWluZXJARnVydG1laWVyLml0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
  30. gQC8dxNhck+0nus2wssd6zZxaL5IILzABLMX8M/hSefJjO13krXAMzT3VT690/3q
  31. rVfcHpbYFnNm8Mv2dFvjRl/1Do6joIa20Yep5O9JEES5ggXa8YuJacbyA0ug2Kkp
  32. T0c79e1JX3hMGo/sK4RPXjEp/bzl2415N/KntvUP3Dp/ZwIDAQABo4IBCTCCAQUw
  33. HQYDVR0OBBYEFAG899efZegRV+UKsyaoVM3FXnBKMIHVBgNVHSMEgc0wgcqAFAG8
  34. 99efZegRV+UKsyaoVM3FXnBKoYGupIGrMIGoMQswCQYDVQQGEwJERTEPMA0GA1UE
  35. CBMGQmF5ZXJuMRUwEwYDVQQHEwxHZW5kZXJraW5nZW4xJTAjBgNVBAoTHEZ1cnRt
  36. ZWllciBIYXJkLSB1bmQgU29mdHdhcmUxCzAJBgNVBAsTAklUMRkwFwYDVQQDExBS
  37. YWluZXIgRnVydG1laWVyMSIwIAYJKoZIhvcNAQkBFhNSYWluZXJARnVydG1laWVy
  38. Lml0ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAdIxmYCZ+09jr
  39. xW5A88Hq/KZF0tnQZ9ixYNvrjhNMfpRmZ8budAcEEc+PzC8Q0scjNzqPotaTc89m
  40. wfmBvnKLHL+936sMcouHb/9AaoyVx0off8hvak9RVxcO4ymQu+qKRwsfdO5Q31xC
  41. 0es163+mhqYBiXHzAMZXEsOsa1g2Usg=
  42. -----END CERTIFICATE-----";
  43. /**
  44. * Extract signature from der encoded cert.
  45. * Expects x509 der encoded certificate consisting of a section container
  46. * containing 2 sections and a bitstream. The bitstream contains the
  47. * original encrypted signature, encrypted by the public key of the issuing
  48. * signer.
  49. * @param string $der
  50. * @return string on success
  51. * @return bool false on failures
  52. */
  53. static function extractSignature($der=false) {
  54. if (strlen($der) < 5)
  55. return false;
  56. // skip container sequence
  57. $der = substr($der, 4);
  58. // now burn through two sequences and the return the final bitstream
  59. while (strlen($der) > 1) {
  60. $class = ord($der[0]);
  61. $classHex = dechex($class);
  62. switch ($class) {
  63. // BITSTREAM
  64. case 0x03:
  65. $len = ord($der[1]);
  66. $bytes = 0;
  67. if ($len & 0x80) {
  68. $bytes = $len & 0x0f;
  69. $len = 0;
  70. for ($i = 0; $i < $bytes; $i++) {
  71. $len = ($len << 8) | ord($der[$i + 2]);
  72. }
  73. }
  74. return substr($der, 3 + $bytes, $len);
  75. break;
  76. // SEQUENCE
  77. case 0x30:
  78. $len = ord($der[1]);
  79. $bytes = 0;
  80. if ($len & 0x80) {
  81. $bytes = $len & 0x0f;
  82. $len = 0;
  83. for ($i = 0; $i < $bytes; $i++) {
  84. $len = ($len << 8) | ord($der[$i + 2]);
  85. }
  86. }
  87. $contents = substr($der, 2 + $bytes, $len);
  88. $der = substr($der, 2 + $bytes + $len);
  89. break;
  90. default:
  91. return false;
  92. break;
  93. }
  94. }
  95. return false;
  96. }
  97. /**
  98. * Get signature algorithm oid from der encoded signature data.
  99. * Expects decrypted signature data from a certificate in der format.
  100. * This ASN1 data should contain the following structure:
  101. * SEQUENCE
  102. * SEQUENCE
  103. * OID (signature algorithm)
  104. * NULL
  105. * OCTET STRING (signature hash)
  106. * @return bool false on failures
  107. * @return string oid
  108. */
  109. static function getSignatureAlgorithmOid($der=null) {
  110. // Validate this is the der we need...
  111. if (!is_string($der) or strlen($der) < 5)
  112. return false;
  113. $bit_seq1 = 0;
  114. $bit_seq2 = 2;
  115. $bit_oid = 4;
  116. if (ord($der[$bit_seq1]) !== 0x30)
  117. die('Invalid DER passed to getSignatureAlgorithmOid()');
  118. if (ord($der[$bit_seq2]) !== 0x30)
  119. die('Invalid DER passed to getSignatureAlgorithmOid()');
  120. if (ord($der[$bit_oid]) !== 0x06)
  121. die('Invalid DER passed to getSignatureAlgorithmOid');
  122. // strip out what we don't need and get the oid
  123. $der = substr($der, $bit_oid);
  124. // Get the oid
  125. $len = ord($der[1]);
  126. $bytes = 0;
  127. if ($len & 0x80) {
  128. $bytes = $len & 0x0f;
  129. $len = 0;
  130. for ($i = 0; $i < $bytes; $i++) {
  131. $len = ($len << 8) | ord($der[$i + 2]);
  132. }
  133. }
  134. $oid_data = substr($der, 2 + $bytes, $len);
  135. // Unpack the OID
  136. $oid = floor(ord($oid_data[0]) / 40);
  137. $oid .= '.' . ord($oid_data[0]) % 40;
  138. $value = 0;
  139. $i = 1;
  140. while ($i < strlen($oid_data)) {
  141. $value = $value << 7;
  142. $value = $value | (ord($oid_data[$i]) & 0x7f);
  143. if (!(ord($oid_data[$i]) & 0x80)) {
  144. $oid .= '.' . $value;
  145. $value = 0;
  146. }
  147. $i++;
  148. }
  149. return $oid;
  150. }
  151. /**
  152. * Get signature hash from der encoded signature data.
  153. * Expects decrypted signature data from a certificate in der format.
  154. * This ASN1 data should contain the following structure:
  155. * SEQUENCE
  156. * SEQUENCE
  157. * OID (signature algorithm)
  158. * NULL
  159. * OCTET STRING (signature hash)
  160. * @return bool false on failures
  161. * @return string hash
  162. */
  163. static function getSignatureHash($der=null) {
  164. // Validate this is the der we need...
  165. if (!is_string($der) or strlen($der) < 5)
  166. return false;
  167. if (ord($der[0]) !== 0x30)
  168. die('Invalid DER passed to getSignatureHash()');
  169. // strip out the container sequence
  170. $der = substr($der, 2);
  171. if (ord($der[0]) !== 0x30)
  172. die('Invalid DER passed to getSignatureHash()');
  173. // Get the length of the first sequence so we can strip it out.
  174. $len = ord($der[1]);
  175. $bytes = 0;
  176. if ($len & 0x80) {
  177. $bytes = $len & 0x0f;
  178. $len = 0;
  179. for ($i = 0; $i < $bytes; $i++) {
  180. $len = ($len << 8) | ord($der[$i + 2]);
  181. }
  182. }
  183. $der = substr($der, 2 + $bytes + $len);
  184. // Now we should have an octet string
  185. if (ord($der[0]) !== 0x04)
  186. die('Invalid DER passed to getSignatureHash()');
  187. $len = ord($der[1]);
  188. $bytes = 0;
  189. if ($len & 0x80) {
  190. $bytes = $len & 0x0f;
  191. $len = 0;
  192. for ($i = 0; $i < $bytes; $i++) {
  193. $len = ($len << 8) | ord($der[$i + 2]);
  194. }
  195. }
  196. return bin2hex(substr($der, 2 + $bytes, $len));
  197. }
  198. /**
  199. * Determine if one cert was used to sign another
  200. * Note that more than one CA cert can give a positive result, some certs
  201. * re-issue signing certs after having only changed the expiration dates.
  202. * @param string $cert - PEM encoded cert
  203. * @param string $caCert - PEM encoded cert that possibly signed $cert
  204. * @return bool
  205. */
  206. static function isCertSigner($certPem=null, $caCertPem=null) {
  207. if (!function_exists('openssl_pkey_get_public'))
  208. die('Need the openssl_pkey_get_public() function.');
  209. if (!function_exists('openssl_public_decrypt'))
  210. die('Need the openssl_public_decrypt() function.');
  211. if (!function_exists('hash'))
  212. die('Need the php hash() function.');
  213. if (empty($certPem) or empty($caCertPem))
  214. return false;
  215. // Convert the cert to der for feeding to extractSignature.
  216. $certDer = self::pemToDer($certPem);
  217. if (!is_string($certDer))
  218. return false;
  219. #die('invalid certPem');
  220. // Grab the encrypted signature from the der encoded cert.
  221. $encryptedSig = self::extractSignature($certDer);
  222. if (!is_string($encryptedSig))
  223. die('Failed to extract encrypted signature from certPem.');
  224. // Extract the public key from the ca cert, which is what has
  225. // been used to encrypt the signature in the cert.
  226. $pubKey = openssl_pkey_get_public($caCertPem);
  227. if ($pubKey === false)
  228. die('Failed to extract the public key from the ca cert.');
  229. // Attempt to decrypt the encrypted signature using the CA's public
  230. // key, returning the decrypted signature in $decryptedSig. If
  231. // it can't be decrypted, this ca was not used to sign it for sure...
  232. $rc = openssl_public_decrypt($encryptedSig, $decryptedSig, $pubKey);
  233. if ($rc === false)
  234. return false;
  235. // We now have the decrypted signature, which is der encoded
  236. // asn1 data containing the signature algorithm and signature hash.
  237. // Now we need what was originally hashed by the issuer, which is
  238. // the original DER encoded certificate without the issuer and
  239. // signature information.
  240. $origCert = self::stripSignerAsn($certDer);
  241. if ($origCert === false)
  242. die('Failed to extract unsigned cert.');
  243. // Get the oid of the signature hash algorithm, which is required
  244. // to generate our own hash of the original cert. This hash is
  245. // what will be compared to the issuers hash.
  246. $oid = self::getSignatureAlgorithmOid($decryptedSig);
  247. if ($oid === false)
  248. die('Failed to determine the signature algorithm.');
  249. switch ($oid) {
  250. case '1.2.840.113549.2.2': $algo = 'md2';
  251. break;
  252. case '1.2.840.113549.2.4': $algo = 'md4';
  253. break;
  254. case '1.2.840.113549.2.5': $algo = 'md5';
  255. break;
  256. case '1.3.14.3.2.18': $algo = 'sha';
  257. break;
  258. case '1.3.14.3.2.26': $algo = 'sha1';
  259. break;
  260. case '2.16.840.1.101.3.4.2.1': $algo = 'sha256';
  261. break;
  262. case '2.16.840.1.101.3.4.2.2': $algo = 'sha384';
  263. break;
  264. case '2.16.840.1.101.3.4.2.3': $algo = 'sha512';
  265. break;
  266. default:
  267. die('Unknown signature hash algorithm oid: ' . $oid);
  268. break;
  269. }
  270. // Get the issuer generated hash from the decrypted signature.
  271. $decryptedHash = self::getSignatureHash($decryptedSig);
  272. // Ok, hash the original unsigned cert with the same algorithm
  273. // and if it matches $decryptedHash we have a winner.
  274. $certHash = hash($algo, $origCert);
  275. return ($decryptedHash === $certHash);
  276. }
  277. /**
  278. * Convert pem encoded certificate to DER encoding
  279. * @return string $derEncoded on success
  280. * @return bool false on failures
  281. */
  282. static function pemToDer($pem=null) {
  283. if (!is_string($pem))
  284. return false;
  285. $cert_split = preg_split('/(-----((BEGIN)|(END)) CERTIFICATE-----)/', $pem);
  286. if (!isset($cert_split[1]))
  287. return false;
  288. return base64_decode($cert_split[1]);
  289. }
  290. /**
  291. * Obtain der cert with issuer and signature sections stripped.
  292. * @param string $der - der encoded certificate
  293. * @return string $der on success
  294. * @return bool false on failures.
  295. */
  296. static function stripSignerAsn($der=null) {
  297. if (!is_string($der) or strlen($der) < 8)
  298. return false;
  299. $bit = 4;
  300. $len = ord($der[($bit + 1)]);
  301. $bytes = 0;
  302. if ($len & 0x80) {
  303. $bytes = $len & 0x0f;
  304. $len = 0;
  305. for ($i = 0; $i < $bytes; $i++)
  306. $len = ($len << 8) | ord($der[$bit + $i + 2]);
  307. }
  308. return substr($der, 4, $len + 4);
  309. }
  310. }
  311. ?>