README.rst 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. ======
  2. Scramp
  3. ======
  4. A Python implementation of the `SCRAM authentication protocol
  5. <https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism>`_.
  6. Scramp supports the following mechanisms:
  7. - SCRAM-SHA-1
  8. - SCRAM-SHA-1-PLUS
  9. - SCRAM-SHA-256
  10. - SCRAM-SHA-256-PLUS
  11. - SCRAM-SHA-512
  12. - SCRAM-SHA-512-PLUS
  13. - SCRAM-SHA3-512
  14. - SCRAM-SHA3-512-PLUS
  15. .. contents:: Table of Contents
  16. :depth: 2
  17. :local:
  18. Installation
  19. ------------
  20. - Create a virtual environment: ``python3 -m venv venv``
  21. - Activate the virtual environment: ``source venv/bin/activate``
  22. - Install: ``pip install scramp``
  23. Examples
  24. --------
  25. Client and Server
  26. `````````````````
  27. Here's an example using both the client and the server. It's a bit contrived as normally
  28. you'd be using either the client or server on its own.
  29. >>> from scramp import ScramClient, ScramMechanism
  30. >>>
  31. >>> USERNAME = 'user'
  32. >>> PASSWORD = 'pencil'
  33. >>> MECHANISMS = ['SCRAM-SHA-256']
  34. >>>
  35. >>>
  36. >>> # Choose a mechanism for our server
  37. >>> m = ScramMechanism() # Default is SCRAM-SHA-256
  38. >>>
  39. >>> # On the server side we create the authentication information for each user
  40. >>> # and store it in an authentication database. We'll use a dict:
  41. >>> db = {}
  42. >>>
  43. >>> salt, stored_key, server_key, iteration_count = m.make_auth_info(PASSWORD)
  44. >>>
  45. >>> db[USERNAME] = salt, stored_key, server_key, iteration_count
  46. >>>
  47. >>> # Define your own function for retrieving the authentication information
  48. >>> # from the database given a username
  49. >>>
  50. >>> def auth_fn(username):
  51. ... return db[username]
  52. >>>
  53. >>> # Make the SCRAM server
  54. >>> s = m.make_server(auth_fn)
  55. >>>
  56. >>> # Now set up the client and carry out authentication with the server
  57. >>> c = ScramClient(MECHANISMS, USERNAME, PASSWORD)
  58. >>> cfirst = c.get_client_first()
  59. >>>
  60. >>> s.set_client_first(cfirst)
  61. >>> sfirst = s.get_server_first()
  62. >>>
  63. >>> c.set_server_first(sfirst)
  64. >>> cfinal = c.get_client_final()
  65. >>>
  66. >>> s.set_client_final(cfinal)
  67. >>> sfinal = s.get_server_final()
  68. >>>
  69. >>> c.set_server_final(sfinal)
  70. >>>
  71. >>> # If it all runs through without raising an exception, the authentication
  72. >>> # has succeeded
  73. Client only
  74. ```````````
  75. Here's an example using just the client. The client nonce is specified in order to give
  76. a reproducible example, but in production you'd omit the ``c_nonce`` parameter and let
  77. ``ScramClient`` generate a client nonce:
  78. >>> from scramp import ScramClient
  79. >>>
  80. >>> USERNAME = 'user'
  81. >>> PASSWORD = 'pencil'
  82. >>> C_NONCE = 'rOprNGfwEbeRWgbNEkqO'
  83. >>> MECHANISMS = ['SCRAM-SHA-256']
  84. >>>
  85. >>> # Normally the c_nonce would be omitted, in which case ScramClient will
  86. >>> # generate the nonce itself.
  87. >>>
  88. >>> c = ScramClient(MECHANISMS, USERNAME, PASSWORD, c_nonce=C_NONCE)
  89. >>>
  90. >>> # Get the client first message and send it to the server
  91. >>> cfirst = c.get_client_first()
  92. >>> print(cfirst)
  93. n,,n=user,r=rOprNGfwEbeRWgbNEkqO
  94. >>>
  95. >>> # Set the first message from the server
  96. >>> c.set_server_first(
  97. ... 'r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
  98. ... 's=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096')
  99. >>>
  100. >>> # Get the client final message and send it to the server
  101. >>> cfinal = c.get_client_final()
  102. >>> print(cfinal)
  103. c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=
  104. >>>
  105. >>> # Set the final message from the server
  106. >>> c.set_server_final('v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=')
  107. >>>
  108. >>> # If it all runs through without raising an exception, the authentication
  109. >>> # has succeeded
  110. Server only
  111. ```````````
  112. Here's an example using just the server. The server nonce and salt is specified in order
  113. to give a reproducible example, but in production you'd omit the ``s_nonce`` and
  114. ``salt`` parameters and let Scramp generate them:
  115. >>> from scramp import ScramMechanism
  116. >>>
  117. >>> USERNAME = 'user'
  118. >>> PASSWORD = 'pencil'
  119. >>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
  120. >>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
  121. >>>
  122. >>> db = {}
  123. >>>
  124. >>> m = ScramMechanism()
  125. >>>
  126. >>> salt, stored_key, server_key, iteration_count = m.make_auth_info(
  127. ... PASSWORD, salt=SALT)
  128. >>>
  129. >>> db[USERNAME] = salt, stored_key, server_key, iteration_count
  130. >>>
  131. >>> # Define your own function for getting a password given a username
  132. >>> def auth_fn(username):
  133. ... return db[username]
  134. >>>
  135. >>> # Normally the s_nonce parameter would be omitted, in which case the
  136. >>> # server will generate the nonce itself.
  137. >>>
  138. >>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
  139. >>>
  140. >>> # Set the first message from the client
  141. >>> s.set_client_first('n,,n=user,r=rOprNGfwEbeRWgbNEkqO')
  142. >>>
  143. >>> # Get the first server message, and send it to the client
  144. >>> sfirst = s.get_server_first()
  145. >>> print(sfirst)
  146. r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
  147. >>>
  148. >>> # Set the final message from the client
  149. >>> s.set_client_final(
  150. ... 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
  151. ... 'p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=')
  152. >>>
  153. >>> # Get the final server message and send it to the client
  154. >>> sfinal = s.get_server_final()
  155. >>> print(sfinal)
  156. v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
  157. >>>
  158. >>> # If it all runs through without raising an exception, the authentication
  159. >>> # has succeeded
  160. Server only with passlib
  161. ````````````````````````
  162. Here's an example using just the server and using the `passlib hashing library
  163. <https://passlib.readthedocs.io/en/stable/index.html>`_. The server nonce and salt is
  164. specified in order to give a reproducible example, but in production you'd omit the
  165. ``s_nonce`` and ``salt`` parameters and let Scramp generate them:
  166. >>> from scramp import ScramMechanism
  167. >>> from passlib.hash import scram
  168. >>>
  169. >>> USERNAME = 'user'
  170. >>> PASSWORD = 'pencil'
  171. >>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
  172. >>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
  173. >>> ITERATION_COUNT = 4096
  174. >>>
  175. >>> db = {}
  176. >>> hash = scram.using(salt=SALT, rounds=ITERATION_COUNT).hash(PASSWORD)
  177. >>>
  178. >>> salt, iteration_count, digest = scram.extract_digest_info(hash, 'sha-256')
  179. >>>
  180. >>> stored_key, server_key = m.make_stored_server_keys(digest)
  181. >>>
  182. >>> db[USERNAME] = salt, stored_key, server_key, iteration_count
  183. >>>
  184. >>> # Define your own function for getting a password given a username
  185. >>> def auth_fn(username):
  186. ... return db[username]
  187. >>>
  188. >>> # Normally the s_nonce parameter would be omitted, in which case the
  189. >>> # server will generate the nonce itself.
  190. >>>
  191. >>> m = ScramMechanism()
  192. >>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
  193. >>>
  194. >>> # Set the first message from the client
  195. >>> s.set_client_first('n,,n=user,r=rOprNGfwEbeRWgbNEkqO')
  196. >>>
  197. >>> # Get the first server message, and send it to the client
  198. >>> sfirst = s.get_server_first()
  199. >>> print(sfirst)
  200. r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
  201. >>>
  202. >>> # Set the final message from the client
  203. >>> s.set_client_final(
  204. ... 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
  205. ... 'p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=')
  206. >>>
  207. >>> # Get the final server message and send it to the client
  208. >>> sfinal = s.get_server_final()
  209. >>> print(sfinal)
  210. v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
  211. >>>
  212. >>> # If it all runs through without raising an exception, the authentication
  213. >>> # has succeeded
  214. Server Error
  215. ````````````
  216. Here's an example of when setting a message from the client causes an error. The server
  217. nonce and salt is specified in order to give a reproducible example, but in production
  218. you'd omit the ``s_nonce`` and ``salt`` parameters and let Scramp generate them:
  219. >>> from scramp import ScramException, ScramMechanism
  220. >>>
  221. >>> USERNAME = 'user'
  222. >>> PASSWORD = 'pencil'
  223. >>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
  224. >>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
  225. >>>
  226. >>> db = {}
  227. >>>
  228. >>> m = ScramMechanism()
  229. >>>
  230. >>> salt, stored_key, server_key, iteration_count = m.make_auth_info(
  231. ... PASSWORD, salt=SALT)
  232. >>>
  233. >>> db[USERNAME] = salt, stored_key, server_key, iteration_count
  234. >>>
  235. >>> # Define your own function for getting a password given a username
  236. >>> def auth_fn(username):
  237. ... return db[username]
  238. >>>
  239. >>> # Normally the s_nonce parameter would be omitted, in which case the
  240. >>> # server will generate the nonce itself.
  241. >>>
  242. >>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
  243. >>>
  244. >>> try:
  245. ... # Set the first message from the client
  246. ... s.set_client_first('p=tls-unique,,n=user,r=rOprNGfwEbeRWgbNEkqO')
  247. ... except ScramException as e:
  248. ... print(e)
  249. ... # Get the final server message and send it to the client
  250. ... sfinal = s.get_server_final()
  251. ... print(sfinal)
  252. Received GS2 flag 'p' which indicates that the client requires channel binding, but the server does not: channel-binding-not-supported
  253. e=channel-binding-not-supported
  254. Standards
  255. ---------
  256. `RFC 5802 <https://tools.ietf.org/html/rfc5802>`_
  257. Describes SCRAM.
  258. `RFC 7677 <https://datatracker.ietf.org/doc/html/rfc7677>`_
  259. Registers SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
  260. `draft-melnikov-scram-sha-512-02 <https://datatracker.ietf.org/doc/html/draft-melnikov-scram-sha-512>`_
  261. Registers SCRAM-SHA-512 and SCRAM-SHA-512-PLUS.
  262. `draft-melnikov-scram-sha3-512 <https://datatracker.ietf.org/doc/html/draft-melnikov-scram-sha3-512>`_
  263. Registers SCRAM-SHA3-512 and SCRAM-SHA3-512-PLUS.
  264. `RFC 5929 <https://datatracker.ietf.org/doc/html/rfc5929>`_
  265. Channel Bindings for TLS.
  266. `draft-ietf-kitten-tls-channel-bindings-for-tls13 <https://datatracker.ietf.org/doc/html/draft-ietf-kitten-tls-channel-bindings-for-tls13>`_
  267. Defines the ``tls-exporter`` channel binding, which is `not yet supported by Scramp
  268. <https://github.com/tlocke/scramp/issues/9>`_.
  269. API Docs
  270. --------
  271. scramp.MECHANISMS
  272. `````````````````
  273. A tuple of the supported mechanism names.
  274. scramp.ScramClient
  275. ``````````````````
  276. ``ScramClient(mechanisms, username, password, channel_binding=None, c_nonce=None)``
  277. Constructor of the ``ScramClient`` class, with the following parameters:
  278. ``mechanisms``
  279. A list or tuple of mechanism names. ScramClient will choose the most secure. If
  280. ``cbind_data`` is ``None``, the '-PLUS' variants will be filtered out first. The
  281. chosen mechanism is available as the property ``mechanism_name``.
  282. ``username``
  283. ``password``
  284. ``channel_binding``
  285. Providing a value for this parameter allows channel binding to be used (ie. it lets
  286. you use mechanisms ending in '-PLUS'). The value for ``channel_binding`` is a tuple
  287. consisting of the channel binding name and the channel binding data. For example, if
  288. the channel binding name is ``tls-unique``, the ``channel_binding`` parameter would
  289. be ``('tls-unique', data)``, where ``data`` is obtained by calling
  290. `SSLSocket.get_channel_binding()
  291. <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding>`_.
  292. The convenience function ``scramp.make_channel_binding()`` can be used to create a
  293. channel binding tuple.
  294. ``c_nonce``
  295. The client nonce. It's sometimes useful to set this when testing / debugging, but in
  296. production this should be omitted, in which case ``ScramClient`` will generate a
  297. client nonce.
  298. The ``ScramClient`` object has the following methods and properties:
  299. ``get_client_first()``
  300. Get the client first message.
  301. ``set_server_first(message)``
  302. Set the first message from the server.
  303. ``get_client_final()``
  304. Get the final client message.
  305. ``set_server_final(message)``
  306. Set the final message from the server.
  307. ``mechanism_name``
  308. The mechanism chosen from the list given in the constructor.
  309. scramp.ScramMechanism
  310. `````````````````````
  311. ``ScramMechanism(mechanism='SCRAM-SHA-256')``
  312. Constructor of the ``ScramMechanism`` class, with the following parameter:
  313. ``mechanism``
  314. The SCRAM mechanism to use.
  315. The ``ScramMechanism`` object has the following methods and properties:
  316. ``make_auth_info(password, iteration_count=None, salt=None)``
  317. returns the tuple ``(salt, stored_key, server_key, iteration_count)`` which is stored
  318. in the authentication database on the server side. It has the following parameters:
  319. ``password``
  320. The user's password as a ``str``.
  321. ``iteration_count``
  322. The rounds as an ``int``. If ``None`` then use the minimum associated with the
  323. mechanism.
  324. ``salt``
  325. It's sometimes useful to set this binary parameter when testing / debugging, but in
  326. production this should be omitted, in which case a salt will be generated.
  327. ``make_server(auth_fn, channel_binding=None, s_nonce=None)``
  328. returns a ``ScramServer`` object. It takes the following parameters:
  329. ``auth_fn``
  330. This is a function provided by the programmer that has one parameter, a username of
  331. type ``str`` and returns returns the tuple ``(salt, stored_key, server_key,
  332. iteration_count)``. Where ``salt``, ``stored_key`` and ``server_key`` are of a
  333. binary type, and ``iteration_count`` is an ``int``.
  334. ``channel_binding``
  335. Providing a value for this parameter allows channel binding to be used (ie. it lets
  336. you use mechanisms ending in ``-PLUS``). The value for ``channel_binding`` is a
  337. tuple consisting of the channel binding name and the channel binding data. For
  338. example, if the channel binding name is 'tls-unique', the ``channel_binding``
  339. parameter would be ``('tls-unique', data)``, where ``data`` is obtained by calling
  340. `SSLSocket.get_channel_binding()
  341. <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding>`_.
  342. The convenience function ``scramp.make_channel_binding()`` can be used to create a
  343. channel binding tuple. If ``channel_binding`` is provided and the mechanism isn't a
  344. ``-PLUS`` variant, then the server will negotiate with the client to use the
  345. ``-PLUS`` variant if the client supports it, or otherwise to use the mechanism
  346. without channel binding.
  347. ``s_nonce``
  348. The server nonce as a ``str``. It's sometimes useful to set this when testing /
  349. debugging, but in production this should be omitted, in which case ``ScramServer``
  350. will generate a server nonce.
  351. ``make_stored_server_keys(salted_password)``
  352. returns ``(stored_key, server_key)`` tuple of ``bytes`` objects given a salted
  353. password. This is useful if you want to use a separate hashing implementation from
  354. the one provided by Scramp. It takes the following parameter:
  355. ``salted_password``
  356. A binary object representing the hashed password.
  357. ``iteration_count``
  358. The minimum iteration count recommended for this mechanism.
  359. scramp.ScramServer
  360. ``````````````````
  361. The ``ScramServer`` object has the following methods:
  362. ``set_client_first(message)``
  363. Set the first message from the client.
  364. ``get_server_first()``
  365. Get the server first message.
  366. ``set_client_final(message)``
  367. Set the final client message.
  368. ``get_server_final()``
  369. Get the server final message.
  370. scramp.make_channel_binding()
  371. `````````````````````````````
  372. ``make_channel_binding(name, ssl_socket)``
  373. A helper function that makes a ``channel_binding`` tuple when given a channel binding
  374. name and an SSL socket. The parameters are:
  375. ``name``
  376. A channel binding name such as 'tls-unique' or 'tls-server-end-point'.
  377. ``ssl_socket``
  378. An instance of `ssl.SSLSocket
  379. <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket>`_.
  380. README.rst
  381. ----------
  382. This file is written in the `reStructuredText
  383. <https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_ format. To generate an
  384. HTML page from it, do:
  385. - Activate the virtual environment: ``source venv/bin/activate``
  386. - Install ``Sphinx``: ``pip install Sphinx``
  387. - Run ``rst2html.py``: ``rst2html.py README.rst README.html``
  388. Testing
  389. -------
  390. - Activate the virtual environment: ``source venv/bin/activate``
  391. - Install ``tox``: ``pip install tox``
  392. - Run ``tox``: ``tox``
  393. Doing A Release Of Scramp
  394. -------------------------
  395. Run ``tox`` to make sure all tests pass, then update the release notes, then do::
  396. git tag -a x.y.z -m "version x.y.z"
  397. rm -r dist
  398. python -m build
  399. twine upload --sign dist/*
  400. Release Notes
  401. -------------
  402. Version 1.4.4, 2022-11-01
  403. `````````````````````````
  404. - Tighten up parsing of messages to make sure that a ``ScramException`` is raised if a
  405. message is malformed.
  406. Version 1.4.3, 2022-10-26
  407. `````````````````````````
  408. - The client now sends a gs2-cbind-flag of 'y' if the client supports channel
  409. binding, but thinks the server does not.
  410. Version 1.4.2, 2022-10-22
  411. `````````````````````````
  412. - Switch to using the MIT-0 licence https://choosealicense.com/licenses/mit-0/
  413. - When creating a ScramClient, allow non ``-PLUS`` variants, even if a
  414. ``channel_binding`` parameter is provided. Previously this would raise and
  415. exception.
  416. Version 1.4.1, 2021-08-25
  417. `````````````````````````
  418. - When using ``make_channel_binding()`` to create a tls-server-end-point channel
  419. binding, support certificates with hash algorithm of sha512.
  420. Version 1.4.0, 2021-03-28
  421. `````````````````````````
  422. - Raise an exception if the client receives an error from the server.
  423. Version 1.3.0, 2021-03-28
  424. `````````````````````````
  425. - As the specification allows, server errors are now sent to the client in the
  426. ``server_final`` message, an exception is still thrown as before.
  427. Version 1.2.2, 2021-02-13
  428. `````````````````````````
  429. - Fix bug in generating the AuthMessage. It was incorrect when channel binding
  430. was used. So now Scramp supports channel binding.
  431. Version 1.2.1, 2021-02-07
  432. `````````````````````````
  433. - Add support for channel binding.
  434. - Add support for SCRAM-SHA-512 and SCRAM-SHA3-512 and their channel binding
  435. variants.
  436. Version 1.2.0, 2020-05-30
  437. `````````````````````````
  438. - This is a backwardly incompatible change on the server side, the client side will
  439. work as before. The idea of this change is to make it possible to have an
  440. authentication database. That is, the authentication information can be stored, and
  441. then retrieved when needed to authenticate the user.
  442. - In addition, it's now possible on the server side to use a third party hashing library
  443. such as passlib as the hashing implementation.
  444. Version 1.1.1, 2020-03-28
  445. `````````````````````````
  446. - Add the README and LICENCE to the distribution.
  447. Version 1.1.0, 2019-02-24
  448. `````````````````````````
  449. - Add support for the SCRAM-SHA-1 mechanism.
  450. Version 1.0.0, 2019-02-17
  451. `````````````````````````
  452. - Implement the server side as well as the client side.
  453. Version 0.0.0, 2019-02-10
  454. `````````````````````````
  455. - Copied SCRAM implementation from `pg8000 <https://github.com/tlocke/pg8000>`_. The
  456. idea is to make it a general SCRAM implemtation. Credit to the `Scrampy
  457. <https://github.com/cagdass/scrampy>`_ project which I read through to help with this
  458. project. Also credit to the `passlib <https://github.com/efficks/passlib>`_ project
  459. from which I copied the ``saslprep`` function.