yglukhov / nim-jwt

JWT implementation for nim-lang
MIT License
51 stars 11 forks source link

verifySignature doesn't work with AWS cognito / oauth2 which uses ES256 #11

Closed timotheecour closed 2 years ago

timotheecour commented 4 years ago

/cc @yglukhov

verifySignature doesn't work with AWS cognito / oauth2 which uses ES256

I'm not sure why this was commented, but I tried uncommenting and it still didn't work

  # of ES256:
  #   result = crypto.bearVerifyECPem(data, secret, signature, addr sha256Vtable, addr ecPrimeI15, sha256SIZE)

whereas this snippet works in python3:

https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html#user-claims-encoding

import jwt
import requests
import base64
import json

# Step 1: Get the key id from JWT headers (the kid field)
encoded_jwt = headers.dict['x-amzn-oidc-data']
jwt_headers = encoded_jwt.split('.')[0]
decoded_jwt_headers = base64.b64decode(jwt_headers)
decoded_json = json.loads(decoded_jwt_headers)
kid = decoded_json['kid']

# Step 2: Get the public key from regional endpoint
url = 'https://public-keys.auth.elb.' + region + '.amazonaws.com/' + kid
req = requests.get(url)
pub_key = req.text

# Step 3: Get the payload
payload = jwt.decode(encoded_jwt, pub_key, algorithms=['ES256'])

note:

curl https://public-keys.auth.elb.us-west-2.amazonaws.com/<insert kid from header>

links

yglukhov commented 4 years ago

get pub_key as above get header from x-amzn-oidc-data

I would really appreciate a runnable example with hardcoded sample values.

timotheecour commented 4 years ago

I would really appreciate a runnable example with hardcoded sample values.

on my TODO list; in the meantime, here's the approach I've started: wrap examples/main-auth.c from https://github.com/benmcollins/libjwt (that binary works fine including in my above example, reporting the correct error codes (see JWT_VALIDATION_*)

which allows writing a jwt verification using a simple wrapper (wrapping a single function, or maybe just a few for added flexibility) instead of relying on a lot of wrappers (bearssl etc) and duplicating tricky functionality.

all it needs is:

brew install libjwt
{.passl:"-ljwt".}
{.passc: """ example c code""".}

and this can be turned into a higher level wrapper

this would likely belong in a separate jwt library, but since nim doesn't have yet a fully featured jwt solution, I think it's worth exploring in parallel

timotheecour commented 4 years ago

I would really appreciate a runnable example with hardcoded sample values.

@yglukhov here you go. just need to uncomment this in jwt.nim

  # of ES256:
  #   result = crypto.bearVerifyECPem(data, secret, signature, addr sha256Vtable, addr ecPrimeI15, sha256SIZE)

it fails with jwt.nim, and works with benmcollins/libjwt

#[
from https://jwt.io/ ES256
]#
import pkg/jwt
import tables, json

proc test1() =
  let pub = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----
"""
  let priv = """
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----
"""
  let token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA"
  let jwt = token.toJWT()
  let ok = verify(jwt, pub , alg = SignatureAlgorithm.ES256)
  doAssert ok # fails

test1()
yglukhov commented 4 years ago

Thanks! I now remember the root cause of the problem is that there's an issue decoding a PEM EC public key into bearssl public key. Somehow signature verification fails with the decoded key. I've stolen the decoder from here https://github.com/earlephilhower/bearssl-esp8266, but I'm not sure how to fix it.

timotheecour commented 4 years ago

not sure if related to jwkToPem see:

https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html

Use the public key to verify the signature using your JWT library. You might need to convert the JWK to PEM format first. This example takes the JWT and JWK and uses the Node.js library, jsonwebtoken, to verify the JWT signature:

jangko commented 2 years ago

what happen is:

or:

pem public keys can be taken from: https://jwt.io/ -> debugger section and the der structures/values can be decoded using https://lapo.it/asn1js/#

use brEcComputePublicKey/br_ec_compute_pub to compute public key from private key and compare it with decoded public key to know the difference.

yglukhov commented 2 years ago

@jangko Thanks for the tip! Unfortunately just tried that, and it did not help :( I tried your second suggestion (moving key_data):

proc bearVerifyECPem*(data, key: string, sig: openarray[byte], alg: ptr HashClass, impl: ptr EcImpl, hashLen: int): bool =
  # Step 1. Extract RSA key from `key` in PEM format
  var pkCtx: PkeyDecoderContext
  decodeFromPem(pkCtx, key)
  if pkeyDecoderKeyType(addr pkCtx) != KEYTYPE_EC:
    invalidPemKey()
  template pk(): EcPublicKey = pkCtx.key.ec

  assert((pk.q == addr pkCtx.key_data) and pk.qlen == 64)
  discard c_memmove(addr pkCtx.key_data[1], addr pkCtx.key_data[0], 64)
  pkCtx.key_data[0] = 0x04
  pk.qlen = 65

  var digest: array[64, byte]
  calcHash(alg, data, digest)

  let s = ecdsaVrfyRawGetDefault()
  result = s(impl, cast[ptr cuchar](addr digest[0]), hashLen, addr pk, cast[ptr cuchar](unsafeAddr sig[0]), sig.len) == 1
  echo "VERIFY: ", result
jangko commented 2 years ago

weird, it works for me.

import ../jwt/private/crypto
import bearssl

const
  ec256PrivKey = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----"""
  ec256PubKey = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----"""

const data = "hello bear"

var sig = bearSignECPem(data, ec256PrivKey, addr sha256Vtable, addr ecPrimeI15)

doAssert bearVerifyECPem(data, ec256PubKey, sig, addr sha256Vtable, addr ecPrimeI15, sha256SIZE)

it doesn't matter using moveMem or c_memmove, they are the same

proc bearVerifyECPem*(data, key: string, sig: openarray[byte], alg: ptr HashClass, impl: ptr EcImpl, hashLen: int): bool =
  # Step 1. Extract RSA key from `key` in PEM format
  var pkCtx: PkeyDecoderContext
  decodeFromPem(pkCtx, key)
  if pkeyDecoderKeyType(addr pkCtx) != KEYTYPE_EC:
    invalidPemKey()
  template pk(): EcPublicKey = pkCtx.key.ec

  assert((pk.q == addr pkCtx.key_data) and pk.qlen == 64)
  moveMem(addr pkCtx.key_data[1], addr pkCtx.key_data[0], 64)
  pkCtx.key_data[0] = 0x04
  pk.qlen = 65

  var digest: array[64, byte]
  calcHash(alg, data, digest)
  let s = ecdsaVrfyRawGetDefault()
  result = s(impl, cast[ptr cuchar](addr digest[0]), hashLen, addr pk, cast[ptr cuchar](unsafeAddr sig[0]), sig.len) == 1

this one from t_jwt.nim also works

check:
  signedECToken("ES256", ec256PrivKey).verify(ec256PubKey, ES256)
yglukhov commented 2 years ago

My bad, it was my local changes that confused me, and you're right, it works perfectly. Thanks a lot!