TlsHelper.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Util;
  12. use Symfony\Component\Process\PhpProcess;
  13. use Composer\CaBundle\CaBundle;
  14. /**
  15. * @author Chris Smith <chris@cs278.org>
  16. */
  17. final class TlsHelper
  18. {
  19. private static $useOpensslParse;
  20. /**
  21. * Match hostname against a certificate.
  22. *
  23. * @param mixed $certificate X.509 certificate
  24. * @param string $hostname Hostname in the URL
  25. * @param string $cn Set to the common name of the certificate iff match found
  26. *
  27. * @return bool
  28. */
  29. public static function checkCertificateHost($certificate, $hostname, &$cn = null)
  30. {
  31. $names = self::getCertificateNames($certificate);
  32. if (empty($names)) {
  33. return false;
  34. }
  35. $combinedNames = array_merge($names['san'], array($names['cn']));
  36. $hostname = strtolower($hostname);
  37. foreach ($combinedNames as $certName) {
  38. $matcher = self::certNameMatcher($certName);
  39. if ($matcher && $matcher($hostname)) {
  40. $cn = $names['cn'];
  41. return true;
  42. }
  43. }
  44. return false;
  45. }
  46. /**
  47. * Extract DNS names out of an X.509 certificate.
  48. *
  49. * @param mixed $certificate X.509 certificate
  50. *
  51. * @return array|null
  52. */
  53. public static function getCertificateNames($certificate)
  54. {
  55. if (is_array($certificate)) {
  56. $info = $certificate;
  57. } elseif (CaBundle::isOpensslParseSafe()) {
  58. $info = openssl_x509_parse($certificate, false);
  59. }
  60. if (!isset($info['subject']['commonName'])) {
  61. return null;
  62. }
  63. $commonName = strtolower($info['subject']['commonName']);
  64. $subjectAltNames = array();
  65. if (isset($info['extensions']['subjectAltName'])) {
  66. $subjectAltNames = preg_split('{\s*,\s*}', $info['extensions']['subjectAltName']);
  67. $subjectAltNames = array_filter(array_map(function ($name) {
  68. if (0 === strpos($name, 'DNS:')) {
  69. return strtolower(ltrim(substr($name, 4)));
  70. }
  71. return null;
  72. }, $subjectAltNames));
  73. $subjectAltNames = array_values($subjectAltNames);
  74. }
  75. return array(
  76. 'cn' => $commonName,
  77. 'san' => $subjectAltNames,
  78. );
  79. }
  80. /**
  81. * Get the certificate pin.
  82. *
  83. * By Kevin McArthur of StormTide Digital Studios Inc.
  84. * @KevinSMcArthur / https://github.com/StormTide
  85. *
  86. * See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02
  87. *
  88. * This method was adapted from Sslurp.
  89. * https://github.com/EvanDotPro/Sslurp
  90. *
  91. * (c) Evan Coury <me@evancoury.com>
  92. *
  93. * For the full copyright and license information, please see below:
  94. *
  95. * Copyright (c) 2013, Evan Coury
  96. * All rights reserved.
  97. *
  98. * Redistribution and use in source and binary forms, with or without modification,
  99. * are permitted provided that the following conditions are met:
  100. *
  101. * * Redistributions of source code must retain the above copyright notice,
  102. * this list of conditions and the following disclaimer.
  103. *
  104. * * Redistributions in binary form must reproduce the above copyright notice,
  105. * this list of conditions and the following disclaimer in the documentation
  106. * and/or other materials provided with the distribution.
  107. *
  108. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  109. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  110. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  111. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  112. * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  113. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  114. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  115. * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  116. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  117. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  118. */
  119. public static function getCertificateFingerprint($certificate)
  120. {
  121. $pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate));
  122. $pubkeypem = $pubkeydetails['key'];
  123. //Convert PEM to DER before SHA1'ing
  124. $start = '-----BEGIN PUBLIC KEY-----';
  125. $end = '-----END PUBLIC KEY-----';
  126. $pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1));
  127. $der = base64_decode($pemtrim);
  128. return sha1($der);
  129. }
  130. /**
  131. * Test if it is safe to use the PHP function openssl_x509_parse().
  132. *
  133. * This checks if OpenSSL extensions is vulnerable to remote code execution
  134. * via the exploit documented as CVE-2013-6420.
  135. *
  136. * @return bool
  137. */
  138. public static function isOpensslParseSafe()
  139. {
  140. return CaBundle::isOpensslParseSafe();
  141. }
  142. /**
  143. * Convert certificate name into matching function.
  144. *
  145. * @param string $certName CN/SAN
  146. *
  147. * @return callable|null
  148. */
  149. private static function certNameMatcher($certName)
  150. {
  151. $wildcards = substr_count($certName, '*');
  152. if (0 === $wildcards) {
  153. // Literal match.
  154. return function ($hostname) use ($certName) {
  155. return $hostname === $certName;
  156. };
  157. }
  158. if (1 === $wildcards) {
  159. $components = explode('.', $certName);
  160. if (3 > count($components)) {
  161. // Must have 3+ components
  162. return;
  163. }
  164. $firstComponent = $components[0];
  165. // Wildcard must be the last character.
  166. if ('*' !== $firstComponent[strlen($firstComponent) - 1]) {
  167. return;
  168. }
  169. $wildcardRegex = preg_quote($certName);
  170. $wildcardRegex = str_replace('\\*', '[a-z0-9-]+', $wildcardRegex);
  171. $wildcardRegex = "{^{$wildcardRegex}$}";
  172. return function ($hostname) use ($wildcardRegex) {
  173. return 1 === preg_match($wildcardRegex, $hostname);
  174. };
  175. }
  176. }
  177. }