You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2022/05/24 20:08:12 UTC

[couchdb] 01/02: Fix ES{256,384,512} support

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 <rn...@apache.org>
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)).