TlsHelper.php 6.5 KB

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