Skip to content

Add ed25519 auth support #791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
cryptography
PyNaCl>=1.4.0
pytest
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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