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/azure-pipelines.yml b/azure-pipelines.yml index fa4d3d285..8106c201d 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.2': image: 'mysql:9.2' 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..f85c6ca8b 100644 --- a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml +++ b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml @@ -1,8 +1,12 @@  - + PKV006 .NETFramework,Version=v4.5 + + PKV006 + .NETStandard,Version=v2.0 + \ No newline at end of file diff --git a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj index bd2122bb3..e054301a2 100644 --- a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj +++ b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj @@ -1,22 +1,26 @@ - net462;netstandard2.0 + net472;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..4c7adc3ac --- /dev/null +++ b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs @@ -0,0 +1,86 @@ +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); +#else + using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512)) + privateKey = pbkdf2.GetBytes(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.Authentication.Ed25519/packages.lock.json b/src/MySqlConnector.Authentication.Ed25519/packages.lock.json index c4c1e88fd..124b3005e 100644 --- a/src/MySqlConnector.Authentication.Ed25519/packages.lock.json +++ b/src/MySqlConnector.Authentication.Ed25519/packages.lock.json @@ -1,7 +1,7 @@ { "version": 2, "dependencies": { - ".NETFramework,Version=v4.6.2": { + ".NETFramework,Version=v4.7.2": { "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -125,7 +125,7 @@ } } }, - ".NETStandard,Version=v2.0": { + ".NETStandard,Version=v2.1": { "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -142,15 +142,6 @@ "resolved": "6.0.0", "contentHash": "+/SsmiySsXJlvQLCGBqaZKNVt3s/Y/HbAdwtop7Km2CnuZbaScoqkWJEBQ5Cy9ebkn6kCYKrHsXgwrFdTgcb3g==" }, - "NETStandard.Library": { - "type": "Direct", - "requested": "[2.0.3, )", - "resolved": "2.0.3", - "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, "StyleCop.Analyzers": { "type": "Direct", "requested": "[1.2.0-beta.556, )", @@ -160,24 +151,11 @@ "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", @@ -217,19 +195,14 @@ "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[8.0.2, )", - "System.Diagnostics.DiagnosticSource": "[8.0.1, )", - "System.Threading.Tasks.Extensions": "[4.5.4, )" + "System.Diagnostics.DiagnosticSource": "[8.0.1, )" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", "requested": "[8.0.2, )", "resolved": "8.0.2", - "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "8.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } + "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "CentralTransitive", @@ -252,14 +225,84 @@ "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "6.0.0" } + } + }, + "net6.0": { + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } }, - "System.Threading.Tasks.Extensions": { + "MinVer": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "+/SsmiySsXJlvQLCGBqaZKNVt3s/Y/HbAdwtop7Km2CnuZbaScoqkWJEBQ5Cy9ebkn6kCYKrHsXgwrFdTgcb3g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "mysqlconnector": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "[8.0.2, )", + "System.Diagnostics.DiagnosticSource": "[8.0.1, )" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive", - "requested": "[4.5.4, )", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "requested": "[8.0.2, )", + "resolved": "8.0.2", + "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[8.0.2, )", + "resolved": "8.0.2", + "contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "System.Diagnostics.DiagnosticSource": "8.0.1" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "CentralTransitive", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "vaoWjvkG1aenR2XdjaVivlCV9fADfgyhW5bZtXT23qaEea0lWiUljdQuze4E31vKM7ZWJaSUsbYIKE3rnzfZUg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" } } } diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index afcb1a123..cfb6e9a8c 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -914,6 +914,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..b5aaf4ec3 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; @@ -426,10 +423,11 @@ public async Task CachingSha2WithoutSecureConnection() } #if !MYSQL_DATA +#if NET472_OR_GREATER || NET6_0_OR_GREATER [SkippableFact(ServerFeatures.Ed25519)] public async Task Ed25519Authentication() { - Ed25519AuthenticationPlugin.Install(); + MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install(); var csb = AppConfig.CreateConnectionStringBuilder(); csb.UserID = "ed25519user"; @@ -442,7 +440,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 +448,19 @@ 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 #endif // To create a MariaDB GSSAPI user for a current user diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index bb388f662..e273ae465 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -53,10 +53,13 @@ - + + + + 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, } diff --git a/tests/IntegrationTests/SslTests.cs b/tests/IntegrationTests/SslTests.cs index b27742e02..6ce73b803 100644 --- a/tests/IntegrationTests/SslTests.cs +++ b/tests/IntegrationTests/SslTests.cs @@ -236,6 +236,7 @@ public async Task ConnectZeroConfigurationSslNative() } #if !MYSQL_DATA +#if NET472_OR_GREATER || NET6_0_OR_GREATER [SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.Ed25519)] public async Task ConnectZeroConfigurationSslEd25519() { @@ -249,6 +250,7 @@ public async Task ConnectZeroConfigurationSslEd25519() using var connection = new MySqlConnection(csb.ConnectionString); await connection.OpenAsync(); } +#endif #endif [SkippableFact(ConfigSettings.RequiresSsl)] diff --git a/tests/IntegrationTests/packages.lock.json b/tests/IntegrationTests/packages.lock.json index 4e6439560..8cf38d8f4 100644 --- a/tests/IntegrationTests/packages.lock.json +++ b/tests/IntegrationTests/packages.lock.json @@ -320,10 +320,7 @@ } }, "mysqlconnector.authentication.ed25519": { - "type": "Project", - "dependencies": { - "MySqlConnector": "[1.0.0, )" - } + "type": "Project" }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "CentralTransitive",