frasertweedale / hs-jose

Haskell JOSE and JWT library
http://hackage.haskell.org/package/jose
Apache License 2.0
122 stars 46 forks source link

How do I make a JWT from an EC private key? #121

Closed ericpashman closed 6 months ago

ericpashman commented 6 months ago

There are several Haskell libraries that will parse a PEM-encoded EC private key into one of various datatypes; e.g., the pemToKey function from the x509-store library will produce a representation of an EC private key in its PrivKey type. But as far as I can tell, hs-jose doesn't provide functionality directly to create a JWK from any representation of an EC private key.

It seems straightforward to create a JWK by extracting the parameters of the EC private key from a third-party datatype, then to use those parameters manually to construct an ECKeyParameters value, then a KeyMaterial value, and finally to use fromKeyMaterial to make a JWK.

frasertweedale commented 6 months ago

For the first issue: I will address this by:

  1. Exporting the ECKeyParameters constructor, so that you can construct the key material yourself then use fromKeyMaterial.
  2. Also add convenience functions for converting from the crypton-x509 PubKey and PrivKey types (i.e., what the crypton-x509-store package produces).

For the JWT with custom claims, the preferred approach is described in the Crypto.JWT module doc. Define a record type that includes a ClaimsSet value, and provide instances of HasClaimsSet and (To|From)JSON.

frasertweedale commented 6 months ago

@ericpashman do you want to take a look at branch https://github.com/frasertweedale/hs-jose/tree/feature/121-key-conversion and see if it meets your needs? You can use Crypto.JOSE.JWK.fromX509PrivKey to construct the JWK from a Data.X509.PrivKey.

ericpashman commented 6 months ago

Hi, Fraser, thank you for these changes.

Your addition of fromX509PrivKey does what I need to create a JWK. This solves my issue (1). Providing this function makes it unnecessary (for my purposes) to expose the ECKeyParameters constructor, so you can revert that commit if you want.

On my issue (2): the hang-up is that there isn't an "obvious" way to create a SignedJWT with custom headers. (The API I'm working with requires a nonce header in the JWT, which I think is somewhat common.) That is, the signJWT function from Crypto.JWT explicitly takes a JWSHeader () value, so it is not possible to pass custom headers created as documented here.

It's possible to get around this by using the signJWS function directly, to create a JWS that meets the definition of a JWT (i.e., a JWS with a payload of JSON-encoded claims), but IMO it'd be kinder to users not to make them figure out how to do this. It looks to me like signJWT can be generalized to accept custom header types in a backwards-compatible way simply by generalizing its type signature:

signJWT
  :: ( MonadRandom m, MonadError e m, AsError e
     , HasJWSHeader header, HasParams header, ProtectionIndicator p
     , ToJSON payload )
  => JWK
  -> header p  --^ Header with protection indicator p
  -> payload
  -> m SignedJWT
signJWT k h c = signJWS (encode c) (Identity (h, k))

Thanks again.

frasertweedale commented 6 months ago

@ericpashman thanks for the feedback, and thank you for the clarifications. I will extract (2) as a separate issue and continue discussion there.