Crypter icon indicating copy to clipboard operation
Crypter copied to clipboard

[Feature] Switch JWT to public-key signature

Open Jack-Edwards opened this issue 1 year ago • 8 comments

Description

If the plan is to implement a self-hosted storage option for Crypter, then the existing Crypter code needs to switch to signing JWTs with a private key, instead of a symmetric key.

This will let self-hosted applications verify the authenticity of JWT signatures using the corresponding public key that the Crypter API will need to make publicly available.

Implementation notes

The code to sign and verify JWTs with a symmetric key is already written. Really that just needs to be swapped out for public key.

~~An endpoint will also need to be added to the API, to expose the public key.~~ Edit: This won't be necessary until we implement OpenID Connect.

Good First Issue

This may be a good first issue for someone who is interested in ASP.NET and is comfortable with JWT creation and verification.

Jack-Edwards avatar Jul 08 '23 15:07 Jack-Edwards

Hey, I would like to try this out but I have never done any work in JWT creation and verification. Is there any documentation or video or resource that might be helpful for the same?

rkj43 avatar Jul 08 '23 19:07 rkj43

With respect to the code, this is where the signing algorithm and key are configured: https://github.com/Crypter-File-Transfer/Crypter/blob/main/Crypter.Core/Identity/TokenParametersProvider.cs

I would tackle this issue across multiple parts:

  1. Read up on JWTs, understand why JWTs must be cryptographically signed, and understand the strengths and drawbacks of the different asymmetric signing algorithms.
  2. Choose an asymmetric signing algorithm with support in .NET. After consuming those articles below, it sounds like EdDSA is the best algorithm overall. The down side is that ASP.NET does not yet support EdDSA out-of-the-box. We would need to use Scott Brady's NuGet package.
  3. Refactor the TokenParametersProvider class to use the new signing algorithm. Update the backend's configuration with properties for a private key and public key (or calculate the public key during startup, from the private key).
  4. Run unit tests. There are a few tests to make sure tokens can be created and validated, but there are no tests that verify invalid tokens would be rejected. Some additional test coverage here would be nice, granted we're touching this part of the code.

Down the road, I believe Crypter would also need to implement something like OpenID Connect in order to support self-hosted storage servers. That's not going to be for a long time though - this doesn't need to be accomplished now as part of this issue. Just thinking out loud.

Good and relevant articles

A good intro to the different types of signing keys for JWT: https://www.scottbrady91.com/jose/jwts-which-signing-algorithm-should-i-use

Here's an extremely comprehensive post on the topic of authentication in the context of ASP.NET: https://www.reddit.com/r/dotnet/comments/we9qx8/a_comprehensive_overview_of_authentication_in/

And another post with much more code examples: https://dev.to/mohammedahmed/build-your-own-oauth-20-server-and-openid-connect-provider-in-aspnet-core-60-1g1m

EdDSA example: https://www.scottbrady91.com/c-sharp/eddsa-for-jwt-signing-in-dotnet-core

Jack-Edwards avatar Jul 09 '23 02:07 Jack-Edwards

@rkj43 I understand these "Good First Issues" are learning opportunities for a lot of people. I can help you out as much (or as little) as you need. Just reach out when you have questions or need some direction.

Jack-Edwards avatar Jul 09 '23 02:07 Jack-Edwards

Thank you! I will look into this.

rkj43 avatar Jul 09 '23 10:07 rkj43

@Jack-Edwards I have been looking into this and trying to implement this.

I referred Scott Brady's IdentityModel to implement the below code but as I am new to this I am not entirely sure if this is what you want.

`public static TokenValidationParameters GetTokenValidationParameters(TokenSettings tokenSettings) { var privateKeyBytes = Convert.FromBase64String(tokenSettings.PrivateKey); var edDsa = EdDsa.Create(new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519) { D = privateKeyBytes });

     var edDsaSecurityKey = new EdDsaSecurityKey(edDsa);

     return new TokenValidationParameters
     {
        ValidateAudience = true,
        ValidAudience = tokenSettings.Audience,
        ValidIssuer = tokenSettings.Issuer,
        ValidateIssuer = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = edDsaSecurityKey,
        ValidateLifetime = true,
        ClockSkew = TokenValidationParameters.DefaultClockSkew,
        RequireExpirationTime = true,
        ValidAlgorithms = new[] { "EdDSA" }
     };
  }`

In this code, I am using the Ed25519PrivateKey class provided by the ScottBrady.IdentityModel.Crypto namespace. The private key is imported from the base64-encoded string using Convert.FromBase64String. The corresponding public key is obtained from the private key using privateKey.GetPublicKey.

An EdDsaSecurityKey is then created with the public and private keys, which is used as the IssuerSigningKey in the TokenValidationParameters.

rkj43 avatar Jul 12 '23 17:07 rkj43

This does look correct, or at least very close to correct. I'd need to play around with your code to make sure.

If you open a Pull Request, I can clone your branch and test it.

Jack-Edwards avatar Jul 12 '23 17:07 Jack-Edwards

Given my current situation with university, graduation, and the job search, I'm swamped with commitments. I won't be able to dedicate time to this for a while. To ensure the task doesn't stall, I'll unassign myself so someone else can take a look and hopefully resolve it sooner.

Thanks for your understanding, and I hope to contribute again once things settle down on my end.

rkj43 avatar Sep 07 '23 16:09 rkj43

@rkj43 I understand. I've contributed to the project much less over the past couple of months as parenthood and a new job have taken priority. Good luck on the job search.

Jack-Edwards avatar Sep 08 '23 02:09 Jack-Edwards