This is an automated email from the ASF dual-hosted git repository. vatamane pushed a commit to branch jwtf-es256-fail-erlang-20 in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit 793967191dab6056b4a2d3d831f4e76ec8870f3d Author: Robert Newson <[email protected]> AuthorDate: Tue May 24 16:16:57 2022 +0100 Fix ES{256,384,512} support DERp --- src/jwtf/src/jwtf.erl | 39 +++++++++++++++++++++++-- src/jwtf/test/jwtf_tests.erl | 69 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/jwtf/src/jwtf.erl b/src/jwtf/src/jwtf.erl index 2bb005bcb..f0a5bd6ca 100644 --- a/src/jwtf/src/jwtf.erl +++ b/src/jwtf/src/jwtf.erl @@ -27,6 +27,8 @@ verification_algorithm/1 ]). +-include_lib("public_key/include/public_key.hrl"). + -define(ALGS, [ % RSA PKCS#1 signature with SHA-256 {<<"RS256">>, {public_key, sha256}}, @@ -70,7 +72,13 @@ encode(Header = {HeaderProps}, Claims, Key) -> SignatureOrMac = case verification_algorithm(Alg) of {public_key, Algorithm} -> - public_key:sign(Message, Algorithm, Key); + Signature = public_key:sign(Message, Algorithm, Key), + case Alg of + <<"ES", _/binary>> -> + der_to_jose(Alg, Signature); + _ -> + Signature + end; {hmac, Algorithm} -> hmac(Algorithm, Key, Message) end, @@ -268,12 +276,19 @@ key(Props, Checks, KS) -> verify(Alg, Header, Payload, SignatureOrMac0, Key) -> Message = <<Header/binary, $., Payload/binary>>, SignatureOrMac1 = b64url:decode(SignatureOrMac0), + SignatureOrMac2 = + case Alg of + <<"ES", _/binary>> -> + jose_to_der(SignatureOrMac1); + _ -> + SignatureOrMac1 + end, {VerificationMethod, Algorithm} = verification_algorithm(Alg), case VerificationMethod of public_key -> - public_key_verify(Algorithm, Message, SignatureOrMac1, Key); + public_key_verify(Algorithm, Message, SignatureOrMac2, Key); hmac -> - hmac_verify(Algorithm, Message, SignatureOrMac1, Key) + hmac_verify(Algorithm, Message, SignatureOrMac2, Key) end. public_key_verify(Algorithm, Message, Signature, PublicKey) -> @@ -292,6 +307,24 @@ hmac_verify(Algorithm, Message, HMAC, SecretKey) -> throw({bad_request, <<"Bad HMAC">>}) end. +jose_to_der(Signature) -> + NumLen = 8 * byte_size(Signature) div 2, + <<R:NumLen, S:NumLen>> = Signature, + SigValue = #'ECDSA-Sig-Value'{r = R, s = S}, + public_key:der_encode('ECDSA-Sig-Value', SigValue). + +der_to_jose(Alg, Signature) -> + #'ECDSA-Sig-Value'{r = R, s = S} = public_key:der_decode('ECDSA-Sig-Value', Signature), + Len = rs_len(Alg), + <<R:Len, S:Len>>. + +rs_len(<<"ES256">>) -> + 256; +rs_len(<<"ES384">>) -> + 384; +rs_len(<<"ES512">>) -> + 512. + split(EncodedToken) -> case binary:split(EncodedToken, <<$.>>, [global]) of [_, _, _] = Split -> Split; diff --git a/src/jwtf/test/jwtf_tests.erl b/src/jwtf/test/jwtf_tests.erl index e36ecbd23..b0b9f1fab 100644 --- a/src/jwtf/test/jwtf_tests.erl +++ b/src/jwtf/test/jwtf_tests.erl @@ -24,7 +24,7 @@ encode(Header0, Payload0) -> valid_header() -> {[{<<"typ">>, <<"JWT">>}, {<<"alg">>, <<"RS256">>}]}. -jwt_io_pubkey() -> +jwt_io_rsa_pubkey() -> PublicKeyPEM = << "-----BEGIN PUBLIC KEY-----\n" "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGH" @@ -36,6 +36,16 @@ jwt_io_pubkey() -> [PEMEntry] = public_key:pem_decode(PublicKeyPEM), public_key:pem_entry_decode(PEMEntry). +jwt_io_ec_pubkey() -> + PublicKeyPEM = << + "-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9" + "q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n" + "-----END PUBLIC KEY-----\n" + >>, + [PEMEntry] = public_key:pem_decode(PublicKeyPEM), + public_key:pem_entry_decode(PEMEntry). + b64_badarg_test() -> Encoded = <<"0.0.0">>, ?assertEqual( @@ -169,7 +179,7 @@ bad_rs256_sig_test() -> {[{<<"typ">>, <<"JWT">>}, {<<"alg">>, <<"RS256">>}]}, {[]} ), - KS = fun(<<"RS256">>, undefined) -> jwt_io_pubkey() end, + KS = fun(<<"RS256">>, undefined) -> jwt_io_rsa_pubkey() end, ?assertEqual( {error, {bad_request, <<"Bad signature">>}}, jwtf:decode(Encoded, [], KS) @@ -264,7 +274,28 @@ rs256_test() -> >>, Checks = [sig, alg], - KS = fun(<<"RS256">>, undefined) -> jwt_io_pubkey() end, + KS = fun(<<"RS256">>, undefined) -> jwt_io_rsa_pubkey() end, + + ExpectedPayload = + {[ + {<<"sub">>, <<"1234567890">>}, + {<<"name">>, <<"John Doe">>}, + {<<"admin">>, true} + ]}, + + ?assertMatch({ok, ExpectedPayload}, jwtf:decode(EncodedToken, Checks, KS)). + +%% jwt.io generated +es256_test() -> + EncodedToken = << + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0N" + "TY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.1g" + "LptYop2guxSZHmf0ga292suPxwBdkijA1ZopCSSYLBdEl8Bg2fsxoU" + "cZuSGztMU9qAKV2p80NQn8czeGhHXA" + >>, + + Checks = [sig, alg], + KS = fun(<<"ES256">>, undefined) -> jwt_io_ec_pubkey() end, ExpectedPayload = {[ @@ -292,10 +323,12 @@ encode_decode_test_() -> encode_decode(Alg) -> {EncodeKey, DecodeKey} = - case jwtf:verification_algorithm(Alg) of - {public_key, _Algorithm} -> - create_keypair(); - {hmac, _Algorithm} -> + case Alg of + <<"RS", _/binary>> -> + create_rsa_keypair(); + <<"ES", _/binary>> -> + create_ec_keypair(); + <<"HS", _/binary>> -> Key = <<"a-super-secret-key">>, {Key, Key} end, @@ -319,7 +352,7 @@ claims() -> {<<"exp">>, EpochSeconds + 3600} ]}. -create_keypair() -> +create_rsa_keypair() -> %% https://tools.ietf.org/html/rfc7517#appendix-C N = decode(<< "t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy" @@ -349,6 +382,26 @@ create_keypair() -> }, {RSAPrivateKey, RSAPublicKey}. +create_ec_keypair() -> + PublicPEM = <<"-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9" + "q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n" + "-----END PUBLIC KEY-----">>, + PrivatePEM = <<"-----BEGIN PRIVATE KEY-----\n" + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2" + "OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r" + "1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G\n" + "-----END PRIVATE KEY-----">>, + + [PublicEntry] = public_key:pem_decode(PublicPEM), + ECPublicKey = public_key:pem_entry_decode(PublicEntry), + + [PrivateEntry] = public_key:pem_decode(PrivatePEM), + ECPrivateKey = public_key:pem_entry_decode(PrivateEntry), + + {ECPrivateKey, ECPublicKey}. + + decode(Goop) -> crypto:bytes_to_integer(b64url:decode(Goop)).
