TlsHelper.php 6.5 KB

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