diff options
author | Robert Newson <rnewson@apache.org> | 2022-05-24 16:16:57 +0100 |
---|---|---|
committer | Robert Newson <rnewson@apache.org> | 2022-05-25 12:45:48 +0100 |
commit | 1c2955795cdf1632026da0306e86e258e495499f (patch) | |
tree | 96a1686b8e3fc2f986d5fbbfefac3948e7d0bb9d | |
parent | c1c9c90ed48db936e1bed3f3ff2fc2b354ea8552 (diff) | |
download | couchdb-1c2955795cdf1632026da0306e86e258e495499f.tar.gz |
Fix ES{256,384,512} support
We forgot to transform the JOSE signature format to DER when verifying
ES signatures (and the reverse when signing).
Co-authored-by: Nick Vatamaniuc <vatamane@apache.org>
Co-authored-by: Jay Doane <jaydoane@apache.org>
-rw-r--r-- | src/jwtf/src/jwtf.erl | 35 | ||||
-rw-r--r-- | src/jwtf/test/jwtf_tests.erl | 84 | ||||
-rw-r--r-- | test/elixir/test/jwtauth_test.exs | 3 |
3 files changed, 110 insertions, 12 deletions
diff --git a/src/jwtf/src/jwtf.erl b/src/jwtf/src/jwtf.erl index 2bb005bcb..b8b30c4f6 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,20 @@ 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(<<"ES", SizeBin/binary>>) -> + binary_to_integer(SizeBin). + 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..f7f410e67 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,41 @@ 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 = + case public_key:pem_entry_decode(PrivateEntry) of + #'ECPrivateKey'{} = Key -> + Key; + {'PrivateKeyInfo', v1, + {'PrivateKeyInfo_privateKeyAlgorithm', ?'id-ecPublicKey', + {asn1_OPENTYPE, Parameters}}, + PrivKey, _} -> + EcPrivKey = public_key:der_decode('ECPrivateKey', PrivKey), + EcPrivKey#'ECPrivateKey'{ + parameters = public_key:der_decode('EcpkParameters', Parameters) + } + end, + + {ECPrivateKey, ECPublicKey}. + decode(Goop) -> crypto:bytes_to_integer(b64url:decode(Goop)). diff --git a/test/elixir/test/jwtauth_test.exs b/test/elixir/test/jwtauth_test.exs index 3f5102921..e4f21f261 100644 --- a/test/elixir/test/jwtauth_test.exs +++ b/test/elixir/test/jwtauth_test.exs @@ -75,7 +75,7 @@ defmodule JwtAuthTest do test "jwt auth with EC secret", _context do require JwtAuthTest.EC - private_key = :public_key.generate_key({:namedCurve, :secp384r1}) + private_key = :public_key.generate_key({:namedCurve, :secp256r1}) point = EC.point(point: EC.private(private_key, :publicKey)) public_key = {point, EC.private(private_key, :parameters)} @@ -125,6 +125,7 @@ defmodule JwtAuthTest do headers: [authorization: "Bearer #{token}"] ) + assert resp.status_code == 200 assert resp.body["userCtx"]["name"] == "couch@apache.org" assert resp.body["info"]["authenticated"] == "jwt" end |