summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2022-05-24 16:16:57 +0100
committerRobert Newson <rnewson@apache.org>2022-05-25 12:45:48 +0100
commit1c2955795cdf1632026da0306e86e258e495499f (patch)
tree96a1686b8e3fc2f986d5fbbfefac3948e7d0bb9d
parentc1c9c90ed48db936e1bed3f3ff2fc2b354ea8552 (diff)
downloadcouchdb-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.erl35
-rw-r--r--src/jwtf/test/jwtf_tests.erl84
-rw-r--r--test/elixir/test/jwtauth_test.exs3
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