Sfoglia il codice sorgente

feat(license): port 4.4 parser to 5.0

Thales Macedo Garitezi 3 anni fa
parent
commit
d12229700a

+ 4 - 1
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() ::

+ 264 - 0
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(<<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).

+ 25 - 0
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-----

+ 100 - 89
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

+ 113 - 0
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}]).

+ 3 - 0
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]),