Skip to content

Commit

Permalink
Support for MariaDB's auth_ed25519 authentication plugin (#786)
Browse files Browse the repository at this point in the history
  • Loading branch information
dciabrin committed Jul 16, 2020
1 parent 466ecfe commit e96aba0
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 3 deletions.
8 changes: 6 additions & 2 deletions .travis.yml
Expand Up @@ -23,6 +23,7 @@ matrix:
python: "2.7"
- env:
- DB=mariadb:10.3
- TEST_MARIADB_AUTH=yes
python: "3.7"
- env:
- DB=mysql:5.5
Expand All @@ -46,7 +47,7 @@ matrix:
# http://dev.mysql.com/downloads/mysql/5.7.html has latest development release version
# really only need libaio1 for DB builds however libaio-dev is whitelisted for container builds and liaio1 isn't
install:
- pip install -U coveralls coverage cryptography pytest pytest-cov
- pip install -U coveralls coverage cryptography PyNaCl pytest pytest-cov

before_script:
- ./.travis/initializedb.sh
Expand All @@ -57,7 +58,10 @@ before_script:
script:
- pytest -v --cov --cov-config .coveragerc pymysql
- if [ "${TEST_AUTH}" = "yes" ];
then pytest -v --cov --cov-config .coveragerc tests;
then pytest -v --cov --cov-config .coveragerc tests/test_auth.py;
fi
- if [ "${TEST_MARIADB_AUTH}" = "yes" ];
then pytest -v --cov --cov-config .coveragerc tests/test_mariadb_auth.py;
fi
- if [ ! -z "${DB}" ];
then docker logs mysqld;
Expand Down
10 changes: 9 additions & 1 deletion .travis/initializedb.sh
Expand Up @@ -6,7 +6,7 @@ docker pull ${DB}
docker run -it --name=mysqld -d -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 ${DB}

mysql() {
docker exec mysqld mysql "${@}"
docker exec -i mysqld mysql "${@}"
}
while :
do
Expand All @@ -33,6 +33,14 @@ if [ $DB == 'mysql:8.0' ]; then
nopass_caching_sha2 IDENTIFIED WITH "caching_sha2_password"
PASSWORD EXPIRE NEVER;'
mysql -e 'GRANT RELOAD ON *.* TO user_caching_sha2;'
elif [[ $DB == mariadb:10.* ]] && [ ${DB#mariadb:10.} -ge 3 ]; then
mysql -e '
INSTALL SONAME "auth_ed25519";
CREATE FUNCTION ed25519_password RETURNS STRING SONAME "auth_ed25519.so";'
# we need to pass the hashed password manually until 10.4, so hide it here
mysql -sNe "SELECT CONCAT('CREATE USER nopass_ed25519 IDENTIFIED VIA ed25519 USING \"',ed25519_password(\"\"),'\";');" | mysql
mysql -sNe "SELECT CONCAT('CREATE USER user_ed25519 IDENTIFIED VIA ed25519 USING \"',ed25519_password(\"pass_ed25519\"),'\";');" | mysql
WITH_PLUGIN=''
else
WITH_PLUGIN=''
fi
Expand Down
5 changes: 5 additions & 0 deletions README.rst
Expand Up @@ -66,6 +66,11 @@ you need to install additional dependency::

$ python3 -m pip install PyMySQL[rsa]

To use MariaDB's "ed25519" authentication method, you need to install
additional dependency::

$ python3 -m pip install PyMySQL[ed25519]


Documentation
-------------
Expand Down
60 changes: 60 additions & 0 deletions pymysql/_auth.py
Expand Up @@ -113,6 +113,66 @@ def _hash_password_323(password):
return struct.pack(">LL", r1, r2)


# MariaDB's client_ed25519-plugin
# https://mariadb.com/kb/en/library/connection/#client_ed25519-plugin

_nacl_bindings = False


def _init_nacl():
global _nacl_bindings
try:
from nacl import bindings
_nacl_bindings = bindings
except ImportError:
raise RuntimeError("'pynacl' package is required for ed25519_password auth method")


def _scalar_clamp(s32):
ba = bytearray(s32)
ba0 = bytes(bytearray([ba[0] & 248]))
ba31 = bytes(bytearray([(ba[31] & 127) | 64]))
return ba0 + bytes(s32[1:31]) + ba31


def ed25519_password(password, scramble):
"""Sign a random scramble with elliptic curve Ed25519.
Secret and public key are derived from password.
"""
# variable names based on rfc8032 section-5.1.6
#
if not _nacl_bindings:
_init_nacl()

# h = SHA512(password)
h = hashlib.sha512(password).digest()

# s = prune(first_half(h))
s = _scalar_clamp(h[:32])

# r = SHA512(second_half(h) || M)
r = hashlib.sha512(h[32:] + scramble).digest()

# R = encoded point [r]B
r = _nacl_bindings.crypto_core_ed25519_scalar_reduce(r)
R = _nacl_bindings.crypto_scalarmult_ed25519_base_noclamp(r)

# A = encoded point [s]B
A = _nacl_bindings.crypto_scalarmult_ed25519_base_noclamp(s)

# k = SHA512(R || A || M)
k = hashlib.sha512(R + A + scramble).digest()

# S = (k * s + r) mod L
k = _nacl_bindings.crypto_core_ed25519_scalar_reduce(k)
ks = _nacl_bindings.crypto_core_ed25519_scalar_mul(k, s)
S = _nacl_bindings.crypto_core_ed25519_scalar_add(ks, r)

# signature = R || S
return R + S


# sha256_password


Expand Down
2 changes: 2 additions & 0 deletions pymysql/connections.py
Expand Up @@ -894,6 +894,8 @@ def _process_auth(self, plugin_name, auth_packet):
return _auth.sha256_password_auth(self, auth_packet)
elif plugin_name == b"mysql_native_password":
data = _auth.scramble_native_password(self.password, auth_packet.read_all())
elif plugin_name == b'client_ed25519':
data = _auth.ed25519_password(self.password, auth_packet.read_all())
elif plugin_name == b"mysql_old_password":
data = _auth.scramble_old_password(self.password, auth_packet.read_all()) + b'\0'
elif plugin_name == b"mysql_clear_password":
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
@@ -1,2 +1,3 @@
cryptography
PyNaCl>=1.4.0
pytest
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -19,6 +19,7 @@
packages=find_packages(exclude=['tests*', 'pymysql.tests*']),
extras_require={
"rsa": ["cryptography"],
"ed25519": ["PyNaCl>=1.4.0"],
},
classifiers=[
'Development Status :: 5 - Production/Stable',
Expand Down
23 changes: 23 additions & 0 deletions tests/test_mariadb_auth.py
@@ -0,0 +1,23 @@
"""Test for auth methods supported by MariaDB 10.3+"""

import pymysql

# pymysql.connections.DEBUG = True
# pymysql._auth.DEBUG = True

host = "127.0.0.1"
port = 3306


def test_ed25519_no_password():
con = pymysql.connect(user="nopass_ed25519", host=host, port=port, ssl=None)
con.close()


def test_ed25519_password(): # nosec
con = pymysql.connect(user="user_ed25519", password="pass_ed25519",
host=host, port=port, ssl=None)
con.close()


# default mariadb docker images aren't configured with SSL

0 comments on commit e96aba0

Please sign in to comment.