PageRenderTime 180ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/M2Crypto/SSL/Checker.py

https://gitlab.com/t0b3/m2crypto
Python | 297 lines | 257 code | 14 blank | 26 comment | 3 complexity | 6f123bb3dc142faa3d9b1af08677a26e MD5 | raw file
  1. """
  2. SSL peer certificate checking routines
  3. Copyright (c) 2004-2007 Open Source Applications Foundation.
  4. All rights reserved.
  5. Copyright 2008 Heikki Toivonen. All rights reserved.
  6. """
  7. __all__ = ['SSLVerificationError', 'NoCertificate', 'WrongCertificate',
  8. 'WrongHost', 'Checker']
  9. import re
  10. import socket
  11. from M2Crypto import X509, m2, util # noqa
  12. if util.py27plus:
  13. from typing import AnyStr, Optional # noqa
  14. class SSLVerificationError(Exception):
  15. pass
  16. class NoCertificate(SSLVerificationError):
  17. pass
  18. class WrongCertificate(SSLVerificationError):
  19. pass
  20. class WrongHost(SSLVerificationError):
  21. def __init__(self, expectedHost, actualHost, fieldName='commonName'):
  22. # type: (str, AnyStr, str) -> None
  23. """
  24. This exception will be raised if the certificate returned by the
  25. peer was issued for a different host than we tried to connect to.
  26. This could be due to a server misconfiguration or an active attack.
  27. @param expectedHost: The name of the host we expected to find in the
  28. certificate.
  29. @param actualHost: The name of the host we actually found in the
  30. certificate.
  31. @param fieldName: The field name where we noticed the error. This
  32. should be either 'commonName' or 'subjectAltName'.
  33. """
  34. if fieldName not in ('commonName', 'subjectAltName'):
  35. raise ValueError(
  36. 'Unknown fieldName, should be either commonName ' +
  37. 'or subjectAltName')
  38. SSLVerificationError.__init__(self)
  39. self.expectedHost = expectedHost
  40. self.actualHost = actualHost
  41. self.fieldName = fieldName
  42. def __str__(self):
  43. # type: () -> str
  44. s = 'Peer certificate %s does not match host, expected %s, got %s' \
  45. % (self.fieldName, self.expectedHost, self.actualHost)
  46. return util.py3str(s)
  47. class Checker:
  48. numericIpMatch = re.compile('^[0-9]+(\.[0-9]+)*$')
  49. def __init__(self, host=None, peerCertHash=None, peerCertDigest='sha1'):
  50. # type: (Optional[str], Optional[bytes], str) -> None
  51. self.host = host
  52. if peerCertHash is not None:
  53. peerCertHash = util.py3bytes(peerCertHash)
  54. self.fingerprint = peerCertHash
  55. self.digest = peerCertDigest # type: str
  56. def __call__(self, peerCert, host=None):
  57. # type: (X509.X509, Optional[str]) -> bool
  58. if peerCert is None:
  59. raise NoCertificate('peer did not return certificate')
  60. if host is not None:
  61. self.host = host # type: str
  62. if self.fingerprint:
  63. if self.digest not in ('sha1', 'md5'):
  64. raise ValueError('unsupported digest "%s"' % (self.digest))
  65. if self.digest == 'sha1':
  66. expected_len = 40
  67. elif self.digest == 'md5':
  68. expected_len = 32
  69. else:
  70. raise ValueError('Unexpected digest {0}'.format(self.digest))
  71. if len(self.fingerprint) != expected_len:
  72. raise WrongCertificate(
  73. ('peer certificate fingerprint length does not match\n' +
  74. 'fingerprint: {0}\nexpected = {1}\n' +
  75. 'observed = {2}').format(self.fingerprint,
  76. expected_len,
  77. len(self.fingerprint)))
  78. expected_fingerprint = util.py3str(self.fingerprint)
  79. observed_fingerprint = peerCert.get_fingerprint(md=self.digest)
  80. if observed_fingerprint != expected_fingerprint:
  81. raise WrongCertificate(
  82. ('peer certificate fingerprint does not match\n' +
  83. 'expected = {0},\n' +
  84. 'observed = {1}').format(expected_fingerprint,
  85. observed_fingerprint))
  86. if self.host:
  87. hostValidationPassed = False
  88. self.useSubjectAltNameOnly = False
  89. # subjectAltName=DNS:somehost[, ...]*
  90. try:
  91. subjectAltName = peerCert.get_ext('subjectAltName').get_value()
  92. if self._splitSubjectAltName(self.host, subjectAltName):
  93. hostValidationPassed = True
  94. elif self.useSubjectAltNameOnly:
  95. raise WrongHost(expectedHost=self.host,
  96. actualHost=subjectAltName,
  97. fieldName='subjectAltName')
  98. except LookupError:
  99. pass
  100. # commonName=somehost[, ...]*
  101. if not hostValidationPassed:
  102. hasCommonName = False
  103. commonNames = ''
  104. for entry in peerCert.get_subject().get_entries_by_nid(
  105. m2.NID_commonName):
  106. hasCommonName = True
  107. commonName = entry.get_data().as_text()
  108. if not commonNames:
  109. commonNames = commonName
  110. else:
  111. commonNames += ',' + commonName
  112. if self._match(self.host, commonName):
  113. hostValidationPassed = True
  114. break
  115. if not hasCommonName:
  116. raise WrongCertificate('no commonName in peer certificate')
  117. if not hostValidationPassed:
  118. raise WrongHost(expectedHost=self.host,
  119. actualHost=commonNames,
  120. fieldName='commonName')
  121. return True
  122. def _splitSubjectAltName(self, host, subjectAltName):
  123. # type: (AnyStr, AnyStr) -> bool
  124. """
  125. >>> check = Checker()
  126. >>> check._splitSubjectAltName(host='my.example.com',
  127. ... subjectAltName='DNS:my.example.com')
  128. True
  129. >>> check._splitSubjectAltName(host='my.example.com',
  130. ... subjectAltName='DNS:*.example.com')
  131. True
  132. >>> check._splitSubjectAltName(host='my.example.com',
  133. ... subjectAltName='DNS:m*.example.com')
  134. True
  135. >>> check._splitSubjectAltName(host='my.example.com',
  136. ... subjectAltName='DNS:m*ample.com')
  137. False
  138. >>> check.useSubjectAltNameOnly
  139. True
  140. >>> check._splitSubjectAltName(host='my.example.com',
  141. ... subjectAltName='DNS:m*ample.com, othername:<unsupported>')
  142. False
  143. >>> check._splitSubjectAltName(host='my.example.com',
  144. ... subjectAltName='DNS:m*ample.com, DNS:my.example.org')
  145. False
  146. >>> check._splitSubjectAltName(host='my.example.com',
  147. ... subjectAltName='DNS:m*ample.com, DNS:my.example.com')
  148. True
  149. >>> check._splitSubjectAltName(host='my.example.com',
  150. ... subjectAltName='DNS:my.example.com, DNS:my.example.org')
  151. True
  152. >>> check.useSubjectAltNameOnly
  153. True
  154. >>> check._splitSubjectAltName(host='my.example.com',
  155. ... subjectAltName='')
  156. False
  157. >>> check._splitSubjectAltName(host='my.example.com',
  158. ... subjectAltName='othername:<unsupported>')
  159. False
  160. >>> check.useSubjectAltNameOnly
  161. False
  162. """
  163. self.useSubjectAltNameOnly = False
  164. for certHost in subjectAltName.split(','):
  165. certHost = certHost.lower().strip()
  166. if certHost[:4] == 'dns:':
  167. self.useSubjectAltNameOnly = True
  168. if self._match(host, certHost[4:]):
  169. return True
  170. elif certHost[:11] == 'ip address:':
  171. self.useSubjectAltNameOnly = True
  172. if self._matchIPAddress(host, certHost[11:]):
  173. return True
  174. return False
  175. def _match(self, host, certHost):
  176. # type: (str, str) -> bool
  177. """
  178. >>> check = Checker()
  179. >>> check._match(host='my.example.com', certHost='my.example.com')
  180. True
  181. >>> check._match(host='my.example.com', certHost='*.example.com')
  182. True
  183. >>> check._match(host='my.example.com', certHost='m*.example.com')
  184. True
  185. >>> check._match(host='my.example.com', certHost='m*.EXAMPLE.com')
  186. True
  187. >>> check._match(host='my.example.com', certHost='m*ample.com')
  188. False
  189. >>> check._match(host='my.example.com', certHost='*.*.com')
  190. False
  191. >>> check._match(host='1.2.3.4', certHost='1.2.3.4')
  192. True
  193. >>> check._match(host='1.2.3.4', certHost='*.2.3.4')
  194. False
  195. >>> check._match(host='1234', certHost='1234')
  196. True
  197. """
  198. # XXX See RFC 2818 and 3280 for matching rules, this is may not
  199. # XXX yet be complete.
  200. host = host.lower()
  201. certHost = certHost.lower()
  202. if host == certHost:
  203. return True
  204. if certHost.count('*') > 1:
  205. # Not sure about this, but being conservative
  206. return False
  207. if self.numericIpMatch.match(host) or \
  208. self.numericIpMatch.match(certHost.replace('*', '')):
  209. # Not sure if * allowed in numeric IP, but think not.
  210. return False
  211. if certHost.find('\\') > -1:
  212. # Not sure about this, maybe some encoding might have these.
  213. # But being conservative for now, because regex below relies
  214. # on this.
  215. return False
  216. # Massage certHost so that it can be used in regex
  217. certHost = certHost.replace('.', '\.')
  218. certHost = certHost.replace('*', '[^\.]*')
  219. if re.compile('^%s$' % (certHost)).match(host):
  220. return True
  221. return False
  222. def _matchIPAddress(self, host, certHost):
  223. # type: (AnyStr, AnyStr) -> bool
  224. """
  225. >>> check = Checker()
  226. >>> check._matchIPAddress(host='my.example.com',
  227. ... certHost='my.example.com')
  228. False
  229. >>> check._matchIPAddress(host='1.2.3.4', certHost='1.2.3.4')
  230. True
  231. >>> check._matchIPAddress(host='1.2.3.4', certHost='*.2.3.4')
  232. False
  233. >>> check._matchIPAddress(host='1.2.3.4', certHost='1.2.3.40')
  234. False
  235. >>> check._matchIPAddress(host='::1', certHost='::1')
  236. True
  237. >>> check._matchIPAddress(host='::1', certHost='0:0:0:0:0:0:0:1')
  238. True
  239. >>> check._matchIPAddress(host='::1', certHost='::2')
  240. False
  241. """
  242. try:
  243. canonical = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM, 0,
  244. socket.AI_NUMERICHOST)
  245. certCanonical = socket.getaddrinfo(certHost, 0, 0,
  246. socket.SOCK_STREAM, 0,
  247. socket.AI_NUMERICHOST)
  248. except:
  249. return False
  250. return canonical == certCanonical
  251. if __name__ == '__main__':
  252. import doctest
  253. doctest.testmod()