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 to construct HMAC-SHA256 from simple string? #42

Closed begriffs closed 7 years ago

begriffs commented 7 years ago

I'm working on code which can either load a full JWK from a file in its normal JSON format, or else construct a HMAC-SHA256 key from just a secret string. I intend this key to be used to verify a signed (not encrypted) JWT. Can't get the following snippet to compile though because I don't know how to construct the KeyUse (it's defined with template haskell somehow).

import Data.ByteString.Base64  (encode)

hs256jwk :: ByteString -> JWK
hs256jwk key =
  fromKeyMaterial km
    & jwkUse .~ Just "sig"
    & jwkAlg .~ Just (JWSAlg HS256)
 where
  km = OctKeyMaterial (OctKeyParameters Oct (Base64Octets b64))
  b64 = encode key

Am I even going about this the right way?

sophie-h commented 7 years ago

I think the export of KeyUse is missing in the library. This should be easy to fix.

For the other parts of your code, you can check if genJWK does what you need. Otherwise, you need something like

-import Data.ByteString.Base64  (encode)
-
 hs256jwk :: ByteString -> JWK
 hs256jwk key =
   fromKeyMaterial km
-    & jwkUse .~ Just "sig"
+    & jwkUse .~ Just Sig -- needs hs-jose fix
     & jwkAlg .~ Just (JWSAlg HS256)
  where
-  km = OctKeyMaterial (OctKeyParameters Oct (Base64Octets b64))
-  b64 = encode key
+  km = OctKeyMaterial (OctKeyParameters (Base64Octets key)
begriffs commented 7 years ago

Thanks for the super fast reply!

Another thing is I'm using jose-0.5.0.2 which is pinned there on Stackage, and not sure when a newer version of the library will be available that includes your fix. So maybe a workaround is to construct an aeson Value for this jwk and then "parse" it?

frasertweedale commented 7 years ago

On Fri, Apr 21, 2017 at 08:19:25AM -0700, Joe Nelson wrote:

Thanks for the super fast reply!

Another thing is I'm using jose-0.5.0.2 which is pinned there on Stackage, and not sure when a newer version of the library will be available that includes your fix. So maybe a workaround is to construct an aeson Value for this jwk and then "parse" it?

I'll release a fix on the 0.5 branch, too.

Thanks for reporting; I'll get onto this in the next day or so.

-- You are receiving this because you are subscribed to this thread. Reply to this email directly or view it on GitHub: https://github.com/frasertweedale/hs-jose/issues/42#issuecomment-296220251

frasertweedale commented 7 years ago

@begriffs released v0.5.0.3 and v0.6.0.1 ; the former should be picked up in next lts-8 release.

begriffs commented 7 years ago

Thanks for the new version! I pulled in 0.5.0.3 with stack extra-deps. I wonder if I'm still constructing the key wrong because I always get JWSInvalidSignature when I call validateJWSJWT with that generated key and the defaultJWTValidationSettings.

frasertweedale commented 7 years ago

On Sat, Apr 22, 2017 at 07:11:11AM -0700, Joe Nelson wrote:

Thanks for the new version! I pulled in 0.5.0.3 with stack extra-deps. I wonder if I'm still constructing the key wrong because I always get JWSInvalidSignature when I call validateJWSJWT with that generated key and the defaultJWTValidationSettings.

Can you post a minimal reproducer? I'll take a look at it.

Cheers, Fraser

begriffs commented 7 years ago

Thanks for taking a look at this, I'm getting pretty confused!

These are the steps I took in GHCI to reproduce the problem.

-- my own helper method
let k = parseJWK "safe"

Results in:

JWK {
    _jwkMaterial = OctKeyMaterial (OctKeyParameters {octKty = Oct, octK = Base64Octets "safe"})
  , _jwkUse = Just Sig
  , _jwkKeyOps = Nothing
  , _jwkAlg = Just (JWSAlg HS256)
  , _jwkKid = Nothing
  , _jwkX5u = Nothing
  , _jwkX5c = Nothing
  , _jwkX5t = Nothing
  , _jwkX5tS256 = Nothing
  }

Now I parse an example payload:

let payload = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"

eJwt <- runExceptT $ decodeCompact payload :: IO (Either JWTError JWT)

The result:

Right (JWT {jwtCrypto = JWTJWS (JWS (Base64Octets "{\"role\":\"postgrest_test_author\",\"id\":\"jdoe\"}") [Signature {_protectedRaw = Just "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", _header = JWSHeader {_jwsHeaderAlg = HeaderParam Protected HS256, _jwsHeaderJku = Nothing, _jwsHeaderJwk = Nothing, _jwsHeaderKid = Nothing, _jwsHeaderX5u = Nothing, _jwsHeaderX5c = Nothing, _jwsHeaderX5t = Nothing, _jwsHeaderX5tS256 = Nothing, _jwsHeaderTyp = Just (HeaderParam Protected "JWT"), _jwsHeaderCty = Nothing, _jwsHeaderCrit = Nothing}, _signature = Base64Octets "\203\139\217\186\237]\r\220\NUL\151O\146\211C\STXEdX2Ry`\ETX\DC2\138\190\132\179\165\173[\GS"}]), jwtClaimsSet = ClaimsSet {_claimIss = Nothing, _claimSub = Nothing, _claimAud = Nothing, _claimExp = Nothing, _claimNbf = Nothing, _claimIat = Nothing, _claimJti = Nothing, _unregisteredClaims = fromList [("role",String "postgrest_test_author"),("id",String "jdoe")]}})

Now to put the two together and try to validate:

eJwt' <- (runExceptT $ do
  jwt <- decodeCompact payload
  validateJWSJWT defaultJWTValidationSettings k jwt
  return jwt
  ) :: IO (Either JWTError JWT)

Sadly this produces Left (JWSError JWSInvalidSignature).

frasertweedale commented 7 years ago

@begriffs This occurs because they key is too short. See https://tools.ietf.org/html/rfc7518#section-3.2

A key of the same size as the hash output (for instance, 256 bits for "HS256") or larger MUST be used with this algorithm. (This requirement is based on Section 5.3.4 (Security Effect of the HMAC Key) of NIST SP 800-117 [NIST.800-107], which states that the effective security strength is the minimum of the security strength of the key and two times the size of the internal hash value.)

What library / program did you use to produce the JWT?

begriffs commented 7 years ago

In https://jwt.io/ I pasted this into the payload field

{
  "role": "postgrest_test_author",
  "id": "jdoe"
}

and changed the signature to safe (with an unchecked checkbox for "secret is base64 encoded").

begriffs commented 7 years ago

By "key" I am interpreting that to mean the secret passphrase for signing the jwt.

People have been providing this key via a config file parameter. If I now change the program to make it die on short keys then it will break backwards compatibility.

Then again maybe it was a bad idea for my program to allow short keys in the first place...

frasertweedale commented 7 years ago

On Wed, May 03, 2017 at 08:30:56PM -0700, Joe Nelson wrote:

In https://jwt.io/ I pasted this into the payload field

{
  "role": "postgrest_test_author",
  "id": "jdoe"
}

and changed the signature to safe (with an unchecked checkbox for "secret is base64 encoded).

Huh. Well, it shouldn't do that, according to the spec (unless it is doing some kind of key derivation to get a long-enough key to pass to the underlying library).

Try "reallyreallyreallyreallyverysafe" :)

begriffs commented 7 years ago

Because the failure will only happen when the message to be verified is longer than the secret, I guess there is no abstract minimum size requirement for the key. Is there a way that my code can detect this jose error to provide a friendly error to the user so that they know to change the key rather than thinking the jwk is invalid for another unknown reason?

frasertweedale commented 7 years ago

@begriffs the failure happens when the digest size for the HMAC algorithm is longer than the key (it has nothing to do with message length). So for HMAC-SHA256, that's 32-bytes minimum key size.

It is an error to use short keys. Whatever library you are using to produce these JWTs, is violating the spec by allowing short keys to be used.

You should break compatibility in the name of security. If you want to continue to allow short keys you can use a key derivation function (e.g. PBKDF2, scrypt) to stretch a low-entropy short key into a pseudorandom key of appropriate length. Of course, this key will not work for validating tokens produced with the short key used verbatim. If your tokens are short lived, this is not really a major problem and you might wish to pursue this option so that users don't need to change their config.

frasertweedale commented 7 years ago

@begriffs regarding detecting the problem, I'm pondering how to expose a better error, but leaning against it since the short keys should never have been used in the first place, and it is a security bug in whatever library produced the token. But there is one way to detect it in your program: use the supplied key to attempt to sign a token. If the key is too short you will get a KeySizeTooSmall error. See https://hackage.haskell.org/package/jose-0.5.0.3/docs/Crypto-JOSE-Error.html#t:Error

begriffs commented 7 years ago

Thanks a lot for this suggestion and helping me debug! I would have been lost for a long time with this one. I've got a lot to learn about crypto. I also confirmed that my code works for the key reallyreallyreallyreallyverysafe.

frasertweedale commented 7 years ago

@begriffs you are welcome. Thank you for your valuable contribution!

begriffs commented 7 years ago

This is technically another issue but I'll tack it on the end of the current conversation...

The following JWK gives me JWSError JWSInvalidSignature, do you know what might be wrong with it?

{
  "kty": "RSA",
  "use": "sig",
  "n": "AN2Vq1GNGOiCjdaiOAYcUdgu6B1RYBj2JHd_LhqtY0DUqhLyRXDfdwmJtevxu_BQBSlqsLCW91sfp28Q5-i7T-AIVCwdR9CtIO_4y5JQwB7yPMoTipb6Mr7FBT1rTcZScoeSSV75DSlf-DqNdnuvX_EArkOjaRD5fnEr1yKlGAQr",
  "e": "AQAB",
  "d": "D-onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu_aF5YhXBwkppwxg-EOmXeh-MzL7Zh284OuPbkglAaGhV9bb6_5CpuGb1esyPbYW-Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2Ylk",
  "p": "APLCDZH_u3dDY4Tb7KjfzYIsl2uVItVE5YrBvi1vY-OFjhcDBXx3W_LRF6fFMH4rky7nu5VJMe2swrQYC0Wvzfc",
  "q": "AOmr8dtDh6OtWHpWv-JOZEA7YxH9TdC3yPgXFmbcbEt4wzN4tR6dCkrua7Aj2_GnCaOUTtuoBla3wLA3CFygvm0",
  "dp": "ZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI_Q9FuBo6rKwl4BFoToD7WIUS-hpkagwWiz-6zLoX1dbOZw",
  "dq": "CmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU-upvDEKZsZc_UhT_SySDOxQ4G_523Y0sz_OZtSWcol_UMgQ",
  "qi": "Lesy--GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp-DyAe-b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw"
}
begriffs commented 7 years ago

Actually that error happens when the key fails to validate a jwt, the key itself may be fine.

frasertweedale commented 7 years ago

@begriffs this is also a "key too small" problem. It's a 1024-bit key, but JWA RSA signature algorithms require 2048-bit key at minimum: https://tools.ietf.org/html/rfc7518#section-3.5.

I think I have worked out how I am going to deal with this. A preliminary "check key" step, as a part of every verification, will check for simple problems like this and return the appropriate error if there is a problem. I'll open a new issue for that.

begriffs commented 7 years ago

Specific error messages will be really helpful, thanks for looking into how to do that.