potatosalad / erlang-jose

JSON Object Signing and Encryption (JOSE) for Erlang and Elixir
http://hexdocs.pm/jose
MIT License
309 stars 102 forks source link

Signing using JWK (pem file) does not work #40

Closed PhillippOhlandt closed 4 years ago

PhillippOhlandt commented 6 years ago

Hey,

I got some problems signing my JWT.

Here is what I did:

$ openssl genrsa -out key.pem 2048
$ mv key.pem apps/open_banking/priv/
iex> private_key = Path.join(:code.priv_dir(:open_banking), "key.pem")
iex> rsa_private_jwk = JOSE.JWK.from_pem_file private_key
%JOSE.JWK{fields: %{}, keys: :undefined,
 kty: {:jose_jwk_kty_rsa,
  {:RSAPrivateKey, :"two-prime",
   22332046457435375700241003783212182157205040613088050719590460598788118069205188827134018462832392367420766922675394541217931575350309172026899043009661168397423302584811127842859679661836353606078889592361604906869138279289527532047246660686961391605200017082581947908542302592280837181705509488144630324336776733764948348943636700234620953579588331252380307886562130640702222294659019555815317154077272471492817527117870405984207980260266219135412084712972560648491942923930106640788079505822690722812708890791400059595754605411902505602892984101896880989203912385120404069539802499161112324404101236303359646521127,
   65537,
   5451395077987261248645125326515227888840902692039346253444744017263718401378528325319896049077507562048879705036253747812754159367292462793327904696103870668812701142130528147917499355021712696492822004342599681097585702611865991084301266119444105511390357710715260122386739656549870046430638277481999419087461624894059458878952978396535102971239299099318937593948732260285460999588619876956332528349571659566659574974593931448789108228935216664260481475883213290473542682238114271084408603871967592252370681167305912634957815186927661169325493241219170057029631164522504445029609195486179757953089159104620695570177,
   150887572182841440636877430850908581755558371140126630838246938591497064624568162594488086971879222753955672313964965227039323255542843809749142999120593590046863974369594156037537394851849981259256929111503923815819237701779532116940472653266570337129764457982654879705888137749160267628309997231862574866919,
   148004544936106543508552321602208804176694304414002164923904570823798268536802882401528847675097095905997211404107810718458081557809555190293572137345019351371409317991610435490704214236427684081041827184226202265919219822532141503574411026716274645851436486375693069449772728421718102720809102539460159191233,
   142603801416497795010567331270494936377275889193101658414180953193138787032990942871049904923191894321639080022806597512818539803852300269039659554488768884354070337496044866728855893613206829565239710884499466518577715855326341452785082253851688360948479191342106910510139392466015194724047376574215115502419,
   19837220542880508387309818773422679339733017531662954006005428233154462224808528297221865480233347428754436501116667063657716837874775055183159706035348734034918587198655813744149805725815657978971747409650166481588025495843910019796414643005870828526771413038803850070140586942587726235555291769635748330497,
   50403300109040396758556275682841361005603617537181539020347988394450752313081994368221099880412626826381519161645518774185327850635905880855265366670844196584423855117267341372161428320313466558605873133909267516902767431014853648114897372120368656497856772558151087460387068286871345423932505740591096089993,
   :asn1_NOVALUE}}}

iex> rsa_public_jwk  = JOSE.JWK.to_public(rsa_private_jwk)
%JOSE.JWK{fields: %{}, keys: :undefined,
 kty: {:jose_jwk_kty_rsa,
  {:RSAPublicKey,
   22332046457435375700241003783212182157205040613088050719590460598788118069205188827134018462832392367420766922675394541217931575350309172026899043009661168397423302584811127842859679661836353606078889592361604906869138279289527532047246660686961391605200017082581947908542302592280837181705509488144630324336776733764948348943636700234620953579588331252380307886562130640702222294659019555815317154077272471492817527117870405984207980260266219135412084712972560648491942923930106640788079505822690722812708890791400059595754605411902505602892984101896880989203912385120404069539802499161112324404101236303359646521127,
   65537}}}

iex> data = %{"claims" => %{"id_token" => %{"openbanking_intent_id" => %{"value" => "id here", "essential" => true}}}}
iex> data_string = Poison.encode data
iex> signed = JOSE.JWK.sign(data_string, rsa_private_jwk)
** (FunctionClauseError) no function clause matching in :jose_jws.sign/4

    The following arguments were given to :jose_jws.sign/4:

        # 1
        {:jose_jwk, :undefined,
     {:jose_jwk_kty_rsa,
      {:RSAPrivateKey, :"two-prime",
       22332046457435375700241003783212182157205040613088050719590460598788118069205188827134018462832392367420766922675394541217931575350309172026899043009661168397423302584811127842859679661836353606078889592361604906869138279289527532047246660686961391605200017082581947908542302592280837181705509488144630324336776733764948348943636700234620953579588331252380307886562130640702222294659019555815317154077272471492817527117870405984207980260266219135412084712972560648491942923930106640788079505822690722812708890791400059595754605411902505602892984101896880989203912385120404069539802499161112324404101236303359646521127,
       65537,
       5451395077987261248645125326515227888840902692039346253444744017263718401378528325319896049077507562048879705036253747812754159367292462793327904696103870668812701142130528147917499355021712696492822004342599681097585702611865991084301266119444105511390357710715260122386739656549870046430638277481999419087461624894059458878952978396535102971239299099318937593948732260285460999588619876956332528349571659566659574974593931448789108228935216664260481475883213290473542682238114271084408603871967592252370681167305912634957815186927661169325493241219170057029631164522504445029609195486179757953089159104620695570177,
       150887572182841440636877430850908581755558371140126630838246938591497064624568162594488086971879222753955672313964965227039323255542843809749142999120593590046863974369594156037537394851849981259256929111503923815819237701779532116940472653266570337129764457982654879705888137749160267628309997231862574866919,
       148004544936106543508552321602208804176694304414002164923904570823798268536802882401528847675097095905997211404107810718458081557809555190293572137345019351371409317991610435490704214236427684081041827184226202265919219822532141503574411026716274645851436486375693069449772728421718102720809102539460159191233,
       142603801416497795010567331270494936377275889193101658414180953193138787032990942871049904923191894321639080022806597512818539803852300269039659554488768884354070337496044866728855893613206829565239710884499466518577715855326341452785082253851688360948479191342106910510139392466015194724047376574215115502419,
       19837220542880508387309818773422679339733017531662954006005428233154462224808528297221865480233347428754436501116667063657716837874775055183159706035348734034918587198655813744149805725815657978971747409650166481588025495843910019796414643005870828526771413038803850070140586942587726235555291769635748330497,
       50403300109040396758556275682841361005603617537181539020347988394450752313081994368221099880412626826381519161645518774185327850635905880855265366670844196584423855117267341372161428320313466558605873133909267516902767431014853648114897372120368656497856772558151087460387068286871345423932505740591096089993,
       :asn1_NOVALUE}}, %{}}

        # 2
        {:ok,
     "{\"claims\":{\"id_token\":{\"openbanking_intent_id\":{\"value\":\"id here\",\"essential\":true}}}}"}

        # 3
        %{}

        # 4
        {:jose_jws, {:jose_jws_alg_rsa_pss, :PS256}, :undefined, %{}}

    (jose) src/jose_jws.erl:259: :jose_jws.sign/4

I basically followed the example in the README.

I want to implement https://openbanking.atlassian.net/wiki/spaces/DZ/pages/7046134/Open+Banking+Security+Profile+-+Implementer+s+Draft+v1.1.0#OpenBankingSecurityProfile-Implementer'sDraftv1.1.0-JSONSecuritySuiteInformationv1.0 and have quite a lot of problems doing so. Maybe someone who is a bit deeper into this topic could help me out with this.

Thanks.

potatosalad commented 6 years ago

@PhillippOhlandt It looks like you're actually passing {:ok, "..."} instead of just "..." as the second argument there.

Try using {:ok, data_string} = Poison.encode(data) or data_string = Poison.encode!(data) and let me know if things work as expected or not.

PhillippOhlandt commented 6 years ago

@potatosalad Uh, you are right. I simply didn't see that. Now it works.

iex(8)> signed = JOSE.JWK.sign(data_string, %{ "alg" => "RS256" }, rsa_private_jwk)
{%{alg: :jose_jws_alg_rsa_pkcs1_v1_5},
 %{"payload" => "eyJjbGFpbXMiOnsiaWRfdG9rZW4iOnsib3BlbmJhbmtpbmdfaW50ZW50X2lkIjp7InZhbHVlIjoiaWQgaGVyZSIsImVzc2VudGlhbCI6dHJ1ZX19fX0",
   "protected" => "eyJhbGciOiJSUzI1NiJ9",
   "signature" => "dBPQvhT8fmGRUh2bngAyYbnoKWqCrXz7j891u-flOfvyqFbcMW99bFE-7NgtSuykfzjRZmWKD52OuigyXI7ToFxFhZwqcmzECEoEECqNHiGf4BF-MdUx4m8MSfFQ01U0T8-aqyg9MLxBwq72RC0toWeSlljukzEG_4o8pK64Hz3e_g6vwjeIAW74ROGClYHlQ20G0oeV1rkkom9VVZHcgd1eeGtqZr640SwAhP6vgIRtwRm9wSrS_PpRWRo8AyoDVC-lB2li_m7vaMrGzBfUgYb_qUGaqScU7l-Ur1w9x1iclZY3iraWopw7yfcx_gY7U4nC1_us1IQ383R0zvUntA"}}

I guess the "signature" is my JWT now, right? (Sorry, the whole thing with the banking spec just confuses me.)

EDIT: I should think before I post. The whole thing seems to be the JWT. I guess I have to concatenate them with a . to build the JWT. Is there a helper function for that?

potatosalad commented 6 years ago

Is there a helper function for that?

Yes, JOSE.JWS.compact/1.

Also, you might have a better experience overall using JOSE.JWT (optionally with ojson, but poison also works):

secret_jwk = JOSE.JWK.from("{\"d\":\"Ky7xJxzbqt7Sgpv9rHKJnegcazBv4Rpz6Z0Q4n7TrlKrdyFYQxGAbhQdNcIWn33_D97JwKtmXcQwYz2A8vXlZHbkwBhbBcSCFQ5YSQdU-KCAoSkj-oaLVlzSs6Gudqxwa3o59S6Y_sCz7v4-zAbf8MxqmKB1cBG7fmF3T8b8LTAAwtvm2UHtnH9dFgcOFGbDLAZnLa6atBT2f5kKJttYet40yPyilOIONZs714QkWhk6ZyzWYtE11zyIwZu4IUlE4JHyF41O1LrYZ5BxdxZBXTTSY0EOz8JM0tMUgr8BxnnI2fjZYwV1E097HFLNUYR-a6bWUngc24oL5NkE_xLHAQ\",\"dp\":\"yxMV-OPnEwEZOySOwJOaRKAQ-uYIe1P-hv3zoEGn-EgSzMYNpFH60O3Aqjn3DaIGch-NzykXk8yd1izQCK2u4nGLk_S89_W8Vcninj_satS-iaBlfNz5B0h14jwGfiqUyZYugnmPqh0zbgTetjpNq9UwcWmu7P2SQjSO5FVj81M\",\"dq\":\"HD_Hn1t44qEafLr9KAHeMIB3CAJOyRzgvM6gGLTkNRzOrhHINMkdJDxYHSTSya8F24aGfDnW6Y3JdFh8wU0pyhrQ5z-iK1XVvAbMn3GTiLDw9QDzlhsFEbHMB-Bou5iuLxue5OLAb4fhrlav04NgPjvy7x_dpDWGjpnJOWeyPAE\",\"e\":\"AQAB\",\"kty\":\"RSA\",\"n\":\"sOdk8QW1jengFToHHKWfKbqWqeQT3fws_6ninQHLiCaPug1eMcMvMv23YYFFL5mQZmDevTnhOxaY5-5mnLyzhCYrD6rrU8-biE_W5Zcv2GcSr1ijJqtQslQwA2YHwCqdZPNyBVJRLwqCH5c_X017LZyFmfTEg1TFh87u1XB-SmL8aaLiyl3H8fdYV3ws6AJIn9IyuKGxst6VCgRpoHw3Aif5BRY9bYYbHQpPXEkKZxNDZlZKPD59qvFfqZ3ciyqYWOdXXbHk9H4uo3h-buWLeb9W1GN0R31027gTaZ_dlkrQUIa4tIbMJA9U5kq4KkyFPdECAdF0CrbQm8WhNepXJw\",\"p\":\"1t78KQZNhi6ICa3IshBJs823ECTZICUjsYGUTxAumKbsfbt90TQYGYdDIkMJL6vmaJFA2vvKmqL1U9WWRfKIVb0gZbVmwX-A0HdVHyljMX-oCnolXDLtds_kNCJwotDCLHWpVOX1OlFFeFQBDel9oEfu_7k-JNgw5uLz9sat4ec\",\"q\":\"0sP1t1jWcZpDFZwIexNMius8ZmRz-c_KqFEL3wmz0WY7EjMChd4EHpYLAGCFyQdyjS8nt0Qr7mre9GYm_QZimgOVvkRayEbovTe9943Xux4f4b73z-4jzz0E8eg8gL2akrRmme2AxBbiVoKC3KeDE_ClZM2LiwznZ_7UQrVDuME\",\"qi\":\"R8bUn9ptKidLtMn8LtkibUOOTb0KPTnt9Ftz8t1h121X3qfO-pQWUcDB1gJNhb0kMsNVrZsl1xsIE-aCPPCIJ-WK9nklET-D7DIRYU_tkdmtaIIGNpWCid2qIZZdkb0pGZSIHUydiCvXwwqF_yxiZBGVA9N2gkfoRKA9Zmw3TYk\"}")
# The Open Banking docs mention using the "certificate id" as the key id,
# but I'm not sure how they're calculating that.
# Here I'm just using the JWK's thumbprint as the key id (or "kid").
jws = JOSE.JWS.from(%{
  "alg" => "RS256",
  "kid" => JOSE.JWK.thumbprint(secret_jwk),
  "typ" => "JWT"
})
jwt = JOSE.JWT.from(%{
  "claims" => %{
    "id_token" => %{
      "openbanking_intent_id" => %{
        "value" => "id here",
        "essential" => true
      }
    }
  }
})

signed = JOSE.JWT.sign(secret_jwk, jws, jwt) |> JOSE.JWS.compact() |> elem(1)
# => "eyJhbGciOiJSUzI1NiIsImtpZCI6IkR0c05zLUdKQ0lwLXJTS3RnaUttazI1UnNOS0xNX25pQWttSThvNDV6dW8iLCJ0eXAiOiJKV1QifQ.eyJjbGFpbXMiOnsiaWRfdG9rZW4iOnsib3BlbmJhbmtpbmdfaW50ZW50X2lkIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiJpZCBoZXJlIn19fX0.ftfY08cUPmpx4TBcEJ6MLbqZGgSfVPo7cqWBiGJCSrYCrZoLiABZ5xW9TtKlYdSwhYB-CnfhQgMhGBFs-ITKjrr1FfRYV8RvdpLdtCR_fk72YcPMN_5YZwqDXLtvFNBC56hKhv-UqUeCG3zygXfiqfQeBeXb0czAwHSx5Zw1F7FxLSYSSGyaQwG_gTcQm8XEpV-goEAfERdTY_SkW0xk-KqcHAIFyr5mMVxshRINp1wsTsunfpRcUyRye9FcyK0Nbr3Sn3o4gfCLvXzVcI366Hvdw_Qk9sZGpO7-WMnM2eEBws-QIYjxVZb_OWUeedEnVrkU5LePmeSlRoHmC831cw"

# Verification (only verifies signature, not any internal claims)
public_jwk = JOSE.JWK.to_public(secret_jwk)
{true, ^jwt, ^jws} = JOSE.JWT.verify_strict(public_jwk, ["RS256"], signed)

The JSON encoding and decoding is handled internally by whatever JOSE.json_module/0 returns:

iex> JOSE.json_module()
:jose_json_ojson
PhillippOhlandt commented 6 years ago

Wow, that looks like something that could work! Thank you. Just that I load my secret_jwk from the pem file I generated (like in my example), right?

potatosalad commented 6 years ago

Yes, in my example I'm loading the same PEM file private key you posted originally.

I just converted it to JSON with JOSE.JWK.to_binary/1.