From 78d3f49fe4d3760b66841e6fd0b8f568b8e26857 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 15 Dec 2023 12:56:38 +0100 Subject: [PATCH 1/9] fix(gw): use more conservative set of DTLS options --- apps/emqx/test/emqx_common_test_helpers.erl | 26 ++++++++ apps/emqx_gateway/src/emqx_gateway_schema.erl | 6 +- .../test/emqx_sn_protocol_SUITE.erl | 64 ++++++++++++++----- .../test/emqx_sn_protocol_SUITE_data/ca.crt | 20 ++++++ .../dtls.server.crt | 19 ++++++ .../dtls.server.key | 27 ++++++++ 6 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt create mode 100644 apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt create mode 100644 apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index 6f2b05161..96fd83438 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -65,6 +65,11 @@ select_free_port/1 ]). +-export([ + ssl_verify_fun_allow_any_host/0, + ssl_verify_fun_allow_any_host_impl/3 +]). + -export([ emqx_cluster/1, emqx_cluster/2, @@ -1421,3 +1426,24 @@ group_path(Config) -> _:_ -> [] end. + +%% almost verify_none equivalent, but only ignores 'hostname_check_failed' +ssl_verify_fun_allow_any_host_impl(_Cert, Event, State) -> + case Event of + valid -> + {valid, State}; + valid_peer -> + {valid, State}; + {bad_cert, hostname_check_failed} -> + {valid, State}; + {bad_cert, _} -> + {fail, Event}; + {extension, _} -> + {unknown, State} + end. + +ssl_verify_fun_allow_any_host() -> + [ + {verify, verify_peer}, + {verify_fun, {fun ?MODULE:ssl_verify_fun_allow_any_host_impl/3, _State = #{}}} + ]. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index c0abb48ce..ed54383f5 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -174,7 +174,11 @@ fields(dtls_opts) -> reuse_sessions => true, versions => dtls_all_available }, - false + %% NOTE + %% Although dTLS listener is started through `esockd`, it doesn't really support stuff + %% more tightly related to `emqx_listeners` (`enable_crl_check`, `oscp`) and plain TLS + %% (`gc_after_handshake`). + _IsRanchListener = true ). desc(gateway) -> diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl index 0c4c4e6bf..4f69fc271 100644 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl @@ -66,7 +66,6 @@ -elvis([{elvis_style, dont_repeat_yourself, disable}]). -define(CONF_DEFAULT, << - "\n" "gateway.mqttsn {\n" " gateway_id = 1\n" " broadcast = true\n" @@ -86,6 +85,14 @@ " listeners.udp.default {\n" " bind = 1884\n" " }\n" + " listeners.dtls.default {\n" + " bind = 1885\n" + " dtls_options {\n" + " cacertfile = \"${certdir}ca.crt\"\n" + " certfile = \"${certdir}dtls.server.crt\"\n" + " keyfile = \"${certdir}dtls.server.key\"\n" + " }\n" + " }\n" "}\n" >>). @@ -97,9 +104,13 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + ConfTemplate = emqx_template:parse(?CONF_DEFAULT), + Conf = emqx_template:render_strict(ConfTemplate, #{ + certdir => ?config(data_dir, Config) + }), Apps = emqx_cth_suite:start( [ - {emqx_conf, ?CONF_DEFAULT}, + {emqx_conf, Conf}, emqx_gateway, emqx_auth, emqx_management, @@ -191,6 +202,25 @@ t_first_disconnect(_) -> ?assertEqual(<<2, ?SN_DISCONNECT>>, receive_response(Socket)), gen_udp:close(Socket). +t_connect_dtls(Config) -> + SockName = {'mqttsn:dtls:default', 1885}, + ?assertEqual(true, lists:keymember(SockName, 1, esockd:listeners())), + + ClientOpts = [ + binary, + {active, false}, + {protocol, dtls}, + {cacertfile, filename:join(?config(data_dir, Config), "ca.crt")} + | emqx_common_test_helpers:ssl_verify_fun_allow_any_host() + ], + {ok, Socket} = ssl:connect(?HOST, 1885, ClientOpts, 1000), + ok = ssl:send(Socket, make_connect_msg(<<"client_id_test1">>, 1)), + ?assertEqual({ok, <<3, ?SN_CONNACK, 0>>}, ssl:recv(Socket, 0, 1000)), + + ok = ssl:send(Socket, make_disconnect_msg(undefined)), + ?assertEqual({ok, <<2, ?SN_DISCONNECT>>}, ssl:recv(Socket, 0, 1000)), + ssl:close(Socket). + t_subscribe(_) -> Dup = 0, QoS = 0, @@ -2444,10 +2474,7 @@ send_searchgw_msg(Socket) -> Radius = 0, ok = gen_udp:send(Socket, ?HOST, ?PORT, <>). -send_connect_msg(Socket, ClientId) -> - send_connect_msg(Socket, ClientId, 1). - -send_connect_msg(Socket, ClientId, CleanSession) when +make_connect_msg(ClientId, CleanSession) when CleanSession == 0; CleanSession == 1 -> @@ -2460,9 +2487,14 @@ send_connect_msg(Socket, ClientId, CleanSession) when TopicIdType = 0, ProtocolId = 1, Duration = 10, - Packet = - <>, + <>. + +send_connect_msg(Socket, ClientId) -> + send_connect_msg(Socket, ClientId, 1). + +send_connect_msg(Socket, ClientId, CleanSession) -> + Packet = make_connect_msg(ClientId, CleanSession), ok = gen_udp:send(Socket, ?HOST, ?PORT, Packet). send_connect_msg_with_will(Socket, Duration, ClientId) -> @@ -2724,15 +2756,17 @@ send_pingreq_msg(Socket, ClientId) -> ?LOG("send_pingreq_msg ClientId=~p", [ClientId]), ok = gen_udp:send(Socket, ?HOST, ?PORT, PingReqPacket). -send_disconnect_msg(Socket, Duration) -> +make_disconnect_msg(Duration) -> Length = 2, Length2 = 4, MsgType = ?SN_DISCONNECT, - DisConnectPacket = - case Duration of - undefined -> <>; - Other -> <> - end, + case Duration of + undefined -> <>; + Other -> <> + end. + +send_disconnect_msg(Socket, Duration) -> + DisConnectPacket = make_disconnect_msg(Duration), ?LOG("send_disconnect_msg Duration=~p", [Duration]), ok = gen_udp:send(Socket, ?HOST, ?PORT, DisConnectPacket). diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt new file mode 100644 index 000000000..604fd2362 --- /dev/null +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUTCCAjmgAwIBAgIJAPPYCjTmxdt/MA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV +BAYTAkNOMREwDwYDVQQIDAhoYW5nemhvdTEMMAoGA1UECgwDRU1RMQ8wDQYDVQQD +DAZSb290Q0EwHhcNMjAwNTA4MDgwNjUyWhcNMzAwNTA2MDgwNjUyWjA/MQswCQYD +VQQGEwJDTjERMA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UE +AwwGUm9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcgVLex1 +EZ9ON64EX8v+wcSjzOZpiEOsAOuSXOEN3wb8FKUxCdsGrsJYB7a5VM/Jot25Mod2 +juS3OBMg6r85k2TWjdxUoUs+HiUB/pP/ARaaW6VntpAEokpij/przWMPgJnBF3Ur +MjtbLayH9hGmpQrI5c2vmHQ2reRZnSFbY+2b8SXZ+3lZZgz9+BaQYWdQWfaUWEHZ +uDaNiViVO0OT8DRjCuiDp3yYDj3iLWbTA/gDL6Tf5XuHuEwcOQUrd+h0hyIphO8D +tsrsHZ14j4AWYLk1CPA6pq1HIUvEl2rANx2lVUNv+nt64K/Mr3RnVQd9s8bK+TXQ +KGHd2Lv/PALYuwIDAQABo1AwTjAdBgNVHQ4EFgQUGBmW+iDzxctWAWxmhgdlE8Pj +EbQwHwYDVR0jBBgwFoAUGBmW+iDzxctWAWxmhgdlE8PjEbQwDAYDVR0TBAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAGbhRUjpIred4cFAFJ7bbYD9hKu/yzWPWkMRa +ErlCKHmuYsYk+5d16JQhJaFy6MGXfLgo3KV2itl0d+OWNH0U9ULXcglTxy6+njo5 +CFqdUBPwN1jxhzo9yteDMKF4+AHIxbvCAJa17qcwUKR5MKNvv09C6pvQDJLzid7y +E2dkgSuggik3oa0427KvctFf8uhOV94RvEDyqvT5+pgNYZ2Yfga9pD/jjpoHEUlo +88IGU8/wJCx3Ds2yc8+oBg/ynxG8f/HmCC1ET6EHHoe2jlo8FpU/SgGtghS1YL30 +IWxNsPrUP+XsZpBJy/mvOhE5QXo6Y35zDqqj8tI7AGmAWu22jg== +-----END CERTIFICATE----- diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt new file mode 100644 index 000000000..092390b1d --- /dev/null +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER +MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB +MB4XDTIwMDUwODA4MDcwNVoXDTMwMDUwNjA4MDcwNVowPzELMAkGA1UEBhMCQ04x +ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBlNlcnZl +cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALNeWT3pE+QFfiRJzKmn +AMUrWo3K2j/Tm3+Xnl6WLz67/0rcYrJbbKvS3uyRP/stXyXEKw9CepyQ1ViBVFkW +Aoy8qQEOWFDsZc/5UzhXUnb6LXr3qTkFEjNmhj+7uzv/lbBxlUG1NlYzSeOB6/RT +8zH/lhOeKhLnWYPXdXKsa1FL6ij4X8DeDO1kY7fvAGmBn/THh1uTpDizM4YmeI+7 +4dmayA5xXvARte5h4Vu5SIze7iC057N+vymToMk2Jgk+ZZFpyXrnq+yo6RaD3ANc +lrc4FbeUQZ5a5s5Sxgs9a0Y3WMG+7c5VnVXcbjBRz/aq2NtOnQQjikKKQA8GF080 +BQkCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL +BQADggEBAJefnMZpaRDHQSNUIEL3iwGXE9c6PmIsQVE2ustr+CakBp3TZ4l0enLt +iGMfEVFju69cO4oyokWv+hl5eCMkHBf14Kv51vj448jowYnF1zmzn7SEzm5Uzlsa +sqjtAprnLyof69WtLU1j5rYWBuFX86yOTwRAFNjm9fvhAcrEONBsQtqipBWkMROp +iUYMkRqbKcQMdwxov+lHBYKq9zbWRoqLROAn54SRqgQk6c15JdEfgOOjShbsOkIH +UhqcwRkQic7n1zwHVGVDgNIZVgmJ2IdIWBlPEC7oLrRrBD/X1iEEXtKab6p5o22n +KB5mN+iQaE+Oe2cpGKZJiJRdM+IqDDQ= +-----END CERTIFICATE----- diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key new file mode 100644 index 000000000..6c338216e --- /dev/null +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAs15ZPekT5AV+JEnMqacAxStajcraP9Obf5eeXpYvPrv/Stxi +sltsq9Le7JE/+y1fJcQrD0J6nJDVWIFUWRYCjLypAQ5YUOxlz/lTOFdSdvotevep +OQUSM2aGP7u7O/+VsHGVQbU2VjNJ44Hr9FPzMf+WE54qEudZg9d1cqxrUUvqKPhf +wN4M7WRjt+8AaYGf9MeHW5OkOLMzhiZ4j7vh2ZrIDnFe8BG17mHhW7lIjN7uILTn +s36/KZOgyTYmCT5lkWnJeuer7KjpFoPcA1yWtzgVt5RBnlrmzlLGCz1rRjdYwb7t +zlWdVdxuMFHP9qrY206dBCOKQopADwYXTzQFCQIDAQABAoIBAQCuvCbr7Pd3lvI/ +n7VFQG+7pHRe1VKwAxDkx2t8cYos7y/QWcm8Ptwqtw58HzPZGWYrgGMCRpzzkRSF +V9g3wP1S5Scu5C6dBu5YIGc157tqNGXB+SpdZddJQ4Nc6yGHXYERllT04ffBGc3N +WG/oYS/1cSteiSIrsDy/91FvGRCi7FPxH3wIgHssY/tw69s1Cfvaq5lr2NTFzxIG +xCvpJKEdSfVfS9I7LYiymVjst3IOR/w76/ZFY9cRa8ZtmQSWWsm0TUpRC1jdcbkm +ZoJptYWlP+gSwx/fpMYftrkJFGOJhHJHQhwxT5X/ajAISeqjjwkWSEJLwnHQd11C +Zy2+29lBAoGBANlEAIK4VxCqyPXNKfoOOi5dS64NfvyH4A1v2+KaHWc7lqaqPN49 +ezfN2n3X+KWx4cviDD914Yc2JQ1vVJjSaHci7yivocDo2OfZDmjBqzaMp/y+rX1R +/f3MmiTqMa468rjaxI9RRZu7vDgpTR+za1+OBCgMzjvAng8dJuN/5gjlAoGBANNY +uYPKtearBmkqdrSV7eTUe49Nhr0XotLaVBH37TCW0Xv9wjO2xmbm5Ga/DCtPIsBb +yPeYwX9FjoasuadUD7hRvbFu6dBa0HGLmkXRJZTcD7MEX2Lhu4BuC72yDLLFd0r+ +Ep9WP7F5iJyagYqIZtz+4uf7gBvUDdmvXz3sGr1VAoGAdXTD6eeKeiI6PlhKBztF +zOb3EQOO0SsLv3fnodu7ZaHbUgLaoTMPuB17r2jgrYM7FKQCBxTNdfGZmmfDjlLB +0xZ5wL8ibU30ZXL8zTlWPElST9sto4B+FYVVF/vcG9sWeUUb2ncPcJ/Po3UAktDG +jYQTTyuNGtSJHpad/YOZctkCgYBtWRaC7bq3of0rJGFOhdQT9SwItN/lrfj8hyHA +OjpqTV4NfPmhsAtu6j96OZaeQc+FHvgXwt06cE6Rt4RG4uNPRluTFgO7XYFDfitP +vCppnoIw6S5BBvHwPP+uIhUX2bsi/dm8vu8tb+gSvo4PkwtFhEr6I9HglBKmcmog +q6waEQKBgHyecFBeM6Ls11Cd64vborwJPAuxIW7HBAFj/BS99oeG4TjBx4Sz2dFd +rzUibJt4ndnHIvCN8JQkjNG14i9hJln+H3mRss8fbZ9vQdqG+2vOWADYSzzsNI55 +RFY7JjluKcVkp/zCDeUxTU3O6sS+v6/3VE11Cob6OYQx3lN5wrZ3 +-----END RSA PRIVATE KEY----- From 3d679f7e2671e561a9a75bb19340555d39e9321f Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Sat, 16 Dec 2023 01:11:37 +0100 Subject: [PATCH 2/9] test(cth-tls): add test helpers for TLS related activities Currently, only certificate issuing and related utilities are there. --- apps/emqx/test/emqx_cth_tls.erl | 339 ++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 apps/emqx/test/emqx_cth_tls.erl diff --git a/apps/emqx/test/emqx_cth_tls.erl b/apps/emqx/test/emqx_cth_tls.erl new file mode 100644 index 000000000..ccec7626d --- /dev/null +++ b/apps/emqx/test/emqx_cth_tls.erl @@ -0,0 +1,339 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_cth_tls). + +-include_lib("public_key/include/public_key.hrl"). + +-export([gen_cert/1]). +-export([write_cert/2]). +-export([write_cert/3]). +-export([write_pem/2]). + +%% ------------------------------------------------------------------- +%% Certificate Issuing +%% Heavily inspired by: ${ERL_SRC}/lib/public_key/test/erl_make_certs.erl +%% ------------------------------------------------------------------- + +-type pem_entry() :: public_key:pem_entry(). +-type certificate() :: pem_entry(). +-type private_key() :: pem_entry(). + +-type cert_subject() :: #{ + name => string(), + email => string(), + city => string(), + state => string(), + org => string(), + org_unit => string(), + country => string(), + serial => string(), + title => string(), + dnQualifer => string() +}. + +-type cert_validity() :: + {_From :: calendar:date(), _To :: calendar:date()}. + +-type cert_extensions() :: #{ + basic_constraints => false | ca | _PathLenContraint :: pos_integer(), + key_usage => false | certsign +}. + +%% @doc Generate a certificate and a private key. +%% If you need root (CA) certificate, use `root` as `issuer` option. By default, the +%% generated certificate will have according extensions (constraints, key usage, etc). +%% Once root certificate + private key pair is generated, you can use the result +%% as `issuer` option to generate other certificates signed by this root. +-spec gen_cert(Opts) -> {certificate(), private_key()} when + Opts :: #{ + key := ec | rsa | PrivKeyIn, + issuer := root | {CertificateIn, PrivKeyIn}, + subject => cert_subject(), + validity => cert_validity(), + extensions => cert_extensions() | false + }, + CertificateIn :: certificate() | public_key:der_encoded() | #'OTPCertificate'{}, + PrivKeyIn :: private_key() | _PEM :: binary(). +gen_cert(Opts) -> + SubjectPrivateKey = get_privkey(Opts), + {TBSCert, IssuerKey} = make_tbs(SubjectPrivateKey, Opts), + Cert = public_key:pkix_sign(TBSCert, IssuerKey), + true = verify_signature(Cert, IssuerKey), + {encode_cert(Cert), encode_privkey(SubjectPrivateKey)}. + +get_privkey(#{key := Algo}) when is_atom(Algo) -> + gen_privkey(Algo); +get_privkey(#{key := Key}) -> + decode_privkey(Key). + +make_tbs(SubjectKey, Opts) -> + {Issuer, IssuerKey} = issuer(Opts, SubjectKey), + Subject = + case Opts of + #{issuer := root} -> + Issuer; + #{} -> + subject(Opts) + end, + { + #'OTPTBSCertificate'{ + version = v3, + serialNumber = rand:uniform(1000000000000), + signature = sign_algorithm(IssuerKey, Opts), + issuer = Issuer, + validity = validity(Opts), + subject = Subject, + subjectPublicKeyInfo = publickey(SubjectKey), + extensions = extensions(Opts) + }, + IssuerKey + }. + +issuer(Opts = #{issuer := root}, SubjectKey) -> + %% Self signed + {subject(Opts), SubjectKey}; +issuer(#{issuer := {Issuer, IssuerKey}}, _SubjectKey) -> + {issuer_subject(Issuer), decode_privkey(IssuerKey)}. + +issuer_subject({'Certificate', IssuerDer, _}) when is_binary(IssuerDer) -> + issuer_subject(IssuerDer); +issuer_subject(IssuerDer) when is_binary(IssuerDer) -> + issuer_subject(public_key:pkix_decode_cert(IssuerDer, otp)); +issuer_subject(#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{subject = Subject}}) -> + Subject. + +subject(Opts = #{}) -> + Subject = maps:get(subject, Opts, #{}), + Entries = maps:map( + fun(N, V) -> [subject_entry(N, V)] end, + maps:merge(default_subject(Opts), Subject) + ), + {rdnSequence, maps:values(Entries)}. + +subject_entry(name, Name) -> + typed_attr(?'id-at-commonName', {printableString, Name}); +subject_entry(email, Email) -> + typed_attr(?'id-emailAddress', Email); +subject_entry(city, City) -> + typed_attr(?'id-at-localityName', {printableString, City}); +subject_entry(state, State) -> + typed_attr(?'id-at-stateOrProvinceName', {printableString, State}); +subject_entry(org, Org) -> + typed_attr(?'id-at-organizationName', {printableString, Org}); +subject_entry(org_unit, OrgUnit) -> + typed_attr(?'id-at-organizationalUnitName', {printableString, OrgUnit}); +subject_entry(country, Country) -> + typed_attr(?'id-at-countryName', Country); +subject_entry(serial, Serial) -> + typed_attr(?'id-at-serialNumber', Serial); +subject_entry(title, Title) -> + typed_attr(?'id-at-title', {printableString, Title}); +subject_entry(dnQualifer, DnQ) -> + typed_attr(?'id-at-dnQualifier', DnQ). + +subject_info(Info, Subject, Default) -> + case subject_info(Info, Subject) of + undefined -> Default; + Value -> Value + end. + +subject_info(Info, {rdnSequence, Entries}) -> + subject_info(Info, Entries); +subject_info(name, Entries) when is_list(Entries) -> + get_string(find_subject_entry(?'id-at-commonName', Entries)); +subject_info(org, Entries) when is_list(Entries) -> + get_string(find_subject_entry(?'id-at-organizationName', Entries)); +subject_info(org_unit, Entries) when is_list(Entries) -> + get_string(find_subject_entry(?'id-at-organizationalUnitName', Entries)); +subject_info(country, Entries) when is_list(Entries) -> + find_subject_entry(?'id-at-countryName', Entries). + +find_subject_entry(Oid, Entries) -> + emqx_maybe:from_list([ + Value + || Attrs <- Entries, + #'AttributeTypeAndValue'{type = T, value = Value} <- Attrs, + T =:= Oid + ]). + +get_string({printableString, String}) -> + String; +get_string(undefined) -> + undefined. + +typed_attr(Type, Value) -> + #'AttributeTypeAndValue'{type = Type, value = Value}. + +sign_algorithm(#'ECPrivateKey'{parameters = Parms}, _Opts) -> + #'SignatureAlgorithm'{ + algorithm = ?'ecdsa-with-SHA256', + parameters = Parms + }. + +validity(Opts) -> + {From, To} = maps:get(validity, Opts, default_validity()), + #'Validity'{ + notBefore = {generalTime, format_date(From)}, + notAfter = {generalTime, format_date(To)} + }. + +publickey(#'ECPrivateKey'{parameters = Params, publicKey = PubKey}) -> + #'OTPSubjectPublicKeyInfo'{ + algorithm = #'PublicKeyAlgorithm'{ + algorithm = ?'id-ecPublicKey', + parameters = Params + }, + subjectPublicKey = #'ECPoint'{point = PubKey} + }. + +extensions(#{extensions := false}) -> + asn1_NOVALUE; +extensions(Opts) -> + Exts = maps:get(extensions, Opts, #{}), + Default = default_extensions(Opts), + maps:fold( + fun(Name, Data, Acc) -> Acc ++ extension(Name, Data) end, + [], + maps:merge(Default, Exts) + ). + +extension(basic_constraints, false) -> + []; +extension(basic_constraints, ca) -> + [ + #'Extension'{ + extnID = ?'id-ce-basicConstraints', + extnValue = #'BasicConstraints'{cA = true}, + critical = true + } + ]; +extension(basic_constraints, Len) when is_integer(Len) -> + [ + #'Extension'{ + extnID = ?'id-ce-basicConstraints', + extnValue = #'BasicConstraints'{cA = true, pathLenConstraint = Len}, + critical = true + } + ]; +extension(key_usage, false) -> + []; +extension(key_usage, certsign) -> + [ + #'Extension'{ + extnID = ?'id-ce-keyUsage', + extnValue = [keyCertSign], + critical = true + } + ]. + +default_validity() -> + {shift_date(date(), -1), shift_date(date(), +7)}. + +default_subject(#{issuer := root}) -> + #{ + name => "RootCA", + org => "EMQ", + org_unit => "EMQX", + country => "CN" + }; +default_subject(#{}) -> + #{ + name => "Server", + org => "EMQ", + org_unit => "EMQX", + country => "CN" + }. + +default_extensions(#{issuer := root}) -> + #{ + basic_constraints => ca, + key_usage => certsign + }; +default_extensions(#{}) -> + #{}. + +%% ------------------------------------------------------------------- + +verify_signature(CertDer, #'ECPrivateKey'{parameters = Params, publicKey = PubKey}) -> + public_key:pkix_verify(CertDer, {#'ECPoint'{point = PubKey}, Params}); +verify_signature(CertDer, KeyPem) -> + verify_signature(CertDer, decode_privkey(KeyPem)). + +%% ------------------------------------------------------------------- + +gen_privkey(ec) -> + public_key:generate_key({namedCurve, secp256k1}); +gen_privkey(rsa) -> + public_key:generate_key({rsa, 2048, 17}). + +decode_privkey(#'ECPrivateKey'{} = Key) -> + Key; +decode_privkey(#'RSAPrivateKey'{} = Key) -> + Key; +decode_privkey(PemEntry = {_, _, _}) -> + public_key:pem_entry_decode(PemEntry); +decode_privkey(PemBinary) when is_binary(PemBinary) -> + [KeyInfo] = public_key:pem_decode(PemBinary), + decode_privkey(KeyInfo). + +-spec encode_privkey(#'ECPrivateKey'{} | #'RSAPrivateKey'{}) -> private_key(). +encode_privkey(Key = #'ECPrivateKey'{}) -> + {ok, Der} = 'OTP-PUB-KEY':encode('ECPrivateKey', Key), + {'ECPrivateKey', Der, not_encrypted}; +encode_privkey(Key = #'RSAPrivateKey'{}) -> + {ok, Der} = 'OTP-PUB-KEY':encode('RSAPrivateKey', Key), + {'RSAPrivateKey', Der, not_encrypted}. + +-spec encode_cert(public_key:der_encoded()) -> certificate(). +encode_cert(Der) -> + {'Certificate', Der, not_encrypted}. + +%% ------------------------------------------------------------------- + +shift_date(Date, Offset) -> + calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date) + Offset). + +format_date({Y, M, D}) -> + lists:flatten(io_lib:format("~w~2..0w~2..0w000000Z", [Y, M, D])). + +%% ------------------------------------------------------------------- + +%% @doc Write certificate + private key pair to respective files. +%% Files are created in the given directory. The filenames are derived +%% from the subject information in the certificate. +-spec write_cert(_Dir :: file:name(), {certificate(), private_key()}) -> + {file:name(), file:name()}. +write_cert(Dir, {Cert, Key}) -> + Subject = issuer_subject(Cert), + Filename = subject_info(org, Subject, "ORG") ++ "." ++ subject_info(name, Subject, "XXX"), + write_cert(Dir, Filename, {Cert, Key}). + +-spec write_cert(_Dir :: file:name(), _Prefix :: string(), {certificate(), private_key()}) -> + {file:name(), file:name()}. +write_cert(Dir, Filename, {Cert, Key}) -> + Certfile = filename:join(Dir, Filename ++ ".crt"), + Keyfile = filename:join(Dir, Filename ++ ".key"), + ok = write_pem(Certfile, Cert), + ok = write_pem(Keyfile, Key), + {Certfile, Keyfile}. + +-spec write_pem(file:name(), pem_entry() | [pem_entry()]) -> + ok | {error, file:posix()}. +write_pem(Name, Entries = [_ | _]) -> + file:write_file(Name, public_key:pem_encode(Entries)); +write_pem(Name, Entry) -> + write_pem(Name, [Entry]). From 3ea2bbefaa27f7d09db032e30055f26b3b972150 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Sat, 16 Dec 2023 01:13:54 +0100 Subject: [PATCH 3/9] test(mqttsn): generate TLS certificates for tests on the fly --- .../test/emqx_sn_protocol_SUITE.erl | 27 ++++++++++++------- .../test/emqx_sn_protocol_SUITE_data/ca.crt | 20 -------------- .../dtls.server.crt | 19 ------------- .../dtls.server.key | 27 ------------------- 4 files changed, 18 insertions(+), 75 deletions(-) delete mode 100644 apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt delete mode 100644 apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt delete mode 100644 apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl index 4f69fc271..6d0d7128a 100644 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl @@ -88,9 +88,9 @@ " listeners.dtls.default {\n" " bind = 1885\n" " dtls_options {\n" - " cacertfile = \"${certdir}ca.crt\"\n" - " certfile = \"${certdir}dtls.server.crt\"\n" - " keyfile = \"${certdir}dtls.server.key\"\n" + " cacertfile = \"${cacertfile}\"\n" + " certfile = \"${certfile}\"\n" + " keyfile = \"${keyfile}\"\n" " }\n" " }\n" "}\n" @@ -104,10 +104,19 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - ConfTemplate = emqx_template:parse(?CONF_DEFAULT), - Conf = emqx_template:render_strict(ConfTemplate, #{ - certdir => ?config(data_dir, Config) - }), + PrivDir = ?config(priv_dir, Config), + Root = emqx_cth_tls:gen_cert(#{key => ec, issuer => root}), + Server = emqx_cth_tls:gen_cert(#{key => ec, issuer => Root}), + {CACertfile, _} = emqx_cth_tls:write_cert(PrivDir, Root), + {Certfile, Keyfile} = emqx_cth_tls:write_cert(PrivDir, Server), + Conf = emqx_template:render_strict( + emqx_template:parse(?CONF_DEFAULT), + #{ + cacertfile => CACertfile, + certfile => Certfile, + keyfile => Keyfile + } + ), Apps = emqx_cth_suite:start( [ {emqx_conf, Conf}, @@ -119,7 +128,7 @@ init_per_suite(Config) -> #{work_dir => emqx_cth_suite:work_dir(Config)} ), emqx_common_test_http:create_default_app(), - [{suite_apps, Apps} | Config]. + [{suite_apps, Apps}, {cacertfile, CACertfile} | Config]. end_per_suite(Config) -> {ok, _} = emqx:remove_config([gateway, mqttsn]), @@ -210,7 +219,7 @@ t_connect_dtls(Config) -> binary, {active, false}, {protocol, dtls}, - {cacertfile, filename:join(?config(data_dir, Config), "ca.crt")} + {cacertfile, ?config(cacertfile, Config)} | emqx_common_test_helpers:ssl_verify_fun_allow_any_host() ], {ok, Socket} = ssl:connect(?HOST, 1885, ClientOpts, 1000), diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt deleted file mode 100644 index 604fd2362..000000000 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/ca.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDUTCCAjmgAwIBAgIJAPPYCjTmxdt/MA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV -BAYTAkNOMREwDwYDVQQIDAhoYW5nemhvdTEMMAoGA1UECgwDRU1RMQ8wDQYDVQQD -DAZSb290Q0EwHhcNMjAwNTA4MDgwNjUyWhcNMzAwNTA2MDgwNjUyWjA/MQswCQYD -VQQGEwJDTjERMA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UE -AwwGUm9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcgVLex1 -EZ9ON64EX8v+wcSjzOZpiEOsAOuSXOEN3wb8FKUxCdsGrsJYB7a5VM/Jot25Mod2 -juS3OBMg6r85k2TWjdxUoUs+HiUB/pP/ARaaW6VntpAEokpij/przWMPgJnBF3Ur -MjtbLayH9hGmpQrI5c2vmHQ2reRZnSFbY+2b8SXZ+3lZZgz9+BaQYWdQWfaUWEHZ -uDaNiViVO0OT8DRjCuiDp3yYDj3iLWbTA/gDL6Tf5XuHuEwcOQUrd+h0hyIphO8D -tsrsHZ14j4AWYLk1CPA6pq1HIUvEl2rANx2lVUNv+nt64K/Mr3RnVQd9s8bK+TXQ -KGHd2Lv/PALYuwIDAQABo1AwTjAdBgNVHQ4EFgQUGBmW+iDzxctWAWxmhgdlE8Pj -EbQwHwYDVR0jBBgwFoAUGBmW+iDzxctWAWxmhgdlE8PjEbQwDAYDVR0TBAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAQEAGbhRUjpIred4cFAFJ7bbYD9hKu/yzWPWkMRa -ErlCKHmuYsYk+5d16JQhJaFy6MGXfLgo3KV2itl0d+OWNH0U9ULXcglTxy6+njo5 -CFqdUBPwN1jxhzo9yteDMKF4+AHIxbvCAJa17qcwUKR5MKNvv09C6pvQDJLzid7y -E2dkgSuggik3oa0427KvctFf8uhOV94RvEDyqvT5+pgNYZ2Yfga9pD/jjpoHEUlo -88IGU8/wJCx3Ds2yc8+oBg/ynxG8f/HmCC1ET6EHHoe2jlo8FpU/SgGtghS1YL30 -IWxNsPrUP+XsZpBJy/mvOhE5QXo6Y35zDqqj8tI7AGmAWu22jg== ------END CERTIFICATE----- diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt deleted file mode 100644 index 092390b1d..000000000 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER -MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB -MB4XDTIwMDUwODA4MDcwNVoXDTMwMDUwNjA4MDcwNVowPzELMAkGA1UEBhMCQ04x -ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBlNlcnZl -cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALNeWT3pE+QFfiRJzKmn -AMUrWo3K2j/Tm3+Xnl6WLz67/0rcYrJbbKvS3uyRP/stXyXEKw9CepyQ1ViBVFkW -Aoy8qQEOWFDsZc/5UzhXUnb6LXr3qTkFEjNmhj+7uzv/lbBxlUG1NlYzSeOB6/RT -8zH/lhOeKhLnWYPXdXKsa1FL6ij4X8DeDO1kY7fvAGmBn/THh1uTpDizM4YmeI+7 -4dmayA5xXvARte5h4Vu5SIze7iC057N+vymToMk2Jgk+ZZFpyXrnq+yo6RaD3ANc -lrc4FbeUQZ5a5s5Sxgs9a0Y3WMG+7c5VnVXcbjBRz/aq2NtOnQQjikKKQA8GF080 -BQkCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL -BQADggEBAJefnMZpaRDHQSNUIEL3iwGXE9c6PmIsQVE2ustr+CakBp3TZ4l0enLt -iGMfEVFju69cO4oyokWv+hl5eCMkHBf14Kv51vj448jowYnF1zmzn7SEzm5Uzlsa -sqjtAprnLyof69WtLU1j5rYWBuFX86yOTwRAFNjm9fvhAcrEONBsQtqipBWkMROp -iUYMkRqbKcQMdwxov+lHBYKq9zbWRoqLROAn54SRqgQk6c15JdEfgOOjShbsOkIH -UhqcwRkQic7n1zwHVGVDgNIZVgmJ2IdIWBlPEC7oLrRrBD/X1iEEXtKab6p5o22n -KB5mN+iQaE+Oe2cpGKZJiJRdM+IqDDQ= ------END CERTIFICATE----- diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key deleted file mode 100644 index 6c338216e..000000000 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE_data/dtls.server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAs15ZPekT5AV+JEnMqacAxStajcraP9Obf5eeXpYvPrv/Stxi -sltsq9Le7JE/+y1fJcQrD0J6nJDVWIFUWRYCjLypAQ5YUOxlz/lTOFdSdvotevep -OQUSM2aGP7u7O/+VsHGVQbU2VjNJ44Hr9FPzMf+WE54qEudZg9d1cqxrUUvqKPhf -wN4M7WRjt+8AaYGf9MeHW5OkOLMzhiZ4j7vh2ZrIDnFe8BG17mHhW7lIjN7uILTn -s36/KZOgyTYmCT5lkWnJeuer7KjpFoPcA1yWtzgVt5RBnlrmzlLGCz1rRjdYwb7t -zlWdVdxuMFHP9qrY206dBCOKQopADwYXTzQFCQIDAQABAoIBAQCuvCbr7Pd3lvI/ -n7VFQG+7pHRe1VKwAxDkx2t8cYos7y/QWcm8Ptwqtw58HzPZGWYrgGMCRpzzkRSF -V9g3wP1S5Scu5C6dBu5YIGc157tqNGXB+SpdZddJQ4Nc6yGHXYERllT04ffBGc3N -WG/oYS/1cSteiSIrsDy/91FvGRCi7FPxH3wIgHssY/tw69s1Cfvaq5lr2NTFzxIG -xCvpJKEdSfVfS9I7LYiymVjst3IOR/w76/ZFY9cRa8ZtmQSWWsm0TUpRC1jdcbkm -ZoJptYWlP+gSwx/fpMYftrkJFGOJhHJHQhwxT5X/ajAISeqjjwkWSEJLwnHQd11C -Zy2+29lBAoGBANlEAIK4VxCqyPXNKfoOOi5dS64NfvyH4A1v2+KaHWc7lqaqPN49 -ezfN2n3X+KWx4cviDD914Yc2JQ1vVJjSaHci7yivocDo2OfZDmjBqzaMp/y+rX1R -/f3MmiTqMa468rjaxI9RRZu7vDgpTR+za1+OBCgMzjvAng8dJuN/5gjlAoGBANNY -uYPKtearBmkqdrSV7eTUe49Nhr0XotLaVBH37TCW0Xv9wjO2xmbm5Ga/DCtPIsBb -yPeYwX9FjoasuadUD7hRvbFu6dBa0HGLmkXRJZTcD7MEX2Lhu4BuC72yDLLFd0r+ -Ep9WP7F5iJyagYqIZtz+4uf7gBvUDdmvXz3sGr1VAoGAdXTD6eeKeiI6PlhKBztF -zOb3EQOO0SsLv3fnodu7ZaHbUgLaoTMPuB17r2jgrYM7FKQCBxTNdfGZmmfDjlLB -0xZ5wL8ibU30ZXL8zTlWPElST9sto4B+FYVVF/vcG9sWeUUb2ncPcJ/Po3UAktDG -jYQTTyuNGtSJHpad/YOZctkCgYBtWRaC7bq3of0rJGFOhdQT9SwItN/lrfj8hyHA -OjpqTV4NfPmhsAtu6j96OZaeQc+FHvgXwt06cE6Rt4RG4uNPRluTFgO7XYFDfitP -vCppnoIw6S5BBvHwPP+uIhUX2bsi/dm8vu8tb+gSvo4PkwtFhEr6I9HglBKmcmog -q6waEQKBgHyecFBeM6Ls11Cd64vborwJPAuxIW7HBAFj/BS99oeG4TjBx4Sz2dFd -rzUibJt4ndnHIvCN8JQkjNG14i9hJln+H3mRss8fbZ9vQdqG+2vOWADYSzzsNI55 -RFY7JjluKcVkp/zCDeUxTU3O6sS+v6/3VE11Cob6OYQx3lN5wrZ3 ------END RSA PRIVATE KEY----- From 1ef700e2cb9679f879e633e0e195976f4be3bc53 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Sat, 16 Dec 2023 01:21:27 +0100 Subject: [PATCH 4/9] chore: add changelog entry --- changes/ce/fix-12180.en.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/ce/fix-12180.en.md diff --git a/changes/ce/fix-12180.en.md b/changes/ce/fix-12180.en.md new file mode 100644 index 000000000..3ded8a507 --- /dev/null +++ b/changes/ce/fix-12180.en.md @@ -0,0 +1 @@ +Fix an issue where DTLS enabled MQTT-SN gateways could not be started, caused by incompatibility of default listener configuration with the DTLS implementation. From 5c5ecbe3cf377c99fd54e3da845392b655dc3988 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Sat, 16 Dec 2023 23:09:12 +0100 Subject: [PATCH 5/9] fix(gw): unbreak schema + transform DTLS options properly Thus ensuring full backward compatibility. Unsupported options (`gc_after_handshake`, `ocsp`) are silently ignored now. Also make sure that UDP configuration are part of DTLS option set, as expected by `esockd`. --- apps/emqx_gateway/src/emqx_gateway_schema.erl | 6 +-- apps/emqx_gateway/src/emqx_gateway_utils.erl | 48 +++++++++++++++---- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index ed54383f5..d51bf93a9 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -174,11 +174,7 @@ fields(dtls_opts) -> reuse_sessions => true, versions => dtls_all_available }, - %% NOTE - %% Although dTLS listener is started through `esockd`, it doesn't really support stuff - %% more tightly related to `emqx_listeners` (`enable_crl_check`, `oscp`) and plain TLS - %% (`gc_after_handshake`). - _IsRanchListener = true + _IsRanchListener = false ). desc(gateway) -> diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index c8cf979e3..7d6b4c172 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -273,7 +273,7 @@ merge_default(Udp, Options) -> udp -> {udp_options, default_udp_options()}; dtls -> - {udp_options, default_udp_options()}; + {dtls_options, default_udp_options()}; tcp -> {tcp_options, default_tcp_options()}; ssl -> @@ -525,9 +525,10 @@ esockd_opts(Type, Opts0) when ?IS_ESOCKD_LISTENER(Type) -> udp -> Opts2#{udp_options => sock_opts(udp_options, Opts0)}; dtls -> + UDPOpts = sock_opts(udp_options, Opts0), + DTLSOpts = ssl_opts(dtls_options, Opts0), Opts2#{ - udp_options => sock_opts(udp_options, Opts0), - dtls_options => ssl_opts(dtls_options, Opts0) + dtls_options => UDPOpts ++ DTLSOpts } end ). @@ -541,12 +542,41 @@ sock_opts(Name, Opts) -> ). ssl_opts(Name, Opts) -> - Type = - case Name of - ssl_options -> tls; - dtls_options -> dtls - end, - emqx_tls_lib:to_server_opts(Type, maps:get(Name, Opts, #{})). + SSLOpts = maps:get(Name, Opts, #{}), + emqx_utils:run_fold( + [ + fun ssl_opts_crl_config/2, + fun ssl_opts_drop_unsupported/2, + fun ssl_server_opts/2 + ], + SSLOpts, + Name + ). + +ssl_opts_crl_config(#{enable_crl_check := true} = SSLOpts, _Name) -> + HTTPTimeout = emqx_config:get([crl_cache, http_timeout], timer:seconds(15)), + NSSLOpts = maps:remove(enable_crl_check, SSLOpts), + NSSLOpts#{ + %% `crl_check => true' doesn't work + crl_check => peer, + crl_cache => {emqx_ssl_crl_cache, {internal, [{http, HTTPTimeout}]}} + }; +ssl_opts_crl_config(SSLOpts, _Name) -> + %% NOTE: Removing this because DTLS doesn't like any unknown options. + maps:remove(enable_crl_check, SSLOpts). + +ssl_opts_drop_unsupported(SSLOpts, ssl_options) -> + %% TODO: Support OCSP stapling + maps:without([ocsp], SSLOpts); +ssl_opts_drop_unsupported(SSLOpts, dtls_options) -> + %% TODO: Support OCSP stapling + %% NOTE: Removing those because DTLS doesn't like any unknown options. + maps:without([ocsp, gc_after_handshake], SSLOpts). + +ssl_server_opts(SSLOpts, ssl_options) -> + emqx_tls_lib:to_server_opts(tls, SSLOpts); +ssl_server_opts(SSLOpts, dtls_options) -> + emqx_tls_lib:to_server_opts(dtls, SSLOpts). ranch_opts(Type, ListenOn, Opts) -> NumAcceptors = maps:get(acceptors, Opts, 4), From 62bb9938e79a0fa65c76cacee6138c8283a6f146 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 20 Dec 2023 12:08:30 +0100 Subject: [PATCH 6/9] test(exproto): switch to `emqx_cth_suite` + fix listener options --- .../test/emqx_exproto_SUITE.erl | 74 +++++++++---------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl index 74a488abb..2d2583beb 100644 --- a/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway_exproto/test/emqx_exproto_SUITE.erl @@ -20,7 +20,6 @@ -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx_hooks.hrl"). -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -44,14 +43,6 @@ -define(TCPOPTS, [binary, {active, false}]). -define(DTLSOPTS, [binary, {active, false}, {protocol, dtls}]). --define(PORT, 7993). - --define(DEFAULT_CLIENT, #{ - proto_name => <<"demo">>, - proto_ver => <<"v0.1">>, - clientid => <<"test_client_1">> -}). - %%-------------------------------------------------------------------- -define(CONF_DEFAULT, << "\n" @@ -126,15 +117,33 @@ init_per_group(_, Cfg) -> init_per_group(LisType, ServiceName, Scheme, Cfg) -> Svrs = emqx_exproto_echo_svr:start(Scheme), - application:load(emqx_gateway_exproto), - emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_auth, emqx_gateway], - fun(App) -> - set_special_cfg(App, LisType, ServiceName, Scheme) - end + Addrs = lists:flatten(io_lib:format("~s://127.0.0.1:9001", [Scheme])), + GWConfig = #{ + server => #{bind => 9100}, + idle_timeout => 5000, + mountpoint => <<"ct/">>, + handler => #{ + address => Addrs, + service_name => ServiceName, + ssl_options => #{enable => Scheme == https} + }, + listeners => listener_confs(LisType) + }, + Apps = emqx_cth_suite:start( + [ + emqx_conf, + emqx_auth, + {emqx_gateway, #{ + config => + #{gateway => #{exproto => GWConfig}} + }}, + emqx_gateway_exproto + ], + #{work_dir => emqx_cth_suite:work_dir(Cfg)} ), [ {servers, Svrs}, + {apps, Apps}, {listener_type, LisType}, {service_name, ServiceName}, {grpc_client_scheme, Scheme} @@ -142,8 +151,7 @@ init_per_group(LisType, ServiceName, Scheme, Cfg) -> ]. end_per_group(_, Cfg) -> - emqx_config:erase(gateway), - emqx_common_test_helpers:stop_apps([emqx_gateway, emqx_auth, emqx_conf]), + ok = emqx_cth_suite:stop(proplists:get_value(apps, Cfg)), emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)). init_per_testcase(TestCase, Cfg) when @@ -159,27 +167,12 @@ init_per_testcase(_TestCase, Cfg) -> end_per_testcase(_TestCase, _Cfg) -> ok. -set_special_cfg(emqx_gateway, LisType, ServiceName, Scheme) -> - Addrs = lists:flatten(io_lib:format("~s://127.0.0.1:9001", [Scheme])), - emqx_config:put( - [gateway, exproto], - #{ - server => #{bind => 9100}, - idle_timeout => 5000, - mountpoint => <<"ct/">>, - handler => #{ - address => Addrs, - service_name => ServiceName, - ssl_options => #{enable => Scheme == https} - }, - listeners => listener_confs(LisType) - } - ); -set_special_cfg(_, _, _, _) -> - ok. - listener_confs(Type) -> - Default = #{bind => 7993, acceptors => 8}, + Default = #{ + bind => 7993, + max_connections => 64, + access_rules => ["allow all"] + }, #{Type => #{'default' => maps:merge(Default, socketopts(Type))}}. default_config() -> @@ -636,9 +629,13 @@ close({dtls, Sock}) -> %% Server-Opts socketopts(tcp) -> - #{tcp_options => tcp_opts()}; + #{ + acceptors => 8, + tcp_options => tcp_opts() + }; socketopts(ssl) -> #{ + acceptors => 8, tcp_options => tcp_opts(), ssl_options => ssl_opts() }; @@ -646,6 +643,7 @@ socketopts(udp) -> #{udp_options => udp_opts()}; socketopts(dtls) -> #{ + acceptors => 8, udp_options => udp_opts(), dtls_options => dtls_opts() }. From 32a64cf20125118376d56315ae2ef1b0eb2387ee Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 20 Dec 2023 12:09:31 +0100 Subject: [PATCH 7/9] fix(gw): update DTLS listener setup according to esockd 5.11.x Also drop unnecessary UDP default options. --- apps/emqx_gateway/src/emqx_gateway_utils.erl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 7d6b4c172..47e1f7583 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -528,7 +528,8 @@ esockd_opts(Type, Opts0) when ?IS_ESOCKD_LISTENER(Type) -> UDPOpts = sock_opts(udp_options, Opts0), DTLSOpts = ssl_opts(dtls_options, Opts0), Opts2#{ - dtls_options => UDPOpts ++ DTLSOpts + udp_options => UDPOpts, + dtls_options => DTLSOpts } end ). @@ -565,13 +566,9 @@ ssl_opts_crl_config(SSLOpts, _Name) -> %% NOTE: Removing this because DTLS doesn't like any unknown options. maps:remove(enable_crl_check, SSLOpts). -ssl_opts_drop_unsupported(SSLOpts, ssl_options) -> +ssl_opts_drop_unsupported(SSLOpts, _Name) -> %% TODO: Support OCSP stapling - maps:without([ocsp], SSLOpts); -ssl_opts_drop_unsupported(SSLOpts, dtls_options) -> - %% TODO: Support OCSP stapling - %% NOTE: Removing those because DTLS doesn't like any unknown options. - maps:without([ocsp, gc_after_handshake], SSLOpts). + maps:without([ocsp], SSLOpts). ssl_server_opts(SSLOpts, ssl_options) -> emqx_tls_lib:to_server_opts(tls, SSLOpts); @@ -665,7 +662,7 @@ default_tcp_options() -> ]. default_udp_options() -> - [binary]. + []. default_subopts() -> %% Retain Handling From 76c89ad37288c4d09cd2030ace5361539bba926d Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 20 Dec 2023 13:56:18 +0100 Subject: [PATCH 8/9] test(gw-authz): switch to `emqx_cth_suite` + simplify testsuite Also separate DTLS related MQTT-SN test config, so that it doesn't leak into this test suite. --- apps/emqx/test/emqx_cth_suite.erl | 1 - .../test/emqx_gateway_auth_ct.erl | 5 +- .../test/emqx_gateway_authz_SUITE.erl | 53 ++++++++----------- .../test/emqx_gateway_test_utils.erl | 16 ++++-- .../test/emqx_sn_protocol_SUITE.erl | 8 ++- 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/apps/emqx/test/emqx_cth_suite.erl b/apps/emqx/test/emqx_cth_suite.erl index 5e91b92c9..042ef91db 100644 --- a/apps/emqx/test/emqx_cth_suite.erl +++ b/apps/emqx/test/emqx_cth_suite.erl @@ -58,7 +58,6 @@ -module(emqx_cth_suite). -include_lib("common_test/include/ct.hrl"). --include_lib("emqx/include/emqx_access_control.hrl"). -export([start/2]). -export([stop/1]). diff --git a/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl b/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl index 215302105..81ab2f368 100644 --- a/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl +++ b/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl @@ -238,9 +238,12 @@ http_authz_config() -> init_gateway_conf() -> ok = emqx_common_test_helpers:load_config( emqx_gateway_schema, - merge_conf([X:default_config() || X <- ?CONFS], []) + merge_conf(list_gateway_conf(), []) ). +list_gateway_conf() -> + [X:default_config() || X <- ?CONFS]. + merge_conf([Conf | T], Acc) -> case re:run(Conf, "\s*gateway\\.(.*)", [global, {capture, all_but_first, list}, dotall]) of {match, [[Content]]} -> diff --git a/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl index dd149133b..9ae464ebb 100644 --- a/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --import(emqx_gateway_auth_ct, [init_gateway_conf/0, with_resource/3]). +-import(emqx_gateway_auth_ct, [with_resource/3]). -define(checkMatch(Guard), (fun(Expr) -> @@ -54,44 +54,33 @@ groups() -> emqx_gateway_auth_ct:init_groups(?MODULE, ?AUTHNS). init_per_group(AuthName, Conf) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - ok = emqx_authz_test_lib:reset_authorizers(), - emqx_gateway_auth_ct:start_auth(AuthName), - timer:sleep(500), - Conf. + Apps = emqx_cth_suite:start( + [ + {emqx_conf, "authorization { no_match = deny, cache { enable = false } }"}, + emqx_auth, + emqx_auth_http, + {emqx_gateway, emqx_gateway_auth_ct:list_gateway_conf()} + | emqx_gateway_test_utils:all_gateway_apps() + ], + #{work_dir => emqx_cth_suite:work_dir(Conf)} + ), + ok = emqx_gateway_auth_ct:start_auth(AuthName), + [{group_apps, Apps} | Conf]. end_per_group(AuthName, Conf) -> - emqx_gateway_auth_ct:stop_auth(AuthName), + ok = emqx_gateway_auth_ct:stop_auth(AuthName), + ok = emqx_cth_suite:stop(?config(group_apps, Conf)), Conf. init_per_suite(Config) -> - emqx_config:erase(gateway), - emqx_gateway_test_utils:load_all_gateway_apps(), - init_gateway_conf(), - emqx_mgmt_api_test_util:init_suite([ - grpc, emqx_conf, emqx_auth, emqx_auth_http, emqx_gateway - ]), - meck:new(emqx_authz_file, [non_strict, passthrough, no_history, no_link]), - meck:expect(emqx_authz_file, create, fun(S) -> S end), - application:ensure_all_started(cowboy), - emqx_gateway_auth_ct:start(), - Config. + {ok, Apps1} = application:ensure_all_started(grpc), + {ok, Apps2} = application:ensure_all_started(cowboy), + {ok, _} = emqx_gateway_auth_ct:start(), + [{suite_apps, Apps1 ++ Apps2} | Config]. end_per_suite(Config) -> - meck:unload(emqx_authz_file), - emqx_gateway_auth_ct:stop(), - ok = emqx_authz_test_lib:restore_authorizers(), - emqx_config:erase(gateway), - emqx_mgmt_api_test_util:end_suite([ - emqx_gateway, emqx_auth_http, emqx_auth, emqx_conf, grpc - ]), - Config. - -init_per_testcase(_Case, Config) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - Config. - -end_per_testcase(_Case, Config) -> + ok = emqx_gateway_auth_ct:stop(), + ok = emqx_cth_suite:stop_apps(?config(suite_apps, Config)), Config. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_gateway/test/emqx_gateway_test_utils.erl b/apps/emqx_gateway/test/emqx_gateway_test_utils.erl index 950ae1bcf..2e8be5119 100644 --- a/apps/emqx_gateway/test/emqx_gateway_test_utils.erl +++ b/apps/emqx_gateway/test/emqx_gateway_test_utils.erl @@ -103,12 +103,18 @@ assert_fields_exist(Ks, Map) -> end, Ks ). + load_all_gateway_apps() -> - application:load(emqx_gateway_stomp), - application:load(emqx_gateway_mqttsn), - application:load(emqx_gateway_coap), - application:load(emqx_gateway_lwm2m), - application:load(emqx_gateway_exproto). + emqx_cth_suite:load_apps(all_gateway_apps()). + +all_gateway_apps() -> + [ + emqx_gateway_stomp, + emqx_gateway_mqttsn, + emqx_gateway_coap, + emqx_gateway_lwm2m, + emqx_gateway_exproto + ]. %%-------------------------------------------------------------------- %% http diff --git a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl index 6d0d7128a..25d8bcc51 100644 --- a/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway_mqttsn/test/emqx_sn_protocol_SUITE.erl @@ -85,6 +85,12 @@ " listeners.udp.default {\n" " bind = 1884\n" " }\n" + "}\n" +>>). + +-define(CONF_DTLS, << + "\n" + "gateway.mqttsn {" " listeners.dtls.default {\n" " bind = 1885\n" " dtls_options {\n" @@ -110,7 +116,7 @@ init_per_suite(Config) -> {CACertfile, _} = emqx_cth_tls:write_cert(PrivDir, Root), {Certfile, Keyfile} = emqx_cth_tls:write_cert(PrivDir, Server), Conf = emqx_template:render_strict( - emqx_template:parse(?CONF_DEFAULT), + emqx_template:parse([?CONF_DEFAULT, ?CONF_DTLS]), #{ cacertfile => CACertfile, certfile => Certfile, From ccb9a977d6dee83316d0f60d7e30e69f4bd0c4da Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 20 Dec 2023 14:38:18 +0100 Subject: [PATCH 9/9] test(gw-authn): switch to `emqx_cth_suite` + simplify testsuite --- .../test/emqx_gateway_authn_SUITE.erl | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl index 0072447b6..7495c5858 100644 --- a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --import(emqx_gateway_auth_ct, [init_gateway_conf/0, with_resource/3]). +-import(emqx_gateway_auth_ct, [with_resource/3]). -define(checkMatch(Guard), (fun(Expr) -> @@ -54,40 +54,37 @@ groups() -> emqx_gateway_auth_ct:init_groups(?MODULE, ?AUTHNS). init_per_group(AuthName, Conf) -> - ct:pal("on group start:~p~n", [AuthName]), - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - emqx_gateway_auth_ct:start_auth(AuthName), - timer:sleep(500), - Conf. + Apps = emqx_cth_suite:start( + [ + emqx_conf, + emqx_auth, + emqx_auth_http, + emqx_management, + {emqx_dashboard, "dashboard.listeners.http { enable = true, bind = 18083 }"}, + {emqx_gateway, emqx_gateway_auth_ct:list_gateway_conf()} + | emqx_gateway_test_utils:all_gateway_apps() + ], + #{work_dir => emqx_cth_suite:work_dir(Conf)} + ), + _ = emqx_common_test_http:create_default_app(), + ok = emqx_gateway_auth_ct:start_auth(AuthName), + [{group_apps, Apps} | Conf]. end_per_group(AuthName, Conf) -> - ct:pal("on group stop:~p~n", [AuthName]), - emqx_gateway_auth_ct:stop_auth(AuthName), + ok = emqx_gateway_auth_ct:stop_auth(AuthName), + _ = emqx_common_test_http:delete_default_app(), + ok = emqx_cth_suite:stop(?config(group_apps, Conf)), Conf. init_per_suite(Config) -> - emqx_gateway_test_utils:load_all_gateway_apps(), - emqx_config:erase(gateway), - init_gateway_conf(), - emqx_mgmt_api_test_util:init_suite([grpc, emqx_conf, emqx_auth, emqx_auth_http, emqx_gateway]), - application:ensure_all_started(cowboy), - emqx_gateway_auth_ct:start(), - timer:sleep(500), - Config. + {ok, Apps1} = application:ensure_all_started(grpc), + {ok, Apps2} = application:ensure_all_started(cowboy), + {ok, _} = emqx_gateway_auth_ct:start(), + [{suite_apps, Apps1 ++ Apps2} | Config]. end_per_suite(Config) -> - emqx_gateway_auth_ct:stop(), - emqx_config:erase(gateway), - emqx_mgmt_api_test_util:end_suite([ - cowboy, emqx_conf, emqx_auth, emqx_auth_http, emqx_gateway, grpc - ]), - Config. - -init_per_testcase(_Case, Config) -> - {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), - Config. - -end_per_testcase(_Case, Config) -> + ok = emqx_gateway_auth_ct:stop(), + ok = emqx_cth_suite:stop_apps(?config(suite_apps, Config)), Config. %%------------------------------------------------------------------------------