From 3ae378e717ebb9fefab07730c97fed095ad84a0f Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 8 Apr 2022 15:58:35 -0300 Subject: [PATCH 1/2] chore: enable easy printing ct logs during dev --- Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index c86152bb8..6696b110b 100644 --- a/Makefile +++ b/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 From d12229700aec4b44f3bd650167896fa96620d559 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 8 Apr 2022 15:59:59 -0300 Subject: [PATCH 2/2] feat(license): port 4.4 parser to 5.0 --- .../emqx_license/src/emqx_license_parser.erl | 5 +- .../src/emqx_license_parser_legacy.erl | 264 ++++++++++++++++++ lib-ee/emqx_license/test/data/emqx.lic | 25 ++ .../test/emqx_license_parser_SUITE.erl | 189 +++++++------ .../test/emqx_license_parser_legacy_SUITE.erl | 113 ++++++++ .../test/emqx_license_test_lib.erl | 3 + 6 files changed, 509 insertions(+), 90 deletions(-) create mode 100644 lib-ee/emqx_license/src/emqx_license_parser_legacy.erl create mode 100644 lib-ee/emqx_license/test/data/emqx.lic create mode 100644 lib-ee/emqx_license/test/emqx_license_parser_legacy_SUITE.erl diff --git a/lib-ee/emqx_license/src/emqx_license_parser.erl b/lib-ee/emqx_license/src/emqx_license_parser.erl index 0ba2d8e25..f0ac7b8f5 100644 --- a/lib-ee/emqx_license/src/emqx_license_parser.erl +++ b/lib-ee/emqx_license/src/emqx_license_parser.erl @@ -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() :: diff --git a/lib-ee/emqx_license/src/emqx_license_parser_legacy.erl b/lib-ee/emqx_license/src/emqx_license_parser_legacy.erl new file mode 100644 index 000000000..894d5d8aa --- /dev/null +++ b/lib-ee/emqx_license/src/emqx_license_parser_legacy.erl @@ -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(<>), b2l(<>), b2l(<>)}, { + b2l(<>), b2l(<>), b2l(<>) + }}; +local_time([Y1, Y2, M1, M2, D1, D2, H1, H2, Min1, Min2, S1, S2, $Z]) -> + {{b2l(<<"20", Y1, Y2>>), b2l(<>), b2l(<>)}, { + b2l(<>), b2l(<>), b2l(<>) + }}. + +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). diff --git a/lib-ee/emqx_license/test/data/emqx.lic b/lib-ee/emqx_license/test/data/emqx.lic new file mode 100644 index 000000000..e6a6d15db --- /dev/null +++ b/lib-ee/emqx_license/test/data/emqx.lic @@ -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----- diff --git a/lib-ee/emqx_license/test/emqx_license_parser_SUITE.erl b/lib-ee/emqx_license/test/emqx_license_parser_SUITE.erl index 2292eabc6..a1633d17f 100644 --- a/lib-ee/emqx_license/test/emqx_license_parser_SUITE.erl +++ b/lib-ee/emqx_license/test/emqx_license_parser_SUITE.erl @@ -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 diff --git a/lib-ee/emqx_license/test/emqx_license_parser_legacy_SUITE.erl b/lib-ee/emqx_license/test/emqx_license_parser_legacy_SUITE.erl new file mode 100644 index 000000000..02b842d94 --- /dev/null +++ b/lib-ee/emqx_license/test/emqx_license_parser_legacy_SUITE.erl @@ -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}]). diff --git a/lib-ee/emqx_license/test/emqx_license_test_lib.erl b/lib-ee/emqx_license/test/emqx_license_test_lib.erl index 67f01cd92..bf0ea6e66 100644 --- a/lib-ee/emqx_license/test/emqx_license_test_lib.erl +++ b/lib-ee/emqx_license/test/emqx_license_test_lib.erl @@ -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]),