sspi.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. """
  2. Helper classes for SSPI authentication via the win32security module.
  3. SSPI authentication involves a token-exchange "dance", the exact details
  4. of which depends on the authentication provider used. There are also
  5. a number of complex flags and constants that need to be used - in most
  6. cases, there are reasonable defaults.
  7. These classes attempt to hide these details from you until you really need
  8. to know. They are not designed to handle all cases, just the common ones.
  9. If you need finer control than offered here, just use the win32security
  10. functions directly.
  11. """
  12. # Based on Roger Upole's sspi demos.
  13. # $Id$
  14. import win32security, sspicon
  15. error = win32security.error
  16. class _BaseAuth(object):
  17. def __init__(self):
  18. self.reset()
  19. def reset(self):
  20. """Reset everything to an unauthorized state"""
  21. self.ctxt = None
  22. self.authenticated = False
  23. # The next seq_num for an encrypt/sign operation
  24. self.next_seq_num = 0
  25. def _get_next_seq_num(self):
  26. """Get the next sequence number for a transmission. Default
  27. implementation is to increment a counter
  28. """
  29. ret = self.next_seq_num
  30. self.next_seq_num = self.next_seq_num + 1
  31. return ret
  32. def encrypt(self, data):
  33. """Encrypt a string, returning a tuple of (encrypted_data, encryption_data).
  34. These can be passed to decrypt to get back the original string.
  35. """
  36. pkg_size_info=self.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES)
  37. trailersize=pkg_size_info['SecurityTrailer']
  38. encbuf=win32security.PySecBufferDescType()
  39. encbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA))
  40. encbuf.append(win32security.PySecBufferType(trailersize, sspicon.SECBUFFER_TOKEN))
  41. encbuf[0].Buffer=data
  42. self.ctxt.EncryptMessage(0,encbuf,self._get_next_seq_num())
  43. return encbuf[0].Buffer, encbuf[1].Buffer
  44. def decrypt(self, data, trailer):
  45. """Decrypt a previously encrypted string, returning the orignal data"""
  46. encbuf=win32security.PySecBufferDescType()
  47. encbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA))
  48. encbuf.append(win32security.PySecBufferType(len(trailer), sspicon.SECBUFFER_TOKEN))
  49. encbuf[0].Buffer=data
  50. encbuf[1].Buffer=trailer
  51. self.ctxt.DecryptMessage(encbuf,self._get_next_seq_num())
  52. return encbuf[0].Buffer
  53. def sign(self, data):
  54. """sign a string suitable for transmission, returning the signature.
  55. Passing the data and signature to verify will determine if the data
  56. is unchanged.
  57. """
  58. pkg_size_info=self.ctxt.QueryContextAttributes(sspicon.SECPKG_ATTR_SIZES)
  59. sigsize=pkg_size_info['MaxSignature']
  60. sigbuf=win32security.PySecBufferDescType()
  61. sigbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA))
  62. sigbuf.append(win32security.PySecBufferType(sigsize, sspicon.SECBUFFER_TOKEN))
  63. sigbuf[0].Buffer=data
  64. self.ctxt.MakeSignature(0,sigbuf,self._get_next_seq_num())
  65. return sigbuf[1].Buffer
  66. def verify(self, data, sig):
  67. """Verifies data and its signature. If verification fails, an sspi.error
  68. will be raised.
  69. """
  70. sigbuf=win32security.PySecBufferDescType()
  71. sigbuf.append(win32security.PySecBufferType(len(data), sspicon.SECBUFFER_DATA))
  72. sigbuf.append(win32security.PySecBufferType(len(sig), sspicon.SECBUFFER_TOKEN))
  73. sigbuf[0].Buffer=data
  74. sigbuf[1].Buffer=sig
  75. self.ctxt.VerifySignature(sigbuf,self._get_next_seq_num())
  76. class ClientAuth(_BaseAuth):
  77. """Manages the client side of an SSPI authentication handshake
  78. """
  79. def __init__(self,
  80. pkg_name, # Name of the package to used.
  81. client_name = None, # User for whom credentials are used.
  82. auth_info = None, # or a tuple of (username, domain, password)
  83. targetspn = None, # Target security context provider name.
  84. scflags=None, # security context flags
  85. datarep=sspicon.SECURITY_NETWORK_DREP):
  86. if scflags is None:
  87. scflags = sspicon.ISC_REQ_INTEGRITY|sspicon.ISC_REQ_SEQUENCE_DETECT|\
  88. sspicon.ISC_REQ_REPLAY_DETECT|sspicon.ISC_REQ_CONFIDENTIALITY
  89. self.scflags=scflags
  90. self.datarep=datarep
  91. self.targetspn=targetspn
  92. self.pkg_info=win32security.QuerySecurityPackageInfo(pkg_name)
  93. self.credentials, \
  94. self.credentials_expiry=win32security.AcquireCredentialsHandle(
  95. client_name, self.pkg_info['Name'],
  96. sspicon.SECPKG_CRED_OUTBOUND,
  97. None, auth_info)
  98. _BaseAuth.__init__(self)
  99. # Perform *one* step of the client authentication process.
  100. def authorize(self, sec_buffer_in):
  101. if sec_buffer_in is not None and type(sec_buffer_in) != win32security.PySecBufferDescType:
  102. # User passed us the raw data - wrap it into a SecBufferDesc
  103. sec_buffer_new=win32security.PySecBufferDescType()
  104. tokenbuf=win32security.PySecBufferType(self.pkg_info['MaxToken'],
  105. sspicon.SECBUFFER_TOKEN)
  106. tokenbuf.Buffer=sec_buffer_in
  107. sec_buffer_new.append(tokenbuf)
  108. sec_buffer_in = sec_buffer_new
  109. sec_buffer_out=win32security.PySecBufferDescType()
  110. tokenbuf=win32security.PySecBufferType(self.pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN)
  111. sec_buffer_out.append(tokenbuf)
  112. ## input context handle should be NULL on first call
  113. ctxtin=self.ctxt
  114. if self.ctxt is None:
  115. self.ctxt=win32security.PyCtxtHandleType()
  116. err, attr, exp=win32security.InitializeSecurityContext(
  117. self.credentials,
  118. ctxtin,
  119. self.targetspn,
  120. self.scflags,
  121. self.datarep,
  122. sec_buffer_in,
  123. self.ctxt,
  124. sec_buffer_out)
  125. # Stash these away incase someone needs to know the state from the
  126. # final call.
  127. self.ctxt_attr = attr
  128. self.ctxt_expiry = exp
  129. if err in (sspicon.SEC_I_COMPLETE_NEEDED,sspicon.SEC_I_COMPLETE_AND_CONTINUE):
  130. self.ctxt.CompleteAuthToken(sec_buffer_out)
  131. self.authenticated = err == 0
  132. return err, sec_buffer_out
  133. class ServerAuth(_BaseAuth):
  134. """Manages the server side of an SSPI authentication handshake
  135. """
  136. def __init__(self,
  137. pkg_name,
  138. spn = None,
  139. scflags=None,
  140. datarep=sspicon.SECURITY_NETWORK_DREP):
  141. self.spn=spn
  142. self.datarep=datarep
  143. if scflags is None:
  144. scflags = sspicon.ASC_REQ_INTEGRITY|sspicon.ASC_REQ_SEQUENCE_DETECT|\
  145. sspicon.ASC_REQ_REPLAY_DETECT|sspicon.ASC_REQ_CONFIDENTIALITY
  146. # Should we default to sspicon.KerbAddExtraCredentialsMessage
  147. # if pkg_name=='Kerberos'?
  148. self.scflags=scflags
  149. self.pkg_info=win32security.QuerySecurityPackageInfo(pkg_name)
  150. self.credentials, \
  151. self.credentials_expiry=win32security.AcquireCredentialsHandle(spn,
  152. self.pkg_info['Name'], sspicon.SECPKG_CRED_INBOUND, None, None)
  153. _BaseAuth.__init__(self)
  154. # Perform *one* step of the server authentication process.
  155. def authorize(self, sec_buffer_in):
  156. if sec_buffer_in is not None and type(sec_buffer_in) != win32security.PySecBufferDescType:
  157. # User passed us the raw data - wrap it into a SecBufferDesc
  158. sec_buffer_new=win32security.PySecBufferDescType()
  159. tokenbuf=win32security.PySecBufferType(self.pkg_info['MaxToken'],
  160. sspicon.SECBUFFER_TOKEN)
  161. tokenbuf.Buffer=sec_buffer_in
  162. sec_buffer_new.append(tokenbuf)
  163. sec_buffer_in = sec_buffer_new
  164. sec_buffer_out=win32security.PySecBufferDescType()
  165. tokenbuf=win32security.PySecBufferType(self.pkg_info['MaxToken'], sspicon.SECBUFFER_TOKEN)
  166. sec_buffer_out.append(tokenbuf)
  167. ## input context handle is None initially, then handle returned from last call thereafter
  168. ctxtin=self.ctxt
  169. if self.ctxt is None:
  170. self.ctxt=win32security.PyCtxtHandleType()
  171. err, attr, exp = win32security.AcceptSecurityContext(self.credentials, ctxtin,
  172. sec_buffer_in, self.scflags,
  173. self.datarep, self.ctxt, sec_buffer_out)
  174. # Stash these away incase someone needs to know the state from the
  175. # final call.
  176. self.ctxt_attr = attr
  177. self.ctxt_expiry = exp
  178. if err in (sspicon.SEC_I_COMPLETE_NEEDED,sspicon.SEC_I_COMPLETE_AND_CONTINUE):
  179. self.ctxt.CompleteAuthToken(sec_buffer_out)
  180. self.authenticated = err == 0
  181. return err, sec_buffer_out
  182. if __name__=='__main__':
  183. # Setup the 2 contexts.
  184. sspiclient=ClientAuth("NTLM")
  185. sspiserver=ServerAuth("NTLM")
  186. # Perform the authentication dance, each loop exchanging more information
  187. # on the way to completing authentication.
  188. sec_buffer=None
  189. while 1:
  190. err, sec_buffer = sspiclient.authorize(sec_buffer)
  191. err, sec_buffer = sspiserver.authorize(sec_buffer)
  192. if err==0:
  193. break
  194. data = "hello".encode("ascii") # py3k-friendly
  195. sig = sspiclient.sign(data)
  196. sspiserver.verify(data, sig)
  197. data, key = sspiclient.encrypt(data)
  198. assert sspiserver.decrypt(data, key) == data
  199. print "cool!"