dvsekhvalnov / jose-jwt

Ultimate Javascript Object Signing and Encryption (JOSE), JSON Web Token (JWT) and Json Web Keys (JWK) Implementation for .NET and .NET Core
MIT License
936 stars 184 forks source link

Support JWK #10

Closed mwegman closed 2 years ago

mwegman commented 9 years ago

Is there any plans to support JSON Web Keys (JWK) in the future?

dvsekhvalnov commented 9 years ago

hi @mwegman

Yep. I have some ideas to make separate library for JWK support, it's good that you asked. Is there are particular features you are interested for JWK?

mwegman commented 9 years ago

@dvsekhvalnov, how about:

dvsekhvalnov commented 9 years ago

This is pretty much what i've been thinking about, the bridge between C# keys and json JWK structure.

I was planing to start something this summer, will let you know once have something to show.

tomclock commented 7 years ago

I have to encode the token of ES256. The customer had give me the "token", and the "public key". When I tried to use : string json2 = Jose.JWT.Decode(token, Ecc256Public());
I am not clear about how to transfer the "public key"(a long string) into the parameters of byte[], so I can get the EccKey by EccKey.New(byte[] x, byte[] y, usage: usage);
I have checked the UnitTest file there is no example I can refer.

dvsekhvalnov commented 7 years ago

@tomclock, mmm...can you show how it looks like? You can send me to email address directly if you don't want to expose it here. (though it's public part anyway).

tomclock commented 7 years ago

I have the public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKW28ktNBhCSzaDzNSMwarj8mmXSf 8G1wOr5fXL6j8KZEKszkIjd+zcWVq3ShlYVeTCFcVvBQIvzKd1X7elw0Gg== and the token: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJjY2lfY29kZSI6Ijk5OTkiLCJ1dWlkIjoiOTllMzVjMDctOGU4NC00NGMwLWI2OTUtMGFiMTk2NTBiODY4IiwiZW1haWwiOiJrYW5la29Ac21pbGUtd29ya3MuY28uanAiLCJwc2V1ZG9ueW0iOiJhYzIwM2I2MDhlNWJiMDc4Iiwicm9sZXMiOlsiY2NpIl0sImlzcyI6InBvcnRhbC5jY2ktamlneW8uanAiLCJpYXQiOjE0NzcyOTY5OTZ9.Bryo17_lDpyTLZHhLzRzbglhll4nfLmFcuLuVkQ2lUufsGPzfONWT14VzZQDmh6_V3ZyiREChq7d3xg7tccDLQ the encoded data is json format string. the problem is how to transfer the public key into the byte[] x, and byte[] y. where I will using the following code to create EccKey. EccKey.New(x, y, usage: usage)

tomclock commented 7 years ago

we got the public key from the pem file(please unzip the file and that the end attached name to pem) : hanbaishi-pub.bin.gz

dvsekhvalnov commented 7 years ago

hm.. have no idea what's this. How did you decode .bin file?

dvsekhvalnov commented 7 years ago

May be try something like: CngKey.Import(keyBytes, CngKeyBlobFormat.EccPublicBlob);

CngKey support importing from some formats. Or ask what is the format of public key you've received.

tomclock commented 7 years ago

thanks for you reply . And I added some message to you GitHub.

On Tue, Oct 25, 2016 at 7:33 PM, dvsekhvalnov notifications@github.com wrote:

mmm...can you show how it looks like? You can send me to email address directly if you don't want to expose it here. (though it's public part anyway).

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/dvsekhvalnov/jose-jwt/issues/10#issuecomment-255998868, or mute the thread https://github.com/notifications/unsubscribe-auth/AQosmYkR1dy-SBZODqON3qtOapo_zd52ks5q3drrgaJpZM4FdHuc .

i love you, my friend!

tomclock commented 7 years ago

this is not a bin file, it is a pem file , you can directly change it name into a pem file.

On Tue, Oct 25, 2016 at 8:28 PM, dvsekhvalnov notifications@github.com wrote:

hm.. have no idea what's this. How did you decode .bin file?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dvsekhvalnov/jose-jwt/issues/10#issuecomment-256009340, or mute the thread https://github.com/notifications/unsubscribe-auth/AQosmUuQY7QSmaXRGsNigEWo5hzAWrX7ks5q3efhgaJpZM4FdHuc .

i love you, my friend!

dvsekhvalnov commented 7 years ago

PEM is a way to encode data, container. It can contain certificates or keys or signing requests, usually looks like:

-----BEGIN SOMETHING -----
MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNV
BAsME0RvY3...................QG8drorw==
-----END SOMETHING-----

so what exactly inside .bin file? Is it encoding for what? https://tools.ietf.org/html/rfc5915 ?

tomclock commented 7 years ago

THANKS, I just handle it in the R language, an decode the token ..

Thanks you for your kindly teaching.

On Wed, Oct 26, 2016 at 5:07 AM, dvsekhvalnov notifications@github.com wrote:

PEM is a way to encode data, container. It can contain certificates or keys or signing requests, usually looks like:

-----BEGIN SOMETHING ----- MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNV BAsME0RvY3...................QG8drorw== -----END SOMETHING-----

so what exactly inside .bin file? Is it encoding for what? https://tools.ietf.org/html/rfc5915 ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dvsekhvalnov/jose-jwt/issues/10#issuecomment-256160487, or mute the thread https://github.com/notifications/unsubscribe-auth/AQosmSbRzQfDzH65dePwX5ZqWl60qC6iks5q3mGBgaJpZM4FdHuc .

i love you, my friend!

TheKnight2002 commented 7 years ago

Hi, I also need to create JWS tokens which are signed with a JWK. Any suggestions on how I can use a JWK to sign JWT? Thanks

dvsekhvalnov commented 7 years ago

Hi @TheKnight2002 ,

sorry never got back to that request, probably i should.

Well, JWK - is just json for keeping key params (exp, mod, e.t.c.) Until library supports it you can parse jwk (json) yourself and use RsaKey or EccKey helpers to construct key itself. Check out: https://github.com/dvsekhvalnov/jose-jwt/blob/master/jose-jwt/Security/Cryptography/RsaKey.cs#L18

P.S> and yes, i'm open to pull requests :)

TheKnight2002 commented 7 years ago

Thanks for your quick reply. In that code I see I need P and Q, problem is that I only have E, N and D ☺ Does this mean they key is not valid for signing? Also the Microsoft.Azure.KeyVault.WebKey.JsonWebKey need at least DP and DQ, which I don’t have. We are not real specialists in this matter, but my guess is that our key is incomplete to do RSA signing?

Thanks for your feedback David

From: dvsekhvalnov [mailto:notifications@github.com] Sent: 27 July 2017 7:00 PM To: dvsekhvalnov/jose-jwt Cc: DE RIDDER David; Mention Subject: Re: [dvsekhvalnov/jose-jwt] Support JWK (#10)

Hi @TheKnight2002https://github.com/theknight2002 ,

sorry never got back to that request, probably i should.

Well, JWK - is just json for keeping key params (exp, mod, e.t.c.) Until library supports it you can parse jwk (json) yourself and use RsaKey or EccKey helpers to construct key itself. Check out: https://github.com/dvsekhvalnov/jose-jwt/blob/master/jose-jwt/Security/Cryptography/RsaKey.cs#L18

P.S> and yes, i'm open to pull requests :)

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/dvsekhvalnov/jose-jwt/issues/10#issuecomment-318423567, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AZfo6YAqzVSAT3hZBhGlOZo_eA_jWETRks5sSMIYgaJpZM4FdHuc. -----Disclaimer-----

This message may contain confidential information intended solely for the use of the named addressee. If you are not the intended recipient, you should not read, use, disclose or reproduce the content of this message. If you have received this message by mistake, please notify the sender immediately. Any views or opinions presented in this message are solely those of the author and do not necessarily represent those of AXA Belgium, AXA Bank Belgium, AXA Tech Belgium GIE - ESV or any other entity of the AXA Group, unless otherwise stated by the sender and duly authorized by the said companies.


dvsekhvalnov commented 7 years ago

@TheKnight2002 , can you post example of JWK you have? (well obviously if not secret production private keys)

TheKnight2002 commented 7 years ago

Sure, it is just a test key ☺

{ "kty": "RSA", "e": "AQAB", "use": "sig", "kid": "MobID-POC.sig.2017", "alg": "RS256", "d": "cOM-b5jarvvGd8l9JV_nclKCuY0UWNYdj8FH-QL61wVbgHkwrGXGkRSqJWMAuQJErAZsgtHXlaaCm8HdvnQL3cI1xmonBUJk2Z5YHAODEbIkA17G27DbzG8uoFENzR72Iis3baJYHx2lUs_yqFPYZajZhkYPUDFGDurYpoePlz-fZQ_ZaklOSRcwsaav1VruFgJmj5HXnigiiIJlC_ojQchcIr6Cscs6fP0DjK7ClvGggMVX1ZmHhG-DLdg-mZVWGU_O23_68fxZaDj5no3Czb56eniUY7tSTf23mWx-HLbqaFdRPrYQHz6JPEVHEeflZCmP8om7eMPYizFzT2lgAQ", "n": "3HNVB4_K2JDqPcHKxwqIeA0ZDR5vehGfv03857wMKMQnWDeKoWY9UklQ0BLXoczX5L7UUMjaCd8cBxzv8-5CaLRLfiNf_OYE-UAmhlf9uIS2Q0VXaBtGOBrQNq0NePzpnMiZ-P7iafvDGbtT2bnTpXLIJ8j_f5uZuTfGCWZDfBCs09Ryf4JICFhKm9thWWPUW3fxB21v0DWJ8xQ6QneUjPcGVnmB6CkeFp8Ur68NhrXag2VsaxxhVZFLtRxZTOfG08-anS1Z4UlYyyTsrwTjmOTn4K5xAwjYCOYey-whGS9kiX8C4akNiNvPKoEwm5o-RdsmmzjXnyAgtvNJcFL30Q" }

Thanks for your support

From: dvsekhvalnov [mailto:notifications@github.com] Sent: 28 July 2017 9:53 AM To: dvsekhvalnov/jose-jwt Cc: DE RIDDER David; Mention Subject: Re: [dvsekhvalnov/jose-jwt] Support JWK (#10)

@TheKnight2002https://github.com/theknight2002 , can you post example of JWK you have? (well obviously if not secret production private keys)

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/dvsekhvalnov/jose-jwt/issues/10#issuecomment-318586699, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AZfo6QhQIYFc_hm92vFNVqAdtajp_7aGks5sSZNggaJpZM4FdHuc. -----Disclaimer-----

This message may contain confidential information intended solely for the use of the named addressee. If you are not the intended recipient, you should not read, use, disclose or reproduce the content of this message. If you have received this message by mistake, please notify the sender immediately. Any views or opinions presented in this message are solely those of the author and do not necessarily represent those of AXA Belgium, AXA Bank Belgium, AXA Tech Belgium GIE - ESV or any other entity of the AXA Group, unless otherwise stated by the sender and duly authorized by the said companies.


dvsekhvalnov commented 7 years ago

That's perfectly fine private key. Here: e - exponent n - modulus d - private key exponent

Unfortunately RsaKey do not have corresponding method for constructing private key from (e,n,d) but only from (e,n,p,q).

But SDK's RSAParameters provides appropriate D field as well, see https://msdn.microsoft.com/en-us/library/system.security.cryptography.rsaparameters(v=vs.110).aspx

You can simply extend RsaKey with appropriate New(..) functions.

nanasess commented 7 years ago

I have created a example code. see also, https://gist.github.com/nanasess/ff8629c476edfddc1b304f9759b9eaa3

psteniusubi commented 7 years ago

Hi @dvsekhvalnov ,

I have created code for JWK support. It converts byte[], RSA and ECDsa keys to and from JWK Json format.

The code exists here https://github.com/psteniusubi/jose-jwt/tree/master/jose-jwt/jwk. There are also unit tests with test vectors from RFC 7515.

Please take a look. Let me know if you find this useful.

Thanks,

dvsekhvalnov commented 7 years ago

Hi @psteniusubi ,

thanks, looks interesting ! i'll take a deeper look tomorrow.

dvsekhvalnov commented 7 years ago

Hi @psteniusubi , something you want to submit pull request for? Or just posted as example?

psteniusubi commented 7 years ago

@dvsekhvalnov - Yes I'd be happy to submit a pull request. However this is new to me so I need some advice how to proceed. Thanks!

dvsekhvalnov commented 7 years ago

Hi @psteniusubi , okay let's break it down then :)

TL,DR:

For inclusion to library implementation is slightly overcomplicated to my taste and also contains some issues i'll outline below. I'll also provide my vision on it, so feel free to discuss if you want to make the contribution, i'm always open.

  1. There are bunch of twisted factories, interfaces and patterns, which really doesn't bring anything. I'm talking about IJwkAlgorithm, JwkFactory. I'd remove them.

  2. Some things are only .NET 4.7, like ECCurve.NamedCurves.. While the library still strives for .NET40+ compatibility.

  3. Somewhere there are references to platform specific ECDsaCng and RSACng classes, they do not exists on linux if we talking about .NET core. Again i'd prefer portable implementation as much as possible.

  4. Conversion from JWK class only to ECDsa and RSA classes. While most of algorithms in a library can work with ECDsa, CngKey, RSA and RSACryptoServiceProvider.

  5. Haven't noticed any tests :) Tests are must, it's library.

  6. Extension methods in Jose.jwk.util.Helpers. Extensions are great, but they pollute global namespace, especially when applied to SDK classes. If all libraries expose extensions on same objects, it's nightmare. Would prefer not to expose any extensions from library (unless you writing library of extensions).

dvsekhvalnov commented 7 years ago

Basically i can see 2 scenarios right now:

How would i recommend to implement it:

  1. Single class Jose.JWK.JsonWebKey, which is dumb union of all known properties in JWT (i'll post json example below). That will allow to parse it from JSON string without any gimmicks with every modern json parser library.

  2. new JsonWebKey(IDictionaty<string, object> params) - constructor to bridge library

  3. IDictionary<string, object> JsonWebKey.ToDictionary() - to bridge library

  4. EccKey JsonWebKey.ToEcc() - to convert to EccKey helper (or exception if not ECC type)

  5. RsaKey JsonWebKey.ToRsa() - to convert to RsaKey helper (or exception if not RSA type)

  6. byte[] JsonWebKey.ToByte() - to convert to byte[] (or exception if not OCT type)

  7. Extend RsaKey to bridge with JsonWebKey and provide convertions to RSACryproServiceProvider and RSA (CngKey is already there)

  8. Extend EccKey to bridge with JsonWebKey and provide convertions to ECDsa (CngKey is already there)

I'm not aware of any context for using x509 fields at the momemt, so unsure what kind of support may needed here.

dvsekhvalnov commented 7 years ago

Here is example of JWK with all known fields to me so far, as reference:

{
// key usage and meta   
    "kty": "EC",
    "kid": "Public key used in JWS spec Appendix A.3 example",
    "use": "sig",
    "key_ops" : ["sign", "verify"],
    "alg": "RS256", 

//elliptic  

    "crv": "P-256",
    "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
    "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
    "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",

//rsa

    "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4
         cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMst
         n64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2Q
         vzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbIS
         D08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw
         0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
     "e":"AQAB",
     "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9
         M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqij
         wp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d
         _cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBz
         nbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFz
         me1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q",
     "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPV
         nwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqV
         WlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs",
     "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyum
         qjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgx
         kIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
     "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oim
         YwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_Nmtu
         YZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0",
     "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUU
         vMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9
         GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk",
     "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzg
         UIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rx
         yR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU"

//symmetric
     "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75
          aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"

//x509 links    
    "x5u": "https://example.com/key.pem"
    "x5c":
           ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJB
           gNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYD
           VQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1
           wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBg
           NVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDV
           QQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1w
           YmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnH
           YMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66
           s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6
           SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpn
           fajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPq
           PvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVk
           aZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BA
           QUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL
           +9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1
           zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL
           2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo
           4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTq
           gawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA=="],

    "x5t": "NmJmOGUxMzZlYjM2ZDRhNTZlYTA1YzdhZTRiOWE0NWI2M2JmOTc1ZA",
    "x5t#S256": "NmJmOGUxMzZlYjM2ZDRhNTZlYTA1YzdhZTRiOWE0NWI2M2JmOTc1ZA"
}
psteniusubi commented 7 years ago

Thanks! I'll look into these.

All unit tests are in a separate project: https://github.com/psteniusubi/jose-jwt/tree/master/UnitTestProject1

My plan for cross-platform was to move platform specific things into JwtSettings: code in JwkFactory and the CreateAlgorithm method in JwkRsa and JwkEc

dvsekhvalnov commented 7 years ago

I think you can do platform neutral way: ECDsa.Create() and RSA.Create()

Unsure if there is a way to import parameters in versions earlier than .NET 4.7

psteniusubi commented 7 years ago

I have tested on .net core 2 on Linux. Did fix some issues: One major issue was with NewtonsoftMapper which would not handle recursive arrays and objects The JWE test using RSA OAEP currently fails on Linux with error bcrypt.dll is missing. Other tests are ok.

osharper commented 4 years ago

so, what's the current status of JWK support? It seems like pull request was never created. Why and what has to be done in order to add JWK features in package? Can try to help.

dvsekhvalnov commented 4 years ago

Hi @osharper , well i guess no work have happened on adding JWK support. If you interested i can take time and revisit what i've said above since it was quite some time ago :)

Do you have specific use-case you'd like to support with library?

KamranShahid commented 4 years ago

I have use following https://stackoverflow.com/questions/61418205/manually-create-jwk-from-rsa-publickey-in-net-core-3-1 @dvsekhvalnov In my application your library is also used at some place. If you can do any improvement in above post then it would be great. Thanks

dvsekhvalnov commented 2 years ago

v4.0.0 released.