From 227f5b0e6d4c54d95c1f4730f2f6abfc8d94cf90 Mon Sep 17 00:00:00 2001 From: "Bradley Grainger (aider)" Date: Mon, 27 Jan 2025 06:49:40 -0800 Subject: [PATCH 1/3] Add Parsec authentication plugin implementation. Create a new ServerFeature for parsec. Drop netstandard2.0 support. PBKDF2-SHA512 support was only added in .NET Framework 4.7.2 and .NET Standard 2.1. Signed-off-by: Bradley Grainger --- .ci/config/config.compression.json | 2 +- .ci/config/config.json | 2 +- .ci/docker-run.sh | 6 ++ .ci/server/init_parsec.sql | 3 + Directory.Packages.props | 1 + azure-pipelines.yml | 18 ++-- .../Chaos.NaCl/CryptoBytes.cs | 37 +++++++ .../Chaos.NaCl/Ed25519.cs | 63 ++++++++++++ .../Internal/Ed25519Ref10/GroupElement.cs | 99 ++++++++++--------- .../Chaos.NaCl/Internal/Ed25519Ref10/base2.cs | 50 ++++++++++ .../Internal/Ed25519Ref10/keypair.cs | 33 +++++++ .../Chaos.NaCl/Internal/Ed25519Ref10/sign.cs | 50 ++++++++++ .../Internal/Ed25519Ref10/sqrtm1.cs | 9 ++ .../CompatibilitySuppressions.xml | 2 +- ...SqlConnector.Authentication.Ed25519.csproj | 18 +++- .../ParsecAuthenticationPlugin.cs | 90 +++++++++++++++++ .../docs/README.md | 10 +- src/MySqlConnector/Core/ServerSession.cs | 15 +++ tests/IntegrationTests/ConnectAsync.cs | 19 +++- tests/IntegrationTests/ServerFeatures.cs | 5 + 20 files changed, 464 insertions(+), 68 deletions(-) create mode 100644 .ci/server/init_parsec.sql create mode 100644 src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs create mode 100644 src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs create mode 100644 src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs create mode 100644 src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs create mode 100644 src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs create mode 100644 src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs create mode 100644 src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs diff --git a/.ci/config/config.compression.json b/.ci/config/config.compression.json index 09326f1f6..448eb1b2b 100644 --- a/.ci/config/config.compression.json +++ b/.ci/config/config.compression.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime", + "UnsupportedFeatures": "Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV" } diff --git a/.ci/config/config.json b/.ci/config/config.json index bc38f605a..e32c72260 100644 --- a/.ci/config/config.json +++ b/.ci/config/config.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime", + "UnsupportedFeatures": "Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV" } diff --git a/.ci/docker-run.sh b/.ci/docker-run.sh index 7a791e21d..1247f47c2 100755 --- a/.ci/docker-run.sh +++ b/.ci/docker-run.sh @@ -79,6 +79,12 @@ for i in `seq 1 120`; do if [ $? -ne 0 ]; then exit $?; fi fi + if [[ $OMIT_FEATURES != *"ParsecAuthentication"* ]]; then + echo "Installing auth_parsec component" + docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init_parsec.sql" + if [ $? -ne 0 ]; then exit $?; fi + fi + if [[ $OMIT_FEATURES != *"QueryAttributes"* ]]; then echo "Installing query_attributes component" docker exec mysql $MYSQL -uroot -ptest -e "INSTALL COMPONENT 'file://component_query_attributes';" diff --git a/.ci/server/init_parsec.sql b/.ci/server/init_parsec.sql new file mode 100644 index 000000000..85c8d6894 --- /dev/null +++ b/.ci/server/init_parsec.sql @@ -0,0 +1,3 @@ +INSTALL SONAME 'auth_parsec'; +CREATE USER 'parsec-user'@'%' IDENTIFIED via parsec using PASSWORD('P@rs3c-Pa55'); +GRANT ALL PRIVILEGES ON *.* TO 'parsec-user'@'%'; diff --git a/Directory.Packages.props b/Directory.Packages.props index 25fe3b765..9f75afc5a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58e7b9801..d7cba324e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -51,7 +51,7 @@ jobs: arguments: 'tests\IntegrationTests\IntegrationTests.csproj -c MySqlData' testRunTitle: 'MySql.Data integration tests' env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,TlsFingerprintValidation,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,StreamingResults,TlsFingerprintValidation,UnixDomainSocket' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=root;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600' DATA__CERTIFICATESPATH: '$(Build.Repository.LocalPath)\.ci\server\certs\' DATA__MYSQLBULKLOADERLOCALCSVFILE: '$(Build.Repository.LocalPath)\tests\TestData\LoadData_UTF8_BOM_Unix.CSV' @@ -120,7 +120,7 @@ jobs: arguments: '-c Release --no-restore -p:TestTfmsInParallel=false' testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net481/net9.0', 'No SSL') }} env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True' - job: windows_integration_tests_2 @@ -158,7 +158,7 @@ jobs: arguments: '-c Release --no-restore -p:TestTfmsInParallel=false' testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net8.0', 'No SSL') }} env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True' - job: linux_integration_tests @@ -171,27 +171,27 @@ jobs: 'MySQL 8.0': image: 'mysql:8.0' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' + unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' 'MySQL 8.4': image: 'mysql:8.4' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' + unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' 'MySQL 9.3': image: 'mysql:9.3' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' + unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime' 'MariaDB 10.6': image: 'mariadb:10.6' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin' 'MariaDB 10.11': image: 'mariadb:10.11' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin' 'MariaDB 11.4': image: 'mariadb:11.4' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Sha256Password,Tls11,UuidToBin,Redirection' 'MariaDB 11.6': image: 'mariadb:11.6' connectionStringExtra: '' diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs new file mode 100644 index 000000000..c637b6f98 --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Chaos.NaCl +{ + internal static class CryptoBytes + { + public static void Wipe(byte[] data) + { + if (data == null) + throw new ArgumentNullException("data"); + InternalWipe(data, 0, data.Length); + } + + // Secure wiping is hard + // * the GC can move around and copy memory + // Perhaps this can be avoided by using unmanaged memory or by fixing the position of the array in memory + // * Swap files and error dumps can contain secret information + // It seems possible to lock memory in RAM, no idea about error dumps + // * Compiler could optimize out the wiping if it knows that data won't be read back + // I hope this is enough, suppressing inlining + // but perhaps `RtlSecureZeroMemory` is needed + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void InternalWipe(byte[] data, int offset, int count) + { + Array.Clear(data, offset, count); + } + + // shallow wipe of structs + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void InternalWipe(ref T data) + where T : struct + { + data = default(T); + } + } +} diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs new file mode 100644 index 000000000..613f8330a --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs @@ -0,0 +1,63 @@ +using System; +using System.Security.Cryptography; +using Chaos.NaCl.Internal.Ed25519Ref10; + +namespace Chaos.NaCl +{ + internal static class Ed25519 + { + public static readonly int PublicKeySizeInBytes = 32; + public static readonly int SignatureSizeInBytes = 64; + public static readonly int ExpandedPrivateKeySizeInBytes = 32 * 2; + public static readonly int PrivateKeySeedSizeInBytes = 32; + public static readonly int SharedKeySizeInBytes = 32; + + public static void Sign(ArraySegment signature, ArraySegment message, ArraySegment expandedPrivateKey) + { + if (signature.Array == null) + throw new ArgumentNullException("signature.Array"); + if (signature.Count != SignatureSizeInBytes) + throw new ArgumentException("signature.Count"); + if (expandedPrivateKey.Array == null) + throw new ArgumentNullException("expandedPrivateKey.Array"); + if (expandedPrivateKey.Count != ExpandedPrivateKeySizeInBytes) + throw new ArgumentException("expandedPrivateKey.Count"); + if (message.Array == null) + throw new ArgumentNullException("message.Array"); + Ed25519Operations.crypto_sign2(signature.Array, signature.Offset, message.Array, message.Offset, message.Count, expandedPrivateKey.Array, expandedPrivateKey.Offset); + } + + public static byte[] Sign(byte[] message, byte[] expandedPrivateKey) + { + var signature = new byte[SignatureSizeInBytes]; + Sign(new ArraySegment(signature), new ArraySegment(message), new ArraySegment(expandedPrivateKey)); + return signature; + } + + public static byte[] ExpandedPrivateKeyFromSeed(byte[] privateKeySeed) + { + byte[] privateKey; + byte[] publicKey; + KeyPairFromSeed(out publicKey, out privateKey, privateKeySeed); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(publicKey); +#else + CryptoBytes.Wipe(publicKey); +#endif + return privateKey; + } + + public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed) + { + if (privateKeySeed == null) + throw new ArgumentNullException("privateKeySeed"); + if (privateKeySeed.Length != PrivateKeySeedSizeInBytes) + throw new ArgumentException("privateKeySeed"); + var pk = new byte[PublicKeySizeInBytes]; + var sk = new byte[ExpandedPrivateKeySizeInBytes]; + Ed25519Operations.crypto_sign_keypair(pk, 0, sk, 0, privateKeySeed, 0); + publicKey = pk; + expandedPrivateKey = sk; + } + } +} diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs index 9ae034b6e..abeaca869 100644 --- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs @@ -1,52 +1,63 @@ -namespace Chaos.NaCl.Internal.Ed25519Ref10; +using System; -/* -ge means group element. - -Here the group is the set of pairs (x,y) of field elements (see fe.h) -satisfying -x^2 + y^2 = 1 + d x^2y^2 -where d = -121665/121666. +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + /* + ge means group element. -Representations: - ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z - ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT - ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T - ge_precomp (Duif): (y+x,y-x,2dxy) -*/ + Here the group is the set of pairs (x,y) of field elements (see fe.h) + satisfying -x^2 + y^2 = 1 + d x^2y^2 + where d = -121665/121666. -internal struct GroupElementP2 -{ - public FieldElement X; - public FieldElement Y; - public FieldElement Z; -} ; + Representations: + ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge_precomp (Duif): (y+x,y-x,2dxy) + */ -internal struct GroupElementP3 -{ - public FieldElement X; - public FieldElement Y; - public FieldElement Z; - public FieldElement T; -} ; + internal struct GroupElementP2 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + } ; -internal struct GroupElementP1P1 -{ - public FieldElement X; - public FieldElement Y; - public FieldElement Z; - public FieldElement T; -} ; + internal struct GroupElementP3 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + public FieldElement T; + } ; -internal struct GroupElementPreComp -{ - public FieldElement yplusx; - public FieldElement yminusx; - public FieldElement xy2d; + internal struct GroupElementP1P1 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + public FieldElement T; + } ; - public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d) + internal struct GroupElementPreComp + { + public FieldElement yplusx; + public FieldElement yminusx; + public FieldElement xy2d; + + public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d) + { + this.yplusx = yplusx; + this.yminusx = yminusx; + this.xy2d = xy2d; + } + } ; + + internal struct GroupElementCached { - this.yplusx = yplusx; - this.yminusx = yminusx; - this.xy2d = xy2d; - } -} ; + public FieldElement YplusX; + public FieldElement YminusX; + public FieldElement Z; + public FieldElement T2d; + } ; +} diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs new file mode 100644 index 000000000..03676e5cf --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs @@ -0,0 +1,50 @@ +using System; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static readonly GroupElementPreComp[] Base2 = new GroupElementPreComp[]{ + new GroupElementPreComp( + new FieldElement( 25967493,-14356035,29566456,3660896,-12694345,4014787,27544626,-11754271,-6079156,2047605 ), + new FieldElement( -12545711,934262,-2722910,3049990,-727428,9406986,12720692,5043384,19500929,-15469378 ), + new FieldElement( -8738181,4489570,9688441,-14785194,10184609,-12363380,29287919,11864899,-24514362,-4438546 ) + ), + new GroupElementPreComp( + new FieldElement( 15636291,-9688557,24204773,-7912398,616977,-16685262,27787600,-14772189,28944400,-1550024 ), + new FieldElement( 16568933,4717097,-11556148,-1102322,15682896,-11807043,16354577,-11775962,7689662,11199574 ), + new FieldElement( 30464156,-5976125,-11779434,-15670865,23220365,15915852,7512774,10017326,-17749093,-9920357 ) + ), + new GroupElementPreComp( + new FieldElement( 10861363,11473154,27284546,1981175,-30064349,12577861,32867885,14515107,-15438304,10819380 ), + new FieldElement( 4708026,6336745,20377586,9066809,-11272109,6594696,-25653668,12483688,-12668491,5581306 ), + new FieldElement( 19563160,16186464,-29386857,4097519,10237984,-4348115,28542350,13850243,-23678021,-15815942 ) + ), + new GroupElementPreComp( + new FieldElement( 5153746,9909285,1723747,-2777874,30523605,5516873,19480852,5230134,-23952439,-15175766 ), + new FieldElement( -30269007,-3463509,7665486,10083793,28475525,1649722,20654025,16520125,30598449,7715701 ), + new FieldElement( 28881845,14381568,9657904,3680757,-20181635,7843316,-31400660,1370708,29794553,-1409300 ) + ), + new GroupElementPreComp( + new FieldElement( -22518993,-6692182,14201702,-8745502,-23510406,8844726,18474211,-1361450,-13062696,13821877 ), + new FieldElement( -6455177,-7839871,3374702,-4740862,-27098617,-10571707,31655028,-7212327,18853322,-14220951 ), + new FieldElement( 4566830,-12963868,-28974889,-12240689,-7602672,-2830569,-8514358,-10431137,2207753,-3209784 ) + ), + new GroupElementPreComp( + new FieldElement( -25154831,-4185821,29681144,7868801,-6854661,-9423865,-12437364,-663000,-31111463,-16132436 ), + new FieldElement( 25576264,-2703214,7349804,-11814844,16472782,9300885,3844789,15725684,171356,6466918 ), + new FieldElement( 23103977,13316479,9739013,-16149481,817875,-15038942,8965339,-14088058,-30714912,16193877 ) + ), + new GroupElementPreComp( + new FieldElement( -33521811,3180713,-2394130,14003687,-16903474,-16270840,17238398,4729455,-18074513,9256800 ), + new FieldElement( -25182317,-4174131,32336398,5036987,-21236817,11360617,22616405,9761698,-19827198,630305 ), + new FieldElement( -13720693,2639453,-24237460,-7406481,9494427,-5774029,-6554551,-15960994,-2449256,-14291300 ) + ), + new GroupElementPreComp( + new FieldElement( -3151181,-5046075,9282714,6866145,-31907062,-863023,-18940575,15033784,25105118,-7894876 ), + new FieldElement( -24326370,15950226,-31801215,-14592823,-11662737,-5090925,1573892,-2625887,2198790,-15804619 ), + new FieldElement( -3099351,10324967,-2241613,7453183,-5446979,-2735503,-13812022,-16236442,-32461234,-12290683 ) + ) + }; + } +} diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs new file mode 100644 index 000000000..037efcfd1 --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Cryptography; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class Ed25519Operations + { + public static void crypto_sign_keypair(byte[] pk, int pkoffset, byte[] sk, int skoffset, byte[] seed, int seedoffset) + { + GroupElementP3 A; + int i; + + Array.Copy(seed, seedoffset, sk, skoffset, 32); +#if NET5_0_OR_GREATER + byte[] h = SHA512.HashData(sk.AsSpan(skoffset, 32)); +#else + using var hash = SHA512.Create(); + byte[] h = hash.ComputeHash(sk, skoffset, 32); +#endif + ScalarOperations.sc_clamp(h, 0); + + GroupOperations.ge_scalarmult_base(out A, h, 0); + GroupOperations.ge_p3_tobytes(pk, pkoffset, ref A); + + for (i = 0; i < 32; ++i) sk[skoffset + 32 + i] = pk[pkoffset + i]; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(h); +#else + CryptoBytes.Wipe(h); +#endif + } + } +} diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs new file mode 100644 index 000000000..a56625557 --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs @@ -0,0 +1,50 @@ +using System; +using System.Security.Cryptography; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class Ed25519Operations + { + public static void crypto_sign2( + byte[] sig, int sigoffset, + byte[] m, int moffset, int mlen, + byte[] sk, int skoffset) + { + byte[] az; + byte[] r; + byte[] hram; + GroupElementP3 R; + using (var hasher = SHA512.Create()) + { + az = hasher.ComputeHash(sk, skoffset, 32); + ScalarOperations.sc_clamp(az, 0); + + hasher.Initialize(); + hasher.TransformBlock(az, 32, 32, null, 0); + hasher.TransformFinalBlock(m, moffset, mlen); + r = hasher.Hash; + + ScalarOperations.sc_reduce(r); + GroupOperations.ge_scalarmult_base(out R, r, 0); + GroupOperations.ge_p3_tobytes(sig, sigoffset, ref R); + + hasher.Initialize(); + hasher.TransformBlock(sig, sigoffset, 32, null, 0); + hasher.TransformBlock(sk, skoffset + 32, 32, null, 0); + hasher.TransformFinalBlock(m, moffset, mlen); + hram = hasher.Hash; + + ScalarOperations.sc_reduce(hram); + var s = new byte[32];//todo: remove allocation + Array.Copy(sig, sigoffset + 32, s, 0, 32); + ScalarOperations.sc_muladd(s, hram, az, r); + Array.Copy(s, 0, sig, sigoffset + 32, 32); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(s); +#else + CryptoBytes.Wipe(s); +#endif + } + } + } +} diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs new file mode 100644 index 000000000..fb8b50122 --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs @@ -0,0 +1,9 @@ +using System; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static FieldElement sqrtm1 = new FieldElement(-32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482); + } +} \ No newline at end of file diff --git a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml index 738dbc799..e66567aca 100644 --- a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml +++ b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + PKV006 diff --git a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj index bd2122bb3..37349bfd2 100644 --- a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj +++ b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj @@ -1,22 +1,30 @@ - net462;netstandard2.0 + net462;net472;netstandard2.0;netstandard2.1;net6.0 MySqlConnector Ed25519 Authentication Plugin - Implements the client_ed25519 authentication plugin for MariaDB. + Implements the client_ed25519 and parsec authentication plugins for MariaDB. Copyright 2019–2024 Bradley Grainger Bradley Grainger README.md - mariadb;mysqlconnector;authentication;ed25519 - SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1106;SA1107;SA1111;SA1119;SA1121;SA1300;SA1307;SA1312;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1512;SA1518;SA1601 + mariadb;mysqlconnector;authentication;ed25519;parsec + CA1305;CA1507;CA1802;CA2208;CS0649;IDE0049;SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1028;SA1106;SA1107;SA1111;SA1119;SA1121;SA1124;SA1137;SA1214;SA1300;SA1307;SA1309;SA1312;SA1313;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1509;SA1512;SA1515;SA1518;SA1520;SA1601 + + + + + + + + - + diff --git a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs new file mode 100644 index 000000000..91a579b3e --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs @@ -0,0 +1,90 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace MySqlConnector.Authentication.Ed25519; + +/// +/// Provides an implementation of the Parsec authentication plugin for MariaDB. +/// +public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin +{ + /// + /// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before + /// opening a connection that uses Parsec authentication. + /// + public static void Install() + { + if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0) + AuthenticationPlugins.Register(new ParsecAuthenticationPlugin()); + } + + /// + /// Gets the authentication plugin name. + /// + public string Name => "parsec"; + + /// + /// Creates the authentication response. + /// + public byte[] CreateResponse(string password, ReadOnlySpan authenticationData) + { + // first 32 bytes are server scramble + var serverScramble = authenticationData.Slice(0, 32); + + // generate client scramble +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + Span clientScramble = stackalloc byte[32]; + RandomNumberGenerator.Fill(clientScramble); +#else + var clientScramble = new byte[32]; + using var randomNumberGenerator = RandomNumberGenerator.Create(); + randomNumberGenerator.GetBytes(clientScramble); +#endif + + // parse extended salt from remaining authentication data and verify format + var extendedSalt = authenticationData.Slice(32); + if (extendedSalt[0] != (byte) 'P') + throw new ArgumentException("Invalid extended salt", nameof(authenticationData)); + if (extendedSalt[1] is not (>= 0 and <= 3)) + throw new ArgumentException("Invalid iteration count", nameof(authenticationData)); + + var iterationCount = 1024 << extendedSalt[1]; + var salt = extendedSalt.Slice(2); + + // derive private key using PBKDF2-SHA512 + byte[] privateKey; +#if NET6_0_OR_GREATER + privateKey = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32); +#elif NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER + using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512)) + privateKey = pbkdf2.GetBytes(32); +#else + privateKey = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2( + password, salt.ToArray(), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512, + iterationCount, numBytesRequested: 32); +#endif + var expandedPrivateKey = Chaos.NaCl.Ed25519.ExpandedPrivateKeyFromSeed(privateKey); + + // generate Ed25519 keypair and sign concatenated scrambles + var message = new byte[serverScramble.Length + clientScramble.Length]; + serverScramble.CopyTo(message); + clientScramble.CopyTo(message.AsSpan(serverScramble.Length)); + + var signature = Chaos.NaCl.Ed25519.Sign(message, expandedPrivateKey); + + // return client scramble followed by signature + var response = new byte[clientScramble.Length + signature.Length]; + clientScramble.CopyTo(response.AsSpan()); + signature.CopyTo(response.AsSpan(clientScramble.Length)); + + return response; + } + + private ParsecAuthenticationPlugin() + { + } + + private static int s_isInstalled; +} diff --git a/src/MySqlConnector.Authentication.Ed25519/docs/README.md b/src/MySqlConnector.Authentication.Ed25519/docs/README.md index 0dd5a9af7..7f17e4c81 100644 --- a/src/MySqlConnector.Authentication.Ed25519/docs/README.md +++ b/src/MySqlConnector.Authentication.Ed25519/docs/README.md @@ -1,7 +1,13 @@ ## About -This package implements the `client_ed25519` [authentication plugin for MariaDB](https://mariadb.com/kb/en/authentication-plugin-ed25519/). +This package implements the following authentication plugins for MariaDB: + +* [`client_ed25519`](https://mariadb.com/kb/en/authentication-plugin-ed25519/). +* [PARSEC](https://mariadb.com/kb/en/authentication-plugin-parsec/) ## How to Use -Call `Ed25519AuthenticationPlugin.Install()` from your application startup code to enable it. +Call either the following methods from your application startup code to enable the corresponding authentication plugin: + +* `Ed25519AuthenticationPlugin.Install()` +* `ParsecAuthenticationPlugin.Install()` diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 67e5a438e..6f740c38b 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -937,6 +937,21 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + case "parsec": + if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin)) + throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication."); + payload = new([]); + await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); + payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + + Span combinedData = stackalloc byte[switchRequest.Data.Length + payload.Span.Length]; + switchRequest.Data.CopyTo(combinedData); + payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length)); + + payload = new(parsecPlugin.CreateResponse(password, combinedData)); + await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); + return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + default: Log.AuthenticationMethodNotSupported(m_logger, Id, switchRequest.Name); throw new NotSupportedException($"Authentication method '{switchRequest.Name}' is not supported."); diff --git a/tests/IntegrationTests/ConnectAsync.cs b/tests/IntegrationTests/ConnectAsync.cs index ce31fdf55..03e69afde 100644 --- a/tests/IntegrationTests/ConnectAsync.cs +++ b/tests/IntegrationTests/ConnectAsync.cs @@ -1,7 +1,4 @@ using System.Security.Authentication; -#if !MYSQL_DATA -using MySqlConnector.Authentication.Ed25519; -#endif namespace IntegrationTests; @@ -429,7 +426,7 @@ public async Task CachingSha2WithoutSecureConnection() [SkippableFact(ServerFeatures.Ed25519)] public async Task Ed25519Authentication() { - Ed25519AuthenticationPlugin.Install(); + MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install(); var csb = AppConfig.CreateConnectionStringBuilder(); csb.UserID = "ed25519user"; @@ -442,7 +439,7 @@ public async Task Ed25519Authentication() [SkippableFact(ServerFeatures.Ed25519)] public async Task MultiAuthentication() { - Ed25519AuthenticationPlugin.Install(); + MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install(); var csb = AppConfig.CreateConnectionStringBuilder(); csb.UserID = "multiAuthUser"; csb.Password = "secret"; @@ -450,6 +447,18 @@ public async Task MultiAuthentication() using var connection = new MySqlConnection(csb.ConnectionString); await connection.OpenAsync(); } + + [SkippableFact(ServerFeatures.ParsecAuthentication)] + public async Task Parsec() + { + MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install(); + var csb = AppConfig.CreateConnectionStringBuilder(); + csb.UserID = "parsec-user"; + csb.Password = "P@rs3c-Pa55"; + csb.Database = null; + using var connection = new MySqlConnection(csb.ConnectionString); + await connection.OpenAsync(); + } #endif // To create a MariaDB GSSAPI user for a current user diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs index 120b541bf..f78c4829d 100644 --- a/tests/IntegrationTests/ServerFeatures.cs +++ b/tests/IntegrationTests/ServerFeatures.cs @@ -45,4 +45,9 @@ public enum ServerFeatures /// Server provides hash of TLS certificate in first OK packet. /// TlsFingerprintValidation = 0x100_0000, + + /// + /// Server supports the 'parsec' authentication plugin. + /// + ParsecAuthentication = 0x200_0000, } From ec3619e31f2cb313d239051f425ddfd39c70c994 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Mon, 27 Jan 2025 09:47:57 -0800 Subject: [PATCH 2/3] Enable TLS validation with parsec. Introduce new IAuthenticationPlugin3 interface and deprecate IAuthenticationPlugin2. Authentication plugins will now compute the password hash and the authentication response in one call, and the session will cache the password hash for later use. Signed-off-by: Bradley Grainger --- .../Chaos.NaCl/Ed25519.cs | 13 ---- .../Ed25519AuthenticationPlugin.cs | 19 +++-- .../ParsecAuthenticationPlugin.cs | 38 ++++++---- .../docs/README.md | 2 +- .../Authentication/IAuthenticationPlugin.cs | 19 +++++ src/MySqlConnector/Core/ServerSession.cs | 69 ++++++++----------- .../Payloads/HandshakeResponse41Payload.cs | 6 +- .../Serialization/AuthenticationUtility.cs | 27 ++++---- tests/IntegrationTests/SslTests.cs | 14 ++++ 9 files changed, 121 insertions(+), 86 deletions(-) diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs index 613f8330a..f3a0b7011 100644 --- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs +++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs @@ -34,19 +34,6 @@ public static byte[] Sign(byte[] message, byte[] expandedPrivateKey) return signature; } - public static byte[] ExpandedPrivateKeyFromSeed(byte[] privateKeySeed) - { - byte[] privateKey; - byte[] publicKey; - KeyPairFromSeed(out publicKey, out privateKey, privateKeySeed); -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - CryptographicOperations.ZeroMemory(publicKey); -#else - CryptoBytes.Wipe(publicKey); -#endif - return privateKey; - } - public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed) { if (privateKeySeed == null) diff --git a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs index f2ea62c3e..cd123acf2 100644 --- a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs +++ b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs @@ -10,7 +10,9 @@ namespace MySqlConnector.Authentication.Ed25519; /// Provides an implementation of the client_ed25519 authentication plugin for MariaDB. /// /// See Authentication Plugin - ed25519. -public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2 +#pragma warning disable CS0618 // Type or member is obsolete +public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3, IAuthenticationPlugin2 +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before @@ -32,7 +34,7 @@ public static void Install() /// public byte[] CreateResponse(string password, ReadOnlySpan authenticationData) { - CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse); + CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _); return authenticationResponse; } @@ -41,11 +43,20 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD /// public byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData) { - CreateResponseAndHash(password, authenticationData, out var passwordHash, out _); + CreateResponseAndPasswordHash(password, authenticationData, out _, out var passwordHash); return passwordHash; } - private static void CreateResponseAndHash(string password, ReadOnlySpan authenticationData, out byte[] passwordHash, out byte[] authenticationResponse) + /// + /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification). + /// + /// The client's password. + /// The authentication data supplied by the server; this is the auth method data + /// from the Authentication + /// Method Switch Request Packet. + /// The authentication response. + /// The authentication-method-specific hash of the client's password. + public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash) { // Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java // C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7 diff --git a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs index 91a579b3e..d10011216 100644 --- a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs +++ b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs @@ -8,7 +8,7 @@ namespace MySqlConnector.Authentication.Ed25519; /// /// Provides an implementation of the Parsec authentication plugin for MariaDB. /// -public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin +public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3 { /// /// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before @@ -29,6 +29,15 @@ public static void Install() /// Creates the authentication response. /// public byte[] CreateResponse(string password, ReadOnlySpan authenticationData) + { + CreateResponseAndPasswordHash(password, authenticationData, out var response, out _); + return response; + } + + /// + /// Creates the authentication response. + /// + public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash) { // first 32 bytes are server scramble var serverScramble = authenticationData.Slice(0, 32); @@ -54,32 +63,37 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD var salt = extendedSalt.Slice(2); // derive private key using PBKDF2-SHA512 - byte[] privateKey; + byte[] privateKeySeed; #if NET6_0_OR_GREATER - privateKey = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32); + privateKeySeed = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32); #elif NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512)) - privateKey = pbkdf2.GetBytes(32); + privateKeySeed = pbkdf2.GetBytes(32); #else - privateKey = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2( + privateKeySeed = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2( password, salt.ToArray(), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512, iterationCount, numBytesRequested: 32); #endif - var expandedPrivateKey = Chaos.NaCl.Ed25519.ExpandedPrivateKeyFromSeed(privateKey); + Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed); // generate Ed25519 keypair and sign concatenated scrambles var message = new byte[serverScramble.Length + clientScramble.Length]; serverScramble.CopyTo(message); clientScramble.CopyTo(message.AsSpan(serverScramble.Length)); - var signature = Chaos.NaCl.Ed25519.Sign(message, expandedPrivateKey); + var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(privateKey); +#endif // return client scramble followed by signature - var response = new byte[clientScramble.Length + signature.Length]; - clientScramble.CopyTo(response.AsSpan()); - signature.CopyTo(response.AsSpan(clientScramble.Length)); - - return response; + authenticationResponse = new byte[clientScramble.Length + signature.Length]; + clientScramble.CopyTo(authenticationResponse.AsSpan()); + signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length)); + + // "password hash" for parsec is the extended salt followed by the public key + passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey]; } private ParsecAuthenticationPlugin() diff --git a/src/MySqlConnector.Authentication.Ed25519/docs/README.md b/src/MySqlConnector.Authentication.Ed25519/docs/README.md index 7f17e4c81..b2d794296 100644 --- a/src/MySqlConnector.Authentication.Ed25519/docs/README.md +++ b/src/MySqlConnector.Authentication.Ed25519/docs/README.md @@ -7,7 +7,7 @@ This package implements the following authentication plugins for MariaDB: ## How to Use -Call either the following methods from your application startup code to enable the corresponding authentication plugin: +Call either of the following methods from your application startup code to enable the corresponding authentication plugin: * `Ed25519AuthenticationPlugin.Install()` * `ParsecAuthenticationPlugin.Install()` diff --git a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs index 6dfface4e..bc432c0d1 100644 --- a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs +++ b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs @@ -24,6 +24,7 @@ public interface IAuthenticationPlugin /// /// is an extension to that returns a hash of the client's password. /// +[Obsolete("Use IAuthenticationPlugin3 instead.")] public interface IAuthenticationPlugin2 : IAuthenticationPlugin { /// @@ -36,3 +37,21 @@ public interface IAuthenticationPlugin2 : IAuthenticationPlugin /// The authentication-method-specific hash of the client's password. byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData); } + +/// +/// is an extension to that also returns a hash of the client's password. +/// +/// If an authentication plugin supports this interface, the base method will not be called. +public interface IAuthenticationPlugin3 : IAuthenticationPlugin +{ + /// + /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification). + /// + /// The client's password. + /// The authentication data supplied by the server; this is the auth method data + /// from the Authentication + /// Method Switch Request Packet. + /// The authentication response. + /// The authentication-method-specific hash of the client's password. + void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash); +} diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 6f740c38b..23efbfb44 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -448,13 +448,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella var initialHandshake = InitialHandshakePayload.Create(payload.Span); // if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use - m_currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! : + var currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! : (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" : "mysql_native_password"; - Log.ServerSentAuthPluginName(m_logger, Id, m_currentAuthenticationMethod); - if (m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password") + Log.ServerSentAuthPluginName(m_logger, Id, currentAuthenticationMethod); + if (currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password") { - Log.UnsupportedAuthenticationMethod(m_logger, Id, m_currentAuthenticationMethod); + Log.UnsupportedAuthenticationMethod(m_logger, Id, currentAuthenticationMethod); throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported."); } @@ -528,11 +528,17 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella if (m_supportsConnectionAttributes && cs.ConnectionAttributes is null) cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName); + var password = GetPassword(cs, connection); + // send a caching_sha2_password response if the server advertised support in the initial handshake var useCachingSha2 = initialHandshake.AuthPluginName == "caching_sha2_password"; + byte[] authenticationResponse; + if (useCachingSha2) + authenticationResponse = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(initialHandshake.AuthPluginData.AsSpan()), password); + else + AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out authenticationResponse, out m_passwordHash); - var password = GetPassword(cs, connection); - using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, useCachingSha2, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) + using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false); payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -553,7 +559,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } else if (!m_isSecureConnection && password.Length != 0) { - var publicKey = await GetRsaPublicKeyAsync(m_currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false); + var publicKey = await GetRsaPublicKeyAsync(currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false); payload = await SendEncryptedPasswordAsync(AuthPluginData, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false); } else @@ -583,7 +589,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella // there is no shared secret that can be used to validate the certificate Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors); } - else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password)) + else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20))) { Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors); ignoreCertificateError = true; @@ -649,36 +655,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella /// /// The validation hash received from the server. /// The auth plugin data from the initial handshake. - /// The user's password. /// true if the validation hash matches the locally-computed value; otherwise, false. - private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge, string password) + private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge) { // expect 0x01 followed by 64 hex characters giving a SHA2 hash if (validationHash?.Length != 65 || validationHash[0] != 1) return false; - byte[]? passwordHashResult = null; - switch (m_currentAuthenticationMethod) - { - case "mysql_native_password": - passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true); - break; - - case "client_ed25519": - AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin); - if (ed25519Plugin is IAuthenticationPlugin2 plugin2) - passwordHashResult = plugin2.CreatePasswordHash(password, challenge); - break; - } - if (passwordHashResult is null) + // the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use + if (m_passwordHash is null) return false; - Span combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length]; - passwordHashResult.CopyTo(combined); - challenge.CopyTo(combined[passwordHashResult.Length..]); - m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]); - + // hash password hash || scramble || certificate thumbprint Span hashBytes = stackalloc byte[32]; + Span combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!]; #if NET5_0_OR_GREATER SHA256.TryHashData(combined, hashBytes, out _); #else @@ -827,8 +817,8 @@ public async Task TryResetConnectionAsync(ConnectionSettings cs, MySqlConn DatabaseOverride = null; } var password = GetPassword(cs, connection); - var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password); - using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) + AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash); + using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false); payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature) @@ -872,13 +862,12 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, // if the server didn't support the hashed password; rehash with the new challenge var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span); Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name); - m_currentAuthenticationMethod = switchRequest.Name; switch (switchRequest.Name) { case "mysql_native_password": AuthPluginData = switchRequest.Data; - var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password); - payload = new(hashedPassword); + AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash); + payload = new(nativeResponse); await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -931,14 +920,15 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html."); case "client_ed25519": - if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin)) + if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3) throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication."); - payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data)); + ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash); + payload = new(ed25519Response); await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); case "parsec": - if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin)) + if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3) throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication."); payload = new([]); await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); @@ -948,7 +938,8 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, switchRequest.Data.CopyTo(combinedData); payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length)); - payload = new(parsecPlugin.CreateResponse(password, combinedData)); + parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash); + payload = new(parsecResponse); await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -2216,7 +2207,7 @@ protected override void OnStatementBegin(int index) private PayloadData m_setNamesPayload; private byte[]? m_pipelinedResetConnectionBytes; private Dictionary? m_preparedStatements; - private string? m_currentAuthenticationMethod; + private byte[]? m_passwordHash; private byte[]? m_remoteCertificateSha2Thumbprint; private SslPolicyErrors m_sslPolicyErrors; } diff --git a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs index fdc5f2352..d817e0290 100644 --- a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs +++ b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs @@ -56,14 +56,12 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet) => CreateCapabilitiesPayload(serverCapabilities, cs, compressionMethod, characterSet, ProtocolCapabilities.Ssl).ToPayloadData(); - public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, bool useCachingSha2, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes) + public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, byte[] authenticationResponse, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes) { // TODO: verify server capabilities var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, compressionMethod, characterSet); writer.WriteNullTerminatedString(cs.UserID); - var authenticationResponse = useCachingSha2 ? AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(handshake.AuthPluginData.AsSpan()), password) : - AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password); writer.Write((byte) authenticationResponse.Length); writer.Write(authenticationResponse); @@ -71,7 +69,7 @@ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSe writer.WriteNullTerminatedString(cs.Database); if ((handshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0) - writer.Write(useCachingSha2 ? "caching_sha2_password\0"u8 : "mysql_native_password\0"u8); + writer.Write(handshake.AuthPluginName == "caching_sha2_password" ? "caching_sha2_password\0"u8 : "mysql_native_password\0"u8); if (connectionAttributes is not null) writer.Write(connectionAttributes); diff --git a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs index 659d2350d..d1f325682 100644 --- a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs +++ b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs @@ -24,24 +24,26 @@ public static byte[] GetNullTerminatedPasswordBytes(string password) return passwordBytes; } - public static byte[] CreateAuthenticationResponse(ReadOnlySpan challenge, string password) => - string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, onlyHashPassword: false); - /// /// Hashes a password with the "Secure Password Authentication" method. /// - /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake). /// The password to hash. - /// If true, is ignored and only the twice-hashed password - /// is returned, instead of performing the full "secure password authentication" algorithm that XORs the hashed password against - /// a hash derived from the challenge. - /// A 20-byte password hash. + /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake). + /// The authentication response. + /// The twice-hashed password. /// See Secure Password Authentication. #if NET5_0_OR_GREATER [SkipLocalsInit] #endif - public static byte[] HashPassword(ReadOnlySpan challenge, string password, bool onlyHashPassword) + public static void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash) { + if (string.IsNullOrEmpty(password)) + { + authenticationResponse = []; + passwordHash = []; + return; + } + #if !NET5_0_OR_GREATER using var sha1 = SHA1.Create(); #endif @@ -58,10 +60,9 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password, sha1.TryComputeHash(passwordBytes, hashedPassword, out _); sha1.TryComputeHash(hashedPassword, combined[20..], out _); #endif - if (onlyHashPassword) - return combined[20..].ToArray(); + passwordHash = combined[20..].ToArray(); - challenge[..20].CopyTo(combined); + authenticationData[..20].CopyTo(combined); Span xorBytes = stackalloc byte[20]; #if NET5_0_OR_GREATER SHA1.TryHashData(combined, xorBytes, out _); @@ -71,7 +72,7 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password, for (var i = 0; i < hashedPassword.Length; i++) hashedPassword[i] ^= xorBytes[i]; - return hashedPassword.ToArray(); + authenticationResponse = hashedPassword.ToArray(); } public static byte[] CreateScrambleResponse(ReadOnlySpan nonce, string password) => diff --git a/tests/IntegrationTests/SslTests.cs b/tests/IntegrationTests/SslTests.cs index b27742e02..be0dcf3da 100644 --- a/tests/IntegrationTests/SslTests.cs +++ b/tests/IntegrationTests/SslTests.cs @@ -249,6 +249,20 @@ public async Task ConnectZeroConfigurationSslEd25519() using var connection = new MySqlConnection(csb.ConnectionString); await connection.OpenAsync(); } + + [SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.ParsecAuthentication)] + public async Task ConnectZeroConfigurationSslParsec() + { + MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install(); + var csb = AppConfig.CreateConnectionStringBuilder(); + csb.CertificateFile = null; + csb.SslMode = MySqlSslMode.VerifyFull; + csb.SslCa = ""; + csb.UserID = "parsec-user"; + csb.Password = "P@rs3c-Pa55"; + using var connection = new MySqlConnection(csb.ConnectionString); + await connection.OpenAsync(); + } #endif [SkippableFact(ConfigSettings.RequiresSsl)] From 48238f8de2bb1cf6cfd9c7f540b2e9d3daefe1de Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sun, 18 May 2025 15:20:35 -0700 Subject: [PATCH 3/3] Use one-shot hash methods on modern .NET. Signed-off-by: Bradley Grainger --- .../Ed25519AuthenticationPlugin.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs index cd123acf2..b7f718b17 100644 --- a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs +++ b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs @@ -88,8 +88,12 @@ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan au az[31] |= 64; */ +#if NET5_0_OR_GREATER + byte[] az = SHA512.HashData(passwordBytes); +#else using var sha512 = SHA512.Create(); byte[] az = sha512.ComputeHash(passwordBytes); +#endif ScalarOperations.sc_clamp(az, 0); /*** Java @@ -115,7 +119,11 @@ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan au byte[] sm = new byte[64 + authenticationData.Length]; authenticationData.CopyTo(sm.AsSpan().Slice(64)); Buffer.BlockCopy(az, 32, sm, 32, 32); +#if NET5_0_OR_GREATER + byte[] nonce = SHA512.HashData(sm.AsSpan(32, authenticationData.Length + 32)); +#else byte[] nonce = sha512.ComputeHash(sm, 32, authenticationData.Length + 32); +#endif /*** Java ScalarOps scalar = new ScalarOps(); @@ -173,7 +181,11 @@ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan au return 0; */ +#if NET5_0_OR_GREATER + var hram = SHA512.HashData(sm); +#else var hram = sha512.ComputeHash(sm); +#endif ScalarOperations.sc_reduce(hram); var temp = new byte[32]; ScalarOperations.sc_muladd(temp, hram, az, nonce);