Merge pull request #7582 from thalesmg/port-44-license-parser
feat(license): port 4.4 parser to 5.0
This commit is contained in:
commit
2305d90775
9
Makefile
9
Makefile
|
@ -24,6 +24,7 @@ PKG_PROFILES := emqx-pkg emqx-edge-pkg emqx-enterprise-pkg
|
|||
PROFILES := $(REL_PROFILES) $(PKG_PROFILES) default
|
||||
|
||||
CT_NODE_NAME ?= 'test@127.0.0.1'
|
||||
CT_READABLE ?= false
|
||||
|
||||
export REBAR_GIT_CLONE_OPTIONS += --depth=1
|
||||
|
||||
|
@ -73,7 +74,7 @@ ct: $(REBAR) conf-segs
|
|||
|
||||
.PHONY: static_checks
|
||||
static_checks:
|
||||
@$(REBAR) as check do xref, dialyzer, ct --suite apps/emqx/test/emqx_static_checks --readable false
|
||||
@$(REBAR) as check do xref, dialyzer, ct --suite apps/emqx/test/emqx_static_checks --readable $(CT_READABLE)
|
||||
|
||||
APPS=$(shell $(CURDIR)/scripts/find-apps.sh)
|
||||
|
||||
|
@ -96,11 +97,11 @@ $(foreach app,$(APPS),$(eval $(call gen-app-prop-target,$(app))))
|
|||
.PHONY: ct-suite
|
||||
ct-suite: $(REBAR)
|
||||
ifneq ($(TESTCASE),)
|
||||
$(REBAR) ct -v --readable=false --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE)
|
||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE)
|
||||
else ifneq ($(GROUP),)
|
||||
$(REBAR) ct -v --readable=false --name $(CT_NODE_NAME) --suite $(SUITE) --group $(GROUP)
|
||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --group $(GROUP)
|
||||
else
|
||||
$(REBAR) ct -v --readable=false --name $(CT_NODE_NAME) --suite $(SUITE)
|
||||
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE)
|
||||
endif
|
||||
|
||||
.PHONY: cover
|
||||
|
|
|
@ -19,7 +19,10 @@
|
|||
""
|
||||
>>).
|
||||
|
||||
-define(LICENSE_PARSE_MODULES, [emqx_license_parser_v20220101]).
|
||||
-define(LICENSE_PARSE_MODULES, [
|
||||
emqx_license_parser_v20220101,
|
||||
emqx_license_parser_legacy
|
||||
]).
|
||||
|
||||
-type license_data() :: term().
|
||||
-type customer_type() ::
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_license_parser_legacy).
|
||||
|
||||
-behaviour(emqx_license_parser).
|
||||
|
||||
-include_lib("public_key/include/public_key.hrl").
|
||||
-include("emqx_license.hrl").
|
||||
|
||||
-elvis([{elvis_style, atom_naming_convention, disable}]).
|
||||
|
||||
-define(CACERT, <<
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDVDCCAjwCCQCckt8CVupoRDANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJD\n"
|
||||
"TjERMA8GA1UECAwIWmhlamlhbmcxETAPBgNVBAcMCEhhbmd6aG91MQwwCgYDVQQK\n"
|
||||
"DANFTVExDDAKBgNVBAsMA0VNUTEbMBkGA1UEAwwSRU1RWCBFbnRlcnByaXNlIHY1\n"
|
||||
"MB4XDTIyMDQwODE1MTA1M1oXDTIzMDQwODE1MTA1M1owbDELMAkGA1UEBhMCQ04x\n"
|
||||
"ETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhIYW5nemhvdTEMMAoGA1UECgwD\n"
|
||||
"RU1RMQwwCgYDVQQLDANFTVExGzAZBgNVBAMMEkVNUVggRW50ZXJwcmlzZSB2NTCC\n"
|
||||
"ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMiYB/gbxCSErWL8sNZHkP4s\n"
|
||||
"VTyeBho5T+5Uyp2S95qmcj10FBGi50ZnEN/62vMWED3HzEXsp6pq2Jk+Of3g9rSu\n"
|
||||
"63V082HzlqFNHFzUDGkEu23tWyxeEKwBGyYRLIJI1/az99Jq82Qo0UZ5ELVpouAz\n"
|
||||
"QVOKjpehHvWgEuWmPi+w1uuOieO08nO4AAOLHWcNOChgV50sl88gbz2n/kAcjqzl\n"
|
||||
"1MQXMXoRzfzseNf3bmBV0keNFOpcqePTWCeshFFVkqeKMbK5HIKsnoDSl3VtQ/KK\n"
|
||||
"iV88WpW4f0QfGGJV/gHt++4BAZS3nzxXUhGA0Tf2o7N1CHqnXuottJVcgzyIxHEC\n"
|
||||
"AwEAATANBgkqhkiG9w0BAQsFAAOCAQEANh3ofOa9Aoqb7gUoTb6dNj883aHZ4aHi\n"
|
||||
"kQVo4fVc4IH1MLVNuH/H/aqQ+YtRbbE4YT0icApJFa8qriv8afD9reh5/6ySdsms\n"
|
||||
"RAXSogCuAPk2DwT1fyQa6A45x5EBpgwW10rYhwa5JJi6YKPpWS/Uo1Fgk9YGmeW4\n"
|
||||
"FgGWYvWQHQIXhjfTC0wJPXlsDB2AB7xMINlOSfg/Bz8mhz7iOjM4pkvnTj17JrgR\n"
|
||||
"VQLAj4NFAvdLFFjhZarFtCjPiCE4gb5YZI/Os4iMenD1ZWnYy9Sy7JSNXhWda6e2\n"
|
||||
"WGl1AsyDsVPdvAzcB5ymrLnptCzZYT29PSubmCHS9nFgT6hkWCam4g==\n"
|
||||
"-----END CERTIFICATE-----"
|
||||
>>).
|
||||
|
||||
%% emqx_license_parser callbacks
|
||||
-export([
|
||||
parse/2,
|
||||
dump/1,
|
||||
customer_type/1,
|
||||
license_type/1,
|
||||
expiry_date/1,
|
||||
max_connections/1
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% emqx_license_parser API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% Sample parsed data:
|
||||
%% #{customer => <<"EMQ X Evaluation">>,
|
||||
%% email => "contact@emqx.io",
|
||||
%% permits =>
|
||||
%% #{customer_type => 10,
|
||||
%% enabled_plugins =>
|
||||
%% [emqx_backend_redis,emqx_backend_mysql,
|
||||
%% emqx_backend_pgsql,emqx_backend_mongo,
|
||||
%% emqx_backend_cassa,emqx_bridge_kafka,
|
||||
%% emqx_bridge_rabbit],
|
||||
%% max_connections => 10,type => 1},
|
||||
%% product => "EMQX Enterprise",
|
||||
%% validity =>
|
||||
%% {{{2020,6,20},{3,2,52}},{{2049,1,1},{3,2,52}}},
|
||||
%% vendor => "EMQ Technologies Co., Ltd.",
|
||||
%% version => "5.0.0-alpha.1-22e2ad1c"}
|
||||
|
||||
parse(Contents, _PublicKey) ->
|
||||
case decode_and_verify_signature(Contents) of
|
||||
{ok, DerCert} ->
|
||||
parse_payload(DerCert);
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
dump(#{
|
||||
customer := Customer,
|
||||
email := Email,
|
||||
permits :=
|
||||
#{
|
||||
customer_type := CustomerType,
|
||||
max_connections := MaxConnections,
|
||||
type := Type
|
||||
},
|
||||
validity := {{StartAtDate, _StartAtTime}, {ExpiryAtDate, _ExpiryAtTime}}
|
||||
}) ->
|
||||
{DateNow, _} = calendar:universal_time(),
|
||||
Expiry = DateNow > ExpiryAtDate,
|
||||
[
|
||||
{customer, Customer},
|
||||
{email, Email},
|
||||
{max_connections, MaxConnections},
|
||||
{start_at, format_date(StartAtDate)},
|
||||
{expiry_at, format_date(ExpiryAtDate)},
|
||||
{type, format_type(Type)},
|
||||
{customer_type, CustomerType},
|
||||
{expiry, Expiry}
|
||||
].
|
||||
|
||||
customer_type(#{permits := Permits}) ->
|
||||
maps:get(customer_type, Permits, ?LARGE_CUSTOMER).
|
||||
|
||||
license_type(#{permits := Permits}) ->
|
||||
maps:get(type, Permits, ?TRIAL).
|
||||
|
||||
expiry_date(#{validity := {_From, {EndDate, _EndTime}}}) ->
|
||||
EndDate.
|
||||
|
||||
max_connections(#{permits := Permits}) ->
|
||||
maps:get(max_connections, Permits, 0).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
decode_and_verify_signature(Contents) ->
|
||||
try
|
||||
{ok, Cert, DerCert} = decode_license(Contents),
|
||||
[{'Certificate', DerCaCert, _}] = public_key:pem_decode(?CACERT),
|
||||
CaCert = public_key:pkix_decode_cert(DerCaCert, otp),
|
||||
Result = public_key:pkix_path_validation(
|
||||
CaCert,
|
||||
[DerCert],
|
||||
[{verify_fun, {fun verify_fun/3, user_state}}]
|
||||
),
|
||||
case Result of
|
||||
{ok, _Info} ->
|
||||
{ok, Cert};
|
||||
{error, {bad_cert, Reason}} ->
|
||||
{error, Reason}
|
||||
end
|
||||
catch
|
||||
throw:bad_license_format ->
|
||||
{error, bad_license_format};
|
||||
_:_ ->
|
||||
{error, bad_certificate}
|
||||
end.
|
||||
|
||||
decode_license(Contents) ->
|
||||
case public_key:pem_decode(Contents) of
|
||||
[{'Certificate', DerCert, _}] ->
|
||||
Cert = public_key:pkix_decode_cert(DerCert, otp),
|
||||
{ok, Cert, DerCert};
|
||||
_ ->
|
||||
throw(bad_license_format)
|
||||
end.
|
||||
|
||||
parse_payload(DerCert) ->
|
||||
try
|
||||
{Start, End} = read_validity(DerCert),
|
||||
Subject = read_subject(DerCert),
|
||||
Permits = read_permits(DerCert),
|
||||
LicenseData = maps:merge(
|
||||
#{
|
||||
vendor => "EMQ Technologies Co., Ltd.",
|
||||
product => emqx_sys:sysdescr(),
|
||||
version => emqx_sys:version(),
|
||||
validity => {Start, End},
|
||||
permits => Permits
|
||||
},
|
||||
Subject
|
||||
),
|
||||
{ok, LicenseData}
|
||||
catch
|
||||
_:_ ->
|
||||
{error, bad_license}
|
||||
end.
|
||||
|
||||
read_validity(#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{validity = Validity}}) ->
|
||||
case Validity of
|
||||
{'Validity', {utcTime, Start0}, {utcTime, End0}} ->
|
||||
{local_time(Start0), local_time(End0)};
|
||||
{'Validity', {utcTime, Start0}, {generalTime, End0}} ->
|
||||
{local_time(Start0), local_time(End0)}
|
||||
end.
|
||||
|
||||
local_time([Y01, Y0, Y1, Y2, M1, M2, D1, D2, H1, H2, Min1, Min2, S1, S2, $Z]) ->
|
||||
{{b2l(<<Y01, Y0, Y1, Y2>>), b2l(<<M1, M2>>), b2l(<<D1, D2>>)}, {
|
||||
b2l(<<H1, H2>>), b2l(<<Min1, Min2>>), b2l(<<S1, S2>>)
|
||||
}};
|
||||
local_time([Y1, Y2, M1, M2, D1, D2, H1, H2, Min1, Min2, S1, S2, $Z]) ->
|
||||
{{b2l(<<"20", Y1, Y2>>), b2l(<<M1, M2>>), b2l(<<D1, D2>>)}, {
|
||||
b2l(<<H1, H2>>), b2l(<<Min1, Min2>>), b2l(<<S1, S2>>)
|
||||
}}.
|
||||
|
||||
b2l(L) -> binary_to_integer(L).
|
||||
|
||||
read_subject(#'OTPCertificate'{tbsCertificate = TbsCertificate}) ->
|
||||
#'OTPTBSCertificate'{subject = {rdnSequence, RDNs}} = TbsCertificate,
|
||||
read_subject(lists:flatten(RDNs), #{}).
|
||||
|
||||
read_subject([], Subject) ->
|
||||
Subject;
|
||||
read_subject([#'AttributeTypeAndValue'{type = {2, 5, 4, 3}, value = V0} | RDNs], Subject) ->
|
||||
V = unwrap_utf8_string(V0),
|
||||
read_subject(RDNs, maps:put(customer, V, Subject));
|
||||
read_subject([#'AttributeTypeAndValue'{type = {2, 5, 4, 10}, value = V0} | RDNs], Subject) ->
|
||||
V = unwrap_utf8_string(V0),
|
||||
read_subject(RDNs, maps:put(customer, V, Subject));
|
||||
read_subject(
|
||||
[#'AttributeTypeAndValue'{type = {1, 2, 840, 113549, 1, 9, 1}, value = V} | RDNs],
|
||||
Subject
|
||||
) ->
|
||||
read_subject(RDNs, maps:put(email, V, Subject));
|
||||
read_subject([_ | RDNs], Subject) ->
|
||||
read_subject(RDNs, Subject).
|
||||
|
||||
read_permits(#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{extensions = Extensions}}) ->
|
||||
read_permits(Extensions, #{}).
|
||||
|
||||
read_permits([], Permits) ->
|
||||
Permits;
|
||||
read_permits(
|
||||
[#'Extension'{extnID = {1, 3, 6, 1, 4, 1, 52509, 1}, extnValue = Val} | More], Permits
|
||||
) ->
|
||||
MaxConns = list_to_integer(parse_utf8_string(Val)),
|
||||
read_permits(More, maps:put(max_connections, MaxConns, Permits));
|
||||
read_permits(
|
||||
[#'Extension'{extnID = {1, 3, 6, 1, 4, 1, 52509, 2}, extnValue = Val} | More], Permits
|
||||
) ->
|
||||
Plugins = [list_to_atom(Plugin) || Plugin <- string:tokens(parse_utf8_string(Val), ",")],
|
||||
read_permits(More, maps:put(enabled_plugins, Plugins, Permits));
|
||||
read_permits(
|
||||
[#'Extension'{extnID = {1, 3, 6, 1, 4, 1, 52509, 3}, extnValue = Val} | More], Permits
|
||||
) ->
|
||||
Type = list_to_integer(parse_utf8_string(Val)),
|
||||
read_permits(More, maps:put(type, Type, Permits));
|
||||
read_permits(
|
||||
[#'Extension'{extnID = {1, 3, 6, 1, 4, 1, 52509, 4}, extnValue = Val} | More], Permits
|
||||
) ->
|
||||
CustomerType = list_to_integer(parse_utf8_string(Val)),
|
||||
read_permits(More, maps:put(customer_type, CustomerType, Permits));
|
||||
read_permits([_ | More], Permits) ->
|
||||
read_permits(More, Permits).
|
||||
|
||||
unwrap_utf8_string({utf8String, Str}) -> Str;
|
||||
unwrap_utf8_string(Str) -> Str.
|
||||
|
||||
parse_utf8_string(Val) ->
|
||||
{utf8String, Str} = public_key:der_decode('DisplayText', Val),
|
||||
binary_to_list(Str).
|
||||
|
||||
format_date({Year, Month, Day}) ->
|
||||
iolist_to_binary(
|
||||
io_lib:format(
|
||||
"~4..0w-~2..0w-~2..0w",
|
||||
[Year, Month, Day]
|
||||
)
|
||||
).
|
||||
|
||||
format_type(?OFFICIAL) -> <<"official">>;
|
||||
format_type(?TRIAL) -> <<"trial">>.
|
||||
|
||||
%% We want to issue new CA certificates with different issuer and keep
|
||||
%% validating old licenses.
|
||||
verify_fun(_OTPCertificate, {bad_cert, invalid_issuer}, UserState) ->
|
||||
{valid, UserState};
|
||||
%% We want to continue using the same CA certificate even after it
|
||||
%% expires.
|
||||
verify_fun(_OTPCertificate, {bad_cert, cert_expired}, UserState) ->
|
||||
{valid, UserState};
|
||||
verify_fun(OTPCertificate, Event, State) ->
|
||||
DefaultVerifyFun = element(1, ?DEFAULT_VERIFYFUN),
|
||||
DefaultVerifyFun(OTPCertificate, Event, State).
|
|
@ -0,0 +1,25 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIENzCCAx+gAwIBAgIDdMvVMA0GCSqGSIb3DQEBBQUAMIGDMQswCQYDVQQGEwJD
|
||||
TjERMA8GA1UECAwIWmhlamlhbmcxETAPBgNVBAcMCEhhbmd6aG91MQwwCgYDVQQK
|
||||
DANFTVExDDAKBgNVBAsMA0VNUTESMBAGA1UEAwwJKi5lbXF4LmlvMR4wHAYJKoZI
|
||||
hvcNAQkBFg96aGFuZ3doQGVtcXguaW8wHhcNMjAwNjIwMDMwMjUyWhcNNDkwMTAx
|
||||
MDMwMjUyWjBjMQswCQYDVQQGEwJDTjEZMBcGA1UECgwQRU1RIFggRXZhbHVhdGlv
|
||||
bjEZMBcGA1UEAwwQRU1RIFggRXZhbHVhdGlvbjEeMBwGCSqGSIb3DQEJARYPY29u
|
||||
dGFjdEBlbXF4LmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArw+3
|
||||
2w9B7Rr3M7IOiMc7OD3Nzv2KUwtK6OSQ07Y7ikDJh0jynWcw6QamTiRWM2Ale8jr
|
||||
0XAmKgwUSI42+f4w84nPpAH4k1L0zupaR10VYKIowZqXVEvSyV8G2N7091+6Jcon
|
||||
DcaNBqZLRe1DiZXMJlhXnDgq14FPAxffKhCXiCgYtluLDDLKv+w9BaQGZVjxlFe5
|
||||
cw32+z/xHU366npHBpafCbxBtWsNvchMVtLBqv9yPmrMqeBROyoJaI3nL78xDgpd
|
||||
cRorqo+uQ1HWdcM6InEFET6pwkeuAF8/jJRlT12XGgZKKgFQTCkZi4hv7aywkGBE
|
||||
JruPif/wlK0YuPJu6QIDAQABo4HSMIHPMBEGCSsGAQQBg5odAQQEDAIxMDCBlAYJ
|
||||
KwYBBAGDmh0CBIGGDIGDZW1xeF9iYWNrZW5kX3JlZGlzLGVtcXhfYmFja2VuZF9t
|
||||
eXNxbCxlbXF4X2JhY2tlbmRfcGdzcWwsZW1xeF9iYWNrZW5kX21vbmdvLGVtcXhf
|
||||
YmFja2VuZF9jYXNzYSxlbXF4X2JyaWRnZV9rYWZrYSxlbXF4X2JyaWRnZV9yYWJi
|
||||
aXQwEAYJKwYBBAGDmh0DBAMMATEwEQYJKwYBBAGDmh0EBAQMAjEwMA0GCSqGSIb3
|
||||
DQEBBQUAA4IBAQDHUe6+P2U4jMD23u96vxCeQrhc/rXWvpmU5XB8Q/VGnJTmv3yU
|
||||
EPyTFKtEZYVX29z16xoipUE6crlHhETOfezYsm9K0DxF3fNilOLRKkg9VEWcb5hj
|
||||
iL3a2tdZ4sq+h/Z1elIXD71JJBAImjr6BljTIdUCfVtNvxlE8M0D/rKSn2jwzsjI
|
||||
UrW88THMtlz9sb56kmM3JIOoIJoep6xNEajIBnoChSGjtBYFNFwzdwSTCodYkgPu
|
||||
JifqxTKSuwAGSlqxJUwhjWG8ulzL3/pCAYEwlWmd2+nsfotQdiANdaPnez7o0z0s
|
||||
EujOCZMbK8qNfSbyo50q5iIXhz2ZIGl+4hdp
|
||||
-----END CERTIFICATE-----
|
|
@ -43,96 +43,104 @@ t_parse(_Config) ->
|
|||
?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_pem())),
|
||||
|
||||
%% invalid version
|
||||
?assertMatch(
|
||||
{error, [{emqx_license_parser_v20220101, invalid_version}]},
|
||||
emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220101",
|
||||
"0",
|
||||
"10",
|
||||
"Foo",
|
||||
"contact@foo.com",
|
||||
"20220111",
|
||||
"100000",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
)
|
||||
Res1 = emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220101",
|
||||
"0",
|
||||
"10",
|
||||
"Foo",
|
||||
"contact@foo.com",
|
||||
"20220111",
|
||||
"100000",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
),
|
||||
?assertMatch({error, _}, Res1),
|
||||
{error, Err1} = Res1,
|
||||
?assertEqual(
|
||||
invalid_version,
|
||||
proplists:get_value(emqx_license_parser_v20220101, Err1)
|
||||
),
|
||||
|
||||
%% invalid field number
|
||||
?assertMatch(
|
||||
{error, [{emqx_license_parser_v20220101, invalid_field_number}]},
|
||||
emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220111",
|
||||
"0",
|
||||
"10",
|
||||
"Foo",
|
||||
"Bar",
|
||||
"contact@foo.com",
|
||||
"20220111",
|
||||
"100000",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
)
|
||||
Res2 = emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220111",
|
||||
"0",
|
||||
"10",
|
||||
"Foo",
|
||||
"Bar",
|
||||
"contact@foo.com",
|
||||
"20220111",
|
||||
"100000",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
),
|
||||
?assertMatch({error, _}, Res2),
|
||||
{error, Err2} = Res2,
|
||||
?assertEqual(
|
||||
invalid_field_number,
|
||||
proplists:get_value(emqx_license_parser_v20220101, Err2)
|
||||
),
|
||||
|
||||
?assertMatch(
|
||||
{error, [
|
||||
{emqx_license_parser_v20220101, [
|
||||
{type, invalid_license_type},
|
||||
{customer_type, invalid_customer_type},
|
||||
{date_start, invalid_date},
|
||||
{days, invalid_int_value}
|
||||
]}
|
||||
]},
|
||||
emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220111",
|
||||
"zero",
|
||||
"ten",
|
||||
"Foo",
|
||||
"contact@foo.com",
|
||||
"20220231",
|
||||
"-10",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
)
|
||||
Res3 = emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220111",
|
||||
"zero",
|
||||
"ten",
|
||||
"Foo",
|
||||
"contact@foo.com",
|
||||
"20220231",
|
||||
"-10",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
),
|
||||
?assertMatch({error, _}, Res3),
|
||||
{error, Err3} = Res3,
|
||||
?assertEqual(
|
||||
[
|
||||
{type, invalid_license_type},
|
||||
{customer_type, invalid_customer_type},
|
||||
{date_start, invalid_date},
|
||||
{days, invalid_int_value}
|
||||
],
|
||||
proplists:get_value(emqx_license_parser_v20220101, Err3)
|
||||
),
|
||||
|
||||
?assertMatch(
|
||||
{error, [
|
||||
{emqx_license_parser_v20220101, [
|
||||
{type, invalid_license_type},
|
||||
{customer_type, invalid_customer_type},
|
||||
{date_start, invalid_date},
|
||||
{days, invalid_int_value}
|
||||
]}
|
||||
]},
|
||||
emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220111",
|
||||
"zero",
|
||||
"ten",
|
||||
"Foo",
|
||||
"contact@foo.com",
|
||||
"2022-02-1st",
|
||||
"-10",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
)
|
||||
Res4 = emqx_license_parser:parse(
|
||||
emqx_license_test_lib:make_license(
|
||||
[
|
||||
"220111",
|
||||
"zero",
|
||||
"ten",
|
||||
"Foo",
|
||||
"contact@foo.com",
|
||||
"2022-02-1st",
|
||||
"-10",
|
||||
"10"
|
||||
]
|
||||
),
|
||||
public_key_pem()
|
||||
),
|
||||
?assertMatch({error, _}, Res4),
|
||||
{error, Err4} = Res4,
|
||||
?assertEqual(
|
||||
[
|
||||
{type, invalid_license_type},
|
||||
{customer_type, invalid_customer_type},
|
||||
{date_start, invalid_date},
|
||||
{days, invalid_int_value}
|
||||
],
|
||||
proplists:get_value(emqx_license_parser_v20220101, Err4)
|
||||
),
|
||||
|
||||
%% invalid signature
|
||||
|
@ -167,12 +175,15 @@ t_parse(_Config) ->
|
|||
<<".">>
|
||||
),
|
||||
|
||||
?assertMatch(
|
||||
{error, [{emqx_license_parser_v20220101, invalid_signature}]},
|
||||
emqx_license_parser:parse(
|
||||
iolist_to_binary([LicensePart, <<".">>, SignaturePart]),
|
||||
public_key_pem()
|
||||
)
|
||||
Res5 = emqx_license_parser:parse(
|
||||
iolist_to_binary([LicensePart, <<".">>, SignaturePart]),
|
||||
public_key_pem()
|
||||
),
|
||||
?assertMatch({error, _}, Res5),
|
||||
{error, Err5} = Res5,
|
||||
?assertEqual(
|
||||
invalid_signature,
|
||||
proplists:get_value(emqx_license_parser_v20220101, Err5)
|
||||
),
|
||||
|
||||
%% totally invalid strings as license
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_license_parser_legacy_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("public_key/include/public_key.hrl").
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
_ = application:load(emqx_conf),
|
||||
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_) ->
|
||||
emqx_common_test_helpers:stop_apps([emqx_license]),
|
||||
ok.
|
||||
|
||||
init_per_testcase(_Case, Config) ->
|
||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||
Config.
|
||||
|
||||
end_per_testcase(_Case, _Config) ->
|
||||
ok.
|
||||
|
||||
set_special_configs(emqx_license) ->
|
||||
Config = #{file => emqx_license_test_lib:default_license()},
|
||||
emqx_config:put([license], Config);
|
||||
set_special_configs(_) ->
|
||||
ok.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Tests - emqx_license_parser API
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_parse(_Config) ->
|
||||
?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_pem())),
|
||||
|
||||
Res1 = emqx_license_parser:parse(tampered_license(), public_key_pem()),
|
||||
?assertMatch({error, _}, Res1),
|
||||
{error, Errors} = Res1,
|
||||
?assertEqual(
|
||||
invalid_signature,
|
||||
proplists:get_value(emqx_license_parser_legacy, Errors)
|
||||
),
|
||||
|
||||
ok.
|
||||
|
||||
t_dump(_Config) ->
|
||||
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_pem()),
|
||||
?assertEqual(
|
||||
[
|
||||
{customer, <<"EMQ X Evaluation">>},
|
||||
{email, "contact@emqx.io"},
|
||||
{max_connections, 10},
|
||||
{start_at, <<"2020-06-20">>},
|
||||
{expiry_at, <<"2049-01-01">>},
|
||||
{type, <<"official">>},
|
||||
{customer_type, 10},
|
||||
{expiry, false}
|
||||
],
|
||||
emqx_license_parser:dump(License)
|
||||
).
|
||||
|
||||
t_customer_type(_Config) ->
|
||||
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_pem()),
|
||||
?assertEqual(10, emqx_license_parser:customer_type(License)).
|
||||
|
||||
t_license_type(_Config) ->
|
||||
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_pem()),
|
||||
?assertEqual(1, emqx_license_parser:license_type(License)).
|
||||
|
||||
t_max_connections(_Config) ->
|
||||
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_pem()),
|
||||
?assertEqual(10, emqx_license_parser:max_connections(License)).
|
||||
|
||||
t_expiry_date(_Config) ->
|
||||
{ok, License} = emqx_license_parser:parse(sample_license(), public_key_pem()),
|
||||
?assertEqual({2049, 1, 1}, emqx_license_parser:expiry_date(License)).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% not used for this parser, but required for the behaviour.
|
||||
public_key_pem() ->
|
||||
emqx_license_test_lib:public_key_pem().
|
||||
|
||||
sample_license() ->
|
||||
emqx_license_test_lib:legacy_license().
|
||||
|
||||
tampered_license() ->
|
||||
LicenseBin = emqx_license_test_lib:legacy_license(),
|
||||
[{'Certificate', DerCert, _}] = public_key:pem_decode(LicenseBin),
|
||||
Cert = public_key:pkix_decode_cert(DerCert, otp),
|
||||
TbsCert = Cert#'OTPCertificate'.tbsCertificate,
|
||||
Validity0 = TbsCert#'OTPTBSCertificate'.validity,
|
||||
Validity = Validity0#'Validity'{notBefore = {utcTime, "19800620030252Z"}},
|
||||
|
||||
TamperedCert = Cert#'OTPCertificate'{
|
||||
tbsCertificate =
|
||||
TbsCert#'OTPTBSCertificate'{
|
||||
validity = Validity
|
||||
}
|
||||
},
|
||||
TamperedCertDer = public_key:pkix_encode('OTPCertificate', TamperedCert, otp),
|
||||
public_key:pem_encode([{'Certificate', TamperedCertDer, not_encrypted}]).
|
|
@ -32,6 +32,9 @@ public_key_pem() ->
|
|||
test_key(Filename) ->
|
||||
test_key(Filename, decoded).
|
||||
|
||||
legacy_license() ->
|
||||
test_key("emqx.lic", pem).
|
||||
|
||||
test_key(Filename, Format) ->
|
||||
Dir = code:lib_dir(emqx_license, test),
|
||||
Path = filename:join([Dir, "data", Filename]),
|
||||
|
|
Loading…
Reference in New Issue