summaryrefslogtreecommitdiffstats
path: root/contrib/python/scramp
diff options
context:
space:
mode:
authorvitalyisaev <[email protected]>2023-11-14 09:58:56 +0300
committervitalyisaev <[email protected]>2023-11-14 10:20:20 +0300
commitc2b2dfd9827a400a8495e172a56343462e3ceb82 (patch)
treecd4e4f597d01bede4c82dffeb2d780d0a9046bd0 /contrib/python/scramp
parentd4ae8f119e67808cb0cf776ba6e0cf95296f2df7 (diff)
YQ Connector: move tests from yql to ydb (OSS)
Перенос папки с тестами на Коннектор из папки yql в папку ydb (синхронизируется с github).
Diffstat (limited to 'contrib/python/scramp')
-rw-r--r--contrib/python/scramp/.dist-info/METADATA610
-rw-r--r--contrib/python/scramp/.dist-info/top_level.txt1
-rw-r--r--contrib/python/scramp/LICENSE18
-rw-r--r--contrib/python/scramp/README.rst584
-rw-r--r--contrib/python/scramp/scramp/__init__.py15
-rw-r--r--contrib/python/scramp/scramp/core.py696
-rw-r--r--contrib/python/scramp/scramp/utils.py34
-rw-r--r--contrib/python/scramp/ya.make28
8 files changed, 1986 insertions, 0 deletions
diff --git a/contrib/python/scramp/.dist-info/METADATA b/contrib/python/scramp/.dist-info/METADATA
new file mode 100644
index 00000000000..b2254632735
--- /dev/null
+++ b/contrib/python/scramp/.dist-info/METADATA
@@ -0,0 +1,610 @@
+Metadata-Version: 2.1
+Name: scramp
+Version: 1.4.4
+Summary: An implementation of the SCRAM protocol.
+License: MIT No Attribution
+Project-URL: Homepage, https://github.com/tlocke/scramp
+Keywords: SCRAM,authentication,SASL
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT No Attribution License (MIT-0)
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: Implementation
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Operating System :: OS Independent
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: asn1crypto (>=1.5.1)
+Requires-Dist: importlib-metadata (>=1.0) ; python_version < "3.8"
+
+======
+Scramp
+======
+
+A Python implementation of the `SCRAM authentication protocol
+<https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism>`_.
+Scramp supports the following mechanisms:
+
+- SCRAM-SHA-1
+- SCRAM-SHA-1-PLUS
+- SCRAM-SHA-256
+- SCRAM-SHA-256-PLUS
+- SCRAM-SHA-512
+- SCRAM-SHA-512-PLUS
+- SCRAM-SHA3-512
+- SCRAM-SHA3-512-PLUS
+
+.. contents:: Table of Contents
+ :depth: 2
+ :local:
+
+Installation
+------------
+
+- Create a virtual environment: ``python3 -m venv venv``
+- Activate the virtual environment: ``source venv/bin/activate``
+- Install: ``pip install scramp``
+
+
+Examples
+--------
+
+Client and Server
+`````````````````
+
+Here's an example using both the client and the server. It's a bit contrived as normally
+you'd be using either the client or server on its own.
+
+>>> from scramp import ScramClient, ScramMechanism
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> MECHANISMS = ['SCRAM-SHA-256']
+>>>
+>>>
+>>> # Choose a mechanism for our server
+>>> m = ScramMechanism() # Default is SCRAM-SHA-256
+>>>
+>>> # On the server side we create the authentication information for each user
+>>> # and store it in an authentication database. We'll use a dict:
+>>> db = {}
+>>>
+>>> salt, stored_key, server_key, iteration_count = m.make_auth_info(PASSWORD)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for retrieving the authentication information
+>>> # from the database given a username
+>>>
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Make the SCRAM server
+>>> s = m.make_server(auth_fn)
+>>>
+>>> # Now set up the client and carry out authentication with the server
+>>> c = ScramClient(MECHANISMS, USERNAME, PASSWORD)
+>>> cfirst = c.get_client_first()
+>>>
+>>> s.set_client_first(cfirst)
+>>> sfirst = s.get_server_first()
+>>>
+>>> c.set_server_first(sfirst)
+>>> cfinal = c.get_client_final()
+>>>
+>>> s.set_client_final(cfinal)
+>>> sfinal = s.get_server_final()
+>>>
+>>> c.set_server_final(sfinal)
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Client only
+```````````
+
+Here's an example using just the client. The client nonce is specified in order to give
+a reproducible example, but in production you'd omit the ``c_nonce`` parameter and let
+``ScramClient`` generate a client nonce:
+
+>>> from scramp import ScramClient
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> C_NONCE = 'rOprNGfwEbeRWgbNEkqO'
+>>> MECHANISMS = ['SCRAM-SHA-256']
+>>>
+>>> # Normally the c_nonce would be omitted, in which case ScramClient will
+>>> # generate the nonce itself.
+>>>
+>>> c = ScramClient(MECHANISMS, USERNAME, PASSWORD, c_nonce=C_NONCE)
+>>>
+>>> # Get the client first message and send it to the server
+>>> cfirst = c.get_client_first()
+>>> print(cfirst)
+n,,n=user,r=rOprNGfwEbeRWgbNEkqO
+>>>
+>>> # Set the first message from the server
+>>> c.set_server_first(
+... 'r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
+... 's=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096')
+>>>
+>>> # Get the client final message and send it to the server
+>>> cfinal = c.get_client_final()
+>>> print(cfinal)
+c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=
+>>>
+>>> # Set the final message from the server
+>>> c.set_server_final('v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=')
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Server only
+```````````
+
+Here's an example using just the server. The server nonce and salt is specified in order
+to give a reproducible example, but in production you'd omit the ``s_nonce`` and
+``salt`` parameters and let Scramp generate them:
+
+>>> from scramp import ScramMechanism
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
+>>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
+>>>
+>>> db = {}
+>>>
+>>> m = ScramMechanism()
+>>>
+>>> salt, stored_key, server_key, iteration_count = m.make_auth_info(
+... PASSWORD, salt=SALT)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for getting a password given a username
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Normally the s_nonce parameter would be omitted, in which case the
+>>> # server will generate the nonce itself.
+>>>
+>>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
+>>>
+>>> # Set the first message from the client
+>>> s.set_client_first('n,,n=user,r=rOprNGfwEbeRWgbNEkqO')
+>>>
+>>> # Get the first server message, and send it to the client
+>>> sfirst = s.get_server_first()
+>>> print(sfirst)
+r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
+>>>
+>>> # Set the final message from the client
+>>> s.set_client_final(
+... 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
+... 'p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=')
+>>>
+>>> # Get the final server message and send it to the client
+>>> sfinal = s.get_server_final()
+>>> print(sfinal)
+v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Server only with passlib
+````````````````````````
+
+Here's an example using just the server and using the `passlib hashing library
+<https://passlib.readthedocs.io/en/stable/index.html>`_. The server nonce and salt is
+specified in order to give a reproducible example, but in production you'd omit the
+``s_nonce`` and ``salt`` parameters and let Scramp generate them:
+
+>>> from scramp import ScramMechanism
+>>> from passlib.hash import scram
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
+>>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
+>>> ITERATION_COUNT = 4096
+>>>
+>>> db = {}
+>>> hash = scram.using(salt=SALT, rounds=ITERATION_COUNT).hash(PASSWORD)
+>>>
+>>> salt, iteration_count, digest = scram.extract_digest_info(hash, 'sha-256')
+>>>
+>>> stored_key, server_key = m.make_stored_server_keys(digest)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for getting a password given a username
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Normally the s_nonce parameter would be omitted, in which case the
+>>> # server will generate the nonce itself.
+>>>
+>>> m = ScramMechanism()
+>>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
+>>>
+>>> # Set the first message from the client
+>>> s.set_client_first('n,,n=user,r=rOprNGfwEbeRWgbNEkqO')
+>>>
+>>> # Get the first server message, and send it to the client
+>>> sfirst = s.get_server_first()
+>>> print(sfirst)
+r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
+>>>
+>>> # Set the final message from the client
+>>> s.set_client_final(
+... 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
+... 'p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=')
+>>>
+>>> # Get the final server message and send it to the client
+>>> sfinal = s.get_server_final()
+>>> print(sfinal)
+v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Server Error
+````````````
+
+Here's an example of when setting a message from the client causes an error. The server
+nonce and salt is specified in order to give a reproducible example, but in production
+you'd omit the ``s_nonce`` and ``salt`` parameters and let Scramp generate them:
+
+>>> from scramp import ScramException, ScramMechanism
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
+>>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
+>>>
+>>> db = {}
+>>>
+>>> m = ScramMechanism()
+>>>
+>>> salt, stored_key, server_key, iteration_count = m.make_auth_info(
+... PASSWORD, salt=SALT)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for getting a password given a username
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Normally the s_nonce parameter would be omitted, in which case the
+>>> # server will generate the nonce itself.
+>>>
+>>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
+>>>
+>>> try:
+... # Set the first message from the client
+... s.set_client_first('p=tls-unique,,n=user,r=rOprNGfwEbeRWgbNEkqO')
+... except ScramException as e:
+... print(e)
+... # Get the final server message and send it to the client
+... sfinal = s.get_server_final()
+... print(sfinal)
+Received GS2 flag 'p' which indicates that the client requires channel binding, but the server does not: channel-binding-not-supported
+e=channel-binding-not-supported
+
+
+Standards
+---------
+
+`RFC 5802 <https://tools.ietf.org/html/rfc5802>`_
+ Describes SCRAM.
+`RFC 7677 <https://datatracker.ietf.org/doc/html/rfc7677>`_
+ Registers SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
+`draft-melnikov-scram-sha-512-02 <https://datatracker.ietf.org/doc/html/draft-melnikov-scram-sha-512>`_
+ Registers SCRAM-SHA-512 and SCRAM-SHA-512-PLUS.
+`draft-melnikov-scram-sha3-512 <https://datatracker.ietf.org/doc/html/draft-melnikov-scram-sha3-512>`_
+ Registers SCRAM-SHA3-512 and SCRAM-SHA3-512-PLUS.
+`RFC 5929 <https://datatracker.ietf.org/doc/html/rfc5929>`_
+ Channel Bindings for TLS.
+`draft-ietf-kitten-tls-channel-bindings-for-tls13 <https://datatracker.ietf.org/doc/html/draft-ietf-kitten-tls-channel-bindings-for-tls13>`_
+ Defines the ``tls-exporter`` channel binding, which is `not yet supported by Scramp
+ <https://github.com/tlocke/scramp/issues/9>`_.
+
+
+API Docs
+--------
+
+
+scramp.MECHANISMS
+`````````````````
+
+A tuple of the supported mechanism names.
+
+
+scramp.ScramClient
+``````````````````
+
+``ScramClient(mechanisms, username, password, channel_binding=None, c_nonce=None)``
+ Constructor of the ``ScramClient`` class, with the following parameters:
+
+ ``mechanisms``
+ A list or tuple of mechanism names. ScramClient will choose the most secure. If
+ ``cbind_data`` is ``None``, the '-PLUS' variants will be filtered out first. The
+ chosen mechanism is available as the property ``mechanism_name``.
+
+ ``username``
+
+ ``password``
+
+ ``channel_binding``
+ Providing a value for this parameter allows channel binding to be used (ie. it lets
+ you use mechanisms ending in '-PLUS'). The value for ``channel_binding`` is a tuple
+ consisting of the channel binding name and the channel binding data. For example, if
+ the channel binding name is ``tls-unique``, the ``channel_binding`` parameter would
+ be ``('tls-unique', data)``, where ``data`` is obtained by calling
+ `SSLSocket.get_channel_binding()
+ <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding>`_.
+ The convenience function ``scramp.make_channel_binding()`` can be used to create a
+ channel binding tuple.
+
+ ``c_nonce``
+ The client nonce. It's sometimes useful to set this when testing / debugging, but in
+ production this should be omitted, in which case ``ScramClient`` will generate a
+ client nonce.
+
+The ``ScramClient`` object has the following methods and properties:
+
+``get_client_first()``
+ Get the client first message.
+``set_server_first(message)``
+ Set the first message from the server.
+``get_client_final()``
+ Get the final client message.
+``set_server_final(message)``
+ Set the final message from the server.
+``mechanism_name``
+ The mechanism chosen from the list given in the constructor.
+
+
+scramp.ScramMechanism
+`````````````````````
+
+``ScramMechanism(mechanism='SCRAM-SHA-256')``
+ Constructor of the ``ScramMechanism`` class, with the following parameter:
+
+ ``mechanism``
+ The SCRAM mechanism to use.
+
+The ``ScramMechanism`` object has the following methods and properties:
+
+``make_auth_info(password, iteration_count=None, salt=None)``
+ returns the tuple ``(salt, stored_key, server_key, iteration_count)`` which is stored
+ in the authentication database on the server side. It has the following parameters:
+
+ ``password``
+ The user's password as a ``str``.
+
+ ``iteration_count``
+ The rounds as an ``int``. If ``None`` then use the minimum associated with the
+ mechanism.
+ ``salt``
+ It's sometimes useful to set this binary parameter when testing / debugging, but in
+ production this should be omitted, in which case a salt will be generated.
+
+``make_server(auth_fn, channel_binding=None, s_nonce=None)``
+ returns a ``ScramServer`` object. It takes the following parameters:
+
+ ``auth_fn``
+ This is a function provided by the programmer that has one parameter, a username of
+ type ``str`` and returns returns the tuple ``(salt, stored_key, server_key,
+ iteration_count)``. Where ``salt``, ``stored_key`` and ``server_key`` are of a
+ binary type, and ``iteration_count`` is an ``int``.
+
+ ``channel_binding``
+ Providing a value for this parameter allows channel binding to be used (ie. it lets
+ you use mechanisms ending in ``-PLUS``). The value for ``channel_binding`` is a
+ tuple consisting of the channel binding name and the channel binding data. For
+ example, if the channel binding name is 'tls-unique', the ``channel_binding``
+ parameter would be ``('tls-unique', data)``, where ``data`` is obtained by calling
+ `SSLSocket.get_channel_binding()
+ <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding>`_.
+ The convenience function ``scramp.make_channel_binding()`` can be used to create a
+ channel binding tuple. If ``channel_binding`` is provided and the mechanism isn't a
+ ``-PLUS`` variant, then the server will negotiate with the client to use the
+ ``-PLUS`` variant if the client supports it, or otherwise to use the mechanism
+ without channel binding.
+
+ ``s_nonce``
+ The server nonce as a ``str``. It's sometimes useful to set this when testing /
+ debugging, but in production this should be omitted, in which case ``ScramServer``
+ will generate a server nonce.
+
+``make_stored_server_keys(salted_password)``
+ returns ``(stored_key, server_key)`` tuple of ``bytes`` objects given a salted
+ password. This is useful if you want to use a separate hashing implementation from
+ the one provided by Scramp. It takes the following parameter:
+
+ ``salted_password``
+ A binary object representing the hashed password.
+
+``iteration_count``
+ The minimum iteration count recommended for this mechanism.
+
+
+scramp.ScramServer
+``````````````````
+
+The ``ScramServer`` object has the following methods:
+
+``set_client_first(message)``
+ Set the first message from the client.
+
+``get_server_first()``
+ Get the server first message.
+
+``set_client_final(message)``
+ Set the final client message.
+
+``get_server_final()``
+ Get the server final message.
+
+
+scramp.make_channel_binding()
+`````````````````````````````
+
+``make_channel_binding(name, ssl_socket)``
+ A helper function that makes a ``channel_binding`` tuple when given a channel binding
+ name and an SSL socket. The parameters are:
+
+ ``name``
+ A channel binding name such as 'tls-unique' or 'tls-server-end-point'.
+
+ ``ssl_socket``
+ An instance of `ssl.SSLSocket
+ <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket>`_.
+
+
+README.rst
+----------
+
+This file is written in the `reStructuredText
+<https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_ format. To generate an
+HTML page from it, do:
+
+- Activate the virtual environment: ``source venv/bin/activate``
+- Install ``Sphinx``: ``pip install Sphinx``
+- Run ``rst2html.py``: ``rst2html.py README.rst README.html``
+
+
+Testing
+-------
+
+- Activate the virtual environment: ``source venv/bin/activate``
+- Install ``tox``: ``pip install tox``
+- Run ``tox``: ``tox``
+
+
+Doing A Release Of Scramp
+-------------------------
+
+Run ``tox`` to make sure all tests pass, then update the release notes, then do::
+
+ git tag -a x.y.z -m "version x.y.z"
+ rm -r dist
+ python -m build
+ twine upload --sign dist/*
+
+
+Release Notes
+-------------
+
+Version 1.4.4, 2022-11-01
+`````````````````````````
+
+- Tighten up parsing of messages to make sure that a ``ScramException`` is raised if a
+ message is malformed.
+
+
+Version 1.4.3, 2022-10-26
+`````````````````````````
+
+- The client now sends a gs2-cbind-flag of 'y' if the client supports channel
+ binding, but thinks the server does not.
+
+
+Version 1.4.2, 2022-10-22
+`````````````````````````
+
+- Switch to using the MIT-0 licence https://choosealicense.com/licenses/mit-0/
+
+- When creating a ScramClient, allow non ``-PLUS`` variants, even if a
+ ``channel_binding`` parameter is provided. Previously this would raise and
+ exception.
+
+
+Version 1.4.1, 2021-08-25
+`````````````````````````
+
+- When using ``make_channel_binding()`` to create a tls-server-end-point channel
+ binding, support certificates with hash algorithm of sha512.
+
+
+Version 1.4.0, 2021-03-28
+`````````````````````````
+
+- Raise an exception if the client receives an error from the server.
+
+
+Version 1.3.0, 2021-03-28
+`````````````````````````
+
+- As the specification allows, server errors are now sent to the client in the
+ ``server_final`` message, an exception is still thrown as before.
+
+
+Version 1.2.2, 2021-02-13
+`````````````````````````
+
+- Fix bug in generating the AuthMessage. It was incorrect when channel binding
+ was used. So now Scramp supports channel binding.
+
+
+Version 1.2.1, 2021-02-07
+`````````````````````````
+
+- Add support for channel binding.
+
+- Add support for SCRAM-SHA-512 and SCRAM-SHA3-512 and their channel binding
+ variants.
+
+
+Version 1.2.0, 2020-05-30
+`````````````````````````
+
+- This is a backwardly incompatible change on the server side, the client side will
+ work as before. The idea of this change is to make it possible to have an
+ authentication database. That is, the authentication information can be stored, and
+ then retrieved when needed to authenticate the user.
+
+- In addition, it's now possible on the server side to use a third party hashing library
+ such as passlib as the hashing implementation.
+
+
+Version 1.1.1, 2020-03-28
+`````````````````````````
+
+- Add the README and LICENCE to the distribution.
+
+
+Version 1.1.0, 2019-02-24
+`````````````````````````
+
+- Add support for the SCRAM-SHA-1 mechanism.
+
+
+Version 1.0.0, 2019-02-17
+`````````````````````````
+
+- Implement the server side as well as the client side.
+
+
+Version 0.0.0, 2019-02-10
+`````````````````````````
+
+- Copied SCRAM implementation from `pg8000 <https://github.com/tlocke/pg8000>`_. The
+ idea is to make it a general SCRAM implemtation. Credit to the `Scrampy
+ <https://github.com/cagdass/scrampy>`_ project which I read through to help with this
+ project. Also credit to the `passlib <https://github.com/efficks/passlib>`_ project
+ from which I copied the ``saslprep`` function.
diff --git a/contrib/python/scramp/.dist-info/top_level.txt b/contrib/python/scramp/.dist-info/top_level.txt
new file mode 100644
index 00000000000..01f01e6b9ad
--- /dev/null
+++ b/contrib/python/scramp/.dist-info/top_level.txt
@@ -0,0 +1 @@
+scramp
diff --git a/contrib/python/scramp/LICENSE b/contrib/python/scramp/LICENSE
new file mode 100644
index 00000000000..7fb92115643
--- /dev/null
+++ b/contrib/python/scramp/LICENSE
@@ -0,0 +1,18 @@
+MIT No Attribution
+
+Copyright The Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/contrib/python/scramp/README.rst b/contrib/python/scramp/README.rst
new file mode 100644
index 00000000000..d9f038c0703
--- /dev/null
+++ b/contrib/python/scramp/README.rst
@@ -0,0 +1,584 @@
+======
+Scramp
+======
+
+A Python implementation of the `SCRAM authentication protocol
+<https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism>`_.
+Scramp supports the following mechanisms:
+
+- SCRAM-SHA-1
+- SCRAM-SHA-1-PLUS
+- SCRAM-SHA-256
+- SCRAM-SHA-256-PLUS
+- SCRAM-SHA-512
+- SCRAM-SHA-512-PLUS
+- SCRAM-SHA3-512
+- SCRAM-SHA3-512-PLUS
+
+.. contents:: Table of Contents
+ :depth: 2
+ :local:
+
+Installation
+------------
+
+- Create a virtual environment: ``python3 -m venv venv``
+- Activate the virtual environment: ``source venv/bin/activate``
+- Install: ``pip install scramp``
+
+
+Examples
+--------
+
+Client and Server
+`````````````````
+
+Here's an example using both the client and the server. It's a bit contrived as normally
+you'd be using either the client or server on its own.
+
+>>> from scramp import ScramClient, ScramMechanism
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> MECHANISMS = ['SCRAM-SHA-256']
+>>>
+>>>
+>>> # Choose a mechanism for our server
+>>> m = ScramMechanism() # Default is SCRAM-SHA-256
+>>>
+>>> # On the server side we create the authentication information for each user
+>>> # and store it in an authentication database. We'll use a dict:
+>>> db = {}
+>>>
+>>> salt, stored_key, server_key, iteration_count = m.make_auth_info(PASSWORD)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for retrieving the authentication information
+>>> # from the database given a username
+>>>
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Make the SCRAM server
+>>> s = m.make_server(auth_fn)
+>>>
+>>> # Now set up the client and carry out authentication with the server
+>>> c = ScramClient(MECHANISMS, USERNAME, PASSWORD)
+>>> cfirst = c.get_client_first()
+>>>
+>>> s.set_client_first(cfirst)
+>>> sfirst = s.get_server_first()
+>>>
+>>> c.set_server_first(sfirst)
+>>> cfinal = c.get_client_final()
+>>>
+>>> s.set_client_final(cfinal)
+>>> sfinal = s.get_server_final()
+>>>
+>>> c.set_server_final(sfinal)
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Client only
+```````````
+
+Here's an example using just the client. The client nonce is specified in order to give
+a reproducible example, but in production you'd omit the ``c_nonce`` parameter and let
+``ScramClient`` generate a client nonce:
+
+>>> from scramp import ScramClient
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> C_NONCE = 'rOprNGfwEbeRWgbNEkqO'
+>>> MECHANISMS = ['SCRAM-SHA-256']
+>>>
+>>> # Normally the c_nonce would be omitted, in which case ScramClient will
+>>> # generate the nonce itself.
+>>>
+>>> c = ScramClient(MECHANISMS, USERNAME, PASSWORD, c_nonce=C_NONCE)
+>>>
+>>> # Get the client first message and send it to the server
+>>> cfirst = c.get_client_first()
+>>> print(cfirst)
+n,,n=user,r=rOprNGfwEbeRWgbNEkqO
+>>>
+>>> # Set the first message from the server
+>>> c.set_server_first(
+... 'r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
+... 's=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096')
+>>>
+>>> # Get the client final message and send it to the server
+>>> cfinal = c.get_client_final()
+>>> print(cfinal)
+c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=
+>>>
+>>> # Set the final message from the server
+>>> c.set_server_final('v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=')
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Server only
+```````````
+
+Here's an example using just the server. The server nonce and salt is specified in order
+to give a reproducible example, but in production you'd omit the ``s_nonce`` and
+``salt`` parameters and let Scramp generate them:
+
+>>> from scramp import ScramMechanism
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
+>>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
+>>>
+>>> db = {}
+>>>
+>>> m = ScramMechanism()
+>>>
+>>> salt, stored_key, server_key, iteration_count = m.make_auth_info(
+... PASSWORD, salt=SALT)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for getting a password given a username
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Normally the s_nonce parameter would be omitted, in which case the
+>>> # server will generate the nonce itself.
+>>>
+>>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
+>>>
+>>> # Set the first message from the client
+>>> s.set_client_first('n,,n=user,r=rOprNGfwEbeRWgbNEkqO')
+>>>
+>>> # Get the first server message, and send it to the client
+>>> sfirst = s.get_server_first()
+>>> print(sfirst)
+r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
+>>>
+>>> # Set the final message from the client
+>>> s.set_client_final(
+... 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
+... 'p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=')
+>>>
+>>> # Get the final server message and send it to the client
+>>> sfinal = s.get_server_final()
+>>> print(sfinal)
+v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Server only with passlib
+````````````````````````
+
+Here's an example using just the server and using the `passlib hashing library
+<https://passlib.readthedocs.io/en/stable/index.html>`_. The server nonce and salt is
+specified in order to give a reproducible example, but in production you'd omit the
+``s_nonce`` and ``salt`` parameters and let Scramp generate them:
+
+>>> from scramp import ScramMechanism
+>>> from passlib.hash import scram
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
+>>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
+>>> ITERATION_COUNT = 4096
+>>>
+>>> db = {}
+>>> hash = scram.using(salt=SALT, rounds=ITERATION_COUNT).hash(PASSWORD)
+>>>
+>>> salt, iteration_count, digest = scram.extract_digest_info(hash, 'sha-256')
+>>>
+>>> stored_key, server_key = m.make_stored_server_keys(digest)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for getting a password given a username
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Normally the s_nonce parameter would be omitted, in which case the
+>>> # server will generate the nonce itself.
+>>>
+>>> m = ScramMechanism()
+>>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
+>>>
+>>> # Set the first message from the client
+>>> s.set_client_first('n,,n=user,r=rOprNGfwEbeRWgbNEkqO')
+>>>
+>>> # Get the first server message, and send it to the client
+>>> sfirst = s.get_server_first()
+>>> print(sfirst)
+r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
+>>>
+>>> # Set the final message from the client
+>>> s.set_client_final(
+... 'c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,'
+... 'p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=')
+>>>
+>>> # Get the final server message and send it to the client
+>>> sfinal = s.get_server_final()
+>>> print(sfinal)
+v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
+>>>
+>>> # If it all runs through without raising an exception, the authentication
+>>> # has succeeded
+
+
+Server Error
+````````````
+
+Here's an example of when setting a message from the client causes an error. The server
+nonce and salt is specified in order to give a reproducible example, but in production
+you'd omit the ``s_nonce`` and ``salt`` parameters and let Scramp generate them:
+
+>>> from scramp import ScramException, ScramMechanism
+>>>
+>>> USERNAME = 'user'
+>>> PASSWORD = 'pencil'
+>>> S_NONCE = '%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0'
+>>> SALT = b'[m\x99h\x9d\x125\x8e\xec\xa0K\x14\x126\xfa\x81'
+>>>
+>>> db = {}
+>>>
+>>> m = ScramMechanism()
+>>>
+>>> salt, stored_key, server_key, iteration_count = m.make_auth_info(
+... PASSWORD, salt=SALT)
+>>>
+>>> db[USERNAME] = salt, stored_key, server_key, iteration_count
+>>>
+>>> # Define your own function for getting a password given a username
+>>> def auth_fn(username):
+... return db[username]
+>>>
+>>> # Normally the s_nonce parameter would be omitted, in which case the
+>>> # server will generate the nonce itself.
+>>>
+>>> s = m.make_server(auth_fn, s_nonce=S_NONCE)
+>>>
+>>> try:
+... # Set the first message from the client
+... s.set_client_first('p=tls-unique,,n=user,r=rOprNGfwEbeRWgbNEkqO')
+... except ScramException as e:
+... print(e)
+... # Get the final server message and send it to the client
+... sfinal = s.get_server_final()
+... print(sfinal)
+Received GS2 flag 'p' which indicates that the client requires channel binding, but the server does not: channel-binding-not-supported
+e=channel-binding-not-supported
+
+
+Standards
+---------
+
+`RFC 5802 <https://tools.ietf.org/html/rfc5802>`_
+ Describes SCRAM.
+`RFC 7677 <https://datatracker.ietf.org/doc/html/rfc7677>`_
+ Registers SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
+`draft-melnikov-scram-sha-512-02 <https://datatracker.ietf.org/doc/html/draft-melnikov-scram-sha-512>`_
+ Registers SCRAM-SHA-512 and SCRAM-SHA-512-PLUS.
+`draft-melnikov-scram-sha3-512 <https://datatracker.ietf.org/doc/html/draft-melnikov-scram-sha3-512>`_
+ Registers SCRAM-SHA3-512 and SCRAM-SHA3-512-PLUS.
+`RFC 5929 <https://datatracker.ietf.org/doc/html/rfc5929>`_
+ Channel Bindings for TLS.
+`draft-ietf-kitten-tls-channel-bindings-for-tls13 <https://datatracker.ietf.org/doc/html/draft-ietf-kitten-tls-channel-bindings-for-tls13>`_
+ Defines the ``tls-exporter`` channel binding, which is `not yet supported by Scramp
+ <https://github.com/tlocke/scramp/issues/9>`_.
+
+
+API Docs
+--------
+
+
+scramp.MECHANISMS
+`````````````````
+
+A tuple of the supported mechanism names.
+
+
+scramp.ScramClient
+``````````````````
+
+``ScramClient(mechanisms, username, password, channel_binding=None, c_nonce=None)``
+ Constructor of the ``ScramClient`` class, with the following parameters:
+
+ ``mechanisms``
+ A list or tuple of mechanism names. ScramClient will choose the most secure. If
+ ``cbind_data`` is ``None``, the '-PLUS' variants will be filtered out first. The
+ chosen mechanism is available as the property ``mechanism_name``.
+
+ ``username``
+
+ ``password``
+
+ ``channel_binding``
+ Providing a value for this parameter allows channel binding to be used (ie. it lets
+ you use mechanisms ending in '-PLUS'). The value for ``channel_binding`` is a tuple
+ consisting of the channel binding name and the channel binding data. For example, if
+ the channel binding name is ``tls-unique``, the ``channel_binding`` parameter would
+ be ``('tls-unique', data)``, where ``data`` is obtained by calling
+ `SSLSocket.get_channel_binding()
+ <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding>`_.
+ The convenience function ``scramp.make_channel_binding()`` can be used to create a
+ channel binding tuple.
+
+ ``c_nonce``
+ The client nonce. It's sometimes useful to set this when testing / debugging, but in
+ production this should be omitted, in which case ``ScramClient`` will generate a
+ client nonce.
+
+The ``ScramClient`` object has the following methods and properties:
+
+``get_client_first()``
+ Get the client first message.
+``set_server_first(message)``
+ Set the first message from the server.
+``get_client_final()``
+ Get the final client message.
+``set_server_final(message)``
+ Set the final message from the server.
+``mechanism_name``
+ The mechanism chosen from the list given in the constructor.
+
+
+scramp.ScramMechanism
+`````````````````````
+
+``ScramMechanism(mechanism='SCRAM-SHA-256')``
+ Constructor of the ``ScramMechanism`` class, with the following parameter:
+
+ ``mechanism``
+ The SCRAM mechanism to use.
+
+The ``ScramMechanism`` object has the following methods and properties:
+
+``make_auth_info(password, iteration_count=None, salt=None)``
+ returns the tuple ``(salt, stored_key, server_key, iteration_count)`` which is stored
+ in the authentication database on the server side. It has the following parameters:
+
+ ``password``
+ The user's password as a ``str``.
+
+ ``iteration_count``
+ The rounds as an ``int``. If ``None`` then use the minimum associated with the
+ mechanism.
+ ``salt``
+ It's sometimes useful to set this binary parameter when testing / debugging, but in
+ production this should be omitted, in which case a salt will be generated.
+
+``make_server(auth_fn, channel_binding=None, s_nonce=None)``
+ returns a ``ScramServer`` object. It takes the following parameters:
+
+ ``auth_fn``
+ This is a function provided by the programmer that has one parameter, a username of
+ type ``str`` and returns returns the tuple ``(salt, stored_key, server_key,
+ iteration_count)``. Where ``salt``, ``stored_key`` and ``server_key`` are of a
+ binary type, and ``iteration_count`` is an ``int``.
+
+ ``channel_binding``
+ Providing a value for this parameter allows channel binding to be used (ie. it lets
+ you use mechanisms ending in ``-PLUS``). The value for ``channel_binding`` is a
+ tuple consisting of the channel binding name and the channel binding data. For
+ example, if the channel binding name is 'tls-unique', the ``channel_binding``
+ parameter would be ``('tls-unique', data)``, where ``data`` is obtained by calling
+ `SSLSocket.get_channel_binding()
+ <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding>`_.
+ The convenience function ``scramp.make_channel_binding()`` can be used to create a
+ channel binding tuple. If ``channel_binding`` is provided and the mechanism isn't a
+ ``-PLUS`` variant, then the server will negotiate with the client to use the
+ ``-PLUS`` variant if the client supports it, or otherwise to use the mechanism
+ without channel binding.
+
+ ``s_nonce``
+ The server nonce as a ``str``. It's sometimes useful to set this when testing /
+ debugging, but in production this should be omitted, in which case ``ScramServer``
+ will generate a server nonce.
+
+``make_stored_server_keys(salted_password)``
+ returns ``(stored_key, server_key)`` tuple of ``bytes`` objects given a salted
+ password. This is useful if you want to use a separate hashing implementation from
+ the one provided by Scramp. It takes the following parameter:
+
+ ``salted_password``
+ A binary object representing the hashed password.
+
+``iteration_count``
+ The minimum iteration count recommended for this mechanism.
+
+
+scramp.ScramServer
+``````````````````
+
+The ``ScramServer`` object has the following methods:
+
+``set_client_first(message)``
+ Set the first message from the client.
+
+``get_server_first()``
+ Get the server first message.
+
+``set_client_final(message)``
+ Set the final client message.
+
+``get_server_final()``
+ Get the server final message.
+
+
+scramp.make_channel_binding()
+`````````````````````````````
+
+``make_channel_binding(name, ssl_socket)``
+ A helper function that makes a ``channel_binding`` tuple when given a channel binding
+ name and an SSL socket. The parameters are:
+
+ ``name``
+ A channel binding name such as 'tls-unique' or 'tls-server-end-point'.
+
+ ``ssl_socket``
+ An instance of `ssl.SSLSocket
+ <https://docs.python.org/3/library/ssl.html#ssl.SSLSocket>`_.
+
+
+README.rst
+----------
+
+This file is written in the `reStructuredText
+<https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_ format. To generate an
+HTML page from it, do:
+
+- Activate the virtual environment: ``source venv/bin/activate``
+- Install ``Sphinx``: ``pip install Sphinx``
+- Run ``rst2html.py``: ``rst2html.py README.rst README.html``
+
+
+Testing
+-------
+
+- Activate the virtual environment: ``source venv/bin/activate``
+- Install ``tox``: ``pip install tox``
+- Run ``tox``: ``tox``
+
+
+Doing A Release Of Scramp
+-------------------------
+
+Run ``tox`` to make sure all tests pass, then update the release notes, then do::
+
+ git tag -a x.y.z -m "version x.y.z"
+ rm -r dist
+ python -m build
+ twine upload --sign dist/*
+
+
+Release Notes
+-------------
+
+Version 1.4.4, 2022-11-01
+`````````````````````````
+
+- Tighten up parsing of messages to make sure that a ``ScramException`` is raised if a
+ message is malformed.
+
+
+Version 1.4.3, 2022-10-26
+`````````````````````````
+
+- The client now sends a gs2-cbind-flag of 'y' if the client supports channel
+ binding, but thinks the server does not.
+
+
+Version 1.4.2, 2022-10-22
+`````````````````````````
+
+- Switch to using the MIT-0 licence https://choosealicense.com/licenses/mit-0/
+
+- When creating a ScramClient, allow non ``-PLUS`` variants, even if a
+ ``channel_binding`` parameter is provided. Previously this would raise and
+ exception.
+
+
+Version 1.4.1, 2021-08-25
+`````````````````````````
+
+- When using ``make_channel_binding()`` to create a tls-server-end-point channel
+ binding, support certificates with hash algorithm of sha512.
+
+
+Version 1.4.0, 2021-03-28
+`````````````````````````
+
+- Raise an exception if the client receives an error from the server.
+
+
+Version 1.3.0, 2021-03-28
+`````````````````````````
+
+- As the specification allows, server errors are now sent to the client in the
+ ``server_final`` message, an exception is still thrown as before.
+
+
+Version 1.2.2, 2021-02-13
+`````````````````````````
+
+- Fix bug in generating the AuthMessage. It was incorrect when channel binding
+ was used. So now Scramp supports channel binding.
+
+
+Version 1.2.1, 2021-02-07
+`````````````````````````
+
+- Add support for channel binding.
+
+- Add support for SCRAM-SHA-512 and SCRAM-SHA3-512 and their channel binding
+ variants.
+
+
+Version 1.2.0, 2020-05-30
+`````````````````````````
+
+- This is a backwardly incompatible change on the server side, the client side will
+ work as before. The idea of this change is to make it possible to have an
+ authentication database. That is, the authentication information can be stored, and
+ then retrieved when needed to authenticate the user.
+
+- In addition, it's now possible on the server side to use a third party hashing library
+ such as passlib as the hashing implementation.
+
+
+Version 1.1.1, 2020-03-28
+`````````````````````````
+
+- Add the README and LICENCE to the distribution.
+
+
+Version 1.1.0, 2019-02-24
+`````````````````````````
+
+- Add support for the SCRAM-SHA-1 mechanism.
+
+
+Version 1.0.0, 2019-02-17
+`````````````````````````
+
+- Implement the server side as well as the client side.
+
+
+Version 0.0.0, 2019-02-10
+`````````````````````````
+
+- Copied SCRAM implementation from `pg8000 <https://github.com/tlocke/pg8000>`_. The
+ idea is to make it a general SCRAM implemtation. Credit to the `Scrampy
+ <https://github.com/cagdass/scrampy>`_ project which I read through to help with this
+ project. Also credit to the `passlib <https://github.com/efficks/passlib>`_ project
+ from which I copied the ``saslprep`` function.
diff --git a/contrib/python/scramp/scramp/__init__.py b/contrib/python/scramp/scramp/__init__.py
new file mode 100644
index 00000000000..e7a71b0eef9
--- /dev/null
+++ b/contrib/python/scramp/scramp/__init__.py
@@ -0,0 +1,15 @@
+from scramp.core import (
+ ScramClient,
+ ScramException,
+ ScramMechanism,
+ make_channel_binding,
+)
+
+__all__ = [ScramClient, ScramMechanism, ScramException, make_channel_binding]
+
+try:
+ from importlib.metadata import version
+except ImportError:
+ from importlib_metadata import version
+
+__version__ = version("scramp")
diff --git a/contrib/python/scramp/scramp/core.py b/contrib/python/scramp/scramp/core.py
new file mode 100644
index 00000000000..0be76f59a56
--- /dev/null
+++ b/contrib/python/scramp/scramp/core.py
@@ -0,0 +1,696 @@
+import hashlib
+import unicodedata
+from enum import IntEnum, unique
+from functools import wraps
+from operator import attrgetter
+from os import urandom
+from stringprep import (
+ in_table_a1,
+ in_table_b1,
+ in_table_c12,
+ in_table_c21_c22,
+ in_table_c3,
+ in_table_c4,
+ in_table_c5,
+ in_table_c6,
+ in_table_c7,
+ in_table_c8,
+ in_table_c9,
+ in_table_d1,
+ in_table_d2,
+)
+from uuid import uuid4
+
+from asn1crypto.x509 import Certificate
+
+from scramp.utils import b64dec, b64enc, h, hi, hmac, uenc, xor
+
+# https://tools.ietf.org/html/rfc5802
+# https://www.rfc-editor.org/rfc/rfc7677.txt
+
+
+@unique
+class ClientStage(IntEnum):
+ get_client_first = 1
+ set_server_first = 2
+ get_client_final = 3
+ set_server_final = 4
+
+
+@unique
+class ServerStage(IntEnum):
+ set_client_first = 1
+ get_server_first = 2
+ set_client_final = 3
+ get_server_final = 4
+
+
+def _check_stage(Stages, current_stage, next_stage):
+ if current_stage is None:
+ if next_stage != 1:
+ raise ScramException(f"The method {Stages(1).name} must be called first.")
+ elif current_stage == 4:
+ raise ScramException("The authentication sequence has already finished.")
+ elif next_stage != current_stage + 1:
+ raise ScramException(
+ f"The next method to be called is "
+ f"{Stages(current_stage + 1).name}, not this method."
+ )
+
+
+class ScramException(Exception):
+ def __init__(self, message, server_error=None):
+ super().__init__(message)
+ self.server_error = server_error
+
+ def __str__(self):
+ s_str = "" if self.server_error is None else f": {self.server_error}"
+ return super().__str__() + s_str
+
+
+MECHANISMS = (
+ "SCRAM-SHA-1",
+ "SCRAM-SHA-1-PLUS",
+ "SCRAM-SHA-256",
+ "SCRAM-SHA-256-PLUS",
+ "SCRAM-SHA-512",
+ "SCRAM-SHA-512-PLUS",
+ "SCRAM-SHA3-512",
+ "SCRAM-SHA3-512-PLUS",
+)
+
+
+CHANNEL_TYPES = (
+ "tls-server-end-point",
+ "tls-unique",
+ "tls-unique-for-telnet",
+)
+
+
+def _make_cb_data(name, ssl_socket):
+ if name == "tls-unique":
+ return ssl_socket.get_channel_binding(name)
+
+ elif name == "tls-server-end-point":
+ cert_bin = ssl_socket.getpeercert(binary_form=True)
+ cert = Certificate.load(cert_bin)
+
+ # Find the hash algorithm to use according to
+ # https://tools.ietf.org/html/rfc5929#section-4
+ hash_algo = cert.hash_algo
+ if hash_algo in ("md5", "sha1"):
+ hash_algo = "sha256"
+
+ try:
+ hash_obj = hashlib.new(hash_algo, cert_bin)
+ except ValueError as e:
+ raise ScramException(
+ f"Hash algorithm {hash_algo} not supported by hashlib. {e}"
+ )
+ return hash_obj.digest()
+
+ else:
+ raise ScramException(f"Channel binding name {name} not recognized.")
+
+
+def make_channel_binding(name, ssl_socket):
+ return name, _make_cb_data(name, ssl_socket)
+
+
+class ScramMechanism:
+ MECH_LOOKUP = {
+ "SCRAM-SHA-1": (hashlib.sha1, False, 4096, 0),
+ "SCRAM-SHA-1-PLUS": (hashlib.sha1, True, 4096, 1),
+ "SCRAM-SHA-256": (hashlib.sha256, False, 4096, 2),
+ "SCRAM-SHA-256-PLUS": (hashlib.sha256, True, 4096, 3),
+ "SCRAM-SHA-512": (hashlib.sha512, False, 4096, 4),
+ "SCRAM-SHA-512-PLUS": (hashlib.sha512, True, 4096, 5),
+ "SCRAM-SHA3-512": (hashlib.sha3_512, False, 10000, 6),
+ "SCRAM-SHA3-512-PLUS": (hashlib.sha3_512, True, 10000, 7),
+ }
+
+ def __init__(self, mechanism="SCRAM-SHA-256"):
+ if mechanism not in MECHANISMS:
+ raise ScramException(
+ f"The mechanism name '{mechanism}' is not supported. The "
+ f"supported mechanisms are {MECHANISMS}."
+ )
+ self.name = mechanism
+ (
+ self.hf,
+ self.use_binding,
+ self.iteration_count,
+ self.strength,
+ ) = self.MECH_LOOKUP[mechanism]
+
+ def make_auth_info(self, password, iteration_count=None, salt=None):
+ if iteration_count is None:
+ iteration_count = self.iteration_count
+ salt, stored_key, server_key = _make_auth_info(
+ self.hf, password, iteration_count, salt=salt
+ )
+ return salt, stored_key, server_key, iteration_count
+
+ def make_stored_server_keys(self, salted_password):
+ _, stored_key, server_key = _c_key_stored_key_s_key(self.hf, salted_password)
+ return stored_key, server_key
+
+ def make_server(self, auth_fn, channel_binding=None, s_nonce=None):
+ return ScramServer(
+ self, auth_fn, channel_binding=channel_binding, s_nonce=s_nonce
+ )
+
+
+def _make_auth_info(hf, password, i, salt=None):
+ if salt is None:
+ salt = urandom(16)
+
+ salted_password = _make_salted_password(hf, password, salt, i)
+ _, stored_key, server_key = _c_key_stored_key_s_key(hf, salted_password)
+ return salt, stored_key, server_key
+
+
+def _validate_channel_binding(channel_binding):
+ if channel_binding is None:
+ return
+
+ if not isinstance(channel_binding, tuple):
+ raise ScramException(
+ "The channel_binding parameter must either be None or a tuple."
+ )
+
+ if len(channel_binding) != 2:
+ raise ScramException(
+ "The channel_binding parameter must either be None or a tuple of two "
+ "elements (type, data)."
+ )
+
+ channel_type, channel_data = channel_binding
+ if channel_type not in CHANNEL_TYPES:
+ raise ScramException(
+ "The channel_binding parameter must either be None or a tuple with the "
+ "first element a str specifying one of the channel types {CHANNEL_TYPES}."
+ )
+
+ if not isinstance(channel_data, bytes):
+ raise ScramException(
+ "The channel_binding parameter must either be None or a tuple with the "
+ "second element a bytes object containing the bind data."
+ )
+
+
+class ScramClient:
+ def __init__(
+ self, mechanisms, username, password, channel_binding=None, c_nonce=None
+ ):
+ if not isinstance(mechanisms, (list, tuple)):
+ raise ScramException(
+ "The 'mechanisms' parameter must be a list or tuple of mechanism names."
+ )
+
+ _validate_channel_binding(channel_binding)
+
+ ms = (ScramMechanism(m) for m in mechanisms)
+ mechs = [m for m in ms if not (channel_binding is None and m.use_binding)]
+ if len(mechs) == 0:
+ raise ScramException(
+ f"There are no suitable mechanisms in the list provided: {mechanisms}"
+ )
+
+ mech = sorted(mechs, key=attrgetter("strength"))[-1]
+ self.hf, self.use_binding = mech.hf, mech.use_binding
+ self.mechanism_name = mech.name
+
+ self.c_nonce = _make_nonce() if c_nonce is None else c_nonce
+ self.username = username
+ self.password = password
+ self.channel_binding = channel_binding
+ self.stage = None
+
+ def _set_stage(self, next_stage):
+ _check_stage(ClientStage, self.stage, next_stage)
+ self.stage = next_stage
+
+ def get_client_first(self):
+ self._set_stage(ClientStage.get_client_first)
+ self.client_first_bare, client_first = _get_client_first(
+ self.username, self.c_nonce, self.channel_binding, self.use_binding
+ )
+ return client_first
+
+ def set_server_first(self, message):
+ self._set_stage(ClientStage.set_server_first)
+ self.server_first = message
+ self.nonce, self.salt, self.iterations = _set_server_first(
+ message, self.c_nonce
+ )
+
+ def get_client_final(self):
+ self._set_stage(ClientStage.get_client_final)
+ self.server_signature, cfinal = _get_client_final(
+ self.hf,
+ self.password,
+ self.salt,
+ self.iterations,
+ self.nonce,
+ self.client_first_bare,
+ self.server_first,
+ self.channel_binding,
+ self.use_binding,
+ )
+ return cfinal
+
+ def set_server_final(self, message):
+ self._set_stage(ClientStage.set_server_final)
+ _set_server_final(message, self.server_signature)
+
+
+def set_error(f):
+ @wraps(f)
+ def wrapper(self, *args, **kwds):
+ try:
+ return f(self, *args, **kwds)
+ except ScramException as e:
+ if e.server_error is not None:
+ self.error = e.server_error
+ self.stage = ServerStage.set_client_final
+ raise e
+
+ return wrapper
+
+
+class ScramServer:
+ def __init__(self, mechanism, auth_fn, channel_binding=None, s_nonce=None):
+
+ _validate_channel_binding(channel_binding)
+
+ self.channel_binding = channel_binding
+ self.s_nonce = _make_nonce() if s_nonce is None else s_nonce
+ self.auth_fn = auth_fn
+ self.stage = None
+ self.server_signature = None
+ self.error = None
+
+ self._set_mechanism(mechanism)
+
+ def _set_mechanism(self, mechanism):
+ if mechanism.use_binding and self.channel_binding is None:
+ raise ScramException(
+ "The mechanism requires channel binding, and so channel_binding can't "
+ "be None."
+ )
+ self.m = mechanism
+
+ def _set_stage(self, next_stage):
+ _check_stage(ServerStage, self.stage, next_stage)
+ self.stage = next_stage
+
+ @set_error
+ def set_client_first(self, client_first):
+ self._set_stage(ServerStage.set_client_first)
+ (
+ self.nonce,
+ self.user,
+ self.client_first_bare,
+ upgrade_mechanism,
+ ) = _set_client_first(
+ client_first, self.s_nonce, self.channel_binding, self.m.use_binding
+ )
+
+ if upgrade_mechanism:
+ mech = ScramMechanism(f"{self.m.name}-PLUS")
+ self._set_mechanism(mech)
+
+ salt, self.stored_key, self.server_key, self.i = self.auth_fn(self.user)
+ self.salt = b64enc(salt)
+
+ @set_error
+ def get_server_first(self):
+ self._set_stage(ServerStage.get_server_first)
+ self.server_first = _get_server_first(
+ self.nonce,
+ self.salt,
+ self.i,
+ )
+ return self.server_first
+
+ @set_error
+ def set_client_final(self, client_final):
+ self._set_stage(ServerStage.set_client_final)
+ self.server_signature = _set_client_final(
+ self.m.hf,
+ client_final,
+ self.s_nonce,
+ self.stored_key,
+ self.server_key,
+ self.client_first_bare,
+ self.server_first,
+ self.channel_binding,
+ self.m.use_binding,
+ )
+
+ @set_error
+ def get_server_final(self):
+ self._set_stage(ServerStage.get_server_final)
+ return _get_server_final(self.server_signature, self.error)
+
+
+def _make_nonce():
+ return str(uuid4()).replace("-", "")
+
+
+def _make_auth_message(client_first_bare, server_first, client_final_without_proof):
+ msg = client_first_bare, server_first, client_final_without_proof
+ return uenc(",".join(msg))
+
+
+def _make_salted_password(hf, password, salt, iterations):
+ return hi(hf, uenc(saslprep(password)), salt, iterations)
+
+
+def _c_key_stored_key_s_key(hf, salted_password):
+ client_key = hmac(hf, salted_password, b"Client Key")
+ stored_key = h(hf, client_key)
+ server_key = hmac(hf, salted_password, b"Server Key")
+
+ return client_key, stored_key, server_key
+
+
+def _check_client_key(hf, stored_key, auth_msg, proof):
+ client_signature = hmac(hf, stored_key, auth_msg)
+ client_key = xor(client_signature, b64dec(proof))
+ key = h(hf, client_key)
+
+ if key != stored_key:
+ raise ScramException("The client keys don't match.", SERVER_ERROR_INVALID_PROOF)
+
+
+def _make_gs2_header(channel_binding, use_binding):
+ if channel_binding is None:
+ return "n", "n,,"
+ else:
+ if use_binding:
+ channel_type, _ = channel_binding
+ return "p", f"p={channel_type},,"
+ else:
+ return "y", "y,,"
+
+
+def _make_cbind_input(channel_binding, use_binding):
+ gs2_cbind_flag, gs2_header = _make_gs2_header(channel_binding, use_binding)
+ gs2_header_bin = gs2_header.encode("ascii")
+
+ if gs2_cbind_flag in ("y", "n"):
+ return gs2_header_bin
+ elif gs2_cbind_flag == "p":
+ _, cbind_data = channel_binding
+ return gs2_header_bin + cbind_data
+ else:
+ raise ScramException(f"The gs2_cbind_flag '{gs2_cbind_flag}' is not recognized")
+
+
+def _parse_message(msg, desc, *validations):
+ m = {}
+ for p in msg.split(","):
+ if len(p) < 2 or p[1] != "=":
+ raise ScramException(
+ f"Malformed {desc} message. Attributes must be separated by a ',' and "
+ f"each attribute must start with a letter followed by a '='",
+ SERVER_ERROR_OTHER_ERROR,
+ )
+ m[p[0]] = p[2:]
+
+ m = {e[0]: e[2:] for e in msg.split(",")}
+
+ keystr = "".join(m.keys())
+ for validation in validations:
+ if keystr == validation:
+ return m
+
+ if len(validations) == 1:
+ val_str = f"'{validations[0]}'"
+ else:
+ val_str = f"one of {validations}"
+
+ raise ScramException(
+ f"Malformed {desc} message. Expected the attribute list to be {val_str} but "
+ f"found '{keystr}'",
+ SERVER_ERROR_OTHER_ERROR,
+ )
+
+
+def _get_client_first(username, c_nonce, channel_binding, use_binding):
+ try:
+ u = saslprep(username)
+ except ScramException as e:
+ raise ScramException(e.args[0], SERVER_ERROR_INVALID_USERNAME_ENCODING)
+
+ bare = ",".join((f"n={u}", f"r={c_nonce}"))
+ _, gs2_header = _make_gs2_header(channel_binding, use_binding)
+ return bare, gs2_header + bare
+
+
+def _set_client_first(client_first, s_nonce, channel_binding, use_binding):
+ try:
+ first_comma = client_first.index(",")
+ second_comma = client_first.index(",", first_comma + 1)
+ except ValueError:
+ raise ScramException(
+ "The client sent a malformed first message",
+ SERVER_ERROR_OTHER_ERROR,
+ )
+ gs2_header = client_first[:second_comma].split(",")
+ try:
+ gs2_cbind_flag = gs2_header[0]
+ gs2_char = gs2_cbind_flag[0]
+ except IndexError:
+ raise ScramException(
+ "The client sent malformed gs2 data",
+ SERVER_ERROR_OTHER_ERROR,
+ )
+ upgrade_mechanism = False
+
+ if gs2_char == "y":
+ if channel_binding is not None:
+ raise ScramException(
+ "Recieved GS2 flag 'y' which indicates that the client doesn't think "
+ "the server supports channel binding, but in fact it does",
+ SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING,
+ )
+
+ elif gs2_char == "n":
+ if use_binding:
+ raise ScramException(
+ "Received GS2 flag 'n' which indicates that the client doesn't require "
+ "channel binding, but the server does",
+ SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING,
+ )
+
+ elif gs2_char == "p":
+ if channel_binding is None:
+ raise ScramException(
+ "Received GS2 flag 'p' which indicates that the client requires "
+ "channel binding, but the server does not",
+ SERVER_ERROR_CHANNEL_BINDING_NOT_SUPPORTED,
+ )
+ if not use_binding:
+ upgrade_mechanism = True
+
+ channel_type, _ = channel_binding
+ cb_name = gs2_cbind_flag.split("=")[-1]
+ if cb_name != channel_type:
+ raise ScramException(
+ f"Received channel binding name {cb_name} but this server supports the "
+ f"channel binding name {channel_type}",
+ SERVER_ERROR_UNSUPPORTED_CHANNEL_BINDING_TYPE,
+ )
+
+ else:
+ raise ScramException(
+ f"Received GS2 flag {gs2_char} which isn't recognized",
+ SERVER_ERROR_OTHER_ERROR,
+ )
+
+ client_first_bare = client_first[second_comma + 1 :]
+ msg = _parse_message(client_first_bare, "client first bare", "nr")
+
+ c_nonce = msg["r"]
+ nonce = c_nonce + s_nonce
+ user = msg["n"]
+
+ return nonce, user, client_first_bare, upgrade_mechanism
+
+
+def _get_server_first(nonce, salt, iterations):
+ return ",".join((f"r={nonce}", f"s={salt}", f"i={iterations}"))
+
+
+def _set_server_first(server_first, c_nonce):
+ msg = _parse_message(server_first, "server first", "rsi")
+ if "e" in msg:
+ raise ScramException(f"The server returned the error: {msg['e']}")
+
+ nonce = msg["r"]
+ salt = msg["s"]
+ iterations = int(msg["i"])
+
+ if not nonce.startswith(c_nonce):
+ raise ScramException("Client nonce doesn't match.", SERVER_ERROR_OTHER_ERROR)
+
+ return nonce, salt, iterations
+
+
+def _get_client_final(
+ hf,
+ password,
+ salt_str,
+ iterations,
+ nonce,
+ client_first_bare,
+ server_first,
+ channel_binding,
+ use_binding,
+):
+ salt = b64dec(salt_str)
+ salted_password = _make_salted_password(hf, password, salt, iterations)
+ client_key, stored_key, server_key = _c_key_stored_key_s_key(hf, salted_password)
+
+ cbind_input = _make_cbind_input(channel_binding, use_binding)
+ client_final_without_proof = f"c={b64enc(cbind_input)},r={nonce}"
+ auth_msg = _make_auth_message(
+ client_first_bare, server_first, client_final_without_proof
+ )
+
+ client_signature = hmac(hf, stored_key, auth_msg)
+ client_proof = xor(client_key, client_signature)
+ server_signature = hmac(hf, server_key, auth_msg)
+ client_final = f"{client_final_without_proof},p={b64enc(client_proof)}"
+ return b64enc(server_signature), client_final
+
+
+SERVER_ERROR_INVALID_ENCODING = "invalid-encoding"
+SERVER_ERROR_EXTENSIONS_NOT_SUPPORTED = "extensions-not-supported"
+SERVER_ERROR_INVALID_PROOF = "invalid-proof"
+SERVER_ERROR_INVALID_ENCODING = "invalid-encoding"
+SERVER_ERROR_CHANNEL_BINDINGS_DONT_MATCH = "channel-bindings-dont-match"
+SERVER_ERROR_SERVER_DOES_SUPPORT_CHANNEL_BINDING = "server-does-support-channel-binding"
+SERVER_ERROR_SERVER_DOES_NOT_SUPPORT_CHANNEL_BINDING = (
+ "server does not support channel binding"
+)
+SERVER_ERROR_CHANNEL_BINDING_NOT_SUPPORTED = "channel-binding-not-supported"
+SERVER_ERROR_UNSUPPORTED_CHANNEL_BINDING_TYPE = "unsupported-channel-binding-type"
+SERVER_ERROR_UNKNOWN_USER = "unknown-user"
+SERVER_ERROR_INVALID_USERNAME_ENCODING = "invalid-username-encoding"
+SERVER_ERROR_NO_RESOURCES = "no-resources"
+SERVER_ERROR_OTHER_ERROR = "other-error"
+
+
+def _set_client_final(
+ hf,
+ client_final,
+ s_nonce,
+ stored_key,
+ server_key,
+ client_first_bare,
+ server_first,
+ channel_binding,
+ use_binding,
+):
+
+ msg = _parse_message(client_final, "client final", "crp")
+ chan_binding = msg["c"]
+
+ nonce = msg["r"]
+ proof = msg["p"]
+ if use_binding and b64dec(chan_binding) != _make_cbind_input(
+ channel_binding, use_binding
+ ):
+ raise ScramException(
+ "The channel bindings don't match.",
+ SERVER_ERROR_CHANNEL_BINDINGS_DONT_MATCH,
+ )
+
+ if not nonce.endswith(s_nonce):
+ raise ScramException("Server nonce doesn't match.", SERVER_ERROR_OTHER_ERROR)
+
+ client_final_without_proof = f"c={chan_binding},r={nonce}"
+ auth_msg = _make_auth_message(
+ client_first_bare, server_first, client_final_without_proof
+ )
+ _check_client_key(hf, stored_key, auth_msg, proof)
+
+ sig = hmac(hf, server_key, auth_msg)
+ return b64enc(sig)
+
+
+def _get_server_final(server_signature, error):
+ return f"v={server_signature}" if error is None else f"e={error}"
+
+
+def _set_server_final(message, server_signature):
+ msg = _parse_message(message, "server final", "v", "e")
+ if "e" in msg:
+ raise ScramException(f"The server returned the error: {msg['e']}")
+
+ if server_signature != msg["v"]:
+ raise ScramException(
+ "The server signature doesn't match.", SERVER_ERROR_OTHER_ERROR
+ )
+
+
+def saslprep(source):
+ # mapping stage
+ # - map non-ascii spaces to U+0020 (stringprep C.1.2)
+ # - strip 'commonly mapped to nothing' chars (stringprep B.1)
+ data = "".join(" " if in_table_c12(c) else c for c in source if not in_table_b1(c))
+
+ # normalize to KC form
+ data = unicodedata.normalize("NFKC", data)
+ if not data:
+ return ""
+
+ # check for invalid bi-directional strings.
+ # stringprep requires the following:
+ # - chars in C.8 must be prohibited.
+ # - if any R/AL chars in string:
+ # - no L chars allowed in string
+ # - first and last must be R/AL chars
+ # this checks if start/end are R/AL chars. if so, prohibited loop
+ # will forbid all L chars. if not, prohibited loop will forbid all
+ # R/AL chars instead. in both cases, prohibited loop takes care of C.8.
+ is_ral_char = in_table_d1
+ if is_ral_char(data[0]):
+ if not is_ral_char(data[-1]):
+ raise ScramException(
+ "malformed bidi sequence", SERVER_ERROR_INVALID_ENCODING
+ )
+ # forbid L chars within R/AL sequence.
+ is_forbidden_bidi_char = in_table_d2
+ else:
+ # forbid R/AL chars if start not setup correctly; L chars allowed.
+ is_forbidden_bidi_char = is_ral_char
+
+ # check for prohibited output
+ # stringprep tables A.1, B.1, C.1.2, C.2 - C.9
+ for c in data:
+ # check for chars mapping stage should have removed
+ assert not in_table_b1(c), "failed to strip B.1 in mapping stage"
+ assert not in_table_c12(c), "failed to replace C.1.2 in mapping stage"
+
+ # check for forbidden chars
+ for f, msg in (
+ (in_table_a1, "unassigned code points forbidden"),
+ (in_table_c21_c22, "control characters forbidden"),
+ (in_table_c3, "private use characters forbidden"),
+ (in_table_c4, "non-char code points forbidden"),
+ (in_table_c5, "surrogate codes forbidden"),
+ (in_table_c6, "non-plaintext chars forbidden"),
+ (in_table_c7, "non-canonical chars forbidden"),
+ (in_table_c8, "display-modifying/deprecated chars forbidden"),
+ (in_table_c9, "tagged characters forbidden"),
+ (is_forbidden_bidi_char, "forbidden bidi character"),
+ ):
+ if f(c):
+ raise ScramException(msg, SERVER_ERROR_INVALID_ENCODING)
+
+ return data
diff --git a/contrib/python/scramp/scramp/utils.py b/contrib/python/scramp/scramp/utils.py
new file mode 100644
index 00000000000..47e454e1ae5
--- /dev/null
+++ b/contrib/python/scramp/scramp/utils.py
@@ -0,0 +1,34 @@
+import hmac as hmaca
+from base64 import b64decode, b64encode
+
+
+def hmac(hf, key, msg):
+ return hmaca.new(key, msg=msg, digestmod=hf).digest()
+
+
+def h(hf, msg):
+ return hf(msg).digest()
+
+
+def hi(hf, password, salt, iterations):
+ u = ui = hmac(hf, password, salt + b"\x00\x00\x00\x01")
+ for i in range(iterations - 1):
+ ui = hmac(hf, password, ui)
+ u = xor(u, ui)
+ return u
+
+
+def xor(bytes1, bytes2):
+ return bytes(a ^ b for a, b in zip(bytes1, bytes2))
+
+
+def b64enc(binary):
+ return b64encode(binary).decode("utf8")
+
+
+def b64dec(string):
+ return b64decode(string)
+
+
+def uenc(string):
+ return string.encode("utf-8")
diff --git a/contrib/python/scramp/ya.make b/contrib/python/scramp/ya.make
new file mode 100644
index 00000000000..f2b3de220b2
--- /dev/null
+++ b/contrib/python/scramp/ya.make
@@ -0,0 +1,28 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(1.4.4)
+
+LICENSE(MIT-0)
+
+PEERDIR(
+ contrib/python/asn1crypto
+)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ scramp/__init__.py
+ scramp/core.py
+ scramp/utils.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/scramp/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()