diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b434bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# general +log +/_build/ +*~ +erl_crash.dump +.tags* +*.sublime-* + +.DS_Store + +# rebar +/_checkouts/ + +*.beam + +# generated +apps/swag_server/* +apps/swag_client/* + + +# containerization +\#* +.\#* +Dockerfile +docker-compose.yml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ca5a761 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "build_utils"] + path = build_utils + url = git@github.com:rbkmoney/build_utils.git + branch = master diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1573d0b --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,45 @@ +#!groovy +// -*- mode: groovy -*- + +def finalHook = { + runStage('store CT logs') { + archive '_build/test/logs/' + } +} + +build('erlang_uac', 'docker-host', finalHook) { + checkoutRepo() + loadBuildUtils() + + def pipeDefault + def withWsCache + runStage('load pipeline') { + env.JENKINS_LIB = "build_utils/jenkins_lib" + pipeDefault = load("${env.JENKINS_LIB}/pipeDefault.groovy") + withWsCache = load("${env.JENKINS_LIB}/withWsCache.groovy") + } + + pipeDefault() { + runStage('compile') { + withGithubPrivkey { + sh 'make wc_compile' + } + } + runStage('lint') { + sh 'make wc_lint' + } + runStage('xref') { + sh 'make wc_xref' + } + runStage('dialyze') { + withWsCache("_build/default/rebar3_19.3_plt") { + sh 'make wc_dialyze' + } + } + runStage('test') { + sh "make wc_test" + } + } +} + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a3d4c13 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +REBAR := $(shell which rebar3 2>/dev/null || which ./rebar3) +SUBMODULES = build_utils +SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES)) + +UTILS_PATH := build_utils +TEMPLATES_PATH := . + +# Name of the service +SERVICE_NAME := erlang_uac + +BUILD_IMAGE_TAG := 562313697353c29d4b34fb081a8b70e8c2207134 + +CALL_ANYWHERE := \ + submodules \ + all compile xref lint dialyze test cover \ + start clean distclean + +CALL_W_CONTAINER := $(CALL_ANYWHERE) + +.PHONY: $(CALL_W_CONTAINER) all + +all: compile + +-include $(UTILS_PATH)/make_lib/utils_container.mk + +$(SUBTARGETS): %/.git: % + git submodule update --init $< + touch $@ + +submodules: $(SUBTARGETS) + +compile: + $(REBAR) compile + +xref: + $(REBAR) xref + +lint: + elvis rock + +dialyze: + $(REBAR) dialyzer + +start: submodules + $(REBAR) run + +clean: + $(REBAR) cover -r + $(REBAR) clean + +distclean: + $(REBAR) clean + rm -rf _build + +cover: + $(REBAR) cover + +# CALL_W_CONTAINER +test: + $(REBAR) ct diff --git a/README.md b/README.md index e69de29..e3fa1b2 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +# Erlang UAC + +Вспомогательное приложение для авторизации и работы с jwt-токенами на capi/wapi \ No newline at end of file diff --git a/build_utils b/build_utils new file mode 160000 index 0000000..39039df --- /dev/null +++ b/build_utils @@ -0,0 +1 @@ +Subproject commit 39039dfc60249c8631b996cf60d4f8c4d64c30cd diff --git a/elvis.config b/elvis.config new file mode 100644 index 0000000..3f7ecb4 --- /dev/null +++ b/elvis.config @@ -0,0 +1,57 @@ +[ + {elvis, [ + {config, [ + #{ + dirs => [ + "src", + "test" + ], + filter => "*.erl", + ignore => ["_SUITE.erl$"], + rules => [ + {elvis_style, line_length, #{limit => 120, skip_comments => false}}, + {elvis_style, no_tabs}, + {elvis_style, no_trailing_whitespace}, + {elvis_style, macro_module_names}, + {elvis_style, operator_spaces, #{rules => [{right, ","}, {right, "++"}, {left, "++"}]}}, + {elvis_style, nesting_level, #{level => 4}}, + {elvis_style, god_modules, #{limit => 25}}, + {elvis_style, no_if_expression}, + {elvis_style, invalid_dynamic_call, #{ignore => [capi_swagger_server]}}, + {elvis_style, used_ignored_variable}, + {elvis_style, no_behavior_info}, + {elvis_style, module_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*(_SUITE)?$"}}, + {elvis_style, function_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*$"}}, + {elvis_style, state_record_and_type}, + {elvis_style, no_spec_with_records}, + {elvis_style, dont_repeat_yourself, #{ + min_complexity => 30, + ignore => [ + capi_tests_SUITE + ] + }}, + {elvis_style, no_debug_call, #{}} + ] + }, + #{ + dirs => ["."], + filter => "Makefile", + ruleset => makefiles + }, + #{ + dirs => ["."], + filter => "elvis.config", + ruleset => elvis_config + }, + #{ + dirs => ["."], + filter => "rebar.config", + rules => [ + {elvis_style, line_length, #{limit => 120, skip_comments => false}}, + {elvis_style, no_tabs}, + {elvis_style, no_trailing_whitespace} + ] + } + ]} + ]} +]. diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..70c3ffd --- /dev/null +++ b/rebar.config @@ -0,0 +1,75 @@ +%% Common project erlang options. +{erl_opts, [ + + % mandatory + debug_info, + warnings_as_errors, + warn_export_all, + warn_missing_spec, + warn_untyped_record, + warn_export_vars, + + % by default + warn_unused_record, + warn_bif_clash, + warn_obsolete_guard, + warn_unused_vars, + warn_shadow_vars, + warn_unused_import, + warn_unused_function, + warn_deprecated_function + + % at will + % bin_opt_info + % no_auto_import + % warn_missing_spec_all +]}. + +%% Common project dependencies. +{deps, [ + {rfc3339, "0.2.2"}, + {jsx, "2.8.2"}, + {jose, "1.7.9"}, + {base64url, "0.0.1"}, + {genlib, + {git, "https://github.com/rbkmoney/genlib.git", + {branch, "master"} + } + } +]}. + +%% XRef checks +{xref_checks, [ + undefined_function_calls, + undefined_functions, + deprecated_functions_calls, + deprecated_functions +]}. +% at will +% {xref_warnings, true}. + +%% Tests +{cover_enabled, true}. + +{dialyzer, [ + {warnings, [ + % mandatory + unmatched_returns, + error_handling, + race_conditions, + unknown + ]}, + {plt_apps, all_deps} +]}. + +{profiles, [ + {test, [ + {deps, [ + {snowflake, + {git, "https://github.com/rbkmoney/snowflake.git", + {branch, "master"} + } + } + ]} + ]} +]}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..0594edc --- /dev/null +++ b/rebar.lock @@ -0,0 +1,16 @@ +{"1.1.0", +[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0}, + {<<"genlib">>, + {git,"https://github.com/rbkmoney/genlib.git", + {ref,"41920d7774d119c294f3aaba4043ced12da2a815"}}, + 0}, + {<<"jose">>,{pkg,<<"jose">>,<<"1.7.9">>},0}, + {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.2">>},0}, + {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.2.2">>},0}]}. +[ +{pkg_hash,[ + {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, + {<<"jose">>, <<"9DC5A14AB62DB4E41677FCC97993752562FB57AD0B8BA062589682EDD3ACB91F">>}, + {<<"jsx">>, <<"7ACC7D785B5ABE8A6E9ADBDE926A24E481F29956DD8B4DF49E3E4E7BCC92A018">>}, + {<<"rfc3339">>, <<"1552DF616ACA368D982E9F085A0E933B6688A3F4938A671798978EC2C0C58730">>}]} +]. diff --git a/src/uac.app.src b/src/uac.app.src new file mode 100644 index 0000000..fca3063 --- /dev/null +++ b/src/uac.app.src @@ -0,0 +1,23 @@ +{application, uac, [ + {description, "Universal authorizer"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {uac, []}}, + {applications, [ + kernel, + stdlib, + public_key, + genlib, + jose, + rfc3339, + base64url, + jsx + ]}, + {env, []}, + {modules, []}, + {maintainers, [ + "" + ]}, + {licenses, []}, + {links, []} +]}. diff --git a/src/uac.erl b/src/uac.erl new file mode 100644 index 0000000..6756fca --- /dev/null +++ b/src/uac.erl @@ -0,0 +1,129 @@ +-module(uac). + +%% App + +-behaviour(application). +-export([start/2, stop/1]). + +%% Supervisor + +-behaviour(supervisor). +-export([init/1]). + +%%API + +-export([configure/1]). +-export([authorize_api_key/2]). +-export([authorize_operation/2]). + +-type context() :: uac_authorizer_jwt:t(). +-type claims() :: uac_authorizer_jwt:claims(). + +-type configuration() :: #{ + jwt := uac_authorizer_jwt:options(), + access := uac_conf:options() +}. + +-type verification_opts() :: #{ + check_expired_as_of => genlib_time:ts() +}. + +-type api_key() :: binary(). + +-export_type([context/0]). +-export_type([claims/0]). +-export_type([verification_opts/0]). + +%% +% API +%% + +-spec configure(configuration()) -> + ok. +configure(Config) -> + AuthorizerConfig = maps:get(jwt, Config), + AccessConfig = maps:get(access, Config), + ok = uac_authorizer_jwt:configure(AuthorizerConfig), + ok = uac_conf:configure(AccessConfig). + +-spec authorize_api_key( + ApiKey :: api_key(), + VerificationOpts :: verification_opts() +) -> {ok, Context :: context()} | {error, Reason :: atom()}. + +authorize_api_key(ApiKey, VerificationOpts) -> + case parse_api_key(ApiKey) of + {ok, {Type, Credentials}} -> + authorize_api_key(Type, Credentials, VerificationOpts); + {error, Error} -> + {error, Error} + end. + +-spec parse_api_key(ApiKey :: api_key()) -> + {ok, {bearer, Credentials :: binary()}} | {error, Reason :: atom()}. + +parse_api_key(ApiKey) -> + case ApiKey of + <<"Bearer ", Credentials/binary>> -> + {ok, {bearer, Credentials}}; + _ -> + {error, unsupported_auth_scheme} + end. + +-spec authorize_api_key( + Type :: atom(), + Credentials :: binary(), + VerificationOpts :: verification_opts() +) -> + {ok, Context :: context()} | {error, Reason :: atom()}. + +authorize_api_key(bearer, Token, VerificationOpts) -> + uac_authorizer_jwt:verify(Token, VerificationOpts). + +%% + +-spec authorize_operation( + AccessScope :: uac_conf:operation_access_scopes(), + Auth :: uac_authorizer_jwt:t() +) -> + ok | {error, unauthorized}. + +authorize_operation(AccessScope, {_, {_SubjectID, ACL}, _}) -> + case lists:all( + fun ({Scope, Permission}) -> + lists:member(Permission, uac_acl:match(Scope, ACL)) + end, + AccessScope + ) of + true -> + ok; + false -> + {error, unauthorized} + end. + +%% +% App +%% + +-spec start(any(), any()) -> + {ok, pid()} | {error, Reason :: term()}. +start(_StartType, _StartArgs) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +-spec stop(any()) -> + ok. +stop(_State) -> + ok. + +%% +% Supervisor +%% + +-spec init([]) -> + {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + AuthorizerSpec = uac_authorizer_jwt:get_child_spec(), + AccessSpec = uac_conf:get_child_spec(), + SupFlags = #{}, + Children = AuthorizerSpec ++ AccessSpec, + {ok, {SupFlags, Children}}. diff --git a/src/uac_acl.erl b/src/uac_acl.erl new file mode 100644 index 0000000..a3edd60 --- /dev/null +++ b/src/uac_acl.erl @@ -0,0 +1,242 @@ +-module(uac_acl). + +%% + +-opaque t() :: [{{priority(), scope()}, [permission()]}]. + +-type priority() :: integer(). +-type scope() :: [resource() | {resource(), resource_id()}, ...]. +-type resource() :: atom(). +-type resource_id() :: binary(). +-type permission() :: read | write. + +-export_type([t/0]). +-export_type([scope/0]). +-export_type([resource/0]). +-export_type([permission/0]). + +-export([new/0]). +-export([to_list/1]). +-export([from_list/1]). +-export([insert_scope/3]). +-export([remove_scope/3]). + +-export([match/2]). + +-export([decode/1]). +-export([encode/1]). + +%% + +-spec new() -> + t(). + +new() -> + []. + +-spec to_list(t()) -> + [{scope(), permission()}]. + +to_list(ACL) -> + [{S, P} || {{_, S}, P} <- ACL]. + +-spec from_list([{scope(), permission()}]) -> + t(). + +from_list(L) -> + lists:foldl(fun ({S, P}, ACL) -> insert_scope(S, P, ACL) end, new(), L). + +-spec insert_scope(scope(), permission(), t()) -> + t(). + +insert_scope(Scope, Permission, ACL) -> + Priority = compute_priority(Scope, Permission), + insert({{Priority, Scope}, [Permission]}, ACL). + +insert({PS, _} = V, [{PS0, _} = V0 | Vs]) when PS < PS0 -> + [V0 | insert(V, Vs)]; +insert({PS, Perms}, [{PS, Perms0} | Vs]) -> + % NOTE squashing permissions of entries with the same scope + [{PS, lists:usort(Perms ++ Perms0)} | Vs]; +insert({PS, _} = V, [{PS0, _} | _] = Vs) when PS > PS0 -> + [V | Vs]; +insert(V, []) -> + [V]. + +-spec remove_scope(scope(), permission(), t()) -> + t(). + +remove_scope(Scope, Permission, ACL) -> + Priority = compute_priority(Scope, Permission), + remove({{Priority, Scope}, [Permission]}, ACL). + +remove(V, [V | Vs]) -> + Vs; +remove({PS, Perms}, [{PS, Perms0} | Vs]) -> + [{PS, Perms0 -- Perms} | Vs]; +remove(V, [V0 | Vs]) -> + [V0 | remove(V, Vs)]; +remove(_, []) -> + []. + +compute_priority(Scope, Permission) -> + % NOTE + % Scope priority depends on the following attributes, in the order of decreasing + % importance: + % 1. Depth, deeper is more important + % 2. Scope element specificity, element marked with an ID is more important + compute_scope_priority(Scope) + compute_permission_priority(Permission). + +compute_scope_priority(Scope) when length(Scope) > 0 -> + compute_scope_priority(Scope, get_resource_hierarchy(), 0); +compute_scope_priority(Scope) -> + error({badarg, {scope, Scope}}). + +compute_scope_priority([{Resource, _ID} | Rest], H, P) -> + compute_scope_priority(Rest, delve(Resource, H), P * 10 + 2); +compute_scope_priority([Resource | Rest], H, P) -> + compute_scope_priority(Rest, delve(Resource, H), P * 10 + 1); +compute_scope_priority([], _, P) -> + P * 10. + +compute_permission_priority(read) -> + 0; +compute_permission_priority(write) -> + 0; +compute_permission_priority(V) -> + error({badarg, {permission, V}}). + +%% + +-spec match(scope(), t()) -> + [permission()]. + +match(Scope, ACL) when length(Scope) > 0 -> + match_rules(Scope, ACL); +match(Scope, _) -> + error({badarg, {scope, Scope}}). + +match_rules(Scope, [{{_Priority, ScopePrefix}, Permissions} | Rest]) -> + % NOTE + % The `Scope` matches iff `ScopePrefix` is scope prefix of the `Scope`. + % An element of a scope matches corresponding element of a scope prefix + % according to the following rules: + % 1. Scope prefix element marked with resource and ID matches exactly the same + % scope element. + % 2. Scope prefix element marked with only resource matches any scope element + % marked with the same resource. + case match_scope(Scope, ScopePrefix) of + true -> + Permissions; + false -> + match_rules(Scope, Rest) + end; +match_rules(_Scope, []) -> + []. + +match_scope([V | Ss], [V | Ss0]) -> + match_scope(Ss, Ss0); +match_scope([{V, _ID} | Ss], [V | Ss0]) -> + match_scope(Ss, Ss0); +match_scope(_, []) -> + true; +match_scope(_, _) -> + false. + +%% + +-spec decode([binary()]) -> + t(). + +decode(V) -> + lists:foldl(fun decode_entry/2, new(), V). + +decode_entry(V, ACL) -> + case binary:split(V, <<":">>, [global]) of + [V1, V2] -> + Scope = decode_scope(V1), + Permission = decode_permission(V2), + insert_scope(Scope, Permission, ACL); + _ -> + error({badarg, {role, V}}) + end. + +decode_scope(V) -> + Hierarchy = get_resource_hierarchy(), + decode_scope_frags(binary:split(V, <<".">>, [global]), Hierarchy). + +decode_scope_frags([V1, V2 | Vs], H) -> + {Resource, H1} = decode_scope_frag_resource(V1, V2, H), + [Resource | decode_scope_frags(Vs, H1)]; +decode_scope_frags([V], H) -> + decode_scope_frags([V, <<"*">>], H); +decode_scope_frags([], _) -> + []. + +decode_scope_frag_resource(V, <<"*">>, H) -> + R = decode_resource(V), + {R, delve(R, H)}; +decode_scope_frag_resource(V, ID, H) -> + R = decode_resource(V), + {{R, ID}, delve(R, H)}. + +decode_resource(V) -> + try binary_to_existing_atom(V, utf8) catch + error:badarg -> + error({badarg, {resource, V}}) + end. + +decode_permission(<<"read">>) -> + read; +decode_permission(<<"write">>) -> + write; +decode_permission(V) -> + error({badarg, {permission, V}}). + +%% + +-spec encode(t()) -> + [binary()]. + +encode(ACL) -> + lists:flatmap(fun encode_entry/1, ACL). + +encode_entry({{_Priority, Scope}, Permissions}) -> + S = encode_scope(Scope), + [begin P = encode_permission(Permission), <> end + || Permission <- Permissions]. + +encode_scope(Scope) -> + Hierarchy = get_resource_hierarchy(), + genlib_string:join($., encode_scope_frags(Scope, Hierarchy)). + +encode_scope_frags([{Resource, ID} | Rest], H) -> + [encode_resource(Resource), ID | encode_scope_frags(Rest, delve(Resource, H))]; +encode_scope_frags([Resource], H) -> + _ = delve(Resource, H), + [encode_resource(Resource)]; +encode_scope_frags([Resource | Rest], H) -> + [encode_resource(Resource), <<"*">> | encode_scope_frags(Rest, delve(Resource, H))]; +encode_scope_frags([], _) -> + []. + +encode_resource(V) -> + atom_to_binary(V, utf8). + +encode_permission(read) -> + <<"read">>; +encode_permission(write) -> + <<"write">>. + +%% + +get_resource_hierarchy() -> + uac_conf:get_resource_hierarchy(). + +delve(Resource, Hierarchy) -> + case maps:find(Resource, Hierarchy) of + {ok, Sub} -> + Sub; + error -> + error({badarg, {resource, Resource}}) + end. diff --git a/src/uac_authorizer_jwt.erl b/src/uac_authorizer_jwt.erl new file mode 100644 index 0000000..bef70c2 --- /dev/null +++ b/src/uac_authorizer_jwt.erl @@ -0,0 +1,388 @@ +-module(uac_authorizer_jwt). + +%% +-export([init/1]). +-export([get_child_spec/0]). + +% TODO +% Extend interface to support proper keystore manipulation + +-export([configure/1]). +-export([issue/4]). +-export([verify/2]). + +%% + +-include_lib("jose/include/jose_jwk.hrl"). +-include_lib("jose/include/jose_jwt.hrl"). + +-type keyname() :: term(). +-type kid() :: binary(). +-type key() :: #jose_jwk{}. +-type token() :: binary(). +-type claims() :: #{binary() => term()}. +-type subject() :: {subject_id(), uac_acl:t()}. +-type subject_id() :: binary(). +-type t() :: {id(), subject(), claims()}. +-type expiration() :: + {lifetime, Seconds :: pos_integer()} | + {deadline, UnixTs :: pos_integer()} | + unlimited. +-type id() :: binary(). + +-export_type([t/0]). +-export_type([subject/0]). +-export_type([claims/0]). +-export_type([token/0]). +-export_type([expiration/0]). + +%% + +-type options() :: #{ + %% The set of keys used to sign issued tokens and verify signatures on such + %% tokens. + keyset => keyset() +}. + +-export_type([options/0]). + +-type keyset() :: #{ + keyname() => keysource() +}. + +-type keysource() :: + {pem_file, file:filename()}. + +-spec get_child_spec() -> + [supervisor:child_spec()]. + +get_child_spec() -> + [#{ + id => ?MODULE, + start => {supervisor, start_link, [?MODULE, []]}, + type => supervisor + }]. + +-spec init([]) -> + {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. + +init([]) -> + ok = create_table(), + {ok, {#{}, []}}. + +%% + +-spec configure(options()) -> + ok. +configure(Options) -> + Keyset = parse_options(Options), + _ = maps:map(fun ensure_store_key/2, Keyset), + ok. + +parse_options(Options) -> + Keyset = maps:get(keyset, Options, #{}), + _ = is_map(Keyset) orelse exit({invalid_option, keyset, Keyset}), + _ = genlib_map:foreach( + fun (K, V) -> + is_keysource(V) orelse exit({invalid_option, K, V}) + end, + Keyset + ), + Keyset. + +is_keysource({pem_file, Fn}) -> + is_list(Fn) orelse is_binary(Fn); +is_keysource(_) -> + false. + +ensure_store_key(Keyname, Source) -> + case store_key(Keyname, Source) of + ok -> + ok; + {error, Reason} -> + exit({import_error, Keyname, Source, Reason}) + end. + +%% + +-spec store_key(keyname(), {pem_file, file:filename()}) -> + ok | {error, file:posix() | {unknown_key, _}}. + +store_key(Keyname, {pem_file, Filename}) -> + store_key(Keyname, {pem_file, Filename}, #{ + kid => fun derive_kid_from_public_key_pem_entry/1 + }). + +derive_kid_from_public_key_pem_entry(JWK) -> + JWKPublic = jose_jwk:to_public(JWK), + {_Module, PublicKey} = JWKPublic#jose_jwk.kty, + {_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey), + base64url:encode(crypto:hash(sha256, Data)). + +-type store_opts() :: #{ + kid => fun ((key()) -> kid()) +}. + +-spec store_key(keyname(), {pem_file, file:filename()}, store_opts()) -> + ok | {error, file:posix() | {unknown_key, _}}. + +store_key(Keyname, {pem_file, Filename}, Opts) -> + case jose_jwk:from_pem_file(Filename) of + JWK = #jose_jwk{} -> + Key = construct_key(derive_kid(JWK, Opts), JWK), + ok = insert_key(Keyname, Key); + Error = {error, _} -> + Error + end. + +derive_kid(JWK, #{kid := DeriveFun}) when is_function(DeriveFun, 1) -> + DeriveFun(JWK). + +construct_key(KID, JWK) -> + Signer = try jose_jwk:signer(JWK) catch error:_ -> undefined end, + Verifier = try jose_jwk:verifier(JWK) catch error:_ -> undefined end, + #{ + jwk => JWK, + kid => KID, + signer => Signer, + can_sign => Signer /= undefined, + verifier => Verifier, + can_verify => Verifier /= undefined + }. + +%% + +-spec issue(id(), expiration(), t(), keyname()) -> + {ok, token()} | + {error, nonexistent_key} | + {error, {invalid_signee, Reason :: atom()}}. + +issue(JTI, Expiration, Auth, Signee) -> + case try_get_key_for_sign(Signee) of + {ok, Key} -> + Claims = construct_final_claims(Auth, Expiration, JTI), + sign(Key, Claims); + {error, Error} -> + {error, Error} + end. + +try_get_key_for_sign(Keyname) -> + case get_key_by_name(Keyname) of + #{can_sign := true} = Key -> + {ok, Key}; + #{} -> + {error, {invalid_signee, signing_not_allowed}}; + undefined -> + {error, nonexistent_key} + end. + +construct_final_claims({{Subject, ACL}, Claims}, Expiration, JTI) -> + maps:merge( + Claims#{ + <<"jti">> => JTI, + <<"sub">> => Subject, + <<"exp">> => get_expires_at(Expiration) + }, + encode_roles(uac_acl:encode(ACL)) + ). + +get_expires_at({lifetime, Lt}) -> + genlib_time:unow() + Lt; +get_expires_at({deadline, Dl}) -> + Dl; +get_expires_at(unlimited) -> + 0. + +sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) -> + JWT = jose_jwt:sign(JWK, JWS#{<<"kid">> => KID}, Claims), + {_Modules, Token} = jose_jws:compact(JWT), + {ok, Token}. + +%% + +-spec verify(token(), uac:verification_opts()) -> + {ok, t()} | + {error, + {invalid_token, + badarg | + {badarg, term()} | + {missing, atom()} | + expired | + {malformed_acl, term()} + } | + {nonexistent_key, kid()} | + invalid_operation | + invalid_signature + }. + +verify(Token, VerificationOpts) -> + try + {_, ExpandedToken} = jose_jws:expand(Token), + #{<<"protected">> := ProtectedHeader} = ExpandedToken, + Header = base64url_to_map(ProtectedHeader), + Alg = get_alg(Header), + KID = get_kid(Header), + verify(KID, Alg, ExpandedToken, VerificationOpts) + catch + %% from get_alg and get_kid + throw:Reason -> + {error, Reason}; + %% TODO we're losing error information here, e.g. stacktrace + error:badarg = Reason -> + {error, {invalid_token, Reason}}; + error:{badarg, _} = Reason -> + {error, {invalid_token, Reason}}; + error:Reason -> + {error, {invalid_token, Reason}} + end. + +verify(KID, Alg, ExpandedToken, VerificationOpts) -> + case get_key_by_kid(KID) of + #{jwk := JWK, verifier := Algs} -> + _ = lists:member(Alg, Algs) orelse throw(invalid_operation), + verify(JWK, ExpandedToken, VerificationOpts); + undefined -> + {error, {nonexistent_key, KID}} + end. + +verify(JWK, ExpandedToken, VerificationOpts) -> + case jose_jwt:verify(JWK, ExpandedToken) of + {true, #jose_jwt{fields = Claims}, _JWS} -> + {KeyMeta, Claims1} = validate_claims(Claims, VerificationOpts), + get_result(KeyMeta, decode_roles(Claims1)); + {false, _JWT, _JWS} -> + {error, invalid_signature} + end. + +validate_claims(Claims, VerificationOpts) -> + validate_claims(Claims, get_validators(), VerificationOpts, #{}). + +validate_claims(Claims, [{Name, Claim, Validator} | Rest], VerificationOpts, Acc) -> + V = Validator(Name, maps:get(Claim, Claims, undefined), VerificationOpts), + validate_claims(maps:without([Claim], Claims), Rest, VerificationOpts, Acc#{Name => V}); +validate_claims(Claims, [], _, Acc) -> + {Acc, Claims}. + +get_result(KeyMeta, {Roles, Claims}) -> + #{token_id := TokenID, subject_id := SubjectID} = KeyMeta, + try + Subject = {SubjectID, uac_acl:decode(Roles)}, + {ok, {TokenID, Subject, Claims}} + catch + error:{badarg, _} = Reason -> + throw({invalid_token, {malformed_acl, Reason}}) + end. + +get_kid(#{<<"kid">> := KID}) when is_binary(KID) -> + KID; +get_kid(#{}) -> + throw({invalid_token, {missing, kid}}). + +get_alg(#{<<"alg">> := Alg}) when is_binary(Alg) -> + Alg; +get_alg(#{}) -> + throw({invalid_token, {missing, alg}}). + +%% + +get_validators() -> + [ + {token_id , <<"jti">> , fun check_presence/3}, + {subject_id , <<"sub">> , fun check_presence/3}, + {expires_at , <<"exp">> , fun check_expiration/3} + ]. + +check_presence(_, V, _) when is_binary(V) -> + V; +check_presence(C, undefined, _) -> + throw({invalid_token, {missing, C}}). + +check_expiration(_, Exp = 0, _) -> + Exp; +check_expiration(_, Exp, Opts) when is_integer(Exp) -> + case get_check_expiry(Opts) of + {true, Now} when Exp > Now -> + Exp; + false when Exp > 0 -> + Exp; + _ -> + throw({invalid_token, expired}) + end; +check_expiration(C, undefined, _) -> + throw({invalid_token, {missing, C}}); +check_expiration(C, V, _) -> + throw({invalid_token, {badarg, {C, V}}}). + +get_check_expiry(Opts) -> + case maps:get(check_expired_as_of, Opts, undefined) of + Now when is_integer(Now) -> + {true, Now}; + undefined -> + false + end. + +%% + +encode_roles(Roles) -> + IssuerResource = uac_conf:get_service_name(), + #{ + <<"resource_access">> => #{ + IssuerResource => #{ + <<"roles">> => Roles + } + } + }. + +decode_roles(Claims = #{ + <<"resource_access">> := Resources +}) when is_map(Resources) andalso map_size(Resources) > 0 -> + Accepted = uac_conf:get_service_name(), + Roles = try_get_roles(Resources, Accepted), + {Roles, maps:remove(<<"resource_access">>, Claims)}; +decode_roles(_) -> + throw({invalid_token, {missing, acl}}). + +try_get_roles(Resources, Accepted) -> + case maps:get(Accepted, Resources, undefined) of + #{<<"roles">> := Roles} -> + Roles; + undefined -> + [] + end. + +%% + +insert_key(Keyname, KeyInfo = #{kid := KID}) -> + insert_values(#{ + {keyname, Keyname} => KeyInfo, + {kid, KID} => KeyInfo + }). + +get_key_by_name(Keyname) -> + lookup_value({keyname, Keyname}). + +get_key_by_kid(KID) -> + lookup_value({kid, KID}). + +base64url_to_map(Base64) when is_binary(Base64) -> + jsx:decode(base64url:decode(Base64), [return_maps]). + +%% + +-define(TABLE, ?MODULE). + +create_table() -> + _ = ets:new(?TABLE, [set, public, named_table, {read_concurrency, true}]), + ok. + +insert_values(Values) -> + true = ets:insert(?TABLE, maps:to_list(Values)), + ok. + +lookup_value(Key) -> + case ets:lookup(?TABLE, Key) of + [{Key, Value}] -> + Value; + [] -> + undefined + end. diff --git a/src/uac_conf.erl b/src/uac_conf.erl new file mode 100644 index 0000000..08401d2 --- /dev/null +++ b/src/uac_conf.erl @@ -0,0 +1,86 @@ +-module(uac_conf). + +%% + +-export([get_child_spec/0]). +-export([init/1]). + +%% API + +-export([configure/1]). +-export([get_service_name/0]). +-export([get_resource_hierarchy/0]). + +-type operation_access_scopes() :: [{uac_acl:scope(), uac_acl:permission()}]. +-type service_name() :: binary(). +-type resource_hierarchy() :: #{uac_acl:resource() => resource_hierarchy() | #{}}. + +-type options() :: #{ + service_name := service_name(), + resource_hierarchy := resource_hierarchy() +}. +-export_type([options/0]). +-export_type([operation_access_scopes/0]). + +%% + +-spec get_child_spec() -> + [supervisor:child_spec()]. + +get_child_spec() -> + [#{ + id => ?MODULE, + start => {supervisor, start_link, [?MODULE, []]}, + type => supervisor + }]. + +-spec init([]) -> + {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. + +init([]) -> + ok = create_table(), + {ok, {#{}, []}}. + +%% +%% API +%% + +-spec get_service_name() -> + service_name(). +get_service_name() -> + lookup_value(service_name). + +-spec get_resource_hierarchy() -> + resource_hierarchy(). +get_resource_hierarchy() -> + lookup_value(resource_hierarchy). + +%% + +-spec configure(options()) -> + ok. +configure(Config) -> + ok = insert_values(Config). + +%% + +-define(TABLE, ?MODULE). + +create_table() -> + _ = ets:new(?TABLE, [set, public, named_table, {read_concurrency, true}]), + ok. + +insert_values(Values) -> + true = ets:insert(?TABLE, maps:to_list(Values)), + ok. + +lookup_value(Key) -> + lookup_value(Key, undefined). + +lookup_value(Key, Default) -> + case ets:lookup(?TABLE, Key) of + [{Key, Value}] -> + Value; + [] -> + Default + end. diff --git a/test/uac_acl_SUITE.erl b/test/uac_acl_SUITE.erl new file mode 100644 index 0000000..9762ada --- /dev/null +++ b/test/uac_acl_SUITE.erl @@ -0,0 +1,181 @@ +-module(uac_acl_SUITE). + +-include_lib("stdlib/include/assert.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-export([all/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([ + illegal_input_test/1, + empty_test/1, + stable_encoding_test/1, + remove_scopes_test/1, + redundancy_test/1, + match_scope_test/1 +]). + +-spec illegal_input_test(config()) -> _. +-spec empty_test(config()) -> _. +-spec stable_encoding_test(config()) -> _. +-spec remove_scopes_test(config()) -> _. +-spec redundancy_test(config()) -> _. +-spec match_scope_test(config()) -> _. + +-type test_case_name() :: atom(). +-type config() :: [{atom(), any()}]. +-type group_name() :: atom(). + +-spec all() -> + [test_case_name()]. +all() -> + [ + illegal_input_test, + empty_test, + stable_encoding_test, + remove_scopes_test, + redundancy_test, + match_scope_test + ]. + +-spec init_per_suite(config()) -> + config(). +init_per_suite(Config) -> + Apps = genlib_app:start_application(uac), + uac:configure(#{ + jwt => #{ + keyset => #{ + test => {pem_file, get_keysource("keys/local/private.pem", Config)} + } + }, + access => #{ + service_name => <<"test">>, + resource_hierarchy => #{ + party => #{invoice_templates => #{invoice_template_invoices => #{}}}, + customers => #{bindings => #{}}, + invoices => #{payments => #{}}, + payment_resources => #{} + } + } + }), + [{apps, Apps}] ++ Config. + +-spec init_per_group(group_name(), config()) -> + config(). +init_per_group(_Name, Config) -> + Config. + +-spec init_per_testcase(group_name(), config()) -> + config(). +init_per_testcase(_Name, Config) -> + Config. + +-spec end_per_suite(config()) -> + config(). +end_per_suite(Config) -> + Config. + +-spec end_per_group(group_name(), config()) -> + config(). +end_per_group(_Name, Config) -> + Config. + +-spec end_per_testcase(test_case_name(), config()) -> + config(). +end_per_testcase(_Name, Config) -> + Config. + + +illegal_input_test(_C) -> + ?assertError({badarg, {scope , _}}, from_list([{[], read}])), + ?assertError({badarg, {permission, _}}, from_list([{[invoices], wread}])), + ?assertError({badarg, {resource , _}}, from_list([{[payments], read}])). + +empty_test(_C) -> + [] = encode(from_list([])), + [] = to_list(decode([])). + +stable_encoding_test(_C) -> + ACL1 = from_list([ + {[party], read}, + {[party], write}, + {[invoices], read}, + {[invoices, payments], read}, + {[{invoices, <<"42">>}, payments], write} + ]), + Enc1 = [ + <<"invoices.42.payments:write">>, + <<"invoices.*.payments:read">>, + <<"party:read">>, + <<"party:write">>, + <<"invoices:read">> + ], + Enc1 = encode(ACL1), + ACL1 = decode(Enc1), + ACL1 = decode(encode(ACL1)). + +redundancy_test(_C) -> + [<<"party:read">>] = encode(from_list([{[party], read}, {[party], read}])). + +remove_scopes_test(_C) -> + ?assertEqual(new(), remove([party], read, new())), + ?assertEqual( + from_list([{[party], write}]), + remove([invoices], read, from_list([{[party], write}, {[invoices], read}])) + ), + ?assertEqual( + new(), + remove([party], read, + remove([party], write, + remove([party], read, + from_list([{[party], read}, {[party], write}]) + ) + ) + ) + ). + +match_scope_test(_C) -> + ACL = from_list([ + {[party], read}, + {[party], write}, + {[invoices], read}, + {[invoices, payments], write}, + {[{invoices, <<"42">>}], write}, + {[{invoices, <<"42">>}, payments], read} + ]), + ?assertError({badarg, _} , match([], ACL)), + ?assertEqual([write] , match([{invoices, <<"42">>}], ACL)), + ?assertEqual([read] , match([{invoices, <<"43">>}], ACL)), + ?assertEqual([read] , match([{invoices, <<"42">>}, {payments, <<"1">>}], ACL)), + ?assertEqual([write] , match([{invoices, <<"43">>}, {payments, <<"1">>}], ACL)), + ?assertEqual([read, write] , match([{party, <<"BLARGH">>}], ACL)), + ?assertEqual([] , match([payment_resources], ACL)). + +new() -> + uac_acl:new(). + +from_list(L) -> + uac_acl:from_list(L). + +to_list(L) -> + uac_acl:to_list(L). + +remove(S, P, ACL) -> + uac_acl:remove_scope(S, P, ACL). + +match(S, ACL) -> + uac_acl:match(S, ACL). + +encode(ACL) -> + uac_acl:encode(ACL). + +decode(Bin) -> + uac_acl:decode(Bin). + +get_keysource(Key, Config) -> + filename:join(?config(data_dir, Config), Key). diff --git a/test/uac_acl_SUITE_data/keys/local/private.pem b/test/uac_acl_SUITE_data/keys/local/private.pem new file mode 100644 index 0000000..4e6d12c --- /dev/null +++ b/test/uac_acl_SUITE_data/keys/local/private.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF +B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T +9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+ +gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX +37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX +BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM +GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ== +-----END RSA PRIVATE KEY----- diff --git a/test/uac_tests_SUITE.erl b/test/uac_tests_SUITE.erl new file mode 100644 index 0000000..30a90ba --- /dev/null +++ b/test/uac_tests_SUITE.erl @@ -0,0 +1,235 @@ +-module(uac_tests_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("jose/include/jose_jwk.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([ + successful_auth_test/1, + invalid_permissions_test/1, + bad_token_test/1, + no_token_test/1, + + force_expiration_test/1, + force_expiration_fail_test/1, + + bad_signee_test/1, + + different_issuers_test/1 +]). + +-type test_case_name() :: atom(). +-type config() :: [{atom(), any()}]. +-type group_name() :: atom(). + +-define(expire_as_of_now, #{ + check_expired_as_of => genlib_time:unow() +}). + +-define(test_service_acl(Access), [{[test_resource], Access}]). + +-spec all() -> + [test_case_name()]. +all() -> + [ + {group, general_tests}, + {group, different_issuers} + ]. + +-spec groups() -> + [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {general_tests, [], + [ + successful_auth_test, + invalid_permissions_test, + bad_token_test, + no_token_test, + force_expiration_test, + force_expiration_fail_test, + bad_signee_test + ] + }, + {different_issuers, [], + [ + different_issuers_test + ] + } + ]. + +-spec init_per_suite(config()) -> + config(). +init_per_suite(Config) -> + Config. + +-spec init_per_group(group_name(), config()) -> + config(). +init_per_group(general_tests, Config) -> + Apps = [ + genlib_app:start_application(snowflake), + genlib_app:start_application(uac) + ], + uac:configure(#{ + jwt => #{ + keyset => #{ + test => {pem_file, get_keysource("keys/local/private.pem", Config)} + } + }, + access => #{ + service_name => <<"test">>, + resource_hierarchy => #{ + test_resource => #{} + } + } + }), + [{apps, Apps}] ++ Config; +init_per_group(different_issuers, Config) -> + Apps = [ + genlib_app:start_application(snowflake), + genlib_app:start_application(uac) + ], + uac:configure(#{ + jwt => #{ + keyset => #{ + test => {pem_file, get_keysource("keys/local/private.pem", Config)} + } + }, + access => #{ + service_name => <<"test">>, + resource_hierarchy => #{ + test_resource => #{} + } + } + }), + [{apps, Apps}] ++ Config. + +-spec init_per_testcase(test_case_name(), config()) -> + config(). +init_per_testcase(_Name, Config) -> + Config. + +-spec end_per_suite(config()) -> + _. +end_per_suite(Config) -> + Config. + +-spec end_per_group(group_name(), config()) -> + _. +end_per_group(_Name, Config) -> + [application:stop(App) || App <- ?config(apps, Config)]. + +-spec end_per_testcase(test_case_name(), config()) -> + _. +end_per_testcase(_Name, Config) -> + Config. + +%% + +-spec successful_auth_test(config()) -> + _. +successful_auth_test(_) -> + {ok, Token} = issue_token(?test_service_acl(write), unlimited), + {ok, AccessContext} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, #{}), + ok = uac:authorize_operation(?test_service_acl(write), AccessContext). + +-spec invalid_permissions_test(config()) -> + _. +invalid_permissions_test(_) -> + {ok, Token} = issue_token(?test_service_acl(read), unlimited), + {ok, AccessContext} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, #{}), + {error, _} = uac:authorize_operation(?test_service_acl(write), AccessContext). + +-spec bad_token_test(config()) -> + _. +bad_token_test(Config) -> + {ok, Token} = issue_dummy_token(?test_service_acl(write), Config), + {error, _} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, #{}). + +-spec no_token_test(config()) -> + _. +no_token_test(_) -> + Token = <<"">>, + {error, _} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, #{}). + +-spec force_expiration_test(config()) -> + _. +force_expiration_test(_) -> + {ok, Token} = issue_token(?test_service_acl(write), {deadline, 1}), + {ok, AccessContext} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, #{}), + ok = uac:authorize_operation(?test_service_acl(write), AccessContext). + +-spec force_expiration_fail_test(config()) -> + _. +force_expiration_fail_test(_) -> + {ok, Token} = issue_token(?test_service_acl(write), {deadline, 1}), + {error, _} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, ?expire_as_of_now). + +-spec bad_signee_test(config()) -> + _. +bad_signee_test(_) -> + ACL = ?test_service_acl(write), + {error, nonexistent_key} = + uac_authorizer_jwt:issue(unique_id(), unlimited, {{<<"TEST">>, uac_acl:from_list(ACL)}, #{}}, random). + +%% + +-spec different_issuers_test(config()) -> + _. +different_issuers_test(_) -> + {ok, Token} = issue_token(?test_service_acl(write), unlimited), + uac:configure(#{ + jwt => #{}, + access => #{ + service_name => <<"SOME_OTHER_SERVICE">>, + resource_hierarchy => #{ + test_resource => #{} + } + } + }), + {ok, {_, {_, []}, _}} = uac:authorize_api_key(<<"Bearer ", Token/binary>>, #{}). + +%% + +issue_token(ACL, LifeTime) -> + PartyID = <<"TEST">>, + Claims = #{<<"TEST">> => <<"TEST">>}, + uac_authorizer_jwt:issue(unique_id(), LifeTime, {{PartyID, uac_acl:from_list(ACL)}, Claims}, test). + +issue_dummy_token(ACL, Config) -> + Claims = #{ + <<"jti">> => unique_id(), + <<"sub">> => <<"TEST">>, + <<"exp">> => 0, + <<"resource_access">> => #{ + <<"common-api">> => #{ + <<"roles">> => uac_acl:encode(uac_acl:from_list(ACL)) + } + } + }, + BadPemFile = get_keysource("keys/local/dummy.pem", Config), + BadJWK = jose_jwk:from_pem_file(BadPemFile), + GoodPemFile = get_keysource("keys/local/private.pem", Config), + GoodJWK = jose_jwk:from_pem_file(GoodPemFile), + JWKPublic = jose_jwk:to_public(GoodJWK), + {_Module, PublicKey} = JWKPublic#jose_jwk.kty, + {_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey), + KID = base64url:encode(crypto:hash(sha256, Data)), + JWT = jose_jwt:sign(BadJWK, #{<<"alg">> => <<"RS256">>, <<"kid">> => KID}, Claims), + {_Modules, Token} = jose_jws:compact(JWT), + {ok, Token}. + +get_keysource(Key, Config) -> + filename:join(?config(data_dir, Config), Key). + +unique_id() -> + <> = snowflake:new(), + genlib_format:format_int_base(ID, 62). diff --git a/test/uac_tests_SUITE_data/keys/local/dummy.pem b/test/uac_tests_SUITE_data/keys/local/dummy.pem new file mode 100644 index 0000000..059582b --- /dev/null +++ b/test/uac_tests_SUITE_data/keys/local/dummy.pem @@ -0,0 +1,13 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp +wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 +1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh +3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 +pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX +GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il +AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF +L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k +X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl +U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ +37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/test/uac_tests_SUITE_data/keys/local/private.pem b/test/uac_tests_SUITE_data/keys/local/private.pem new file mode 100644 index 0000000..4e6d12c --- /dev/null +++ b/test/uac_tests_SUITE_data/keys/local/private.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF +B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T +9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+ +gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX +37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX +BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM +GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ== +-----END RSA PRIVATE KEY----- diff --git a/test/uac_tests_SUITE_data/keys/local/public.pem b/test/uac_tests_SUITE_data/keys/local/public.pem new file mode 100644 index 0000000..c2f50d4 --- /dev/null +++ b/test/uac_tests_SUITE_data/keys/local/public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg +7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ== +-----END PUBLIC KEY-----