From 683e58d9603bd9a25a19743e159a3f02046ca47b Mon Sep 17 00:00:00 2001 From: Dean Gereaux Date: Wed, 6 Jan 2021 12:51:34 -0800 Subject: [PATCH] auth: new authentication library Origin: Original New authentication library for authenticating devices, key features: - Two auth methods, DTLS and Challenge-Response. - Transport independent, initial support for serial and Bluetooth. - Multiple authentication instances. - Refer to ZAUTH RFC for more details: https://github.com/zephyrproject-rtos/zephyr/issues/23465 Signed-off-by: Dean Gereaux --- doc/reference/auth/auth_api_ref.rst | 200 ++++ doc/reference/auth/high_level_design.png | Bin 0 -> 30182 bytes doc/reference/auth/tx_rx_queues.png | Bin 0 -> 112807 bytes doc/reference/auth/xport_interface.png | Bin 0 -> 86267 bytes doc/reference/auth/xport_layer.png | Bin 0 -> 57608 bytes doc/reference/index.rst | 2 + include/auth/auth_lib.h | 335 ++++++ include/auth/auth_xport.h | 426 +++++++ lib/CMakeLists.txt | 1 + lib/Kconfig | 2 + lib/auth/CMakeLists.txt | 24 + lib/auth/Kconfig | 77 ++ lib/auth/auth_chalresp.c | 637 ++++++++++ lib/auth/auth_dtls.c | 959 +++++++++++++++ lib/auth/auth_internal.h | 295 +++++ lib/auth/auth_lib.c | 342 ++++++ lib/auth/auth_xport_bt.c | 537 +++++++++ lib/auth/auth_xport_common.c | 1039 +++++++++++++++++ lib/auth/auth_xport_serial.c | 602 ++++++++++ samples/authentication/auth_samples.rst | 15 + samples/authentication/bluetooth/README.rst | 28 + .../bluetooth/central_auth/CMakeLists.txt | 11 + .../bluetooth/central_auth/dtls.prj.conf | 81 ++ .../bluetooth/central_auth/prj.conf | 38 + .../bluetooth/central_auth/sample.yaml | 8 + .../bluetooth/central_auth/src/main.c | 584 +++++++++ .../bluetooth/peripheral_auth/CMakeLists.txt | 12 + .../bluetooth/peripheral_auth/dtls.prj.conf | 76 ++ .../bluetooth/peripheral_auth/prj.conf | 38 + .../bluetooth/peripheral_auth/sample.yaml | 8 + .../bluetooth/peripheral_auth/src/main.c | 380 ++++++ samples/authentication/certs/README.rst | 14 + samples/authentication/certs/auth_certs.h | 100 ++ .../authentication/certs/auth_dev_client.crt | 48 + .../authentication/certs/auth_dev_server.crt | 48 + .../certs/auth_intermediate.crt | 51 + samples/authentication/certs/auth_rootca.crt | 16 + .../certs/keys/auth_dev_client.key | 8 + .../certs/keys/auth_dev_server.key | 8 + .../authentication/certs/keys/auth_inter.key | 8 + .../authentication/certs/keys/auth_rootca.key | 8 + .../authentication/multi_instance/README.rst | 37 + .../multi_instance/auth_client/CMakeLists.txt | 12 + .../multi_instance/auth_client/prj.conf | 95 ++ .../multi_instance/auth_client/sample.yaml | 8 + .../multi_instance/auth_client/src/main.c | 645 ++++++++++ .../multi_instance/auth_server/CMakeLists.txt | 16 + .../multi_instance/auth_server/prj.conf | 96 ++ .../multi_instance/auth_server/sample.yaml | 8 + .../multi_instance/auth_server/src/main.c | 430 +++++++ .../multi_instance/heart_monitor_device.png | Bin 0 -> 23474 bytes samples/authentication/serial/README.rst | 37 + .../serial/auth_client/CMakeLists.txt | 14 + .../serial/auth_client/dtls.prj.conf | 64 + .../serial/auth_client/prj.conf | 26 + .../serial/auth_client/sample.yaml | 8 + .../serial/auth_client/src/main.c | 239 ++++ .../serial/auth_server/CMakeLists.txt | 16 + .../serial/auth_server/dtls.prj.conf | 65 ++ .../serial/auth_server/prj.conf | 26 + .../serial/auth_server/sample.yaml | 8 + .../serial/auth_server/src/main.c | 233 ++++ samples/index.rst | 1 + tests/lib/authentication/CMakeLists.txt | 8 + tests/lib/authentication/prj.conf | 13 + tests/lib/authentication/src/main.c | 67 ++ tests/lib/authentication/testcase.yaml | 4 + 67 files changed, 9242 insertions(+) create mode 100644 doc/reference/auth/auth_api_ref.rst create mode 100644 doc/reference/auth/high_level_design.png create mode 100644 doc/reference/auth/tx_rx_queues.png create mode 100644 doc/reference/auth/xport_interface.png create mode 100644 doc/reference/auth/xport_layer.png create mode 100644 include/auth/auth_lib.h create mode 100644 include/auth/auth_xport.h create mode 100644 lib/auth/CMakeLists.txt create mode 100644 lib/auth/Kconfig create mode 100644 lib/auth/auth_chalresp.c create mode 100644 lib/auth/auth_dtls.c create mode 100644 lib/auth/auth_internal.h create mode 100644 lib/auth/auth_lib.c create mode 100644 lib/auth/auth_xport_bt.c create mode 100644 lib/auth/auth_xport_common.c create mode 100644 lib/auth/auth_xport_serial.c create mode 100644 samples/authentication/auth_samples.rst create mode 100644 samples/authentication/bluetooth/README.rst create mode 100644 samples/authentication/bluetooth/central_auth/CMakeLists.txt create mode 100644 samples/authentication/bluetooth/central_auth/dtls.prj.conf create mode 100644 samples/authentication/bluetooth/central_auth/prj.conf create mode 100644 samples/authentication/bluetooth/central_auth/sample.yaml create mode 100644 samples/authentication/bluetooth/central_auth/src/main.c create mode 100644 samples/authentication/bluetooth/peripheral_auth/CMakeLists.txt create mode 100644 samples/authentication/bluetooth/peripheral_auth/dtls.prj.conf create mode 100644 samples/authentication/bluetooth/peripheral_auth/prj.conf create mode 100644 samples/authentication/bluetooth/peripheral_auth/sample.yaml create mode 100644 samples/authentication/bluetooth/peripheral_auth/src/main.c create mode 100644 samples/authentication/certs/README.rst create mode 100644 samples/authentication/certs/auth_certs.h create mode 100644 samples/authentication/certs/auth_dev_client.crt create mode 100644 samples/authentication/certs/auth_dev_server.crt create mode 100644 samples/authentication/certs/auth_intermediate.crt create mode 100644 samples/authentication/certs/auth_rootca.crt create mode 100644 samples/authentication/certs/keys/auth_dev_client.key create mode 100644 samples/authentication/certs/keys/auth_dev_server.key create mode 100644 samples/authentication/certs/keys/auth_inter.key create mode 100644 samples/authentication/certs/keys/auth_rootca.key create mode 100644 samples/authentication/multi_instance/README.rst create mode 100644 samples/authentication/multi_instance/auth_client/CMakeLists.txt create mode 100644 samples/authentication/multi_instance/auth_client/prj.conf create mode 100644 samples/authentication/multi_instance/auth_client/sample.yaml create mode 100644 samples/authentication/multi_instance/auth_client/src/main.c create mode 100644 samples/authentication/multi_instance/auth_server/CMakeLists.txt create mode 100644 samples/authentication/multi_instance/auth_server/prj.conf create mode 100644 samples/authentication/multi_instance/auth_server/sample.yaml create mode 100644 samples/authentication/multi_instance/auth_server/src/main.c create mode 100644 samples/authentication/multi_instance/heart_monitor_device.png create mode 100644 samples/authentication/serial/README.rst create mode 100644 samples/authentication/serial/auth_client/CMakeLists.txt create mode 100644 samples/authentication/serial/auth_client/dtls.prj.conf create mode 100644 samples/authentication/serial/auth_client/prj.conf create mode 100644 samples/authentication/serial/auth_client/sample.yaml create mode 100644 samples/authentication/serial/auth_client/src/main.c create mode 100644 samples/authentication/serial/auth_server/CMakeLists.txt create mode 100644 samples/authentication/serial/auth_server/dtls.prj.conf create mode 100644 samples/authentication/serial/auth_server/prj.conf create mode 100644 samples/authentication/serial/auth_server/sample.yaml create mode 100644 samples/authentication/serial/auth_server/src/main.c create mode 100644 tests/lib/authentication/CMakeLists.txt create mode 100644 tests/lib/authentication/prj.conf create mode 100644 tests/lib/authentication/src/main.c create mode 100644 tests/lib/authentication/testcase.yaml diff --git a/doc/reference/auth/auth_api_ref.rst b/doc/reference/auth/auth_api_ref.rst new file mode 100644 index 000000000000..6c6126c67398 --- /dev/null +++ b/doc/reference/auth/auth_api_ref.rst @@ -0,0 +1,200 @@ +Authentication +############################################## + +The Zephyr Authentication Library is a library that provides +authentication services between two devices independent of the lower +transport layer. The library enables firmware applications to authenticate +with a Bluetooth or serial connected device using a simple +Challenge-Response or DTLS authentication method. Authentication +means proving the peer device’s identity. + +Use the authentication samples as a template for adding authentication into your +firmware application. + + + +Kconfig Options +----------------------------- + +:option:`CONFIG_AUTH_LIB`: This option enables the Authentication library. + +:option:`CONFIG_AUTH_CHALLENGE_RESPONSE`: Selects the Challenge Response authentication method + +:option:`CONFIG_AUTH_DTLS`: Selects the DTLS authentication method. + +:option:`CONFIG_AUTH_LOG_LEVEL`: Authentication log level, 0-4 + +:option:`CONFIG_BT_XPORT`: Use Bluetooth as lower transport. + +:option:`CONFIG_BT_XPORT`: Use Bluetooth as lower transport. + +:option:`CONFIG_BT_ALT_AUTH_BT_UUIDS`: Use alternate Bluetooth Auth service UUIDs. + +:option:`CONFIG_NUM_AUTH_INSTANCES`: Each authentication instance uses a thread to authenticate with +a peer over the lower transport. It is possible to have multiple +authentication instances where one instance authenticates a peer +over Bluetooth and another authenticates over a serial link. + +:option:`CONFIG_AUTH_THREAD_PRIORITY`: Authentication thread priority. + +Examples of API usage: +----------------------------- + +The Authentication API is designed to abstract away the authentication method and +transport. The calling application configures the ZAUTH library, starts the authentication +process and monitors results via a status callback. The API is also designed to handle +multiple concurrent authentication processes, for example If device is acting as a +Bluetooth Central and Peripheral. An example of the API used is shown in the following +code snippet. + +.. code-block:: none + + void auth_status(struct authenticate_conn *auth_conn, enum auth_instance_id instance, auth_status_t status, void *context) + { + if(status == AUTH_STATUS_SUCCESSFUL) { + printk(“Authentication Successful.\n”); + } else { + printk(“Authentication status: %s\n”, auth_lib_getstatus_str(status)); + } + } + + /* BLE connection callback */ + void connected(struct bt_conn *conn, uint8_t err) + { + /* start authentication */ + auth_lib_start(¢ral_auth_conn); + } + + void main(void) + { + int err = auth_lib_init(¢ral_auth_conn, AUTH_INST_1, auth_status, NULL, + opt_parms, flags); + + err = bt_enabled(NULL); + + while(true) { + k_yield(); + } + } + +Client Server Model +---------------------- +ZAUTH is designed as a client server model for the authentication message flow. The client initiates the +authentication messaging sequence where the server responds. Depending on the authentication method chosen +(Challenge-Response, DTLS, other), mutual authentication can be used to authenticate both sides of the +connection. For some transports, this model maps nicely to the native transport model. Bluetooth +is an example of this, a peripheral is in the server role and the central is in the client role. For Serial +transports, the choice of which endpoint acts as the client or server is up to the application firmware. + +Authentication Instances +------------------------- +Multiple authentication instances are possible concurrently authenticating connections over different +communication links. For example, a Bluetooth central device could use different instances to authenticate +different peripherals. Another example could be a HVAC controller with Bluetooth to communicate with mobile +devices and a serial interface to control HVAC equipment. One instance would authenticate the mobile device, +the second instance would authenticate the HVAC equipment. + + +Under the hood, an authentication Instance is a Zephyr thread and authentication method. + +Authentication Methods +------------------------- +Two authentication methods are supported, DTLS and simple Challenge-Response. However, the authentication +architecture can support additional authentication methods in the future. + +* DTLS. The TLS protocol is the gold standard of authentication and securing network communications. DTLS + is part of the TLS protocol, but designed for IP datagrams which are lighter weight and ideal for resource + constrained devices. Identities are verified using X.509 certificates and trusted root certificates. The + DTLS handshake steps are used for authentication, a successful handshake means each side of the connection + has been properly authenticated. A result of the DTLS handshake steps is a shared secret key which can be + used to encrypted further communications, this is up to the firmware application to implement. For the ZAUTH + this key is not used. + + +* Challenge-Response. A simple Challenge-Response authentication method is an alternative lighter weight + approach to authentication. This method uses a shared key and a random nonce. Each side exchanges SHA256 + hash of Nonce and shared key, authentication is proven by each side knowing shared key. A Challenge-Response + is not as secure and DTLS, however for some applications it is sufficient. For example, if a vendor wishes + to restrict certain features of an IoT device to paid applications. + + +The authentication is done at the application layer after connecting over the lower transport. This +requires the firmware application to ignore or reject any messages until the authentication process has +completed. This complicates the application firmware but does enable authentication independent of a +vendor’s stack such as Bluetooth, TCP/IP, or serial. In addition, most embedded engineers have no +desire to modify a vendor’s stack. + + +Detailed Design +------------------------- +The high-level diagram below shows the main ZAUTH components. + +.. image:: high_level_design.png + + +Authentication is performed in a separate thread started by the application. Each authentication method uses a +dedicated thread to exchange authentication message with their peer. Adding additional authentication methods is +done by creating a authentication instance. Messages are passed between the authentication thread and lower +transport using an abstracted transport handle which maps to a Tx or Rx queue. The authentication threads are +unaware of how messages are transferred. Optionally the lower transport can be configured to bypass the Tx queue +and send the message directly to the lower transport, by passing the Tx queue. This is ideal for lower transports +that handle their own message queueing. + + +An Authentication method is a defined message protocol between two peers. The message protocol contains details +of the contents and the order of messages. The DTLS protocol is an example of a detailed authentication +protocol. Messages are different sizes and depending on the lower transport, may not fit into a transports MTU +size. For example, the default MTU for Bluetooth is 23 bytes versus the 512 byte minimum possible for DTLS record. + + +Authentication messages larger than the underlying transport MTU are fragmented; ZAUTH disassembles and +re-assembles messages over the transport layer. For example, if a 267 byte message is send over a Bluetooth link +with an MTU of 150, ZAUTH will break up the message into one 150 byte message and a second 117 byte fragments when +sending. The receiving side will reassemble the fragments into the original 267 byte message before +forwarding to the Rx queue. An important caveat is ZAUTH does not handle reordering of fragments, if fragment 2 +arrives before fragment 1, the message is corrupted. + + +The diagram below shows how the Tx and Rx queues are used along with message fragmentation. + +.. image:: tx_rx_queues.png + +The Bluetooth Central Authentication sample (see samples/authentication/bluetooth/central_auth) provides a +good example to drill deeper into the transport layer interface and how Bluetooth is “hooked up” to ZAUTH. +The GREEN boxes are Bluetooth transport specific. + +.. image:: xport_layer.png + +In *auth_xp_bt_init()* the Bluetooth connection (*struct bt_conn*) is added, along with the transport, +to a connection using the *struct auth_xport_connection_map* + +Transport Layer Interface +------------------------------ +Transport layer details vary greatly, it does not make sense to create a one-size-fits-all transport +API. ZAUTH separates the transport into transport independent and transport specific. For example, the details +of the Bluetooth transport are in the *auth_xport_bt.c* file. This includes direct calls into the Zephyr +Bluetooth stack. The transport common function, *auth_xport_init()*, calls the transport specific i +nitialization function, passing the opaque transport handle (*auth_xport_hdl_t*) as an argument and transport +specific parameters. The lower transport is responsible for mapping any transport specific variables to the +transport handle. For example, the Bluetooth transport internally maps the transport handle to a Bluetooth +connection handle, *struct bt_conn*. + +The organization of the transport layers are show in the following diagram, the blue boxs are the Authentication +library code. + +.. image:: xport_interface.png + +API Reference +-------------------------- + +.. doxygengroup:: zauth_api + :project: Zephyr + + + + + + + + + diff --git a/doc/reference/auth/high_level_design.png b/doc/reference/auth/high_level_design.png new file mode 100644 index 0000000000000000000000000000000000000000..b5eb85ea5f5b1c34bc419bafc749c1f792289651 GIT binary patch literal 30182 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 VY3etNQsUcJ$l+aW}KB-FY9YO+0gaDz1SSTXB zB=jad^peow2K|1&bI#1YckbLd|D1P*;SKC}zq_ox*7H1T<)e<4+7((>S^xlW<@vKG zdH?{G0|0Pt{Q@=RFXh(IY09rNUV3WE0AwfoBIUz*MwXEi<%lA}d5SMNs{>Dy=caZ*kqCvDs;ppUHhs9xu+nHA(&FBz5del6?*1 z)fv*Vkd%y!h>T3#a%cUHOg3snHIy=tKbLCr*go^W_c~85IGp)=pYzYP_OpL)6|db* zIUP6UdUNOL<4b>U0jg0iZ~S{3ar4`we{UIu-kJV;%l*oY`3gZ&2QIp@Q6+Y|I+PB2 z?OGyo34@6$k^iIFdGxnb`hqP;GXPbNE&C5anP0KB0d5 ze|p{jeLw#j=J9`i$8YX(*Zvoqrn#EyS&do2;l$Lg?rM40U+ zmARRW@)fJ1Hwn>v-za&hC+Z1Hs zmX3nMtDd-T<|p&>Hf)s9yor1F$BvuE#UzaTEUSd6nZXVxC);Dzj@x6$xMg1=tC*W! ztGI=&=IPOhkikS!gV?zZ)5v}!tZkE)(6Cd}j-aOpLrP&!Cv-b+DnPGX^5LFoVO)e* zcd!JZ-GtIvGmh}$^Nro-%zVFzry)SB_`c0x33wdK=CNZer|D}w;58I_d9EWRE3*Gy zkgO+W?nrR@(?M+&2BaDbZ$=Ae{L}YyfPP7#_GxBqf43ZeNr|Un$;uNV*Kj3|k~Ko6 z2n^i}`56owG3g#jF5_U{-ja~Phje6by3XW7Wd;oD((I$91c)x9y4bBLLDK>KqIaD8 zIA`a+?kUeJu!mYsE2|R~}6Tr z#6;`?pM<6Pw;!B}4nIisZkw=Rg#Op$D?7rQ@CAo455$WVV%njeS9HUOg#Iq~4hUbp z!p|x6HCfYRP_R1m`|_gl+S!;iNVT@^_R?_8ks%1XW6C`JgZKBB{$<(lz6TVO>7b$_dmEYr&H*Q@6JmCWCC7I{!Ky67Y`Hw^q- zsMy`gi&0w>E=uh~*fhqb{<>+WJFddie)l!{k>Yl6G&|6Gb(}?Ev95ZH?0*@^kuj_G z*2VU%TXs5cf3Y3C`N^|hiAz<3Tp=@b9eX>dVb9^=55LACQ(17M9OtBql1M@08QGV@?-xqW!ksg|3n*|Kuc@QZ<@=9oBGoyP_fQ4 zzfYooH0;iywZ=M;4zmch`;A=C?TtDa(sy_i1Qx;}kMtBFFQiZO=E(=pI2sptq0f#t z^j~igwI{l1YLqNOrb^}ajNchr6I3#f*B*#95XP1Fmyge}?h>>i0n?`8ipPQob1%gs zC}+2Tc`>nwmgLqiXL@!oQo4Rl4a`wyDOf653Iu8KjMuhwHe}7q%Ogbyj61voFsq~_ z94E26(;SZwf%Z@Q-tKM0LKLtMR#cl#QgV2_e4H!FVq@Yv&jQ}a-udJIgl3C|JCMd4 zBiELwIEI7t!d?3~GW#6aw1?k`1ymb71+yCQyn9Ve)p%KMeidK;Ljdxn_EV{L)dO?H zV=q*=IX5ljK(CAU)#7bcy8+V_Te+!zk^SHCPoR(S-(c7Gol#6NR!?u83^erZyASvA zSsNK zuKJlrOa}@p(g8i=vSb+>>d?MB5xTb2iq?7zkx^eCX9 zmw^&I8L$4Gm1?{w+cj*a3)07EbJ+s;a0$h5!dTMVe2%Y^fcY!G3M1;iGV-JWF#^OS1$_tjy1T z8^bW(TBxC!tC^a;x7vX@>1mE*16>LVF{!&O!@)-waRSdc=zlXmTJ6H(y|NDdtv(8v zH`TW8_-Gq;@!%+s`?uEcbTBMq_|W>w*rM=b@o^E$L(!wvL#!iZKNg$*j^~;L{y0Fh z@;RTa2RWf3QnM^}zUlO2xSK_ppB^t=*CiwAmK&cOT0`BA(?%QKF#&2@F`r|fZoB7w z9IH3vAIP7Ll>QueJ3rAaUK&#Nk?DRCEGL|ET^DL$wsNB(?n^XKZ+q;+6ArX%X&oHn zUQB;5QJU-8XO(?$4mz$;)hA(f;M8( z9L9Nbrec=EdVbWx;BZbdXrs-4sfrR=wslXwAvg$Nbdb#E_Nn!J`2%bfH#~!wD%UF+ zs`}Bxen%a2ZOKZBKTRrGq`LSAhzuMK3GTY!h<=9Oz$)-Fnvw|qPVKetELik5GsiPN*QvN8a28r9 zS&iaP{V&lw{rH%{FtD{R1&O6@NDtq+Oo5(n|2m_mMRe)EDWT`2?*02E<+z4t^pt#nm=h=oTeJtUKq9A*M@+`BKN0VA}p8U%;oPSrj!yPDDXUl6`v-eb5KPU_u6oniAht`80|D2d5a1Y`i!=!m z+Lx5Jsqs$ zAj<4dFXPBY74uS|Iu5=MX12z3{Qq0o=l}Vv?R1I$pNROJPtVKfbA{@CobKj-=`kbn zQpgc<`e2|HIu%4<1Q_?mFsUHQ_`+}e%df+hXFjh;H=LMfYG_py0k~KG!?tNZ51Ek{ zI+?0Au7`^c9a91RCDm_~?W&SARZ2_&!2dS=XbTz=tw^JKnYSf)IzGS~4Fd zcWvTrVPU;9XU-7#0fpE8l>`)TOL{MSa%i%{1Jv(Q-qB?A?Abpn z6VhPfV6gqEK=3B+ULBzv=p=S2!Hu$g5&RMo5+k_7xKneKMk-1lV5g1f|IkW61k;}xQ`U)@=E3OS>I8q$h~U`~IDl?WoCb)?~wmtngC50CF3laSJl$ZBC;FC<34S5uI|r z7OZjV@Z0=NM@ z>iaf@H_~}ZyKZ4Q^;+b?t!4;mOBdqlIy18U`@*j_u9jHUi<_T!CdXN~*X3wMA3l7- z9k_LIgq_mtr{bUCPi|&fA@CaUQJ=}QkS`zW-IkEyGX6I)UVM(9lJ`rIbI)~q(>gcB zp!f^;J0lfRvl1pz!@+T?z@ryz@|Z(eZ3F+y3qnGMug61wkGom}SP zzb9WmCJ)r57kd#9=#_aF*FmnC4`J)&T+;$a?P{t=_Y_?`CI?NQ9;1p9B7;ZAPd0*18+jn9;aoU$q8(bghQ`99F}~S*z;yB-5wYCg~3= z`ZHIQ#UOj8pgbY}jb~w12hde3FHx?k<@>b-u9B2J>|{B~>fOjbLHNg3>7_H0Kh|8sD>TYITjjo4At_Hn|DS9GP%5U@e`vKV4qs>v2n_M9#q!~x<0_L%DJcaw> zJotYl@Uq2j5JVt_LB?XKNBIm0SZbzWVB@)3)D(cko?hijy(JWYG3pBz*0+ix zl7EHFud~lBQQ8j>$)?R;=X0x^-DTR=KRLFo5?vplFY=L&2Nak+27#rxHnS${`r=5n z7I>_ySxB@T#EQ8CICu5r12x26ZdPiiSB1EqR|kI|T==u#QkB18V^A?L*5L7Kba!oo z2q`#=_?v*AfM$s&o#=0HNuaffXyv*-&WW3y+Q)VL89qM#BOW?sUEAIIZSF}EY!kp8 zs;#o$m0sWFXdkojq{sarjrXBI>fYz}!andU|E$?|t-Xpd;JPeS#cR{x0M44}hwo-| zX6#yycj2IABH|`XTIK?-2R?j0qrzczgAM)FJdnZ(;|SqqIS8(#(33FzK$V!Tqu?(X ztQ1>c+a)go02H$SySH(*A%*f*j?KswJVJ$|&^~mN2uEUNqd%X&A6Ty2v^d{Z00KF8zyB4BUi#*XgvEd~FPRgK=`SgW3& zPVNV?C~BVKIkBpM7pr0r;)VqC(m8bh6Xg_#r|gxhLjEOtDSjLwUT>7MKp@0O=V*fs z`XQ)*-^I5w$aA=1;arDAdMkB3tPq{0+r>(lHlo!TFb?S^;CHyc%2NT}=*9j4S6}U) z^}Z#QFxkWuB?>(=Huv>$wQ(0>-H86aa{^3<6#7S{AS=(Yn%;Jx-r26LhI$`vbCExW z&6HzqzkKf?KXRvSQ=F%SG?Wn9{49vdd)rqoHo#VF)uQR9RPm8{@GowuPwDC;!wJFk z6^%VB-L2`UYg~KV`{A{Q-tuFmwdJZo1YHliM$o~h5#|Lx?M7o7&OsTp=009auaI8QpiDsCIbTw)o-~Opi$r z#Qs0Va9=~vcJg|0j*&p#Q&h`#qX|09?j1u?Fv764sVM84A>Zt?QgM^%nyt?}D`Ey6 zwTS{^PuV5{RK3FYrKxH=NZo44>EHbY6iO0}PqyZ7N$LXtzB2udbIA)id2{?_$wJRk za+DI=B*XmXCJXn^xpZ(B7RQQ$Y;6YIR6pFhGPNzc%g^)&_?Bx1#O@E92)rzy4z5va5uvIX*>26BElm~KDxQV< zRF!G?Sy1HVf0GEww^W}il=i<8u_v+Qr$PlJ-}`p{0Ir~=1JzB-jY_m+ict0jx#~;- zG&VHacGugp`cmVe2+x`3u@-AhR~hf@Jm03-5~Mo>NQ(x$#Q&6@{!4xuW6^OI{L?OEhG{_DrvFJ z#CPyP^)G4T>q<(_cJ#cz5a#al$Z;|^qLNthSqk!4eQHE&sK`OGWcQyeVzv?8ZM6f& zfEjJjA??sQ`W4FCRu_dN4!8B)e6I6-&{s@;Rq$Y2_9wU6q#3ct$yF}!TkUM)hNmUr zy0~@QG3$n2#(ymJE7LUwhF8&+4a1e75^G#A1=0a@?)A3a6g58Rj9%fl@|p}b_mvxM zU519F=P4Xxe5CArSayP7>pk&IwGK&_TGeoL?3PzV=Ifxj=b_pH|`i3pHaykn4XJ_d#s;}weHD5 zk$HQ9p_<2g7hB-s|_c@=B=l$W#3}!GVk;I)3x9HNEvEC8Hb(rQG1e72_s9pZ+mh+r+_u zw?4>K2fQZuTYkwZm(6&3P9w|`|0T*_H5$cAXHvRut9m^WQn7$ycAa|tobi);9?{V1@1GBw&X2_rWS zU1HpFpx+ytQK2BKH{VwBOryB~$gAktQd1SOUeNAT_+MZaA%`Yax7NRY%o~x%32+j+ zG)Y^RVT7(o`kT(Y&vd#f#MjOpdggx9 z3R{_;}zlmX52+f%ndNbfJ}%c}K6=>1Y(6 zQ)c&-<(mu8h8j8dlJdi>XhVgvvKmUh`x9ZHlqt<%V-8D7{?B}L7$|_&3mqYXmGz4856F-4LONhN;|@*gsUMeAI@(fxkcYESwcwWf7SAO?a?j!Yk=%!NtFTc6(-D_k$l>sSqZqG@` zJT8UbzC(w+4FtW*b+;BNE7zt$-ii4igU)ZFjH4oj|M`taKQGvK)Rs9cnRv10K0+%0 zw-Eq<6sL^z*}+j^(|rp2p=ueK){^v9_3ysS!_WuWA=D3^yA zWjS<8@wxVre?C{!{=X8W``=ADu7G%~B_wLVff`e-FoHMW{5^N$-v7smaedDF*EYNN0Dt4n(y4YU@B`9sYq??AmMDOo!==8m%uxmlEz3%Yd<{Ok%CufF6u zwM2k<)c(u4XPQLQo`#r;+-dGBX7$g71O}4x3dAyJL7m3# z@bCJitx!i`0;(p^OK$t~9ybjWTVVVT$eKaRKNHJabDwL1rq;X-TDHL{D#qYI#~O#G zjH`p6se}OIgnBJp(+Ij!2L^={@)^=kONj5%>RFg|M8J|L z-Wm^$P9vKagB>&a+Ac{4Y!)z-nupz0I!N+0dT4v_jz*2=SDG_(Zt*ucv0|Hmy{=>3 zkA(iqEF;OAOG+zNoW%#MbUIySWh^5ZU!F~2>tNHx7rvVU6k%#Sp>gl^s8c<77yt>J z3BmxK?e-gG9R?v^m3Y?AsRkcapJh+4OhFj9a}~5!HTI~tWqG`SYYoCuTvjGSS8%WytmWl;1MSl3LIW|C_321N8g3=kjhqAA3F;$4v#zB z)B0Qf@$prGG$nVgRRDZ=RoUKdX*}0uZz3tSqhyZq}iu0Yq*+0lqYq8!^ za=7oRl;xy+=gC=Jivx2zot(X;Qrx=1O>3ZncbR6fx*7my=zKNV9diPE3xjs(nGOMQ$VZG2l34BZuEr=J~#t-bNJV}0g`2BhRW+8>8&o1$ib-Y zN(*SS^R$&b_J-@PxJono4;Yv^BS-LH%I~|`KXmOnDYaiL>`jSXOAhi%0!-ia6ZxxD{F{t&hsn?B^ouQxkm+7y`eVO0Gxdu&f^RT@p@Oo%?y z$UOkyQ^;SC@d*+2n%W_9P>?wdJBq z-gk+eEMh7KU6#{eJ}yuF0^G)!(+-+$+)w$oq~eMn-;3fSz1L#@l`G{;ly5qIi82US zq$5s-gy5x?D7s97t4_stg)ttqrN@odsWcHcEFuZ{hg{enQTWd%=D^6)S}#Q{xNo;* zX8XJIbxcg2KRIh1;2cX=$Dh_0^@y}K?aedpzw; z=G8A5Wm$t5(M1?kR9ISdlcgY;8iT=np%R!xT;hd*^1${w;Ws?$t)?Z_ zsavh)9h{f-a->+9@|48dSeE!p z)T5OdH;+(NQm>8lePfRGK-P~X+%T_NsQIdsYUsQUqDZ^+B2{aplpHfPGRX3_7^+N= zqDTu|{JRS?KlMqgL}z+gO{DU`iz!!e`^%43#Z*Lh!XJl)?Mqcix^m;X@2AzsyZ{9l zbPo3tilTT8U~#s5hC9^pq^S>OX}iy;KoK)N2maVm^3x?Ffl;=SzW!|Q?YdU8B;dj2 zGk3*2#w8*~C5%Om>PtVyyq;)yK{QH~a4evl%l4JLA<{$j51+2`i1y2KvH;m{QVq6f zz>4I?tD1$EO@#U>m?*&@u;CnlHgs#}(gZ)i1I*o;y>=*nnle7U{y$Xf{WWJw+y4r` zif8_6lBaRqwfN!5`dva0Ai^plPGUz-4fFEK@BvhepN$fu?S}T98G;8*XjT8Fv?zsS zL&!uqzF)qpZphO^Jm;>_qcHVC4^=9P zj~V%8ejPo9H}s{KDza25ata)^COlz$@UMja$@G`fQT=SGR>lhg6^KDS@*c<0-ap>! z?-?#SXjG%6_gimb>yaPNT2))0sVRB`W?G-IrF~%nGu1fGe*)sArT@E|6#Q2Ce5?S! zcA|FiOsw86|yhsFzUT_Rh*`o7zkLow; zo~>mUWvSXwu-&J+KD6jB352ns?LwWN5Wy0#S$yjxCN3HHdSugoA!<6Ofn&9>T)VVi zAO=c7yUh$0jqjp25&adG(30QAZ)%;YAlr-VRf^#A*MxzLk-w%91vL$ zV^0Wrc{#Oeg1G(*n6(*?Vckio0yWvra;&oDeDbrM>c8n~Caic@CIMWH`h3{J-@AK7 zs@^TNfcq3_;retr`UY(1Eb$wehO}Lz*RSVYy+++^uTOtxlJB%`AOzVT)WcZHpooCJ zd(KOZ&V)^T|Es7CI6HgpWI4-HyR6+7C$CfI+S-fQ0HX~y74~C?_M_vNks%lWJ79L| z%tPy0NgutQvr+@6SvdxZs-CLypgXkZDET61i>F;_R_DZcj`IYpMvJsL`5iT%e0(L7 zbCHmP4Kcf}Gt+w#69Z~H?+*H1`0S5tS-iH^^+Rxi-1ez{KGtxJDZJ+LMGBh())|rO z4SiK7avB@W;M<$J(|+G%9Uh1K9qs56qEYhcrkd37HekjpT^O1y>zH5uh z+^5of_l$u`!74pPpDqMoi2N-q)9m0~EaTO@RL}X0DqKe3JIrEUpKM4>eP4A;{|mXK zL*NK`JID``J|{P<&T)qbA6>VU&T4QM2Wrk|8>>MTX1iMUAsP>0(_%ND} zd+#$pIVi-Y!Aj<4fq;qvc!>KpY2k8o4Mp^~@<8)y?OW9e7RWJAV{a04#^`aSw<}lm z=f%{#q};T0UoT&smK(Fa`&+uWb9thaWU>T7M5G-{KKWKDLxhDGW0Ic7lg%;-4MWX! z+>@GNnL^gd=OQk`?K^DM)7R=dU5GFM6>u_3!J<8x!}xi7Zaq(&(Z$-kLUq*vUWh{o^f)L-{NHJsQmUq`6D{n_c6fjk{SxG&yOu%nziG7ThM(+>a08oL5fFkIybo9Otj+cHq}b+*tjb zvcB*8=qlb9ISbEWr4K4DF6)?e&qlxB)A*6zvPzgFbxK9PFl`99zF1T7@Kchkb1U`&Lbrvixeud8_$~ z@dS?<-XrE~g`nqZpDw{LA$p@N4qs9^{_3Ck^1Y?eQkBo-O1Zw{3=A5~v044cu60qq zCSH|5vzC3&+X}Juj_vD^v!T0?dFw? zd>&Mbv*ko85>!%z9BAq4?2$lWk@l9(Gae)}D*S@HvPxT%DF*%8{qc)@tKH{4TqiOk z<+d=-NK()CFszGmiXUfuTqFJj=_Q|5&&>Lzed&q^ z;5nWywuowrG%v}38ED{#@(0=VB=5D;SU*&3$XCVpEzQo+{bJ4HUUk3A5*97@AiG;0GR%iL($)8+W6i(z(Ym@Ak(O5g zzP_zKb3{q9K3|PpyH@ZyiM~z=oKPXFE>(QuCVc5? zf3h_~nm49*^BZ&C+p*+Fx#fi+f{(569A&sD21#U>B<>Q!T0|dt2*cFs0Ch#1W>I z6xTwRfhv1k+W{d%8KgE2_b>4!knHkb9Hkqaz+VKq=^UjayM6?BeM-BUpUI{<_95BG&_k$_Au^i`KP$9r*P>9~R16-b`*$ zWVz3TdZfd>jCKC-{=J=CwQxXyMY&WutIC7PU7)?r5pbShU>57TjJ* zNGZ#Dw5H=)xd-j;G|JK}ttf05IAw1Z6X^lTXt;Mg|U+$uZcP4y7 zPt9TDxRfRZ0GNzWKUUv@jPv4rI~VF$-OV^e3&Hp7E;ADrwg@eVRNWk2aihMchA#P> znI}c7m6Qs_ts;26+?MZ?x#iy{zlzmoY5ir9d{A}6O;*+)y2QmN(-uzYd9@Hn_Uw5{ z(E$~%O#NRdXhKcLvef;|7jeU5cW`Y1iz3w+o$zoFuV*ukF$?Cc91+)WqUb&k`(d46 zcOI?yR^%Mc2)@1CrCw^N5x^Fbm$^t|G!J&rcO6@ubTyKD^!)(d^VSZwlAH?# zZau%0zCntXUm(O}?m_ZE1dXn-BRui*Eiqw1o~LJoWyWibcW^UMtu`a)xjm&?8S8nW zhY2a({xpVe1J>QG`i@@i6*gX4j#_ixobQ(X`Zx7u#EicZ8@H#oWUpMRlL3D+(#kTF zExe-8Ci_V6W)|Fc%@!i7vH1Q^mX2Yoe+qSp6AbnOZ7sWHzlyEqjsHaBcsu(sIM{KtLUf$XsFSU&T4|&a*HmRLo>OtZbb_4Xi9(B5L zGI9h+DMlyx^liVuCJm>m&V*VWDTP}P6`J~Lb_|D>L}SA{&ve8Y0Ox5{<);VxpqZ{* z9lJ^=4d9H`;0Fpc!O4RMPGz z;k@gSsJQrx&t2fQe*d?75V>B77nkTX`>wOC8n@^Ug`s^`#kzRNsg9&q9UiKu3Y@^S zJ7LW~I!(^BOOQ2xoR7{iU^0VM!m6tfOBSel_%qN-=*mn$*AK|C2>NQsalvwQFYR>f zulxX!N?gKp-f43E5O^|4A0h4Wk;aiyZsx5sLAO(0N#GqWG4Z%xxN4mv6}j5eCn%rc zHLuP6(t$ewb1eU*_VCW!Qo360Vk^wuX*VRhT}_e=VK~9@OQo0n*r0@a0hXg|>szBMy@iXHUU5xfvlHN`O z?0KJ?AtepEMVNBGQE5<@3*A^_xu4Zg;VPz~xcpd~xI|&@T-sqDEH7DHT#d%x>(>AH zS;!wJTPu0dPhSJ7P~41^R~lW@nQt*`3s6s)Yh)hm`bTPAH`KAA2V?r(v_;1UmPal6 z`od3iIk@NxF1l8e1OB!Xn^YtcI>};29()w`=vbR@u6V|yW4TgML4dAjn;oRjGBO&H zl~jABQ%y322OFDYzzHAB!J7G5K%K1y%=*|<ICyoSqfBtVgL@NbKr8@mU2MKw(4u4|*ge`66(dlrpOfLaS|KSz+`M!_uhW zM{T(q`EvdUp@3zBZ`(Op86tUYk0rzc~BB9%WhICt<7@y8#=+i!;H=@1nel8{dkE9=}$P zWLabE64!QeN_TPo?qHLJ*{0|fXJ}hDMhB0rF8s6mbgD}0Yn=DXBWKm~vhcUAf{Q;V zbW|~75-MY^`@t=_8oOm(gXFsh+xbF9NPXMN+j5UYm(3>CrTy|=(xpWVYgW!CyRPMP z)^?)d5oC3~c@MH9M~51Bx`4La*nm-F$Y^dnDv(%C-Q6a14zKVfX=PGC&Ed@$G%S*2 zH_rg!;!kQ1O|@a&`DqL}iKee;w{Noz9Z=Mi$7y_iD`9e-eJJ2dG%b5^S*GRK+D{cG zb8L~Z)hpc<_(v@4N-if6eF{^t-(a+Wp$!#EOC9zsEI+5{bHboQ}Dt&8V z40dnWV9?MikPE*Xu{T=~#6bBvR^BLRcTphr8De-caMn1D@&4Myo`ozFC>WWXKNNa1 z);&b8v`wd{0m&SWDnA-bmk~6CS!vA^R(f~tZ)7yxAINn{I}AggfqVr;P=_Hq8p@(g zA$;ENZq(n%OGRXKiL_7m1Jh(|Lm$LgC2iT>4=LOe4C7nHUKAkc-~3gqoXATxF1-Cq zs8gx-QTI-6_7aKNYiQts`JSY8BFCO9VJkdBiPcH`@a7gA;!yLd@AHt(!kMuFX?39= zb$rDTEys(^0l7`riDSUT@obzfR9mp8-jTiyI-tQMr7?qfR0t2w8SUNnk8l@=ZTiRTzNRhv)-q9kqavc2H()aRD0)o4NR z37ME!{#-YJ!}>@3FwQku?>M;RaMPP5?I?0{&6E(IdAK8Q=8`HmDRA(-?%nV0wsm;w zv3RfaNg7kU%28>*igd-aufHfPz9q{6k>!zGiigXFP43wreW+6*Hf*>@>1SjLk`9ODjo9@MMAk`k@<4#-+cAbxyU06-?{eBWIDf~Y{Q@SmCF3Q zV&M4&_wBKjc(5-%SU`HNQUSH+K-0N1ipY9s)th&F#qto5@^;@Mh;fB)B48gL=`}yl z3?d)V%r(^Liq*0PxDH_=QW;VX5~El7PqPi4o#kfMf!C0j?S!o37XD=L@qDnk0%=WO zN!|TpIz2%uIq)n~KT*$o<&NM5hqq@PgQp}0T(1}tLkm`&E9VmQuWi_I1RZ@~7$=rh zZyI$t95>&N`vxsMd+ej1wc~XRdD5Xo`sI(rO|LE@hy=gI>HLwk^c>x`IFX~El+2Jy zSh+rpw2xuSY=EUTX97g|AzdN2z52sF>G!om^s}dVvT5zD_c7W0Q6e7>$(?CNYZS@# z>*YCd;`+XM|6!Y!v&n<$n?=hx{QIk@Dc1Du$~)Pvu1kkYr+S?GxHTouX(@*Ha?f2w z9~U+yS{n*>2@`4q4Ualbc#3{mte7a69@XtHNvrbIgA11jY|FwDY$CmBUs<=p&@#O&8yl_QvrtD>RQzeMCNAfaSj&6qnF@6 zU_ubWl-#6|LrQbTGF*J*1o6`am7y^b3v3fR#n)|qC`o$%F&rgK0ykNMP8xxUY`P;2 zI2RRDLt?q^5}!DMi{8CwW7n7IcQ;1)Z8_GBZp1A>!E|kGd!_Z}Yed(|DvDtH%6k>J z1FK6~j7*AI15W$P9iXc$U7*tFkiI?D(0&mpgYSxb|GN{iaY&c{{+Hn4i8P@EU@;D{ zW3-vc#n*c{(y_%9ODS>rY{Lp`g)EnGpVH8i*E=PK4&4vCNT2#rj^4fKZds)4yJ8$Zx_^6@#I!qBm|| zFEl&qHNsBVrBFA`A?AAzN&lpU2KH~OYpVAPf!LGO&(zLu45jU7TRF?+!ww(1b+UGv zkve6cmg?856sI3NHox7F%z>@@G%;=(yRg1`=yRNCG{2NXcf2#8QGCr_U9Z=DF78iy z>VT4-(z%>6PjlNeWq^f7G5u_-<8Xh3dKNc8(613G8j?Q?sPgo zyuplsY|r$p=(iagVU z!*RBjXB#^%8i<#;kZpZbg2^ZrJ9%;Qc$`eOKgY%;Cc=&oGzNJ|$p>XF{v-SDT6^F9 z9UuplV3e;}IO5#eIC7#-zz&o3BBX9_N=zrd6r7oT=sTSWu4zx|vK4L6e>G0L4MqRD zx$BKvHmdu{K|$Z2^duJ3hw5hJKu-iR6>?vi=DNh2?mH|bM`ZNtzwLTNkWgtLly>@G za_s2DF;Gg>7A$ZjdI^lz5rv!xrtYso7F~#7hz{O&oJdp8*brnE z^IL-HhNr0y`9C|@gt+Jy%NnjBk+iZ-87}NyD#8Jf_~2b}T#vJq{{XV2ws|mYBF_7} zyiiE5fSNE}HWn*ce^%!vL&f}-5Lv^#&-@nHu*Ek+kQr?EXbbqy932U4QuMFb9+TWD zEh(pw->#?IAn2U5$6-@9T9+^F0C~}(q0!N%?`RLXjP}<0+3Z4arwHZ8w^oG5wn?li zExM+HVp@*ucS{#t>eB8a@JTD18|)bVfK_0!vW$z0+lZ^x!xCU+vwih+~U-CK4gi=Xl=-L?K8=r?ckVit9*jaUWBs+gNoF6e+FRuJ^%y` z+Naxl5>#Nyw&iofX3YutuAXLxkn`R7O0+j6`YNf*<({}2tR^=?IeP{DmyIgFn!PT< zRM!nlhL;lxm8&$|8+wMV{OKK;%Jk4x8Rc$v^Ixh0kGKr|SCpviYYTq-AgIjyotUOw z_q?|=zz4>DfB2-%M0;xew3d^w%VeMNBKFb9VDNX}*5%~As9m4952cb8fDpFiE+gDx znLXsB?OaXPBQ8sNCVC+xlr^H{*+mM;>~iTf#}u9=CSo3(RaE-e zN02c4wRZfG6?<4J*1<@r;~2w(o>r6xZVyQt0|jtyxe!S0G`5aOrV3>RoG_&5{3)bg zAJHw~_6)fx<0h9B*ykN@-GOhq5#SnX;$tt|6Ol;L;NV@fKAm zcr%6NVfO!0Y(F2L(`Z=7n0}lPJ&lsPT@@6vcHK*~X(N*7zoqt@fwK&NoG%7C(curA z0->1^Y2?(r*GHD?r?tMi@3noe1TM#XcfEFn?VV}Pt(pN{Y`9#-Njm=!Gfsj&C0s6t zGctp2+j7IHXig5L|66+l336qnX+22V>A!$}ppQgWWr=AHy)OrDUh7QD4Dt6B%r8DD zn)S%DBbQ2<$R$S++8xd9j^f}q3zRetV7hBpue5GFxu+B`e{*D7My-vy#zfnuSVvO= z@`Mj!Jeqh#I68jOT0dC7J#A%Z#|or=td}hi6~8k{V?i@urM-k-nA=Rg0A}1M!eZz)vW~|pA3$6PnKiu<~J0Ph>`)_OGfnFCgLk!`leTQo!8vAToy1n## zu5Z7n?Kn7VhnfPY9)Z`+W4HA(gl-hSu@CT;U5V=~c!tQjc54kMFzon(8H!lj*6-dv z@4uZ@TE-Sb3>+3__E`!yJwyBW+_2*@Y|xprEHJp!4G^&pAO1jz!~h8@S!sbin6r&_r>pudRV1=k4b5> zb1~kpWya-iOgK(#E_|QQ$D@zV^1~JMLUGywZX3gOrSTi}mvx)VzI!v=|3n(GL(YQToDGaU47*k`(ICs3;U zxc++7k+@UCcQm$ulv6elumGEtXj_Y3D+M=#=HCY}ej*=zW=HO11zYOpvHcVj-3tEq z%L?SUJ;}_6OyjS37m$`efaZ${tUIAB74xPeie%)3D zvIODSVWB)E>VNsvv)6N_7IA1)flMi{uJ?$J%yKb@lR^B9o7uN%;S0oXl*Q!$7)E)sO)2t?KpAHu@51X&EZ%fdmJO< z*yHy&^m@NvpU?Mq`Ch*M)1~zE^z?k(@Aup7cD*}pbjNW>8ylC~ni$?K&DdYZZHtpf<0BO{s#HP;0LF;qCQCVZzlgc73Es=gg+6Uu)>k;95{pNn@ zo+VbI=dPT{w1?AY2!3SuuN}+9MA0ZqFxmDe)s*!nFk^{Go_FTEDPbtPW}1RYR_R#7 z+qSnV64?!Gr3EaKxIwPS(Vg=L_2kB|rUG_5e2qEKDlR$IVuz=r@)OD16jt+fd9Tbe ztwBwwBs-Nk2#XYHN4wt@UD0?Avx1Bj#jKTX-G89c`O$L6Ws`sT)5aUGU}oH~VNf_T z#g+jQ`gz@dE%@O>*xdJr*hVH(zp$%$RAa45z7JDl8LmdBS-z;jsNy zlY9-a_7O-|r`Nbf7Rg|BTg-%NU()nygTFirjGxGg(7Py^-S*22=y}8IZPhek>Msmd z_rv;u1CCKgo`718lyxX^M09$1ZrBtOlKuIwVs=M{bM^GHVaZ~k_qNth~!3@ z?aIm~Ihy8n!_#m67#aI{k{L)JW zPE-6l$~;Vlb>Ny?pSQJ%fkCxQrNOe}`j4thK5`o}5UmK}Z=&oe0APLGW!?>wccuEPz8u)3lQ}yLMjVybUU(H<>jBjWR%2Jn{Dx2Kzl-sD-EyR&5hE{n9 zgoqJl*fOsfSRHRa7D^;+Uv88Pl{89ZArI+jDNIcCs}zwzUPkLF50W3`_nED@B#a^8 zB?IF>BhAz-%~l7kLo%z!=P2~mhTKOi<}5Cc6lAlTm=Pxg&CBsLHj$H!k2eO(3D1mX ze3Tm14xT03xM;$zgLa?eqzrOa+Sos)qIVvl2#c$6<{!B^<>%t&_L18XBNK1#la*a{ zTw=Q-iE~fn?qR4%^c%TSG}`6b^eQW%4+Z=hj1B~HGQF!MAEt;L|8nsSlta7 z7`KPssw1U+1+TDc){2ywJMtOtMrf%O-gAohvgZQN*$*J696&z+BD@jK#oYa&O9}JI zeV(PS0%*j^-R;(CN%iA#tj8hPd#hg9QA- z3$r0^%E5S*_UYMhzNmP#LKY5ozP(Q%E=#h>DyiEI&hA-~q#yISu81SZehp-AU=7fx z+dVhDynN)h_uJFwO<(hV+u8|SVgN45$cgri1o$=JA2hEArYzql;X6J#Kt7yCoYI{0 z_Pt|^y>OhZxA`DjXI|cb>}F&y&f79kr=n;6`P}fJPAdY`(&91hcfPQ6`-@GAM2ej3 zj;XfRNG^_@49c|Gl4R`>M2IBVb2-wA2m~pb4E-8#v-AF`sW$WdDk=vUGqGC@cJ~dm zKrt^mC8&5&b!M}F*Vyk_49OIbzx-?K$Id!)uGP@-75;R$xk+l`1iRmNLrzLYB-0CS zQtFpW?iZ8>mtE-QlA{vKq41c%F*E5#-R=3*!_CF3>^2$QNm`vs=?4+LOC1ef*Ygdx zL2)UnUvuPmFzFT|ern%=-I3U9a(L$>6F;yR_FWjV_9bb2JFI5l4# zs=Pbh=GPwY#H{OexURzA+&k5xj{xCEnwK@n!3U*rzIf%RVe>vkojh%HFKe~&?9Ljg zYt>_P>%b*wjXkqqb3r>uZ|OB3h`p|eDZ$nwE<}04U%`RaOhYn$DZzuKMV!ZzgZ*>m z_@6;d_I&4T6Aw}W#eMOk_Z20m-Op^~TyItCQ$OM6Rhdv+5<5*_gH*X# z#6reTP7ii}5sVu!I(Al!Id8orNxg4y!eklRhK@T92Huk;v|bC`AQbSNP!1`VQLe!a zl%t?j{YBV!CBo0^N5?KMX7oZ@AChdDAA=}I&Ri6pD62R0Xb`^|id&CL=B{NSp3w$c zba75hyHj#OcRpX1ot6-=O-|{p4WMsWRFckkyJXqw%JN*OfSSb(xic0cJ)YCdwSZvM z4xXq;;8DF;**etj)j#UVC5AEW zpj$6`593d+D+hX9PWyc`o<5>V`67DhgA8-GeH_B7pmAIP;yrzN-ec0}ud6f1q4r7O zx}U=9ssY|SY4pGIURS2QXL5|tMUDz(Wm^mjP%5%}f*=^4a{4Y>75gi7NvU2Rs&JJw z{L(|pRRtZRd_Rq*0x<>$HtVDI%uL=ACv(s&tPKLjQ7La%U`zCY!d#B6_AeY$DMmc? zaQVz=jn@^q>DF-}OEeb!?k5@vvmQ-3KqNlpQs$p@_f43_MY=h?{(;Qk+)jS&^q3N@ zEGGHxW*L|DIT~EZsl;x-Wh&+|N5pks|Ja(D3PZ`F0Ll8?J}rS;$(fy2cjHMOf1|u- zOi=&wt=t;lX7mrw7l%IdL0VJY_m}?(f)y@g9p%8H*9Vy7|3?rkOHY9K(Qo(#9t8UC zyXLR=XB{U~=KMC%B#<(&xJpnSsfB0?w<<^rM{H&(ri(q zvUy;{G*?qvc__%Pbn$Krw&AIYesx{F4Dr*(>XPkzu7171T%IaFVQOl6`6wjT(WlnT zaI*h|Aq5+jlQ5a{fzJK;!>~rO$z4ZH>BB7RkJh`kRW_mNrBr7=-Ire&|MaQhrAdHq zW#qrlLlC2?pOy^x{Il|Y_Te$rlbH4uTSxzW>9fI?hQOJe=u~*}3*;+NJ`Mf6CcJcD z`@hZ^55pq{X7IhwG(KeyR$D()hRYVj7}1$o3^1aD_O*(2j?soj4qw8td&n`*HUY-TXiY1tXoGawCh%jf3tZ#R|t zK|$%@z;l?e!6{XseaE70k6gPfUXdg#XhN^W*+ZzpVg4=PGw>DHFe^GSUbq(ccA)Y4 z^^5?r9Z&8abmJgi|Lq}@B>=Tx*uU!Ny}o2bY}TTtoLCVTdSlliVeAx9JhS#gQl|}0 zLjmlkMi%dj-zxBMWJilEYubTq(!gBm9c2vcna7rqmXq7qDl3`#C1c-e5Vn%JIs5A+ zK$0j0MZVtlYBor`8<$Uo2NO%u-Qo=d_l5rxX$)EFso{T=gafbKi~sm*$>;Z{%@V^@+g4;qnkI=^ou11=vFMuPy4uj&?Il42HNcn#JKB*&<9ML1u*`vKPKj zaWpt#uC!WOyYqjz9Hu+QFmdGq%J^k53a%Ry;?!#!MVav+!v@v#aax0kV0 z5jZ2|Hh8^G_?lC2L(oEdS^d&%JDupdgos~J#K3CnY0|gKbv(iEIM+R3`b)1)%8@c^ zPI8zymnI=3UneGPs}VB1bnvn^0!77EVr!JqE9_!7eQVTGVxwVB*ga$$yBB@GBr>4m zwz#nnjISg7gS_xiyC9j{R66}H*~FopTcGHx&fy-G?gkq7^5RL-#islr z9Y=SIb%S)e_eZO9oC=cp>9y0LveMPIFRkpQo~gJ@T|Y9?m*uxG$T)Q8(ho*GRpCj} zj9lBJ@D{gvI(+=zhGch{8dajtq*kEyMV`!tD4hMqo=GB)Wl6Zyq?de+tJ~DSEQTNP z5cj~T2P{skJe>YHYK#p(Inm)s>toPm2;I4JDJ^bqn1TtUi;Lt)czK-bHa9FCeJA7P zu0xZBZ<`Xa=XZfLko~VFqBtz^rb80lKjo5Tke1vBPD39Db$9S1Wat4DaM z?OtmuvgPXMRqZE{G-CgfOnL^`v{`fGR_j)*8U^|#xWf(B_f zxWJ7IOBnd~9}CK1^53_u{E4+P`JMHO*j*^cyxBKGPxnjVSXhk{$)`u%lmBUS+tWs7 zBiVi@#s;eYoMteVacihoRkgkA$K^~YvFo%wRc_+>ZmH1IHSC>{XMCdoNFar2{`9-5?s)u2!*;d|KIE^}TH(Dsv2?c-!dn zxfZMYw~coD>a=qRNeb}(lu8j1iX4Kl>>mf}v%50!PeqXgo@X_76AupKkt-cGqNIuX z_`1Fqz3%P!as}Rlehpf)gd9h!^|q=TwHb@mV;5TB#PAWDbOtD3sjOb5EQ)G;1SmWe zZhO)tvl$0fh;!3nb1&-uY?+qFW^mwB0{Bu@yL2)t>rYZT6fIL)iHD3%QhQUuia_idbxRg zFqsav}l@*cnXYfGl#Go!FQ(T$k#WCqV<>u^- zsfruc1^(%Xa1m~*@bdC9w;FT=)|G9=ZuL#WPJ2_;bsohW0=>*tLmg9?x?Es4w+`@A zvTEyi!KM;yve-U!CVe)vz>k@(K9;_?U7-UNRrqNn(=p~Yw6->GkMbV@jb1TOPVzlB z&QgN9sfG6o>)nxt9#ARWJC>UxNzvX3%OcO7-T_AjoD`R{FzYP6$}0R~Y8-DlS#WaZ z7?PWru3T5J-x8Hmf#(0mlfq+k(PUXOYHrfiVTj*1Ii?{uFvI*KIp0FoL#({;4ZNvV zC+6frqR^AqKGQb+DGY(SJKfl~$%`xRx^t-&QD=ffdm(D?9&Y%UtSaxmw>Qk|rxhVo zAcA9wacPQYJDR#+UWSSoMb}34_NBq2XMk-_EhkB3yHbqrGY~e!onIf}$Kexu6_#>nd zeA_f{XSb?1n(cMZG>?rg#8CvMsj-392v;~JUQ%>OXuBV6LeMvCjiv$CXv!p}HvYjp zqQ7M3<+>$p12I%_ye}7N2+5@zz`&e&6_CzkGT&an1u9Mv53i7bQ^W7Br|5ge0%$#mM*%bQsh6 zx~dui3j^v`j254yP(otCf9IU1f+jc59;$^k zV*Sy4phylu6vncqiHK$DX?+lUKRX!f#TF6HEQQht>K#d&GIQ6gyJI^znpyO^c&4KI zE!3{7H`u2b!)#)m?*@uOu_dIK&S)IG51tsJqiiPXn|(f4K_a#)H16#!!dKot--Q;4 z2*INAsAHJuZUW^z0M>=)9rEprBuY)b>j z(*PtPh3@^~IN};h3lU@EV{Gv2+f;otwNOZHViIX^Cm|_A zquAHVq~`M^Bv!IVa7=8ck^$w@Lm{MSH6SzC;S@>EH|)yPewnO*MjS&T7xw3oVzepU zk3!?KHX3v_zEN%P1~}d$<6=9Cy5?D=Vdg62`R})#*j}!3VBVrY&m}O0D(Adpq_|xcBh6?b8!PvQdS3*1hlOHPP>=P${23nVcG7;Z zFRz4J#f>(I4#{ey$0TDon8PuiU+xdO>7<_-bn8$kyAn?tlJ;pYkOx@}v&ElQpk6Ha z9o=rD9+nt6)#C1^gm@~wH6Qt8(E6f?0csnqf%BunES{ac70|_t6nUN_0&^Qj(~+qw z-I)Crqbyv#CoA&RUI)qy9Q43>v@VDFhv?O2Ff^;OJvxXM{aR2vNOyDZtLBH+H=Ug7 zatLGJ21VN*i=nmJZ8pknb7UBag2ou(CS0D3b!pB%M;*tuK&J^GLlYDIDB($0!iOV7 z-0L(FOZcE?|A*T7G>aj9mO&cvEr~!k1cPTbCMcP$?}+VASV)5Sq`=XPuyBNZ}FM4-q*XV zH@a{LDqy(gErmB2sw~-7-fTN5IK8YSgl{PKQGv-LDD zuq1`s`?vH+Qc3o|)?(bbj+8PCoNW({5LtbHg@lBFLvAN;{6#7}lt~sNGgep`q>R=@ z=?SzwIYxKgD(J$bJ+ZN=n5I{*Rl_EMJ(7jEty3~y;z6TD=n zpk@6eGnqPCiuq)A5TCNE!+Ds#k)ET*D|p_XvKm)M!rJ(;dI{6x$>quw_#|OK2$B#h z4h%K`-uqXN;c*NhnXB5^it>Tp_!svZ+a<_9y&u@OExi^}=zK7fvSYHJJ$;S`*`G@+ zc=aS%5WcVK=$1Qy_VHGJ#y#1q)dU^vjEvQRU$kFLJ)|wt-;sO)(sL1jF=sRrZ+2L} zGsmSHtSibb>$Vy#7$qi8Qv)3BF8b)L!3f%K^DlAUx3+MjMR&7Czr8qR(j-ol#SFuC{lpe=!ZtTozAr zL9S{A?`gG|G@R>jc`&Q*dKkhD__-WV4v52PgU4=R`?IeHv35WBe^eIaR`QlgEbxGY zN`8uGU3+jVF1&p@(sMIqZ+Wg*`<`@f`{PBO2_0;R+jajt>x$2uJcr5DDNP^*J@5o4 zlAd5NMSq5Iv+iYB#~0A>so~^%zg5t8r+n(E?OwjHNdh?x;B$3M7Q0RQqXbLoxG9Ju zD*fHIZZ}h^cBQFy*5&|B5G^qY@%XUIKTbga1q@%DEC>PLHBNj^BXS?}13tZypyRVf z$LF7wE+J7cf~W*?kVESUvW(`8zqj*fd=tRiBA>)pkyjGel3DVu^|VQ|8zf(JaT3=| z{-sw|aJ|StT>RE9VM-OaUR( zJT1giq8|W5_BCCj`zl0O``KZnlV10T$O1}f=pC|1_n5)CDDVLPCAI3AdW)ZcedxQW zr2R|neZ#^O1cwf5VzQXU*x+ghEkl`4b=qf;4%A=&KwdsnAq$4=-`W&4*i0oYGu!ZLy#Nsj$4ph+XKjZrtNM}U zG-miJg@v)GP$#8)4zS1bYYv(Ur^0&x>!l6B^h3HMQpc^2XW{Gofr#k zlpW~zD(0W!wJY51jedN{J8?vWraQs5vgU~7pR%c)h99gM8QO`Kw`fa>rZX&*xtZB4 zG8#objb`@DMhq}+#)Zlc4IXMQ&**Cv4z?!LSL8Y=4_vcaMC;?eEOwQDWEAeQe1AMh z^E7{=&h8@Lc71si_O-<3h*>D6>?6tGJNY$BbAx-G0v7m`jEj#l?dUi#Ir?RyqwSHL zDNwcAX&MR+zWbckM5u=Z{J3t4n_;s0p?}))tX)XsoUq)|dd}03C49l}te(ll(+Y`B z+3X*g(?gfAX=SgWKLHmZ?TiC^bY>A^cA${4fCc+vvK<}+@h0{ec}`Z}FRX`Y04^d~^w0ENE#0(n?3ZYfeL zs)?TRDvNtY<|2!fq6@A#XL=#mYA~rNcS80FNhbMLw1jbgRpPt;gr&}^(Q-&TQ}^qu z#w6{gKTPnouLhmH^q|wY{pjVCjrKe8o?F)bzaCzt+}hV0WYPR949z+o;y#Mk^%Qz+ z7$JjIszPtGV|=a1xaeEDRID0J#Hropx(la@JyxP{PoovS^&F8X?g@_Oczd{EDEv9} zQlXiDL}Tud)92=Z`)XtA**wS#MbG_L{OEXKbZPEeR`@H1TX<;a0lw`CQMZ|tSy+$G z@zN;Ctz31n(p_egwzb1$*2~RB>|ddtWGsIm?>JJeNKSroob8#mQ@w zg+L=7p4rX2aVB+DK+%=PC9ds(EwRbKP=dqwLy@rxL$>PqRia7^GmG#EE@z*r7=G9N zQz>_US5VY{THX1)G0_uhk=FH|0d>zd63maK_x!i3-4H!K3*3W2epAmd_zO*@CM|5y zL$5?^mXh&@Gdq9;1Vg(FRbZPD)>lKY=g%&k?RhFI0F@}P;#M@r$h}vR@}1m?Aa#IX zp&9BPx9dGwKSs{*Mv1(5Mt0I613vVQ=bTkniYlwo(m^;K{Sh7CUvdW+Vw#iw-Nzzv zed4cQ6IPeugw*i1BPxYVtJ9^FkDGRL9|t5pIwzE*zY^m#@<`Iz1$rX@gq4$E6TXOS zp&a~P+dmkLu4ha;>PMY|UQ%9aW`hmx^D*ZBRJ#UmpB*00QRbcTtT#{yHiTHw6QBDh z@&E!r7PzdaAPaqCk~sSdUijs1oqbK_bPG zWyovJo&s>qNERVQ(Apa{>%U&`ML)Qj?3W%@ht+nUC~1JZM3Z759um100}>%M#s2%4 z#`UV7yBo2+dnEmXjKr)vo)Pd@Mq#wC6ZKuO5hpn>{4>>DSgd%z_tqJ9y?1=aklE_t zn!l|d;r17))LxJ>p;$aq>U9tZGPqAa9{N>_CY}vqil!0eyxZhAIq;epG2s$YewBVc z6CXcQ3>R>q{9A64MQMifcc2vq=k^yoasi5=tJh^T=s>GuGcBZB|Z&P+vgvW?p}&(QGxZ1JI_?gvY> z=&9OMiE&$5ZBl;%B!bx8Sqn13n|!o&(&ND07w2AB8&&)6i1+`1L=NxyC3%?91#Tbu1ldZTSeYOQrcJh-=M;fJ6kGp zxp`!#>?I>-%;&%^$Pf+qVWgO>(By4PK`wsZd3GzMWu1@MY1I@_~A=+(bZ4XH)qJ$q> z;5O8XXlU=|1veWt`}a{UVBEgp!GW7pGL%g4I|TX}M}c$7_-%FMW?c~TaB4-v@TE*v zs)sWe3Wpdb&L-e+_1TjT#cn-}Dr_ZW|Gy{{9#_5MGRdhszHpom_FDb(fk|DUVxOg_YI+a$lgQCmhL|+qL)g&18tDcqg#Ih-Kwa_ zOB;cDZw*lmh&U#uq2BlaNn_d6Z5Xd+VNpl7Wh_SOLdx5#RKnM&aj)cYYv14Q^f+m| zkLJHtP|Jhrd#ISnm;zb_kMtcRT}lDcFSAEpri)-v{e$Dhtw-EShGI{!S(x5$j?-NE zj*O?Yoi`~3i#h+4iER65sh!e~pfstq^6i~+Q3$nl^tX3l&&*0*21m4H_u5X4;Vy7= z>?1wH+1Lm5{FAGVUDgk5`R0GG-N*VmzFEhz7bGHO=C=1XiPuydl3QsRM7foQcO%*C zle1;hWd%)l?gi~>$yM3Q3(uuv;E9{1i{1`czmmnnm*e5i0sgvwa1O<_g3zM+v)9c0 z%1okXy+%z1?j`!_@p|_b+VStG9ie8NFAA^rGpygf`vG84E1%>^&TG0hVerDeQV-AK z)^wu~M&ug}kY8oNbeok{GDAO}?M-Uu1jo=VzfKDicZg$Z3H@)>b4c)4GZ5YHVb^p$ zw$ywz+6!7IKl>*)ioBRor_TR${*P5QK)x}z`})1nwHk=#t(T_BGDlil!a3EFy{y6DV9f$%{Lw}jIj z1IGNpI1udiU~l|M3ict)RoN}q|8zB$M>H-9mEY zsY-KL5KpnU3bOJ~L|X4+llT{cGm?DUl5xd*Mxs5pysccP6oBSH4oP|?YS@4L1fL{a z#oh?{67J&{bM1u7AjPTQM^Lc)Tk^X{#{}kONLp?OXuE(070~?R_1uW7Z^I;UYpnbt_s;Jl zVJM+sLuNg3pSAzt!sXtU$z|z@*%4C{0FC4zexzC0G+|Sj%l1o>F@VmkM-wB9o+ES< z`PU>)o=PVTll#>M=;^Y|-s-ik!86?UMR=#sA})`|(#hNHa6efWTn`4?%72b}S-S39 zE|oT8T(;eMITX4{1i(pzZGU0);^F0iqk+r&r%a;TJE&+eode_Of6Ba-_ql`iAS?8G%2cag}LrMkh4Hy0)Rir<_>iOQ7k zRh?nR+!Dt@t=#u}GMiLcXZmzlW0qPmxZ<%!5Om)meA2U^Tl!Pk(pG&ROOxGD^0GLr zBCV~|8}Zq7ZTdRi)8UW<-GgyB{AE!R07ZiWuiFP*8cMv7r8Q&$;A2+b7sN?^x&xq( zJ=f(Rk#!>5Y?$5dHs_{)({ z@H;G-dl#1|UQDMNYLp)y&0$b-_>3Gr!q+BE>(o5F9zc5DruV;z0k6A@X`Rw$y}K+C zchrW~XP|IbhY)gX#m)HJ^I%+e=TSSq1ayf1`G4{is`BP8gp<67IMkpb3*kI2!)V>? z%*)bgs~xp>FI$HbdLu{ZP*xS@&I7+D1ccfDx+?#Y?n90iZIDw||hB;SZ_PiJ+$ zZNYcjou+oW^MA6*ixChNKp&4`OX@J$w=l(}v}WbMSJf<;!Np`wtyCS>sF4h$dlw07 z-Deg|uQZ2Y(@d2`t!cK}{%63}Cx5m-r%$V{&KlaSin7QIqzrF9jn^a*%L+2)Dshk1 zmd@h7)GqEeH-Hd!Yfqy_4p3Dxj?ibzaj(_K#rL5Dz%z#~V?LXgYDl0U%%+{bjopUK z#JtQsSUczD<){_Cw{QFC9U#%hAdNsopuM2p@zsSacbz)bbm5uTJ*uqFGPuOo`Bc2; zxUt%fVbRo(tB!=E3s-p_yoL;Cg)QCc&A*hTKGY|h*XX7bU-**w?yA!%9ao}vzu8WB z8dj%jPkt`emOc5fT=qtz$c?MjWeXM`PMZNj%I7b3G`iTew&#SOvQkLakH^V^!RDRc zxiQ&@i+KhVj#Eck%Jf5w`qr8)jXXAo(j%%V160n^L0gbFkWXoJ!{wS&lZe#A6)k-5 zxc?=QFf~WAq7_E=M2syWs&pq%2>V3;3`c9xK})=_?`V4Hc7FfHKo{!%c6k$e_x>nm z1P@1=>b*-Aap4%mj1{gp^IpzBPl030IxPA(j2MXO@)o`Z$?YtGcrMG+LSd(Rb-NK>WeQ)^>1F)*Wyoj$+B*ncow6SQtwdOn}e|#M@ z!qT5b+wPA_!^#P|3(4+5F}d0TS) z3A(8#z%<}cqw!zU0QZ$|GNwu*q+QF=&qzy_Fa5r9(>Q{69ba27%gw=s#7>_Gth1Z# z`fDQy+`Q0LUONq&z3`N!J>RT)ShVdKcK~n3ci)W{K1Muus|Cc_%FCUeXp^|K-+Gqp z()=o3VBJoXe%4%rb)A{)MzWIqzGI*l^}Oo-HDp&o8^Fjq>pXd;TmgC8i~~(9AE^)} zYF;!)3uPo8XO7Cw%_`;DE3lAd*S!B!`IL4R=GNicmGqh`a9$r?tM=VJPpn2%icpu{ z|6JXjyK$js^QS7LUF`PZ--9GD9501rGz9Yc80%reS&@#ze(IrH-|OgINVq`Z!liS3 z%;yl80ttQKi7S0##kb>a%NmT~q1;pO5Siyf_ZF~qJ@0LZ z_~>1vkxkKss`g9p7f~-?6lvRm?a3_25M!gifQZ8E8;mvs(7hczUABg*O-9fr`CqS3 zlbWy}dEOc8j05cIMpa<>PXvT7qCxBdRp?fmBc-OvUgmeb!MvXy&?p52_J~`uCGaUF-7a1C=e}QX7+Y z^D)NO-gc|b1d@T-umR4=*f8!d;!ko7LZz!PVS#1#Fnlw*86+Y z7C`L%QB`z%V%AV4jjSx`benMlXK~!luMLG(3Hz3(^Fi;PG{=WCrDhA+QpJZ0Dg&~X zm0|Uk&zTy^zBsq-=lYD8_owk|@=Lav$rg7F&uZJ{Od$#)8bhOWdf<^Yp2>D>Ip+n2 zvI7L`=$v8egNy&Bx%BQ^skq^P>8m-O(rI}o`HOHW8-)z=xvywbK3?U7OlzuYFjEH( zVuJ{>6s{w9J(mQ@PV+TqfSN`JK*?2!9He<)#VaG3bh*L;f=nh`VyfVVYv<5jEr!c? zbSRkhvfZtXNOHvc)~jpE6%*d~rW#dAq9&pTJ#uyrHUbtt@g7zUpBlzTcD}Qyym$A! z;ljG-+_%2gjOK)_t%Ni_GLX_tT`14(e6t|o`PkMnp}4B^PO~>|-4e;vsk+k_3C8?O z#Y;h?2yG_3Wr0cdYhs}Q1H{Dp8$=vt+}ILzb2m4ZTF8C}jQ1v68|QNYL9(ZyTWes) z)MW_&;$u=$QsJrf=^eB;$!=FOS#+RsM5E}sX(8NUchfMkvV3-#$xn%8^X<0XlOIz{ z$bx+tBAT?1$tRziiRS;(WH>Jmi|TwX(Zj3y3+y99a zg2Gy+(cTXODi6wSk_8j{C6-T7&y=ZZG{~Gato%8^;Bwpab!a>E0gN`|)?!wL*@q^M z%{xis*w-SHPX$i-7WVGP0^Jt~yLPEYO}n4waQVd!?4`=@XLtHN3kogl3DH%g{3`t< zaD1$^ENoLL8OuICuoHZ6kv6Pj%_CoV<3j?e*ihm0K;!nHl7!4PhVF)s!&|*)iY|Y!4KXHqX%|m7FbF-Fb3x(5LvBoW>F3d|C;n z@Z@uuapx5gu(LmtZD%D9G+q@Z7F#x=OCY~t*=ZyHh_pWVEjQKb#epxUUC?U&*qukO zLufccr;p;B#Y^q3z*M!aS(=vbdud&tH^_Up0opO8qLQ$KJ|1estyL?`DHCs1DH9I; zJ-|d+QBt)h2m+qo-Dg9^lB;PT(`}S1Y*mJm3}=hhQOm}T{su@@>Y}CbcaC&b2Wl>7 zV}2|Q&;j&l4zhw)bLl%hyzCao&L30<6B6m~FTDZ@8N>Op+z#4&Pf3(WVICoG0#m1; zJB&0(E#dec{$~#QvAg`?0=i8}$<%S{Ga!;;_g7OmL#so=@w9Z_N^O3;5NZsqwv+i< zL0+6~7eDs{gaEJ(K=J+JgUsHtYSuj3nt zThs;_T}-`iP*{CqFmtTSc(+b|_@XZN)DRP#e#p2dX*Q#6G{}W((&^^r%sPeqTKAwC zByPuL&hx-EP_kN0dTwqCKMC@ICR6HSz0Zp8`>gJWogk|RI5=?d1en(i;lLj6<(4o2 z^SCj1#GH*6inEK6JzVakL+q(?QYi~_0o%Kx>?GKf%w&{C5xiwK_0}{ba5A>l9H0qZ zYen5j46dePo1tjR>=?{?MktKlOQ+pp+G6%ES6|M$`}V03v4L?H$EnV4&dRo&=PZDFDkbGQDf8{jkN{UVcSW`TL~poFJ#r^JG>sF{!VcD0=>Uyh~`9x^Lc*tfa;4ehfn zkMo4Sx|jSwrd;0il1Av^4KPOO98f&ABoA&xG9l@C0&4gZ+rPsFQ{j&XDpC1PbdFXZ zmb|HgW zZFAbZ`q<>ybnas1-PLco^4xm`5m{|hl9i986Kx#1b1y1ADNmT;1bZHKN5_gFjaN+@ z6-$bg7!pXjwXzvWw?6K8-UjbL-8n&&L^} z-9=gw7gsasJ09AY{9a`)5<^5FoLBP4Y}xr*>}%+?k8t}igtpDn8F-Y1zF|Pxec|jW z-?_~;=hk2+O5JuKZN=0;rC9!q@n!+;F@w`I!s?Jwlp)>=3bDgZEZ5P156Wd2;bqy0o< z<CWx3|=89ti3;g#$s zlQ2@+Pfx3}1~W~cYU_E9s(c9+W;LR7QN20ad~VJo40J}oqpj1PdqQf?SOuD!{=h!* zmqB9IP5o!K;;{E0NsVl)#@BiOm`M)%&a&%jJe$Z%QHe0v)XI$we?&W^GDn1S%4*>@ ze+1=<{YEN7qEU{Qh)wGVGzMiO1=afTqY|;Y6y+c$EV-Ln-Yb>W z_d7ns4Wvn|vcjfG?Q{w7|F)~;(2|w<{CzLKtFzoYQojpc>-69gO^o{F>)%DeF_V$@ zx#`{MsI~1sFBA+)gqBJB9$WBfVBZ6nH;HeYb(C>`;2-MA-B!Nv-D#_E3F4C^aM5EX zMVX|rTjSfxG(Buheo!&T^%l{1Ihlx88?pMS8Us>OHrx*%Hnw{#Yn^CO1II zD}=qv$e+{N6yT9b$9kZYXN3;O_m@D-vV!+sqB*OlZs<%n|7Db-Yw?4&eqJ!1wYA&g zXENSZZ|5QzC2;nT-oDo~0q)PyE-;UsSJ=+c`;o0XIRlvaEY5F75uWy(r5}_^owi5Z z1B8v4pOpO(?mt~$t^0N=AC@O5YB6Y@We1k>wqkmwO-EiHom+#_P5T(b>9xMha>`5F zm26^SGS4#a!>ZHd%q;Dyxe@ZH!{?nk^C6nETCt8sqywo6win|&Mf|2pS1tY9`-Hq&Vhj&n@sy`*XnJPw+-}cq;f(6o_xo6c~2gR?&uf?lhW1+@lxp`K_`>wKa@QpP?k?*_lku=aFyl;nKD(gx#%UGy6W&NC-mpRdFy>%hS7Z?;WMTge6(# zD}Rxkb671*K6n6TOjbRgTbr;`r?Cvf?cTc}wCLEdo+V9zl1G(X@SRwQ^ZSHJJ@Nz~ z%8^X%75O|{w78kc1-h^EFht_V4A!RwzogoD+z;)N4;w5;ESYm#KNL2g?6|5oOf??8 zUcvv$q|9CJ2r}Gyn~Y&k>PmTQyVJ?Sllw5})13HSrT4A5P4Pkxfw7j_43NIT_MR{p zmD|A!lqerf5k&$6Fd-6GvIwp(lq7rHT7-oJ30bPWL9%bSZ*`w#;FIyCLtnZ7DLGW{ zQC)7XA!h}BEEezBjiGu}My~vaMA7NLspC`Y8Z!V*SeDmjr3EEl6x(TC5*;j<4` zu95B)`1ofnNI;&6&%5nRj}>?Ey2CBWyr)3;`ft+iyACTC*|(neaEPA3T+T5rnry)j zWAB(z8B~oAGA{oAFKw?_shMZ*+_tk5IJFU%U*e}!%yBqN2W-3^LK6jwvlmidYDsCf zTQ#+C*H-1og#cocy-`cM&GSq}53?C^fNAp@{YCBO@)HIfTVNSmX;Vk%serM7wrX{j z&Mm#!T0xE32Vno6g2PNlYOw$-*mOqv;0ir$b&!meeEA`Fd*tqZz{ksDpJP*|US%GG zmubUqTJhV%>Ay_s?lpB4Ik);#b!6IF*+QbDjBGzHM3E>!!7aV@*j=jnlM1cJ$Z|I0K_~unMSxD> z$-Yj}3svnq;*2c~;a#|@>%upGf;Hf$txyWLKbXIgy-OCLy{kf|PIs;)jq6%0pTS*D zp11^F%a1;WS(G;-57V9C9rwAe>^zE_Abb!7eh}9CH#d%nq!a5(5r&QL`sW4B8R}0a zpFB>cxh!_U~?%kM=4 zJ;{{S>vHS2w<{YH_8?aDL$ZyF^pm-wJgIYileXUQR?&m1>?^dx4*^J`ay;bk!%0Tj zKZz8C?HZ{pGUn25{7U;qJJR`mDyoIP^rQ&5P<2TPvX8;lXTDk90}mdTGwH_jAH3_Q z99nCMxlE6Brc6CNUTR&Wl&V!}b=lb&%SvOFD@K`byF{gFwb*2uJzq$ zv?`Z-|E1QZO`+1w?17ZcOqrtF&rW2;h{cVbsm1+dYvilAyjk8nmC{4-%3;Gm3Y_r zfvuP_=d87g@0t~6PDH^XdQzAYTiQM$UxBt%RUBgcgZ6%7hxL?n5~AzOiyxpi`+eoK zN_QBySaySbT{rJ|`_m!$+EVK^NW5zKXnL#CZI}zcs(>l>YqYEv#t9j zxwa#-hCaXKTHr;GKLiQKn8j##+qRuS;ry^)jU;oHe-Hndz>l9#*U=Yf?X+B7 zUD+52kMG}+FCvwG#{VB-?;X}u)4dDF0#@WvK@mkPr|;=v5zjFG=X2^n{{7=;hm>@2_0vJJ*?i^dfst+cUG)y6?5t@O>Hi-GpW| z|CIM02#lrVEyFTzbciueK2~RyjCyMucU=G?QJ0z`B-`3x!dS89+nv1vgMIcdbHLh8 z^A<&*>FkCVx6yloDH<&-LAU~$WB=*1xeUaT|I>Qrw!T5x6r0W6N94Y zAA47pC5hM$)Fp-O7@7jgu`O240Gf*VY}y^HsjSmaYIGlQf1*B7OE2W4jr8JJYzSn4 zOKYxm){*w*2b=yGp8Isg^FW%y2-~1;U0u$eLtwV#Har;dUSVa2lTnO^_*;8(0QG3! zO@%#DCcr`tfI!CZfY~dE6b9G|kc9#ed7~(jetFz6D!|8wl2~dK z!Hh;Ji|Cd+o zU()wYda;O-=Rf!rI2E@z7e;C6Cf4VK_W1ZybylqgIaKpADH?0>_R3$B*PpQ+r15ju zd}YbK`dn3e@U!sosQo5`7!R8+@-o`o2KMPAQHIVGv7Gw~oi2sR1B+O#8^=4-zEVqT zmXwV-O40gOIArLFmk!ysgin|WLGs`-&0>OM0E;fq96VQ;92na3Ac4Y?PRqWbv3K#- zL`r3uR$|&nnfY3JrsF;17;Yi0XVUivB~ja^xl3MR;Wg%g6UH`@Dw8r)W{=N(h8@Tt zet{t{+}jRygVeiXy9Ua|F^-m*X?u3zakYE*i0szV?m1F8zb-(KRlY9k53$CBWxTz; zhxy%IV##OTF-}ACVW>-Jzk-AChj?(gxbY7i? z^I6u1)=PJ?N|kipY&_-_Le^1cp}bKtC5H5FF|Hegf>LfRVTE44Neo6mxT5wG%uKt$ zxVHE*6u2GtXB*H> zLtV*(ar1KsQ+}gZzVSk6>`_iC)GmvffQH<&fUT^+D7_tFXp7{M&4c64%A`9*Sp*~m zCXE}vmlu^-c}a6V?*T)q)WsCsNaJ$@eS27TLkkSTM5Y5f2B$#jc|5$`Ws7-$%}k6N zQv9bvviDV)BX)S`aU9z#~e%!YO-b($s}4ZOKZPHUTC__FU_?-H|TiI zBKqBfzKJLAv@U2B@dzM=;iy`VEo2-B)Xh2cnvnCsQ(T$IG0lW7+2lFIhzPwEOU*l; z!-{=F7Jm2(1=%TC!a;zrnye`tvfIW_T_vtl3O`kF{X_{C$kXK7r>5}TWyCZl!d2~Ig?_Kd$ElRfmO7%0i$cBLuJVz4>eov#A{L7Ip0n-4P?1IcpK!&yUy}mr)Bu)G#>HjtFV(k4h*j$-fi__A&*eN`=u z`4WAmqm-}+8dOdZkxVl#a|X+@hVAGlIzCwgFC*f(epZJlex>9_0OI1=IavJFx6^@f z;t*l_O!HF>Zhmx~DQcY~W)KtD9(USP%2@K!@t9?dnG>^2%-p%(?Uo0AuG}`Ni#{{3 z;XUNc`-soE=H+`lHnp+6t3Gk}7S>%ZL@OT3=JXRI6eN&7w4v|rSlSCtgEC;aeZ|fcf%W zzsMEO%aM9znj-hE;#+-cegZQ(;ox{EYTJe=&-BlAw44<>iKaZ>+ zV`#5Jk%*9pi@Y2D-GYRR`;)-V_~%*HsB+HfBa#0eM(B4QK_$Fp(1cxuGh zK^$*dI`(k!mDMQ@OQB`O3{Rf{u0txjP*bPA%^C&nE6NzQdZ8%|{umuELh&pg4%AS< zLI25gT4bLpS=c|c_Y1gncm4+4YmUaxRuxl=+>v5l>$>?NRySno1;&Ua()*jhXwO%f z2nNP=z7~*zXJ$-xCwE7s0t$nvQ}SKf}yR@Fw-sc1%?^! zq(Y7560e1SqL$h&Ykv5|Y%{^dtEhFtYw&O#Ll}rEeJEbOdP({Gg}%?#d-<+fHMkR} zr9x+V?Minma(#6vWk;9p2@IDppyHn`3O{o5H75Ozl@gq*Aq^=V7`PAVp3+V0wC*r} zj?o9T-kSfQ-{IcHB=&N^g|QD6pg6f(Azmx`qDYdcwM=3}n2K0-gwulg=91t}61p+f zR-dW2xM6Sw>&9TS5!uJb_Uu&PJi39wcyL;tSKBnQ$he#{O^ftEzxih-c{L$Rpd&K7 zK}~tTz@nR-?o41_5_MrPlFB#9VWo8Hmj>@a+f6P5efogsmhc&^E+4ysZ_P!0J@p}j zU*`zp%sVQqMdwxem1 z4YQF!CW_PJPv)1tSmNuL<9>>9z&c}Nwj@o3NEcM~?@jh<_Vf(yPwV?(Z?Lh35z6HF zt^aKp4lJdNr;R3RX_87!{&aJpjchrr1d3K7_@qKk3J*BbeA%V=`?50BoI^7paQ^KKtsTuxIGYY-+WY`1wOb`%a61CrHtyf z2XZl<6t;M$Mw@IVJPPK!zrXKT;jBI7WI~drFdTe^*AeQ52l0~S`aDuK&$bL*L#)r9Z7o6- zagXl>`WHOO8<;z8>$s?Ci0PUVO0)#4%XC*+mC#GQtEF&f54^Ch(d*$Hwj<)xtx)l_ z2Uzmyu)>SR8wqn`hH`x73`qb&M0b7g`dLucPrK6~3$WKOFDw;^v->b<@53;6T@sUk z$2$yxSHc2bsusZ4#ZO~~&d82XXy(N{HlgvQ>7$U=3|%>A0oG*?eN8+sK-CyzIo0W>6-JCd2(C zqn#*ojI_DUe`by?(y4K&?RnF!5Jw<@SZZ3ESA1_icXy!$OK>sM_BP9tveapNs6SPB zv5Pvd=fJFBxxUnV^eNlu?tgD$RI|+*={2~DO}5(F&yrO3kEFU3JbUOS<}y5oU~(a{ zW!l}U<84TRS4?Ghcp5HqAMg15gJL~azQrG>X5~cV2%yNE?0FpL{~SZz?RusJw2Bfi zO%;)BOGc(;OL+o`?+wPys|Gj~zWU(2$0IFQON%+(R^NI(k!o~lqx9LjFO8I@7yQUl zVG(I=b6-HA#}fAc!rL-mX2)ir+6IdR7x^i1C^YP5Zn;P^PhVt)rhp)r5_17*KA5V_ zgjkqRB$~Z4?y>DnjgW`yL#c%F3WJMJ6ei&NN7Dg&s89s0S#N-E1(N9cFV5x=^D}*%u{Zi_kuSyKGa=0wHQ$!d~<$g1cO&h3RqOiBj|ji+!zQrnS8CWp^Q) z@i8}}Va8MG+DDJ|md6Ay$0_UQyRph!Ra@up@~AHCAXn!8*RIDH7V3_c6G=U8XM{2g zz&ngyb;~nbKNXNdcBRe6t9MEewc4~^WiJg}Yn!n$Tyz+?2JDBhd@sQ*z3u`_Xy zK5;k~?gHSROZ&=wtCRsjnkRw@gB{9dXyNGkRs7mp!iixGyvAm@@?TMVGKK&DQef^) z@#9ejJ(VWQtrb&oxN$dBTMJ`?aH~|eAy}oSYv{$-GT-8&q8=G-pl9#h9Wp9;`vtQ+ z#daQ~_TiVvDPJHv+;-h?$vSGFA6SuvZ!dTd<9Vy4=j&dV1@fP3T)HZ!TA_aG%vM6& zI^ZG+9?3~%*wmcdAm4h+WP9d zyc3IIR2kd6_TMW1ok)%NNT$Iv`9h#lh+IE4QT#t_pCJupC(Ls2)A^R_xh4 zkmrE|EN-2ga^>(JO%phl*8r%nN@zgxnR|XvfP+c>&lXA46nQkiQP9q+;nOg30~USn z-n~-$AFsTQhGEFZYqpYZFlBt56ViM|=%)c~j2kM|Uj(=Ow{_ z4f25vlp&c$thE_0&#?CSb}!T2zFiQ?>_I-{nCKli+6u}ktkDZ1WRE|p-GiK?s)%R) zTanu7**9xN7uOayV(N@HW940nGH!=>Q7kdOU+pMP|0*%6Wd;uemqeWuwy2<{cANQc zR?L&NGWGliMoc10y%WqHrJm40+@z+NZIv!@ZwQ{)d$?IwY_Rw4?8D9 zgQHKMS%#;b{}LEj!mFPk3`cG*k6OS2K7ATG>{878J#-F4r}Un%o^T>)-NbpR)ZPdk ziy^al?cD6)(;&mPYnz1v1};9`UX3>NP`AtG>UrnQ9jJz?U5iKr49vK_TFPs8=z}3h z<~NY-v_T*6A5eP|^Y3e6hV09rJ5I+T*M9jsetmzWeVS-{>T6-KrvWbVT-^6$uQsoJ zXN6R5gYbqus zVj{lE7NHFr)b9#VB}jCbItK^e;1pPc4L-~Syo!M$;}BELh;4~mXz2?hH;K5y;)Y{z z@>QF70i@&xAFfcXed*%>KveR)Fb3|+HMjv%q_*xnZGQu79)#|9&x2m4RIhNam)z$H zoODnl)3Ps?r(P@lQ8S#ZqJOa3#I#|reBa~i!oQ)(BxyF|;?0Dx6#uf8f8oJ0HSgeY8Y`ICL+aRx6lA{2ixnDUu$O^(_U?6B#jM22U>&%hPq>BObbt`If~xu zOu_${G$wrgqOimu3E{b86KZY~sJ>rWPj^HX2@DM-e4whP!|i2VbM?w~n;u=XHF%Mj zH6Un=H^mMQ+EfP7!77=-9hS#BY}x~f^qpy4)c2`bt5|xHAiVs>l4$m&FCSoHxT?|7 zrwi_if^|UyPo16-QBv-8N>pMGBh?nIDVE0Fmle9L2YNgxc^7q_>7ScW`r^YpIF0Ymp5gGAKii7WOnqriG$f%!ou6OA;cgNF z1JQ@Wj2YocF%3nRZ2TdH@XlQs2WzvGY_2)Z{HoWrWELMxEAm zkB{cka6zx#bg8J(5bQ|5xqGBw>UVYNk6(VN@_1Xo}dsN)2lxX?zJ?Fw&-ceaW1 z_jY^~b)lV4ym~dwUwJHBIlsj!+PDIXv`&lz(b)m&ZYU4CKV(}$h)@97wUDE`aeky zXXPb0sxO=CqPzVXOIj!c#0!C7esl7I53+V;rb)d)N*Dbde@kIYQ5RfTWY;`_@-sCc zC#J+O$C*D8!(u`NO!SFWjC+neWJEA|I1UP}Gk#X>Zl=Fi))2}=+j7>BE9s&+zc^a3 zt+Jrs}7Z`n_(-E2z2{*=h~Oc*WXvh>b9(a@fp!Up0GZ{m=8@hI#;&3=at+JZL67F~5>u{yTW?|xD zkh(H{)rA%O(twrNj#H#Z)EtP1Tv;a`SxczMt{+vbdB19i`Ii5hfQe1D#o>gl@SCP( zI-$u!X(9DYI7rR0Sf*G}r#)!nb2ck|2%?L0GAR8SMT8f1xEZipuMtcM<*5L5kkjwJ zkj{vTLz*XvqOuL5x-NyjO*&tIbRSdK{J2_}u~wGyL}lBIQ(GR)lgpwOcoZN8vrPZ7oYyeR_W{juFNhhd*vf@IM+0GzBHxhp?=A~1edu|I+-^7 zM&njjY_e{0tobF~vWtxSj;4shH=k@cgnahdL&oD=T#y=7#kjDli6x8yEQk8419dV;gV=%uzO`Tdvpy^GYQ$+@L3N#f z3>gURHp2M*OSp1Ne~e-nBjB8{)<;+MP|;D4s;T;#bM7oFYcG^_HM+@qp%#T z2$z}mv#8uR(`li(zI@VLkJ~D| znepBNFT&o5y8%(2ex24`<~N2;gD!>(jNsoChiD*N#LBr})Xmuf`mN!7smFI|O-*??Sb}^b)Sox`!<`$V|ef;zZ~}-FR3c)yXgG}XaGKLA2{6aiNQ0Gxpz9hOGsYz z<*3nX;dD(cNna;Eq(_r*nB@8>h00hBxv-j*o(xTn$gbXE-z?sGwd@@ETplKe#kE>} z213OVT8k&svCKt^C$Qx#-B-i2SZan!==FjLR+|CjGM4VQZTRl^HV0h zeErFPD5+Iy^(%&HIR3H-X~3NZJ@_d0k0dNX9Db@hbgB057LR97!er+<360?6XY0#W zFjhGg9M?96|22Z_7Kw@J4s(-szQ%bj#W`}UWL+`7*GSjFLHU6k3hi(P>vf(;J?$17A*;jb{?A&L}R zbo%D7Sz1hu(Dn*9w_UZpPU74u@h7$gY@$+dCC9&p;x(9Q%lBjB~=Us#-P1~ zX0xkM7EI!^8Q zwr{UgSl8)&c0F^lXqLK(1WvSNuYH)(fvYoi>MB~zJ<{ikaT%Rqme`nz{$0|7vGzOO-b`_*y4hwp=F`?Wx7z{$*yGEixMP5TG0?)MEtL1%qe6!~ zoLV!Sd{ha8J=ZYHAe_}tQx8)rz_DRxRhoA!VVCU)O=rK3N5PHTImbUixj0Ivs-IbX zs1w}XC7&6Ao7cM~o`0yXB*KDsSdVP?$hV8TD8>R#^nO!9$2?~LLJ4~TVZ;-sdGPAo zPE{+q#(?l%^;O29l*y5d5ZOzNL$yU)23YWBC+^m4ijugcen^P(bA^e^efKYI0w^=P zMC`P*CFw|c(B^&@+$+_6La(vzG=Q!<$x9cAI^I2tpycNS+>W`+sr|GEu_eH z?1lWw@oZ#g@~&6O&reIkHWTcpC8{amvAG0H3>sBlY0T=}TA1}d$di#s_{3oy-(rP0 z(XAczpqn%Aqr`2>wChS%>cqgE%IIiYn*kX+*Nsl`Ut&InO0MzcKi`HYsn;>(jX9okc(ZcttRc`aEfYMOsd^Zm!HtvqL@CWtC;a2l9L$$kFrw2{E zb_UeXnasOk4op&YkeNd3LRW`&N)a%c71U(qmv2y;v_@ubvmpzv@u8|%|IKRk)GbWD zJA-vc3O*-wYk{1Z@Rr?5KAO>|C((JGxF9^+l{$XjtroLa^E9){Egw>5rCJipr}`+2 zcs}yXhFg7=dpd%2$XA6bi!)x(QAp>X$nq;9)|VFA{T;ZUePQ#G&-8}u$EUjum8&&o zy)U}z46+bLu=uzL)4_P&wLDiUkeKc@*b92*8_5!np4x5P%n*r1zAsR4GTKv<7}ugO z;_J&w6_2)3t&QMbPNf!61b#@iop5e9olE9vsa?aM)T*cWwYzcI%@08q4&6qEvtOGF zLV~Pa&-b<@clE{P9u7pi9#!46)IsGGKO>>P@cw#IG`=o^n*64@iHNh0P$l~#H>U8L z7rto}luKG+o6T<*%`U;OggWSrE^BZ9 zrTg|BXwxmEbp-lq{5;NNc#=`vG*y_n+iJz6?~A7Sc|(F`@}4k};lLVI`#tg^A=Tpt zFwP%Y`reHZbWL`Xn6GUP;_=J#$$(7)=q$Al)j2v-ma_h39zZIc?%jVM{aB*r_^()BiR!f-~E(==f4Rs2uaEq(kFz=bu;{A8it74J_aAm7vB1*RNbdi=9Bp=@SR+$;yGSYkHy`)p^p zByEf!Qr8U?Q?%3MYWl56OaPKNK1IB2cHR)Jqce)^ZenB5r>ho||2PTBmKtz@9hSUX zR-p+Wcp>JgcPaKDlWSU+d>Gz^tU8F^!B(I~5I9OdnirgH!7HgHVJhpXbIBauujF*p zw-#S&_PN_?%m1bq7@o{>Fq0;1xShZ&j<`4MzG^;KaKoosf2;l~rYN>a{Brbi3uETa zdM0J4Vo2zEUUp1sOAHh^yS%z%9@`UsXYbA)|Pv#PFh(%|8y_Ounto3thmhU z0hddMIR>ZaY`$X`IN~%yIOZ-I!~id**eKP2-@Eso82*^-8SD(jpJ29hDH`0X=A-ik4I z0aAkab~GrblzSECEm+D)RN633K=ANAPKp7B$hnoN{cd+N-4F>RUNe zt|{NTnaxSn%5RhRLBk~x#+^4dUMn<{W1aQy@CBNf|2Em=Bl`Y_GY_~$o+j`<&y zIWrEr-W^6@SirGN18UNI>FF~yc1OEo5+Y1emyAw5aqhnOk}GD*Hlxhjd#&S5T^>7Kwp!b z&Hy+p*GHSA$zCPi#IAm7utq~cYU!LhG)91E#v`odW&O#@|1^CmNx^T@=E`@O?yH?Q z+>#!gKKYLqpmUJ07A*Lqy1YVaR{5Cb_dLx*>$3^Zwilx7_JN8GTA7CmM;dOci~H0*|Aj< z$iq+>gHCgl7hL)JQ}s_A_D&OkJY*A=t1@F8V!*p@6zO|_^94YLb}zPW;&#U^%dAj3 z#EN0}<638uoz~$-h@zBz>-TlEM|zqodFdMFuA%Q}2My5m>hES++>6}Yu-Ab{<>IQj zCx2%XY=$#1lN5D@T$MxMub{V0m9KSdxed^@Rc8@uEs)vS!I~OAZLRej+lv=%(vY@0 z(wWC>*IKI5FmjEuMWpD)Ay&qYy-Q?b0h;}Iv)br|a(O#_@=lVHHKmp;n3{S`eynwd zy^}Db?LnVvBOr3j6@^cp#Ca$tIVtY$d4+gnrFbTuMJr9JGYaz=UcGviFl(gDxczyz z9PrY@q-gc90-jhq$A(w<>Yo>*>5?aeWqZaYUwoXqtrOWd%arGPg)&%H7&OyoC^EUo z28ETIE$8lLkiS<=Plp6@BmvU~TBASSdNMB^Z6nNS64R_CI?$cZRxM{pwc@1*B5>n> z(&r&iHlqQfk3M2q%Pj;;%0{&_bX*!|_9Bnk?uC#mPzUIa^vSo0zIh{g{E1TPk+Y7q zm?5jmt%5jP6vjI4RVV45NY9O`{H-k{#f%ow>$)@Q7$3fg9-yW}tR{lS#a5ERP)J7@=3dPk-7}#!h z7gQReVFx@f_Gqn-iq}uPI(%#H9Y-p0I*e(JNLB+LLf2xpS8rjD3AH=*6@b5taA4K{ z;y>taDYJS`+6=qlt*pw_evJ**S!mWen_8_q+UhxbeiufhA|SQUny0ixA=yhoDoaE8 zhnhd>VUwJix_Asmb&sfpn7&`M9sr(=f$#1Xco`MOa2ExvU{F=~B0@4Trt6p0h}iPP zKSj&|q1KzZUK`lB`N#a zv1Z&EwTA>nnojgOiTEwtpKL?2Qh(+=y=v+pNb-zgs`VB)@Ru4`z9kb!BOx863+JC=vKZG*AkVA6y zP)5`|hz8R2PK1C5E3x%CVOBZC$vkZ>hG#&bF`5*2#0*C70JjQdORT-3AIfDi{UXHO zFh~ zC}G%Aieao)#fXBYF$(QoW+*%#6*`qGuL}qStlXdFyH8mk1MveM7qAM^eQy~nc zk}NLnvJ_8&dt@!I-Uhv8CpJDbJI8nCSd_mm%WHr32o`vL2Ao>IOoT`lPz&aT-CkA(U8(MJH+Z80Q!BDpIE+AK3pn zGad{~Bg_p(-UX5Jr5`gp`dJdx9_y;9Lq+RY1_x~6 zIdLxmRQ~SxN)+F|P=!u@Mrb)Sx6kBo7Yc8|M$R4y+*Letfv%tGrIh$#Hv zbSRS1)0N(-AA=p)SZDD{E~GAqQjAgWYR~6y<5CoL9@z=ss-GTl_;FJ)e-}(;biMzk z@|eus@ft1O>UwrFp2Z&=Obh8s)*ClIWw;y4s7GdP$2l4>tnxS(+_d1isrg#$65>oO}xi#JAkje_T4lk8}UDMyx}a00u1I#P}~* zo}}CxLr)73KQ|wF7`H?Qx}W};v$>T+lk18)s;H+N241(4-%Y{)^pn7^jwg@2t)4X0 zsyhT%?U6p*PKIcOUE-9z;)B{s*?AiAlAz!-%0TI?Q67n`P;I$4&n+$vxpZSmV{j!( zag35Y7XnX@WjVHo(4=zCiQNJ~g9{a9{NvgC_!$uTD~8zk>OGNC*C5mW<340fRjP-@ z>?NUZy7G&3H%!-)MPX$Q;E}7AtI$i!(bKz&3>k=4HnTH!g9)0HD(9S5<`=JHs)pd|6w0yVyuO+(wJ;>;70{ zQDh!MUqvgnAf{SX@5s(RCO|=Fz++ZjU6L=Y^~Ip{sO#cqZnR>(!iHhjl2mx!om3Cw zUO|kZnSRMj^!0?xmqEV4C4Z;jHlVrq-sVD2YG zrTh#9<1LH6{OfJz3wj`+`8*BH%oczH)PFcBttYoRg!P3@-oQ_v4h*JaOO>!VC=Wyt zD@F>Ml0wjh+9B>M8e=9+smJHR9n6w^N6s6-F<1fcsT^OyUza}2`}J#*ey5&E>&0{l zkU&q+e5VNG0gEkxK8Z-{VPEO*_v!MzC(RxfD3Sk>f&1PurMh00r2gNy^}wJu73i71 zcIBKkxt;?tV@dk_+a~LFpTIN~sw99rw!4Kk9nhbs9UuP*iFrFS^Le-g<#|wh@nV%S z2KBZ;jvCaYRi%H}V?D5c0P)dYei`89F7F8Sfp>exjNcBQ^Xnt;*`S*x=xh9~IqcPIl82-vY7 z_WT50_IcXlF5TTWAaLvALIn^Fn_XEI?5SHcsR>!Bf7th7I>DzRx&fNARdJMyX_zB3kjcy8R7uQXw$dx9i}} z=~8jT^GOAEu>yXNwT73>!lT?!eb>pQJjgphl;ymiSXoHeVg0h+f^!aVyQJdZdTVwR z5MaM6e1CSxt$=sQ_*7w@-Re^pQQH zUW=t5MI0k@mW493X=Car4hVOLaAS|c{1H4@h443sgdJ}5>`x2{6Es0H>qe(u4(|hi zNqoY9ugeS-*8xM>RhbEK(#(i|TvCW5teaNZVv2&Q*WV|r8k7^J3U`weIrp2W8OPE! z{JmoPLohHa%X%h?+awa@diRkST3LFvkEkH%maZ(Ap;e`*T(tvpPFyp8XZ5LOg5Wig zrhY6MJL>;wgH%0fe4=yz`IX-J0}{!PkiS>8fEqD&Pd8xpt9uu9qs|z*EExKxs~k`> zrX5awl_;0vk@9|)d1F;OV;G<_UdH|np|}%sLYc4DJGiGe-tn@}D&fAeqtP+3k_B?T zfB6MrvDTD+t@p@kqY5^2@Y`;8zD?c{3+>>Rvxas4-_K*AH-*ThppFL%CzPwSkwt}5dd}H1izzE^0G@& zf{&K5B~gX1Zx#5@B=b-PuEM@&CZ@P;UJ(S0r~FsR@7=vE1+|6EcZ`lBa*}ncQHBE^ zcuXuVp;Sew?54yi-waeKi+jap(}nuOkO7W*vm5P<4+D+7WRx^J{+Y45_jP^b>yjN! zaWH;<&|{{x|++(v07g(C%@GTxqN?#-T!dxl2&J65;g7o zs3Ul$hTvVTof*eQ`ZOGs99>}waeBF(@Mp}Y10{kYue_{BL^cf4rzLopq+6W!+F-_ zp}nI18xkPj4`zKovewNP;sDI_tpShbAJm%B;?*{oLmq}@+DE2eeh3$8;kA?}#ug@!xiyexs*j1Hyq{G_6p)FK&3eC?8i^!BI6w0tq7l=F2=K~{E zH9k4uK64D>ba=7#*ebry*ku|ZH^u{goS9$-;UOqj@s^$ojYj$I^hWL0dF8RqoL$|{ zf5aasER4%HGFG8eWv_VVsig)rUCugAV1Z-V{Tck%g*`lT(I}kikC6!kJnG;2w4^8i ziaVEmnUA*PeL48fFM8#&u3O;-b!(Txw4B3#Bl2p}fzX91MU&uIjApJ%9TnD}hDkUN z2;H4y1F?l@ey=pN9xs%D=oeipe-1tRU6dq_kn(BWr4Du|0jb&lpI~??YxF73%E(fS z##bJk)R6SNhyfw8z02gXGLXK4X`a)w z^6K@I$Q0wp62hQ6sd}n6q*F?aw?Zb<-I#*K(PEsFptMh-+1~>Wv4>;~DRW}rXX7}& zUNh~(hYyd9Y;9L^(IIGKBUNU*>aL}Fxi0P*RJic5B_HL~SfH-3nIgbQ`IP%G!fqjd z-opCucfmLPpvW)<*k}d=_|25fu(U|k({hn~b_LCT00-+C_PXHiLogqgen)S185BRJ zRpu#1uFuv^j|^xZk^rOw$dDTFG?^z@9=2W>fix@8L}v|PSq6+!Y#kjOiuqUMz4T4L zb6rILuw8bFskZRl7Y`<$>sU6sY*@Y3J8;;{bPR;k6$YX_26(NrYey(8(A}0}Vh9Ms z1$4UWb5<^`HnrjoafiayNk95m5FtM0Xo#qo%5hxJPNVj$ZA#9LfB|k6H;TXvC$go+ zZJ2ZlFDKcj?sxG4kAU3f4WEYK`lD z4zRJFo9WUCPRzfSsL2z)5`F$Niv)EepU9%Oq??E2P8oCF z5pSp<_X5MTSS2;4(Uw|1QtPAn~SDn*$;=%IOD}9CkI?c^4CG|(t*Y%d0 ziZ3*p6HzPh@03mngHrZ3L-?=dJoE4DR)hVL;uY>Fc5snp;ola0kDc{_%LS#ZGIzUH zw1dJEkX1j@uJO*HE=3}C)uLm&7jv&}xsylaB`FPG<#x@TkS}YWK-yg!Zq^F<1i9EA z%8@7BFVx}NR}s7ZMzO8Pp;rn@Mh5kntML@ef|lpg&{E@Pg?j# zr>Dp5yUpH-S5{xEbm}jpf^50lNe&mhM`o41HZS>*Uu@3EU)GYQ$`mzSy*9% zHKtXfqJ%2=D&Xc)s{x~ZNbIH!uzbtmvlGjkrcnmeXy5&VpYr?2tB7^l8ud~JT$wQL z(n7xnbCn$1dKK?tXiDSg;nyp7hX9-@C>g}8; zClk<;h%V(K>(-fi+=?tCnYYJ-8gRv@+NDg%Gbgd2zKq+cJE^)lNdnCV`oCo>+|v`P@Pv2&jAlP-RmV|h=>p;igmbbCYznD#vNOv?)joi^Xhp5RZ;M4+MQPW~39xhAJ z=lWOexJYY@cr3rc*)~5{p0J95{o+6}P(Ie}P<#)^wo6$GYgAwy{DZ52c6JOpI~bZa zWM`nLHU`h|@8{iu9raD%SIJQ(U4wgD6SM;4R#&#Xi%uTYN5&k8-a zl81rUTSML^i#zo1+EKY_KQvucL@Y_jz50w37AH48eOiT<<${d@*Kha(`CBEk>3yv6 z_4Dw=>F$z-p}P?kP$WdUr6qI^x zx`rOQQy3bCff?dmpnE_2-Oulh^~Z1=9J6B9y4M}ob)KK=PGN_jA4Wbnc(5((u`hM6s|Ij34`FwAL3l-l}eTTetfkN zVP&9uP-OK`^cR7GC>m%Y+q0%I5szHY&0Iiv8L!>;YP?Sp1_6?y&MC!Rv%_-uPM%s_ z2kpp}3~PSV-|6da?RX9FUf-yZ^&nq#hW?uJKO8sV12B`!FOIdwg1+~F;2+S7GSc>z z0(ICLIM)v>swIpARd-VhK!=pUzTaF@UN;YhR5lzmqT74O7}M0Uf8H`P@s_rm&@3|4 zTOUEtv~5jVJ6c#?E2pD7+`N53BsY%plRA|M)DA#p_9tJDO{wA5+uO`CUZfd% zu{=neC|)W~*i7bqukyKrMIQC=7sdrbhHOPyVQ;PEb-%u9RKrHtX!-FOr4 z-0b&u$%zVh466JOa`=L|Sr7XphZ31O6h})3Pcf5;^6@>&q?SswiY8V~e;jTdEN`|c zDl9RpQ{+7l2r^_=7szb}rXc*HdlBr2+V7ygx&u?h9_z*i|FdWPzWNrDT&&-L3@09e zLL53ENOW-$qD@Cf2hcvC&?`S<-Vs_2m8DH&6McsFFPN;@^~zalV%tOXUHbS}Cd_`` zG9)UzVsCe3S2+~rk5Jy6IDFj@5%TOX9B_5u3Cler#0L^e--LkB$Xvwp+g4oiBZWu! z@)SDcNi>21|mvA3_1yHOA5}j%92=>LlCZ33hSx~nHX9D%XIjS|8Z1tS z1XzIYl2UTDwLLK@yuxvj+mFc}nw0%rQ+Lx=)s6-A|CNQ~z3g?hw&Z&BiBUhhKBe%2 z@~!$#3{X@=&>fqnvl1l57zPs5u{OWfkFp~2hi|^$eV6ZAH3ABh=g>2ad-^P4xN4Ux zJUxN|h>8gVJhtbqj=}j7&Y2k}Ypn4;zqLZ{1n5Q!)@dWB1Z*7pMtp6MsRXkUy3@*~ zL)dNOq13E>wE1%#v>}fXZ>mpe_ z(9h4bCC_G_$&KZYKW3sIjo`Uim`4;5Q~3d^BX$wN6n{6~WI8Yo2op<>6~Q*gq4~YE zSJfGX8X`BO1=J&V}HZZ}8LZgR=b!$=~iU-{q9qWM_;MuH6oF`;_m zovadtiaL+JODlKvu#~Y^72jm&seIB&+}5_F#(s3BATIpd@LRr3vxs_r<5z%RHA%@R zLXf1*b?GDNBU_WU$cjXsXYv%44j@|Y)Z|FYytSb`a+5g6`T^sGxiefIX4XsW(Ia4t z$VgM|Y2(+YbMlcq{(?2UW%<7nl5+rN3P?x-d<$z2@A=2#q5{7H2QXJW&ip^olVl7Z zd+Z+!d2JWs_}juohl8*LVDWp!;@!{r$^owO1s|Xxzg^;3L(X1@Q&ycBBTu;$h)EZ( z=a-$NmK2RP`u?YZ=(r1l3%(+dSj=`%omQ&xhUl7OMIk_&RX#L!NyygCGqy5u8lR0Dbf#y7zvXuIftISg(1ZNKzHF-XkFp%rmrM)KDdtNU z04LWAeTEXC>2}0l15+mN9$21~)Sq7LoEa}_&XbUjyh?IUXFW5BFD`yd2 zA(Zh?W`P1)0RJ*#;H69`$^HE_g*PjGqNcio(PMWlXKu}n@2N=aFXPtnPu|oKynwy5 zt83(HCtYdUflPB7BO>2Qqu26z}_V$@bgZka^ht@Jy5)hC$= zS9Z{SI1ePzJMu0mM)hRHjfbb808rxfZf-_e%G|h^o7SY{GgCDFm}A)iE3Pk@z#2h? zsq2jHrT8(D)f%*%Rv=cKP<57?TVdVziK}S&tywL639*_ zzQ@X?Ex?VlETd*B{~yz!Cd&vVpwZJQVD&UI*NeW9L})SrT$_1<*`PM<8y!_oWbMu> zAEOUvvccz=cNsSD|dQQ(~xmCzT3DUNiYx>M3aZ`dJMAE61#;RC4u zTXM2kCz59cSVw*<%WF|YE|Hj*a5(uy`Wsb<4y)#C&9YVM^yhAdZ|EX7@Koc3$$Dxv z!I01ySx}w8hKM7oJ42>3Y;}Ez&Iwe`y#ROpR$Ey;0lygaLwfVi^v>{LzODY3@& zWA}#zkLKqIGVf+%d}njMwT*VfLWHm2IX_~wTdmi__H<~})2g0l2zGVD>B%opjVu9s z@1h$V?bZh!t8e&Yi?gGw)sqg#o)BTUnI7UKl|-zjSG?mMKI|>1#%98u8L-{tQ9s1!cTxOa}CXt}6oNu_Y}bUR|qA4ta*InZ~}EKuxVa ze7+3<3?Ga->toV}l~32QivjJ;OLFDnkC?+Gt5R3P#j(Q8$o8KAavrc=w{dL(kq)lY(Gg>I@b!=tg$$HTmR!OjRd5ZtK5HL1_AGzapN%PaV%iu zEPpAs9WXye#W@pgfR&@=hkR)Z{zt)SQeXtV-)0b6#U0Juyl{AA*xj}-$3Dr#|HE5U zmuuSmw!TNpl&wCB-9%D`;vf=FH0{@aucf$S(Qr$>M0)^Hw`l`7+ zXH8Oeq%>pkNl*}`>#6^M3A4&p)x^M@3V6Lqc>zVv+l42y@5giQb2Qu2#ll)~ ziw+5u-0D&3vo?BsLj3+Ho7-uXJWAD%K;r_C^VVlwY9GDmj`+y8XA-_;R!zS6v2QSV ztq`6-=~#9%#j!!6(*KcLTdj0xS@B{)LWcrR)3B)k8awjpx{b-ND<-#A)Dr%%y%rPx1n{=jGfvQxD^HF8R} zIsTm->N6!VQ-0{M$o*{m9#5c8?PH14KJg}=U7OIGLm_xV&E_oAr!k#3uaTF68fcrF z0DxlkG#5u^zQhgcX-68NfE#J|cqjInh(_?y-eZG7ldmgApdSA zyVOFp-npFCc4ABqpa1?1+9f=1-*9ge?%vfB?~&?~s2x;*H?Oyqq7LLuxs;&zlvR zM$@iW_|!%AoA?{Cve(d~_qPK}F@c5!9w-LV;|=hhSHUY_;`MVm z_P^AzQAro%fDi_sb8Qa6$r`tplMLifyKFrD)o8_ZC+Azg#Xf7))_WShwBmv=%xrA+CK&k$1;eWLT~k~ErFVv3e^aF2E9&7PjXMytthsFtG1M|dl|$grUj zJsT@OMwa@%wz1bSGm?M^+242cI-2!t?5STS;t}JXWrC}EGNErB3e!w>zb|QIIOX)IEX>Cxj4Y^W+_dT+$DB(l zsdHxSanTls?3|vmF7`)&euavXxj7RLt9M!WngCtKw}6`MCB)mu2osAkAHIg!JzAH=nlCEPW` z9KiM6s~7zho{Y|a$Zlf68B>&LNrOVlwm^ub^R>tyvJa;X!6_rJF^0~L7PYYYQb?BFO5TOmQI$O}VE)FP z;NV~}CcMR#qpChKGHUQ|_5>&hX6I-T7x>8d1x?WQaH9w9NS$U(i)yO1o?C20XCXfyzZA z4MsLw^fRIWVOhrpS&+URo;?79CTC__nrscH8RC9HWPx%qVtMrS+Xr*yXobG}%SzrD|wjB&^`op4AxUj1DZb4iGP&n?s*cm?Yrsgvo?b% zIaMDcWBDsE<5U34HVdMD&47+MtZ{D?5jY=YXZhZzU;ejCsHNy_-nDq5SbJpa%u8TmUS} z4lHirz?6=(g`=+l6$#xOAn+mMkww|?2bg50DuK7W0te$0|@({!5xNh$KzkQ#C8mKA1}lpe>A68Z6@xCleDtHSa$XZ=e-0B8(uU zc2YO>a2tchPS)`g48N~3+2N7#7u#OoenczMLoi~_kvYmTX-=2ZWj73XuiyO$8}6-$ zYk=FQv^HQKHi!rya!tvPSa(9m5vyY_L(slj_<$a43;$hQ(k%pb!_r zgq{JRj2D#)KNQ08ge?>UDR!V|v(BZ0www!3>?sS9;M@uj7TqTnZblLdHMfD-9X)s( zsc~K!@^E{89|6pnz;p>&4xB6OQHgk;XD1an40=;_c+;bgf9|OZi~_`RL^%t0Ga1p% z=j;s*lh^d$X;TY|4v9E?IxR{LkZf4huS#lIOCy9u?D8MZ|Cx#@LVNenIe8AFT;d1j z-HCmElNgWAd#1Mc;G@1O7GDs%z|p68s{D@c;JT`XL^zB0KeK06c{w*)9trQeQxfOZ zs06g@HgO$i5fx$#R&e5%^QpSciz#^G))Srf0QYRmBv{U7PbevOt01AeXs<+kOolYCF~h}lt~wRGf0HFBMjB(Is{$#rqci)S zSq&#Wv9+si!ay_*J_;l%J8GJLljS_tV9O^)e&1?i>C`NYrFYQUh>)-|`6aEW2~R0n zwf|_h!l@{r^%}`)6?0pwK}~uaGF0Vr%75@Y%V6=LYF#2SW|gZK@l0CJ=XeTR zBXQ1PSTUqTWo50%a@bYJThjMqmjGR}q?QEYlobJ05q7w=o?*ZHN*v_S#{!PH?xD(! zNYUY+t%i|lg#WTLq3}7TUrSyZH z@Gtm^=$Mq z!&R;|ALt`h%cit`%%5USRKL6{H!!9GpQl!+?!$UVC&(vBUjdqLAP)2S{;tSFYrdk6 zZr+MA^atY2$>LbqAsGCm*l0#|MD^CMlC1J|Xrmmp!zMJ`uudgJun|}j;B6RcZH4Nf zUjqQ^3WFKSA{Y%^Bi$~ZJO&87;e}TcGIHeq1}C;Xb(gWnej16Av;FnK54k_jA07*< z51TD^Oq5LID7m!Y4d1J`?%|J1+voPU)ax7S%~}kkAg378552>A8QrU zA=|Lc(X-f92L^491P%EJqb#W8ouSF;;*J-9TKMpmxB!>z)5D8gFkoE+f&+jipv|_G z6Kig&pq)f*HZ2W;E4M6_Z(fk~lF`iQde_t^`j zFWy`K3#l=lsu`8e##T_=6DO^pDp&?YuWh66gP+Fq@j}JA-uQ&^Zh`ix-^?XM`ElPL zOVk~mnwmO;bB+R5Bx_Mg120Gi*98kl&*oN$q${QaHv;eh{dF#pX+b*Cdn0t4lQcQa zR!-XNH%CCHbfCph>|SGST*H0fyHXjK0K>S4j(hd!M?@rIIrnqg*Fw;8@yic&0b}3c zu60VfrW_PE3-(i}OJW0d!CU%kuTn4loN+XFTYQ5iOYcP+>a>BGDy$*_e}acmSEW8c z-O?AH)#qoFSuuDKv&VD&aIsGLcI^k1ak0mF)$j|I5!K_4e~8wPlC}Wp-@Etkhd7)h zMzu-A_6yDuAaI=j>yc^DY4nBJ;{ zTX9~{YF4;|zoP!_TwFfsSE0Kpq4_;J)n{B-Oz;X`>uXKTxCtoek_V?d+h#D^jxWUo z_y{i^N}RgSHj^D{mAUamzg8e2=(_UL%aa2`&*6rDz~PQ+1Ep@?Gqk&7Xn$!k74{m&obJD?Y&=CSw3|T(P2baY$g3 zasgn#2DRC&Is%i&rR8E>iFs8Qd0k+8Yt~l-WQn{tk59e8kz5}k$yza^n+~MjO1Aqv zoYGJ6AGfnuzKuAlNBz{&&of}zVEPQzNF^~UA-Ln2Kd{3I&?tR)w8b)TnB5vLDv z7YYzSK&=S81EsHy`MdsdxbWs&{}&ZOaRcwM)1LXisdPV=0nFF`iF^DQvnMj~KL`3h z;E$J#jb#ykxG;cdNg0@@|No&NFDW_7HT_Iw5-F@;OMBX^(e*=@od1bqmQw0$9AHLG}V^eA@ zI`0|ONu3{DaVF>kvJH4~n##&&%OwPRwvtM;eH&TCmu`b+=#OLIp%s0+4wBxez(05y zAZ|NFAe6*k-Myc1J7vf5#YGnp;$PZ#ChAm*4@gJyN7$i%d46h_qewW#%&d+$n?dSi z&~S;?`9$57$C*p+r2nS=sj0sDRf0sq_rPX+0(Johf$2Xyu1cMCbpDI41q6q)-!h-q zc9c7PpdosgH~w1vuO*Dtq4WhY_c8gGJfMI_k>hmjN}j?c8mj7=`8&FcHl!!^7t$*= z@Fy*=T1{ZxvQdM^dYlwgscIMHfIyl^ttv9SFG1xLe_?XK>lAmv&eL89b^suGy`JMg z%s+cNohW~{!i=(!K#@k$AO4KwLRny9d;-N)(>s*q>Z{Vcdc&g8+BL9MWfIaQfin%E%izN7) z_?qi~lU_i6u&)r_huX(=yv71L-?<5wUbX303@3yHizG-5w76AxyK0&zU;}_#M87!u z^h|c%)y1*p{|)3Es`@}GSV7e8FMkK9Pv+{_!p_Nc(GetwT>Sf0>z}?rPYf_?0e1rU zlnDXw+E;$Y|GH()z`$@Jxd1k)7(i+8ZH!0u&7V$zROa|zkG^s%ic3;dnF0zugAjE1(qPh^t+!b{J;pT--U5BCa@3pJ<}c$hqvDgMtARq^be!;veJ> zc+PLyOFQxW)x}ihViTyjW!1NKG@{xtTo~f$b7UTQB4AwC!=H#X*yU~FD{M#DxsEDO z{8;;*H2AMR>h-FbiyhYBbMGe6&`)9rDkuFBWCSQ0g1CqSN=XPsoGh^|92mNyh_%i-J%i$dp>3S| z#7sIv0)C^-uZ4<{5u#djCw zj;_i*Ni1zRXhM)7$o}WIk@plu6itgu(-I$kJv)K5>kAbY!$e9d$mgLZS`Lczv+o>@iA7WBt zfPuqxUKi!rsm$gWm}!el9X9RZXc$$E@BBHC6!%350U<32zk7VKHTmiG=fx_{?yo<5 z&*76~R8;<*gYeV)1QY>Mud6sPY=KM7>(f7SnjN^X zdCgb?b}xDi=x*h!&13ndn9((FjIjPWHf~zRPawqFr-Xeq!+9){7_-X5O?}t3gXC=W z9?q~02=ms~DH(-%5B0UjjR0wvsnupS>MBidio13}?4!$q8+e0{T0;c;q@BOUq>72M ztCzIWcL)E27Vx{GtF-~~G6VM!Db$Zd900|PdN)^`H~BK$3Ki}KZROe17ABfVaBYX_ z7b_*KeLsmdp`IZV%wIqYjwgD^C;w(_G zW9dF{F@GM8;V;jpzeor-fN|~f>zC4+cI)gMs+8BKgWkIu_GDcKr8GJ-5d(451sCU) zzL)_6o9gr2mY!j96DE0TQMTW;JXTK1K=dU@9I18}dsIOd&g*w&$@uJAMdxjiN?v)j zyLZFOI2zgPRlKd*mp_FHGb?!qVU(p;B%NS0c2~lQv@+e>{iK3U4?g-(r4Ktc!NSWBhmNPz_CT#7 zUvFJpfTlygrc$_s)vR3pLgi*EfNe>y!O-OBv@4Hb6ikrMFidu^ITJUWV(v>2vZR0? z$TAb+%1&nrxHN}dP+Ss=+mN%m@Qxj}UX#O2&_c_bl~b49s>n=^i!y~XU394J({_c! zp^LWC4j-aa_PY z-;=1Hfo>LrTKTo^vntUl(fDGO19jl{{bvdA$vU-~UBR{#I2(|M-t2P^ z2`r{bqW9bd^6s$V8?jeoNk6#lRrBB;kpF}SuKshKY^)xMLMuhzu&XO6x_8g6u!-#^ zCsl0gy<)^PhPChK z+O*m1*-563MNAZd4Q>AGo38gCFte;UEvgNvI;0Hu1P($XdR%XW8x(%uWCps18mjU)Vt(7d?_S_Z{Wz%M)ZvtaIx=v(o#QkoI;BB-C1C4xSKwxQK(DL4wBKjg zJN8ppIuDLV#9A0$6vn6uf6%xS)C0d06=dsEprze7`b42ehl9gW5c;FiWxvy3GNdOk z^N;Xi!^NV&a@SPlvq+%br+RNM>$2JM6(D2QE~gzm?hl8UoJ`zHP8z?$s7HL_3-geq z=GY`}5jtu}?Yr$`eUwgJae|J4Y;J!tfY>Ggeom)G2o-f*4P+#EWOmczL1SksPrx0E z0LA1oaZGu&Bxj5-t!L%6-kF5XAG()%hp|bl7y(oT^~DW5t`IHJsG??a zQfgBHnd1Wrb{Z4xRUdf{1BvOa1D9B@G1Y)Moq-LgE4V~nLVcan<&yH|8q%%E|m>UH;5>dnQyvRUg!yYc+@K2)uG zMx#)B7C~57k+uXElDMJ{VqWQa@{DL!^i>w!G^s9K#Uv|NW|Ris3K*94csr(R0MkuxWD9>cNOQ} zQlhz+W%(iu)x8zTlNr~I)gP8{FOg+#EL@e;T`>fY0;N0!n$mzOB=Ngikv*&*W@jBO|Z9FZsvOa`vg`e_crDc_FNp~!3v1)tQ!aDv40&hZcCP>-R-03-_L#W$C3ne6|~w1HNXeXz!j0oiC~ zg^`w+8f_0$_3n`N2}6%)B+n=p0oC+OKZG)!em-h#r7XGkOE18zbLsHPU@v z4a>?;K?gpMw)Rl;`C8HvpJ_K$?&!O|J2EagBKB^08(KA39ioi%Rhntk>W*v!qn8qk z)u|j=SM^%4;q;dxgw1q3SD*~&h*>~zI#hEq(sMZjJ1dyEwhD)_?WB~u{&4sboC;f? z0?5#d5D*dc*g+(xxPG52zMML_;z7=n%Ksi`YFvg>rJS@^kr4n9)*zR-IoT3t#U|X-E73T1l9D?JSNzR zDkB@?-v)9=RN`KxpP&lTlp^BWaUzVI`joLws?8Hx6*UkgGN%Cq$##FaOllhC-4sf= z;M20RJlY2qOY3f_jt+H7Z0A!b(?NK>Na6cB3~O&4{Gr46kj2W_GCBhM_+HQ*n0`R z9tjzEFRX?zIm|vyrt+u4_bjkreuoeg6Q_B!Rq~(Da5M3Zp{w?LL!%s8z zd^)?C?AXu?jZx899nP=!*C$cMQATj>EQG7lsd`DC#mr3ad~|#?t9h2AZF28n?H^@@ z!R|n~gvL<4>Ra`Z->FxA`L+dQKRA7+en0cKyREpkPSEs}_^ZhM17~-jX0fD4&=&Kl zFsL};2&mzKltXwZckKpD8wNKrS80AYo4sxTtZguSF;IROHZF>&Vnzdi0RY88& zQP`u&0{ZD|yLz0WWYiW7#>7V6J)+0FCUGUPpTaEfjBqC5uCVA`%5YOm=)N;I>i3>q ze+AZyyHMbc=g(YM{pwxVs<2xX8|GI0ZV7YD-h==r|7^q+;qR6yr7?dt{g{##VJ#+yM% znA_-Tp}o{02K@lQEzR0*p}I{+X~QRcChEV0m={-Ki&4Wxw$#f(GAo0%`JEi=4`CVu zF4u+Pz$uLslum8RG}=v{(nj+eI@oI-J82be04y}Fw^L6ebbN+_uv~2qjkmLkOI`c) zZeJF!VX=7{hMpy`l-$+qGCr-~DB!muVWZ=gTwr!1rRMao{~B5&s8)CVJcYVuZbBU< zZm8j6a+d8BTYjrhUBG6bMC-Z<=Ii~~7s7E| z7!QN%kv2IW&VWkMifTCGO(6LU8+da&^t1BspO7KGtpxb`Y;8EeH8P|m8r=ESxW33O z3DTemvP^abY!Vnhc^5V^sJI}1#eLF!n2QR3kF}I zstSaXY{Svsj0hm|F}|QeRcOjDv*{#wDa#$iKVe?*U^zCC;WC8$E_q&(>w|Ulji|rj zsioQ);S?tsJ$iEwKRJRODDhnd zgqIdM0un6M{f(KUsMK8dd;|973?JlZf^i1>g;tzfxsD_Pu`r-KFMMj$ac|?95?gcN zu)(>Y!RLxp=N|8j`Z)&7n?^GK$1}BA6z~ayU|NUXC2rFOpagiPte7vfwgFfDPK>}V zA{DZA>-3dVsh^@aFcRykkJH|KPhq@iVY-!^)e^y2)zOMz;sRZNtS{CIq1UMuoY>vDXoshRm6)tsps z{fJ?B5phcU#5)t-iZ1qa@}}lqwjgEgzwpB9{t0%v4i`pc`rfe5>s?3 zkNSPofvQv3ZCzPl@I243A~w!{-S^@5vDOdUcX1YsGAaWtpPCPpNl0pr#ESdr1@Ha% z*7|gQTA9E`(hy(bR7-8GNrFLTR zJnkWCaUPwjY5m+!9LjCDP1>|%Qp%6B{e8qUf$b>vwHR4Hkp8{P!jaT3jX4Gnr`)`` zvcp{|^;xtkuJ2nkDR#Q*&Kr}oGi)IY3)3q&>KtibM#T37%I(d=wUwaWw(taFN&WQV z1Ff9{Ctk7Hjp(&L2|xYjt*sK8Fq4sp)kz`Z&-)suSFqk1B-6Y(d1&sPm>I(itXKtY zU)rdX>M~^%i%+LDmZuxXLaR#~{bDuX8*@VCSCwsUUyr@;J8!(xssi@&SH@^=x}ia{ z+vxdpErf^gd?6NdFuXYPhkIrj8*LqTj~pzySOQTL-`0dFGbS>?h6kl^N!h|K9@yTX3c%;@=-5QKFc_{73p%s*uh&5_9y~O;j?{Ubdhe=m=^!F{mZb4gbaZH83Fcd-;xUQTqDB z(Zcgm`ZOB(#OwRI7t%wT*KzcYh0dK0)gxl&G&%2a&7Ovjq8j}9jTT2_rHjvy!Gc9a zHQg}m@fKgBXd+0<$xd%F;J#>%N&;Fxa^m~@M%^sh6rd5%EOTw-3@b@K%|68GAMG0u zpm_y3ooDP0dI&ZR)Kw6xP#vkl;mmg5b0XcGJnJx)C%cJvqUlgrg+S}Wfe}ZE9J0U_ zk^mjdH;T<|h^d>&VICx7W=IBhavYYO>6rF9$u2$`1LbxZ=U~Y8u<8r&m8w7wDcNGo z0~96e3{dQ_qF%Z4^{I#}>uL;W?f_Yrc?5VeDOzjaN7w2=AC*#>&r%Xv_a4W97T&33 zDtY)O$T4?Ip;W#`LNg~3Tp@AeOmjh@u#Xq#wvjsFwB9YQfwlx~E5d6tU9!#HUSp=Z z%Q!!;ChdAt@!`H4iZdQFixm>v(%^G)B9NYXS`OzOK* zXvBM!^}uns(+LX1BK6R+qwxz=0#5 zQTMpj4>Z0#LJ2?Lte<-eqJs_|pl&B}XTK?%3AC%SYVpvV^G0>*&WduS=fQ5VghzKu zp+GB_1pI=tgDWhmVuGIw*fyuGb=*barUz&m_ySgDl@aeqNM)W*-uzvPYD9*{%(7zh zQh7<1heQNkH$!N@VYX6=Ju=0??+-w?Fjr;s^~ZC26t zuVZy?@&}(r07(|B`eC!5_A+AI957?r_3k9zD#70g>DQ^c^8Y(1Q-A6S?ymX7;W4Q- z?|XhI$j@|~yAb{8{zEHz4@o1yV2Da4n02CQ^bl@5=Wo!*F>l@~;mFJ)BkS@e!|G zxB~d(>fWVZ)^;UNf7Rc(`m{T2;TO6VzCJ3dc6+f==VcUAn8I+B@V2e{l=%ALFtv_5 z3n3|^vShrW-5q4LWM8MNe-x7dE9fv~N`&?I`zz~Yq`BX|o%t?I_n~K^QI(DXALuh1 zw9dAG*gchk@H3@taio?warE%Xr@q|dE?{z{y|5s?&R-RYJr|If4-mr!66TH;w<{veW^DStD zY>R2hEOGK*i^WQ5a|<$d2lMABk?zY<2>+bKfLR(Z)b1CF+Qkatl!f8ZXkdK&LMiKj zi0p*1mbpZ=J-8~93R0Ms%#IdmQyNO07wt)@)e{m>$PC^5I=L8)ich$1Xid%{y?4wT z6^_VGDS6EpR2}(kdWLgww%lrM;axnTVhdpIuIZ zoj{0vj8RAT#X5=>{v0eP}x!1W$A7_`vism+)Ab@Gp z;&w*SXl1g0q=@o4#<>>Ph8$8rLXFi@dEz9)FJb-lq2+z@8I*q8Vq=29C2hF5ogQt7xQOe|{eUySe zRTR0Sb>}6~vuBzWBo;iM0X-B&fTIQz+7h!C{n00UeawVzyf<{6DLQqwzxe(62(WnL z94=QXxRj5(Gwb5S2`l?A&=*v%K7CScL8T8{p;A-M&rUz6v&h;NwVrP#E|0d8B`q&b zVBv+$^y#94PfxGC$J>jLe2T{zD?V{<>`TYX(Kttv<4=6=HOajJxl7X!t>5o#RFz=e>8 zkEy!lpB*R^>TB?KqBOv%YQu1VuT(v_t8@LQWw}(QQYpAD!nPy?S&XFuW4>(&cFm_C>eDm5xkq|X(fV02A!KOuo42}78F z!+1iT@WWdohz;n5_v;BoPR2S`a)tXs=WeO9oJrx_~73a7O1^ft){=W+i^s)yVcnK?Yul=Cq3x9tMf=Jr5D=i zO5)nSAmL)b{(bkl2x#Ht`v}J+t$ncT4eq*<%ajl1Jvq<>e`*z zYPWTPPal6qyx|-={fesMe$21^ ze;kT4M=3jMriD%d?EOgDam)*>9+Lf!rfKb?2c3dio-+NsclWMZ?BLbfZ#mj<(^Bbi zp?mOk?bHNgZ?k=}ixUgKSA22PBlYD=_lmN6NgSi(Yi?6mlLQu$)Z5wZNsKbxjFTd{P{@f~OtpiFMXgcZvn*#Qcd@h#ZTRpU|tne(wx6j`TX) zrwUa%z1EIm5b!$|3w(OY#e)fa-&1s`Q%;#_-xIvL)c(d9wpX<#aiwC=x7ne`Ua_zW zGUcB(A#jaEAoG(A@tMq@D{vnwzxU>dp`KO*rvOC_k0k4G5SQ z^}D~a(aqbJ!J+(1lDshlso|G1sj%5k=fT6IOSlI-pNnlk>DRCCQo^NFLD#v&jvswh z$o;nA`$Val#35~XJia3mtYYLWKg=**?#rbVa`w%7ooDpy`)> zgzXmkE=zfktcPczBB$!cJXOB)QAiHNNI%IRrm^aE*ZRROLRPy!eE}Q$PfU9h>Bm1C z1tGKay6u#4)7_1SxFBzy{6}X(!o=+}w4s~=@5}XNgzCm=gNV;=kl(ETJl2wET8%6v zd(B3F^<#wQdS~ZbW61DY{^hCboAMwJ$u`v=duHl3#x4;JIxPTT27coZY+LsdB1U|OMa@~rA4!TiLKPxlr4Mc ztU#F=ctQVcwZ}rFNgOyQVxA>^Kkr$W&xRES3MNyN`y^(|q)}GO3tpD2f1cUpV=D+{ z=v#V>OJuW~R$eAoJ910befn#=(F8x}yX@bW;pIy~wy>)eoar~`AT;V6WTGPlb9JD@ zP#s9t@1F!EufN{NAIO4+v86oa{d=o`KaL_%mOWL0|B#%aqs{WJHFA)&e$j!^bFR7~ z$01GUI~s%6>V+Jci97E5(#@S6uG=jA`)n@-Y1^+>C^tMii8mEnVQml!+us(C{N{J+ z(nsiax5}R_dr{*ba`LbSKl7bFkBTza83yk_Q$KgA@+;idxjSGs$K+>sA%}5-QvW`r zyM}97w4U|U;G$8<*cr=79J=)*REfud-m*Fkl&$$qVFkTiLmMb zIXL$GPQ57V&5>~7{f{3v;>m-5C@VV`c_e@MC{}1d_V;#YQx@EK9|V0j+8_Nx9FVq0 z+7Z>cni$c(HCf`^_~b|QX46rP`QBaOpKMntuU~l%?hgC7V`k0z_sZPOk&mCT9W&_9 zVnmE5Q$E*k1TE|nahY$$$?cw)oXjmQG9;Y+=64i%*ObHUg_GEjOntX)bxPEHHf(5= z`R}{^dqu1HXUPDEjR(g3p~Rt4{n=T8E_7AY!pbUq8852TR9b?;zx(G&;;)^?0`dQgv9kb*vuWCOa0u@1A-KCka1ZWo!QCym zTW}BV?(XjH?(S}9Bk%iv-(ROrovi|jg576!cBZHM?yLLZs5y)rqowIPd1WUO#ipmW}!YN^_C!=aYVSEmpYah#!q#DDm32DKdWu*Kj2{_oW$M~5#v zN}_^uL_zRUu0g71#gOu~B6S`v?hZ~VTALu!1)d5V2H8X4<;*0M zmn`Pr2?y+XI549$%k!`@jzUMI_c5CId_@_)Lw{VA=OIoN!du^s%zpj*cd?|uXB^-< z_Qybv0EX*%kLTnuvL=<%(q#|V z+$9s8E7t!%o-Ymu_qH40_QruIe{KiVCLuB;qdapsd*O-mw5z=}(3HRVE&})bKO<&k zg#fAkF5i6Ue7?X8*}IG|dm4G95GO$ZBhiguPS>FW*;{>H*8gKXV(Wm_M%2@Wz&(YL zOyb`;-LclGGd(+CdjT;I2^TUqN11Ekf}4I@f^J>O@ih6L$rP3A_g4LP5fBwc#1P0G z&rApjeu$11=Vr&8IF933y>aEaY*QmPZHPEcESj@~`R6ow%#SSC+^Ak;Qhjb841ePU zSV9AXN2d_4>;qxlKB|9LwJf(C9i1DPP$OwnQqr%6d_HnU0{`xHz~-y$IC)pnQF6k6 zczm4A;_}mNzMg^oj|Jq8bF9xo3#`8^q!;s4Wz@2XoDv3b$ZnYz!#zfj5N9!-D?gbg zKA5veo#+EB4Hm;4y#%bkK4x|OD<%luy>Db>q~`AA=IP|JqUL?N4PD8S-^B$W9lY=8 zK=fynlMY`o!j)T7H+FU+e+rNs2|~;`zPc);BL_Atwq4l%h)|}|(x*4_;t`*H6cn7H zV@FGDy|s~i+To;p?Ng)8@J#oY;l=dL=imfdCndZ!=j6hDqe?%#5pnSS^O+cA5T$A@ zszSA}yL6h}ih-{c*#ca*G38@s+2z|~i9sbQJZ^CQO;tKv%eT+wnl2ymXA*v2i0vUE zTdk8)2SlkitgpLl-`evGCM@C^NO`95?K-wCwpD(bSmWnN7Uv3|;~ zrz0*!nFts8wnx&aj*&?P!udXjt~r}~03O}s8{01W>x}UX{~{Qtx!+Ne!@lxF+kfTM z+=VJ)!{p9A_gXHQCi3SJvy?)U-nGs$A3HF2NeRv-z;3}-Q$I#sdw6Tux$K#=9B$Lh zZ=QrA5-)WNxd0Y1tO7Tv5lQLft=0BLSfC2`BPI*}OF2W*pAB(hjV7`+y_-?gEa?Vu z4k}7mpDilErFD7j&;*6LwKcB1t5&_d+9R_Dc>jqr@;XX4q*l(uUL*x)aeX62dj|cVd6GlH1vs(f z%q-TQzC0gHxLkY%6|X)y8Y93b9K<8QlZ_h z(!2CR1>7Ma+Jq9Es;t_0_0x!Rg-b7omOf(5WFJqC-4Z9L)|Tz4u{#N#F1t8GcBN}k z!XwXps^67>hZv9a8PxPiHmAFB|KdU$>TVKSjjEfL*0<{^EJ0(mBIud#-TS3QV%TP= z!rO{2t830z#3ZMON5vl2p*HGi>q4oO^U&K>TIIw|6*#+%*cCnYv@pFtGe_3 z!kXWlFZUB^F>-Lw0G=-dywW?|?05zbQ58JE3q5R$K3Akrz&7^FNm%XK7bHDUN<%11 zlJebP@C>}LNaC6dvHS7_=&HIuQq>?SgLE}RV%V$h=kKdgLIZt+c5?}KTwmd;AVGQEo25efEDkV~^*_#$2xcW*|z)e3fIn0iW zd)=ZD%(w`rUBb|{p7#X_d-Ph-pr?9^2`MB{=D-zfc;qS_q_;HfiJAg`R@X%sLM;*+ z6qrGE$?85&Q}-OM`HY8*`!$d^4Gl{Op?~(vc$Oj?OVv1hh|5Ce+Q&s1D^_PtAWf}t z@gnafarTW;F>rxjbTN-R%P{+*`-N}TA~S_tP?OL`qRzwJXjo2jbDhkT3}+;< z)hBBOd0=Fuq%M^w%^Sb%6*O2~;Bqg<_Y%+m_&&AF_hR0YgN?lW&n_(ZxJ#O#)2Ua8 z?aPmGsYB|(mmksK^$q`^NA0l5LdE4NglksranQwiI~=DH4f1sknT{BBAAmMnE|5+)muAq+SKSHyn(Dp-b7 zo3Lx3f70#8qXmj$wk+ukdiKx6IUZptu+>ervs3qr!96vfBh$dLT2PBCYF!I zIEP4CefyfeEOmX{D9~3qw-I`PsoxQ!M$6d_afRthQr0P8x|-)0@p30&Aq367-`z~E z(U_bM!gPe+p^nw=@S4W+^ISA)NsBKYNsxfDo$ezPY+CHGQ>)!18+m4?5gN=)xcpKu zvtH?(k+h_;J>pgG|FzT?Ao~0b6m9 z0XcY4v{sfZ^=U%gMH+biWB%fEHVs7B7bkZ%3y)=?oI_ko?)Hh3SDHHQ( zDz*%VL6bf#$;j|)q z`}r6_@xt8WZGdMtu;61uaXBTMlN%cHPS~_Nin~=myM(F38eE-C+K;76YQfuPDUVY~ z^ajU+kTW^vci=JYxw>2S)X^x0rq$ov2!x!1s4ojQ_;CCXV7oh@IE{| zEU4lIzTrnhYF>l!a9@%{NMeJo?}ZKh@#7Y=Uv8w`slejxw=ayoqK9@E2swd^J|Nk| zH8EOytFK2x#-oY4K=DV8)nG^t|FChJ8t)S6sBiMF95deHBAAJag;Oz5UHUmL zX&9N?uHiGQJ7j4AJ07pZlicLIUG^r&cdq2@>ZS_yy=g<>?O!HbiAI;@*JQcj*fe2 zauo|rfGaYgR%+I0f6>X;IETlBtFfER&f2g_vq~Co;eiTfT-}?EWcW+sF`E~o8ug{q z)EaQKo}H`gTm(6@NC`%@;eCG;TG5A1Y|~k!UXzjO^l|}~Gxr?i6O;v$+jIPh@}IRjU@62FgB1x{xpy^!5Jq<=X0iob|2-*@*!U!u z@GQoP5<+v8bfffBE$u8I;=uEyU1D$J>Q6F)ikZI0L2I0ccXZ9@FSHwR>=lEU8u~*J7IbH0RI$zzU1ST)%La>b zpof-X!yI);q#&TL;Cli^2aVOj?1{rba$ly*$l9Zl!0&4xD{q^R1{CJ)YsRpY!9TLq zT0jDGfD%3|#;F)6KUD~}^t^0U5p(!s0Zrpo|xH`i(_@XNO}F6k{4$kQyk_kEsFVal%#T z=vG!81(lTxi3x!*8R1Gkf#isg*zK6)rcncntMaGM)gQq8!k;}!O*USbDH5lMEjKGQnHI+2t;Ha{IFitgflG*@)q+wA zX`n{_>$XvL|4YNUG{gN9O!lYsMnBk#+Nl1L_@}`Ks3cU0=7v@=afH;iY+P`cH8zeI zVav<3!u@Rnwpm3V#~jP6Z5{WuSqV+&zdhz3iL!;oX2c;xSC64{??SGS=irW;ba-d$ zp`a+i@eB_|ZaZf&5X238q)p7GcbKPT)St4H@uIF85m>|Nt$V;_w#7k6R2SWRkek~L ziu0!9Viks>NMjy;vwjKiCy^#RPR;h#Wo=k|k3y0q zR`l&S0eERb8?~AD+3)hjbXM~&hOq+0ZT*i@(Ls2t@Z4`#SVnm5p!{h9e-`ZAwa(H)E9~Yw6Lg5#cEX+J~_$+$dv*k`8)r%(o2ZLwF#NDfJ?azO5TsromPbA<6_kv zsWt?K2}@iNbd;K(XHs4qg76qH`I}`5`<*adAdGbGC~ZWewT?1`4?t#Xt7WOjmN!|- zXz>oF)L|t_-NObJLaw97NGu(-S9t9sr>ynuqvIXi*(j_&eJJd>92un;^#Ri8i|VA7u-;#Ewu2pJh2lJ ztliVmQR=A0-`DHguCBZ|HalXZbnX+)u}EmN2o* z*WKUet9wqLJB@qQgAN=+t11QxZ$*NzS&{g0)e?ctMwi}kX|0-J4*H01T1kJ-<3!j! zA3AedrY`XweG!Cbg&$#-hxp-I&Ich={{+WU)Uv;MtsjHvBNg;)#(MjpF?r^F9}84w zuL~Io<=Z*?7TI6RZY8qZ;MveYt%Jt{Df@&s2db%2EH3jwL+2HNMW5km7vD-ND$T^FA^YDGAFC(3+?Z=O< zi}U>z0vZ8Zm4|W$N=KBA1sr6_=r5mp38berg^nY&dQ&I7^Z2KCwy|n}QhHc{5;k0d zFlU8YzMu}-RYy_WtIBCAe-fOTV85#)PVH;wQHg*bFUXvA>)WVg*51k>h5oAIiM7A& zM8wF53MWNxjKz%{_-7(K`nVPV%*I6R8Eks;wESazCE1OTrCibYiNzyA1%7 znh?-vn$?(?Twc~0V%NkuZTM(=BnJ-A2a%=P|)n*M{PgtM%Mjyj(>V!Q$4(wlvAyuKn`qkGin7ME`25?7WnxtO<)Cx?) z%Bp&)er;@2U-!lp8*GYQZqDbd_mOV4tJ;M#qj~KtF6hnS zJ3{?e(T5^=)CioE(mN-Cg#CAu$-FI|br*1Oc1pF6LA1Sh2dtH*A&=36r+BUK{VZWZ ze3a^$DR399(*mW)vidup+GLmTNnY385?#k7R9s~>CcRkV`z1wGnnSFVEf7O^yPMF{ z`|R*hL(6ZRPmVPo_J1zNCK?6_S#MN}1f{rv?QT{X}19l$V#^Z6fx13Oab& z0_c&q?pq$UN}CH2g{I*_$#94H58V^gbWo4pX3;Ob&e)4>U~CNQ=pIIB9|$?4Dkm8r z#qXNKR#d!hTqtlKpGbmU%|iqh*K#TV08YECeKy<8P5kQ(>>CNZ%A+xi^RA2E5@Ku| zTdga~_I|rx`>^&->X)bB4CeKyzmliywtT=aI_=P^<Ncx-SND{j|&&}R}9CZG?}OfUne%}UseL2M>;en zu$Q9J8}|t430$V{u0|V@vFaF-a?0$VI&_|1`tBe&bHH}@8gk9|;;`g<5Mb`kON({w zRl~@n?h2YQt{reY^;0(zLf@2EL^$cwpYt~>KqAmhq%BysnjxAL@=7Nn??=lA*>Nl# z9fHca)Mg<blIHBEg} z#*3oQ|3UTPxl1GVC&JNo&P+eW-Z!VF1C!?x!EehYMZ*s2m5B9Ndk#qEv(U%B#; z{r0Yqj(M}Fxes1|Zw6sqMlpwP5Z1?k^men0{K%j(WS$f$`Ek-UWi!;@dOToaEvbgv z%b|9b#@f)3?e$PlbDU11O@-GhT-)W4+D zz@Ud6(2p~{6r%i<>N|!lN*RKM9E!y+1{eWD%TQky1}TpeO65>aae-+~lE%)7%BF`j>0i0xPWo?kqURHM>VNI^Ol|6Ra!ftF4L|)>r@gy0 zwwPdSa~gKK|115lbv!&EkrFv8-#T;Kv-M8Vb(dOWEAvfNb-dwh$vFpQxe0PsyL}}z zLWB74r&06~FnO$!S#6L!SOwuM_JMXsI z9|zXk@J-^j%#|;PFMUO$j!hPBZzIuw_y$#Rf@RR_KQx4$`18{YDG2dMTVMx>ynfcb z*+I!EkSEEdXQU&wCvc6Z=K3r~Nb^Poc1*MR0{`BXod2xbajbg9vva-ZfTLQc=L1LC zy&HbjIGz^XvFH#m*?sKHz*L8G+ zc*O$g=>~T^UJ1&bcb^dt*~H6%ZVVQhGCke_J|$-aAD{gs2p_|(TW*xBn(Rf-IJ4*Y zO^}iwKCPeo?vUg5w>G#gQ}I%Rp(L0^QmYUqyYYR>O9`B8km_VwAx^30V?>X6SYh5Y zGwMCd95vb*9UNc(nx+Cv;Obnkhh52!a$8#nqzPVxyOK1vp4<7(MCwC!fXsN81_?oN zhW8+Zt&J4IPKM1#p}`>SyucY%A(Iiwun6&a1F@#e>y1pv*di!xqO%%9VRJZX9G>hs z3s5WQZN&Cm(UP0KpM=~$8CTQbxzU~tN`ekCD?!q#8 zvQD-VOO=*0@VHeoG|#zvX<0^~)jl_HtLldZA2%|z9kF4&-n)GDAY?#7>^Y{%st||Y zcH?xYN-gEH&bI>_6i~14)a=T=yJeXN>(dBFcEt?sFTigpcLX=XEZe6{QrHsvF)VO| zQ-k~E-9@k>7O9a20C`rXFYAP!&fODfpL$*)vM1*;h&L())fpw%0G(FwU<;;`tVQC+vr*J+~o9h^#_9Dkv?_99oh!1CrFd5jg`o z7(3t132r7KnC7Jy^X*S_?84#H0~?UkpX)td-QN_t&Hgd3aWhMF!z-mWN#H5MD<}|mhYgK4OLOh3Juc!*O7N0KJ2}kak5ABz! zZDyF&gCdCo*IkwyW_vII9rNc@ur?6E`=N12`S3oujfUjek|?N`?Xy;_g)6M9p8zf ztc|Bceswal>Rf!dTjt!XCc;aS=|V#%7_<;S&~=CArj-~9!;c=H{<M&1}i5{Qm>bRxDG`q(23}?wuZqHr?Uv$*6a6@j2d_oL{GLm*s9*- zVMYPEFUUAlQrf{zIXYSOcmZqmh4Q0{p#G5r%-cV>N|9de1zQ*L(}7*Obrav6Ff`1r z9QT0d;WQ^Ti}tiT9*66It6MYID?Z5boE@1FOWjrNK2b0KEOkG%jdB>9OUZHgqk`TK zo$hl%dL>N=6q4cy4~_S6ZV^d;CZ$-F29@rfBIPgzo4=DyFBhS6cXqpphsbXKzi}a7JwJoF?Lrh#g5Q3#Vh;LjcOKaW zFUE^GF4B4Ll6(fkvGtZI$5J{##RurW@wO>qV}i?djvLaB$q_OgK}3Doz$T@b9#PX! zt=C(H?ORO7mQuZ>M#u!u@9^qNf_2*kYoVN2XggO>zbOh?sW*ksz@4?@ zwV-;frTQPeb5ya!>!a#EQ_|p>0KEd7&zZ2sd;Uf{S7wNJ{RngODr_>BQ8?Pg$pI{h zh;nuN)iztYvus%%HPws9Ej`&L z2msJ@(e-}n6;^(*ufV&IxBo56Ld3q=LZR;@_kdY?h!ic>B#$V{PdFe5a~=4WnX=^H z8Q<;ZJ8kJdlHT3xS7fmKRP|fIA1iclv-dquE1hxeRt6;MM~^@2_W6#Ef{dOLRf=im zS-NazUZk0}S4kAKF9!!i!D>#rTM zZv|xeQ6q?5uIJgvXSvfm?KtOk#~a>#-y(Rs#I!nmLgCWDg*R%Y#1?K%IUBZcJkybV z2b{3g0OMnd0+z;@&HB02rLWS`JqlEYH% zALb{)v*`pVQkhg=7vAX@NL6CgO>ZD93G$eT#)BR#1Me||!}nls=j2TFaUYOt?Oud2 zBq&E~bNcd%*i@9o2|@zcvLyS`o1B-m!vk7T;2Y%!p-NC|T3wj#_q9v)r!nw4QHz{> z&81G1NUbivfzI)zn0H`=AHK@8Dnr_PqnjhuJ z)$eFsc0mB=MdH8cs?Rqrjm9fGZ*NRs+>_n~kTiEvZgGpD*3MqQ*Wbn)xC@JYrO`Z= z*Fx%?oaE+)dBS2D6~V%=p)CR~aC+LFalUv^LtGSzl-^))P~v5bSv`~lm+U|V+6!m4A(LV_xqePr0$ty^VO>iF6<2+V|lh2us2!;1!X1vUdt9HGP0(z;pm zBW2t_7}sj5N#)L>__Uq{t{CmK^AbB5RZjCRpSvvUAnS*X`KsZLVa_(` zt>P=fI`q3iGrEe{VHP>Y)uvN&=b8dZ^X2%c9Ddin%-0nfOa!lS4l3g94)gAo>-Ut?7Bt`q4FAh)JN^*SF42za9G(MZT|^T&_y z8Xsi5@jg&61`WXd3Wq4)yWeedEDL39TOSaTKvuGoY_i_l(c$sGzHyU}_Y8_=H zJl0mL_n2+~LNg8%jUno`3**4p%R5%jaQA8jR^{sDI+5@`niGdO{U8^rq^ZNAqdM_( ze#e>cN)S2ZlDla2x_jyq7pN|A-O&CK!h}009Uz2vkA(D?Q!v&-+=eL)WXw_#z^HudyCz3!i8zW z6qNR17D=M*fH<(I3x(c#P;vG+CVM4UL-KqBeP1EJE!>7} zcU$`M3PdA|9OKjQRvK$1M2Sl4T*`7segr)4S$rw`LTqcTgo#jun~a^{aNS>VLA5iy zeq_a;m6f@tTy6K|%MtmHs<;6(0AB*H&aYx6$^|-p|i&m zdQTMQNmslE{=C3m^+rRNb?=;7YmTa^$ANg2vI~0b7sGSuIC6nk>JIuyq<1HzZ52hG&x=+NlFRA?Y*ER>y zHchRJO+(~rq?hL>dM;|Wps`vD-nHc|zNk~$0`S~;&F?ZpR9SglrIar?XBE{XEx=C1 zhZ@hWG6upSwd>$9k44m81b?ZW$Dcb6S1btU&Y}BXWaao2H<7qtt$3^hpNcTm z09~eVQeekC=xkpJal+`W^b|YGou-Fu)N-27wn6_IR4mwck-IpY)lC~CnZ1NJGq0VwX{hC6lARdvgXgf zXsbo74K>2WrMw{||5_lLD~={bQa`gJ1IFLvlu2`ZKP~R_7eXUne#q#3?9hgH6H34O zY1`tpbjv&hheAoRVO3fxJirZ-SieGw4pgd~Cx1=7%&cIZa`v2RBJ?5L`}xQoaDu=v z!%VRQrJB+9d4$zMdoj>+QJU~>MbROth|N|i{2l%p&0Wt+E?>1sF9|?)3 z&r8pgkfI%8Srindm1@i%o#EQ>YOAeTO|hGE5UfETF??oj;WcDQml)Q&O_v`xOVXM! zi+6z1B5T2H9Urn;l3Q&xZYhh4OG7SC>V1oQ_AEw-vk}uALUeTRqwBRPQpr21s2~>f zK)aRjThJ?{AA>ryTUM7eB#^z)CO(3@y9-Aftd+JqUwUk1hVS*HX95v=h>6PKC z+fM$OF;p{t*=6>g`VEn(`h{0AI12kcNa%ZERs$$4THWYbQA-)hr)g045)^fQYP)cD zIyWKY8ZDAf=CBo$5rEb+3;6z3TdQ&B#QK$(bj_bpjQvv}45&kA-BdKrH@>CJ&%S8M zqgo-NEv-a8f%%J3pUIH3;jRiGQcGD|bKJ93*%J6CbAsyyLT_Yvup3`a|E9b*}cRmpV zbXbPg7q%ZnzPc(Dx6^2l9ydS+v5GKx`bQyT0HUpXjeI<%elF34G+n26{AWntyOtpgQOs_XTOTcOpZ^JgCy?^zN937bgI`HP?MI$(Jkk| zYg0Fn$2_bxV-%|P+}N01)SUMNJe8gj%`7Py6?0auaxUC&4@;rRD@?Ueam6dIjDc!w z-k#?dBUc=5(wSc8@O|{TddsCUS=K^re8_<=t@zZfDXaCA+bTHizUp)Kg5e9?rn;F} z<5C%@xr3En+}&%R5){hKz=B_%bHvVmo7Qy!aHECbZvx9@@R$Z$8hY z{}-R9G98h7`CuPyZ)fknwm$##y1xR#9pD@kXPzqk-bN%b!^CC@S2d<;)U2le!g~e) z1X?Wb(S7 z2E#Sp37vb^7tsoO6C*D5_dS0=T+EI51sf?a_GNHEH85;@PLF>8C6c7W^Kh!Lqo^xF7`oKOi%uU;f^Vu%b^-8QWP zL=ec7Bv?uH5)SiM|I%niYvJCwsoWcDbxBWM@f5(WKdzG=q(h#AhhdK<*ZO2q)qlnX zHml7~r8*5sK)Tw02IMeGJow5hn`)W4)YTe2Y1SHw+i!UL)He@L4R_TV{t|m{V5#jT zjSy1vq+m9$>YamwgW@K>JdANlw6D8JGy}}h&tGBU+U*yp%brnQ5=aSp`(8^Af}R8K zyrrM6boL-0+G@Zjll+fbuqs@<90B6eVFMNLps+uL%|mgqUM`6Hc`Wdlu(GT*i@JtH zEH^IyXlbp#N-~6QQ_ELHZIv0pU)p4NmDhROgj1VPx?0kM#sgeex9uy0!K;DH*A{S_wqWx6eJVw_-5Coa&9u(3C@VdKb|*xlyuRZi!XhZ4SW$9N>U_*RRKk ztS1n2 zR|yop`6$ zb(B!hLWVI3-#&1Cpneq9y-P}@&waW`;<|x0~NEV0+U)FrY=k zzGZLI1-l9vH(wLbgwwQvd&s!_8T|fC6L(YR{g{$zD~*QGq!9B2`-SDeU`JierI6F3 zR4$xP_dl{o+*4TG6NOf=BFl5EQt&xtMvLx=vLhHzguRX?QC0BY3Z*RY!9&vVNp3DuAv$cTyb4W!@!W1XNb2-QuBS}H#c zK5PMh3^Dy7u@5}&MqEMn+k=mI>;uZ^@(w10aQ<>XwV2Ca4^Y$CFmVH8`8916*3PiP8Fs6*kfzYSzE12W@`1F=%hdW-6*VC@c|`tW0j ztqcJpV>KEHK3J|C@8lo-GCau6tmfDJ#8WF8u5!erx)9opDW=&z=53;y+rzoSc>oXQ{P^;SZ){*nw~+&Qoz!G z!SuDq3WL7{)S+5n6qGq4zoU~-pmCa>Vs+X~HoIV~pHh5s8%&bysv;id$&aY$w3?Pz z{MyD&G9)R@O7r<7E#Ol91*%B{jBiw0LqQ(Dslq8!Y>s+XotXs{^O521!Es?qpru$y zzwq@i6n(n-zI$W12&xosOq10OtL$l+Ysj*RSWn8Og8b z&p=bz7BPj;ABWE{OvldNDkQclY#oalVrTL%0m0Viw(^F^-ktmKRfU%D3IT`Zdz#77 zuK!078Z**Z+C*Xg72`p%Sien&5dtX$DO^Uh84a-;zw}lyxx5p&obXAz&Dn)?k0>t* z3qT@-T>~6LgLX%4abSwX zoQP?b)B+_aB!O#P%b~fR#BTZ%2qY=BdlcYEl&d)5n8k2J>s99}@)vg~Q%H3>^;~S@ z1Yynzaa_cK8Ah3bKpxeXg%VBFXL4!&my@;zuLHcs zaEjsoT--D4ahUt=A?KMIRbu0K5P33@$OQu>f|A*>(J;SOc)~W}X z63NM%n3FdQ7+Q+OJ}|}%-!=0n&Rab9Hy37oW*93;JcVIeH!8OKP67duMztmvQYBN6 z+hm~j9`L>tuS0@1Xj3uoRhI_>!9$2A=NoBC?H&Z#IKSf}(E%wOVy@Xmu@4-O7HgtN z(h0@Y12&tRJ_}LeV$w>0vixXnfc8S=U`>NgWXkr7Z@CY5FPAd`+}X|E%} z3A00$e^qL3mzl4I4eL`~(MuxmS~aizLAB*up#g8c$XNJ8?p5T^eOx zd02&<`Q@h#kG+rOEL7*yW&$KT{yn07o*{LJXWqr;NtxcN`bF@on5IY^HjUrF@F7@t zr=CL=wM$p<0Upa;HZoVf1$tX3KB2Qjxqu@i=8>9@wXmq`;^+`Z-xldZ2^&qSaza_U z$Gif8ePZ70&aI0Di+9td-ozRziq+$aa zSD@}^GljliO~e*c%@C>`iSkk$#*Rthk8T{*xZ2WYR=quKPMh=O<}P2$@%sSkWAdx` zydCo&v9#EE*Sw%e6UUU1c$9wL$0UmOHzVq@^`BI!Aps5SBS-6BrgV4#PtI{i&OFi+ zxi~MeoPZYp7mum3at}y2kT??7(M(ff&In(<)*)90DiaKeELQ<^t>NiPM#&+HTfcqE z%K@@!IE>HnRWLYVvSZauMxTA^)5-=^;@JM-LY+&DKmx^L-Duqxc??SMp|l-}G*dz7 zECKyQOzX+hhYx~5D5^q8#<&qK&h7_l>%2@5YM8Z&9%G$plWP@MZO$0rkatUzE$}=0 z5!>DKaTnidtWWC^LT7japyOYWZTv@)NvaZJP|^TBcP7D7)gC{bW!QftSzQxUPy_w) zJ_D#zb_@;rKtvqpYFqUHZgH0%{vTYZYwOD)F;VlyqIvS~I`(2~^L~X|VL^q292&HM zOm!WkE0B4j$I#0Ob=VH{jRAMi~(CeI;Fohz5fF2xw zDwz)xh_kFNrS{l0;sB=M&to<=?>Gd1%8`DTc*0{@ob=8jxWN zK|s_*zXhbDZQDCp+Y4xa2Ni&Crcy`Gj(9v#&!VHh;V6!p?=dkkYX6(tbZi#ViU~Rp zF3#vUskK6AH>a+P7p3Lc9CNJShQEUO`yR5?ej&ry?ILp#^`$UtBtIY~LWE8l$Du5fNv30mUrJUt`^Xlai8Vm(C$u`^#pUMggcPPp;GNNsJjlEm#j zGr_sexim$KN z-vt8rA!{+Kt)tZNJ+-?TA>oSKRvhe~-UWa^PLAfk0arf`UkJBHLtbTkPlyl)p_83< zKz*!i-TZVr4+vA_hGqxVcEwa!a~d1M&;hul-z=VHF~)2g7b%=swvkTfH)TWKzF)AR z9vsp15>NE6!tsQJX2VUyOEQvNvR@VM|A)M{42tVpzXl;BB!plAf=eKQ-~@sO2^KWC zy95#%cXxM4kl+M&cZc8_v~hQW)40s(hWzfm|M%9+TQgNtH8tl$e}L1c_t|Hky`R0F zwbnCS3#C>hRX&f{N!OFB+JtbXB3BUfbp{L?sQz(!a}~44uRgH1#S~f{Twi{!%snPv z#$6(^Udl>L)vhR&nd%bUsg&S(7Q%;1Qxz)@B|xg9gymqpj-@FvsVo|1wLIXDkKt)c zaB&zfim^ysz1e3gSoU6f8MHqhcs-{69mdZ*uz~97e+54d*|D_=r9bLnwlF_R&kZY}Ub^sO1utC4PZe3r zlaMWa$9yA=!>nYX#g=>!HIb_nQMI>+eR&clXkEDKYGnDB3zXgAeen%gNB!#%jy)H( zOE@D4p92{e6j;VZ_m?~jPA{tBgG$ag6@FRs`o9KNY4uD=wi*g7iib})4I5#Z4qcBx zlWBS=pxs8+8$yg-$vexRG_T87Yc7zWFp*fo^;n z5*U7J!Jr=-uE6hXjTYoIbLdi?2`;k0O{jKqK^|QiMoaK4LaXt43DS=eiLBO8{T-8zQM2U z&ymf6cEElj^?fkyUKGB}UK#;uP19AoLgI;V8g&5;yySgqCCnTAv$3~Wv|bD*XrI{) z@Q){BaQBQ2!mt=VEaBhJVgrCnvQ2g3K@TgOM8r~*iP^bDl$NqxeFw*WYciw)n3vadb$-NEM2VDkz> zO%F%!dclmDqeac|k62uyC{uDP#d5<3zP`l5eEW|lzL9e?fTfT344*)16f{!k3-9S< z7#tE;`#a%>sST|sn|4TZAOnHK$Z#Tl-&&Vke)66l*xpC{7hF=S7m7Xp+~lts{wP{p z|HQP;ll<|8P%{Yog@gE!Bl5-B1DjNC>Pig`r!m!zd|#?x7OCh-YE-u}>HYlqJKTBw zVh~Knwi{)RWm$P!dUafeFaD~An$Quoew1 zn^65|5)gi5LBfJL&cv)?RU{_VBo02bB&MlZ)t&72IRneF3<|3r=;1V-_kgT zm7hy3cMIL}RdNEDA;9LC6&TPgh{RCC4LhR~a;JZ0Ma~(3c__2*lBCk1bsmfLC(4vb zgY?<8Dh=k&QozZneNV-(7JW-ng32X}fH)f3YJw|g5N$kO{z-EDGU*#s*0{@w@3y^k7Q3aFqo z&2M7xX57yj%;d7-tnQTUJNJ6=8VRkyHqqI3n(S5II;RuQdTQTajBZc)r?2^3^_Qk_ z8ZJdAlcV<1KowBvUba{39=e|ZX&uzpat9$vRiPU#K}367sq{;I@YR!t5kyod^$oUD zf}AnTDy!JPsEC&naH6&xR+L7^n!c)-p4Tz2xUtnz0*Bp)Lxjr!5Yf6mQL{GLcUbFlQ@&Hfz}#=9_e_Q7jcEfq2g3Sh;k4^V37A>DGc&^#6RG$;eu40=|V8l$s zy?bJ2%L&})>Ojx3$sQaQDBrVQ_xGE!VXh}5t3Jk$Kg7@OA{xE4k3r?ZeFL8QmF1PE z&I~o&%2@b{;(0JYY;HC!;i40?bMlBYB`tYWG1Y~av={Vw^k7@>Lid5j+iYQ{JU*vb zSQY~5+4M6pB@FX|-tBvTAHh%yfG2k{!gdxzD87Z*)UUPhr-MWJ@~|xKM%N@pM(SBd z;-R9c-`?+tI0pv3kMpdkAN_M=7$2RLT5w)Fy>XGl_bTMvNOFdot<7;6S_3?S!trHk zq=Z#xR7#dY=v{VYnB-|>K2B8;^38Q`zOd(pgw*!gEbzday$0}uOj^O?eW#(0xgkhJ z55q!hm@L*H!JOSJ{OkO$pOEcsv0@@}Lq9i+2i{4_XXEZA z<2yqrjdqo?+mP&k!18yef%1;a-C>2f$Yb6FQ?s*t1z@5a5`7NTz{34aZJIZ?s<*w4dOr6T7gWc4*Zp_C&^R4Az7gsU zc+!VEgNY!puIYcn%JA8FK{{utK`Ds@A zywf&RvcH8>p30#5+WzvrD~GG}ZU>ylFYOIoyc>qrO*%}Sa0FwI-wZ)Y!uxE9p@pP- z%bGo@;O}J&h+K8(MWzkjOMS0F?yT^l9wUsa=|mcc>{TDPC5O>Q%kHq|FLFn6R#2>2 z??`wD%qMP-5*G@UV<|-jGpxdy@I0L(^78{H$s_!xTUh&f6UnRQtd$++ttlJ{2n|TA zO5S|pQoVlWz02cqWZ=<}HbH-ip(Ym>EVnnlpPx4B24Kk*TC-cVDeQh_cs*c!I_Ev_ zsZAdQ{4>bCAO_S*_>`R8=8Tu?m7Q;FD7T+02tU@t+9kG@Ih;xNHpB`>OP{vOXSH{N zqoN#KY+GbrLe9wZ;)}M+rdn1^XaZ54+>oPRDB5757zgZ1{?D-qj+JWP!dA|K(6*wgLP z4)>(Q+>PhIqJ3Td#-wpfovlyY&S;Wu+2L=4HGHDbDgX;FDj}c%0RNPT~ z(#P5=8@E2!+UlJ_KBvEPZ=>F)mT5T=@d?jawe)ArQ2U>(9xvk93xkTES6HQ};FI+& z)Mt_U)&R2F{WDuxhz(6-B4ESXh+93y;I33al_lM{@+xbm#bR}hDAY-9fDxG z^LluF4aMUzkcNXPerN)noYtfz7S*z*kh0>>iz$(Mo=qbR^2e^*XBR(`0-RUO&L7l? z<+FfLLk0VP@o(CKpDShqX{-4#hA8(aW7jFI5!Z_Q}UP)%t?Z}_wrnjik$9nObHY8GToF) zK!i@3L5$!=KYMJ?kII?((sax;9V_oWPSNDge!erOWU3zwHdfM!|LElTy8WW3mK{bQ z9yf;GJksBrv+0P2z>I{BIdO;2JG|*xN$d;QA03qWEtLRKEspC(i8UET7v2Vobl$cd zUQLP1;%k(2R%?w0O|`p?duqq-#~Z{1)8}{8yidDE87X-b_B3TfAs&4M7da!0nt@~) zeFEu&CR-a9grV^ zNgoq|&GW(x1?0EZ5pf-V4wa29sk-i~9K0Z_o#dcR+WuN!Z}L|2yJy)s)Xqa|Hmh{r zG|aS}B4^rf0p;xQy6LAdH%~zUuOqe<|6@?7u<<+}!RT_V-wzp$cawep$jcw^K;`9w z_8Ox$V;)ma9mh0y^KIWgBFb2He12ovAF21eNac2c-FS-=lVpI^=ZA?s+=YHGU&AFa(^+D3Kr=WmvKg3+#q;S5d>lP4n z(8;vwq-PHz-EZ9$ig*NGS(jPvdhz*&ZXs{;DIF~ogaCZsfgSSV3v*iC)t2nwDBsuS7hz_UHg?%)a{nSG7NU|GqS5B#|b?DO~Ro%?^gbIsh{uT(We1-1EKIRK&_+>XqP%VN-fz;MK%j&!L_j znOq`hAW0vp^pw4Ud~4bavZ4fB?LHqMvlY}%Rf%w&a>HaI!yLJnr$AOaq%QJ^k}Or# z;(7Yd?i>sMdlZo=rRWI|q7(H7HBZ<^Og!X%_Lx_|m{-XLF*Y(wZ+-syppAmfeqh?C z4}3AEZC-Jab}vHbygkNXA%wCklceg4#;FOX-zpSfun@1-^zL_L#}wGPWe|k@W+3j= zkm3s6oW#UPx`ZdI57v?w_ZWP<02yWb(1rThxcuaiolWn`@@S>D1b$+=b+W$C)pl*R zc2HYD?Svg5^6YE*gJf0Gr3ls#_2+hen6xIR7R}i@WTswBL|rkyM)ky9Utm$(&V=~9P=9& zs=1pubeG&=`UK@9dqOtFqYlUl{SM{Z8ZrE^Z01V_kG+aEN1T9ALfP{?kFsmoI+8xH zU98L$3zYXHi`NmEd6;XF!wf~%08jh(mDXU*?AKte=Ux@EO4jEk{$gDU>*yE}X}CXt zKJbfLZ(B&C6Eepm3HU(?Vm`x#;%frd7?j^Jqv#(#5GE@hG&z)~GpzTo0hyN6MzlVKdHWv z+ZO%LK)OQdf4D^f*L@2WXTuJ|D6M86k_=ZhxAu1N7?~LEYR*^Ybl*OCxlU3i1EfjF zE-I+g9blKLOeR;ZEM6I1vp=E5kU@FNc<)o2zPc1rcOmXjdR}c_@=$iyION(H>Cq}^ zmL7--7c=F{7}%=|ANif4u-7^9@7BI}^yW>y&PzkUG#-op^0DbRZvddzze!NOK%s?7#Yr&6{OIb9AYzkGF*l`5N?#=K z66hS&irVM&FF$#|`Sl|M1_s_gkLGhXz*krFB@=uqoe5jx%`GjT2jKvW9_;wqj|(H$ zBN(Z)8nH*(5Bpg@FpO1f*8$6+{fnobYzxz87$CR8Xw}r&@7pCuL?j#GcRZqGi(6R> zp}NOkv;iqO)-ec0s#od+(3Dj>-nr2@?jmDwUul-V-I+$}zw_m?hkDYmQNc+F3bM2` z-x~_c0O1!zB@p7Lhe1H~NJIe;P_w=wJs+vs|JtcG7BKF=eLC$u$dpDy+ zPKwlQMER^*31igyU!sEC*`_`;o(KYrMq?7D@i6Zq&I*3I(~#(W{Ax{}Q@~72%w6%< zYx|dT&UTo(gLDGkIM6Z2viiz@l#|681LMQzbXh6M7q{6x^KL+jm*{xO$p8e(RbNl$ zN%i#_G#DBEcNcz&S9kWsm=t7OrwE8t0r*JwmJLs#mI!~3ExdmQHo%~O1phNc$jDM1 z0YirSm;!irxdH0;-M6wS>dA^_&`F<=SS~OC^E>YUyaXujDIkoC!AwZF!m!R3Otfb{ zLwui91Jnx|*bjN1B}qOFE4eYDxzxa9OUX2rgW4U`tZe9#CFh71^0uY_Md#j%4=aO@7RIQOB ziM8sB{PNJzQKxN9qZRO29otdH`kO7_mq|ru6Z+!9U2}%SLlcU;KGu${=I1D!)LUHn3CB^n=PeD$08- zqn{R`l$N+@_@mdhkOoI+Nny2G(d{i6@mnZXGtQ1*$d)+#shF+8M%9$63)P}U)fyrA zyuZ+rsYh}5%305Mol*le6H@V?GD;mc0dEv^;;$%P%r|NV-*A07f^r4V2YH(Hj-=_o?^07uQA7hxkB5#6ggyHVRM&o}(#sfW9OvFVBr1Ze8JrOtBe6|v-}#u+ zBJ9y#V<--$NxspT{)(v^YD-W?8pLbxy%u?z1ZSqvn*nvt^RyeUd~kAS9?5hU0ezGE zZ6^1V>(YMv_n)kIrcS!F{1ga670dnZ3?K;q-2fsK}vX~c3eOpU?E9$)%;aapz_ON&`nH_uie z;J3V=>W{2n;x;fy&AJgwy0YTGobcYTu$V}t*Ax{cRFwD;Bm`yU8O~8aqd(g~`~9U7 zVKf!@4gR3VDV3L%9Yfo(z`}WW5Y(9h_0D*=!%(Sn#k9#0UbqQ0&G@o*KdVI`B($MT z$of8D0SYtR&8~>YvLfjgG~8I;rq|2iCE6vFgTNFP52TM_#S$l5^SeVQu{?g|AZ)6Qh{h*^SWLs^7 z-nB~3QZlYg&)jO*@R4BNPfwo}M$hYf9Y>q=2o51%q>*X@b{DYV{E#LF*ncun6V0j8(nqjY_`7JIOw4Ky_O3ZpQsNu`@u_6DV)LV?j8WPghUO;0~ zE9_d~+VCpV36M9|joZ$Tm8?60rlfFu_kW3_58a?`ai#Tkg+PRpvxeK(M-TB#?f?7w z1;{yNe%ZkY6Y@>8X`cikJy3#K*APc<@I)E=bB0Gwj(Wcd2Fax3j%V>cV;hw|?FGyk zmxM~l=$X^~LP)jDHrQy3pFoSr0DkNGb>}$dH>IpC5XIbT9y(bGi!;V6#CXe#XX+*}by>smwwR|Y!zv>Y3%^#fKiH=tCEo5YK z-iXQ7sAjJ=#iXeV=fD8iEv3JnLLu_g&KqRBUz`6U^!iOVz=YN@YAH-aptpxS*{_)!bJXYj-3U) zb#vAtb_E>^~{e>dq3RnsKiYM!{ zI2Qw~rOy)qa{l-Ld;zU|c_$>5V~_LmQS~6;rCZZdt1BILQcPS}xb=g2*Xsp5H)C^0 zoj&AJickGkQE~4jWfrStM^zh_vPQ29*y9kG5!FmC17PmLvQgu!MczY`V3}4zS862Y}xHzb<+YU<_P#tgh4_m2BWR2%#d963>{Ql^o zFzo_Fy3N6hr|hD*S+b^PF`Sm{WZ`+=$3Q2gV|@j~t1k zcw!$CU>%OE)J6qH#eZmaz+%nplP#_v7ja<&<=rjD)xQjZn#ijXJGPh?D<^(!8D|3P z`UuSpTN6wd4r9Lr!1KeBUr^t88-1tf5^GJ7+j%hte$ZvO`N2P@7D*QB*WnB)(Y-UXBJiztSE&>4d zXP_;`{b`E6EJVicG@Bgrd;O^jxzR4Q3}%0otG3s4%kvZ&+2Z7nit$7-Um{RnZL}JQ zDp5WF9@9|M)yqGoZDv%SD)S|A3p*ZqybJsW_$Oi`8HK2|qX%#Hq2%uM0!s*V4_QsT ztj&=R-8{b0;Q37Qu?LU9qx75H*Hw$KalY+s z<*4brP0Q{w1DF3VEl~|OdFKdzAo`b&q1UK=_1GW;h^R-Q^YAj%kqwUC{D{cg59Y(l zw#eNkA{t}h(y92+tcb!TbSycGT4Ly*6_ZO*OY>JC;um5u6+L`s(CrIo;0K*iTRz~l38KdRk>UGu#|xK` z)P^M7Bhs5BDrFnW5?%{@2gs(Y4yKr>lb(|C2EJvE1ge>`k&EhiLSr0brQr}d{}N}a0f%`TfB^HMOpaE#^#giVjs z40A1k@n-#;>F`8Y-paXhbglY*YLO26u`G)|MytV2ZnZD4O)ol0Xus43d=pqjYJb_1 z?^gKuGbYOMNV@PKy6rJPIx0YHeSK$yJKVA^d`F?qVRsF%_)rG*!7ctiMY}PzTgPdu zE>(8ogrvuhLk`$yw>QvNElP7yJg&U{t+y#Ck==XOk%G~ar5^#FBv~p2UrI+>4(iS8 zJluV8Dn&#ICX}-bdbu%DA0^i;&H5~7 z6@gz252yFFQKsI_8!9J70h|ZFd~xd&PZ*qyq#v(-GraJu@Ut_r=RCx!AD{<6Pz8a{3`5_?=jkI%+lv2*B=Y3)N!CeKO3(o|4FLHej(ZQM9nmR{ydK9 z_zU`$k^MCUyX>Mi{V8E>$f9!F$BVC1DNAS1ldJm=o~^4ARYAGHVdPPvYAS42-I)6| za#^UB`aovbC0e&(m7^QX3*VhQz~`N8QOs_n+G@1m=x|*?&=;mfM$%aqkM3lOM^;Z; zB{S`!)St9XMdG$#`YN%^_G%c;Os#!8&bJR>Ut~ews%+EAo@f@`aUim0v6YmyRxFSc zpuFxRhZ(0LIGzi40F{~^|FxC8FXJRCSSjKG7Fk{=Apkl#%woAC2U;owV6NN)ellLN z8kPANWl#&pxBtKo_*hhL)ReyxUX50keHqmK<%_ZfP(M>UfGFq=3IH+w>Hpgp#lP^H zjF*nt-)oh?AYy2{@gEA8>4*Ga0r0^PjK46(C@Fi~-#H=}hTj6$O(iJI{5#+~pzs6i zs9gm8bE@&bPR*_S=Tz^1PW=gr|9e%5f1R3HSo*gCK6jl{@Y7gZ@HU^9)}-fK7|c0Bn*1vZITo41`dfpv&)fX8G9 z^ygFY@5TC}f+yjn+T>${{M}1mUT!u3PDj}#dP<0Q@-67+{~iDYec(_K<)81#5B|L& zXs_gc0XiKZh;k9V@2qX@?nX9QBjNYptQn#X?#R=WTsl7^-;fUAEa6cXay2%yXj*FQ z1nRDGMnNA4aO8FZu+yw_k6RnsC4EqoGgd-ZyMsPx+2*c%)%}l-zF`C!z(%qzS?#1P zUXpqjc*Xux>C_BpQEGgc6$xOo`+%?O*E#s2MJ^G*^G3g9Mhn=S(^FqxEer!C#idM_2l z08yj8QM>Es=)9oVoIIDHHmmtE)8__UZxYjL-HJ*8-HAAdo8Rhk+ZW~%?Q)~`lLX&Z zHwT4jOz`=u2YFeJhT{>1Efjr|F_e|z*iZ0ORlu5gtdjM3~KGF8!Xk!j+I11>(S^GjC+SY_MFfvF7w3$G>3c$R4n zzV<|2eyuFx`lP8@8wwCttNjF5V<(!pTAhh%%NH1TCxe(_k2{YT^-3@Ru%O4*? zgzc+IMPekVySumAGP7vE6z$zxt`X)AJbalr@R~drC){TP0zjD6xc30Ejh)ca=551AD?oERK?#qq*zQ?}EzFs(E zAyd6WM_e!o3s+IQr}aV4*Hr}z;}`9JI6*~w)Gu4sT+;90C0eterGh#pAF#oi)Rl!z zIXe*DY3w}A5RjEt)RUTnunA{kbx)i-z-YRDk07+g>v4Q&qCY+=PrTsgF!O`5@Ph`j z8YH(D!0Zr@%nS?~DydG&qNdPu!wh%&yE;Fl{>h_<=7(iNPg*zyPn`)z%Enf=<*CF_ z8-hiFYD8qa)96;{c>2SQ8M2-;*CUKB;mBfoFf-0BC2%hD|8PFgP&lQ#t86p`f4+2% zrjqhGed_1c3RnHX$53TtZ`W-kfLKe3^_tu@mN={4e+CRG9Qyy z*`(aVWX`UN#trpgn#YI)>ID;tFTF)g4i=i!(>}XOFO{;GMc6z0z=aT581E6TNn~O6 z*V=Q~K~n%g#%3CuP22jX0MV<#QOot2#JPVs;1RorrgpN*`2a?a3lRf_YPr_=qbs%@zKs}@j%mX<+nC*ka*HPs zwse}e0PMtRyEnGfNIeKt3G3M53skmyt3i`P_n%G^_h5F{+a*6^5N(`u#~+rTXA56h z-VZvCr#rBk5Pj*3V|roo#W*_g3HF1c&PE^p#ZkYxC9cE;jy>&FWBGv1v%AeufI`Xo zkG|LeDHx4XVlxsqY@~8Cq&}ov z)YAXA1mT?QJuz}i5LztgRR+8r04Lxi^~hTdaR1ix_fzif*gW3b>54P&o$$uv!|TDqWl# z9aoRYsb|rkIecsy-pD<~zf%+7;XLY&>!tf4$ekEhdwJIyW=N@NSD3nB%(_DSyflkE zyN0tXLf?=$-8Rh}X&u?^JGITSGezU~s(7etSN`5~4$4IzZ8%O9LpC5ohx!o+;mB$7 z|4w7Zs9b*)NvA9~bfcuTzOt;lZQ>nXiNJ_!_qoL3r<=1pDWkK$M=m5cMwfI6p2C2 zj`L_;mfDeJ__L=KHANEJw(TjzJMD^3XmyIb=GTF^LOnq=g3$AU=@~Zgo{(co5Lj)d;IZ)aRNY`SuYFQi%>$8VTC|3dC z>EyeOn1VwN-Ak3d-4t6`y_~3PLviAPrt!gx!C@K;kL?Bw9q?J7%3qiW!9RUk zB!J#GiGz@&;jdJs$#1dtoW(rWJ-ylRhvi{ltprRS|7tts+6>ESpiGQ|z0?@dARVx^ zS@{TfZ4MnX697O(fq##`(9*9RDDtK{f)y-udM-Yl`qvABS z)@Zr8;N@Yt!#Vr?ezdT00HXzf`GnOn5WHZ@agcc#)Ls*CJdEP#CzWRP%7Z~)1xB+l zQ8t(x@Q*xscu)>11MXrJU;)-2LQCJ&gif{sNzYhcC8eSceh!*rf^_Gsla8$!q(nz* z-odKbn-C%FbrtLpcO?WJd(&qh`+EeFsMzttR@{R5v-cnyaurX$$S$vXfYI3>m+bK< zQC{YZxr~*fOexlx!dng|C~f~8;7_1U{y$57XXvh6{K0A?s!+cLu<4I_ORrWRPp!Q} z5P5l@@_z8WVzR2s<&AG>1e5I%tf~$BC<6!uTJ$D&G@z*Rdj3}=(U&4U_v$2NZYig$ z$2qs0a=+l)Kugqt^wbDNDwK1)c$=(^;o1BqyEHqsYK_f>&wC&9*Je@bUJ%HV#@_{| z{)Es`L3+={!wdaysq?15Mz%zlbVPBS;UBd#0iQ<1rLu&p7Lr7C;Xk}P-);Ss47%3J z)tj#)E-13JOY#*{Z1q2rIZ>7~68~<{0gW^6JJhETn7$c2cvu$kZVb7xj7?{tu|AO0 z$bkZt{zov|_jD>K5BQIQyz#9&OfsAdvH$&$tDXyzM3Gj&+YvDx3TY;uvpD^*yKX#siF1>obaCw>)E_6C?;s2VPt9T{W1CoM z=UPu3c-^6rZhACt-$4HXWpI)W^a=183hc=`9E?=y1}Jxty}2=at8 z2tti*v<;*a_yfY@SqDyP;sYy=D%XO;k4xjDAR+Bi`zjXd>_zk@@^>gh22@RSCqWj7 zA}G|vY0zd@&X*j$E6=GizjYo2bGI`wp3jU+ScgHOE9U^(8YG-4Q!k{<-qo&45EW$2 zjS0l5QLo~Tjx=AjsWu2I(8|H^iM&}G&vlw!??+OioyMydN|PtRynX16ImB(ug|_te z(=@%a(kl0C@iSts3F|AtQS<%DS7c7!LCHSBd)VEdPRalfd)D{|P%Jojlx;&cpO-L5 zd8b+Y-xvkL86@)CZIDZo)-QN6$}H{WtUwa$_bg?2l_9P3Oh&#UL4wPW+nNsHx#Q3F zW~t}A4j;1j^)JIqTwghl4B556p$;@rG zM4sZOfwELyJ?7QT3A__7gLH@1qbU2&k%ox#-=MOmjyK@UJKgU1XB49gyhCt%Ba`uG(M1zh!emm4nY_AAAodseaL|73?>+rKI$qj3@yjw6Z zFgReCKx`(BC#+72Uvat*h-;;&EAM zFdYtEGO4))9p*RFBD0*cC^i zAPaDS6-?@w!%OvHPbm)nZggzaZjI((c7zh;HCpy9w0LdjC&LQTrTX53UbVZRI7p8% z3@ZcW{{0cjo;a7QgU&IflZaA%h-1S=Aasyr8|vKgi?)2y5bRBWtV#iFe6VoeA$T$k z;iXy`w8~B*F2iCfmC!~Be!{-%C3&+y{c-y9_f5X<1jw41+a&>~?Vs$%QhF|~g!?;M zdCNGh40M?y^hfbya!Yk)XJ?6agJ9gI?6#0CdQ)JegjtD%hY_k-9sFBbC)cM@ zWcmZD-*{FwAlMmSq-+o|WJV&teItv>1(HtiUj~z+wuhVf6(SIczPW-v34&v=5;SV9 zbJSx>_1&%xgli-PKZ{;0Q7|#d7E%J`t4OTc204IU3n4@(3yH-_nJwm{ns?2UEZ5oB ze*J+)e_ZTV%;&laLk9EZBmL_PEwoV#D)Zhx?$m9mr)_gKU=7Pej~8UHx-ZZzD&w@g z2p)wj+hbYLPYRS%d`~CD;8pcf6fYStWXQ=}K8V|_Tn{2{W{t@f`+K2bjGAq560gfzsN42k!39;Pp8$$nO6rO&s>vn+A`>P z#ixLd)@&*O^Nj?q-6Y$r_;f`KekRUfYTO&Ei-i~gV2F;g8Ug7HpmR_#|29qhCNBk1j4rcuO{PJdhgyAGO}90Pz?C@r*93g4uHL=sRSOd6F)1& z{M(5#cPA>^{QHSecStZFtZ0*ak@>``h^-bJKD4L}=0vI3B_{ z;~H9h(4M6zu*tds^{dAh!awV|BI%~LLY>Bx?k*q* z34@~CphMb!>l`-x%O9_6QS%{hwd>Sr%n&1S7MIWQl#h6C?2bCj`I?!z52ljUvWtxPKO8hkX^*Z2ooT>^ z=4d3=)sMn^NyFXn1l}Ibl?SJ4W4F4TY+iMF-(|B1nvsa<;&UKzmi&34ruzz7uoZ9i zel^J!eQthW8LL`E1>gQ9C_JYzHM0xPjd%#t2?q^J+%4bvENsgfc)4olCZgbnY?=$7 zL90KEJ!NPbBKoB&9$Ol7uv)sT>|CiHD}>^Y6};`?i-SvfZ>dJ|MRd;&rT|#K}g+p*8nIiL;ZQ7wM<{fpG)10VY=E?wKSjfj|F_Q zOyIp{ZaiNktK4ny=CT33cH3`T;?VdlfPa}=|Ab_4usQBl<<@MEq}HyZ(!$PPm_Frf zo{|xCBy?3cz`X7qigu|p#>mF51o>`uJ~6HBuBCSTRU_iDjO*fq=gc=jZG*`Bd(^eA z1?)*oSNp3~KdMik*4J(`>WDa1&^=Ti=x#rR@V?xX{d7yCRhC=?KASMDLm51;`_+&^ zKzvn3klvtJ?MrfVjXd1c-x;@;Usa!e*ytuVJ%x;RdeAoOCjU5&<`$(BZB0*VcyO#- ziuvjsp5fW%Zoo^88|8=+vZ7n91oS;-qfR*s!VLDtWrF(p+qiew@Mu-P$XzPH>YPQ+ zf37*vrU$2nz-A_U4iV(w^=;I-tuj?0#zEG56K#>VL3jTR1V@Nv6UWvJT9 za4tvWVobu~*2Uwia~%@FusZRP7m{Bm=McDO$3A@Kw!E6SX=-o^3)!k4VZS-^4B%D| z4ylo3M5p&Kw|YK>LZFxzqAlYUCp{qqlBRzi5tVHiK#1Ou-(&0bqJ&T1utJq4VmM@2 zQQlru@zZL@^fftadO^IlF?~z5w`edt0f8PDE3T8ldIHBX2Xhi0nms$~?kPEnV`ozD z`}OOWAtVj!b`Q2|w)h%OrO*tImQ4!?(0lMZzM4j;#V5N7^3H$D!W)-L6Kb@}ERf9f z7Elpztc)&*%S$Yx3w29i?l9c8BOaOyDJvf*?!oU{pD*ekq#qr>^5|se3QNgvHJrM< z7B08aIF2JoHi`~xs+f7!T$Z1V8kri0CgSQc%;KVI8(iW4l3M_?lS9i04({qT>5oTn zaLrG2yzP-8B!hM2UX|>_M^DQk?T?qe&DbE_=LF;I8js}mf-r;ijf8&guobEoXeJoW z27KccrTL<~w`!uanlraB@wv~X(Y5;L2RXz`-^9eceue>X=1dE3nvvvBO||%Vb@#R6 zm9^;191p$ZnX3_&j1Bmxe0-zXu)zxZmIa&V>;t#^pQ?z7q>1$^47bTllCX)~KZ=rt z6SD2WH(VDymdCM!sH}Ys~f3Bj1gR?_u z0Y2eA`p!Iva;9S-d}U*|A#yIttz;=^lt2Cb@fM_|B-A|Rv$xs&kKESg-_<_n)}=q? zTD~7){_*Z7DSCtfZx|g-M?%8f)~~`YQ`ToT=&Dg?w7mvKOi;5!l-DvGh`!@mmi&fq{)W-2p{3usz6&A2BGp)F-scVJJ zN%xoX$Lv$thuIK9bM*(puH8yUY2!?)3k^s4#bEo{T*sXW4Ce+)v;+nHv>{c}H!hYQ zUIu9-8|*kHCZubU8m=Z&H*N0uOXl!1>s7pr>#(V;yy`Y3a&r zQ7)$FH|OWre`?5Zvp8|lP2KJyHK>nIk;rG%ecABIh+XS1T$j%SH7KTFSy39tJtY|c z|M53#GsM?o=R;FDZDoQ?s4FdK350>dw5lLKD+Xs7%v`)QZw^#;c(*FJGliS-zDgeu zpyaX$biJ!t`!ojDO`s;Bv!*@qxkx;5u-Ui{64+pf=kyxvXOI#~ym7tEqNY0fI`2-L z)3SWn(_%m)h?c*ePS{W2ERa1qTj<5hUHI)6ASUEaGPvGo24qJ_uT$T8o>=I;qc2}G ziGd{h6`|&rl#VzW2s3%4W3;k?26y!^pxt$jXTocoH-dMridixzP;}lByfOuJ%EK~f zk03F17(qOxoo(evZo`C0I#9x{K%84cMFav=%F%0sy{u*!7lnDHJXj$eQ%LbSQC3!- zTK)}Sd8Zb~h8ZNsSDcI$gABDH9@ln_+ZT|Z(PubcL1rUnJY{Q<#is@B>tlXR?8ytt z&(g343fB8rT!>W%E<3&sYq#WM^faEw&IQ`Y6f{Z_UC3p54jIRlYQ!q0m~Pf*3`m>C zzZZ4GDQo{=>8wP*0YOM&=lhZ(d27xW^)sB^u|ZDy2q^@19~7P{z>PfPJ$=}3r=qjr zep65~Bi)W<=5`j%w#su9tv83Zsjzx*V*cK1Be587x%{&MW8{%WL$21CN)Zk@W4~%b zk^cCVvr^rt_8@{)84LF)NqK`n@~avB$-J;Z2ZUREPn;65?Cac{BHj{09bYTc*~E)i zif@b#^V+A4#(X0=IE0Z4N^&5%x$*hyh~tj4y}cD?8oQ(>uXT?qY&PXn1%87I;ur)9 zuv5q1KVl(K*^~KbxdSvB6ZZc4Y7F!duBt50A%l+(QXIZ%C0(Ful|KPfTSUQBGOs-|C9s#Wj5XR^ay~Q;}hC%-+hF>m-+JZ9uhsaszeeRpovj-X;M|>%?OE*4F2OI5BryI>yG#y61Nq!3(3#WlYbP zXClLMg;jF}BO~uY@)t7SLDT#PhH>hdl@oL}>yqN;ORj{X$+fCAT$5+1XZXC}@o(g+-$x5nUkjV54iaV@W^J`AD8iZM z%juQ%^h#~jZ0H@#I*y?#y&vT%LT$LM5wpG39K6n#S?8xiEu{oq*IvEf;wFgx=^)p6 zayz8|uz+2I@_mZoE0M*-yN5winm|?{GOEFe@`WRg9lJr?f=U@s~kSpuMpxf+(VGz9O zjI4N{Mu}lu$yd)zpQ2vYe8;%j(QlvSCah15uP^g_9LdzH!1;NS@PVGYAbQQN!P!#2v&T@c^;TxGkbp1xybNiO+%keXIn^uTgQ zCB_Is3)vhVI&#b!Gn>;r>OC^d8;IA5FTu3UWF0{LDAgDQmM1FN`AxiJ09w=fG{VHJ zEI!(iq7)^pYTfNDTbs$=n2oE3g_SS^JzzCeDCDR_Zwi)oS?ya!b+;-Ii z8iL4O+ij=$gw)iV!Guga@hl@Ji-d{8#LIg%^J;3QYV}K8_wB1g1giBvcKtTuVYvcr|0x6U z?BD908@rg*DAYiw1(0LH0v<4RD#YwM>)IYjIX}N zTRG#jHXlT4(D#*4QLS5j8HKg_s>Z3AVGoD+`bdglL%DUoFT9|jh#tK5J5)d}c_4~J z$--yn*izu;S=4UA2+2S!*&1GuMMPf4)GSHc>z_rRGI-ew1Z39J*7H^Wk1g5MTWRE< zlb?O1@2;Gzas6bkms+|KJ@8@zp{aL58*uOAtJAe54-PN;i&TM=o%PTe=VPwVl$LCJ z_I^iM($yOK^{4o+WX)GUnR|qR;ZAbYxp1JLXKl*8_Tt^H-h2;WsqE0TG(F?dj)0Ea zi}~-{uG||c`zgx4^ZfE>=g+B1o|4L2@u=ncEzyk5a;u8hp`qrk`!epBvHkt^W%=Rd zljc-kIB<8@;(+_5f1ilQuTU&;TsxyFJF?2;rn0%nx4R2F?*uvj%&aY+(7pNRzAwPj z`ov7tD;(payYE$f&glubR$!j}&uk0ZtKErP#FZHsl=;C0ZqN$%dD`386bjYU?z)!A zzozQT?!%?Ojx&G#wP2NHP3xZt@Aza_{&HH%{%=M4y*(w;j8o(1y*h1`SZy00-JX@y zmTLZGw*8jdO`BYDdEPV?*Z+$5dzBChEDhO~mG0YSRvU43#ivh`s~#TrHH^Pp_iO+9 z*J1ys{tiAZ7x3%K&CJ)evkuHXv-WtgqyFCyJG|_>;{4~&dOiD#W8~VniqD(=*?2@% zEkCaMX=9_+3X8>ipT@8H6VYR^W&b|Ai{7Gh8%1S(=D%9?>g$*suBwQSefKAo<7|Niy0x4J(6Qr(@($go2@BbDI}>lPzWGiC0Z zw@g*9UvJ&G#WnU|JR`7%5Ln9tlmLMT`W)a;05XiWJ@;DgqDTB&$h6a_d zar>9%Wdk)Dh-Cg_$mUUS4a>Qc-+PEFZT%KEh*}9w-UOT}4nqK%_}W0a2=y&><>PLg>APU`0W|LhnUtfCwaX5*6u$ z9wHqCLJdU-EkMo+e80W#KKtBz_W9%9bMAT`9|L5~Io6nCjxpXb-nrgC*3~%6z{x;I zM|bwoLp1|Bx)UCBbjMKiCxPG4_jWJ>{~hr((6~=m+`&Bu{BYdeI&&bbuafH#b&vukbl{q$XMEuurf%92+gjF3p zEqSLaZwbZ65~{|oXRsTwfbm$(4j zT#a0pk4ubYl#xGn@Nh4FhW!uqbE62bi~Eoh={AzPyhrK2zyIZ{M5m(hRQ-RZ*gQb4 zBljaL4{p5kr1@d*QM&X!(kP|_Wx3UHif;Bs;s0q${{Np@{Pz-vT<#C-lih)@^`M@O z-lz+PDZfc-0t~#Xj)i92=}GkXH-u{oR`xgMe?M>>&Eq9jbzD}fyZZB@-G-d*?RJYi zgT}O6tZYWeS$MsA1)BDxW#gD&90l{J?!Gs z!DF2f{r(-&baX14_h=g?#O2EF@V;gky?CH#Y~?=Z7a4GHG9oKkI5V_st)0Yw0zYX%?nJVCT#iSG59xoNcGg zA3x3)nz7D%duCa~{{%Q*1`sv{59z7Pj}77dA4Mx^8Pf;uw=bN)7Fu z))epDxyYH(j_B;k41K@@AFJ6+%Sy<)nkX9;>p-a`^Lj__r+EhP-h&uL3tAb%7lnD{ zz-36@^oza?r^V*)6Z8Z2c7l({c7ALA($_Y?uyLN|5f6-IwsmLI(}jQ1@^nU#Q^ZQ8SfJXYd4TNFR8MQsOkN%#X?XT3`BTL4-iv=T#m3_$b2vmtGx!JX|%T|AM z1|jNoRv-E9c1bWB9i5lVp)8sAquuVoXQY2^7ZE_b>3!SDUca-nL#}2@5{EZFoQ%r% zYRIh~_Ae`jL%L(l*ZhjY;`ZA1<0st-KHGe4kx?aA+Cuzrb>i0V6`2bB8U#nCf;yGG zJ4;=S3?}c!v8bQnE#xqfs`yzcTU^CBTx%w|lIcla#BG#p7Jyosz_Y1=mNS$u+h9l7 z8yvY{_C=zs?KjagYAk@+cQqXDm~fAnlMY6_--{ZiS#=e(u-0v{d`F1@e(SqprYEF^CW^moRTV*G9*#`_EW%WeoN-9J};Ovo?)VFlK_n5<#d$t^mP z9mp1x|ID7n89seeytO(nv<#RoMD9={iQ15m#_1!&<`#nE$4f!N)NP!(fulj!WMuv# zKO!s=8BNNX+L8{!Z<-tbC_=CKm^Qcu=$#i;M}=tlnSe+{%;B)7JGM& z$UYx)njZggVoz>Wv&$UByJ9Ijq?zi;{@vem_?L49BoO4SMj5GJ#%twLD(B4F$d8F6 ze)KbI^DGkXgM();>b8Jw&{!Mv!5n#kast*uTMWg+#b`UNT!H<3b^hgdnvN9Oj{nAT zH<1Zj`!5(+Lp&~8lR^twZ-|K@Z}y?ZmSf@y zFu-U$he?2yR$VDW@w-Y>9urVe7H8s+b<(q`7SPUd=0o>z^K6?C zDq7k0Xn^`_@0RL(=Y$&RI1~Ei{K_vlN))fboXiqe1@QgvFAfq6tB1^1QUznHJ#+%w zX?H6cl@nYmVVT+TJhqn>Z^0y8oYGF-E7GBs@ZM3bXLdhXRh;zP|3XgQs58B!UVNkH z46In8f=I-{C+yP-dwCf1crfzmV)^!f@Q;}fmn(9APikNLg1}$_dg;rnjw1h4=w~)) zOQ?znXZ(4D_|oI=R(#CO*)|aMtW$iZzt%=*?|D>t z4IIaJE*ton*>quKSU4c0SPA=-jw`eBoT?YWRzK`^l%KB=IF|2@bNI<$8)shLIXgN- zo6;i1O~A^t*e=Q;sC?pdNh>#z=@WBa`V&5xOF$;$5OSzOHN6Nv-lv#xUi?NxAjr{G zRvNoFfbh1>(GKDF;y!1l{6w)@uZ_TW8=)6?!}*Ww&PX0T0eYW5+JD4NF>q3)!PF6C zJt6dSJVhs|e90uHj3=}C>FUn(Hu0gpexjt3rh9Q6(-drJw`aK4zjWPrkX$b%HUf*h z5+<*FPj4(XT_RAnRsS~%v7)6|hZ_M?3M_14Mj+7saRQ*6>r991@OFPs@=nKY@z`dc&GNds8-dgnk-lk%2w0989!! zUl^It!}cI7+7k@}mFHxl`WQ5n%Q||0EvFTl5+rzXbvy~fwN-sC+Xl>2CIy2%T8}0> zd&48wQe-s6M!At{7yiV$j^ahpTu#APrV} zQXV_PYe+Um{M{8gJ3be$Niai~G}ek`Mtz>-3bInF25apbebxBeW@I^0eEo?@ncfMg z=6drmx!N8VOd5sv9Z$j-2k;s!eeOg#fT}cyn>@ea^R_0JpG(z{c?@;Di>f9PWtvbG zgLR+Lw!+#&(!Rm%Nxk~8>SteqrQzX!EBfNM-M{4rX(hyBR!V|bheeAvODGU=jp;f$Ctl#sDs2aN0}G>4#s~{YY;j^D=Ndim9CYdY2i^`_WKp5Z z!NI{P2XKHCPTL2^&m7)} zI&efRZihFrn9^1?eT3!k#wWmR-%pRzqwe3o|NFPxNWb|%<4YfWt={is^@$TFi2AfS zRXh*o46WXoN06J8_w^wV2=UzEU+zZ^B$SVRmPoxL61QtFn8-%w1)lR2l@d9CD~J*i zi*kp_#t1v+Sp{wc#G>uo_$X|1#F@#BE0~$%_VcfhBXnK+KCWPUb5R}G%rUylAAz|p ztC*jkI+_B4&QF~b`tHr&tZt77GFrT-#vu zqIBI3qK?b-YW_OsP3 zhLu-+3oq`)#S!k7!wqSG{_;DTDTPR1`L5n{x;Oruewde}GhS%_XSCrrHHWDaidG~h z*395@896)!&CM6i2?AUDpX9jcen>deL4I7HqGMrJ#dguCptXv<;7UD3-x>%>bXJ@+ zSLJZ-sAck{E1H$ptn)ic88pzaN$Z%Pa*YOjF=fO2&!kt|X%T0d8K96KZG3V_6>ojR zJK%PikRvLaF~Ktiy&e-xjk9443=Fz{enTsP%^_F#br`KI#B_GZ^g17yW1J|c^-mGz zrPqz(tkVj|a@M~o!?i&R%7jkvD5Abt#_wF_>(zSn-8$s04MH4p+X(AdkItzm`4a6< zIZ+MEAmVW#JN>dc?KPn7p|Y`pG@_}3~C6~%m;3cQA`dB^uUS&!-P zH5t{}B9=^ziti9C=EK8!zVb13H810Ndbd+kYS00bF1E#8`7Th zq7VbQFQc&dd1jxvy;kDB*+oY_-{fM9!$%E14WrV1t|C*boM8hk1d$`9bBVN|-S%cV zh%O}Z0FRH$7?1yXJvXKYB!6ZT;NXdQ#TS6Y{G&=D7_J&U0i z`~|m3jrS#2c8`k$xV?7aPey`g=oO{qeg;bdroW+j01Yp* zXTE0KfxcTyLLdt7=dZcXeFLzVx*DXtDiZ&Z1Vk2-pn^7gHizAIAn#{u60$ng#Lx~O z;PX^i8Wg4446M3KKj&aUR>?F9mo(jDMgIPpl)Sv1yJWTzq3Osyb0PEHk5^KY1rMdp ziTT6>tJt}nyAT;YTH3d0xX-v?=_zIZC3b&7-V6t|q$l@^a~qkX++gl# z5On1Fsgx)=o}!+4ktM$MP+^Fg+wnA9$@)UJArr`PPJ&BPV$6zj{J+B)!VZy$F>vw0Adu#P@-iX4!&nKtP{nU#hd}z~tMhVw>leX5tL4GX?ksP1!~hu8C^;T0h{I5Pp`SC$>`bLesgT$ON+g~ z3;-62Ft$6HMuS2b$lGhP_ejGFVQf6g3sLgqU)Ot&aJiCQ>Q;>;Np%1(cFph48w;&+NUBY3o6^8w(U!Hq;RlJd8LiD}wrOp-)*GvHtet)lW=&1TsO(N!fYYXo`+P_XV)(;p{ z>h4Jq%r0B+t}KBi-*dJyh4#V=L)+)uSnNM8AfyYj3RR6<=ND7yL)(}fDscGn^@Sn{ zRVz~LJZz(d(`g@VNC8z@i#XqVxlm4hw~Jec40fK{T?6xSXM@+6QBAtcfEfg#|jL1>+AEaTd~*P9Y8a-2aE?m zN6;(cT~EB{kf*h(gY1A!YX0`n8wskNW?v_;tm)v5 z%Qb{IBrD&?m^2yfwD3CnBGVD`0JIg2Ym|c3l>D(Wt-sRrC?RHxFzXoU?KwS5(Coe`EW3|Z#`KxB@DFEPA0MQarOr}9SNkYRyeOFUYJnH z;9iMmB=Mh|#a?e%Fz~%>xolp;`*$f`lUic641@Ojr+F0c#>eKhAOAv>qsS6oSB8=Y zV)uea?gF-$E_JxKEq6A1o9|wT-nKISf%80J`|fJ!hOdkFH0ysAUMW;&$4MG1f>Nx!*qQ4w0DuwK`jIr!8mxvw~vur=ELFj29`D z;q1a$-9ZgE{Rg3goa>VSP6aL%lY1YzzBe7$wlkB!q#KP5FXA$CJSN%Nw`!Q+rip3`Cg{mLq|v>OHA$~7{hJf zQ#$ewL9k+G&v2ZsYD4SZiYF*R@o<=Q#dA|W=Ekrs(?|xy?t2er2t|W$R<(>UajOpR z`mk`oUEWOm`lcl5iPuAcHgMm|x7%f>pi4r|E6{x!)&Zhi`ITmpWszAU%Cti;6xhZBMK`I+2nSez9B#eZ$Zu!Le6Kli1GlZd zY}-(p6}RN6heD}v0YY7U$}idvnpDVAg9D=(;s?1Et9xu9kj1-Jazx3p-g7@$lRV0Z zNU>fS+eqiS*b(@f^ZYM2C+&wYx~~2m2_1d$7NyZDh*OUJ$E9SM!aK?3Up}Kx*6uK+^5bj+SQQoh(3IEpABoINv5->4Lrf+!L9%e2H z%oziEhi@>6=ws1}eqgCk{AQXpQSLgW_99iaja_%mPf53>m(9`t-unU^f)g60bhsKU z?ere<9Bm&ziQGUwF$Ap&O$UfJ>C_o3lM&KII{1e0825e$op{Md6}r_HjQs&MYXrOL z_Jjfp*GDFQUx&t+{BgdffkITOys_(qkm7c#^*;ux_4iQy3VBBhaPph^)l;g9@+8M> zo%pXDjK+yG+=W7xZWS)NK<|s!8n5|KQ4VpA84aDM84!gU4(E*LGS zt&+JZyB9DbeZ68d3<6!^9+7^E>dC3gDT*LAXDIF*_NG;C162m6Te;g{>mSBBcqDR# ze*KxZJ5fMx3^|CB8Z=*V*;_%&Leu(-eC-3xAVcP-$mHr7Z`Vfe#4j}i3d9J|8@kjMu4Kl>ao-8VAw*z$%wu=&RrE7vbCnTwO^C~X(QQ#* z9RE#?3V<3@c&%H73%eE&ZJcl7%Si}woKq)VUtKARr)UphZ8_j~tH=g4k*VIgYH2>- zYiBz~WoIo%TNibfGT4~_w>qSZ?#v1+cUBp>wpZz`)fKzAv?nTpwfc?Y6~Tj9dWV*E z{KCs&-8^^yH@MdQHb-Y<+%@U4Qj}R;bi7x_Gbvre8Ik@qEoIHV&VGSx;7rOlgIi@4DMdjEPIZICZBfKHv8eVXh!2A<{;^BR7?maU^-F0< zN}+?*7H^Dv8&B{B{c#;5bzeV}K=8`X7C`s+(N5WcnjD$e9!;f9qGD!DULiQG-_<) z69oCU)xHI+c7EeDs{(u!obhv!edw|A57M{71#lNCx-3R|f^9@@&P8OUj0rlv-rEAZ zuTz8h_Tuyo26E~pb;39Pot^CWpxAm6R*6eIjQsPP(%!@g)^jyCk9YCkZ1=zhJ z45CrBFHJJS?pCEiYS8HCYJuyg4@L}$RQost*%m@RE`T*4qAkRNoYl?V6nL9^P#Ndh zIEq!t_N|@dTZk@Bk(~PwGq=97c>)Kn&a|fEN+N@Io`$yy{WikiI8(m) zMOLeGy0E*y^bX83h*FX2Y6?g9mZZ7rW7sOHoYZKV^mtgKS&>*i)Dk9Jl?$nO@bB_m z^l0xu?ZqaqMnkHmlqI@REH}^%j8by2$axq3*!0s{v5$Sc_hGm+MO0@KTy&Pv)o3RfU7XAP44$71!gkL#za4-l{LQiL!%mp}yfuwX%{=$i zZp;E+eXo-phi4UU0zaQwUik1=K}Rd~D|gw#$J@eSXwJ2kHbm0n3@J}!@9Pt((>Kf) zG!?6e5L+e+_Vmk>llVcF}ke70uX61axlN-SA409=}#Lsr7FYl(@-dhe zf*ep`<+6&Bwz3!_$8~hL3uqtFg3`G@>)>a-*VuDK!J~vn`u8;Y;H8k4$)NwZd!)^=7k*w z!^<~5PI>vn_Ih=L_<^M0QETlMxAM`Jw?8GHn0ma5!6d~<%*bV@up;ldyK`PlD_no% z8Qq&8VVSGxyya&5DycdJ+4po$rv~yg_xfDxTiKmK0@+7DdjRrt@I<2cQ&fjnyZtbW z0Ch@9Y|TGA_=|T2mGJFf9{&4jjfmmP)8Gh8c8f>l!QufqN|KbO?Lz2+9@ROIYbe2S z%9wRFqvQhWyTl^5jNS?4^PAmBYWoB%l{=fmcq}dFA#!F4`84tNvYBGCl=eB|xwwtl z3ole64*57;)~26^PHYJwyUNo#HK6P3x$Dn~x677QonljW7bX**_{g)n+Oj_LUIp<> z{aS{E_q+>l8W;nmN%_V|8v$F&?Nw?UIOkMS@(?fwMSn!Od+a;3bElw_c4h+sdO}0+U9@EzTyS?U*-HE!CQ@HI~aM}KSYO-91g2Pu` z{1>g*{3p1{R!xq~OHr6-8Ui=UN3SnjYYh{%@ojbhCLsQS zfLaAfm4{+8vQqy;WVSxDdCE(tSSFHab~3W>`OL6rUtz$+!d;z|3N=~iXy0xNKUcd= z@4vg$Q<}nbq=GloS9g1OVOLpb+$7pH$Oxr1fN0nMGK;ILShwf~|KMMJrUS z!dU279{$fMQ2&18sN)pVr8_++TA`q(0x2QFj$uAZcl7=LA&}#s?F6XceXQ-oM~lqvEQ^ zRgm@}62bRJ&NSdA#Hcp-^QS^zfG8x<$t-h_uc^QQ`wO81xO{GZPT<3a^Bi!g0p&f~ zeo#bS*BF%?VQr?Le`ki@2WWY(*B{~Bt~;W+$odT4tt0|_o)xab10Xo(dT zHsz_f#PLVY$k?%k3p~4VYQIRscu`+=Tck_pO&(Y+4-zVL=guz_aOeC%Y45mEo@nj- z87rSKv-cDdAz(HEiQdoiB!t!nD8tKw-8*v&8)>_8oRwBcQn^fy!aHYOZI3HdHLux) z?$xxg(3ntki5h@+rDt)RI(mKdJ@OORznV)D&1lkaP7C~&Vg5o(xqE*vDc7@u$CR1_ z0csP}H47NVHZC?O;J1P)*s89*dN66?^f9ld_!rf8vyoSmk)`>=qfMvYfCcH` z3C+$5qc)lxG3nH*XXy#`fSyW0zPE&s;L&j|ewjvM*oldkBUS5^Vo z9*Q#iQDu#ZYOmeP5gfb=o2EgLKgu>xud<@NYu1T>M{tFaGDmkdi3{e7{f~I5o0*ST z<-I$H4T@xHaQR%S5>g%(?t6-qjhzI?%e%W0WI~b+D+CVvr@tz_&+S)Z7rF1d)IT3l z6EpJdR>TC=Qbw^+CG&wOT{=*bO`!p{{rUXW`VoYClq03F?g+iRYRsL0JnJf|f(|zi z;ch%?-JmD0S8|oOJQp)`QmDbou2)!tgu87IhhseRf;|)-dw;uO=a5{N_FRN}Ok;vU ze|Y<}`T98hh>~<8zhw#bj-2@%$D^G!a(9?S)OP=;CMUaMZgWX6llP?WhJ1l&x|dr7 z^q|2NypBJ|JxR)ME7t4b=Xj++ysdUFcio_Hz_SCNrzSzj?Pp%)4j_XDXVvyL_MhW; z&$;lf3($E^RmzU4FF&SX^mOn=_s#-*TzL!-Yf31o&V(DTas0zO8bS>Pe7pA! z%Gb2Dwq9Wtx$WYH4u*nSpndb(zZbPrvqiR2F0thoB zgf{;q?Y3#S;Qn_MomceRF@5fTOL;`3SRIWS>25bnAl^$t8Mnnx+`9EQMU7X_Vw*+GQN`v z{rIOI3i|8*o;TRBjJHj!xAB;w*s{64(hL~rt*&l$xBqH4eP zD-5X2wp8D=Nk17nbiJ<}ygVH}_l0vUIS|vEl}vSSGA_}m(vpEx+e9u-n(m~%njb?} zoF`3w+p2UCp$z+eI2J=D?EgK;J|d}OOhC?}a;eYa*og4DQz)ve(D{?~*4%2MdxG%P zV;rwYz_Tnn0_FYc(>m-kgbVx`A`Hqs_u$>tw}KADJl^75u95?iqg0If&anLscvEjA z%2;?4Uu2Hy{XjUMf#N&+_ybekc=pwieaEV8*&TqXy9PjJ*EGwLXO(Zkec1P)&rCd? z?K#8(^4j#c_`bcggB>PtpEM~noO5bT$cQvt;qXMWurYZBRP|Jod(h-ux#P4lHGx7B=Fr1}^zc_W1q5hHxd5NXp=PSkKlcMJs3I;9ZS_ z&Pp*Ttvn^Wdb!Gqcbb_S7L?t@4$I_|G0XY&;Ya_?9$-(i7%QEe3dtRw@&+W`c$=Bl#4* zfkL38EW{MI)L*Nfv_8y6u!%K1IajU~AUhOsV!`ql-60W&?nn=%Z!}EWnFj%~1z97P5@AcNOa=KJwH-Tj-G zER8t|=s%zFLZ#iWdt39%K? zDOuA!)6@}+n?V*j{!u=O>K(HvBm7ZwSRlKuY+fROC+lKORsgvEt?$EL73zo`CaL=7 z)Rj82b(N276yD2$JK6IroywCF^)fm@;E=pHYLagsKl^pZqHZH<605P18Ei_FH~r*T z5nS{}>1E_GFkc>`-s~y<4=cqAVb3*am@TsOQ5Y3^{-o4iOzB9}KxUQ43LL|o$cc^Q z?dzx$?51&81AfL$4bD8Nu=Yc=QWSPb@jfPm#LHqdzWdaTpmD7vq676DNk%U;r5|h} zX~u|`8R|evVOKs5K=bZx{YF-FtaCq*->W-8G*70-SyB}N1|vdNSnKrz>v7u#ABV4v zXQVvM4f9zS6dXPMluWGh@oC#Pa8G>wnv`I3);c2sGn~L7nFF5J@r;!+z5Y2thfb_%j;DSO-r;|TKqe!Tr&=ElM@sy|J3_BO7)JGIVyMpU&2 zfptle)0WUsm#jPbJKWA>&6uYd-ci;vLd9dI&OE~7yjeZ%ID;cwJa zulY(nqz1bzam30ljW5~Y&Ds0@R(Z@~)~0VNJPpYKOQ@B>_h3cwamKEJ68zOIpvo79 zS#j1>O^XqPXjWEC>S3B$hUECFWl?p%;#2)8&_+rX@$AtbOFNtWu?z66GDV?{Dxrb0 z!kI1KaDAj1wUi16N==e#v^Ffu<%4Fb+1DApmLpVdL-p-@ zJhuZK7(Ayd{s*1ryW&i7!9_Rqf^w2Yq=+-SKL2o3L{tcsNB~; zpazHEX~#aaTAj?R2K9;DNa?yNbqX}kG_AR|fcMEqcdTSY|XJ-S213NAK#$kmR zBR=XNGteQ{H_LKuFSwtd`<999EklW=*Z0!aBTFC1mG*z>S0DSgel>77q5ptb9P&qo z3M9O%nR;H^$vWoMIhF!V)L31EKbswW%gYMrV?T$NRyY{?$Vh_928iyP33k{ji-lLV3_`-_sR%JI$}ws`zwk!h@b=ie>P7J_$-Pd2rK9vkETe|* z*OQ>Z>mN0-s~Gz9Hfr68l^wqQb?7un()9za8O_shDG{jWj+?!0q+-p0^!#IZN0wts z>@f9ZVWbaG&#)x(0d--!vHh%<$$`}lq0?*6H~6N(dkFhY@t08&bmPC|cN0J-#;PxbJ+w-Y zRRQ?Hv8%OT;A+ZN?)rwdocE(|yQIIBum4Ddw+>IE9E!QCw}Z9?)Q(;K`Es0(QG=X7 z{&<=}wXaP{5G*s!z$K^V`Cz+xBNd@WBGh`NNy`pBc)VBlCNFAFLGKnENj)Uvr?58z zH03`NOQj6bBy(4?k_?$RRQDOcIFg}Lj47{mPGJyUMU9}s4lZt7C_5cXa-{Lx95xmb zcUt=D+tiFEWR#0w`&m$Ho|;I&4Ks-g!&JVdf13C_#S_XO2)0T~qkO3>xnq&xUVGFB zLq}LIE<)E0iN!c7py{Qx>Fu(OZ-#otj#>DRSHieH9klq5VO-6kJ#;=>S2?Fg?NmI~ z)cJ6Gf}~HnF);QkqWNcx7p^K%F>^I>WaYJ~Et5o4U>))L&N3+vb4&U8ptFVlaB>Kk z8j#@xAMvt=wDLdGCADDhmK+e;aJi1UfNNdU$ZB~eGt&{xu{Yn=Q;6bBlyQT0PU zvod1KchHI=VUJ`r0LB{kh@w(Ka5@+5ER7zamV!k#-8p%)#5yM3Mo4%#Ck|&dA<}(J z%5(lUj+=Gj+{){Po>lw#xuNr3c48Xz9%>e2Lj}1NC%pWhT<==96bF-e_MM`_5V*5h zSO3Kdct_mQwwAH%9KHKkoM%Jq1ZAxGo5cF>S2NJ^nXz}Cw`OMJZW05*I@!jB^0SVV zXLCiUg;1zQ^ zFR=Tm8BX!fGObE#4o>Z?dfSvXuE92~>KDN9>rL>f|G-;-e>w%6!#Z@HZcRqz8EU>* zqk=ySgs7h)-QCr<_7v*wis=Xs581Q;=XsJpXOh4zjvEkiso1(#{sLJvG`eO=LS(>l zau-sS6}A51d7Wo%)TbTifWq@U8J~+DER6mB3NBhfA1y|#DVnsK9MjmGXYP!1ghuc` zF9RUl%V2q^(TM%uKp5);{Y@IVpggeAxp#-zC-3{JZw~_5qdQ#NIaD;Viie0vmMSppU5+8WCN&oZDXQ|!_fk65fl3+4L+$E*~T`(*@=2kkw# zjf+oH2E{q(j0y+b;Li>QFSE7H*C0YR@Xxb#_7$+Ss$F^A6I}X)Yiy{igyU#Pfse>;NRruXYrZt=(JJ zHil0;ze#eaUGq&|@+3 ztlb~dhgD$RD}Am;4h&n$Zb^X2QWC+GwLruOgrE-sZ8LFNDc(6~Ekrt~A}yEVKf3uI1W%HVOrn{PgnK;-fyukF1LcEs8(7ed@db&H{K0B?js zrl?gQSG>OI<} z&l#-Ye(8XTKkFC}C8yDLCo@IL3tHi%-LgA>~ks*iqCE9&4v6Je3~e?EHKUPOpvj)EhdFanac_HlD`N* z%k6drEw~ab8!Anb#)3^{B2%a4m-Bs%P`!8pLEUx!NBh$b6En|T;WgiF;@j(({Q=PTP&~ah0uK0XR)Oz zu;s_ypSjutxv$KzH$`phm<3O=5X2kP;_0Nk3sT$7vWl9(R6f?J_TE0DrW-QC_8ZSf zO7dJlMZP*wWl{bho+eA>B~H~pMte8055e-**s_P)dOG+UGf-p4{SP#Gj^iw{ z#fP+cRIOno~Cax^VnCO4l`dz*4C3UF_p9oDs&(sw;jZ^GCePukn6y^}Li%tCOOH zR0|rr;az8CH0KzIHj`zvew<-LUMgq^7`MOKWa4H1?1AIo`ZdWmj*D_06HaxQZVUPu zwq{IX&&K3$pFOLnQONz_LV;GeV`-ycf6*H1*R{E@o6K_!W^rjUp7gm-^f2u5&A)e268=~U@^rfp~E zhi+iluzUGcN8W}O=#e-6Y^LCT=X*-}md><33*>tgH`{u1jjU%p3AFptQ(mX>X1Tj( z6sOP_xF4LG`^EBWiKKedmaaqCThl>fH;;W@w0px&-=>7`~7lzKlNp z>gt2pw;ak)xz~v6z{boLX_Ea);@j03=m1l}`D5C3M#t)cU+&EBLQA8k7xPT3_<~2< z{X%a-mv}V!W*4eazm#A*56U@gKIe3N1G|C+t}f05_Ackf<4gA2p^Qy)XJE^Hvk%fL z9QMX@O@x0e%4*$G@T?G&t`?-xM?g6XFPp1tO$E+kFA#!qJq#lNE%M1 zuW%3bk?A(OOmWmqinO6Im24fAA}AT6oIUmUji2WwjQB&VHM=Ojd)_t@n&vL3ySUqUu>FbAJeN2d%b%npm0&NWu(SQFU8VkIHt0yjRd*x36w{ z(cZ7KDQaNt?k^$S0@S0c6KO0$R^{1czq(GPfglZ~ocSW-X%b|$4(KO@q`ef0oo6|S zuND7_{-ZwTJIUW!iOllqK(_`TjeDJdV|WhMf&96PuF)im^@^r^DemQLx7s7!A4wcq zt+@M*gzS^<-9YxKFsnbP5guOZ9^FeJEx=I^Mtst8s-Rpm9o^n zq~W>iUBw2SUHO>#zVf8wFy)DhZxZT?+s{iFZ(>l|TK2YyJB>b2c8A8VXR=esjEd`n z0cI_pw8A_P)zB^%r;H&BJsRF^i6r&6nbEp+deqO9kX@q|O z&0X(@y&Nuhwnr{;y6|T-JNb>T@WS6~4dpXu`hpR_!504)udm2SRi9l@JvLxsn0Plb zTQ6EyhV+URR3J`zGCg*QIlmxt5Y^fD@dd_py#J=!HOF;UY}c_io+Lwgl@8oiYJb!tz{7T?i+WO7k zjXmFGC${JfiC(>cR+%sX%z4L(z3IZE*l{Y(a2H*xSB;()zpq$t7-P#zZl~1dU|Pv zal)Ql`QIp%q;CrJ`bJarMChxeghdq~H7)19u6a|r&NwTXG##*>FM~$Q{m#7CDULxl zX#Isnb{k_JbSi8oE(BEgTkE|xH(TTFLp3Y}^4Nv;#)hb@< zBiEaG?iiRvNMNhL+sUcs>lvEEN79NcxvT?$0G?9JO*OP+2O_(6CAhO#=o6x+KnE}2 zzf&I#hU}Yb*w;r(Z8BvuuV+Q8(o}?qve8%T=_J?C;!a^zuN1K{G{$OtsR>{!HBcX0 z(~|AU^Ez$95>dXd8*SQaeLwvTYg04^%AsE`4^0L&K?Wsy2bGZ5e;t`_$C$TVAogRK zIy%=cR0k402*LB{9f(IX0-$08SBP%z(DAC$WSmL$z07Jf+;cWeWOWgLhL(1H&u&A|tWa`%wf-ETjjbcww z5i{_fiDq0B_Z`Pyq_t=n`Cd?sr{PuQ3WR1F>9TPN&Cla1RGJxmL@OAdQ zd$+VU1C;84s|)Xic;58Srk4%3PpH}EDoIF0Z`n?>*X0E^G4z@CT@3N6dXyTGereXb}Cf7^!%U)&^_)bj;#a?HQ06Zc+wn5)0&j5Kx5P-MW&!bj-88$7lw;}|`5!mffp<%gpqoCPo#$RH zzXCcN3|sdf9;gl2Oi%s`@As*|Kl2VSTBg%^LNibeop#;yx^Nd+S%KFS0a1tX;4D%)*FdRlxPgIzk-q*@ULYW;*dLs) zq4VO`ioNB&zia}A;kRi{rc3sq$xk=(+D2+=GD>;LiTXDV`y51Tt`#MsAAV5LM^!1nXPV*v4%^Wdk*)h}y-f*1P+`lvr_-0e@=O+rRZhZza_B7D(u3 zv*E_@t*FhKD6Fvn9%Gk@UM?$(f!5cwS&&B?2|!P#p^A6LReAHx0(yCC!IFZ*y($7A zKap%UV6lhY2SiQ@nQ^p~F`fppS1;ZXFZCr}(g<7y;IcUk&UPo>_K)-^B;J&#F^Cg` zs{|ElHTwm7YwJtd6!JfmEyA=9sLfti5e)3ZMHbgT7!RTdD7?vSh%jNNp*KcaPPL-L zGX4JAw_X}qZ7v}D`qpyDT% zDAGtbh?J5d49HN@-Q5y`fPzSOgX93i&>^jK4MU?e3_}kcXAkK27teXFbN+kZ{fC#p z#9C|b759CABB}9oQtVus2Lp?H3vJ}rMHVE~#Qkf-+qoeF)dDm&92xJZ&()MN0eQzX zhvgg%xQ><~0uQtnXCrM{?Q4 z9Z9BShH^z}vBQdogW95tEBGoPy91g?E!3!84L@Ti95^s6U#lx>cQUwmaF_ZO{3m-l zH8%ZlF)>lC(sEd7aEPraW$|d11sXY{G{CTQW^8nB1gr4)dC#i$tXO7)o5pGQk+nl`*~kuF8GwNIfuHxf%~X>l@~Gpj_Y5x2dA+?K z5tmbVtgEWrt4+r9ST=%Ey#UowS#YFk!_M20Dk$UdG+cFU%Yc7z_MWzYQNx*=Pr|Bu zg$3%^QSyHTowxY<#x72NWA4WJw{#H=#g(B6IJz)JyhY)n=Fvp-iC=t8v)9?4w{)&R zu^9&1-%EVE024N?8+|AVwK^88hYi9q_;?)(rCk+yG1GX-f~WXV0R{`?r@OCEko0>d znG8kxSnyBu;n3y-*2wCa8PUYGaCtc)c92+>Ts;$g)H5u{(~p$jgC8Oc8N0?nE!3rN zkTc#>QEyE1!)@Dgdiu-OSe{V&h~5P{3!t@g$i(}WU!v`K5`hOjFX$fhf$7+3JQ`~f zsBNkj@S?t%=BS~b8THgltYFonO_U7>-x?*zne}LA-_hw}c8vkOe6!fL#Ep{%G*l3h zqxR?Vrao|*pyt!t?sJMqZ1(luD;f$++*Q(qdBs+9BqT2NZFZc#Cb_fn+uTDhm^444 zMV@7a^s(9nT}2yZd}R9gZimaVQ<;9ZSiDoBDHcJISE??{Dlw^1C+Az>^!I#7 ze7$PI(Zs!OgTQ|N@My&lk#Cp^;JKH$(BM<<_%eXbj(R66?_%XhvW*J$jQfE2~T<=B8P|EX4t4$WQhn|GWNHY+n` zd25zpDszb_*0w@gfJ^GKtkJf3ovN5Eh@MsBGqOD&z~{dICG}-O`WAE>4bq5j*fP=N z)u2lf&o`kMy}<7C)Fmf0f?ZB-Tx4!MiM>jt1>`6Zs?b3-a31#1xu%P7O{WLL#Oyxk zpV$mf;-A>;Xzql`C+1!fgdmEN%zl7C!411W)YBrlBGC@cTwn`-dFMAfW-JISgHLtx~NVb+jj8-`la`1ujZG|khPjb<%UagV*=G^nNr4imz&X$>d z5HS)GZj^X;pKXDqPE#ZHDPske=!clrE+9jehpgSk@+zOQ!xHLb0ypeM^ zexO8W)dM>&XwHm!ZPD8}>(1=VDXp#T``nr|cjy3aH} ztUmv6aI`aSfhV&%cD~L|cw`H!3V%-CsPS9y$r-oGSyR4M^vx3-dkXNeC|}=Elds{$ z#1_n*1nHz_wQ8&B#g$n5#uTOekOY5#-@*W~@9Ne9THkxqv9+PdO-nWo6CQvv5ycJ#@d%W6pL%boYy7BeO$%(~e!xhJwa zh$FHsegzLS$*RMK;c1h1dY$&ub4VshMN#Kj+w~sX7**R%JMH&s|IV7Ya{Z>v*av8z z=)~Hq^TR$LZpY=<7n(lrMigRcE}|=iB+oe7%%enSA2f1~j=sE=!D!pl>rE7d1d|}i zGls3$wQ%c=ToY-W@9WfvoD*UB^q&a&<$H+$L4{Q#$B$iS0PBu~R!{|HW3|D#fUcCl zJ}<6;=BcQVT=(0l<3Y{^mXz#UDJ`FSh6~%+F*)HWA?G+n(plHCYUJ?bj?P=fAu09G z+(OQ!bB=ajm&^@-nFCzZ2aW~A)$hrlFo(o0x9^TIm2b2u_6)2Z4-NC%-vEaJ$np2@ zIxxDQ?|Va=)0T~yU}q>qGEK1~hU7p)bD7t<;s=njYltU`eOA$v`iR~ts?`jDg=Y2V zI{@vOj7NX7sK-fdtFr~t<~TmUwu!lxuYWA<_fkF8gosv5!oyDEb9CZ={9%1w5yZA< zaeM@u2!Tx&wo~v3XfsB5o?O1MU*L;j`UH+#LWWOF^h#nG8qE7diV%|=!(7y(|36~6 z3!pFX;{W3!uXste}8%*!E>emgM0$_&+_rb8e}mS5Nckzj&&Jc;gJeDnkK3* zl#u~l2JShOdLJMqJ+*kPmE@IJxb{_I(aupp%LbUHA}}WqTY(+s?4`tqXKw)lRd<6x8iiKAJ|7{As&Zkq{z*)XfZ3*Xf-#%3Y=27s1XE&@kiqINBKcUti5mvQpQCQ#?H4@I`HCH zMCnB!6)nP7P;Aq25qnvH-dJ(5TNZ9{pz`I)jSpWa$mJoKFXOn0zG4APKxQ!jz?*3{ zh{GNDyy`SrEOz{D?e?|;Q<9xuX;X!<8YQG?pB0Zpsm7DIFU_D2-qwQM>VclrB*YYV z8u4Ia$xVH!;-JydNccoK(qUMAs-S3We|3A;3|uloajpS6IOH3**$cZFO2npq#2U$C z&?LK4Rr=@Huv{Bm4_oOUbn3<`EA|l|k`5mU_~{Te7a<6CSXv(0?c(|U8`R)G{7>k$ zEf8ao3TB?6v-|YBBwucOxBoL~!m}0YaED5VT|2oNw$!tP3SL4*p`|u2%E%mZ#dYJI zD!m?f($7lM=H&t&t1Uq>zu!5iZY^+F(x+gD8C$g5l)=(}ZaP4C8&9j&0d#Jsa*pw- z)8`7o( zoi%@h+XTq=fk>^T-C?1QMm z#5q7B+a7`H4n!m-)r{Q|1e@ z5=gFj1P$JF4}hX~U+a%tW33f~dcW6Hx;Re3nAFv*#+3&gKWm!2cFUFKTkXsE_&b&X z*ovPPu?#NSSZG^SCTyr$GvV)k3R7hVAf9ZPY8j-EtG>0Kkj}B?Q)>K(8_1lw-{qAJWajJcK;ym_RfDQ8SQt3o*Q?LSToTvi z!)@jQG=`ZuJEOQt%rzH$>HQ}Ieu^QC8vJDC$`v13`)OIh_p}#t46W-G)jqk^U}{P^ z^j-W14}{P@juLNlp@=V15w8rQ&B6?(sw8Sf2vlmdvDvHDuq`GPYS>_TAWC!#K*DCS z`ev@*eYF}WoL!lcP${t2_zWCWfz8DKkWp{Vjf`9s3I_ruh5LjbFN;R}{}hdenBUi% zI`h~-UP{&Cp{%EiIm5q?Rv1L8#nz0Mw%oLM?y$2&s;F7HQZOZ>WyS8df$8gYKHY!5 zuNdq76Z&Yk@VbOqD=3ZRkvc>tSE@ZJ(n_Cr zIm$@J$=L+2P&3#4Ec2zd@y&$%MB_Go%I{quPc)OJg&~rIdnWR=F6y6-l-H|$4KH#( zj^g$;)+yW?Tn`Glm69ID#Ff_UXB%Pa>%*lm&D!T&^hOM*Lfz$b#7GxensBArtt0Pd zMqDlr-~;(N$o?*dOMtk;GthX=uJRP8kb19&NA`c$hUAP-%{xB693Exd0qwStEwH$0 zsr2A#n`J+cKew6MTF%GEagakp25s7oz`$98sLSEcY!oE%n|z+fG@?t@#Y?lU-KpVP zS;nUH#5St8o+aAKxi4azF2%+d=-U=u0oYJfglhNyN{}-V!^C@V-m5K7LCu zLYCYEx}N9(WvqM$&!He@ZMX%5>34d#ImNL=>?ccxA|10eu2^E8sYIcdC`Dp+c4t_} z6N^#C`TCx}#O$n>F(h4L{A}RnQU6#Nye&_H=ld8hZQlSO#8AA~DZF5_3lH~K08!Ovw)MUq zokjrM&Btk}y+n4Hoyt!I3{C*#5CLx4vaz@|!5w-2q4J4}doUWgREA{h@yl{hm{tCC zLc#Hd=QzkaT^rt+u%DL0sbq zuH~0gEI;lHz1RC$GdfnW+A>ML5F7Wt3`n@*%FY+}kH3kbp>u-stH``taBw#t2x77e zim9--az*J)@jKVpkBhbO;4_%)!DUmPsgI>BEs%HpXQc)JhiG!qPeM%8tB*^^a+#HU z;(?kND?X?zvFzooZYKv>zQGQB2mqM&n;^-U>MnVJh0(ljG7t~m=wwm%yit-G7CP9Y?pd)rQUOe?iVgevF9x3ion-P0p(hOoOF@)|oc zs^Wmmoq$u4TmKVYhnSROhlUXQbvSWw#G0~b$Wsy@Vg)I%}Rs55PX*b&T;6LV2l1^l-#t0VRw zGCefVy1CG^DE{lPdUR#}L4qu3(0zdHUU7V9KzyIH9zM=H3nA(@; zpR&(;B9{5MWV2&dfjPz@`=9?INy!N2ohk=_yA}thcdiKDn+XLl7(x#;fs6{~`d>{T z$S^XZfZpfbZZdD5M;CCZ=fD2=8t7cV@_eF{_WzF1!!8L^h`>CD4_%Wx%qtxR0s)IwU&PyvjMJd+i?0s&~E5MT`dlv0l5()$&Z3Y4c)GfwmokYd( zoW{?p&2^}kIG@kKP=g6UHgD{YKHq7FzQHq|{@UIUQH_CR25nM!5Hs%z`1ELBg}bx5QDe+UqX>1iS5kBRqw~gS z9#>AUy7xe+J2g^cSOD8T6eun|L6he8>=<~t)PFV*Zd~(|8xR~slI!9tX_BRt8Qiem@jP_1HE=zryI$SU zIt}0gC-VkU#N#5R2wT{d7x|ZJq|J;Xczq^HRG-Tr(_0I%!OlL$r@x9*7(Q3ICRy<0 z6poi?W)L%rYIf$V&Ni<4D10GFjLxE8MnJ<6xZX?S*lT77(PkpO4GC;4^TEd?Q?aNj@wY(DG9}Hf;aU^5!9?k+f*#0; z!3C)8Oja060m6%2F&au zwQgNN+CiyI8c0x+DQ9WKv>BOG8d7|0=r?9A{ zqOrQ2+ccPb50SPx9slBB#kmlQ7j>Bk!Q^^?Pzmrx(*97Sg(OiL!v48Z<2Gp6^R{GA z*p8O`%}-4{9+M)vc8Z)rO-TP?gL${$b@-^rljY-ExeVQFPPpRSv8)8Gmml;CPux28 zb#{>)6`U37(1Sag=f2%{$TY5zW7D}>0ogWTY&tyJGnzm*q_|mDTbN8*)o_V41QUR5 zKi4;06i<>5(6BSy>1yG(%GqQ?m-`JyG~F2+)OLh47^APNl@q|*P#alz6(T~TUisof zU!Qt~!?RfPk$N#@eU&+!{8GL}-79V{LB#MJubjZ!YkK1pEI$@GErIfo&A1xUMeyDD zMX#@z3iwU3rD1SnLPJ7~zh%S*3G@|*71}KG*?QqrB=?2(^>fd{tzWJpEsyUB8$gJh z-M9`aqpw1ek zDZ?~)D5(i^T(e5*yQqPDEvV;II()QWJ+3pRiXIRGuhn_HEo5sj&kDSb)&<@4#Ge#i zDlLDFoqCwi0iX%$Y)`~6H@NzPnf&;bJC5E|C1#cAwA1q;%nh@W`Qp*M+y6a)OJO2k zL5OKh7p>Vb_*6FcPbKFYU1EOa;_8`Ds`KeugEsj{@yYp6CNqT$AnUe&QaN~&xAE+; ztpUNLr>wH<;R=jw!LktpGlRj#J!~qrdE6I&BD4%bFM|%EJsD#1R4ko zd+|WAw1Is<)dd!y>I@5LQ?m92!*a&=0=^$~vZMn1(h=)=^;3C_O4UePZv}{Iagm`h ztFtnH2K27oOZB2xIMLs4B(D95Ej+XrE= z-}nm`MCF8m{{n)*Ibx)rlmTW+4gaQ0O_JHMPl-Y(TfnL+twFB z24u6Q{(8$&nrJAkicLRNS)jmv;n=>#YD@L_YwM9G0v)?mx9r&AHYN&!SNY1Zu3XQ> zNR>;0Ir@}S(qI?b?or0mVx861lyY$0;^5uPVR3@vEt!PR`v=UI_DQ9cVR`Na zAUBV&>XdJY+;G$u9ydFk_z*w@&>12!X*L_fYB5S@+$d;lMpTT^&d+-Pev z>(|KS(OWS_xrhu2cf}_hSF*E|w~k(6tcZ};waFH60v?Ue_yYC;Kxt#J$ZxVHHKhX+5CG*GhrS?2 z&iqERef37s&oj#~U4v`H{&frl8tNAR>DW=LWmoHD>n5)ly8LAdt zkkxKicvq^m1DV3;@1&RcY42@EGN$0*C;R`QfBHQbv3Wd!(eN*T+C^zsJ+CBiI*}2R z`SOo;{^k(T?g_=#BE4ad>gJ0GW1sP7rg#35N2K_j3yR$5n~L1ezwP`qc5T>?%OCM6 zPHn8KT$eWajm5o@o-S*D9U4Zx@7ouRM6sDFb0oMR;$>GC-hTE_eRBB7@i>GG;XpZl z?)koAtllfxta`H^2Tn$Dr+K#C}r!nhuaIZGjEtK5)Tyjs|10P z&OvxD`ci6UGrraATYt;Pv(wBdd*PNiwQ(C`TU0ml#pWCkOS|&C1mhcVD;OyYqXVozzp#uh>l{o~V+XJyUE+X4ZT1hcymG#a{TiJkC>y2AQfAjl`%v_%wGoxsRAC0}xhZw?@yzXoWNu*bnDKoJJ{V#q0#+);E~5D;pp2s->~ zO(nM?YGq$hZwA>hqyEP-pk#1XiIYGuDXTTAsphno(B0B8z^jiHuz9#LzcTBT$`NlQ>e20k=xPEDWJgynwLq~>hW^X^Fy?test0g)7Th0pz z8^lt>OEbI-x1vl0T$$#AB2B{F?*7qOgda@&^7%#|Ju~E;5r_yLxYeX^&y)gHy5HSY>MWOvVW-PcOVP z=E8)`c8;w1Dtu&XbjfX&R5sMPJ56CBv2j>rziL{@dlBPsM&aI92dw>$my+Sj(qVap zwuw5^`WXsx0c^E`ide}rAp`~dHi`Dkn)g}*R|cs%;^7dCZ^!gV^fTKNa|vG8EirI4>>y{Dc#814fi*V8EStPi^?daQ*kz#H?LruaGpf6R_*7hBqSY7GMO}P;N{{a%&1LjJM`>hY*d6p8v z>)r<88pM-?7w!VL3JATlRKd`O+peMMO8o@>Qg@C!$(z-4{d0Pjr2h9_6- znv*CxU{BRk%%x^j^(E>pu~zPOHLOll z01+h=qXkbbx>kNx=;+g(UeZ7?$xc1~0~Kt29NY z9@Rc#9%A5hsr?#cs^Ds_cRE}sd^FW|@-DMZV?EN+sDj}K-tZy!-5GV0_%C3LQ9vY( zQxOwE`IA=btKoKV@6+TF-e@YSB2cu4a@iBrr~76to(K0nZeb+k*Qp!SeWrMw;*7&W zz61vWtxCFVRegZ8nZ0{CG$oJjJqzR+GXrZx4X4BVSuKwqizeCb9C9{R#QB+>|AT7e z`Jl{)^9JRueBisb4zE$IbIoPQCMY$f{^9%>-rUqjEsSptled292=4A z#=<=AD`-eLydKPlQGEL8zLdVhbDSVv6GdDDl$S0I*rr|rLSRhddA-C@7+03W^|SMa zH^Dn5D4LLmf^Q@H2prb93yKSyIY3rpnQ*IomOqR5?QJk;*!N^53B3rk|D>XBC2TQG zwh@^IYB@7~mhy+P%4j0@Sw0#&PhB_rv*Iuyls=q+)Q_62UIub8jzSkIbIZIKiwy{% zJ>PJBBEMYNrLuGHEm}(tcB{{w0o|$MqzCnlU;6wVzra-@I+c&!;)T(NZQfdsTk3-! z0c#O3Rn?AJ0{L9Sx5j708?#ph zd$8scoF1DQCcPRSf323@04|yaJTOg!BqQi%6Z7mIec#!d{f@X97OP@=y?TB7s&79t zZ6O@3YjNS3fJOZ5=7cM`@A_B1hJF;miDr1=Xr}!>F0v_i#RqK13OG<9F5MEWzuQTa z3^~=;=2-RU)E@#^Jy3acEsu{;v_=~lW|LExv}L(;8gO}EGdqV*sj+8k{I%$U=vE4 zDkh4|*0o6<%O}on;-%EZN1mRr?{>^F7qXJworp|K0}Q1UE`TCwNIX<#q1|csou;sS zLnAZ)R2Y@YidY4*-bR5I!3;)dlP=IbLt^C17H;oZEHH&%f%V{VE!D7UKNXuRQ6y6?1w;bEXhq!kF5026Q(3(JRoBBIt*I?q!y=qYGRv?n*!+F(MD#OYlphhL~dB3cQr9 zxBu^&4G=7hFYB`PWt*MQ;ZD|-)iyXeE#p=8v1ceY(${?)(Zn(4)iP%OU1!V{O(Aaq z)#yu)C_yR$>OSI>ZTlhU`xGUWRmdtV=^xVs-au)Fof-#inqt(K&i#blvDnPxa?jJN zM7Q@}kGDGP3LU%g_w|e+{^iIwpxUF?;bt||rKyEX1C$$_00jgd56`ICr0}W_Oh&_I z32r?a;N92xI@Rq?7BzEz=E8v2fsjPFh^zPMzCedvn0LIsngl}CysgH+7}G1~%U(&L zUmGvx^|B6lB4MZ#@D3h9wR2tmfSk{l-x^wZ%PLC)nz%;o4|kt)-M1_4+A~r9E{3i)XUK%2Z%m zt_Hf@E!z-d@HPbDkD^dG9cK z0>=vAH{H^I(hAM5Q$c6|pap`(+-- zf`E^i2O*Fxxd6>w`A(G{LB=&)7+5r2P#2Da!BzF!T|7@$#D=3m{5koo9_8bG=9t{w zbLvZKOV4V0fizkr3+nZ@?NR>PIdIyzvdj;-(&=Lo(OI0cv)l~p_j-Ozm|4qXKJP|) zJ}A!J5$j3Ku-<= zfkHQOfuVVC4CwxUlVJLpQYhXF&xnlHZob2LZR7DBdD$Tb6OXvE!AS1!V?d_u`LRI8 zq!g8a|N0ge#tpJepA@LM^#j7FEygyRCb%*9p zhB#N|LWL9P+kci|_-@-{aw)k0Kq5!2XpD1_Nyw&1Hs1&VIX3yNOzoYIg@g^ma}U=%dAms=VXx+3Ain&)152{G!%l>0g0M-PnSyvY#L zMF7&2ircey!i|6N*Ob0SV*It4YkZ4>tc^MqUoDLUbYn9L*NXO(;Y$vp7^YBEjlts! zHH~fQ4V%_34=-zUF#yQLT)$G+iRaa9)b`2?0J7d!3@n_IdxWk3)2qKU8o0}aBf^Cb zfn()^LiDG(OAmqMXJE4-ouQG%=66*321;r+L(TyifEtgb)G|U9rQP8W1k(PR8)V&W z>PZ=k>aNV`Ls7|Ni-^x7Vg>zLm4KbkO&lS1|4A6|j^PMInz09uTw zYxq-g#K{7PzLyBBf2Y4t@U_}o0`!~nvXsQ=yp&=RzF#Y0MqYVw@Jrqe*8`!k2Q9nj~#-;$Fjk0L$F&72F-nJ0M>PY-6l^eEWKVHvUt{y305Bgg7dYeidiDQ= zUF`vS0yUEAit!L1_ggA|c{J94c{E5Jqu^JA#4417p%_fHCUnW-=Qys3P?e=xuOEr( z!^G^T>Z~JJVsZkaH)G-=5>7USdpLh*@(5`{Y$lg1Q}0?;>6~%xM7YP=PkX&^`J|v4 zuwy54j#x!Bre-Mjp8j<_07gI8#*lfU8h2k-D7<+eas^p zdEQy+u4Gn@RFBVje+v!d#$TIc;gI05k3&*7kxjNKo>EPH8Ahz}Tq5oh+{&yc)2E)U z-UoZsgBdV{nX&JH#k5}Vh*?VybvbCl#clEnYJwG9I~dLVG>Hgu)~hA~BorNHwG=Sy z|I5`9Skjzol)gOrL#0Oa{-yX6@ilV_W7U=}nc1pv8Lt#Y-i*sH5+|GFsjY#jXJt%% zFyXUN*AT+~)?zZ|MxvFnN4(*I4vC-^rS zJ~@k5hGUJ{@!2ge520^UGZf2lMG0~rLo5*`%7#7Kd}@wrXSpx4yvKULFBEIDc7yw*}aWJ5JyV(Nw_Amh=t zb!G69Yw{e!HE{_lI=iiMen4X%tp_vLG*U@lrU8Le6P+hUoh=8=UE*gVZjp^R zPwt{u87jWI?EXmFM-}OHg123YdgGT$!J}-vD$oawg(m>O6Tq#U^a{)jAZ56tZc2f#oY+jBNRIXl09e)O2g*c=r#aER zZd#$C`|1dbR#gw6x~Y70iF(_cN?I(Iu7gP9JYQ8~ zYI8=yVA>FNN!pBnIiK}{{B`V>;wJ23%Kk&9Ud)q-1lu-9q(3f{&hZ^*ZA6t^XNV!@x?xBYhVwIY| znw>u{S4DuBNbd5~dpl|ORP>=mbU#ZhaNJNX<>J8Uh(2|UG@nyzJfvn=73tyGdV-(I zPNe%v^3SE}QuO!-%(v^yQmNpmicXZd;>#=U7A?GdHrIQ@l#PRqdRlXc(;vRm^?EC) z|9u$rG6YBMTIZ_Q4?Z*B0+VNUgPyHfF#jE^&YR~2DsI9du~H15%6Z}*_NyNr)n2<> zhT<<~2**M7^VmKZ=_SvwTxI)~J!NCYQPel`EBZM~UUzP?#pT!Q6XDxTFQ3m3jT_iX zY`lNfIjglP`LdO>?*6h`UuX;Y@nrvw5i71OwQ<8@g6j?6Y2P?vmpbr)(%)_vCX8^= zA*PAa2ZX+mQH8?wXbtac`L&d8B^w&HvbUNN+_badVKO<84{j$Q{W(fro3Yxg#;Ft# z(D6=XP3WUiaEHFnQ#&6gS%#z&MVM$-TdAMOfVidFYbUAi&36(`b)JgpG_l_teYK*8AY|>=23?2dS_WYw69ZgN&6zB(RQ%PRF%7pG~_hf z;*>HclMI$Wxm8o+KK{9Wl<@NLNKP0|i`#_6(7K`c2WTx+Q>yW=`bO9*XzXtqX36io zm>Y?*(ia{ zTl!5kUJe}2dxC57J$V&hG)Qqd8(VvwLOE#c!#%_0Ht}4?CzP#;{&^=h@J>PG$E$O? zZSPKh3||tCE;VmSI)4 zT0r>}Lh%$VDOsfc>yuSR_OI1$Nkau-%%mlU;|?5vRw}pHg;4)(En!5o!56u|-!Eyn z01VDQf71{c`SFC7lL+%KtvPvib~eA)QTgb#Vx9Vsq$KjHs;ciZGrE(Qw-1*+yLjo1 zYT$>QuFlSTtiPH+;4v^T+|_*U?JcU9Ecm1%YH5A6RKk)pCNAz{sX<2wD=jT;M$89s zHo}N_%-d->r@lUsD={6QEj6NyWz+VHjEo!wUPEiH@s=+3;=v-D3T1Ej_$rblzuW6%t79Nn ziKhInQ+__!3rC*-{L{Q2`MfwxKh zU>1msl9JcLFE9;1`O|RP0zC_hP@}GxpD-JXox`P6WO5U8v}T-u1HGWrY5+Fg18O=t ziOctR{_~#M`S}1~I3C?`pz=Af`$R5quPc_lBU>(Z{;1|CrBS~P_u$~*hY^W0YP_us z5A%JN96ziUJGl_Qm5Pg(7gzoJt1Q2@ol3%#ms)958=Oy`F*E8;YDP2mVY*UwosW;t zjgU*i(PUP(38Jd&GSlF-&7#dXU!=%X`^Zw=7DP`U`H77^=c#j*j?N@=H3CQEAvj7Yxn(FJJ%_w;8N9MkH-iwFOTj8^~DB5 z1|*U{SG}fMuy`@|W$5-z9j|Ph#*hk{9AV52`0?iC^fV$aj-i6-6_?FM?S^P6=oM2J zbE4bB z&b|YoE+6^asYjEvrSgzUs_;jpJy3hqSHFN$ey!BUa1rvKx%tWGb+;dP1ky=1To0oY z)yK_&D9Hq$U}GN5;rm&?oCmXB>Ek`*GX7)=$Beu8iEH)R3t8EarZPNnaq$u;G_s3r zeqkY42GiQYn@lxJC$RO=oJ7madnlI> zcyNVAIZNavalG4AW%=x*5>SoJf47=Ub5*s$7xQ7=tb&4Ie1~lNTW|O^)dz3u+C*r>QrHd^qCnv-MGsO}p{k14o9Xv{$4;77C76e%0&rKW`a5M|TlOkm!$*Pv>ghE&Gw&9fc*I+||J12^pF# zl?UXOKm1B}$uwZs73P&>^jE$M#TIfX?y}l34NuuW`mSwBOfK>Puh9Qu;mewwoSaXr zR%NeE(`*nAH6j6e@dNzqM|7UDJ@>7|*)Zlr@1f0Y14$!Kq$r-m>p6^2e|>*ev~Rd^G=^h!hyTP4-cV;!B=Kg^tsbuu zv>t9egMjN0s3j8xz1}+MuUZQcwV+f7b|oUmy_WJbG;{Os`r|06Uai1)-(oWtS5dw< zsfKz}_&+K&h**t$Sh1|BnY%hrvF2x-fMkDq=aro@&ex7tlUX~dyB8;}z99)wgb*PK zG5FAlST`{+>0T(RJWPK+i=Jmo6c|84cxuI3bNlB<3uSn467`|M80|P=H_nT}Rn9c{ z!ptb}i4{`Ra@Ea%zggFd3~TN49R*U{H|liD#a9C46T%-VtEel#$Ku%G;COuH3@R#F zF>n`o&%5?b;Mb(bb)ur*jCIX8;&wc#rF)8tsEQs{F`AFzuFr1pzl4la)>C@EjZ$a$KCOg5Yv|B*8@?3LpvQZH<@e0a`cu^n?3EKG zci;d=RKktcf_(u8%x_nx_z7?vx1Rc)z{C^0m+06g{G@1P!|1F><~9l94Q;8~8{*r` z?q95vFO5jD+Tg$w9vmDDJnOCP?VUy>l&-(70#i>_QcttH%w^l}~#>I^sJCVpQ$kr3;+xc$B>QEtYI^n@80T zDI(GYxY!=vufW|RM!cacQ<*IlwEVc}c6?W4gN~e0y+L-mau`JW7@@^iP(dp&mvta$ zlvEjPaTnVzZs!d-zNVpCq49a8w4S0LcjOci4WSTIvF<{R2gxIzW4nTFLtvI?7)t@G zne*CP&;2Qv8(nPV!mh8`YWwq4o&sYGY`m9ad;<3RQXsmI9m76;TY!#R&zEcEu*>-4 zT|VvyKW!A7-Vs<8ej|Nt!u}~`Pp~3@Ahr^3HiIzHco`^*5mG@(Z)&|Lbw9ts;@x?Z zZ59UVZHOy#ByL`~qPF@Je&d#;Jx z{RNqV)by*UiFfHQ5IgD+r951m(+NN2mHzy-n_v_2Lu5Nf&h)FinO5|_cSVQ zeBdu(tkL&{imSYM*1CMn1Yx&b5Z<=a@hiLJ_l*}bVaw-+3@;W8hh3jB#oCW}NbPMCA^V0qN5$~>t z2I1Y?n=6^##;q|T&hPzH^phxEd8h~aE@>L@$c$ltQ1{eKfAZ;IfZS@#<+U8Kd3Ca9hrJ6uklT3~+hu6r3K&CnGHDy*d_#uy{ zs^CAbR>(#Ilkrbd?A*NpDMM#{+GG>IBLPsN%g1Pie-cGfO&-%H}k8`uZ1 z<@`f`t%-?WEP*2Xp*FUkLKA>hTbVhXwRRKeUY8NcVqYpdW!~dx?YL)pe!wy0=hr7J zwa&}R~zm^les7Ai_uWmd&2R{n}B%3jfLKLwrl_$G9nMX}0ex(y3TpTaNwp!Ka z2M8%It$CB*Ec6qlSn!E^kP+(lFGH4`^^a$n(xLH1M<#^ofXQi7|1Z5|8^bzt3M!TO zH<6XHZXd>tpCf;KmVBqA?&wfd=d_RC+WIojxF}F{hL7XLx#h}@H@f^E4D@*mZ=W4Om?<=hc;=+&eMaZ_7<1w48qjKTQD+6j3oT zAs43^c?9_!9T!7O^?YTiye5*z@d5pzz)vfK&c20Np@H7dw~ zIb5BN^@amIcGG!r<8L&XU+AWpYj@^)dX|SP zr=t4*L)}}4MHPK*!=fT60@6cENw;)KBPk8iAl=;{-JO!s-Q6A1-Q5jCH}3)C_dIW2 z-=E+7<0Uh5X76>@Uh7`>z4kt@p9Re!X2mvj=JJD7ne&Aey}F!+^Jw7&nb-wBX!2Z- z$69|ix5Nx&(cCae>>>a7dj7hi=920H-zp|4PldL$in8qlVFvM?hQggV9o=!Rs%=(8 z8{US6Qgj2! zF7N;wfOJ7{<^k>5ZEperm#^p^1gr2p_%XE9j7Fieo|0I3weA1W|;@v~0YwZmxK z`&W>%8|;sm4ms;udJPqtu_E}|ZetOtmKH-A_YNcBoO&MdU>lu$VxDB@Z1Wy6WZaf4 z%=SM07(CFDf=^b|BPtRXe1T^P(lLAvv zzd=wRtMzM~KnS?Z`dky}0*$cmW1=!RAo^8O1Xjbsj93oltP3eJUhZq1K8#8@AAUTQ zc|_c6-IH9&@Wk)@^b@|j^N2l?p{E!+#>{#@@As_}Sfk;z>1rPNCs~!Df;Hw99a!ld zOsE2Zo+~==v&YE23zX^YPxJW}4it#0oy)cl0U(y~+=u77BWBZ%96_$$0B(7MsI!^^ z7D0%K^FX}9DWF6C=Hi#0esSb5MAIkSgLZ-W<(&@;Uck|Wi!ql$<&2*cpUKTw&u-3XFm+Vrc`9$wMSb^wzM27 zQoKHkta`J8if5#cC1p|W)@TKruCZBb4?5c-_nrLVl(MrP&_$+mV$vSJdx0-_0|j|z zk9BWG+6kW&WY?Qf-74EI$PRZHnw@(HVUH`Pc|VufQ~8(%>vjTnj@24lT@i!HG4<<; zFE+`A;~&>h6tK4O$jF4*rE(mmy~f65z|eqkDqa;3bW?KqjTNkDU6VAsbqfwF#l6<+ z>DMKd%B6@KM|hE0$Rov9OczfF#511fxo49w3vPqzE9qChpADflXCvWbcUN&5U-+zS zYJA~-j-NbnzUsXI5%Ezi&$Aj2q4!8irT5cE-zSu!fQj0(1IlLP14~TBhvt-vQRV&2 zP*UY4h1IB7N0BiB-4#_8vnsr?WSG?MQ}%p$iZU5$1^ZIN;+y+M7#=hXuG_*4X+iD+ z=py++Jv!Mc5NGS#6jhU}X}U1RNuSi09*!@*K)Lz3 z%!Cv#=c)M#+d%S>Ypk%5n)Z@cwdbd*E7tBs1y<+t=g)z}FLK$3q1lE`8z{_Nx=$xhlB@{SsV#>o9|UeCo|0u z;CjHiG19 zVq;)liM{reNYsG+wKOFCm&O8ugX+CT0kVidN?Ara2*y03!-4Wm5Qc@lKcg4@`o zLOvWO{((&OOCK#QBYY`W7x)0d3-5+%wNwk7KrbaEghzt8HYQnwrj@BaU@eeRfq4ok zaou^$mD5V(BS`aVgM8*V4p+A}5I!9W!td*Vy=PzB<0sZ!O2 zhKFkw@9}*3@@{W$53rNHz5NQpBf{06nHzg&9AOXDK>F2xtzwlgKveP%Pks00H&31W zm1~SFTQVFy0PKcv+$S7UZ0FR_-DIu<5XsVR^p$?m_yp#aPgLO#K{+{8L`1|#a?e~s z>|}E=hF8ti)zuvS@t(arOB58MR7Du!nlE%>SN)1bW%d4v!322634n`yN~{JMptW$- z9m_@ax=Qi=P#>^*(KQ8}AP{Z&_@ciw?6szUj%X){%8gM8)j0vkTFH}+_c5o{!4YcC zfy|OwHd|VaTOH(Hz>@Lp-kF-2=^;X9G@0{7A{JNgk7HN~%LT%?U!(Jl=JDDuAu%zO zi^pqNw%P?7klo`PQZMF|t0eJEf#6k1o_g7^d;nkb37e63 zz5mTh`{oWst5jQUhbsfeFfeJGh;|r_p2lC8_aqndVDjh{Y2=$;d?7rAwWa!sO)SJgo*gl)p2i z3jm(luoNY)8rDl?G+o*O814J_XTS)~_h*0K6TUyBxNmnpFxJ(skQiXB>FFSfx_8LP zFWK1G0Mn|fCOw(Hw(9X8&GQ`J3k;b5VR9c{GI($XgYQt)q528dGAUO|{l0LwCwuSjrEAWZgDxr!#S z|9HG&w(LKlLkxK9|LM{O*#B;uY$y;#u-PW`roB9%;DH|i81?qYd_0!>?|$V;Czrw~ zV+8>aneIsh0T$11O4UZ=yuZ`} zjC^}_0(=@UE7Av1zym21OP@S2oBy93`~3lMc6Ge|sK@{`ZU8D}crO(@xE<-KkfVJ5 z_dXg#1N@``P|j*~!99kTR8+TrhBZ7Rt^4au1~`AGD;WS3ie}p*L^XTvMMXt+>ESXq zEnQuLK%z06qqr$63JT;9fH(c|H`d|fBkA>0 zJ#tpS>>s79rvU@8>oYLms3@YRse%3)gU=eM)2FJ<(||htF@7)P0;2*VNXOc*h^K{! z5%||Qk{p1Ab^2oQ01JzXjw#vTF*3?g)g3jzfPb>^_}>7BpZY5Nzq<63GydmYe0qS> zeRM1!Mhc6FbUY&c>wi`$eFlg+fbqujfaCLEfH+(YxCp=qhvM=6<4ipMIun9oXhA^% zP?iHHlSlVb^(Awu0nrgyen7Mat^vq>PchQqUnm=U6a|2y9taQ`!>Rm$g@}{v+@EgD zKc7w&&=H>Y!e+C1nw9K-zRPFz6fWasZ1DbwT?McIE(Cn?rTu-%nExK8$?AYofgna@ zs=^TMF%DJW|Me)}d|yC)1Z)l{R+qOYg8;z*VlapV{;)q&p+i~s-m6X7D9E?_`dT{p@8)g$D<0YtG^#wG^7mA~pe7LWl2l#cK(c0l;y z^YP^T?qV?pjVppj**P7(el$y>ix-}Q(5J#X(aYIz$7S4f6NHS#YLh!R$Qa5eQz}(-6_V_Qs~}4f4-lZp8n-lbl|Qd1#Q&r`5H+yZWz#6qDPm|YknDa`Jn>fp%;-^ zD_&1_CGbi76;F?=lB)3pQo5g`K^%Ywu&x^~R{~z?9vght#=1V;Ca46WKdrgIpL`+- zL*!Km=x=X;deO7JozEVkd?y`p6oAhLu;xUA>?fj8iuFKjxBKVA6=uMl1qADry2D05 zDFcp_{LC)JBsIkbz@kT@$q~4I$v;0Z;0J`p67yAo8qwA`2E+D-%!r-6fj9;cBVg!S z{{r~f8(^`fPgk71w$#$nTJHcJSdk6>&qXrcz`)P~IUkUIM<$II8&!aBhh_X*ga#Ss z2lM1#&ybLjJ%Li_QN)h^m+-Tr99Dyv`10<7IX%WoVE)}w|CtTXi0BNE$NUSt(b;VB zl`{c=Md9-=hLD%rpXdx5Fq6J>+0Kah0;tjlK&Z7ew3Oy87hAzT&D0|epA1FS;De>F%V&eU)GcPH{y= z)GUx1Ri75x+uv)KG75POhr>a}#CB^4qQT)Td=?SF4*)IZ2mDMa>VN$x;Bgb~41awH z_|bf;_lN*`ddhzXSTVlgfgufdo_$WmlJ1AsH z|Ey1Rxx0%Gj}Uae4_rs6```K`{sn7bcyhsiZ3(~^puHmK(Mw-EH8AYtT_1|6I9&My zUJO*OXyQ1?l#qu177$5vbbyZwF{ zIJ~vkU1POa$^03QWjC?T_|`J~IPNp^*Y9cpg_Ys&tKM#gtGd~C*gK$_YOA7#jBn7v z6*u|NN|?1bUGwZQB!$LgkBqmVaD zBeY0a}~*>}cm{(mZ{8J7db_>TSk_2DWaaU1&lxYDZOsI{W2u`Atnb zkepO#Dx}R?OnSa8%qe%~YOwc{g%LB)n#dCK3PGm&^|e)Gx7tv`)&Nhkdv(I@ii^Py zyT+$h6u|KTtR7GuoFFlQM<@3BwF(a}ZKaxEjo8ErtGLSz%DJ#u%;vlM9)jZrk}02t zAN;~~crpVZ8V9oI&L+~Z0&pE5tF35v{_w?R9N@Z$ z9nIZmAt<=P2@Le|A`5W$q3(U073`T!+ic)8LsNnTxX|ajOMknQB(Z6`8ORxggFk3U z&zWnFnqsKB^rfA1Q~lx=9&xne=oTEyjQl6D3;dDTb0^EjCe{AP>r2sizvXq8|0=Hs z{O{y->3{M%?EfUMCl0-U=iDIYdV|$YyqLr|yJE$tG7tW)H|fe<_NbZTQIW|6x>bMP zWFUoQA=;Qx^26xJ5m}~e>QsRa6h*qo!lOGX%q`*~|Eyb5{VIv=%3)TqNbBPz=Tr?` znG@L!oz0zG?u~-do-@e>7-1ofA-@!UdxIXvbpIBMl%;diHr|F4ZdsXQkm+}|gusCF zs%AsR0;Hs>?$cuBU5ldR(u2am?Ra8xxC5ptmq0@&^MP&pfYq?vb z^AF#8#A23OKuhl%EF4cAjx;1yQ=U0;vG_)0dK*W}Szwu$;rO_A6ZZK{6CWU_pRkfb zYb}ZzhKIvaQB}2wm*?g7)*vmXIyH#V+6CT2oFK%g%t{IuIif&r$J3I3I%$kU^s`H> zax2-DpI9nkoV@u$|6xWe7TLS|CAbT(!2KnkxIGRYM2Oaex5tNHZ8V>}Zap5-oY9w*_)$qesj}XG*i_2S)v_%}wO?kP|*AaDJ_18nY!A;@W6> zq^w!!;bc9!RS&uPqWtA~daUB~`?$%QE1L5F${1SJqhbWi4>f^pN@+BLaL4Qjh$U$& z^<%h7rb`IgDVwLN}c!*{@}cxF-zk9dljNq z{(kE^1M+Zsak)nCjjP%An`eKddQ8|)85b>YL|so&+6k&nCW{n1XRr==!42H3ueMpw zfSyIWeRH8IB%vTRqTuyEk!?mN8OMST0U7q5 z_x3l;EB6=H6^ePmAQ~%Q#M<#vz1ZtDZYTW7>?j_i{rssNIQ$P5K^w6^muhScr~n(+ z5&q?+;YD8IX{78>sgONn;-m+$_f_?zaZ^CZ(kQIsTBe8sVQS{3T!Q?1!rQi3{o%{b zo#%|wgY&r6W@PZT-uwhCS}>?S_vkMYtPO+>lep}@zMSy(<9e6=q|o1g`ss{&Ilg+$ z(5t|7>zsYl0O5ilv{3slMYgjK0_Q$Oq5jNxSRMk7udp*(wCzy8xBd?w1>oInk-_YN zH|FZnRvpiEJu*GjeTN|j3Rl)nV`tkWMJPRJ@sb(EN^d%xkebE%;POej+vvk>EkwGc_ThD2Oe0^iC zU}_IJU;0K?w_{?>Zzy|vLy1P*R1uQWGDbdBN6w_PNh@;FV{tT=roF-h*tf7~uKL-{ zJTKyfQIrn7Mo*0)e8>=pkm^k##*$ph4V3$nO3$udba-Z1Sz^+Cb3_;HgiPGXWQS)I ztfTxkN_Y5_Aq79SaGisa{^Y51>@j0upm@x^l1?L}=O)gN_jV1&Rd@1^H&JxMgI{e? z-H)ooA=~6UIzvW?Zc&$6Mm)D=-__1e;H?Nf+agt_8H)NX)B{_!ABB3PC!u~F>i;U# zk9s-$7V15}T|ZyTT}P>0a!xDz2`imE$TtFrAwIh}vwhO+!+&e`!|$sWCX0S_)^Z=V9Y+Yw=k~qn41Il>mmD!Nk!L`oGJ64a z()jQGmVj-qy`?snA~I$m2Mm62XIaz2=%?m?zgcn|9-SJMwW%%$$nH`6;@O+w?Gg7? z7h#6A`Loi}$>ry@N%Cw1cqn2mT5G{mT!ycG2IE`-)us9FFy61=d0h!sWp^*fyhyF> z^CIPGv{B0iObDMl?&R+k)jY}c-7hoJ-DM&^0y6#E{+X=_Z1I3*Pp{puNc)AiM7R`# z`I`eqfyzxf7`xu}@Z(v*%e%@M`y)lfwPTDfwJlaHMa<62@EHX@)=BVx&=MVrYk_Gf zU4n=UW^Z|gDzC0a_%OPa|FLDwrFgj|!HZ#D;%E8xn7)3m@u&4k;s>_YcMHo6UwdKl z*vc99E4l*>A|D4Giv`nEJTu4F$0>`{TPXxRfH;TaAvS_9>jy(_7(Vd>>6MrZWLdej zED}B~?GWa?N9wznNbiJ=kGxf=BNQ8dHT#>Ng6f2Qr&KhlVVyJGH(!4Dkm|2vTX}4Y zT<-gu5+$jP(vEk8)EDIzei zK6mndd(F(3B|lmhMJqAj(1P0i#PAP36$${xBm|S-{N0t`pWjbZaY@uC*Ms2T-M*n$ z2MLOC0C|&d1XKsq*t}y(UBvpvO%A&N9);)_=KKXpBL^uZnKrgI%1-Y6ItoSe!V8GB z2k#EQo3Lx{mpp5MCC{KHC|WulsQM<-{prCi$IJ*T1H8AjSJ2q&kZ6XN1K)B=7{ShZ zGZ)^wBvcf@3HDp#6(u^=e;~i4mR6_|t}H6^3Ex)xumIX#uK0Kc@c~N2twH`*Q|?+A z(w0eV70T-xW5Kr|Rz4^8yZ4+|=O4$!*O&io2aM4#%pFaH z1%R}R^ANv@lK&exRSyITA1R=>CIM?dUqQl?^XAKHlq|_pi`Voh6)(+LucSU^1|}b_ zAnk8sE$qQ9lE zYXB_n3&%DbBVRGb%LZ;|PDn#}*X+Q6>4ni~j>SK4P{uVVBBKHnV}3dRfG_Y_e5axL z$H%>MRf*N)<+k+#ch6}%y<4gFjEzr9`%;qL?tRo@k*7GS@EG2TpRZP22;QvF5Pw5h zm?_#92?Cm1DSn*8^JRTzJ9x~H(0el=T#ZTCjFETAp&zP5;idaipnWb5knny!Rzk2) zHO9|@sVm{|7%?Q}`G(kGE#OJY7QsL1=tLWX^EpO0K^W^bJ{*j(q; z0{DZ|f8p3j5Xpp7Buq-XB_BXg)+pil?olC!Ssm))LZ2$`fA4%r07?)2{UxXP;}#R% z|Gu57ac+oBJ$z#~>WG9;6J0gekkr>fx}|ZE=B1$rt0H#Wm^az|YeytLSjmW9GQI6+ z`7b@ZA5H8bG5e{wLdIK?7>eM3?*!ZVc=LNF*cTtxe(RKDJ7nqrbp(Aw8rq*lrSP_p z@4wBOj}hY({@e*Nf4G9zx%!;ugarusa3j$Mw-X>z=9}>>b|d*$K<^L>OG( z2(>Kt-gy4fh^G3VH*z)M?Jw_fL4o7$8NC9&D||RB6Cdq@=AP14;#ell~b`VvQLIKYSHqO)a@u|R8$=WtNsNozoY88_7g-a zC~ezsXuoT~l01CfS#hRy=r^>5Eff^eu~Uqr zdEw$%U~8G-7!hc>1?c}}%%i)T%SnLJTAtu*H-#4T6~C}}z+B+B=tQN!+VdMYW*(m~ zeNm`Z7F2=0;B$Q8%@h_F?khIl#h<3>3dgVIA5!*uRXOHD%6Z^#k*fIhX}c2Q;ZG)h zk+6E31U*STyl@1Ae#D$L!RItS*EK>a5JvYa;|Ee>ckxaWll686ea&M~{aO!0 z+=0R3&f*gD(byeTvH=C}Zgjt=Yb2v%nForl-5A-6`CS#iqVj+##yyM6vqx_I=~eDE z_!Gz14h*rUx=wB4aRd%H03A#Tu={kgnhTiF>+aJ-jBke19EN@NjhOuQ)obj6XSsz7 zS3!B*Xl@x=BDlq>D0l^-^yNUeaSik1My8zrdW+Z9kS)@OZDX9USHF%}rI>WzulSOH z1=1yy9AAcPySv~F9NE_K&)Fjp*`pmlpS@UOo8SG$l-ZC5FDCB~6dq=tiDJ*G5VuE_yEVkqKZm+#Ii9@MGI_9BxsMj| zV&h=-l}O1QI>RBJBdDktn6udJ9W7>N^lP$7`V@dD6kYJ4i!CE5ZboFETz93dlCL6WZ zD^`?oBNLPN&KI=ocu-UNb`$EPo{VNYLvpu`nVnwlh7L)(25Nj_9`?NSx~mMZi))*! zMlP%?JdR4^ugB-B+VwdT(1j_Z8Jge$kR_Tq&~ff}H=HSkd8%p;9cB59dZjn!RnJ9T z<0%nU08kZWzqYQ$ZY;z{rn-Xtyr?Qa!w{((q#kQYW(B>NY9+H^GH8tDd&O__39k=hs2%h$viJ zrz=*uff{e$(O4jf-!8&zrOjSN9Tp!`OQ_#3lxi!E3U8~8^JfOOd+%{Qa=fu;$Ghh1vJb+~NFs+WvTFM-W zNO3y0@t*xSidwR~LhvY-YHM>@*_u0lq*sW;3PH`4X#CYUF6@|7zeV#fLWH+%Tbrwe zd)cLG3Kd0rVUf5-zgzfMAQ{5730k%B-YJVNpn2RJnW0@SEb?R3d5?9qSWcBlSG9jf zu)wWaJ#egT?ftwNpCn^?9v^T?x{IY5vc-ilI@kd@N0*Ar_|hi-&e28#2bFIfU&RPx z{UG>4r=~^GAO?9UXmW>hb)t0jYXQ7I`!UthZW`LVs=Dl*GewVbUnO}@xUE1RCssL^ zTKPr+f3s5`C&D$k`7oP1$5oF_<)!!Ijbab4C7N!OI;<70VriK|Bb-3ewlNs{d6~6` zk;^@b&QvnSYHzW%S=FrE85IVZklajj5?%Gnm0VW_8lXnjC z50oh$XgH3HpG=bO7SS&6={8f57$|cZcM3pPfoKcRJDEt)6>iSDO4YuU+xDCu?8WyL z5V5S)j^j%*nzIP^5IA=BVNCe1%8_g_@EbB=ot|GWHSk|(&i(9pC!cCz!J|I(%;_@Z z;E1+7J30*sA9X$?=EeNmgI@Q|x)zl=*@$v@vg&i&u50@Xb>;7610{w+&TkJqm{c2) z2(YaWin!^iW>%ri3NTiLWh##_#$K!@)!`nB9T;5NealP=n8_)+cjFl@lwHuiRmOjA zdP8M*Knpcy^9q)JwK}*O4cABHx54KU5I}a+3yR;;lPk|bOw&+@?3-?%=GA5J)d)Cf zttajjm{NJ8Rx8Y_AuBENV2$>TheEkZ2{SUR!F8Cc-)Y1qe3~ zDZ`+P|3xEnegLJ^3tQY4lA6QAVonjI7}+?#yF@#&b;g~=t8SE#i5&sj#(|Lmr13zJ=6Qt^`OU99 zy1aeNg>JzXKXa(+J?-`ty6?W>DJH13-RTvEeZEi+&nnOf0hDyeDY@Y;2+^rqa;cN_ zAG=jQFMM(A`r56^_kT~{8br0T&51OlhCrUw)<=DO*&#&YP{37pQkZ~MD*pRV21z+a{C zB7H@@3OT9lvK@pbbC3j1}IIGtQY*lm#vNOE3qUWhfbs94XCd^#c`!>ndlyL#YV%3zd`LylYJy)CO}b zOsgKL$aU4`+_SD?NzcLDfq5GHSh_>m;a7zo2hH@(?F<&+x2hon6QsuGNLPZ#2;f6r zg`YL&!i9Hki&j*do#P4TQC~6sNUR01tT)dp_gVAw(Mf>jTZ^{u2+mRGg@KSmk_s>2U55TU;A=TXfe+)B;O%9YS$>-9nPLifheyd zJL3qt%KBZkTY__q5X~1%g&jj#e&&KAYL$va7#LDAKE6J4!d`kV$Q%m3Pq-9n_s=G{ zR{fU+bQOWRvwCI;?}18Fxro4c6WyuEmZWesLS@rj{2-gs;jB(2D_M#5nj4`QbAR>K zh%Ig#(nG$ZnEHvn_>C}SPzvto6Y$9H7^Tb`LL+!pjeZ5$0%tqF38gPib;Qwlmm_3? zAc;g;^SwVAnO9;@4vNV^iW?pT4_uVq0xc{W&yot|3ZQXDo!R-OEr1dX;r^}3zkP&&< zEZM?Dn$!q4XU5)|?5xzqV&S+eMac!8T~xcvIkySU#+ zL#sXLUBWwSXHl)9?eD41)9zcbIjQhxRh$m5#f>&f1^n)-iVFsMUiXEXXQ_h_88|xj zmt&63&l|c&?mOi@Fk+gVAPbLA9JN}{M^{{&3zvp^eE1h<;!~3Vzu&S-164kq}>mjO&DLB;O!fjm5tZ zBX*vw<`ffdBLKT{AE+ghV%fSEF5jwi7$o*9-9i=W*|trf+HSq<+`nG%X5+ihyx%7) zHN@naH~Tt{V={A87?;U~CZ+F>C8}~uiAuPESC%^G?){P1wtZd!92hZ7Q-!zsrYg9) zS9QA3n!+S9O(7mFZ)T+uu4l5svVs>a?8^@;lejOIGx7%Zzf%~}yOg=38`>kkccRLL zeNg$uwk#iBaRfFxe^}jLy-vkv5cYqwi_kOPU}I2KI1@6kZTUjJv@&2d)_vWP{9ku1j1kFWFoclBoq+RdrtufJka6B*jSxY(- zYf#Nw)9%aDeL;F*{&1pXHmBZ9HJmVes7aF|p(cjw+ICwY*m2i7T@Z_V{L=g4tjp6i zD~^V7#6_hQT$Qza@3v}vzD;qC^NY7+ip~oRoC3eI!gGsXHLw!Hvo9EV78bm2uzu;; z*Chu$))%-}ch+z7XkV?@T;hR4M-CP-O=bo;Fh@GD?+#_+bm_(YL%I;dDhxLlAj^{X zynNAcn-65~Wlh-J69NZX8Iq-p=3OwRXQpGt#i8FGcj^{h2)0Me?|gaw6IFXM^r21& z%i5jCFQ|=jNKTufpx(oTc^(xGPG?3iAnf_Pa$nH(D5>Ehg%YQ7Z#27J+mHv$x&HpNQ( z(pBXSyRci$PzasM3B^MnX|Z!U1t!dH#WsVw-?~({^;z`~J76)ESWYvi)81#%ae_=c z4Kwd2fukd6Y{<81YV9u&&tQX!&Aok^ge(+^W4f;^D!QtFT`Z3(Iv?qMvc0UE$H&pb z(5}&?FyZCE&c0arsRBMwh89&x*T*Up=HpdjKK1*tR zH&=ONm(AwQ)N>h^eTreRZdMIjJ2mV3y=P`n>~R_S{a!`{&1aN5i~ob7t~NiBao|tq(3zGS(+u0krYOJa8BpPIt!b8Iu$Mt|>zbNcQ`F+j5_h z2foV^0k_iZR|~PG4i=*o?~R7=tLc{5-Zveoc@W_dLKQpROZB^W;eqF&1NR9uh5D`O zfSS7B4YZmf=G4EHov;owWnBZ?`G%at#K-XgC)N2m%T=n(O9aIf8xZ;o6t&$9S<_{V zs=&@Y1%*PWKY}H>X$UcDs^kNqw2UW!h?=vzwSe96s#%4*lt??P$?+TIMPrCh-C?N$ zHssg!M>vgN&yB{PRlRvEZKWdLb_;g?3b2k%obp{WGsp8DXNE&zy<~US6(kc>-~JJh zdyobm0k)xaPJ{X`H^sF(_o5ELi}vJ^>at};h7Sy{=7a7}FG%E^YK2dLEv)u=*snB& zFD+5DvE*DYc`L57`GrCpF-<2~Pvm(=Cl|?iM4X*vC*i5{6GMn=Co95JQA)!N1tf!E zYNqkK8<6gC2CQ%GtZAuk2j0hY8(8lI{)Di>Z>Man4C2Dq?1D^J65$w3#cy{xrO%|g z$1&Y&^gKCsJvCNeEQ7k|%!eR)HZ%Wba<*OY!cWK;7SggVS)O*p{0SS*|N>^daM zGS&jrru(J%(MdISKXVPJ!K9so4DVDiA>dOimfrd>#TM2{CsMt?oCpXz$c|Q~MbB1E_ zs;qMvy`)*O3Qe`GNfesL@J_E`-gqn3xi!paZF`KW(6T^we(2bUQ9Nn%DySHE4$Rx{ zf_axY6=6l_q}`NbRKp`L?&eL=jPVUPtahClGcYepmK-{|#*GSw9jpHq3aM|vQy#&58rEptb|%s}X@@!Hz^f8ydJ>d-eT#Ii z!2~KHqoEP}Jx&D?ee@{o+vIH@S+^ONndXWnwh2)iK~tq|&^WX05?cOMg@b`T(iN0T zN1dYZ+Z>A>|C%Q>s7n0B*k_r*VfXY5s?=6{cRP=Bj5*pFvcsYd7K?qJrmV?MTToDR z1Uu$y1;FiMs={4#T$D09#Xw{co?-uAwdhFS_DuKT!R zohR{_<@D`X!oEC8WAD&V{7niAS2jF;W)rS$^;P0YM=l-JzNht;eixH=7eHPC=R}P# zen1y&sv1bVVHCx69s;EO?bSno_WxkAkY9fXzI)?all+a$8Wl0xSgupn@Cjt-(;BAJ z-nx$^oCMW9XC04##Y!j_8A<32n_J|I_x7gE2%4VSxP8$`7mRWqCTR#K^$gk!dGH@kM#hZgcEo;N!i9fsmvify*7NJN8~4Jc zXdPFc6WHQsz(`Cj@-{AZ8<&k5ZJUuY|8&@>o!ZN!5rvnWn|bvnmkpqcW@9r+oSY8a zOF|a3?Gd{bpp>%eN|Oy4`GnqMRe?%(W(t+~tGIRiP1Q<2-J(Q~!pm)IWX;Wic|a{Jln8*!g3Ya|Wsxw@HAk zxBJ*QbU1JNKr1ai4VfAHW2?c%=f{H#k-}ZC8sMBIWn+$%&K?r7A@lYd z2=5^4sSd?bQFo3n>~z7i`0YW>Nf!j**42wNr3P2B8h=avi5k$?RkF4pA(NV5D*bOa z8n+|RfiYPdI32U}a!af9I+w7ix9k>D{19Gjj%^JA_x4rt)VC&U1SMaAM62`jNbs!$Y6k zwY~C%7jw}KZbmKg4=8aB=Tkh<*)ukmrVkA+#?BM>m!?EB>A>;(C@A>&JMR{R26zn? zZ~-x_w)kD$xvoY_o?!8gknw}sd~?`**O81BYIqr+9*Oj-lN@KIF{fvfF}v=^^7bG^ zquJQtO{UHFHP=&mGJAu)TW6@0|w7C|#VQX5V7+*SP zsqpR$_JdGYs8j*<=WXruOrg!ox|uFsokH_F2-o3L@Q+4kL}#L`go)FO7nuhW^t{!T zyM#ngrhGWC0`| zy%fkKjc+OG_Qff9D01w`v2!+q)=Yn(1x~YJ<$RvfkP*U+Z0MqRBWXEu$PJ#@6x|K@ zXw|g+A$(d`9H1jlFm`MUtdc|KAaDrEUsSG-<-L; zZK~Y-F#~Ugpw@`&yW9(}Kf_zHaw9=|cn{aZTT0GceoPHwb}h;*I0Wa4y3W_0&3@UT zpQW8hQ8D4|+R0(#C*+*>g*LXmyNMTLd zM%w5;y=p5&gUbr4;A`riAth#jCLrek*Y33Kb;jOAb(5ht2cFPwG#tOAjh&EC>Ete8 zF%DQ}amsNDie((AXh;_NPY?ch)cnu7Fwh#It1H=H56{pw%!k{SP=hveqSAaAVtD`P zR;(rw3jvb=YnWPxE3MGEb3VPFXyv#GdBQ;}eRo2k4||(HCv&Do0v7vctfl3L_w0MK z@l$Au$-CWt@%WqzgLLrG+C7e+ucHlj93!$Tq*r$iy#!o;d|_dhja0o{7osXRJd_9M z1GUcgQgDf)l7TQylTtB*7&(?;FqfJvk21SIib!W_NG^6Q7wwCp5Si48GSeo1SC@o5 zbj3E89}i>;>@tLvxpm%qk>7~g`I!Q-EF{Lcc&txbvvm*UXX+|fBdmDugvP4txe)R) z_@s6W=iZ+(?n-3zpR&iP7_a{1fvQ^AqOKH;|E%JlYB^)s`(x}8-vs#q!C@SZ>fSOm zHaVp1N=@@jkLd-m(lvjLkdsr}Q=jh%yYvnIu5|E9sz_@8pP9B^AW23&v$@b%8xuku zueP{#jlBw_6B&9Zd&mtU<`Fi!d-f0`%TXMD?bp^j$oDBOUT>~HUBP+VE>x=y=$%1Mb_O^T=_PlQFLrjg`&82pU z%4g?uL!)~&|I%S&ip=2S?DMcgh@(1k$tD^Qwxj^$s?N zm3J87atj-lC>I2P8h%CD*h5Ft9`;f0rQ_OwN^ipvhttgXf^YB)Xt@i_L2N#_W=fTF z4W=+&%Hj;*RYF~%w&n6E=#*0S>v@ruX}~pu8{#Le))CFC8#RbvFJ$a z*;qh{y;wX9!X2EhqBWlq&7UjaFsaZ1RcbA(5vy;jZp&UTdsUo(`7U}YN>F6gb|0%| z3L}O}!$H42y3$4`rL3Ybh06mT4(D;7uoDt}xXlrY9Xk$NGycWyPefbM1JF9=e$&2C16iNUKdN3NR&>q#S zPmx19{Q@iGCf4S~$gry8EvRJk$xYQ^4Z>RSE6uyq26gS2R%!lPok91%&bbck2=_0x zBz3=D$lMFzReQT=fjmQ0lP`e)XRJ^#mcaljW^E-tg{*(CYyGE`e6fynpd98d?TAlv zx+m9%JBMKJ;f?LfCk&IrGLI)^){E;Y zR}L6~=DQqP9Y-G88}xnO*5{R)PJXIS%DtsA%`b}EX)MY#{Iqz?atUqPk0EzuJ<_j> zx-fIaB4r_ZH(vM_@`svCT_F$HVGiw#t@4!`=6H@|LM`%w1(&TXRj%ynt$|5d_}e0X zgGw-6rrV_Oi;Ll<8|O{dzT=#|LEzX-;2@<*$N9;#2144ayL4T(w+mzg83$JvZTW;+ zyJ^BOCy;)er;}d_Sp-JTl7Ct@7!;0MZ(kQlm#NK;g=g0WHHS-FXiD6^2}Fy4(4Ozq z;=k3s_ipg#xLPDI_d@I>yVkh%hcDjb zBde_63Nh()ahI`fdqLgkk&mQ?S8{zdzQyPQxx#gib7imeaWgaiHCNyS<3R1(1VO)H z%W0aZbsmG+NpBVU%qDS_!cC=R+9il#Q{xQK8}Qv`UF7s)H9YTiNe>$WLuEV(B`MR9 zD`*_ID=NE2?`gA~l=Ip6b?2W2xYU@6Vq~*}ifqYcA z?~>}#kyxFATnE)WV?W-R$A}!o3n^d(E*#?vTt}w7w|?A#&<&EjY{2rC2Dmu_Ev9Ze&q#45sM)!V#K*9X`-)g(`OJ6_A20oy^-)iiS|ClU$vK5u;vzer|N> zLXVra{!F$uxKgt=rs3EQMW>qaG~B&j+m7U0P?)*YmLq(ub$y`zP=#iabN;nU2>(zJ zw!pPj%g-MVElSj|g(LDRiz4AOixR*I(!}Z--FshyB?QeV=u!m@&r7i&KL_w-?*)fW z@$rS&$R^|i5b%G|Xe*c5Jw+xG_ZQH+9L{Lglx&#E2 z?oKHc0qJh(?go*T5(Me)8oIj%si6lLy1TpIHRyFc_jCW>cl&>T*9W%QHpiK@)~t1| z^E{4y|Lu2f4?T`O%#=vGrovI%J2c~|3EW7!^Y3Oy{~)9>WzDP?mW7ApVwaq{aV|5) z4Txq5DBo$k{XOX&!e^pOw&93{-@T`&@5~fZTzd6i+(n?J7#H%#;-MtmX8!n&b+V=H zxneu&AFl}6smLZZegfMSnIEbD)euoHfJLMa(EJNU^jIe2r6eh6_cu-%fTe1Bw1ijk@=}jKNcKf-=Yu9ZhJOYT5*j>(# z@(v1~uTm%ZEd_?he%shW>tl#I)AbY!+`?*j`~jaj+Vha*$F(8e6YSUXV-h(_CyXKe z-=bodU`(QjNOyj}e_UPScQ1G@-fpgOlHy;!(bHBb(2-pk>w^$dCm7ZX36uH!%Si-%}kjVOJ=jIJz$@$xq z?*a;c35{a0a?f!Cb|g*;cineU=n}}tq3>6?>jX&+jfiej+X0k-79_{$CJDE|a18 z1Go<&fXk(43$IX%8AHZ}JJJu#-O{kO#sXeR8N-R8-knCjtlJiJ_l%uH z@2Ew@-uw*mZ4}%%miIlDsiyUg9uk1Hc6y;>ExlV9`eHO1d8IbjmT*g3~;k$XDEBt z7@Q@EIWac4c?fm2c!4Qvq0kthzlkE3v{61$x$~f`;BMUZ;sFPJSWLo9GI6lrxQaqJ zyzffaFZ5`*ZxS$aMAR;O9-xJ7sE+o%WYE25K0+52{}^JnUc&jnb&MH6WU?S;C6hm2 z2$I_vs(yt6#AALd&t8Et>MdN$$`b8J@4?0`xohTe#Q;ZG={?!GcYKtFS1F@ynfmH) zzDpFK$&rso(MR@o<)+yoV7qZfT^^D8?upEPyEJ$Cu z0$$z0Wo0Hw^>z+8Zn$xX|Ih3%Zw+;Zz?>%|rugt+byHaMqE^G58de+Oy}q#d-SUYQ z0yDF4raaH6^7s7$W6nZR-xXD0teA3|X$Fp=-gge9aC@2FbjZsPSQXrBQSxHvlBo;W z`&A-F{Xcup@s^s?`MVYE__JWye71rMVk1v1l+D_+cTUt>D%!K~pQvEO2JhF(Tt|Nb zT7$>!qs!O@YVL%qH8S;GR??K_`N=~c4O&94f^^%i2`P!?rQT#GL7F?K4ewI*vX!f& z7_=pGxTrOqYFN)pNtqi1+*TAcL8T-S1Gjr-zB->f;cU3o<#|?U^C)&MX`NyGS|OvN z2DvA`b=SG2u^fUi0tH82nT9nHHp!qpCGF2%gq5&KYFVw;Dbdx2OZ8>rUUvoiwO|Rf zkFWJCS1~w11g>R#QyKId=7*+{(uY76@D&!Y1FHoFcdO=m-mN3)5eIh97WI{1ed-%T zJS8(cMTu=P_sa0C-CgMBN&?oN*#Jv2urooS#KGG4u(eXdJ;qITZOP*Aj`Y{d_#1tI zYTf{ML;3QFA;_#V@@it#9_JD5L`aY>?Y^#FbV;Iow7S&!?4J++d zU90?S`y6$z44X^b;eXC_X{C}_Lz+h_`brdHhhnYMk7fReHY!DqaRt}rF!kKnx-y)z zfQM@leTc(oU*bd`#$6M(*oHN<8>*#Ai)&i8u^EOWTAw=aq=CJ2X^cKDF49%N%ogBL zn9|F2C`+^`O02u_^YPt}kL|x#kO57+HICje8%EvZrOIj}iowB07G-8N`&WvXkJL$; z_x?J1Ri$YY9<4kGZyi{O&Rr9&^@E$^RQ7&|I4w|~*JTtyx)KyMT^F^OAJ)tRMz-9p z;C26i+NJNRcJoNPt9bZ=&D;MkjBP+!4_MMB zTP<>|FGEp2#?1h@{x5XD**%M((Un>NO$bkkpNPwC-+)b;F=wV7TNI%_KkB$tYF1zm=lxe)ukD_(QTpLl>9(Z)(dXY>ZHQtDnaxpiSp$faQWrySYKZN)jEp9?W`fuK1#E90@ya0{WnU>%B|G8GUq3zQpk);D=zp<-9vO;7r=~$u6dU>v}5u3=GwC{r!>X z`PsUWzghd2ouRSDYU6PZgqg%815qDPv^cDbO5cYec@_{CVvt`fUDEgD!V!%Xx-M;9 z=PWs{srlj`PP+o*Wv43DltW#Bry{1RxTz^kmDOC>upG`)_-(s23@~`Q|B@Tu0JfLZ zMW1%KhkoH>UrN3FL;D@u3~!aKyAZr2Kp?Q_u`P z9h-9Mw|m}T%^yjF6BhZBf|^}=%^kh9u(JF(c_9Ak$Jh3?-erpwm>Su;=>=zQ_sDbO zdV$p%j!#*Hs5m-S@Ax7smy{xXqG*auAiz@j&!s;i>k=ZYu>A?O&1U5)HbH@~`m!HM zSa#0@6v{|jEUft_|A?mTko!nki6NFsyFilJbKkrJ_nhqYgpB7e7q(US=<)?05fKsj z*)upD32z;X{VPy$Jn$;GE_EAwW$v}=$QGUg?#6Pae;z0E-;M~?h{&ZtbuMB%tgv9- z3k*+#-)yOD|; zU@?k4(pbT-z*58*>bhI-VRNcFZ%s&x&y<;Jqlh>EbBbW##JF}_f&ZznhB8HQ11-3B zKQ?{^#?);Wn9$><(u|H!7#rN!!R5?_^OX9{!s zpm*&M9PzUP=ZLz9*`}&+^=dG`x`b`C9EO+l9}7_zb1ksG(H#OQ#g+Lb-iC37+0^0x za(Sqczio*Ara`Z*t=({a*z3F;A)#YtM!Q?2`HvPn9V+~656dTUIfb~lyzF_!?K>Zw z(CpRV@kf z5Lg&B2WFmNb3o@<8>ojTj-jS~2qa8BwcNB>TTGQ_1yp&QU*x?Fny3J9mt)exo$NMm zU@eQX(eTyG;VRO5&m36oF-p5>g|dc=IrKUnSI zNJpBAKKxp=&&88QK&v+uB0fF$j>$;-;rt;H$Bp)5aYDMMWPVMhc%*n_0ZrGQ2Kb2) zlA@!R3MmolNqMaH+!QftB!sA5U_!>Lit&nK3J1F<Vs^^{C&6+s2CpZq@w*Ds9Q$Z_%l>$Sy(~K3}A>>KvEv}8`)jVp3grnC>=rSAy4%l^2W^L?pIW2OaqIlba1B-ymCr_Vz#W* zPEZT@Sw5CU92;^`fwx~c;Vpd=)|FLZ=6z0bdR?DLxn2;#dkAp{Qi;-ky`iOr6DFUl zJLD9l5tg3d=E7#lcMU={Outs0B9n|D-tZ+Z>?yfOr5&@#VQ6-YYbUiQlY2Bns{u8) z=fv{~Oq%JAiztfoDT&U-(oIX@{n0C?%JAc6Qj3*298X{rQjYA3bDBT&BixyQ_j)TcR0>McFI6-~y<{K5`HLzyH$o&JfyB&qDf zQ`fG7LuY$%7vOdv*?ly3MH?<5z=$-U+XX>+-<&pW@Zs4~k9-e>+ z3V(^LRe>4bfmcq~9`?$mf}FCECn#D2HsT8Wz|T6mqISMmU+!$>gqeTap?SAeVhJ?5 zz5X>WFx1gEkN-6;Kpo|RXjIe18GNQqX?!$rQ8lr{&)&I>--dnz*ptA8eSvcowl*Ii zMT_~9YID`}!cX_41DKR^BR?cTUz9F;>><3nu)_$4E=A}%R-gIW%`I44*Q%r2<1fq2 zR2Jd%;HLuqC8uz*MeCCm8N?Lo#u7fE?-`p|%fBdC50Lu!_XDymT)$>RuCV2AlNe>b zM=GH0d5TXwRBvDd)qCsL21Ux(I$P|-!P*CSM$nAeKQijx^t^bVOXBHd+j$wY_ch@7 zHUB^!ABl3mcH_d26l35&mIv|4$0b}#-=0vI$`~#b^sNiE3jca?nFth64zS7_(FXb z=(XLXmnO*bS@K>*AiO1y=H>An zd(5E6j%LH(X>;zQtaAcAPsYnQH`@{#)p#3K{#>OcjUL%gquUviB4t(RvGu8>k9Z|B zvi2NcW?}u3cILx!ZGTN9%Je_wT=+~A2Wb5o6Zp5Y?CjQG-`4Lm5LHy!?R_6lsYMXb zb1Z^u!|4Y)&lKOO^xK5r+u3?TWK62onnor*Gow@z`-iUw<@j?2+e8?S8R}nN9*r61 zDWTcgm4Vu_00F((rR1#Rqk_!1hT#zH{_5n+?@e2d=>1}Qm8mQoj{f13EM2K?x9r79 zP9(g|T$F|)lI8aGx;-f~QTPX(&q_THWZvsUN>bh9$3_EH)Vu?8PjpQX%h2I&vj=_K zhlH~;$!7CQ(^av@iw{EVek$#c;|i$B{Gg?Ui>|sb)mKztEe@Zx8?Z+|JY|=BwhuO& zoXFU-Cd{27-ko5ZLDO8JtA5;VMckUNorRlUhjH1i%-^MNOjVJ=s?@)Q#>nkkq}r3# z1u{UtyJ;3Uv4X6CU%<)~N>`C@xKVYnOoM&7mJR8(0IOgrXlU$sLcSiF>dRhTWZ-${c8wTY4g2Iy&~mFlt#;{4Bh8n zZGD?8jvO;qJ}FrYNJY}s?c<6K?>}HR-A0fxsB-=Tb93l;5c3UcEUS-qQnMAagPp2U-*1J856V?t~jB&MNqB?$h?@f4#>c2soHWQ+PCTMtcLF8byu(aU!;7>f%qEi2mJR^ee=H>MN zPf4-;R#@ayOY*yxVrNdtj>>9Si$bsNIsj;w%X6f$3R4?Ku2erE^BwqH`nAJG+sU;f`+NR#SXLOp~tzs5-A_Spr$Hw zN?$4}k{u{0^5r`d=3!6szVyeOvd*_^m9XN`b69F{V<@cxZ zU})`ED0-SOHinGesH&8_!lqbNnkA8;dra#Jl~tGEyZa$B#?*1@$aiav)St`w3$EM5 zcGL@w|2W1o!bd$y^bZ3u-~S;4d!BR>CR>ZNZ;jYQ)q#ZQcFou0C+^4EYN$mA-#(fF zo>x-4fRx5#8xMBKordp`?f0B_&;-XXeot%`DL~^y}m> z|H+miX?3-Ep2LBPZ;wD!A;hv~;J?Jy_trKxx?d%wAou!0PjyB?50bq#D0Pecl4z>~ zvwn3b(J}!jr+^TzN@Hi&x?Qki(eO>}*J@6Zu4as=H>@m*r+60?giqqhddDU2B6d=M@=Tfn{JruV|!vy~G@#X2dXcyy5p-cn8R4;BYqnNfW=Zt7A=p< zH{0y`p7LY_0_z;*Wl>V6bxqYN(JeOp|9#E$;YBe2eO1nQiIjSFT@#ViP|! zfE!T64|L|#r5{$=FKvlbOuZ-E@im@e5Hf?V1O}@3Gqi z8U-5g0lJQV&=Vs$`wBGg|yb}9>>C>grrEhKju7h0gUKUn77yMy&Z1z+9 z(H$TTOqaHFI}hEk)KPL&KYx7O>Si?^Iu9mgOMmt+sO?mb(Bd1f@*;PZ|YEBPwiF71K^ktS(dm7_g-Y??}{3`YJ-o%gLmqH_& zeru!~@nocdz*PV z6~=9c%~lQ9%H0o@tI9Jb6^h!DE2G$U?R?D2Zqi?NnA}CJl00vPPC?tIH>1ixzmJ^( zM=L;$*HOmNJDe!~qyCz|qOMb0@BDpX;ge?u=bj{!_omH%GH!cmuWa7w;X)}IJh(Jw0hEX>G9D(v>c z?i74oF0Z|}86ius_L-INxfmk@GUFRW4eW3w zL_zkLU)N51$2}z8;&d##1b~R?v32UabD9BccASNb*Oh6-T3)ZHSBx4?hpc$K zkYBNEw{`oZXEAl{MP94JLm`1JBKqxrO55te(f=)N>+i1%8jiFrEm39X1id;H#CN9P zCXMssA&%A(H~ok{8~Cy7TNG`L^~#ed39Hnai9z?`t@nX9m|JHl64H4PyG#$BW8P;S zhKk~+QjRa0qm5ti>KALu>}nolB45KFb15>W{xy+h+DH4OJ8eeB0dAL0zDd*2wI^ZQ z0{%JaKX+b2wxgAXF@y!hqWJ`?ys~rJ&d3rUEK|kPZNpmLT^hEwtGkiI0HirZDq? zRl~1PesSD^e9eNq2s48v>Cwbe0LG98SBHJGw5|1h8`xXRuxs@}t({qpGT91VVk6Hl zfNKdPY5~Nu=_g>dOhwbW;IM>=7nf~vMaJhso9#A9i?K1pXS596!ON>%y2yqBga;s% z5%(TRTWJYjVgIv@KKth}5C3R50k<2O{kwcjn>vWhHRc?-?loG3w^I--eUXFqDdM86 z&*+-X`4JEq^FyX{9F9)S>^q8AW|4(!UGh(d`p4iUFtWGoNR*2v*#&pZA6F=DUkhdz z6{`W|yT?f$BirM3$MILM!`r1h#Qc;_>1V$E!E9l*XUHD0vu83-{>55)XaJ?3c!eAf z<{FH#f#A`uwYWD-E6Y9FclVIX!>z1@0lJ3DBfHGym%-GVMwhirN9=#g^`fKifrt#% znwse+yhEUkC!p2W3nxEhk=;E7^oD7cO8)~=`ScoCx7QH}!?7=NV*+fA^Fn>@pHQEE zy(`3suy~DZf!yBZ{Ipr~0&UU;g_*|s7o^GLbStO4hP;ZVP0WQTTSvN2z^3^Jjt}?G zZ}X%~q&AaaAITN=;&gbK7SR~|<>Tu`2mg*t8+z`r8rAT2!7q`m9{5*=1gLvK6&MC@ zA|N{ePIFCcbLm1TB;H~>(HJc1hWfMZc^n`HVSM@em2J-v@>6FbAUl?g>?OyE#wg_c ze~z;TI8IC&u5(6FaZdrr>RrgPwV+LN1=A={*UY?v6B@m^0s(uL<1ujq5!+K{ZVa+( zpO;^Hwe0}a!Vr)dJ;aysw`rg3jh`tOSdTLzqO|zcu$PkS(UhFDLc{6be939~d;J3b z+9!x@zfH)>n@=7{jQyb^-gBSKhj*ud#4XabAoN4K%(&D7(VF$35=HfhM4PfQuYSAg zAblZWqSc0%0qx=l8*|pzBiOOGKnjqAH9z=-1>GvNjfk7;1PDn-|FRt95pm@Vk8rdD zA{-GiJyZZ-X1pCO@km07;;{{tK9Uy7X;g~Ad1BxZ4Y5yD^`)P#L&GUo=G44*Pp+^_^Wv$S7mFNL3l<+H0Xwm`W&vr262LMBzp|0Fh8v{rk!W$z!S{&92C#() zxboO35myoEzcd0{L2f&LLLZkV;i<_(+|)5P%NblYT8+;+Bv=_vpEMDhT*iII{G_}m zkphDt{`nz+JR6{JK?m{Z8NoAoqae?nCLxH)MNUXP5Y$;-F`9F*LyZ)DdLN4e^!72L z?MTmEzvHp#3Zml@h6?wa-6trT%@ck^R7aAKg`_@c#?fRaSmrR+fOor`x!{S2g~<+n znr*jXMvZ{BaZ|y1pCCl%fCo+({&s56BSY>`z(Rp1LN#CPDg9BsIKS1tXbnpD@7eub z$F!A71tme378ls}1h9)&QF$4=_f2i(!uxzgMNN1_5^A0bn+(^F17qxI%C9DDIrkgnBXkof(lxc*}b>2qxG6t(jdv z41GF225(wVvs702STcD4<6)lGHy~o*e#Z)Wt(2qcgJ%1#2}1cT|v*CNEXNY zCDz|C)~NLI4*nNfkgxg=TCn2X#oH4(PfS1tu7|nXI5?BHI;}ne*>ZS^`zz4V>Uh)C zd59POPzAR1UxC1!fn6~sxkkAjJT@d>qjL~`sV;s|95<0yIQisE@DAV0)-uOjg&4^h z)f2r~l%KkX{w0vK*T%#6Ejz0|7vFtmNO8I5Z`ZHO14>QTseWVx&DCipy_bNNsS{S`wNQ&)4rqh*eoW5>8CdNKIR<5EP1<(ls*{sD z-7zQT50w&7PCiv((+^#ZrY{0?g2lQ3viZs8oK{z!{>EpjVOQOdn1l$@_0w0jZDxVJ z3^-HKqifBbRlYtw{=F2=rwhqEbJq3)TYp-oT4R4&r#&^J>p5hb&P|FtL#2|+_B&8C zP0d6dqY-hwdl(gSvdH-BvhcdjWv{r!Rk7rzjGmfiQE{^ka8vXYO4)6I(Wy`KTz$_6~|)# ziCaVl8t^ExId9Lx6LjWSq)%CVri@8?z||mrD08E$rqU5^CT+bAJp$MvMEXmcIV!kC6E7w_fdbMzlZk;!r^^|rZ;S& zGZ3eQQ^%4q|LT0lno1A#tozB-7j|=|>p1TT`T+%^z`)x?{i*AHBB*qzwk=5E`qpK) z>mAY0DN%Y-9}Skco+a4^x^JABMXeoQn}7uK@0NdHdK$7MrCjo%#!y>p;F72tUb6zz1OH6l=%Kzy(k{YB~8CA}?On8 zd;yj9-p7FVr(;?nBsRg{NRUQ6&e=CN3m8%yQXgA&?-dHg61{)M(29(4DvDI98jZ4R zUBPObHrdiz`qS8{ZHE~3x>9xpHRbKlIT;0hf__0`Q{M}+1?GvDSLlmC5cb5}hiPr) zUtHpTe-c+kul=ok*LRyn&-|St5=#+LklfPk7&lsBm*(>|FgJ2GS0uS5eRzI$jr;-@ zQCKK^Wb`BxcN5&_2SF0xlZ|Ixz%yWyEOigNs|G{~hKE6+=`EZ|5Bld^hRV3`P z%JcZKcn`b%%)QJQ<+=o;QsMt=T`yVpmu@#Kv%d2C)Ad6;jO#SM%z(dZ@;g55Rt z6I^RWm%#W>GY~RW>6NP`Do74d>zqJ-EeC1@GtiiI@Of-^&Cm@L=Qe$X87isZ6_?y5 zyyYquiych9Y?MnEuJ*Yd9Ge_*z)M9^7g*fP6TmNa>3L#_aE5tPLRLbdQ0XsL_i)Z3 z(-I=ofw>{CSLZRB&V+%IZX3bCaq^`0u(U?c*9zBpf`)WU85c)yTsfy?RVc_$pl$bS zZSW<<;0M=6mM>I>db{3RJtB$~oh{T?6f`Ybd(Mg^;gEAK9=+jq&3QMkm4n}FEA!mj z6L3ghR6S~B^R45whm5i__eMwwuvnkd=#Wued+ye;pF298ldzA!9M&AZOLMvgRJS}^ zx40pJ`CZ&>#g2Fa>4YSkYjRy%DubNR;e!>;mT?KjSC&qgs6~eQFfIdlE5w#`=kg4U zG*D{{N=$eoeNXaAxHT)E-IKBdsG;+)X4-d1_FxeK;?8##KzaS;!aByq``<-7dYP+W zEMd?_iqJnlH8C(=|3=u-Cj6S`Q?HEn{dE=b0_i-!ZgHW|``RStHr0|RGUa-$r_~J0 z+br-({aj&Y>I1r&=`7{idTjIc-r$I^zaUuG-bHS9tJT>=TKwHFe5%##m>Ah{)XqIg zgURK`%SGV1vzSqgT4YKV8vYD4e9%{qwDc&SB-~x=$yqBQ<6Kb0rItiC&<69^pG|jT z*WOcO?}*MdUtrHg0`ofw&L&Kc`PxLBZYYJggHfy=JB&=GaFpzsqgd!R!0-eE(B{{D z1I5mkR)jcx#t#4(nDAXBLwqTL4Wj^h3^*UABH6XJ8q(6@b^syQUn(+iPpm*ZFekF~ zE}Aw)PVY`zT`2QxUI_elhm#2uC_JTBblk@{7$lYEn2=iD75FB5xaApeq6(`d z$_!ueQmz$@pUU-G>rE*fkr?YllUkqDV>*5!7#e zWPtjY!nKwAm%^n&;T!$!gG|Ha7hEpMV(R|*PtdR+d)`3KNR*D6-1+SdZ)IxkcEww@ zcAn#Agb|PJ)ZycphKtnUm{p$1Q%%gU*aaeJilKSe%u_G!LZzka18o|xWbG4a-I!A~ zkZ=fq`yu+AkHSc%;|}8%mPSEx<|$D`+t5kn*KIR;X2PuRjYvVPa8(m0gBV82}@(!qk~G2wv9T;DBl>{ z%VKgA-aS2VSrky02-$B`G*KrT+o@W2Z~O<2B{6UH>IP_((C0m;Jgj=A>7lXzgVJzi z_6;~5!}y@XU{b70z^Nb5#~UPe&JK^X&0|dN1T#vmRC(T$wzU3-l|}io$96zh zxR3F7l!!_{D=#m_aqFqAdch2_Wh|@QQz}{4r(4P-VxrMy4-6*Cc?so}4TAyec2hX- zDtbOlMv-HGeMGighVuRFF@=&zFS9Y|rwRon5`kEbG~SSUh{YdRSI)ApJz668`-$K@ zCO=u(u1X%`s_qbBSE+E$sHo~LVxgKBDq(rll|oXz?je%_Ly_ky9<`Z^l~S|i!2HR& zd6FCvXN1>lQp`I6U&Qz`jqXap{RD_5)&{&4yxEQD#05J@K!!i`D&seA2+B)PS8q`qJw;6ORDrkEScUBSba=9RcD-w@N^l@4nj@5xyP+?>2s?uh7vz(6~TQ?ogRHUiSq8cJPh2z6Nxd z%SXLj*dnc@Br~Pqb--^n9w<-@k*8?jF1+K;Mggf#}Xqvx}}V4^*?Fmn{Di z?So2cr#Ski(O<(N(DJR+?X0;E!zl;3y_x1XmMXB$x?lN`r}LNvM5KA_f6FO440Zr! zA-e+rjy4aEx%%+bS&vKwzdQ`avv?6el>Y>$s(R@lfLBFFm_`KF(tV*c5mcmZ2d%XdaT|y88MzW* z6q4SAB96L3b`DR4l*Db!Bp6>P#QRc3K9S3%#;BQaM$m1 zxxF$eU01ayAA&}?WUV#(o)ucGxX-kdte83l5Z;pEaEf#zHZ&e^-Fl>o#!cql4=hZB zsH&jdbi?x-8c$3`LQwKKagJzqLEQ{`qnTTgBeAs;=Rsy9?eE_O_{f~v1`|K8?N-js zARlpK)M;-gKL7H<#@%F53hm(A>-VpBU8Q*@HkEy4v0ol}khU87PdrQVow0;uVts|kGkeMKy zk!cRe2++rSmN6`tl^gm)t?S*v_nI$Am zqie~``UE~?mq<;jNBPFzd9zvYUaB#q$6yphwlPJ1ptdu(r^^|_wyy@zSq=`L%l1}_ z3!aQktbK;7ruJu-LBI+f)-uqp8R%7T6w&!F@XrR9bzjE%1TOYZ!%f0wFGf_c8@sSEZ zWz~%&^fwbJ1XOajW`YguO|*$-S4CG~gY8evmv=ie81@P)Np}a~@=E!5X$tt?jrG2B zR1vf#XK_4!IK&{Ie_rrmRMpQk$et|r5aF%Zr%v$i#bz<4RU=fzT*r>rY7vHAmPOy5 z4jHJw8;zzxd-%3jH26L0o6ZByGf2{xLnEr6Os_fjJ(F11_?&L1r)vl7))L)Ud^CdC(D7f^O)8sudf%{nu4(8i?3Q)W)wY!X2s2@Fsl(BPGT7zM+J z(R}q6?3-*E#n-Uv3r4j{R(Ar6spb_t9t{**`HEMg)Ykf(-C=( zp#XF=$&0$(`YSAYw7F*PTZUl^{X*650E~6PYIg$j1fJvqziRDiN78IYlfD|e%6TN* z^-{OtlVgl;c4^@;HJS;`TsH~QnmnVhA(N*BZo1A`SALE+X~NA{gEXIPpxkw=R#CR^ zZcM_uicJ>6CAQ`~tsOPOkf@vFn~+Oj%pZC+bSu9#glkw;K6t=YTC)l&S1U55T~(!j z@PORx&TC}ygMJZ?^y?g|pQt_-7K*Y!nXX_?omKB;fNafSN^>Pp{D zHb`)fw2&8eKnjK8&W@BDd1>~736E^U^FfO;Y9$wYtzA=ixrL-Tn_U)gYM9OAf zswRAJu3ffehWY@$*;{`Z*Ji)+z8k9RzbcBpp}%dgz=D5k9Y$OtI!4-JfszBG}B8qqM` zkn&gM$({tXC+ic9kP7}9fSJ?wl*Tm;O7y2yu)^kc(>fL|y|L9U1*Z!F#+^`Y&?Fpp zhuY~;tdjDrv~07bIj52?G^}pdrs^zL>JOXTCR{^Xi@{E3rZYdiMnl6TkZsUXIL&63 z)E#<%o*Y)~%{{ogy}6vpb~+hJxp_OtnG{fC;ouO~ev*k1-s*j$rgq-daz2%@$!~#h z`RmMOJOOyalOv6r56Vc86RQFkars+kNTcE&667j{=Sh-9hhwr@FSY&cwX;UT0wn=C z_pzWrBW9f;Z=Xt)y@O|{S%Xfn=cmA6JSGa<2Vaq6BTyc!zcpNb)L3j(4yc*Wb;KF& z&+&r?iN46T5C6PwS^bZfxxjxBdOZ3Y@xk#+c3Ydat4(D7)AFNe_rj6(c#q5KdZ+c3 z_rYNf?H}YR#{Jhwge2hag>QlPKT3WE6z4(W8~8U1J?^{z7*^nmxN4b!J^Hgrzk6K! z_oa^A|G!^S$7MKlUP1yfFJXaV`R074Gbn$Kyaj@6>+Pu4zRPUu=J?xc2ICm%)-`;3 z^;}9>Yej2oR!Nf|{r;WXL9fnySC#ENLTL)N#>{6}b)3_iEZDGq+Cv5(u6i7Dnodc2 z4OW3ET6bbSvRw!0Jo-`n>g*BWWR^p6P%?HxBpXMGgji`(e{NCg3dQ!9f7zC%JopG%_7K0qkjQXBxq+RRH zt*`-yTy^{jAK++RtS{}@@evWP%>c2lq)=>dUyC;jz)^svS{b7w5SJ| zEh%lOF)EUg?)P+%6WG&ES$5*{gt6NMfy>Mo9DG7HaXXsX&qYo}Bt1!Q#$IkvE3d83 zYI=9|;L`9KMRU-KOgScfMHWW1v`M9V=#35|(1>v#Xn_J6&Y$=a9Bf$y*3j zKF%MqpDn!RZ8DmrofB_Vb9yx)fD@)mz`r6sQ+paiul@3zk5*Bwe9rvud3%FwE90Eg z6X%*WEy?FPmryLaHJiGjrg_C%l;{x(rlwXo>rC;Q>vdgjLTg1^>#uWKh$evi=H))0 z8z~$1Jb){PMM6*L-fk#b$#u@XkTsyu-bk0ijGHL2- zbV{&Qsh@>hwHv`ftO3D=UNCSh`fM z@H6@ri@YhG3htsT)8szg6=J8c*3{u2uUk>{r+~u(a(;PpSKWZ8->%;lVqso(mp8GP@45X~M zGNQXSQiBLFSs2ML1YsnO|K>TB5;D4Cy}Zb(+ig^~>#LZNQPV1=*&;yLE6afDKMf67 zzdj+UEn2JR=T$q!zj7|mK!BZYg6Ah3rAISI$-HF}?Ho2L2Ky%I)$YJoNy*K-M_bk- z4?RpRCVdVlKZerq`}MG0_0+|suzCgFu5>fIi+4)=+$^BI+TXjs7`~ofiv4g~UqawV z=HV?6GxO7#lwYnhP-2=!RhG}Z;EwCTYyO-}ta-yUHE|~aTTm^03*y68J<;w*jG4XA z_RzEFY}+6z94T_tNa7H>oX-$y&$Fq`%Gat_8Q0`eAtf{!M?w#^rXPmoYtstSQd$Sd zcpZ3s$my~ZWwRQIuMe)AoUVCy;WU#vEb;Q`Ke#y^A}A7oFltc zs@h@CT!XPI7~$Tm=UAM(R7xUrX zRpYMu;TLQFFSsa)_yd{(xN{pf4`r>c?ma3PhxOADrw{$4dLmNIfHQZxe3RwT7b#u!VC>3o=aY2_Up~<#Hf)aHN*-c-ro~a6xcd&9-Z6ll%2?wk zzAd9_2CQs~oxvnft)6!_MV5c?I9rRZG>pGm$gdn+1wYk-s>=OZdE?>ZF*5h(`c*pe z5G=?zHF8EQUnLTovIG@l&fV4R?3UqC_RCt7wGwA#q^nAolb&CQ5O9AFv?FkBot3@e zkZ`M3*+h018}rc|t3x-&v~5Dt3w}Jbx_1|5yOC}R)HWljL-7K4JOwG*O5MR$;V5EUTuH19=}0sl7bT&6|RFJ)xLfe}uQnylv*d?_?WV0T65 z>5rB!*Us+Z4)Vre&z6_~s`Uz;7xxD{AvCu_T(vxC@>HZS1?{Ths3mQkth+;=N;-=xb!;X&B{NUE zN>JE2I=`2YKgke=OnZ$TkG{_$g83C%?Ld(*9izFnp4`}F&W|!HtZA9=i1Ln>6l(^3 z^_8?gDV(QU3|zr(oAbJ?tm7#vFMIaOlXX@={>Go3h6SM&(!No(f(=~e~989y$00S3j;xJ9?NyQR_8Z|#HRP>=X}TW&hh^LzcG?T`_MafN6-w5G-{c6;D>KPm^}Mz@t~1erfi7z<8lC{lLp8s zk@IErnhS%Va+CaI8yCx>k$Hyk1p*%MEUkl@kl%-W?qbj7hjClWlan!)Z;6JLl0b7? zFF!&U%H!p-7Aw(T2mwjW*QL5Q;^Ype<&JnI$#L^WdRt{h;XHx=o5og~#@xBFks;rS~veIobR>xR@z?_e6p4Nt(KA`l+aj zeEsN_-7G@8@L6cv*ajqiX%@4=T)sO21AXKj{C%+MmMGX!%EUppqfb*_l+yh&{wUA= zn=k(cH<*iDp}c_{+Vqa1<{6q<5;s00(qmbn2m|&$IYJ`ZKP}2xRVSZgP?WOOj_Cf7 zHwf;?bI=T?%;8<%Bc|1CF`Gl5aT=ex&sr%NXTn-bxiKq_Ug`w|VyT2j7kpc3*!xym z;{5GkhaP@~ge*3{ZagTk2PrD$x%S1S%WWrvrny?Z{m*MrfT2Vk)z(kuePd9GB3E>*CArd&Ibg^^LFu2FL^uumm z<j=|y8c40=FQ>oAG>RHj%1^YKW#y#zE=*L}T|U8Bnzr)V-6EE(yP$X=MGe zTJ@S2K#MvPzv^lJT)pCvZX6&St3T0El^<-SZ=;fP1Cm$VK41zS-^q@_x+RVDMqVPI zoTqNz*A+VBtOK~6DSE$HgEqgsJ&TR&K1(`Vod&(qtFr4{VQR5-lfqqPu7mqde zA$<2jcb`_Bzu8$2(b>=BPlRVg)Ll0+?o8Bli6}|hkAqfXMswV$hXEv|c^@BHFSa|bn@@jzw`)R(+kOR^>-w%49oe|A%xwceKhFEz zdR^a9vbDs8?s`#eMr(bGLQY!4*qWa%S<*b8y}9YGU#gmmYiby@H!YbS>bO#Gv?;y- zv~G&~Wz|ishgw>iDE~CtyaSXK6IDOwz8?}!WXS39W{*_qw4JIr_eVQ*$7s%0(5oj` znT(~V~AOg`_nTr9})B;}$lYSeFS97nB@L8BkbFho{=oIthX@3jf$eqk=1o_)o zWfpppr?yGQZ<1SOALMK4Az;$BSSvgB;Y6d2V(SxDpv&Z-J_pUE`NUY4;JdNaCh(9S zvlmfn=`I-8;zM{7yZaS6sgRZ{0S&rC?K)3x?7h9bgZ!OTR@ zWY_}y{ZguJd9?ZGyg0Vp_TFg`wVdWG^V%<}oi!^(jip1N*lx=kj`r;uTz$!T55vRN z7Go~T?y@lo=6s`95Xi9?oz>b-y&K72DdFc`*SaUby(w~>jo(;e@=A`pkNjekeB+nn zV3Tb2(Lc&d}aA8)}wy&xD(ee_lyP?ZU4K^I<@jW_+} zxJ)Fcq>2aA(!|)QHGY?UU>v%XBit-kXGz9M8~RQ^cJtB~6~D1t$4!t?02 zzbrlSx{?9tUtT}l?bb89MM`xYK2R7ay*lqBc&u zBASD29AbZg7!jp{^7(qrqH2Wv)nH~xj91W{Sp4(Vh64AKJs->^RH z`HU%m;`IgKE=Rm27U!t`kT~voPfYIMt7jrQwb@?I$}WqSf+%39`7)HueY&MiOQ4= zQWmJ(rr_08R+vRGf2;UBspg{O?FKhuCMzDaxk~|H*Ok0lXoIm)s!4B|Hb1|df&wB3 zUj+d7g~^kVvkS*V4*af^j+Rm7*$${ExSG6`_KOFLhjE?_vrLcQSg*>T((rjCYMrn5 zuA=g);C?DPpbtD!ehag4j{~!LbL{;g+KlTV&+tQnSTyqtn3SIG-71Q*ZS(virc0Q%AID;K+m%y1*g9QJJWkN+ACJPB! zn1q8sKi7AF8czc?#WV(XA#-uTXd}>8?VF5u z6R>K5E1{mZl+nJhe?CG+v4-0s5zdHJOiW10<$I3Eb6eBP<{(^!jTL-4uloxvti~f! zcan6DD0O2d;Vu#_>k0TBK@hI~Z_xP2-?IU*5Ll;F-SoIh8 z2TDz0!;1;XDh2i>_jjstq675e@+&%g@^Lc*erkjGA2JT?JN>7AR9;s9R zTew?gC1IAxgG|T6Top;KR)I?i_5!}|)5oLW*I)zTzzu58WUQRaFp<iE^~!2)3d?MXfhbrOK&(>9^u z)nXt*zs=CPC}Tz@?Z^=8dJ2x5)A5@dTlocLhPigpG zqcgH4@}@C4iYfQfcJAc=$WA;}ORfH-9$sUMRRfY6)4(pQizQ3a-QMi8=v0DA0u&4! zHE59_yqKl?0&X*$@~l5!TT$fsZfXc6K{5{!*`zQ4w*bTmZlOPlCg+#Ot#XaXMSvQI z%=f9t2#92#5WaXAwPgx?oP>9*Y(C*Ru3Je{kaU^=tymzy^99@l-86LCeZ82G&9>FN zJmIZdxF%rB^~b3r$rQCR!A zwrZ!mObi?^NnKc*b115bS)lw8)4@YR1aXY&7VJx_@ z^vGR3C6!c_9^8F6RhnS$MLsbx>JzSg>J{x+%y$sujhqZGW-hs8iiqk07aPh$RU5U1 zeqM08PKRsTH!}KubKH@VzMsGY57k=OO2`*$cI63U5o35I->O~J;NWlb3qhY>+_3IK z+7IQrrQEny$7YMeye_UUGBdUeU`Ap?U|}2c6S6UuJ~C9%<^;*0y~lw6V#Lk7X}JmP zuTUO#vh-_H|764z;G)agC~7R8|3@p){^(jIldaz;q`Ws!h|*1+9=V2;JA~po9c%?k10H|RwD5F>@Qzuu_72hg*8Px(qn8Z2les#+Z+$Zc zdr|rogd0!K51D;FBjFrX{WanAPt^>?fm0uTB+J8d38swliS2DH0ok`{E*DP{Ps>K7 z1!85KcIayE|{zS5L~v`w}Wd&&v_m}&}sAE@>0 z>*77D<-?`JlXqi;qmDKVLS&HR4L33uT2AUuIR!^|9e=E1gP* z6)?x16rJM!GC!3oSxc}0r?IhRPy+;IYl$Om68w>q720bfY~vPLb8f>qfvF_^sppFo zA7_NBoDlxgPEoWd&Fo7j3v>lG_Hg&c(Q>Aq`O*oiUw{yYB%N3Umj0Ic&iE?6qZS|G z;NU7B9!akfg`1K89|Za5Gq)L>!-e{NjGXdl^q|m9!A-$+>rf7py3RYYs|gj4noaJ@ z4W)Ga?ka^G>-~gks<=2C4+4 zuKYJ*JagJw=Kn&B74+#Azx7}b!o(b%ZNHGTzWe`F$_O5TI0fX4Ae9XiXEfVLd z5BqYJdAla1ZIsq9-r2`36s878J)vNxhL8Kwh+{PFW zYOFX6lIoo4+|VoF18cZ!s#esSr#aU%X}UhUl*pJgW~HyWpWoO$*B83h4BxgDyyOpg zFj4k{Jvr;UikDM-r0;Wuc2$GX!poP}CY=p-simu6%qo#HI`vu?{VszW9ZC+~+)_7x z&bccpLOF~?Gd;re1k&&8;_cuer-eaUiLW0tN|0op%~1;$(Zwz62E+PuYdI|L7%o3c z^3nOxM(AIhnI~iGqo#w=(ks;2^|K~Dj9brz?F+7}$@6I0m0@WX(c9`E~9Ywbw@7o!9TH-S3PEw;)YQGkV}l${Nw5^YRyIVl!f*_T0dxl~v%tFGJRx zJ9y&iZ3pLL^mS3=>MlkDJ_d1tx!F{;Ct~?|FKEfYA=#Sae*{^inT(9vx)hz5c@a;Y z4!Vwkv?(`ZA5q0AD)^y)$B5zziZ@M}6mgER!fNf})KX)1uhPO%3r#aC4k2r2RKVu2 z=W-zcVojb7kBp&uG@!E3J;t(rXg4VENxS}CxYc4F?%v2_2^(i-O9<1EhFdUNnPO&k zH}}d1n{|h$^3W?Mb*t3XI+!A08PU;WCm+|;^CMaCErxn34;2Jnf20rGYZ}|RqQSt5 z70YDSh_hf}?~ami-C+tp0zv#B1tT1Zrp|-eWE~#_W3$`Jn?u|~PSI}&GkJ7cejrI~ zGOnZOc6rOqyc1TM_BDikVz|suHsJ<347|Mt>;c=asOEFMuF9i#^04iBWZwk)dmpk3 z$?txGf6aBC0qC|!J1gh6)$BgumhR^q*5BJeIF6V6+5D4{##RpLYEXRsIG=WXkGGv` z%qo6+frdSO0pPSt?c z&hXK6fM|zK9I!p;$k8W5x|gq}621d0yk@9+* z=3+|qgL5E3O~f^61-<1DiZ2{nQ3$#*8e|YxqO9|IEWp}|{;WB#*&Bv8r0m=|>STrw zLa?osR%nKD9a~!p18UD>IJ4VDJRiS5sC?_VX0;PG+T+Yb#8S*&r%?ex`-xY^8bFV%@KZ4W|jPo%KQHJ^=2LuD_~xPrzqI(sFpHtkEOD} zWb>~gKs*+-{2qse7yE0xYS)z$p4%3T_&b~9pJ4$7@7cdWt8>#1Rt5OV6RnRkUv65P zw&`QdtC)XlPtgU`V|$aI&$LSQ!1d(lzRVU_3(qzy7#7?IY%K!shnAKKO*Q$2rgp4& zwP7kuE$asN3$NX?=l^V$b&fvABPztQ99Hhoz3P0f4EL|BCploN^Ct3LE6mA%;>I#B zJfbQ0ZVz7kC6bl=4_w!I)@RLPb@nV)7XnZF^7`{G0kbf~>8y+~rkXZc9$}sRMMX|h zP-ll?7sl&NN$J# literal 0 HcmV?d00001 diff --git a/doc/reference/auth/xport_layer.png b/doc/reference/auth/xport_layer.png new file mode 100644 index 0000000000000000000000000000000000000000..06ffe1d68c6afba307b6afa39b5187af63ff1e04 GIT binary patch literal 57608 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 ajb~nmrx|8OKI6Y1vkO=Lsyyd$?>_;oP>PlS literal 0 HcmV?d00001 diff --git a/doc/reference/index.rst b/doc/reference/index.rst index 29bb60fc9738..8f82cedd54a7 100644 --- a/doc/reference/index.rst +++ b/doc/reference/index.rst @@ -9,6 +9,7 @@ API Reference overview.rst terminology.rst audio/index.rst + auth/auth_api_ref.rst misc/notify.rst bluetooth/index.rst kconfig/index.rst @@ -35,3 +36,4 @@ API Reference settings/index.rst timing_functions/index.rst virtualization/index.rst + diff --git a/include/auth/auth_lib.h b/include/auth/auth_lib.h new file mode 100644 index 000000000000..a319e3e9135c --- /dev/null +++ b/include/auth/auth_lib.h @@ -0,0 +1,335 @@ +/** + * @file auth_lib.h + * + * @brief Authentication library functions + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_AUTH_LIB_H_ +#define ZEPHYR_INCLUDE_AUTH_LIB_H_ + + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Authentication API + * @defgroup zauth_api Authentication API + * @ingroup zauth + * @{ + */ + +#include + +/* + * Determine number of auth instances. + */ +#if (CONFIG_NUM_AUTH_INSTANCES == 0) +#error Error at least one Authentication instance must be defined. +#endif + +#if (CONFIG_NUM_AUTH_INSTANCES > 0) +#define AUTH_INSTANCE_1 +#endif + +#if (CONFIG_NUM_AUTH_INSTANCES > 1) +#define AUTH_INSTANCE_2 +#endif + + +/** + * Auth instance id. Either AUTH_INST_1_ID or AUTH_INST_2_ID + * Each instance performs authentication over a given hardware transport + * such as Bluetooth or serial. It is possible to configure the authentication + * library to authenticate over BLE and Serial. Or if the device + * is a Bluetooth central, then two instances can be used to authenticate + * two different peripherals. + */ +enum auth_instance_id { + +#if defined(AUTH_INSTANCE_1) + /** Auth instance 1 */ + AUTH_INST_1_ID = 0, +#endif + +#if defined(AUTH_INSTANCE_2) + /** Auth instance 2 */ + AUTH_INST_2_ID = 1, +#endif + + AUTH_MAX_INSTANCES +}; + + +#include + +/** Authentication success */ +#define AUTH_SUCCESS 0 +/** Auth error base value */ +#define AUTH_ERROR_BASE (-200) +/** Invalid param passed to function */ +#define AUTH_ERROR_INVALID_PARAM (AUTH_ERROR_BASE - 1) +/** Out of memory */ +#define AUTH_ERROR_NO_MEMORY (AUTH_ERROR_BASE - 2) +/** Operation timeout */ +#define AUTH_ERROR_TIMEOUT (AUTH_ERROR_BASE - 3) +/** No resource such as buffers or DTLS context buffer */ +#define AUTH_ERROR_NO_RESOURCE (AUTH_ERROR_BASE - 4) +/** DTLS initialization failed. */ +#define AUTH_ERROR_DTLS_INIT_FAILED (AUTH_ERROR_BASE - 5) +/** One of the Rx or Tx buffers are full */ +#define AUTH_ERROR_IOBUFF_FULL (AUTH_ERROR_BASE - 6) +/** An unexpected error, such as a Bluetooth handle unexpectedly missing. */ +#define AUTH_ERROR_INTERNAL (AUTH_ERROR_BASE - 7) +/** The lower transport failed to send */ +#define AUTH_ERROR_XPORT_SEND (AUTH_ERROR_BASE - 8) +/** A framing error occurred when re-assembling a message. */ +#define AUTH_ERROR_XPORT_FRAME (AUTH_ERROR_BASE - 9) +/** An error occurred when performming a crypto operation */ +#define AUTH_ERROR_CRYPTO (AUTH_ERROR_BASE - 10) +/** The authentication check failed. */ +#define AUTH_ERROR_FAILED (AUTH_ERROR_BASE - 11) +/** The authentication was canceled*/ +#define AUTH_ERROR_CANCELED (AUTH_ERROR_BASE - 12) + + +/* + * Flags used when initializing authentication connection + */ +enum auth_flags { + /** Server role */ + AUTH_CONN_SERVER = BIT(0), + /** Client role */ + AUTH_CONN_CLIENT = BIT(1), + /** Use DTLS for authentication */ + AUTH_CONN_DTLS_AUTH_METHOD = BIT(2), + /** Use Challenge-Response for authentication */ + AUTH_CONN_CHALLENGE_AUTH_METHOD = BIT(3) +}; + + +/** + * Authentication status enums + */ +enum auth_status { + /** Authentication started */ + AUTH_STATUS_STARTED, + /** Authentication in process */ + AUTH_STATUS_IN_PROCESS, + /** Authentication has been canceled */ + AUTH_STATUS_CANCELED, + /** An internal failure of some type */ + AUTH_STATUS_FAILED, + /** Authentication failed */ + AUTH_STATUS_AUTHENTICATION_FAILED, + /** Authentication successful */ + AUTH_STATUS_SUCCESSFUL +}; + + + +/* Forward declaration */ +struct authenticate_conn; + +/** + * Authentication function prototype + */ +typedef void (*auth_instance_func_t)(struct authenticate_conn *); + +/** + * Authentication callback status function + */ +typedef void (*auth_status_cb_t)(struct authenticate_conn *auth_conn, enum auth_instance_id instance, + enum auth_status status, void *context); + + +/** + * @brief Used to manage one authentication instance with a peer. It is possible + * to have multiple concurrent authentication instances. For example if + * a device is acting as a Central and Peripheral concurrently. + */ +struct authenticate_conn { + bool is_client; /* True if client */ + + /* lower transport opaque handle */ + auth_xport_hdl_t xport_hdl; + + /* current status of the authentication process */ + enum auth_status curr_status; + + /* The auth instance ID for this connection */ + enum auth_instance_id instance; + + /* status callback func */ + auth_status_cb_t status_cb; + void *callback_context; + + /* Work queue used to return status. Important if authentication + * status changes/fails in an ISR context */ + struct k_work auth_status_work; + + + /* authentication function, performs the actual authentication */ + auth_instance_func_t auth_func; + + /* cancel the authentication */ + volatile bool cancel_auth; + + /* Pointer to internal details, do not touch!!! */ + void *internal_obj; +}; + + +/** + * Optional authentication parameters specific to a particular authentication method. + * This is a "catch-all" for parameters such as certificates or device salts when + * using an authentication method. For example, a user password or PIN code would + * be passed to a password based authentication method such as J-PAKE. + */ + +/* + * Optional parameter id + */ +enum auth_opt_param_id { + /** Optional DTLS param */ + AUTH_DTLS_PARAM = 1, + /** Optional Challenge-Response param */ + AUTH_CHALRESP_PARAM, + /** Placeholder for future use */ + AUTH_PLACHOLDER_PARAM +}; + + +/** + * Structs specific to DTLS authentication + */ +struct auth_certificate_info { + const uint8_t *cert; /* Cert or cert chain in PEM format. */ + size_t cert_size; /* Size of cert or chain */ + + /* Optional private key for this cert, set to NULL if + * not used. */ + const uint8_t *priv_key; + size_t priv_key_size; +}; + +/** + * DTLS cert chain plus key info + */ +struct auth_dtls_certs { + struct auth_certificate_info server_ca_chain_pem; + struct auth_certificate_info device_cert_pem; +}; + +/** + * Optional shared key vs. hard-coded key. This shared key could come + * from a secure element such as a Microchip ATECC608A or NXP SE050 device. + */ +struct auth_challenge_resp { + const uint8_t *shared_key; /* 32 byte random nonce */ +}; + +/** + * Placeholder for future params + */ +struct auth_placeholder_opt { + void *placeholder; +}; + + +/** + * Optional parameters specific to the authentication method used. + */ +struct auth_optional_param { + enum auth_opt_param_id param_id; + union opt_params { + struct auth_dtls_certs dtls_certs; + struct auth_challenge_resp chal_resp; + + /* For future use, additional optional params are added + * added here. */ + struct auth_placeholder_opt placeholder_opt; + } param_body; +}; + + +/** + * Initializes authentication library + * + * @param auth_conn Authentication connection struct, initialized by this call. + * @param instance The instance ID. + * @param status_func Status function callback. + * @param context Optional context used in status callback. NULL if not used. + * @param opt_params Optional params specific to the authentication method selected. NULL if not used. + * @param auth_flags Authentication flags. + * + * @return AUTH_SUCCESS on success else one of AUTH_ERROR_* values. + */ +int auth_lib_init(struct authenticate_conn *auth_conn, enum auth_instance_id instance, + auth_status_cb_t status_func, void *context, struct auth_optional_param *opt_params, + enum auth_flags auth_flags); + + +/** + * Frees up any previously allocated resources. + * + * @param auth_conn Pointer to Authentication connection struct. + * + * @return AUTH_SUCCESS on success else one of AUTH_ERROR_* values. + */ +int auth_lib_deinit(struct authenticate_conn *auth_conn); + + +/** + * Starts the authentication process + * + * @param auth_conn Authentication connection struct. + * + * @return AUTH_SUCCESS on success else one of AUTH_ERROR_* values. + */ +int auth_lib_start(struct authenticate_conn *auth_conn); + +/** + * Returns the current status of the authentication process. + * + * @param auth_conn Authentication connection struct. + * + * @return One of AUTH_STATUS_* values + */ +enum auth_status auth_lib_get_status(struct authenticate_conn *auth_conn); + +/** + * Helper routine to return string corresponding to status + * + * @param status Authentication status value. + * + * @return Pointer to string representing the status. + */ +const char *auth_lib_getstatus_str(enum auth_status status); + + +/** + * Cancels the authentication process. Must wait until the AUTH_STATUS_CANCELED + * status is returned. + * + * @param auth_conn Authentication connection struct. + * + * @return One of AUTH_STATUS_* values + */ +int auth_lib_cancel(struct authenticate_conn *auth_conn); + + + +/** + * @} + */ + + +#ifdef __cplusplus +} +#endif + + +#endif /* ZEPHYR_INCLUDE_AUTH_LIB_H_ */ diff --git a/include/auth/auth_xport.h b/include/auth/auth_xport.h new file mode 100644 index 000000000000..d6f3b284d6cd --- /dev/null +++ b/include/auth/auth_xport.h @@ -0,0 +1,426 @@ +/** + * @file auth_xport.h + * + * @brief + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_AUTH_XPORT_H_ +#define ZEPHYR_INCLUDE_AUTH_XPORT_H_ + +#if defined(CONFIG_BT_XPORT) +#include +#endif + + +/** + * Transport functions and defines. + */ + + +/** + * Handle to lower transport, should be treated as opaque object. + * + * @note A typedef is use here because the transport handle is intended to + * be an opaque object to the lower transport layers and to the + * upper layers calling into the transport. This use satisfies + * the Linux coding standards. + */ +typedef void *auth_xport_hdl_t; + +/** + * The lower transport type. + */ +enum auth_xport_type { + AUTH_XP_TYPE_NONE = 0, + AUTH_XP_TYPE_BLUETOOTH, + AUTH_XP_TYPE_SERIAL, + AUTH_XP_TYPE_FUTURE /* new transports added here */ +}; + + +/** + * Transport event type. + */ +enum auth_xport_evt_type { + XP_EVT_NONE = 0, + XP_EVT_CONNECT, + XP_EVT_DISCONNECT, + XP_EVT_RECONNECT, + + /* transport specific events */ + XP_EVT_SERIAL_BAUDCHANGE +}; + +/** + * Transport event structure + */ +struct auth_xport_evt { + enum auth_xport_evt_type event; + + /* transport specific event information */ + void *xport_ctx; +}; + +/** + * Callback invoked when sending data asynchronously. + * + * @param err Error code, 0 == success. + * @param numbytes Number of bytes sent, can be 0. + */ +typedef void (*send_callback_t)(int err, uint16_t numbytes); + + +/** + * Function for sending data directly to the lower layer transport + * instead of putting data on an output queue. Some lower transport + * layers have the ability to queue outbound data, no need to double + * buffer. + * + * @param xport_hdl Opaque transport handle. + * @param data Data to send. + * @param len Number of bytes to send + * + * @return Number of bytes sent, on error negative error value. + */ +typedef int (*send_xport_t)(auth_xport_hdl_t xport_hdl, const uint8_t *data, + const size_t len); + + +/** + * Initializes the lower transport layer. + * + * @param xporthdl New transport handle is returned here. + * @param instance Authentication instance. + * @param xport_type Transport type + * @param xport_params Transport specific params, passed directly to lower transport. + * + * @return 0 on success, else negative error number. + */ +int auth_xport_init(auth_xport_hdl_t *xporthdl, enum auth_instance_id instance, + enum auth_xport_type xport_type, void *xport_params); + +/** + * De-initializes the transport. The lower layer transport should + * free any allocated resources. + * + * @param xporthdl + * + * @return AUTH_SUCCESS or negative error value. + */ +int auth_xport_deinit(const auth_xport_hdl_t xporthdl); + +/** + * Send event to the lower transport. + * + * @param xporthdl Transport handle. + * @param event Event + * + * @return 0 on success, else -1 + */ + +int auth_xport_event(const auth_xport_hdl_t xporthdl, struct auth_xport_evt *event); +/** + * Sends packet of data to peer. + * + * @param xporthdl Transport handle + * @param data Buffer to send. + * @param len Number of bytes to send. + * + * @return Number of bytes sent on success, can be less than requested. + * On error, negative error code. + */ +int auth_xport_send(const auth_xport_hdl_t xporthdl, const uint8_t *data, size_t len); + + +/** + * Receive data from the lower transport. + * + * @param xporthdl Transport handle + * @param buff Buffer to read bytes into. + * @param buf_len Size of buffer. + * @param timeoutMsec Wait timeout in milliseconds. If no bytes available, then + * wait timeoutMec milliseconds. If 0, then will not wait. + * + * @return Negative on error or timeout, else number of bytes received. + */ +int auth_xport_recv(const auth_xport_hdl_t xporthdl, uint8_t *buff, uint32_t buf_len, uint32_t timeoutMsec); + + +/** + * Peeks at the contents of the receive queue used by the lower transport. The + * data returned is not removed from the receive queue. + * + * @param xporthdl Transport handle + * @param buff Buffer to read bytes into. + * @param buf_len Size of buffer. + * + * @return Negative on error or timeout, else number of bytes peeked. + */ +int auth_xport_recv_peek(const auth_xport_hdl_t xporthdl, uint8_t *buff, uint32_t buf_len); + +/** + * Used by lower transport to put bytes reveived into rx queue. + * + * @param xporthdl Transport handle. + * @param buf Pointer to bytes to put. + * @param buflen Byte len of buffer. + * + * @return Number of bytes added to receive queue. + */ +int auth_xport_put_recv(const auth_xport_hdl_t xporthdl, const uint8_t *buf, size_t buflen); + + +/** + * Get the number of bytes queued for sending. + * + * @param xporthdl Transport handle. + * + * @return Number of queued bytes, negative value on error. + */ +int auth_xport_getnum_send_queued_bytes(const auth_xport_hdl_t xporthdl); + + +/** + * Get the number of bytes in the receive queue + * + * @param xporthdl Transport handle. + * + * @return Number of queued bytes, negative value on error. + */ +int auth_xport_getnum_recvqueue_bytes(const auth_xport_hdl_t xporthdl); + +/** + * Get the number of bytes in the receive queue, if no byte wait until + * bytes are received or time out. + * + * @param xporthdl Transport handle.l + * @param waitmsec Number of milliseconds to wait. + * + * @return Number of queued bytes, negative value on error. + */ +int auth_xport_getnum_recvqueue_bytes_wait(const auth_xport_hdl_t xporthdl, uint32_t waitmsec); + +/** + * Sets a direct send function to the lower transport layer instead of + * queuing bytes into an output buffer. Some lower transports can handle + * all of the necessary output queuing while others (serial UARTs for example) + * may not have the ability to queue outbound byes. + * + * @param xporthdl Transport handle. + * @param send_func Lower transport send function. + */ +void auth_xport_set_sendfunc(auth_xport_hdl_t xporthdl, send_xport_t send_func); + + +/** + * Used by the lower transport to set a context for a given transport handle. To + * clear a previously set context, use NULL as context pointer. + * + * @param xporthdl Transport handle. + * @param context Context pointer to set. + * + */ +void auth_xport_set_context(auth_xport_hdl_t xporthdl, void *context); + +/** + * Returns pointer to context. + * + * @param xporthdl Transport handle. + * + * @return Pointer to transport layer context, else NULL + */ +void *auth_xport_get_context(auth_xport_hdl_t xporthdl); + +/** + * Get the application max payload the lower transport can handle in one + * in one frame. The common transport functions will break up a larger + * application packet into multiple frames. + * + * @param xporthdl Transport handle. + * + * @return The max payload, or negative error number. + */ +int auth_xport_get_max_payload(const auth_xport_hdl_t xporthdl); + + +#if defined(CONFIG_BT_XPORT) + + +/** + * Bluetooth UUIDs for the Authentication service. + */ +#if defined(CONFIG_ALT_AUTH_BT_UUIDS) + +/** + * If desired, define differnet UUIDs in this header file to be included + * by the firmware application. + */ +#include "authlib_alt_bt_uuids.h" + +#else + +#define BT_BASE_AUTH_UUID "00000000-a3f6-4491-8b4d-b830f521243b" +#define BT_UUID_AUTH_SVC (0x3010) +#define BT_UUID_AUTH_SVC_CLIENT_CHAR (0x3015) +#define BT_UUID_AUTH_SVC_SERVER_CHAR (0x3016) + +#define AUTH_SERVICE_UUID_BYTES BT_UUID_128_ENCODE(BT_UUID_AUTH_SVC, 0xa3f6, 0x4491, 0x8b4d, 0xb830f521243b) +#define AUTH_SERVICE_UUID BT_UUID_INIT_128(AUTH_SERVICE_UUID_BYTES) + + +#define AUTH_SVC_CLIENT_CHAR_UUID BT_UUID_INIT_128( \ + BT_UUID_128_ENCODE(BT_UUID_AUTH_SVC_CLIENT_CHAR, 0xa3f6, 0x4491, 0x8b4d, 0xb830f521243b)) + +#define AUTH_SVC_SERVER_CHAR BT_UUID_INIT_128( \ + BT_UUID_128_ENCODE(BT_UUID_AUTH_SVC_SERVER_CHAR, 0xa3f6, 0x4491, 0x8b4d, 0xb830f521243b)) +#endif + + +/** + * Bluetooth params. + */ +struct auth_xp_bt_params { + struct bt_conn *conn; + bool is_central; + + /* The BT value handle used by the Central to send to the Peripheral. + * Not used by the Peripheral. */ + uint16_t server_char_hdl; + + /* Client attribute, used by peripheral to indicate data for client. + * Not used by the Central (client) */ + const struct bt_gatt_attr *client_attr; +}; + +/** + * Initialize Bluetooth transport + * + * @param xport_hdl Transport handle. + * @param flags Reserved for future use, should be set to 0. + * @param xport_param Pointer to Bluetooth transport params for use by BT layer. + * + * @return 0 on success, else negative error value + */ +int auth_xp_bt_init(const auth_xport_hdl_t xport_hdl, uint32_t flags, void *xport_param); + + +/** + * Deinit the lower BT later. + * + * @param xport_hdl The transport handle to de-initialize. + * + * @return 0 on success, else negative error number. + */ +int auth_xp_bt_deinit(const auth_xport_hdl_t xport_hdl); + +/* + * Called when the Central (client) writes to a Peripheral (server) characteristic. + * + * @param conn The BT connection. + * @param attr GATT attribute to write to. + * @param buf Data to write. + * @param len Number of bytes to write. + * @param offset Offset to start writing from. + * @param flags BT_GATT_WRITE_* flags + * + * @return Number of bytes written. + */ +ssize_t auth_xp_bt_central_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags); + + +/** + * Called by the Central (client) when a Peripheral (server) writes/updates a characteristic. + * This function is called by the Central BT stack when data is received by the Peripheral (server) + * + * @param conn Bluetooth connection struct. + * @param params Gatt subscription params. + * @param data Data from peripheral. + * @param length Number of bytes reived. + * + * @return BT_GATT_ITER_CONTINUE or BT_GATT_ITER_STOP + */ +uint8_t auth_xp_bt_central_notify(struct bt_conn *conn, struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length); + + +/** + * Sends Bluetooth event to lower Bluetooth transport. + * + * @param xporthdl Transport handle. + * @param event The event. + * + * @return AUTH_SUCCESS, else negative error code. + */ +int auth_xp_bt_event(const auth_xport_hdl_t xporthdl, struct auth_xport_evt *event); + +/** + * Gets the maximum payload for the Bluetooth link, which is the MTU less any + * Bluetooth link overhead. + * + * @param xporthdl Transport handle. + * + * @return Max application payload. + */ +int auth_xp_bt_get_max_payload(const auth_xport_hdl_t xporthdl); + +#endif /* CONFIG_BT_XPORT */ + + +#if defined(CONFIG_SERIAL_XPORT) + +/** + * Serial transport param. + */ +struct auth_xp_serial_params { + const struct device *uart_dev; /* pointer to Uart instance */ +}; + +/** + * Initialize Serial lower layer transport. + * + * @param xport_hdl Transport handle. + * @param flags RFU (Reserved for future use), set to 0. + * @param xport_params Serial specific transport parameters. + * + * @return 0 on success, else negative value. + */ +int auth_xp_serial_init(const auth_xport_hdl_t xport_hdl, uint32_t flags, void *xport_param); + + +/** + * Deinit lower serial transport. + * + * @param xport_hdl Transport handle + * + * @return 0 on success, else negative value. + */ +int auth_xp_serial_deinit(const auth_xport_hdl_t xport_hdl); + + +/** + * Sends an event to lower serial transport. + * + * @param xporthdl Transport handle. + * @param event The event. + * + * @return AUTH_SUCCESS, else negative error code. + */ +int auth_xp_serial_event(const auth_xport_hdl_t xporthdl, struct auth_xport_evt *event); + + +/** + * Gets the maximum payload for the serial link. + * + * @param xporthdl Transport handle. + * + * @return Max application payload. + */ +int auth_xp_serial_get_max_payload(const auth_xport_hdl_t xporthdl); + +#endif /* CONFIG_SERIAL_XPORT */ + + +#endif /* ZEPHYR_INCLUDE_AUTH_XPORT_H_ */ \ No newline at end of file diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 9ca2f968df2f..70833ca71654 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -10,3 +10,4 @@ add_subdirectory_ifdef(CONFIG_FNMATCH fnmatch) add_subdirectory(gui) add_subdirectory(os) add_subdirectory_ifdef(CONFIG_OPENAMP open-amp) +add_subdirectory(auth) diff --git a/lib/Kconfig b/lib/Kconfig index 04b1819ea15f..76a8bee97844 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -19,4 +19,6 @@ source "lib/posix/Kconfig" source "lib/open-amp/Kconfig" +source "lib/auth/Kconfig" + endmenu diff --git a/lib/auth/CMakeLists.txt b/lib/auth/CMakeLists.txt new file mode 100644 index 000000000000..e2c325588391 --- /dev/null +++ b/lib/auth/CMakeLists.txt @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Only use if CONFIG_AUTH_LIB is defined as True +if(CONFIG_AUTH_LIB) + zephyr_interface_library_named(authentication) + zephyr_library() + + zephyr_library_sources_ifdef(CONFIG_AUTH_LIB auth_lib.c auth_xport_common.c) + + zephyr_library_sources_ifdef(CONFIG_BT_XPORT auth_xport_bt.c) + + zephyr_library_sources_ifdef(CONFIG_SERIAL_XPORT auth_xport_serial.c) + + zephyr_library_sources_ifdef(CONFIG_AUTH_CHALLENGE_RESPONSE auth_chalresp.c) + + zephyr_library_sources_ifdef(CONFIG_AUTH_DTLS auth_dtls.c) +endif() + +# Is this the correct way to include Mbed TLS module directory? +if(CONFIG_MBEDTLS) + zephyr_include_directories($ENV{ZEPHYR_BASE}/../modules/crypto/mbedtls/include) + zephyr_include_directories($ENV{ZEPHYR_BASE}/../modules/crypto/mbedtls/configs) +endif() + diff --git a/lib/auth/Kconfig b/lib/auth/Kconfig new file mode 100644 index 000000000000..5ba980df516b --- /dev/null +++ b/lib/auth/Kconfig @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: Apache-2.0 + + + +menuconfig AUTH_LIB + bool "Authentication library" + help + This option enables the Authentication library. + +if AUTH_LIB + config AUTH_CHALLENGE_RESPONSE + bool "Challenge Response (Must use TINYCRYPT and TINYCRYPT_SHA256)" + help + Selects the Challenge Response authentication method. + Must use TINYCRYPT and TINYCRYPT_SHA256 + + config AUTH_DTLS + bool "DTLS Authentication" + help + Selects the DTLS authentication method. + + config AUTH_LOG_LEVEL + int "Authentication log level" + depends on LOG + range 0 4 + default 0 + help + Sets authentication log level + Levels are: + 0 OFF, do not write + 1 ERROR, only write LOG_ERR + 2 WARNING, write LOG_WRN in addition to previous level + 3 INFO, write LOG_INF in addition to previous levels + 4 DEBUG, write LOG_DBG in addition to previous levels + + config BT_XPORT + bool "Bluetooth Transport" + help + If using Bluetooth to authenticate peer. + + + config BT_ALT_AUTH_BT_UUIDS + bool "Authentication service alternate Bluetooth UUIDs" + depends on BT_XPORT + help + To use alternate Bluetooth Auth service UUIDs. Alternative UUIDs should be + set in authlib_alt_bt_uuids.h header and included with firmware app. See + auth_xport.h header file for details. + + + config SERIAL_XPORT + bool "Serial Transport" + help + If using serial port to authenticate peer. + + + config NUM_AUTH_INSTANCES + int "Number of authentication instances" + range 1 2 + default 1 + help + Each authentication instance uses a thread to authenticate with + a peer over the lower transport. It is possible to have multiple + authentication instances where one instance authenticates a peer + over Bluetooth and another authenticates over a serial link. Another example + would be a Bluetooth Central needing to authenticate multiple + connected Peripherals. + + config AUTH_THREAD_PRIORITY + int "Authentication thread priority." + default 0 + help + The priority of the authentication thread. + + +endif + diff --git a/lib/auth/auth_chalresp.c b/lib/auth/auth_chalresp.c new file mode 100644 index 000000000000..74ef6cb5ff05 --- /dev/null +++ b/lib/auth/auth_chalresp.c @@ -0,0 +1,637 @@ +/** + * @file auth_chalresp.c + * + * @brief Challenge-Response method for authenticating connection. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + + + +#define LOG_LEVEL CONFIG_AUTH_LOG_LEVEL +#include +LOG_MODULE_DECLARE(auth_lib, CONFIG_AUTH_LOG_LEVEL); + +#include "auth_internal.h" + +#define AUTH_SHA256_HASH (TC_SHA256_DIGEST_SIZE) +#define AUTH_SHARED_KEY_LEN (32u) +#define AUTH_CHALLENGE_LEN (32u) +#define AUTH_CHAL_RESPONSE_LEN AUTH_SHA256_HASH + +/* magic number to help identify and parse messages */ +#define CHALLENGE_RESP_SOH 0x65A2 + +/* Message IDs */ +#define AUTH_CLIENT_CHAL_MSG_ID 0x01 +#define AUTH_SERVER_CHALRESP_MSG_ID 0x02 +#define AUTH_CLIENT_CHALRESP_MSG_ID 0x03 +#define AUTH_CHALRESP_RESULT_MSG_ID 0x04 + +/* Timeout for receive */ +#define AUTH_RX_TIMEOUT_MSEC (3000u) + + +/* ensure structs are byte aligned */ +#pragma pack(push, 1) + +/** + * Header for challenge response messages + */ +struct chalresp_header { + uint16_t soh; /* start of header */ + uint8_t msg_id; +}; + +/** + * Sent by the client, contains a random challenge which the + * server will hash with the know pre-shared key. + */ +struct client_challenge { + struct chalresp_header hdr; + uint8_t client_challenge[AUTH_CHALLENGE_LEN]; +}; + +/** + * Server response to the client challenge. The server responds with + * the hash of the client challenge and a server random challenge. + */ +struct server_chal_response { + struct chalresp_header hdr; + + /* Server created hash of the client challenge and + * the shared key */ + uint8_t server_response[AUTH_CHAL_RESPONSE_LEN]; + + /* To be hashed with the shared key by the client */ + uint8_t server_challenge[AUTH_CHALLENGE_LEN]; +}; + +/** + * Response from client to server challenge. + */ +struct client_chal_resp { + struct chalresp_header hdr; + /* hash of server challenge with shared key */ + uint8_t client_response[AUTH_CHAL_RESPONSE_LEN]; +}; + +/* From Central or Peripheral indicating result of challenge-response */ +struct auth_chalresp_result { + struct chalresp_header hdr; + uint8_t result; /* 0 == success, 1 == failure */ +}; + +#pragma pack(pop) + + +/** + * Shared key. + * @brief In a production system, the shared key should be stored in a + * secure hardware store such as an ECC608A or TrustZone. + */ +static uint8_t default_shared_key[AUTH_SHARED_KEY_LEN] = { + 0xBD, 0x84, 0xDC, 0x6E, 0x5C, 0x77, 0x41, 0x58, 0xE8, 0xFB, 0x1D, 0xB9, 0x95, 0x39, 0x20, 0xE4, + 0xC5, 0x03, 0x69, 0x9D, 0xBC, 0x53, 0x08, 0x20, 0x1E, 0xF4, 0x72, 0x8E, 0x90, 0x56, 0x49, 0xA8 +}; + +/* default shared key if not set */ +static uint8_t *shared_key = default_shared_key; + +/* If caller specifies a new shared key, it is copied into this buffer. */ +static uint8_t chalresp_key[AUTH_SHARED_KEY_LEN]; + +/** + * Utility function to create the hash of the random challenge and the shared key. + * Uses Tiny Crypt hashing code. + * + * @param random_chal Pointer to 32 byte value to hash with shared key. + * @param hash Buffer where hash is returned. + * + * @return AUTH_SUCCESS on success, else error value. + */ +static int auth_chalresp_hash(const uint8_t *random_chal, uint8_t *hash) +{ + int err = 0; + struct tc_sha256_state_struct hash_state; + + tc_sha256_init(&hash_state); + + /* Update the hash with the random challenge followed by the shared key. */ + if ((err = tc_sha256_update(&hash_state, random_chal, AUTH_CHALLENGE_LEN)) != TC_CRYPTO_SUCCESS || + (err = tc_sha256_update(&hash_state, shared_key, AUTH_SHARED_KEY_LEN)) != TC_CRYPTO_SUCCESS) { + return AUTH_ERROR_CRYPTO; + } + + /* calc the final hash */ + err = tc_sha256_final(hash, &hash_state) == TC_CRYPTO_SUCCESS ? + AUTH_SUCCESS : AUTH_ERROR_CRYPTO; + + return err; +} + +/** + * Checks header and id. + * + * @param hdr Message header. + * @param msg_id Message ID to check for. + * + * @return true if message is valid, else false. + */ +static bool auth_check_msg(struct chalresp_header *hdr, const uint8_t msg_id) +{ + if ((hdr->soh != CHALLENGE_RESP_SOH) || (hdr->msg_id != msg_id)) { + return false; + } + + return true; +} + + +/** + * Sends a challenge to the server. + * + * @param auth_conn Authentication connection structure. + * @param random_chal Random 32 byte challenge to send. + * + * @return true if message send successfully, else false on error. + */ +static bool auth_client_send_challenge(struct authenticate_conn *auth_conn, const uint8_t *random_chal) +{ + int numbytes; + struct client_challenge chal; + + /* build and send challenge message to Peripheral */ + memset(&chal, 0, sizeof(chal)); + chal.hdr.soh = CHALLENGE_RESP_SOH; + chal.hdr.msg_id = AUTH_CLIENT_CHAL_MSG_ID; + + memcpy(&chal.client_challenge, random_chal, sizeof(chal.client_challenge)); + + /* send to server */ + numbytes = auth_xport_send(auth_conn->xport_hdl, (uint8_t *)&chal, sizeof(chal)); + + if ((numbytes <= 0) || (numbytes != sizeof(chal))) { + /* error */ + LOG_ERR("Error sending challenge to server, err: %d", numbytes); + return false; + } + + return true; +} + +/** + * Receives and processes the challenge response from the server. + * + * @param auth_conn Authentication connection structure. + * @param random_chal 32 byte challenge sent to the server. + * @param status Pointer to return authentication status. + * + * @return true on success, else false. + */ +static bool auth_client_recv_chal_resp(struct authenticate_conn *auth_conn, const uint8_t *random_chal, + enum auth_status *status) +{ + uint8_t hash[AUTH_CHAL_RESPONSE_LEN]; + int numbytes; + int err; + struct server_chal_response server_resp; + struct client_chal_resp client_resp; + struct auth_chalresp_result chal_result; + uint8_t *buf = (uint8_t *)&server_resp; + int len = sizeof(server_resp); + + while (len > 0) { + + numbytes = auth_xport_recv(auth_conn->xport_hdl, buf, len, AUTH_RX_TIMEOUT_MSEC); + + /* canceled ? */ + if (auth_conn->cancel_auth) { + *status = AUTH_STATUS_CANCELED; + return false; + } + + /* timed out, try to read again */ + if (numbytes == -EAGAIN) { + continue; + } + + if (numbytes <= 0) { + LOG_ERR("Failed to read server challenge response, err: %d", numbytes); + *status = AUTH_STATUS_FAILED; + return false; + } + + buf += numbytes; + len -= numbytes; + } + + /* check message */ + if (!auth_check_msg(&server_resp.hdr, AUTH_SERVER_CHALRESP_MSG_ID)) { + LOG_ERR("Invalid message received from the server."); + *status = AUTH_STATUS_FAILED; + return false; + } + + + /* Now verify response, is the response correct? Hash the random challenge + * with the shared key. */ + err = auth_chalresp_hash(random_chal, hash); + + if (err) { + LOG_ERR("Failed to calc hash, err: %d", err); + *status = AUTH_STATUS_FAILED; + return false; + } + + /* Does the response match what is expected? */ + if (memcmp(hash, server_resp.server_response, sizeof(hash))) { + /* authentication failed */ + LOG_ERR("Server authentication failed."); + *status = AUTH_STATUS_AUTHENTICATION_FAILED; + + /* send failed message to the Peripheral */ + memset(&chal_result, 0, sizeof(chal_result)); + chal_result.hdr.soh = CHALLENGE_RESP_SOH; + chal_result.hdr.msg_id = AUTH_CHALRESP_RESULT_MSG_ID; + chal_result.result = 1; + + numbytes = auth_xport_send(auth_conn->xport_hdl, (uint8_t *)&chal_result, sizeof(chal_result)); + + if ((numbytes <= 0) || (numbytes != sizeof(chal_result))) { + LOG_ERR("Failed to send authentication error result to server."); + } + + return false; + } + + /* init Client response message */ + memset(&client_resp, 0, sizeof(client_resp)); + client_resp.hdr.soh = CHALLENGE_RESP_SOH; + client_resp.hdr.msg_id = AUTH_CLIENT_CHALRESP_MSG_ID; + + /* Create response to the server's random challenge */ + err = auth_chalresp_hash(server_resp.server_challenge, client_resp.client_response); + + if (err) { + LOG_ERR("Failed to create server response to challenge, err: %d", err); + *status = AUTH_STATUS_FAILED; + return false; + } + + /* send Client's response to the Server's random challenge */ + numbytes = auth_xport_send(auth_conn->xport_hdl, (uint8_t *)&client_resp, sizeof(client_resp)); + + if ((numbytes <= 0) || (numbytes != sizeof(client_resp))) { + LOG_ERR("Failed to send Client response."); + *status = AUTH_STATUS_FAILED; + return false; + } + + /* so far so good, need to wait for Server response */ + *status = AUTH_STATUS_IN_PROCESS; + return true; +} + +/** + * Server waits and receives a message from the client. + * + * @param auth_conn Authentication connection structure. + * @param msgbuf Buffer to copy message into. + * @param msglen Buffer byte length. + * + * @return true if number of bytes were received, else false. + */ +static bool auth_server_recv_msg(struct authenticate_conn *auth_conn, uint8_t *msgbuf, size_t msglen) +{ + int numbytes; + + while ((int)msglen > 0) { + + numbytes = auth_xport_recv(auth_conn->xport_hdl, msgbuf, msglen, AUTH_RX_TIMEOUT_MSEC); + + if (auth_conn->cancel_auth) { + return false; + } + + /* timed out, retry */ + if (numbytes == -EAGAIN) { + continue; + } + + if (numbytes <= 0) { + return false; + } + + msgbuf += numbytes; + msglen -= numbytes; + } + + return true; +} + +/** + * Handles the client challenge, creates a hash of the challenge with the + * shared key. + * + * @param auth_conn Authentication connection structure. + * @param server_random_chal The server random challenge to be sent to the client. + * + * @return true on success, else false on error. + */ +static bool auth_server_recv_challenge(struct authenticate_conn *auth_conn, uint8_t *server_random_chal) +{ + struct client_challenge chal; + struct server_chal_response server_resp; + int numbytes; + + if (!auth_server_recv_msg(auth_conn, (uint8_t *)&chal, sizeof(chal))) { + LOG_ERR("Failed to receive client challenge message."); + return false; + } + + if (!auth_check_msg(&chal.hdr, AUTH_CLIENT_CHAL_MSG_ID)) { + LOG_ERR("Invalid message."); + return false; + } + + /* create response and send back to the Client */ + server_resp.hdr.soh = CHALLENGE_RESP_SOH; + server_resp.hdr.msg_id = AUTH_SERVER_CHALRESP_MSG_ID; + + /* copy the server's challenge for the client */ + memcpy(server_resp.server_challenge, server_random_chal, + sizeof(server_resp.server_challenge)); + + /* Now create the response for the Client */ + auth_chalresp_hash(chal.client_challenge, server_resp.server_response); + + /* Send response */ + numbytes = auth_xport_send(auth_conn->xport_hdl, (uint8_t *)&server_resp, sizeof(server_resp)); + + if ((numbytes <= 0) || (numbytes != sizeof(server_resp))) { + LOG_ERR("Failed to send challenge response to the Client."); + return false; + } + + return true; +} + +/** + * Handles the client response to the server challenge. + * + * @param auth_conn Authentication connection structure. + * @param server_random_chal The server random challenge sent to the client. + * @param status Status of the Challenge-Response authentication set here. + * + * @return true on success, else false. + */ +static bool auth_server_recv_chalresp(struct authenticate_conn *auth_conn, uint8_t *server_random_chal, + enum auth_status *status) +{ + struct client_chal_resp client_resp; + struct auth_chalresp_result result_resp; + uint8_t hash[AUTH_SHA256_HASH]; + int err, numbytes; + + memset(&client_resp, 0, sizeof(client_resp)); + + /* read just the header */ + if (!auth_server_recv_msg(auth_conn, (uint8_t *)&client_resp, sizeof(client_resp.hdr))) { + LOG_ERR("Failed to receive challenge response from the Client"); + *status = AUTH_STATUS_FAILED; + return false; + } + + /* This is a result message, means the Client failed to authenticate the Server. */ + if (client_resp.hdr.msg_id == AUTH_CHALRESP_RESULT_MSG_ID) { + + /* read the remainder of the message */ + auth_server_recv_msg(auth_conn, (uint8_t *)&result_resp.result, sizeof(result_resp.result)); + + /* Result should be non-zero, meaning an authentication failure. */ + if (result_resp.result != 0) { + LOG_ERR("Unexpected result value: %d", result_resp.result); + } + + LOG_ERR("Client authentication failed."); + *status = AUTH_STATUS_AUTHENTICATION_FAILED; + return false; + } + + /* The Client authenticated the Server (this code) response. Now verify the Client's + * response to the Server challenge. */ + if (!auth_server_recv_msg(auth_conn, (uint8_t *)&client_resp.hdr + sizeof(client_resp.hdr), + sizeof(client_resp) - sizeof(client_resp.hdr))) { + LOG_ERR("Failed to read Client response."); + *status = AUTH_STATUS_FAILED; + return false; + } + + err = auth_chalresp_hash(server_random_chal, hash); + if (err) { + LOG_ERR("Failed to create hash."); + *status = AUTH_STATUS_FAILED; + return false; + } + + /* init result response message */ + memset(&result_resp, 0, sizeof(result_resp)); + result_resp.hdr.soh = CHALLENGE_RESP_SOH; + result_resp.hdr.msg_id = AUTH_CHALRESP_RESULT_MSG_ID; + + /* verify Central's response */ + if (memcmp(hash, client_resp.client_response, sizeof(hash))) { + /* authentication failed, the Client did not sent the correct response */ + result_resp.result = 1; + } + + /* send result back to the Client */ + numbytes = auth_xport_send(auth_conn->xport_hdl, (uint8_t *)&result_resp, sizeof(result_resp)); + + if ((numbytes <= 0) || (numbytes != sizeof(result_resp))) { + LOG_ERR("Failed to send Client authentication result."); + *status = AUTH_STATUS_FAILED; + return false; + } + + *status = (result_resp.result == 0) ? AUTH_STATUS_SUCCESSFUL : AUTH_STATUS_AUTHENTICATION_FAILED; + + return true; +} + +/** + * Client function used to execute Challenge-Response authentication. + * + * @param auth_conn Authentication connection structure. + * + * @return AUTH_SUCCESS on success, else AUTH error code. + */ +static int auth_chalresp_client(struct authenticate_conn *auth_conn) +{ + int numbytes; + uint8_t random_chal[AUTH_CHALLENGE_LEN]; + struct auth_chalresp_result server_result; + enum auth_status status; + + /* generate random number as challenge */ + sys_rand_get(random_chal, sizeof(random_chal)); + + + if (!auth_client_send_challenge(auth_conn, random_chal)) { + auth_lib_set_status(auth_conn, AUTH_STATUS_FAILED); + return AUTH_ERROR_FAILED; + } + + /* check for cancel operation */ + if (auth_conn->cancel_auth) { + return AUTH_ERROR_CANCELED; + } + + /* read response from the sever */ + if (!auth_client_recv_chal_resp(auth_conn, random_chal, &status)) { + auth_lib_set_status(auth_conn, status); + return AUTH_ERROR_FAILED; + } + + /* Wait for the final response from the Server indicating success or failure + * of the Client's response. */ + numbytes = auth_xport_recv(auth_conn->xport_hdl, (uint8_t * ) &server_result, + sizeof(server_result), AUTH_RX_TIMEOUT_MSEC); + + /* check for cancel operation */ + if (auth_conn->cancel_auth) { + return AUTH_ERROR_CANCELED; + } + + if ((numbytes <= 0) || (numbytes != sizeof(server_result))) { + LOG_ERR("Failed to receive server authentication result."); + auth_lib_set_status(auth_conn, AUTH_STATUS_AUTHENTICATION_FAILED); + return AUTH_ERROR_FAILED; + } + + /* check message */ + if (!auth_check_msg(&server_result.hdr, AUTH_CHALRESP_RESULT_MSG_ID)) { + LOG_ERR("Server rejected Client response, authentication failed."); + auth_lib_set_status(auth_conn, AUTH_STATUS_AUTHENTICATION_FAILED); + return AUTH_ERROR_FAILED; + } + + /* check the Server result */ + if (server_result.result != 0) { + LOG_ERR("Authentication with server failed."); + auth_lib_set_status(auth_conn, AUTH_STATUS_AUTHENTICATION_FAILED); + return AUTH_ERROR_FAILED; + } + + LOG_INF("Authentication with server successful."); + auth_lib_set_status(auth_conn, AUTH_STATUS_SUCCESSFUL); + + return AUTH_SUCCESS; +} + +/** + * Server function used to execute Challenge-Response authentication. + * + * @param auth_conn Authentication connection structure. + * + * @return AUTH_SUCCESS on success, else AUTH error code. + */ +static int auth_chalresp_server(struct authenticate_conn *auth_conn) +{ + enum auth_status status; + uint8_t random_chal[AUTH_CHALLENGE_LEN]; + + /* generate random number as challenge */ + sys_rand_get(random_chal, sizeof(random_chal)); + + /* Wait for challenge from the Central */ + if (!auth_server_recv_challenge(auth_conn, random_chal)) { + auth_lib_set_status(auth_conn, AUTH_STATUS_FAILED); + return AUTH_ERROR_FAILED; + } + + /* check for cancel operation */ + if (auth_conn->cancel_auth) { + return AUTH_ERROR_CANCELED; + } + + /* Wait for challenge response from the Client */ + auth_server_recv_chalresp(auth_conn, random_chal, &status); + + auth_lib_set_status(auth_conn, status); + + if (status != AUTH_STATUS_SUCCESSFUL) { + LOG_INF("Authentication with Client failed."); + return AUTH_ERROR_FAILED; + } + + LOG_INF("Authentication with client successful."); + + return AUTH_SUCCESS; +} + + +/** + * @see auth_internal.h + */ +int auth_init_chalresp_method(struct authenticate_conn *auth_conn, struct auth_challenge_resp *chal_resp) +{ + /* verify inputs */ + if ((auth_conn == NULL) || (chal_resp == NULL)) { + return AUTH_ERROR_INVALID_PARAM; + } + + /* set new shared key */ + memcpy(chalresp_key, chal_resp->shared_key, AUTH_SHARED_KEY_LEN); + + /* set shared key pointer to new key */ + shared_key = chalresp_key; + + return AUTH_SUCCESS; +} + + +/** + * Use hash (SHA-256) with shared key to authenticate each side. + * + * @param auth_conn The authenticate_conn connection. + */ +void auth_chalresp_thread(struct authenticate_conn *auth_conn) +{ + int ret; + + auth_lib_set_status(auth_conn, AUTH_STATUS_STARTED); + + /** + * Since a device can be a client and server at the same + * time need to use is_client to determine which funcs to call. + * refactor this code. */ + if (auth_conn->is_client) { + ret = auth_chalresp_client(auth_conn); + } else { + ret = auth_chalresp_server(auth_conn); + } + + if (ret) { + LOG_ERR("Challenge-Response authentication failed, err: %d", ret); + } else { + LOG_INF("Successful Challenge-Response."); + } + + /* End of Challenge-Response authentication thread */ + LOG_DBG("Challenge-Response thread complete."); +} diff --git a/lib/auth/auth_dtls.c b/lib/auth/auth_dtls.c new file mode 100644 index 000000000000..dfcfd75a2c74 --- /dev/null +++ b/lib/auth/auth_dtls.c @@ -0,0 +1,959 @@ +/** + * @file BLE Authentication using DTLS + * + * @brief DTLS authentication code using Mbed DTLS + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + + +#if defined(CONFIG_MBEDTLS) +#if !defined(CONFIG_MBEDTLS_CFG_FILE) +#include "mbedtls/config.h" +#else +#include CONFIG_MBEDTLS_CFG_FILE +#endif +#endif /* CONFIG_MBEDTLS_CFG_FILE */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_LEVEL CONFIG_AUTH_LOG_LEVEL +#include +LOG_MODULE_DECLARE(auth_lib, CONFIG_AUTH_LOG_LEVEL); + +#include +#include "auth_internal.h" + +#define MBED_ERROR_BUFLEN (150u) +#define MAX_MBEDTLS_CONTEXT (CONFIG_NUM_AUTH_INSTANCES) +#define AUTH_DTLS_COOKIE_LEN (32u) + +#define DTLS_PACKET_SYNC_BYTES (0x45B8) +#define DTLS_HEADER_BYTES (sizeof(struct dtls_packet_hdr)) + +#define AUTH_DTLS_MIN_TIMEOUT (10000u) +#define ATUH_DTLS_MAX_TIMEOUT (30000u) +#define AUTH_DTLS_HELLO_WAIT_MSEC (15000u) + + +/** + * Header identifying a DTLS packet (aka datagram). Unlike TLS, DTLS packets + * must be forwarded to Mbedtls as one or more complete packets. TLS is + * design to handle an incoming byte stream. + */ +#pragma pack(push, 1) +struct dtls_packet_hdr { + uint16_t sync_bytes; /* use magic number to identify header */ + uint16_t packet_len; /* size of DTLS datagram */ +}; +#pragma pack(pop) + + + +/** + * Mbed context, one context per DTLS connection. + */ +struct mbed_tls_context { + bool in_use; + + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_x509_crt cacert; + mbedtls_x509_crt device_cert; + mbedtls_pk_context device_private_key; + mbedtls_timing_delay_context timer; + mbedtls_ssl_cookie_ctx cookie_ctx; + + /* Temp buffer used to assemble full frame when sending. */ + uint8_t temp_dtlsbuf[CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN]; + + /* cookie used for DTLS */ + uint8_t cookie[AUTH_DTLS_COOKIE_LEN]; +}; + +/** + * List of Mbed instances + */ +static struct mbed_tls_context tlscontext[MAX_MBEDTLS_CONTEXT]; + + +/* ===================== local functions =========================== */ + +/** + * Get a free Mbed context to use with an authentication instance. + * + * @return Pointer to context, NULL if no free context available. + */ +static struct mbed_tls_context *auth_get_mbedcontext(void) +{ + // use mutex lock to protect accessing list + for (int cnt = 0; cnt < MAX_MBEDTLS_CONTEXT; cnt++) { + + if (!tlscontext[cnt].in_use) { + tlscontext[cnt].in_use = true; + return &tlscontext[cnt]; + } + } + + return NULL; +} + +/** + * Free a Mbed context. + * + * @param mbed_ctx Pointer to context. + */ +static void auth_free_mbedcontext(struct mbed_tls_context *mbed_ctx) +{ + mbed_ctx->in_use = false; + + /* Free any MBed tls resources */ + mbedtls_x509_crt_free(&mbed_ctx->cacert); + mbedtls_x509_crt_free(&mbed_ctx->device_cert); + mbedtls_pk_free(&mbed_ctx->device_private_key); + mbedtls_ssl_free(&mbed_ctx->ssl); + mbedtls_ssl_config_free(&mbed_ctx->conf); + mbedtls_ctr_drbg_free(&mbed_ctx->ctr_drbg); + mbedtls_entropy_free(&mbed_ctx->entropy); +} + +/** + * Initialize Mbed context. + * + * @param mbed_ctx Pointer to context. + */ +static void auth_init_context(struct mbed_tls_context *mbed_ctx) +{ + mbedtls_ssl_init(&mbed_ctx->ssl); + mbedtls_ssl_config_init(&mbed_ctx->conf); + mbedtls_x509_crt_init(&mbed_ctx->cacert); + mbedtls_x509_crt_init(&mbed_ctx->device_cert); + mbedtls_pk_init(&mbed_ctx->device_private_key); + mbedtls_ssl_cookie_init(&mbed_ctx->cookie_ctx); + mbedtls_ctr_drbg_init(&mbed_ctx->ctr_drbg); + mbedtls_entropy_init(&mbed_ctx->entropy); + + sys_rand_get(mbed_ctx->cookie, sizeof(mbed_ctx->cookie)); +} + +/** + * Return the handshake state name, helpful for debug purposes. + * + * @param state The state enumeration. + * + * @return Pointer to handshake string name. + */ +static const char *auth_tls_handshake_state(const mbedtls_ssl_states state) +{ + switch (state) { + + case MBEDTLS_SSL_HELLO_REQUEST: + return "MBEDTLS_SSL_HELLO_REQUEST"; + break; + + case MBEDTLS_SSL_CLIENT_HELLO: + return "MBEDTLS_SSL_CLIENT_HELLO"; + break; + + case MBEDTLS_SSL_SERVER_HELLO: + return "MBEDTLS_SSL_SERVER_HELLO"; + break; + + case MBEDTLS_SSL_SERVER_CERTIFICATE: + return "MBEDTLS_SSL_SERVER_CERTIFICATE"; + break; + + case MBEDTLS_SSL_SERVER_KEY_EXCHANGE: + return "MBEDTLS_SSL_SERVER_KEY_EXCHANGE"; + break; + + case MBEDTLS_SSL_CERTIFICATE_REQUEST: + return "MBEDTLS_SSL_CERTIFICATE_REQUEST"; + break; + + case MBEDTLS_SSL_SERVER_HELLO_DONE: + return "MBEDTLS_SSL_SERVER_HELLO_DONE"; + break; + + case MBEDTLS_SSL_CLIENT_CERTIFICATE: + return "MBEDTLS_SSL_CLIENT_CERTIFICATE"; + break; + + case MBEDTLS_SSL_CLIENT_KEY_EXCHANGE: + return "MBEDTLS_SSL_CLIENT_KEY_EXCHANGE"; + break; + + case MBEDTLS_SSL_CERTIFICATE_VERIFY: + return "MBEDTLS_SSL_CERTIFICATE_VERIFY"; + break; + + case MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC: + return "MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC"; + break; + + case MBEDTLS_SSL_CLIENT_FINISHED: + return "MBEDTLS_SSL_CLIENT_FINISHED"; + break; + + case MBEDTLS_SSL_SERVER_CHANGE_CIPHER_SPEC: + return "MBEDTLS_SSL_SERVER_CHANGE_CIPHER_SPEC"; + break; + + case MBEDTLS_SSL_SERVER_FINISHED: + return "MBEDTLS_SSL_SERVER_FINISHED"; + break; + + case MBEDTLS_SSL_FLUSH_BUFFERS: + return "MBEDTLS_SSL_FLUSH_BUFFERS"; + break; + + case MBEDTLS_SSL_HANDSHAKE_WRAPUP: + return "MBEDTLS_SSL_HANDSHAKE_WRAPUP"; + break; + + case MBEDTLS_SSL_HANDSHAKE_OVER: + return "MBEDTLS_SSL_HANDSHAKE_OVER"; + break; + + case MBEDTLS_SSL_SERVER_NEW_SESSION_TICKET: + return "MBEDTLS_SSL_SERVER_NEW_SESSION_TICKET"; + break; + + case MBEDTLS_SSL_SERVER_HELLO_VERIFY_REQUEST_SENT: + return "MBEDTLS_SSL_SERVER_HELLO_VERIFY_REQUEST_SENT"; + break; + + default: + break; + } + + return "unknown"; +} + + +/** + * Gets the time since boot in Milliseconds, used for DTLS retry timer. The absolute + * time (wall clock time) isn't necessary, just the relative time in milliseconds. + * + * @param val Timer value + * @param reset If non-zero then set current milliseconds into the time var. + * + * @return 0 if resetting the time, else the time difference in milliseconds. + */ +static unsigned long auth_tls_timing_get_timer(struct mbedtls_timing_hr_time *val, int reset) +{ + int64_t delta; + int64_t *mssec = (int64_t *) val; + int64_t cur_msg = k_uptime_get(); + + if (reset) { + *mssec = cur_msg; + return 0; + } + + delta = cur_msg - *mssec; + + return (unsigned long )delta; +} + +/** + * Set delays to watch, final and intermediate delays. + * + * @param data Timing delay context. + * @param int_ms Intermediate delay in milliseconds. + * @param fin_ms Final delay in milliseconds. + * + */ +static void auth_tls_timing_set_delay(void *data, uint32_t int_ms, uint32_t fin_ms) +{ + mbedtls_timing_delay_context *ctx = (mbedtls_timing_delay_context *) data; + + ctx->int_ms = int_ms; + ctx->fin_ms = fin_ms; + + if (fin_ms != 0) { + (void) auth_tls_timing_get_timer(&ctx->timer, 1); + } +} + +/** + * Determine the delay result. + * + * @param data Pointer to delay context. + * + * @return -1 if cancelled, 0 if none of the delays is expired, + * 1 if the intermediate delay only is expired, + * 2 if the final delay is expired + */ +static int auth_tls_timing_get_delay(void *data) +{ + mbedtls_timing_delay_context *ctx = (mbedtls_timing_delay_context *)data; + int64_t elapsed_ms; + + if (ctx->fin_ms == 0) { + return -1; + } + + elapsed_ms = auth_tls_timing_get_timer(&ctx->timer, 0); + + if (elapsed_ms >= (int64_t)ctx->fin_ms) { + return 2; + } + + if (elapsed_ms >= (int64_t)ctx->int_ms) { + return 1; + } + + return 0; +} + + +/** + * Function called by Mbed stack to print debug messages. + * + * @param ctx Context + * @param level Debug level + * @param file Source filename of debug log entry. + * @param line Line number of debug log entry. + * @param str Debug/Log message. + */ +static void auth_mbed_debug(void *ctx, int level, const char *file, + int line, const char *str) +{ + const char *p, *basename; + + /** + * @brief Need to define const string here vs. const char *fmt = "[%s:%d] %s" + * because the LOG_ERR(), LOG_* macros can't handle a pointer. + */ +#define LOG_FMT "[%s:%d] %s" + + (void)ctx; + + if (!file || !str) { + return; + } + + /* Extract basename from file */ + for (p = basename = file; *p != '\0'; p++) { + if (*p == '/' || *p == '\\') { + basename = p + 1; + } + } + + + switch (level) { + + case 0: + { + LOG_ERR(LOG_FMT, log_strdup(basename), line, log_strdup(str)); + break; + } + + case 1: + { + LOG_WRN(LOG_FMT, log_strdup(basename), line, log_strdup(str)); + break; + } + + case 2: + { + LOG_INF(LOG_FMT, log_strdup(basename), line, log_strdup(str)); + break; + } + + case 3: + default: + { + LOG_DBG(LOG_FMT, log_strdup(basename), line, log_strdup(str)); + break; + } + } +} + + + +/** + * Mbed routine to send data, called by Mbed TLS library. + * + * @param ctx Context pointer, pointer to struct authenticate_conn + * @param buf Buffer to send. + * @param len Number of bytes to send. + * + * @return Number of bytes sent, else Mbed tls error. + */ +static int auth_mbedtls_tx(void *ctx, const uint8_t *buf, size_t len) +{ + int send_cnt; + struct authenticate_conn *auth_conn = (struct authenticate_conn *)ctx; + + if (auth_conn == NULL) { + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + + struct dtls_packet_hdr *dtls_hdr; + struct mbed_tls_context *mbedctx = (struct mbed_tls_context *)auth_conn->internal_obj; + + if (mbedctx == NULL) { + LOG_ERR("Missing Mbed context."); + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + + dtls_hdr = (struct dtls_packet_hdr *)mbedctx->temp_dtlsbuf; + + /** + * DTLS is targeted for the UDP datagram protocol, as such the Mbed stack + * expects a full DTLS packet (ie datagram) to be receive vs. a partial + * packet. When sending, add a header to enable the receiving side + * to determine when a full DTLS packet has been recevid. + */ + + /* Check the temp buffer is large enough */ + if ((sizeof(mbedctx->temp_dtlsbuf) - DTLS_HEADER_BYTES) < len) { + return MBEDTLS_ERR_SSL_BUFFER_TOO_SMALL; + } + + /* set byte order to Big Endian when sending over lower transport. */ + dtls_hdr->sync_bytes = sys_cpu_to_be16(DTLS_PACKET_SYNC_BYTES); + + /* does not include header */ + dtls_hdr->packet_len = sys_cpu_to_be16((uint16_t)len); + + /* Combine the header with the payload. This maximizes the lower + * transport throughput vs. sending the DTLS header separately then + * sending the body. */ + memcpy(&mbedctx->temp_dtlsbuf[DTLS_HEADER_BYTES], buf, len); + + /* send to peripheral */ + send_cnt = auth_xport_send(auth_conn->xport_hdl, mbedctx->temp_dtlsbuf, + len + DTLS_HEADER_BYTES); + + + if (send_cnt < 0) { + LOG_ERR("Failed to send, err: %d", send_cnt); + return MBEDTLS_ERR_NET_SEND_FAILED; + } + + LOG_INF("Send %d byes.", send_cnt); + + /* return number bytes sent, do not include the DTLS header */ + send_cnt -= DTLS_HEADER_BYTES; + + return send_cnt; +} + +/** + * Mbed receive function, called by Mbed code. + * + * @param ctx Context. + * @param buffer Buffer to copy received bytes into. + * @param len Buffer byte size. + * + * @return Number of bytes copied or negative number on error. + */ +static int auth_mbedtls_rx(void *ctx, uint8_t *buffer, size_t len) +{ + struct authenticate_conn *auth_conn = (struct authenticate_conn *)ctx; + int rx_bytes = 0; + struct dtls_packet_hdr dtls_hdr; + uint16_t packet_len = 0; + + /** + * DTLS is targeted for the UDP datagram protocol, as such the Mbed stack + * expects a full DTLS packet (ie datagram) to be receive vs. a partial + * packet. For the lower transports, a full datagram packet maybe broken + * up into multiple fragments. The receive queue may contain a partial + * DTLS frame. The code here waits until a full DTLS packet is received. + */ + + /* Will wait until a full DTLS packet is received. */ + while (true) { + + rx_bytes = auth_xport_getnum_recvqueue_bytes_wait(auth_conn->xport_hdl, 1000u); + + /* Check for canceled flag */ + if (auth_conn->cancel_auth) { + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + + /* no bytes or timed out */ + if (rx_bytes == 0 || rx_bytes == -EAGAIN) { + continue; + } + + /* an error */ + if (rx_bytes < 0) { + /* an error occurred */ + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + + if (rx_bytes < DTLS_HEADER_BYTES) { + continue; + } + + /* peek into receive queue */ + auth_xport_recv_peek(auth_conn->xport_hdl, (uint8_t *)&dtls_hdr, + sizeof(struct dtls_packet_hdr)); + + /* check for sync bytes */ + if (sys_be16_to_cpu(dtls_hdr.sync_bytes) != DTLS_PACKET_SYNC_BYTES) { + // read bytes and try to peek again + auth_xport_recv(auth_conn->xport_hdl, (uint8_t *)&dtls_hdr, + sizeof(struct dtls_packet_hdr), 1000u); + continue; + } + + // have valid DTLS packet header, check packet length + dtls_hdr.packet_len = sys_be16_to_cpu(dtls_hdr.packet_len); + + /* Is there enough room to copy into Mbedtls buffer? */ + if (dtls_hdr.packet_len > len) { + return MBEDTLS_ERR_SSL_BUFFER_TOO_SMALL; + } + + /* Zero length packet, ignore */ + if (dtls_hdr.packet_len == 0u) { + LOG_ERR("Empty DTLS packet."); + /* Read the DTLS header and return */ + auth_xport_recv(auth_conn->xport_hdl, (uint8_t *)&dtls_hdr, + sizeof(struct dtls_packet_hdr), 1000u); + return 0; + } + + /* rx_bytes must be at least DTLS_HEADER_BYTES in length here. Enough + * to fill a complete DTLS packet. */ + if ((int)dtls_hdr.packet_len <= (rx_bytes - (int)DTLS_HEADER_BYTES)) { + + packet_len = dtls_hdr.packet_len; + + /* copy packet into mbed buffers */ + /* read header, do not forward to Mbed */ + auth_xport_recv(auth_conn->xport_hdl, (uint8_t *)&dtls_hdr, + sizeof(struct dtls_packet_hdr), 1000u); + + /* read packet into mbed buffer*/ + rx_bytes = auth_xport_recv(auth_conn->xport_hdl, buffer, + packet_len, 1000u); + + if (rx_bytes <= 0) { + /* an error occurred */ + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + + len -= rx_bytes; + buffer += rx_bytes; + + /* we're done with one DTLS packet, return */ + return rx_bytes; + } + + /** + * If we're here it means we have a partial DTLS packet, + * wait for more data until there is enough to fill a + * complete DTLS packet. + */ + LOG_DBG("Waiting for more bytes to fill DTLS packet."); + } + + /* Didn't send any bytes */ + return 0; +} + + +/** + * Set the DTLS cookie. + * + * @param auth_conn Pointer to auth connection + * + * @return 0 on success, else error code. + */ +static int auth_tls_set_cookie(struct authenticate_conn *auth_conn) +{ + int ret; + struct mbed_tls_context *mbed_ctx = (struct mbed_tls_context *)auth_conn->internal_obj; + + /* should not be NULL!! */ + if (!mbed_ctx) { + LOG_ERR("No MBED context."); + return AUTH_ERROR_INVALID_PARAM; + } + + ret = mbedtls_ssl_set_client_transport_id(&mbed_ctx->ssl, mbed_ctx->cookie, + sizeof(mbed_ctx->cookie)); + + return ret; +} + +/** + * Pseudo random source for Mbed + * + * @param data Optional entropy source. + * @param output Buffer to receive random numbers. + * @param len Number of bytes requested. + * @param olen Number of bytes actually returned. + * + * @return Always zero. + */ +static int auth_tls_entropy(void *data, unsigned char *output, size_t len, + size_t *olen) +{ + (void) data; + + sys_rand_get(output, len); + *olen = len; + + return 0; +} + + + +/* ================= external/internal funcs ==================== */ +/** + * @see auth_internal.h + * + */ +int auth_init_dtls_method(struct authenticate_conn *auth_conn, struct auth_dtls_certs *certs) +{ + struct mbed_tls_context *mbed_ctx; + int ret; + + LOG_DBG("Initializing Mbed DTLS"); + + /* get, init, and set context pointer */ + mbed_ctx = auth_get_mbedcontext(); + + if (mbed_ctx == NULL) { + LOG_ERR("Unable to allocate Mbed TLS context."); + return AUTH_ERROR_NO_RESOURCE; + } + + /* Init mbed context */ + auth_init_context(mbed_ctx); + + + /* Save MBED tls context as internal object. The intent of using a void + * 'internal_obj' is to provide a var in the struct authentication_conn to + * store different authentication methods. Instead of Mbed, it maybe a + * Challenge-Response.*/ + auth_conn->internal_obj = mbed_ctx; + + int endpoint = auth_conn->is_client ? MBEDTLS_SSL_IS_CLIENT : MBEDTLS_SSL_IS_SERVER; + + mbedtls_ssl_config_defaults(&mbed_ctx->conf, + endpoint, + MBEDTLS_SSL_TRANSPORT_DATAGRAM, + MBEDTLS_SSL_PRESET_DEFAULT); + + + /* set the lower layer transport functions */ + mbedtls_ssl_set_bio(&mbed_ctx->ssl, auth_conn, auth_mbedtls_tx, auth_mbedtls_rx, NULL); + + /* set max record len to 512, as small as possible */ + mbedtls_ssl_conf_max_frag_len(&mbed_ctx->conf, MBEDTLS_SSL_MAX_FRAG_LEN_512); + + /* Set the DTLS time out */ + mbedtls_ssl_conf_handshake_timeout(&mbed_ctx->conf, AUTH_DTLS_MIN_TIMEOUT, + ATUH_DTLS_MAX_TIMEOUT); + + /* Force verification. */ + mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); + + + if ((certs->device_cert_pem.priv_key == NULL) || (certs->device_cert_pem.priv_key_size == 0)) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to get device private key"); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + + ret = mbedtls_pk_parse_key(&mbed_ctx->device_private_key, certs->device_cert_pem.priv_key, + certs->device_cert_pem.priv_key_size, NULL, 0); + + if (ret) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to parse device private key, error: 0x%x", ret); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + /* Setup certs, the CA chain followed by the end device cert. */ + if ((certs->server_ca_chain_pem.cert == NULL) || (certs->server_ca_chain_pem.cert_size == 0)) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to get CA cert chain"); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + /* Parse and set the CA certs */ + ret = mbedtls_x509_crt_parse(&mbed_ctx->cacert, certs->server_ca_chain_pem.cert, + certs->server_ca_chain_pem.cert_size); + + if (ret) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to parse CA cert, error: 0x%x", ret); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + /* set CA certs into context */ + mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->cacert, NULL); + + /* Get and parse the device cert */ + if ((certs->device_cert_pem.cert == NULL) || (certs->device_cert_pem.cert_size == 0)) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to get device cert"); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + ret = mbedtls_x509_crt_parse(&mbed_ctx->device_cert, (const unsigned char *)certs->device_cert_pem.cert, + certs->device_cert_pem.cert_size); + + if (ret) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to parse device cert, error: 0x%x", ret); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + /* Parse and set the device cert */ + ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->device_cert, + &mbed_ctx->device_private_key); + + if (ret) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to set device cert and key, error: 0x%x", ret); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + /* set entropy source */ + mbedtls_entropy_add_source(&mbed_ctx->entropy, auth_tls_entropy, NULL, + 32, MBEDTLS_ENTROPY_SOURCE_STRONG); + + mbedtls_ctr_drbg_seed(&mbed_ctx->ctr_drbg, mbedtls_entropy_func, + &mbed_ctx->entropy, NULL, 0); + + /* setup call to Zephyr random API */ + mbedtls_ssl_conf_rng(&mbed_ctx->conf, mbedtls_ctr_drbg_random, &mbed_ctx->ctr_drbg); + + mbedtls_ssl_conf_dbg(&mbed_ctx->conf, auth_mbed_debug, auth_conn); + +#if defined(MBEDTLS_DEBUG_C) + mbedtls_debug_set_threshold(CONFIG_MBEDTLS_DEBUG_LEVEL); +#endif + + + if (!auth_conn->is_client) { + + auth_tls_set_cookie(auth_conn); + + ret = mbedtls_ssl_cookie_setup(&mbed_ctx->cookie_ctx, + mbedtls_ctr_drbg_random, + &mbed_ctx->ctr_drbg); + + if (ret) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("Failed to setup dtls cookies, error: 0x%x", ret); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + mbedtls_ssl_conf_dtls_cookies(&mbed_ctx->conf, + mbedtls_ssl_cookie_write, + mbedtls_ssl_cookie_check, + &mbed_ctx->cookie_ctx); + } + + ret = mbedtls_ssl_setup(&mbed_ctx->ssl, &mbed_ctx->conf); + + if (ret) { + auth_free_mbedcontext(mbed_ctx); + LOG_ERR("mbedtls_ssl_setup returned %d", ret); + return AUTH_ERROR_DTLS_INIT_FAILED; + } + + /* Setup timers */ + mbedtls_ssl_set_timer_cb(&mbed_ctx->ssl, &mbed_ctx->timer, + auth_tls_timing_set_delay, + auth_tls_timing_get_delay); + + return AUTH_SUCCESS; +} + + +/** + * If performing a DTLS handshake + * + * @param auth_conn The auth connection/instance. + * + */ +void auth_dtls_thead(struct authenticate_conn *auth_conn) +{ + char err_buf[MBED_ERROR_BUFLEN]; + int bytecount = 0; + struct mbed_tls_context *mbed_ctx = (struct mbed_tls_context *) auth_conn->internal_obj; + + /* Set status */ + auth_lib_set_status(auth_conn, AUTH_STATUS_STARTED); + + /** + * For the server we can noty start the handshake, the code will continue + * to read looking for a "Client Hello". So we'll just stay at the + * MBEDTLS_SSL_CLIENT_HELLO state until the central sends the "Client Hello" + * + * For the client, a client hello will be sent immediately. + */ + if (!auth_conn->is_client) { + + /** + * For the the DTLS server, use the auth connection handle as the cookie. + */ + int ret = auth_tls_set_cookie(auth_conn); + + if (ret) { + LOG_ERR("Failed to get connection info for DTLS cookie, auth failed, error: 0x%x", ret); + auth_lib_set_status(auth_conn, AUTH_STATUS_FAILED); + return; + } + + /* Sit in a loop waiting for the initial Client Hello message + * from the client. */ + while (bytecount == 0) { + + if (auth_conn->cancel_auth) { + LOG_INF("DTLS authentication canceled."); + return; + } + + /* Server, wait for client hello */ + bytecount = auth_xport_getnum_recvqueue_bytes_wait(auth_conn->xport_hdl, + AUTH_DTLS_HELLO_WAIT_MSEC); + + if (bytecount == -EAGAIN) { + /* simply timed out waiting for client hello, try again */ + LOG_INF("Timeout waiting for Client Hello"); + continue; + } + + if (bytecount < 0) { + LOG_ERR("Server, error when waiting for client hello, error: %d", bytecount); + auth_lib_set_status(auth_conn, AUTH_STATUS_FAILED); + return; + } + } + + LOG_INF("Server received initial Client Hello from client."); + } + + /* Set status */ + auth_lib_set_status(auth_conn, AUTH_STATUS_IN_PROCESS); + + int ret = 0; + + /* start handshake */ + do { + + while (mbed_ctx->ssl.state != MBEDTLS_SSL_HANDSHAKE_OVER && + !auth_conn->cancel_auth) { + + LOG_INF("Handshake state: %s", auth_tls_handshake_state(mbed_ctx->ssl.state)); + + /* do handshake step */ + ret = mbedtls_ssl_handshake_step(&mbed_ctx->ssl); + + if (ret != 0) { + break; + } + } + + + if (auth_conn->cancel_auth) { + LOG_INF("Authentication canceled."); + break; + } + + if (ret == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED) { + + /* restart handshake to process client cookie */ + LOG_INF("Restarting handshake, need client cookie."); + + /* reset session and cookie info */ + mbedtls_ssl_session_reset(&mbed_ctx->ssl); + + ret = auth_tls_set_cookie(auth_conn); + + if (ret) { + LOG_ERR("Failed to reset cookie information, error: 0x%x", ret); + ret = MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } else { + ret = MBEDTLS_ERR_SSL_WANT_READ; + } + + } + + if (auth_conn->cancel_auth) { + LOG_INF("Authentication canceled."); + break; + } + + } while (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE); + + + if (mbed_ctx->ssl.state == MBEDTLS_SSL_HANDSHAKE_OVER) { + LOG_INF("DTLS Handshake success."); + ret = AUTH_SUCCESS; + } else { + /* All of the MBed error are of the form -0xXXXX. To display + * correctly negate the error value, thus -ret */ + LOG_ERR("DTLS Handshake failed, error: -0x%x", -ret); + mbedtls_strerror(ret, err_buf, sizeof(err_buf)); + LOG_ERR("%s", log_strdup(err_buf)); + } + + + enum auth_status auth_status; + + switch (ret) { + case MBEDTLS_ERR_SSL_BAD_HS_CERTIFICATE: + case MBEDTLS_ERR_SSL_BAD_HS_CERTIFICATE_VERIFY: + auth_status = AUTH_STATUS_AUTHENTICATION_FAILED; + break; + + case AUTH_SUCCESS: + auth_status = AUTH_STATUS_SUCCESSFUL; + break; + + default: + auth_status = AUTH_STATUS_FAILED; + break; + } + + /* now check if cancel occurred */ + if (auth_conn->cancel_auth) { + auth_status = AUTH_STATUS_CANCELED; + } + + /* Call status */ + auth_lib_set_status(auth_conn, auth_status); + + return; +} + + + diff --git a/lib/auth/auth_internal.h b/lib/auth/auth_internal.h new file mode 100644 index 000000000000..bb03cd0b7d9a --- /dev/null +++ b/lib/auth/auth_internal.h @@ -0,0 +1,295 @@ +/** + * @file auth_internal.h + * + * @brief Internal functions used by the authentication library. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#ifndef ZEPHYR_INCLUDE_AUTH_INTERNAL_H_ +#define ZEPHYR_INCLUDE_AUTH_INTERNAL_H_ + +/** + * Thread params + */ +struct auth_thread_params { + struct authenticate_conn *auth_conn; + struct k_sem *thrd_sem; +}; + + + +/** + * Defines to handle message fragmentation over the different transports. + */ + +/* should be at large as the largest msg. */ +#define XPORT_MAX_MESSAGE_SIZE (650u) + +/** + * A message is broken up into multiple fragments. Each fragement has + * sync bytes, flags, and fragment length. + */ +#define XPORT_FRAG_SYNC_BYTE_HIGH (0xA5) /* bits 15-4 sync byte */ +#define XPORT_FRAG_SYNC_BYTE_LOW (0x90) +#define XPORT_FRAG_LOWBYTE_MASK (0xF0) /* bits 3-0 for flags */ + +#define XPORT_FRAG_SYNC_BITS ((XPORT_FRAG_SYNC_BYTE_HIGH << 8u) | \ + XPORT_FRAG_SYNC_BYTE_LOW) +#define XPORT_FRAG_SYNC_MASK (0xFFF0) + +/** + * Bitlfags used to indicate fragment order + */ +#define XPORT_FRAG_BEGIN (0x1) +#define XPORT_FRAG_NEXT (0x2) +#define XPORT_FRAG_END (0x4) +#define XPORT_FRAG_UNUSED (0x8) + +#define XPORT_FRAG_HDR_BYTECNT (sizeof(struct auth_message_frag_hdr)) +#define XPORT_MIN_FRAGMENT XPORT_FRAG_HDR_BYTECNT + + +#pragma pack(push, 1) +/** + * Fragment header + */ +struct auth_message_frag_hdr { + /* bits 15-4 are for fragment sync, bits 3-0 are flags */ + uint16_t sync_flags; /* bytes to insure we're at a fragment */ + uint16_t payload_len; /* number of bytes in the payload, does not + * include the header. */ +}; + +/** + * One fragment, one or more fragments make up a message. + */ +struct auth_message_fragment { + struct auth_message_frag_hdr hdr; + uint8_t frag_payload[XPORT_MAX_MESSAGE_SIZE]; +}; +#pragma pack(pop) + + + +/** + * Starts the authentication thread. + * + * @param auth_conn Pointer to Authentication connection struct. + * + * @return 0 on success else negative error number. + */ +int auth_start_thread(struct authenticate_conn *auth_conn); + + +/** + * Set the authentication status. + * + * @param auth_conn Authentication connection struct. + * @param status Authentication status. + */ +void auth_lib_set_status(struct authenticate_conn *auth_conn, enum auth_status status); + + +/** + * Initializes DTLS authentication method. + * + * @param auth_conn Pointer to Authentication connection struct. + * @param certs Pointer to certs and associated private keys. + * + * @return 0 on success else one of AUTH_ERROR_* values. + */ +int auth_init_dtls_method(struct authenticate_conn *auth_conn, + struct auth_dtls_certs *certs); + + +/** + * Initialize Challenge-Response method with additional parameters. + * + * @param auth_conn Pointer to Authentication connection struct. + * @param chal_resp Pointer to Challenge-Response params. + * + * @return 0 on success else one of AUTH_ERROR_* values. + */ +int auth_init_chalresp_method(struct authenticate_conn *auth_conn, + struct auth_challenge_resp *chal_resp); + + +/** + * Used by the client to send data bytes to the Peripheral. + * If necessary, will break up data into several sends depending + * on the MTU size. + * + * @param auth_conn Pointer to Authentication connection struct. + * @param buf Buffer to send. + * @param len Buffer size. + * + * @return Number of bytes sent or negative error value. + */ +int auth_client_tx(struct authenticate_conn *auth_conn, const unsigned char *buf, + size_t len); + +/** + * Used by the client to read data from the receive buffer. Will not + * block, if no bytes are available from the Peripheral returns 0. + * + * @param auth_conn Pointer to Authentication connection struct. + * @param buf Buffer to copy byes into. + * @param len Buffer length. + * + * @return Number of bytes returned, 0 if no bytes returned, or negative if + * an error occurred. + */ +int auth_client_recv(struct authenticate_conn *auth_conn, unsigned char *buf, + size_t len); + +/** + * Used by the Central to receive data from the Peripheral. Will block until data is + * received or a timeout has occurred. + * + * @param auth_conn Pointer to Authentication connection struct. + * @param buf Buffer to copy byes into. + * @param len Buffer length. + * @param timeout Wait time in msecs for data, K_FOREVER or K_NO_WAIT. + * + * @return Number of bytes returned, 0 if no bytes returned, or negative if + * an error occurred. + */ +int auth_client_recv_timeout(struct authenticate_conn *auth_conn, unsigned char *buf, + size_t len, uint32_t timeout); + +/** + * Used by the server to send data to the Central. Will break up buffer to max MTU + * sizes if necessary and send multiple PDUs. Uses Write Indications to get + * acknowledgement from the Central before sending additional packet. + * + * @param auth_conn Pointer to Authentication connection struct. + * @param buf Data to send + * @param len Data length. + * + * @return Number of bytes send, or negative if an error occurred. + */ +int auth_server_tx(struct authenticate_conn *auth_conn, const unsigned char *buf, + size_t len); + +/** + * Used by the server to read data from the receive buffer. Non-blocking. + * + * @param auth_conn Context, pointer to Authentication connection struct. + * @param buf Buffer to copy bytes into. + * @param len Number of bytes requested. + * + * @return Number of bytes returned, 0 if no bytes, of negative if an error occurred. + */ +int auth_server_recv(struct authenticate_conn *auth_conn, unsigned char *buf, + size_t len); + +/** + * Used by the server to read data from the receive buffer. Blocking call. + * + * @param auth_conn Context, pointer to Authentication connection struct. + * @param buf Buffer to copy bytes into. + * @param len Number of bytes requested. + * @param timeout Wait time in msecs for data, K_FOREVER or K_NO_WAIT. + * + * @return Number of bytes returned, or -EAGAIN if timed out. + */ +int auth_server_recv_timeout(struct authenticate_conn *auth_conn, unsigned char *buf, + size_t len, uint32_t timeout); + + +/** + * Used by the client to send data to Peripheral. + * + * @param conn Pointer to Authentication connection struct. + * @param data Data to send. + * @param len Byte length of data. + * + * @return Number of bytes sent, negative number on error. + */ +int auth_client_tx(struct authenticate_conn *conn, const unsigned char *data, + size_t len); + +/** + * Used by the client to receive data to Peripheral. + * + * @param conn Pointer to Authentication connection struct. + * @param buf Buffer to copy received bytes into. + * @param rxbytes Number of bytes requested. + * + * @return Number of bytes copied into the buffer. On error, negative error number. + */ +int auth_client_rx(struct authenticate_conn *conn, uint8_t *buf, size_t rxbytes); + +/** + * Used by server to send data to the client. + * + * @param conn Pointer to Authentication connection struct. + * @param data Data to send. + * @param len Byte length of data. + * + * @return Number of bytes sent, negative number on error. + */ +int auth_server_tx(struct authenticate_conn *conn, const unsigned char *data, + size_t len); + +/** + * Used by server to receive data. + * + * @param conn Pointer to Authentication connection struct. + * @param buf Buffer to copy received bytes into. + * @param len Number of bytes requested. + * + * @return Number of bytes copied (received) into the buffer. + * On error, negative error number. + */ +int auth_sever_rx(struct authenticate_conn *conn, uint8_t *buf, size_t len); + +/** + * Scans buffer to determine if a fragment is present. + * + * @param buffer Buffer to scan. + * @param buflen Buffer length. + * @param frag_beg_offset Offset from buffer begin where fragment starts. + * @param frag_byte_cnt Number of bytes in this fragment. + * + * @return true if full frame found, else false. + */ +bool auth_message_get_fragment(const uint8_t *buffer, uint16_t buflen, + uint16_t *frag_beg_offset, uint16_t *frag_byte_cnt); + +/** + * Used by lower transport to put received bytes into recv queue. Handle framing and + * puts full message into receive queue. Handles reassembly of message fragments. + * + * @param xporthdl Transport handle. + * @param buff Pointer to one frame. + * @param buflen Number of bytes in frame + * + * @return The number of bytes queued, can be less than requested. + * On error, negative value is returned. + */ +int auth_message_assemble(const auth_xport_hdl_t xporthdl, const uint8_t *buf, + size_t buflen); + +/** + * Swap the fragment header from Big Endian to the processor's byte + * ordering. + * + * @param frag_hdr Pointer to message fragment header. + */ +void auth_message_hdr_to_cpu(struct auth_message_frag_hdr *frag_hdr); + + +/** + * Swaps the fragment header bytes to Big Endian order. + * + * @param frag_hdr Pointer to message fragment header. + */ +void auth_message_hdr_to_be16(struct auth_message_frag_hdr *frag_hdr); + + + + +#endif /* ZEPHYR_INCLUDE_AUTH_INTERNAL_H_ */ \ No newline at end of file diff --git a/lib/auth/auth_lib.c b/lib/auth/auth_lib.c new file mode 100644 index 000000000000..0a0b880eb41e --- /dev/null +++ b/lib/auth/auth_lib.c @@ -0,0 +1,342 @@ +/** + * @file auth_lib.c + * + * @brief Authentication Library functions used to authenticate a + * connection between a client and server. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +LOG_MODULE_REGISTER(auth_lib, CONFIG_AUTH_LOG_LEVEL); + +#include +#include "auth_internal.h" + + +#define AUTH_THRD_STACK_SIZE (4096u) +#define AUTH_THRD_PRIORITY CONFIG_AUTH_THREAD_PRIORITY + + + +#if defined(AUTH_INSTANCE_1) +K_SEM_DEFINE(thrd_sem_1, 0, 1); +#endif + +#if defined(AUTH_INSTANCE_2) +K_SEM_DEFINE(thrd_sem_2, 0, 1); +#endif + +static void auth_thrd_entry(void *, void *, void *); + +static struct auth_thread_params thrd_params[CONFIG_NUM_AUTH_INSTANCES] = +{ +#if defined(AUTH_INSTANCE_1) + { .thrd_sem = &thrd_sem_1 }, +#endif + +#if defined(AUTH_INSTANCE_2) + { .thrd_sem = &thrd_sem_2 }, +#endif +}; + +#if defined(AUTH_INSTANCE_1) +K_THREAD_DEFINE(auth_tid_1, AUTH_THRD_STACK_SIZE, + auth_thrd_entry, &thrd_params[AUTH_INST_1_ID], NULL, NULL, + AUTH_THRD_PRIORITY, 0, 0); +#endif + + +#if defined(AUTH_INSTANCE_2) +K_THREAD_DEFINE(auth_tid_2, AUTH_THRD_STACK_SIZE, + auth_thrd_entry, &thrd_params[AUTH_INST_2_ID], NULL, NULL, + AUTH_THRD_PRIORITY, 0, 0); +#endif + + +/** + * Forward function declarations for authentication threads. + */ +void auth_dtls_thead(struct authenticate_conn *auth_conn); +void auth_chalresp_thread(struct authenticate_conn *auth_conn); + + +/* ========================== local functions ========================= */ + +/** + * Check auth flags consistency. + * + * @param flags Flags to check. + * + * @return true if flags are correct, else false. + */ +static bool auth_lib_checkflags(uint32_t flags) +{ + /* Check for invalid flag combinations */ + + /* server and client roles are mutually exclusive */ + if ((flags & (AUTH_CONN_SERVER | AUTH_CONN_CLIENT)) == + (AUTH_CONN_SERVER | AUTH_CONN_CLIENT)) { + return false; + } + + /* can only define one auth method */ + if ((flags & (AUTH_CONN_DTLS_AUTH_METHOD | AUTH_CONN_CHALLENGE_AUTH_METHOD)) + == (AUTH_CONN_DTLS_AUTH_METHOD | AUTH_CONN_CHALLENGE_AUTH_METHOD)) { + return false; + } + + return true; +} + + +/** + * Invokes the status callback from the system work queue + * + * @param work Pointer to work item. + */ +static void auth_lib_status_work(struct k_work *work) +{ + struct authenticate_conn *auth_conn = + CONTAINER_OF(work, struct authenticate_conn, auth_status_work); + + if (!auth_conn) { + LOG_ERR("Failed to get auth conn struct."); + return; + } + + /* invoke callback */ + auth_conn->status_cb(auth_conn, auth_conn->instance, auth_conn->curr_status, + auth_conn->callback_context); +} + + +/** + * Auth thread starts during boot and waits on semaphore. + * + * @param arg1 Pointer to struct auth_thread_params. + * @param arg2 Unused + * @param arg3 Unused + */ +static void auth_thrd_entry(void *arg1, void *arg2, void *arg3) +{ + int ret; + struct auth_thread_params *thrd_params = (struct auth_thread_params *)arg1; + + while (true) { + + ret = k_sem_take(thrd_params->thrd_sem, K_FOREVER); + + if (ret) { + LOG_ERR("Failed to get semaphore for auth thread, err: %d", ret); + LOG_ERR("Auth thread terminating, instance id: %d.", + thrd_params->auth_conn->instance); + return; + } + + /* call auth thread function */ + thrd_params->auth_conn->auth_func(thrd_params->auth_conn); + } +} + + + +/* ========================= external API ============================ */ + + +/** + * @see auth_lib.h + */ +int auth_lib_init(struct authenticate_conn *auth_conn, enum auth_instance_id instance, + auth_status_cb_t status_func, void *context, struct auth_optional_param *opt_params, + enum auth_flags auth_flags) +{ + int err = 0; + + /* check input params */ + if (status_func == NULL) { + LOG_ERR("Error, status function is NULL."); + return AUTH_ERROR_INVALID_PARAM; + } + + /* check auth flags */ + if (!auth_lib_checkflags(auth_flags)) { + LOG_ERR("Invalid auth flags."); + return AUTH_ERROR_INVALID_PARAM; + } + + /* init the struct to zero */ + memset(auth_conn, 0, sizeof(struct authenticate_conn)); + + /* setup the status callback */ + auth_conn->status_cb = status_func; + auth_conn->callback_context = context; + + auth_conn->cancel_auth = false; + auth_conn->instance = instance; + + /* init the work item used to post authentication status */ + k_work_init(&auth_conn->auth_status_work, auth_lib_status_work); + + auth_conn->is_client = (auth_flags & AUTH_CONN_CLIENT) ? true : false; + +#if defined(CONFIG_AUTH_DTLS) + + if (auth_flags & AUTH_CONN_DTLS_AUTH_METHOD) { + /* Set the DTLS authentication thread */ + auth_conn->auth_func = auth_dtls_thead; + { + if (opt_params == NULL || opt_params->param_id != AUTH_DTLS_PARAM) { + LOG_ERR("Missing certificates for TLS/DTLS authentication."); + return AUTH_ERROR_INVALID_PARAM; + } + + struct auth_dtls_certs *certs = &opt_params->param_body.dtls_certs; + + // init TLS layer + err = auth_init_dtls_method(auth_conn, certs); + } + + if (err) { + LOG_ERR("Failed to initialize MBed TLS, err: %d", err); + return err; + } + } +#endif + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) + + if (auth_flags & AUTH_CONN_CHALLENGE_AUTH_METHOD) { + /* Set the Challenge-Response authentication thread */ + auth_conn->auth_func = auth_chalresp_thread; + + if ((opt_params != NULL) && (opt_params->param_id == AUTH_CHALRESP_PARAM)) { + + struct auth_challenge_resp *chal_resp = &opt_params->param_body.chal_resp; + + err = auth_init_chalresp_method(auth_conn, chal_resp); + + if (err) { + LOG_ERR("Failed to set Challege-Response param, err: %d", err); + return err; + } + } + } +#endif + + /* + * Set auth connect into thread param instance. + * + * @note: k_sem_give() is called to start the thread, which provides + * a compile_barrier() and for SMP systems should handle any + * necessary memory barriers and/or cache syncing. + * The important point is the auth_conn pointer is set into + * a structure used by another thread (auth_thrd_entry) + */ + thrd_params[instance].auth_conn = auth_conn; + + return AUTH_SUCCESS; +} + +/** + * @see auth_lib.h + */ +int auth_lib_deinit(struct authenticate_conn *auth_conn) +{ + /* Free any resources, nothing for now, but maybe + * needed in the future */ + return AUTH_SUCCESS; +} + +/** + * @see auth_lib.h + */ +int auth_lib_start(struct authenticate_conn *auth_conn) +{ + /* Start the authentication thread */ + k_sem_give(thrd_params[auth_conn->instance].thrd_sem); + + return AUTH_SUCCESS; +} + +/** + * @see auth_lib.h + */ +int auth_lib_cancel(struct authenticate_conn *auth_conn) +{ + auth_conn->cancel_auth = true; + + auth_lib_set_status(auth_conn, AUTH_STATUS_CANCELED); + + return AUTH_SUCCESS; +} + +/** + * @see auth_lib.h + */ +const char *auth_lib_getstatus_str(enum auth_status status) +{ + switch (status) { + case AUTH_STATUS_STARTED: + return "Authentication started"; + break; + + case AUTH_STATUS_IN_PROCESS: + return "In process"; + break; + + case AUTH_STATUS_CANCELED: + return "Canceled"; + break; + + case AUTH_STATUS_FAILED: + return "Failure"; + break; + + case AUTH_STATUS_AUTHENTICATION_FAILED: + return "Authentication Failed"; + break; + + case AUTH_STATUS_SUCCESSFUL: + return "Authentication Successful"; + break; + + default: + break; + } + + return "unknown"; +} + +/** + * @see auth_lib.h + */ +enum auth_status auth_lib_get_status(struct authenticate_conn *auth_conn) +{ + return auth_conn->curr_status; +} + +/** + * @see auth_lib.h + */ +void auth_lib_set_status(struct authenticate_conn *auth_conn, enum auth_status status) +{ + auth_conn->curr_status = status; + + if (auth_conn->status_cb) { + + /* submit work item */ + k_work_submit(&auth_conn->auth_status_work); + } +} diff --git a/lib/auth/auth_xport_bt.c b/lib/auth/auth_xport_bt.c new file mode 100644 index 000000000000..daaff7b84f6c --- /dev/null +++ b/lib/auth/auth_xport_bt.c @@ -0,0 +1,537 @@ +/** + * @file auth_xport_bt.c + * + * @brief Bluetooth transport layer. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "auth_internal.h" + + +#define LOG_LEVEL CONFIG_AUTH_LOGLEVEL +#include +LOG_MODULE_REGISTER(auth_bt_xport, CONFIG_AUTH_LOG_LEVEL); + +#define AUTH_BT_WRITE_TIMEOUTMSEC (5000u) + +/* two bytes for header, not sure about extra byte */ +#define BT_LINK_HEADER_BYTES (2u + 1u) + +/** + * Maps transport handle to a BT connection along with + * additional info used to manage BT I/O. + */ +struct auth_xport_connection_map { + + /* Opaque transport handle */ + auth_xport_hdl_t xporthdl; + + /* details for bt connection */ + bool is_central; + + /* BT connection */ + struct bt_conn *conn; + + /* Peripheral (server) characteristic value handle. Used by the + * Central (client) to send data. */ + uint16_t server_char_hdl; + + /* The Central (client) attribute, used by the Peripheral (server) + * to send data to the Central. + */ + const struct bt_gatt_attr *client_attr; + + /* Used to wait for central write completion before + * sending more data */ + struct k_sem auth_central_write_sem; + + volatile uint8_t write_att_err; + + /* Semaphore used when processing peripheral indications */ + struct k_sem auth_indicate_sem; + uint32_t indicate_err; + + uint16_t payload_size; /* BT Link MTU less struct bt_att_write_req */ +}; + + +static struct auth_xport_connection_map bt_conn_map[CONFIG_BT_MAX_CONN]; + + +/** + * Forward declarations + */ +#if defined(CONFIG_BT_GATT_CLIENT) +static int auth_xp_bt_central_tx(struct auth_xport_connection_map *bt_xp_conn, + const unsigned char *buf, size_t len); +#else +static int auth_xp_bt_peripheral_tx(struct auth_xport_connection_map *bt_xp_conn, + const unsigned char *buf, size_t len); +#endif + +/** + * Given a BT connection, return the xport connection info. + * + * @param conn Bluetooth Connection struct. + * + * @return Pointer to connection map for the given BT connection struct. + */ +static struct auth_xport_connection_map *auth_xp_bt_getconn(struct bt_conn *conn) +{ + uint8_t index = bt_conn_index(conn); + + if (index > CONFIG_BT_MAX_CONN) { + return NULL; + } + + return &bt_conn_map[index]; +} + + +#if defined(CONFIG_BT_GATT_CLIENT) +/** + * Called by the Central (Client) to send bytes to the peripheral (Server) + * + * @param xport_hdl Transport handle + * @param data Data to send. + * @param len Number of bytes to send. + * + * @return Total bytes sent on success, on error negative error value. + */ +static int auth_xp_bt_central_send(auth_xport_hdl_t xport_hdl, const uint8_t *data, + const size_t len) +{ + /* get the Xport BT connection info from the xport handle */ + struct auth_xport_connection_map *bt_xp_conn = auth_xport_get_context(xport_hdl); + + /* sanity check */ + if (bt_xp_conn == NULL) { + LOG_ERR("Missing bt transport connection context."); + return AUTH_ERROR_INTERNAL; + } + + int ret = auth_xp_bt_central_tx(bt_xp_conn, data, len); + + return ret; +} +#else +/** + * Sends data to the client via BT indication. + * + * @param xport_hdl Transport handle. + * @param data Data to send. + * @param len Number of bytes to send. + * + * @return Total byte send on success, negative value on error. + */ +static int auth_xp_bt_peripheral_send(auth_xport_hdl_t xport_hdl, const uint8_t *data, + const size_t len) +{ + /* get the Xport BT connection info from the xport handle */ + struct auth_xport_connection_map *bt_xp_conn = auth_xport_get_context(xport_hdl); + + /* sanity check */ + if (bt_xp_conn == NULL) { + LOG_ERR("Missing bt transport connection context."); + return AUTH_ERROR_INTERNAL; + } + + int ret = auth_xp_bt_peripheral_tx(bt_xp_conn, data, len); + + return ret; +} +#endif /* CONFIG_BT_GATT_CLIENT */ + + +/** + * @see auth_xport.h + */ +int auth_xp_bt_init(const auth_xport_hdl_t xport_hdl, uint32_t flags, void *xport_param) +{ + struct auth_xp_bt_params *bt_params = (struct auth_xp_bt_params *)xport_param; + struct auth_xport_connection_map *bt_xp_conn; + + if (bt_params == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + +#if defined(CONFIG_BT_GATT_CLIENT) + if (!bt_params->is_central) { + LOG_ERR("Invalid config, is_central is false for GATT client"); + return AUTH_ERROR_INVALID_PARAM; + } + + /* set direct call function */ + auth_xport_set_sendfunc(xport_hdl, auth_xp_bt_central_send); +#else + if (bt_params->is_central) { + LOG_ERR("Invalid config, is_central is set for GATT server"); + return AUTH_ERROR_INVALID_PARAM; + } + + /* set direct call function */ + auth_xport_set_sendfunc(xport_hdl, auth_xp_bt_peripheral_send); +#endif + + + bt_xp_conn = auth_xp_bt_getconn(bt_params->conn); + + if (bt_xp_conn == NULL) { + LOG_ERR("Failed to get transport map entry."); + return AUTH_ERROR_INTERNAL; + } + + /* Is this entry inuse? Check conn param */ + if (bt_xp_conn->conn != NULL) { + LOG_WRN("BT transport entry in use."); + } + + bt_xp_conn->is_central = bt_params->is_central; + bt_xp_conn->conn = bt_params->conn; + bt_xp_conn->xporthdl = xport_hdl; + bt_xp_conn->payload_size = 0; + + + if (bt_params->is_central) { + bt_xp_conn->server_char_hdl = bt_params->server_char_hdl; + + /* init central write semaphore */ + k_sem_init(&bt_xp_conn->auth_central_write_sem, 0, 1); + + bt_xp_conn->write_att_err = 0; + + } else { + + bt_xp_conn->client_attr = bt_params->client_attr; + + /* init peripheral indicate semaphore */ + k_sem_init(&bt_xp_conn->auth_indicate_sem, 0, 1); + bt_xp_conn->indicate_err = 0; + } + + /* Set the BT xport connection struct into the xport handle */ + auth_xport_set_context(xport_hdl, bt_xp_conn); + + return AUTH_SUCCESS; +} + +/** + * @see auth_xport.h + */ +int auth_xp_bt_deinit(const auth_xport_hdl_t xport_hdl) +{ + if (xport_hdl == NULL) { + LOG_ERR("Transport handle is NULL."); + return AUTH_ERROR_INVALID_PARAM; + } + + /* get xport context which is where the BT connection is saved */ + struct auth_xport_connection_map *bt_xp_conn = auth_xport_get_context(xport_hdl); + + if (bt_xp_conn == NULL) { + LOG_ERR("Missing BT connection"); + return AUTH_ERROR_INTERNAL; + } + + + /* clear out auth_xport_map entry */ + bt_xp_conn->xporthdl = NULL; + bt_xp_conn->is_central = false; + bt_xp_conn->conn = NULL; + bt_xp_conn->payload_size = 0; + bt_xp_conn->server_char_hdl = 0; + bt_xp_conn->client_attr = NULL; + + /* clear direct send function */ + auth_xport_set_sendfunc(xport_hdl, NULL); + auth_xport_set_context(xport_hdl, NULL); + + return AUTH_SUCCESS; +} + +/** + * @see auth_xport.h + */ +int auth_xp_bt_event(const auth_xport_hdl_t xporthdl, struct auth_xport_evt *event) +{ + /* stub for now */ + return AUTH_SUCCESS; +} + +/** + * @see auth_xport.h + */ +int auth_xp_bt_get_max_payload(const auth_xport_hdl_t xporthdl) +{ + struct auth_xport_connection_map *bt_xp_conn = auth_xport_get_context(xporthdl); + + return (int)(bt_gatt_get_mtu(bt_xp_conn->conn) - (uint16_t)BT_LINK_HEADER_BYTES); +} + +#if defined(CONFIG_BT_GATT_CLIENT) + +/** + * @see auth_xport.h + */ +uint8_t auth_xp_bt_central_notify(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) +{ + int err; + struct auth_xport_connection_map *bt_xp_conn = auth_xp_bt_getconn(conn); + + if (bt_xp_conn == NULL) { + LOG_ERR("Failed to get BT connection map."); + return BT_GATT_ITER_CONTINUE; + } + + /* This happens when the connection is dropped */ + if (length == 0) { + return BT_GATT_ITER_CONTINUE; + } + + /* put the received bytes into the receive queue */ + auth_message_hdr_to_cpu((struct auth_message_frag_hdr *)data); + err = auth_message_assemble(bt_xp_conn->xporthdl, data, length); + + if (length < 0) { + LOG_ERR("Failed to set all received bytes, err: %d", length); + } + + return BT_GATT_ITER_CONTINUE; +} + + +/** + * Called by the Bluetooth stack after sending BLE data. + * + * @param conn The Bluetooth connection. + * @param err ATT error code, 0 is success. + * @param params Pointer to write params used when calling bt_gatt_write() + */ +static void auth_xp_bt_central_write_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_write_params *params) +{ + struct auth_xport_connection_map *bt_xp_conn = auth_xp_bt_getconn(conn); + + if (err) { + LOG_ERR("gatt write failed, err: %d", err); + } else { + LOG_DBG("gatt write success, num bytes: %d", params->length); + } + + bt_xp_conn->write_att_err = err; + + k_sem_give(&bt_xp_conn->auth_central_write_sem); +} + + +/** + * Used by the central to write data to the peripheral. + * + * @param bt_xp_conn Bluetooth transport connection map. + * @param buf Data to send. + * @param len Number of bytes to send. + * + * @return Number of bytes sent, negative number on error. + */ +static int auth_xp_bt_central_tx(struct auth_xport_connection_map *bt_xp_conn, + const unsigned char *buf, size_t len) +{ + int err = 0; + uint16_t write_count; + int total_write_cnt = 0; + struct bt_gatt_write_params write_params; + + write_params.func = auth_xp_bt_central_write_cb; + write_params.handle = bt_xp_conn->server_char_hdl; + write_params.offset = 0; + + /* Set payload size if not set. This is necessary when the MTU + * size is negotiated after the BT connection has been established. */ + if (bt_xp_conn->payload_size == 0) { + bt_xp_conn->payload_size = auth_xp_bt_get_max_payload(bt_xp_conn->xporthdl); + } + + /* if necessary break up the write */ + while (len != 0) { + + write_count = MIN(bt_xp_conn->payload_size, len); + + write_params.data = buf; + write_params.length = write_count; + + err = bt_gatt_write(bt_xp_conn->conn, &write_params); + + if (err) { + LOG_ERR("Failed to write to peripheral, err: %d", err); + return err; + } + + /* wait on semaphore for write completion */ + err = k_sem_take(&bt_xp_conn->auth_central_write_sem, + K_MSEC(AUTH_BT_WRITE_TIMEOUTMSEC)); + + if (err) { + LOG_ERR("Failed to take semaphore, err: %d", err); + return err; + } + + /* Was ther an ATT error code in the call back? */ + if (bt_xp_conn->write_att_err != 0) { + LOG_ERR("ATT write error occurred, err: 0x%x", + bt_xp_conn->write_att_err); + return -1; + } + + total_write_cnt += write_count; + buf += write_count; + len -= write_count; + } + + LOG_DBG("Central - num bytes sent: %d", total_write_cnt); + + return total_write_cnt; +} + +#else /* CONFIG_BT_GATT_CLIENT */ + +/** + * Called when the Central has ACK'd receiving data + * Function is called in the System workqueue context + * + * @param conn Bluetooth connection + * @param params Indicate params + * @param err GATT error + */ +static void auth_xp_bt_peripheral_indicate(struct bt_conn *conn, + struct bt_gatt_indicate_params *params, + uint8_t err) +{ + struct auth_xport_connection_map *bt_xp_conn = auth_xp_bt_getconn(conn); + + /* set error */ + bt_xp_conn->indicate_err = err; + + // signal semaphore that chunk fo data was received from the peripheral + k_sem_give(&bt_xp_conn->auth_indicate_sem); + + /* if an error occurred */ + if (err != 0) { + LOG_DBG("Peripheral indication, err: %d", err); + } +} + +/** + * Used by the Peripheral to send data to the central. + * + * @param bt_xp_conn Bluetooth transport connection map. + * @param buf bytes to send. + * @aram len Number of bytes to send. + * + * @return On success, total number of bytes send. On error, negative error value. + */ +static int auth_xp_bt_peripheral_tx(struct auth_xport_connection_map *bt_xp_conn, + const unsigned char *buf, size_t len) +{ + int ret = 0; + int total_bytes_sent = 0; + bool done = false; + size_t send_cnt = 0; + static struct bt_uuid_128 xp_bt_auth_client_char = AUTH_SVC_CLIENT_CHAR_UUID; + + + /* Set payload size if not set. This is necessary when the MTU + * size is negotiated after the BT connection has been established. */ + if (bt_xp_conn->payload_size == 0) { + bt_xp_conn->payload_size = auth_xp_bt_get_max_payload(bt_xp_conn->xporthdl); + } + + + /* Setup the indicate params. The client will use BLE indications vs. + * notifications. This enables the client to know when the central has + * read the attribute and send another packet of data. */ + struct bt_gatt_indicate_params indicate_params; + + /* setup indicate params */ + memset(&indicate_params, 0, sizeof(indicate_params)); + + + indicate_params.uuid = (const struct bt_uuid *)&xp_bt_auth_client_char; + indicate_params.attr = bt_xp_conn->client_attr; + indicate_params.func = auth_xp_bt_peripheral_indicate; + + while (!done) { + + send_cnt = MIN(len, bt_xp_conn->payload_size); + + indicate_params.data = buf; + indicate_params.len = send_cnt; /* bytes to send */ + + ret = bt_gatt_indicate(bt_xp_conn->conn, &indicate_params); + + if (ret) { + LOG_ERR("bt_gatt_indicate failed, error: 0x%x", ret); + } + + /* wait on semaphore before sending next chunk */ + ret = k_sem_take(&bt_xp_conn->auth_indicate_sem, + K_MSEC(AUTH_BT_WRITE_TIMEOUTMSEC)); + + /* on wakeup check if error occurred */ + if (ret) { + LOG_ERR("Wait for central indicated failed, err: %d", ret); + break; + } + + /* update buffer pointer and count */ + total_bytes_sent += send_cnt; + len -= send_cnt; + buf += send_cnt; + + /* are we done sending? */ + if (len == 0) { + ret = total_bytes_sent; + break; + } + } + + return ret; +} + +#endif /* CONFIG_BT_GATT_CLIENT */ + + +/** + * @see auth_xport.h + */ +ssize_t auth_xp_bt_central_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) +{ + struct auth_xport_connection_map *bt_xp_conn = auth_xp_bt_getconn(conn); + + LOG_DBG("client write called, len: %d", len); + + /* put the received bytes into the receive queue */ + auth_message_hdr_to_cpu((struct auth_message_frag_hdr *)buf); + int err = auth_message_assemble(bt_xp_conn->xporthdl, buf, len); + + /* if no error, need to return num of bytes handled. */ + if (err >= 0) { + err = len; + } + + /* return number of bytes writen */ + return err; +} diff --git a/lib/auth/auth_xport_common.c b/lib/auth/auth_xport_common.c new file mode 100644 index 000000000000..3390b26df06e --- /dev/null +++ b/lib/auth/auth_xport_common.c @@ -0,0 +1,1039 @@ +/** + * @file auth_xport_common.c + * + * @brief Common transport routines. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#define LOG_LEVEL CONFIG_AUTH_LOG_LEVEL +#include +LOG_MODULE_DECLARE(auth_lib, CONFIG_AUTH_LOG_LEVEL); + +#include +#include +#include "auth_internal.h" + +/** + * Size of a buffer used for Rx. + */ +#define XPORT_IOBUF_LEN (4096u) + + +/** + * @brief Circular buffer used to save received data. + */ +struct auth_xport_io_buffer { + struct k_mutex buf_mutex; + struct k_sem buf_sem; + + uint32_t head_index; + uint32_t tail_index; + uint32_t num_valid_bytes; + + uint8_t io_buffer[XPORT_IOBUF_LEN]; +}; + + +/** + * Contains buffer used to assemble a message from multiple fragments. + */ +struct auth_message_recv { + /* pointer to buffer where message is assembled */ + uint8_t rx_buffer[XPORT_MAX_MESSAGE_SIZE]; + + /* vars used for re-assembling frames into a message */ + uint32_t rx_curr_offset; + bool rx_first_frag; +}; + + +/** + * Transport instance, contains send and recv circular queues. + */ +struct auth_xport_instance { + /* Send & Recv circular queues */ + struct auth_xport_io_buffer send_buf; + struct auth_xport_io_buffer recv_buf; + + /* Lower transport type */ + enum auth_xport_type xport_type; + + void *xport_ctx; /* transport specific context */ + + /* If the lower transport has a send function */ + send_xport_t send_func; + + /* Struct for handling assembling message from multiple fragments */ + struct auth_message_recv recv_msg; + + uint32_t payload_size; /* Max payload size for lower transport. */ +}; + +/* transport instances */ +static struct auth_xport_instance xport_inst[CONFIG_NUM_AUTH_INSTANCES]; + + +/* ================ local static funcs ================== */ + +/** + * Initializes common transport IO buffers. + * + * @param iobuf Pointer to IO buffer to initialize. + * + * @return 0 for success, else negative error value. + */ +static void auth_xport_iobuffer_init(struct auth_xport_io_buffer *iobuf) +{ + /* init mutex*/ + k_mutex_init(&iobuf->buf_mutex); + + /* init semaphore */ + k_sem_init(&iobuf->buf_sem, 0, 1); + + iobuf->head_index = 0; + iobuf->tail_index = 0; + iobuf->num_valid_bytes = 0; +} + +/** + * Reset queue counters + * + * @param iobuf Pointer to IO buffer to reset. + * + * @return 0 for success + */ +static void auth_xport_iobuffer_reset(struct auth_xport_io_buffer *iobuf) +{ + iobuf->head_index = 0; + iobuf->tail_index = 0; + iobuf->num_valid_bytes = 0; +} + + +/** + * Puts data into the transport buffer. + * + * @param iobuf IO buffer to add data. + * @param in_buf Data to put into IO buffer. + * @param num_bytes Number of bytes to put. + * + * @return On success, number of bytes put into IO buffer, can be less than requested. + * Negative number on error. + */ +static int auth_xport_buffer_put(struct auth_xport_io_buffer *iobuf, + const uint8_t *in_buf, size_t num_bytes) +{ + // Is the buffer full? + if (iobuf->num_valid_bytes == XPORT_IOBUF_LEN) { + return AUTH_ERROR_IOBUFF_FULL; + } + + /* don't put zero bytes */ + if (num_bytes == 0) { + return 0; + } + + /* lock mutex */ + int err = k_mutex_lock(&iobuf->buf_mutex, K_FOREVER); + if (err) { + return err; + } + + uint32_t free_space = XPORT_IOBUF_LEN - iobuf->num_valid_bytes; + uint32_t copy_cnt = MIN(free_space, num_bytes); + uint32_t total_copied = 0; + uint32_t byte_cnt; + + if (iobuf->head_index < iobuf->tail_index) { + // only enough room from head to tail, don't over-write + uint32_t max_copy_cnt = iobuf->tail_index - iobuf->head_index; + + copy_cnt = MIN(max_copy_cnt, copy_cnt); + + memcpy(iobuf->io_buffer + iobuf->head_index, in_buf, copy_cnt); + + total_copied += copy_cnt; + iobuf->head_index += copy_cnt; + iobuf->num_valid_bytes += copy_cnt; + + } else { + + // copy from head to end of buffer + byte_cnt = XPORT_IOBUF_LEN - iobuf->head_index; + + if (byte_cnt > copy_cnt) { + byte_cnt = copy_cnt; + } + + memcpy(iobuf->io_buffer + iobuf->head_index, in_buf, byte_cnt); + + total_copied += byte_cnt; + in_buf += byte_cnt; + copy_cnt -= byte_cnt; + iobuf->head_index += byte_cnt; + + iobuf->num_valid_bytes += byte_cnt; + + // if wrapped, then copy from beginning of buffer + if (copy_cnt > 0) { + memcpy(iobuf->io_buffer, in_buf, copy_cnt); + + total_copied += copy_cnt; + iobuf->num_valid_bytes += copy_cnt; + iobuf->head_index = copy_cnt; + } + } + + /* unlock */ + k_mutex_unlock(&iobuf->buf_mutex); + + /* after putting data into buffer, signal semaphore */ + k_sem_give(&iobuf->buf_sem); + + return (int)total_copied; +} + +/** + * Used to read bytes from an io buffer. + * + * @param iobuf IO buffer to read. + * @param out_buf Buffer to copy bytes into. + * @param num_bytes Number of bytes to read + * @param peek If true, then queue pointers are not updated. + * + * @return On success, number of bytes copied into buffer. Can be less than requested. + * Negative number on error. + */ +static int auth_xport_buffer_get_internal(struct auth_xport_io_buffer *iobuf, + uint8_t *out_buf, size_t num_bytes, bool peek) +{ + /* if no valid bytes, just return zero */ + if (iobuf->num_valid_bytes == 0) { + return 0; + } + + /* lock mutex */ + int err = k_mutex_lock(&iobuf->buf_mutex, K_FOREVER); + if (err) { + return err; + } + + /* number bytes to copy */ + uint32_t copy_cnt = MIN(iobuf->num_valid_bytes, num_bytes); + uint32_t total_copied = 0; + uint32_t byte_cnt = 0; + + if (iobuf->head_index <= iobuf->tail_index) { + + /* How may bytes are available? */ + byte_cnt = XPORT_IOBUF_LEN - iobuf->tail_index; + + if (byte_cnt > copy_cnt) { + byte_cnt = copy_cnt; + } + + /* copy from tail to end of buffer */ + memcpy(out_buf, iobuf->io_buffer + iobuf->tail_index, byte_cnt); + + if (!peek) { + /* update tail index */ + iobuf->tail_index += byte_cnt; + } + + out_buf += byte_cnt; + total_copied += byte_cnt; + + /* update copy count and num valid bytes */ + copy_cnt -= byte_cnt; + + if (!peek) { + iobuf->num_valid_bytes -= byte_cnt; + } + + /* wrapped around, copy from beginning of buffer until + copy_count is satisfied */ + if (copy_cnt > 0) { + + memcpy(out_buf, iobuf->io_buffer, copy_cnt); + + if (!peek) { + iobuf->tail_index = copy_cnt; + iobuf->num_valid_bytes -= copy_cnt; + } + + total_copied += copy_cnt; + } + + } else if (iobuf->head_index > iobuf->tail_index) { + + byte_cnt = iobuf->head_index - iobuf->tail_index; + + if (byte_cnt > copy_cnt) { + byte_cnt = copy_cnt; + } + + memcpy(out_buf, iobuf->io_buffer + iobuf->tail_index, byte_cnt); + + total_copied += byte_cnt; + copy_cnt -= byte_cnt; + + if (!peek) { + iobuf->tail_index += byte_cnt; + iobuf->num_valid_bytes -= byte_cnt; + } + } + + /* unlock */ + k_mutex_unlock(&iobuf->buf_mutex); + + return (int)total_copied; +} + +/** + * Peek at the contents of the input buffer. Copies bytes but does not advance + * the queue pointers. + * + * @param iobuf IO buffer to peek. + * @param out_buf Buffer to copy bytes into. + * @param num_bytes Number of bytes to peek. + * + * @return On success, number of bytes copied into buffer. Can be less than requested. + * Negative number on error. + */ +static int auth_xport_buffer_peek(struct auth_xport_io_buffer *iobuf, + uint8_t *out_buf, size_t num_bytes) +{ + return auth_xport_buffer_get_internal(iobuf, out_buf, num_bytes, true); +} + +/** + * Gets data from a IO buffer. + * + * @param iobuf IO buffer to get data from. + * @param out_buf Buffer to copy bytes into. + * @param num_bytes Number of bytes requested. + * + * @return On success, number of bytes copied into buffer. Can be less than requested. + * Negative number on error. + */ +static int auth_xport_buffer_get(struct auth_xport_io_buffer *iobuf, + uint8_t *out_buf, size_t num_bytes) +{ + return auth_xport_buffer_get_internal(iobuf, out_buf, num_bytes, false); +} + +/** + * Get data from an IO buffer, if no data present wait. + * + * @param iobuf The IO buffer to get from. + * @param out_buf Buffer to copy bytes into. + * @param num_bytes Number of bytes requested. + * @param waitmsec Number of milliseconds to wait if no data. + * + * @return On success, number of bytes copied, can be less than requested amount. + * Negative number on error. -EAGAIN if a timeout occurred. + */ +static int auth_xport_buffer_get_wait(struct auth_xport_io_buffer *iobuf, + uint8_t *out_buf, + int num_bytes, int waitmsec) +{ + /* return any bytes that might be sitting in the buffer */ + int bytecount = auth_xport_buffer_get(iobuf, out_buf, num_bytes); + + if (bytecount > 0) { + /* bytes are avail, return them */ + return bytecount; + } + + do { + int err = k_sem_take(&iobuf->buf_sem, K_MSEC(waitmsec)); + + if (err) { + return err; /* timed out -EAGAIN or error */ + } + + /* return byte count or error (bytecount < 0) */ + bytecount = auth_xport_buffer_get(iobuf, out_buf, num_bytes); + + } while (bytecount == 0); + + return bytecount; +} + +/** + * Get the number of bytes in an IO buffer. + * + * @param iobuf The IO buffer to check. + * + * @return On success, number of bytes. Negative number on error. + */ +static int auth_xport_buffer_bytecount(struct auth_xport_io_buffer *iobuf) +{ + int err = k_mutex_lock(&iobuf->buf_mutex, K_FOREVER); + + if (!err) { + err = (int)iobuf->num_valid_bytes; + } + + /* unlock */ + k_mutex_unlock(&iobuf->buf_mutex); + + return err; +} + + +/** + * Wait for a non-zero byte count. Used to wait until data is received + * from the sender. Wait until waitmsec has timed out or data has been received. + * + * @param iobuf IO buffer to wait on bytes. + * @param waitmsec Number of milliseconds to wait. + * + * @return On success, number of bytes in the IO buffer. + * -EAGAIN on timeout. + */ +static int auth_xport_buffer_bytecount_wait(struct auth_xport_io_buffer *iobuf, + uint32_t waitmsec) +{ + int num_bytes = auth_xport_buffer_bytecount(iobuf); + + /* an error occurred or there are bytes sitting in the io buffer. */ + if (num_bytes != 0) { + return num_bytes; + } + + /* wait for byte to fill the io buffer */ + int err = k_sem_take(&iobuf->buf_sem, K_MSEC(waitmsec)); + + if (err) { + return err; /* timed out -EAGAIN or error */ + } + + /* return the number of bytes in the queue */ + return auth_xport_buffer_bytecount(iobuf); +} + + +/** + * Get the frees pace within an IO bufer. + * + * @param iobuf IO Buffer to check. + * + * @return Amount of free space. + */ +static int auth_xport_buffer_avail_bytes(struct auth_xport_io_buffer *iobuf) +{ + return sizeof(iobuf->io_buffer) - auth_xport_buffer_bytecount(iobuf); +} + +/** + * Internal function to send data to peer + * + * @param xporthdl Transport handle + * @param data Buffer to send. + * @param len Number of bytes to send. + * + * @return Number of bytes sent on success, can be less than requested. + * On error, negative error code. + */ +static int auth_xport_internal_send(const auth_xport_hdl_t xporthdl, + const uint8_t *data, size_t len) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + /* if the lower transport set a send function, call it */ + if (xp_inst->send_func != NULL) { + return xp_inst->send_func(xporthdl, data, len); + } + + /* queue the send bytes into tx buffer */ + return auth_xport_buffer_put(&xp_inst->send_buf, data, len); +} + +/** + * Initializes message receive struct. Used to re-assemble message + * fragments received. + * + * @param recv_msg Received message buffer. + */ +static void auth_message_frag_init(struct auth_message_recv *recv_msg) +{ + recv_msg->rx_curr_offset = 0; + recv_msg->rx_first_frag = true; +} + + +/** + * Returns the lower transport type associated with the opaque + * transport handle. + * + * @param xporthdl The transport handle. + * + * @return Transport type + */ +static enum auth_xport_type auth_get_xport_type(auth_xport_hdl_t xporthdl) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_XP_TYPE_NONE; + } + + return xp_inst->xport_type; +} + + +/* ==================== Non static funcs ================== */ + +/** + * @see auth_xport.h + */ +int auth_xport_init(auth_xport_hdl_t *xporthdl, enum auth_instance_id instance, + enum auth_xport_type xport_type, void *xport_params) +{ + int ret = -1; + + /* verify instance */ + if ((instance < 0) || (instance >= AUTH_MAX_INSTANCES)) { + return AUTH_ERROR_INVALID_PARAM; + } + + if ((xport_type != AUTH_XP_TYPE_BLUETOOTH) && + (xport_type != AUTH_XP_TYPE_SERIAL)) { + return AUTH_ERROR_INVALID_PARAM; + } + + + *xporthdl = &xport_inst[instance]; + + /* init IO buffers */ + auth_xport_iobuffer_init(&xport_inst[instance].send_buf); + auth_xport_iobuffer_init(&xport_inst[instance].recv_buf); + auth_message_frag_init(&xport_inst[instance].recv_msg); + + /* Set the lower transport type */ + xport_inst[instance].xport_type = xport_type; + + +#if defined(CONFIG_BT_XPORT) + if (xport_type == AUTH_XP_TYPE_BLUETOOTH) { + ret = auth_xp_bt_init(*xporthdl, 0, xport_params); + } +#endif + +#if defined(CONFIG_SERIAL_XPORT) + if (xport_type == AUTH_XP_TYPE_SERIAL) { + ret = auth_xp_serial_init(*xporthdl, 0, xport_params); + } +#endif + + + return ret; +} + +/** + * @see auth_xport.h + */ +int auth_xport_deinit(const auth_xport_hdl_t xporthdl) +{ + int ret = 0; + enum auth_xport_type xport_type = AUTH_XP_TYPE_NONE; + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + xport_type = auth_get_xport_type(xporthdl); + + /* reset queues */ + auth_xport_iobuffer_reset(&xp_inst->send_buf); + auth_xport_iobuffer_reset(&xp_inst->recv_buf); + + +#if defined(CONFIG_BT_XPORT) + if (xport_type == AUTH_XP_TYPE_BLUETOOTH) { + ret = auth_xp_bt_deinit(xporthdl); + } +#endif + +#if defined(CONFIG_SERIAL_XPORT) + if (xport_type == AUTH_XP_TYPE_SERIAL) { + ret = auth_xp_serial_deinit(xporthdl); + } +#endif + + xp_inst->xport_type = AUTH_XP_TYPE_NONE; + + return ret; +} + +/** + * @see auth_xport.h + */ +int auth_xport_event(const auth_xport_hdl_t xporthdl, struct auth_xport_evt *event) +{ + int ret = 0; + enum auth_xport_type xport_type = auth_get_xport_type(xporthdl); + +#if defined(CONFIG_BT_XPORT) + if (xport_type == AUTH_XP_TYPE_BLUETOOTH) { + ret = auth_xp_bt_event(xporthdl, event); + } +#endif + +#if defined(CONFIG_SERIAL_XPORT) + if (xport_type == AUTH_XP_TYPE_SERIAL) { + ret = auth_xp_serial_event(xporthdl, event); + } +#endif + + return ret; +} + +/** + * @see auth_xport.h + */ +int auth_xport_get_max_payload(const auth_xport_hdl_t xporthdl) +{ + int mtu = 0; + enum auth_xport_type xport_type = auth_get_xport_type(xporthdl); + +#if defined(CONFIG_BT_XPORT) + if (xport_type == AUTH_XP_TYPE_BLUETOOTH) { + mtu = auth_xp_bt_get_max_payload(xporthdl); + } +#endif + +#if defined(CONFIG_SERIAL_XPORT) + if (xport_type == AUTH_XP_TYPE_SERIAL) { + mtu = auth_xp_serial_get_max_payload(xporthdl); + } +#endif + return mtu; +} + +/** + * @see auth_xport.h + */ +int auth_xport_send(const auth_xport_hdl_t xporthdl, const uint8_t *data, size_t len) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + int fragment_bytes; + int payload_bytes; + int send_count = 0; + int num_fragments = 0; + int send_ret = AUTH_SUCCESS; + struct auth_message_fragment msg_frag; + + + /* If the lower transport MTU size isn't set, get it. This can happen + * when the the MTU is negotiated after the initial connection. */ + if (xp_inst->payload_size == 0) { + xp_inst->payload_size = auth_xport_get_max_payload(xporthdl); + } + + const uint16_t max_frame = MIN(sizeof(msg_frag), xp_inst->payload_size); + const uint16_t max_payload = max_frame - XPORT_FRAG_HDR_BYTECNT; + + /* sanity check */ + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + /* set frame header */ + msg_frag.hdr.sync_flags = XPORT_FRAG_SYNC_BITS | XPORT_FRAG_BEGIN; + + /* Break up data to fit into lower transport MTU */ + while (len > 0) { + + /* get payload bytes */ + payload_bytes = MIN(max_payload, len); + + fragment_bytes = payload_bytes + XPORT_FRAG_HDR_BYTECNT; + + /* is this the last frame? */ + if ((len - payload_bytes) == 0) { + + msg_frag.hdr.sync_flags = XPORT_FRAG_SYNC_BITS | XPORT_FRAG_END; + + /* now check if we're only sending one frame, then set + * the frame begin flag */ + if (num_fragments == 0) { + msg_frag.hdr.sync_flags |= XPORT_FRAG_BEGIN; + } + } + + /* copy body */ + memcpy(msg_frag.frag_payload, data, payload_bytes); + msg_frag.hdr.payload_len = payload_bytes; + + /* convert header to Big Endian, network byte order */ + auth_message_hdr_to_be16(&msg_frag.hdr); + + /* send frame */ + send_ret = auth_xport_internal_send(xporthdl, + (const uint8_t *)&msg_frag, fragment_bytes); + + if (send_ret < 0) { + LOG_ERR("Failed to send xport frame, error: %d", send_ret); + return AUTH_ERROR_XPORT_SEND; + } + + /* verify all bytes were sent */ + if (send_ret != fragment_bytes) { + LOG_ERR("Failed to to send all bytes, send: %d, requested: %d", send_ret, fragment_bytes); + return AUTH_ERROR_XPORT_SEND; + } + + /* set next flags */ + msg_frag.hdr.sync_flags = XPORT_FRAG_SYNC_BITS | XPORT_FRAG_NEXT; + + len -= payload_bytes; + data += payload_bytes; + send_count += payload_bytes; + num_fragments++; + } + + return send_count; +} + + +/** + * @see auth_xport.h + * + * Put bytes received from lower transport into receive queue + */ +int auth_xport_put_recv(const auth_xport_hdl_t xporthdl, const uint8_t *buf, + size_t buflen) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + return auth_xport_buffer_put(&xp_inst->recv_buf, buf, buflen); +} + + +/** + * @see auth_xport.h + */ +int auth_xport_recv(const auth_xport_hdl_t xporthdl, uint8_t *buf, + uint32_t buf_len, uint32_t timeoutMsec) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + return auth_xport_buffer_get_wait(&xp_inst->recv_buf, buf, buf_len, + timeoutMsec); +} + +/** + * @see auth_xport.h + */ +int auth_xport_recv_peek(const auth_xport_hdl_t xporthdl, uint8_t *buff, + uint32_t buf_len) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + return auth_xport_buffer_peek(&xp_inst->recv_buf, buff, buf_len); +} + +/** + * @see auth_xport.h + */ +int auth_xport_getnum_send_queued_bytes(const auth_xport_hdl_t xporthdl) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + return auth_xport_buffer_bytecount(&xp_inst->send_buf); +} + +/** + * @see auth_xport.h + */ +int auth_xport_getnum_recvqueue_bytes(const auth_xport_hdl_t xporthdl) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + return auth_xport_buffer_bytecount(&xp_inst->recv_buf); +} + +/** + * @see auth_xport.h + */ +int auth_xport_getnum_recvqueue_bytes_wait(const auth_xport_hdl_t xporthdl, + uint32_t waitmsec) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + if (xp_inst == NULL) { + return AUTH_ERROR_INVALID_PARAM; + } + + return auth_xport_buffer_bytecount_wait(&xp_inst->recv_buf, waitmsec); +} + + +/** + * @see auth_internal.h + */ +bool auth_message_get_fragment(const uint8_t *buffer, uint16_t buflen, + uint16_t *frag_beg_offset, uint16_t *frag_byte_cnt) +{ + uint16_t cur_offset; + uint16_t temp_payload_len; + struct auth_message_frag_hdr *frm_hdr; + bool found_sync_bytes = false; + + /* quick check */ + if (buflen < XPORT_MIN_FRAGMENT) { + return false; + } + + /* look for sync bytes */ + for (cur_offset = 0; cur_offset < buflen; cur_offset++, buffer++) { + /* look for the first sync byte */ + if (*buffer == XPORT_FRAG_SYNC_BYTE_HIGH) { + if (cur_offset + 1 < buflen) { + if ((*(buffer + 1) & XPORT_FRAG_LOWBYTE_MASK) == + XPORT_FRAG_SYNC_BYTE_LOW) { + /* found sync bytes */ + found_sync_bytes = true; + break; + } + } + } + } + + /* Didn't find Fragment sync bytes */ + if (!found_sync_bytes) { + return false; + } + + /* should have a full header, check frame len */ + frm_hdr = (struct auth_message_frag_hdr *)buffer; + + /* convert from be to cpu */ + temp_payload_len = sys_be16_to_cpu(frm_hdr->payload_len) + + XPORT_FRAG_HDR_BYTECNT; + + /* does the buffer contian all of the fragment bytes? + * Including the header. */ + if (temp_payload_len > (buflen - cur_offset)) { + /* not enough bytes for a full frame */ + return false; + } + + /* Put header vars into CPU byte order. */ + auth_message_hdr_to_cpu(frm_hdr); + + /* Have a full fragment */ + *frag_beg_offset = cur_offset; + + /* Return fragment byte count, including the header */ + *frag_byte_cnt = frm_hdr->payload_len + XPORT_FRAG_HDR_BYTECNT; + + return true; +} + +/** + * @see auth_internal.h + */ +int auth_message_assemble(const auth_xport_hdl_t xporthdl, const uint8_t *buf, + size_t buflen) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + struct auth_message_recv *msg_recv; + struct auth_message_fragment *rx_frag; + int free_buf_space; + int recv_ret = 0; + + /* check input params */ + if ((xp_inst == NULL) || (buf == NULL) || (buflen == 0u)) { + return AUTH_ERROR_INVALID_PARAM; + } + + msg_recv = (struct auth_message_recv *)&xp_inst->recv_msg; + + /* If max payload size isn't set, get it from the lower transport. + * This can happen if the lower transports frame/MTU size is set + * after an initial connection. */ + if (xp_inst->payload_size == 0) { + xp_inst->payload_size = auth_xport_get_max_payload(xporthdl); + } + + /* Reassemble a message from one for more fragments. */ + rx_frag = (struct auth_message_fragment *)buf; + + /* check for start flag */ + if (msg_recv->rx_first_frag) { + + msg_recv->rx_first_frag = false; + + if (!(rx_frag->hdr.sync_flags & XPORT_FRAG_BEGIN)) { + /* reset vars */ + msg_recv->rx_curr_offset = 0; + msg_recv->rx_first_frag = true; + + LOG_ERR("RX-Missing beginning fragment"); + return AUTH_ERROR_XPORT_FRAME; + } + + LOG_DBG("RX-Got BEGIN fragment."); + } + + /* check fragment sync bytes */ + if ((rx_frag->hdr.sync_flags & XPORT_FRAG_SYNC_MASK) != XPORT_FRAG_SYNC_BITS) { + /* reset vars */ + msg_recv->rx_curr_offset = 0; + msg_recv->rx_first_frag = true; + + LOG_ERR("RX-Invalid fragment."); + return AUTH_ERROR_XPORT_FRAME; + } + + + /* Subtract out fragment header */ + buflen -= XPORT_FRAG_HDR_BYTECNT; + + /* move beyond fragment header */ + buf += XPORT_FRAG_HDR_BYTECNT; + + /* sanity check, if zero or negative */ + if (buflen <= 0) { + /* reset vars */ + msg_recv->rx_curr_offset = 0; + msg_recv->rx_first_frag = true; + LOG_ERR("RX-Empty fragment!!"); + return AUTH_ERROR_XPORT_FRAME; + } + + /* ensure there's enough free space in our temp buffer */ + free_buf_space = sizeof(msg_recv->rx_buffer) - msg_recv->rx_curr_offset; + + if (free_buf_space < buflen) { + /* reset vars */ + msg_recv->rx_curr_offset = 0; + msg_recv->rx_first_frag = true; + LOG_ERR("RX-not enough free space"); + return AUTH_ERROR_XPORT_FRAME; + } + + /* copy payload bytes */ + memcpy(msg_recv->rx_buffer + msg_recv->rx_curr_offset, buf, buflen); + + msg_recv->rx_curr_offset += buflen; + + /* returned the number of bytes queued */ + recv_ret = buflen; + + /* Is this the last fragment of the message? */ + if (rx_frag->hdr.sync_flags & XPORT_FRAG_END) { + + /* log number payload bytes received. */ + LOG_INF("RX-Got LAST fragment, total bytes: %d", msg_recv->rx_curr_offset); + + int free_bytes = auth_xport_buffer_avail_bytes(&xp_inst->recv_buf); + + /* Is there enough free space to write entire message? */ + if (free_bytes >= msg_recv->rx_curr_offset) { + + /* copy message into receive buffer */ + recv_ret = auth_xport_buffer_put(&xp_inst->recv_buf, + msg_recv->rx_buffer, + msg_recv->rx_curr_offset); + + } else { + int need = msg_recv->rx_curr_offset - free_bytes; + LOG_ERR("Not enough room in RX buffer, free: %d, need %d bytes.", free_bytes, need); + } + + /* reset vars */ + msg_recv->rx_curr_offset = 0; + msg_recv->rx_first_frag = true; + } + + return recv_ret; +} + +/** + * @see auth_internal.h + */ +void auth_message_hdr_to_cpu(struct auth_message_frag_hdr *frag_hdr) +{ + frag_hdr->sync_flags = sys_be16_to_cpu(frag_hdr->sync_flags); + frag_hdr->payload_len = sys_be16_to_cpu(frag_hdr->payload_len); +} + +/** + * @see auth_internal.h + */ +void auth_message_hdr_to_be16(struct auth_message_frag_hdr *frag_hdr) +{ + frag_hdr->sync_flags = sys_cpu_to_be16(frag_hdr->sync_flags); + frag_hdr->payload_len = sys_cpu_to_be16(frag_hdr->payload_len); +} + + +/** + * @see auth_xport.h + */ +void auth_xport_set_sendfunc(auth_xport_hdl_t xporthdl, send_xport_t send_func) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + xp_inst->send_func = send_func; +} + +/** + * @see auth_xport.h + */ +void auth_xport_set_context(auth_xport_hdl_t xporthdl, void *context) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + xp_inst->xport_ctx = context; +} + +/** + * @see auth_xport.h + */ +void *auth_xport_get_context(auth_xport_hdl_t xporthdl) +{ + struct auth_xport_instance *xp_inst = (struct auth_xport_instance *)xporthdl; + + return xp_inst->xport_ctx; +} + + + + + + diff --git a/lib/auth/auth_xport_serial.c b/lib/auth/auth_xport_serial.c new file mode 100644 index 000000000000..b00005b0761f --- /dev/null +++ b/lib/auth/auth_xport_serial.c @@ -0,0 +1,602 @@ +/** + * @file auth_xport_serial.c + * + * @brief Lower serial transport layer. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "auth_internal.h" + + +#define LOG_LEVEL CONFIG_AUTH_LOGLEVEL +#include +LOG_MODULE_REGISTER(auth_serial_xport, CONFIG_AUTH_LOG_LEVEL); + + +#define SERIAL_LINK_MTU (600u) +#define SERIAL_XP_BUFFER_LEN SERIAL_LINK_MTU +#define NUM_BUFFERS (7u) + +/* Number of events on the msg queue. NOTE: This queue is shared by all instances. */ +#define RX_EVENT_MSGQ_COUNT (10u) + +#define MAX_SERIAL_INSTANCES (3u) +#define SERIAL_XP_RECV_THRD_PRIORITY (0) +#define SERIAL_XP_RECV_STACK_SIZE (1024u) + +#define SERIAL_TX_WAIT_MSEC (1500u) + + + +/* Serial transport instance */ +struct serial_xp_instance { + bool in_use; + auth_xport_hdl_t xport_hdl; + + /* The UART device instance */ + const struct device *uart_dev; + + /* Current transmit buffer */ + uint8_t *tx_buf; + uint16_t tx_bytes; /* number of bytes to send */ + uint16_t curr_tx_cnt; /* current tx send count */ + + /* Semaphore for TX */ + struct k_sem tx_sem; + + /* current rx buffer */ + uint8_t *rx_buf; + uint16_t curr_rx_cnt; +}; + + +/* Buffer used for TX/RX */ +struct serial_xp_buffer { + bool in_use; + uint32_t bufidx; /* Buffer bit index. */ + uint8_t buffer[SERIAL_XP_BUFFER_LEN]; +}; + +/** + * Used to pass bytes to the receive thread from the + * UART interrupt routine. + */ +struct serial_recv_event { + + struct serial_xp_instance *serial_inst; + uint8_t *rx_buf; + uint16_t frag_offset; + uint16_t frag_len; + + /* struct needs to be multiple of 4 bytes to work with the + * message queue, add padding here */ + uint8_t pad[4]; +}; + + +static struct serial_xp_instance serial_xp_inst[CONFIG_NUM_AUTH_INSTANCES]; + + +/** + * Serial receive thread. Used to forward bytes from the ISR to the + * transport receive queue. One thread used for all serial instances. + */ +static void auth_xp_serial_recv_thrd(void *arg1, void *arg2, void *arg3); + +K_THREAD_DEFINE(serial_recv, SERIAL_XP_RECV_STACK_SIZE, auth_xp_serial_recv_thrd, + NULL, NULL, NULL, SERIAL_XP_RECV_THRD_PRIORITY, 0, 0); + +/** + * Receive event message queue + */ +K_MSGQ_DEFINE(recv_event_queue, sizeof(struct serial_recv_event), RX_EVENT_MSGQ_COUNT, 4); + + +/* Atomic bits to determine if a buffer is in use. If bit is set + * buffer is in use. */ +ATOMIC_DEFINE(buffer_in_use, NUM_BUFFERS); + + +/** + * Serial buffer pool, used across all serial instances. + * + * @note: This is a simple implementation of a buffer pool. Another + * option considered was creating a memory slab, this buffer + * pool was selected because of its simplicity. + * + * @note: There is a trade-off between the number of buffers and the + * serial MTU size (SERIAL_LINK_MTU). The larger the MTU size the + * fewer buffers needed. + */ +static struct serial_xp_buffer serial_xp_bufs[NUM_BUFFERS] = { + { .in_use = false, .bufidx = 0 }, + { .in_use = false, .bufidx = 1 }, + { .in_use = false, .bufidx = 2 }, + { .in_use = false, .bufidx = 3 }, + { .in_use = false, .bufidx = 4 }, + { .in_use = false, .bufidx = 5 }, + { .in_use = false, .bufidx = 6 } +}; + + +/** + * Returns information about a serial buffer. + * + * @param buf Pointer to buffer. + * + * @return Pointer to serial buffer info. + */ +static struct serial_xp_buffer *serial_xp_buffer_info(const uint8_t *buf) +{ + if (buf == NULL) { + return NULL; + } + + /* get pointer to containing struct*/ + struct serial_xp_buffer *xp_buf = + (struct serial_xp_buffer *)CONTAINER_OF(buf, struct serial_xp_buffer, buffer); + + return xp_buf; +} + + +/** + * Gets a free buffer. + * + * @param buflen Requested buffer size, if too large NULL return. + * + * @return Pointer to buffer, else NULL on error. + */ +static uint8_t *serial_xp_get_buffer(uint32_t buflen) +{ + int cnt; + + if (buflen > SERIAL_LINK_MTU) { + LOG_ERR("Buffer request too large: %d", buflen); + return NULL; + } + + /* check array of tx buffers */ + for (cnt = 0; cnt < NUM_BUFFERS; cnt++) { + if (!atomic_test_and_set_bit(buffer_in_use, cnt)) { + serial_xp_bufs[cnt].in_use = true; + return serial_xp_bufs[cnt].buffer; + } + } + + return NULL; +} + + +/** + * Free buffer. + * + * @param buffer Buffer to free. + */ +static void serial_xp_free_buffer(const uint8_t *buffer) +{ + if (buffer == NULL) { + return; + } + struct serial_xp_buffer *xp_buffer = serial_xp_buffer_info(buffer); + + xp_buffer->in_use = false; + atomic_clear_bit(buffer_in_use, xp_buffer->bufidx); +} + + +/** + * Gets a serial transport instance. + * + * @return Pointer to serial transport, else NULL on error. + */ +static struct serial_xp_instance *auth_xp_serial_get_instance(void) +{ + uint32_t cnt; + + for (cnt = 0; cnt < MAX_SERIAL_INSTANCES; cnt++) { + + if (!serial_xp_inst[cnt].in_use) { + serial_xp_inst[cnt].in_use = true; + return &serial_xp_inst[cnt]; + } + } + + return NULL; +} + +/** + * Free serial transport instance. + * + * @param serial_inst Pointer to serial transport instance. + */ +static void auth_xp_serial_free_instance(struct serial_xp_instance *serial_inst) +{ + if (serial_inst != NULL) { + + serial_inst->in_use = false; + serial_inst->xport_hdl = NULL; + serial_inst->uart_dev = NULL; + + /* free tx and rx buffers */ + if (serial_inst->tx_buf != NULL) { + serial_xp_free_buffer(serial_inst->tx_buf); + serial_inst->tx_buf = NULL; + } + + if (serial_inst->rx_buf != NULL) { + serial_xp_free_buffer(serial_inst->rx_buf); + serial_inst->rx_buf = NULL; + } + + serial_inst->tx_bytes = 0; + serial_inst->curr_tx_cnt = 0; + serial_inst->curr_rx_cnt = 0; + } +} + + +/** + * UART receive thread. Handles bytes from the UART interrupt routine + * and forwards to the common transport receive queue. + * + * Handles receive byes from all of the serial instances. + * + */ +static void auth_xp_serial_recv_thrd(void *arg1, void *arg2, void *arg3) +{ + struct serial_recv_event recv_event; + + /* unused */ + (void)arg1; + (void)arg2; + (void)arg3; + + while (true) { + + if (k_msgq_get(&recv_event_queue, &recv_event, K_FOREVER) == 0) { + + auth_message_assemble(recv_event.serial_inst->xport_hdl, + recv_event.rx_buf + recv_event.frag_offset, + recv_event.frag_len); + + /* free RX buffer */ + serial_xp_free_buffer(recv_event.rx_buf); + } + + } /* end while() */ + +} + + +/** + * Reads bytes from UART into a rx buffer. When enough bytes have + * accumulated to fill a message fragment, put fragment onto message queue. + * The serial RX thread will read this message and forward bytes to upper + * layer transport. + * + * This function is called in an ISR context. + * + * @param uart Pointer to UART device + * @param xp_inst Pointer to serial transport instance. + * + */ +static void auth_xp_serial_irq_recv_fragment(struct serial_xp_instance *xp_inst) +{ + int num_bytes; + int total_cnt; + uint8_t *new_rxbuf = NULL; + uint16_t frag_beg_offset; + uint16_t frag_bytes; + uint16_t remaining_buffer_bytes; + struct serial_recv_event recv_event; + + if (xp_inst->rx_buf == NULL) { + /* try to allocate buffer */ + xp_inst->rx_buf = serial_xp_get_buffer(SERIAL_XP_BUFFER_LEN); + xp_inst->curr_rx_cnt = 0; + + if (xp_inst->rx_buf == NULL) { + LOG_ERR("Out of free buffers for Rx."); + return; + } + } + + /* Check if rx buffer is full, if so then something went wrong. + * Log and error and drop bytes received so far */ + if (xp_inst->curr_rx_cnt == SERIAL_XP_BUFFER_LEN) { + xp_inst->curr_rx_cnt = 0; + LOG_ERR("Receive buffer full, dropped %d bytes.", SERIAL_XP_BUFFER_LEN); + } + + num_bytes = uart_fifo_read(xp_inst->uart_dev, xp_inst->rx_buf + xp_inst->curr_rx_cnt, + SERIAL_XP_BUFFER_LEN - xp_inst->curr_rx_cnt); + total_cnt += num_bytes; + + xp_inst->curr_rx_cnt += num_bytes; + + + /* Is there a full frame? */ + if (auth_message_get_fragment(xp_inst->rx_buf, xp_inst->curr_rx_cnt, + &frag_beg_offset, &frag_bytes)) { + + /* A full message fragment is present in the input buffer + * starting at frag_beg_offset and frag_bytes in length. + * It's possible to have the beginning of a second fragment + * following this first fragment. */ + + /* get new rx buffer */ + new_rxbuf = serial_xp_get_buffer(SERIAL_XP_BUFFER_LEN); + + /* If there's garbage before the frame start,then skip. + * If there is another fragment or partial fragment following + * then copy to new buffer. 'remaining_buffer_bytes' is the + * number of valid bytes after the current fragment. */ + remaining_buffer_bytes = xp_inst->curr_rx_cnt - frag_beg_offset - frag_bytes; + + /* If fragment bytes are less than the current, then the buffer contains bytes + * for the next fragment. */ + if ((remaining_buffer_bytes != 0) && (new_rxbuf != NULL)) { + /* copy extra bytes to new buffer */ + memcpy(new_rxbuf, xp_inst->rx_buf + frag_beg_offset + frag_bytes, + remaining_buffer_bytes); + } + + + /* Setup receive event */ + recv_event.rx_buf = xp_inst->rx_buf; + recv_event.frag_offset = frag_beg_offset; + recv_event.frag_len = frag_bytes; + recv_event.serial_inst = xp_inst; + + /* send fragment to receive thread via message queue */ + if (k_msgq_put(&recv_event_queue, &recv_event, K_NO_WAIT) != 0) { + /* an error occurred, free buffer */ + serial_xp_free_buffer(recv_event.rx_buf); + LOG_ERR("Failed to queue recv event, dropping recv bytes."); + } + + /* now setup new RX fragment */ + if (new_rxbuf != NULL) { + xp_inst->rx_buf = new_rxbuf; + xp_inst->curr_rx_cnt = remaining_buffer_bytes; + } else { + /* no free buffers */ + xp_inst->rx_buf = NULL; + xp_inst->curr_rx_cnt = 0; + } + } +} + +/** + * For interrupt driven IO for serial link. + * @param dev Pointer to UART device. + * @param user_data Used to store serial instance. + */ +static void auth_xp_serial_irq_cb(const struct device *uart_dev, void *user_data) +{ + int num_bytes; + static int total_cnt = 0; + + enum uart_rx_stop_reason rx_stop; + struct serial_xp_instance *xp_inst = (struct serial_xp_instance *) user_data; + + uart_irq_update(uart_dev); + + /* did an error happen */ + rx_stop = uart_err_check(uart_dev); + + if (rx_stop != 0) { + /* An error, either: + * UART_ERROR_OVERRUN, UART_ERROR_PARITY, + * UART_ERROR_FRAMING, UART_ERROR_BREAK + */ + LOG_ERR("UART error: %d", rx_stop); + return; + } + + /* read any chars first */ + while (uart_irq_rx_ready(uart_dev)) { + auth_xp_serial_irq_recv_fragment(xp_inst); + } + + /* Any bytes ready to send? */ + if (xp_inst->tx_bytes == 0) { + /* check if we can disable TX */ + if (uart_irq_tx_complete(uart_dev)) { + uart_irq_tx_disable(uart_dev); + } + + return; + } + + total_cnt = 0; + while (uart_irq_tx_ready(uart_dev) && xp_inst->tx_buf != NULL) { + + num_bytes = uart_fifo_fill(uart_dev, + xp_inst->tx_buf + xp_inst->curr_tx_cnt, + xp_inst->tx_bytes); + + /* check return can this be an error? */ + xp_inst->tx_bytes -= num_bytes; + xp_inst->curr_tx_cnt += num_bytes; + + total_cnt += num_bytes; + + /* if no more data to send, then break */ + if (xp_inst->tx_bytes == 0) { + LOG_INF("Sent tx buffer, bytes: %d.", xp_inst->curr_tx_cnt); + break; + } + } + + /* we're done sending */ + if ((xp_inst->tx_bytes == 0) && (xp_inst->tx_buf != NULL)) { + serial_xp_free_buffer(xp_inst->tx_buf); + xp_inst->tx_buf = NULL; + xp_inst->curr_tx_cnt = 0; + + /* signal TX complete */ + k_sem_give(&xp_inst->tx_sem); + } +} + + +/** + * Send bytes on the serial link. + * @note: This call will block!!! Should not call from an ISR. + * + * @param xport_hdl Transport handle. + * @param data Bytes to send. + * @param len Number of bytes to send. + * + * @return Number of bytes sent on success, else negative error value. + */ +static int auth_xp_serial_send(auth_xport_hdl_t xport_hdl, const uint8_t *data, + const size_t len) +{ + if (len > SERIAL_LINK_MTU) { + LOG_ERR("Too many bytes to send."); + return AUTH_ERROR_INVALID_PARAM; + } + + struct serial_xp_instance *serial_inst = + (struct serial_xp_instance *)auth_xport_get_context(xport_hdl); + + /* is there a pending TX operation? If so return busy error */ + if (serial_inst->tx_buf != NULL) { + LOG_ERR("TX operation in process."); + return -EAGAIN; + } + + /* get free buffer for tx */ + serial_inst->tx_buf = serial_xp_get_buffer(len); + + if (serial_inst->tx_buf == NULL) { + LOG_ERR("No free TX buffer."); + serial_inst->tx_bytes = 0; + serial_inst->curr_tx_cnt = 0; + return AUTH_ERROR_NO_RESOURCE; + } + + /* fill buffer, set as _in use */ + memcpy(serial_inst->tx_buf, data, len); + serial_inst->tx_bytes = len; + serial_inst->curr_tx_cnt = 0; + + /* should kick off an interrupt */ + uart_irq_tx_enable(serial_inst->uart_dev); + + LOG_INF("Started TX operation"); + + /* Wait on semaphore for TX to complete */ + int ret = k_sem_take(&serial_inst->tx_sem, K_MSEC(SERIAL_TX_WAIT_MSEC)); + + if (ret) { + return ret; + } + + return (int)len; +} + + +/** + * @see auth_xport.h + */ +int auth_xp_serial_init(const auth_xport_hdl_t xport_hdl, uint32_t flags, + void *xport_param) +{ + struct auth_xp_serial_params *serial_param = + (struct auth_xp_serial_params *)xport_param; + + struct serial_xp_instance *serial_inst = auth_xp_serial_get_instance(); + + if (serial_inst == NULL) { + LOG_ERR("No free serial xport instances."); + return AUTH_ERROR_NO_RESOURCE; + } + + serial_inst->xport_hdl = xport_hdl; + serial_inst->uart_dev = serial_param->uart_dev; + + /* set serial irq callback */ + uart_irq_callback_user_data_set(serial_inst->uart_dev, + auth_xp_serial_irq_cb, + serial_inst); + + /* set context into xport handle */ + auth_xport_set_context(xport_hdl, serial_inst); + + auth_xport_set_sendfunc(xport_hdl, auth_xp_serial_send); + + /* Init semaphore used to wait for TX to complete. */ + k_sem_init(&serial_inst->tx_sem, 0, 1); + + /* reset tx vars */ + serial_inst->tx_buf = NULL; + serial_inst->tx_bytes = 0; + serial_inst->curr_tx_cnt = 0; + + /* get rx buffer */ + serial_inst->rx_buf = serial_xp_get_buffer(SERIAL_XP_BUFFER_LEN); + serial_inst->curr_rx_cnt = 0; + + /* enable rx interrupts */ + uart_irq_rx_enable(serial_param->uart_dev); + + /* enable error irq */ + uart_irq_err_enable(serial_param->uart_dev); + + return AUTH_SUCCESS; +} + +/** + * @see auth_xport.h + */ +int auth_xp_serial_deinit(const auth_xport_hdl_t xport_hdl) +{ + struct serial_xp_instance *serial_inst = auth_xport_get_context(xport_hdl); + + auth_xp_serial_free_instance(serial_inst); + + auth_xport_set_context(xport_hdl, NULL); + + return AUTH_SUCCESS; +} + + +/** + * @see auth_xport.h + */ +int auth_xp_serial_event(const auth_xport_hdl_t xporthdl, + struct auth_xport_evt *event) +{ + /* stub for now */ + return AUTH_ERROR_INTERNAL; +} + +/** + * @see auth_xport.h + */ +int auth_xp_serial_get_max_payload(const auth_xport_hdl_t xporthdl) +{ + return SERIAL_LINK_MTU; +} + + + + + diff --git a/samples/authentication/auth_samples.rst b/samples/authentication/auth_samples.rst new file mode 100644 index 000000000000..563dd2d8784d --- /dev/null +++ b/samples/authentication/auth_samples.rst @@ -0,0 +1,15 @@ +.. auth-samples: + +Authentication Samples +###################### + +The following samples show how to use the Authentication library over Bluetooth or Serial transport. + + +.. toctree:: + :maxdepth: 1 + :glob: + + **/* + + diff --git a/samples/authentication/bluetooth/README.rst b/samples/authentication/bluetooth/README.rst new file mode 100644 index 000000000000..2bc0720eb1f5 --- /dev/null +++ b/samples/authentication/bluetooth/README.rst @@ -0,0 +1,28 @@ +.. _auth_bluetooth-sample: + +Authentication over Bluetooth +############################# + +Overview +******** + +There are two Bluetooth firmware applications, central and peripheral. The Central acts as +the client, the peripheral acts as the server. The Central initiates the authentication +messages. + +The authentication method, DTLS or Challenge-Response, is configurable via KConfig menu. + +Building and Running +-------------------- +This sample was developed and tested with two Nordic nRF52840 dev +kits (see: https://www.nordicsemi.com/Software-and-Tools/Development-Kits/nRF52840-DK). Two Ubuntu +VMs were used, one running the Central the other VM running the Peripheral. + +To build the peripheral with DTLS:|br| +cmake -Bbuild_auth_peripheral -DBOARD=nrf52840dk_nrf52840 -DCONF_FILE=dtls.prj.conf samples/authentication/bluetooth/peripheral_auth + +To build the central with DLTS using West:|br| +west build -d build_auth_central -b nrf52840dk_nrf52840 -- -DCONF_FILE=dtls.prj.conf samples/authentication/bluetooth/central_auth |br| +(note: Two dashes '-' after 'west build -b build_auth_central -b nrf52840dk_nrf52840 --', necessary for -DCONF_FILE define) + + diff --git a/samples/authentication/bluetooth/central_auth/CMakeLists.txt b/samples/authentication/bluetooth/central_auth/CMakeLists.txt new file mode 100644 index 000000000000..84226beaad08 --- /dev/null +++ b/samples/authentication/bluetooth/central_auth/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(central_auth) + +target_include_directories(app PUBLIC ${CMAKE_SOURCE_DIR}) + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/authentication/bluetooth/central_auth/dtls.prj.conf b/samples/authentication/bluetooth/central_auth/dtls.prj.conf new file mode 100644 index 000000000000..1416fa23be40 --- /dev/null +++ b/samples/authentication/bluetooth/central_auth/dtls.prj.conf @@ -0,0 +1,81 @@ +CONFIG_BT=y +CONFIG_BT_DEBUG_LOG=y +CONFIG_BT_SMP=y +CONFIG_BT_SIGNING=y +CONFIG_BT_DIS=y +CONFIG_BT_ATT_PREPARE_COUNT=5 +CONFIG_BT_PRIVACY=y +CONFIG_BT_DEVICE_NAME="Zephyr Central Auth" +CONFIG_BT_DEVICE_APPEARANCE=833 +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=65 +CONFIG_BT_RX_BUF_LEN=1600 +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_PERIPHERAL=n +CONFIG_BT_MAX_PAIRED=2 +CONFIG_BT_MAX_CONN=2 + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +# If not using DTLS, then the main stack size can be reduced to 1024 bytes +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_AUTH_LIB=y +CONFIG_BT_XPORT=y +CONFIG_AUTH_DTLS=y + +# logging +CONFIG_LOG=y +CONFIG_AUTH_LOG_LEVEL=0 +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y + +# The assumes the board has a hardware based random +# number generator. +CONFIG_ENTROPY_DEVICE_RANDOM_GENERATOR=y + + +# Mbed config'CONFIG_MBEDTLS=y +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h" +CONFIG_MBEDTLS_TLS_VERSION_1_2=y +CONFIG_MBEDTLS_DTLS=y +CONFIG_MBEDTLS_ENTROPY_ENABLED=y + +# Supported key exchange modes +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED=y + +# Supported elliptic curves +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y + +# Supported cipher modes +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y +CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y +CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y + + + +# Supported message authentication methods +CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y +CONFIG_MBEDTLS_MAC_CMAC_ENABLED=y + + +# Other configurations +CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1500 +CONFIG_MBEDTLS_DEBUG=y +CONFIG_MBEDTLS_DEBUG_LEVEL=0 +CONFIG_MBEDTLS_ENABLE_HEAP=y + +# Mbed uses a chunk of memory, it might be possible to reduce +# this heap usage. +CONFIG_MBEDTLS_HEAP_SIZE=65535 +CONFIG_APP_LINK_WITH_MBEDTLS=y + + + diff --git a/samples/authentication/bluetooth/central_auth/prj.conf b/samples/authentication/bluetooth/central_auth/prj.conf new file mode 100644 index 000000000000..cfe030738af5 --- /dev/null +++ b/samples/authentication/bluetooth/central_auth/prj.conf @@ -0,0 +1,38 @@ +CONFIG_BT=y +CONFIG_BT_DEBUG_LOG=y +CONFIG_BT_SMP=y +CONFIG_BT_SIGNING=y +CONFIG_BT_PERIPHERAL=n +CONFIG_BT_DIS=y +CONFIG_BT_ATT_PREPARE_COUNT=5 +CONFIG_BT_PRIVACY=y +CONFIG_BT_DEVICE_NAME="Zephyr Central Auth" +CONFIG_BT_DEVICE_APPEARANCE=833 +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=65 +CONFIG_BT_RX_BUF_LEN=1600 +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_MAX_PAIRED=2 +CONFIG_BT_MAX_CONN=2 + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +# If not using DTLS, then the main stack size can be reduced to 1024 bytes +CONFIG_MAIN_STACK_SIZE=1024 +CONFIG_AUTH_LIB=y +CONFIG_BT_XPORT=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y + +# logging +CONFIG_LOG=y +CONFIG_AUTH_LOG_LEVEL=0 +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y + diff --git a/samples/authentication/bluetooth/central_auth/sample.yaml b/samples/authentication/bluetooth/central_auth/sample.yaml new file mode 100644 index 000000000000..3e1e7090afbb --- /dev/null +++ b/samples/authentication/bluetooth/central_auth/sample.yaml @@ -0,0 +1,8 @@ +sample: + description: Auth central sample app + name: Auth Central +tests: + sample.central_auth: + harness: bluetooth + platform_allow: qemu_x86 + tags: bluetooth authentication diff --git a/samples/authentication/bluetooth/central_auth/src/main.c b/samples/authentication/bluetooth/central_auth/src/main.c new file mode 100644 index 000000000000..b738c6f3a06c --- /dev/null +++ b/samples/authentication/bluetooth/central_auth/src/main.c @@ -0,0 +1,584 @@ + +/* main.c - Application main entry point + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#if defined(CONFIG_AUTH_DTLS) +#include "../../../certs/auth_certs.h" +#endif + + +LOG_MODULE_REGISTER(central_auth, CONFIG_AUTH_LOG_LEVEL); + + +/** + * Declare UUIDs used for the Authentication service and characteristics + * see auth_xport.h + */ + +static struct bt_uuid_128 auth_service_uuid = AUTH_SERVICE_UUID; + +static struct bt_uuid_128 auth_client_char = AUTH_SVC_CLIENT_CHAR_UUID; + +static struct bt_uuid_128 auth_server_char = AUTH_SVC_SERVER_CHAR; + + + +#if defined(CONFIG_AUTH_DTLS) +/* The Root and Intermediate Certs in a single CA chain. + * plus the server cert. All in PEM format.*/ +static const uint8_t auth_cert_ca_chain[] = AUTH_ROOTCA_CERT_PEM AUTH_INTERMEDIATE_CERT_PEM; +static const uint8_t auth_dev_client_cert[] = AUTH_CLIENT_CERT_PEM; +static const uint8_t auth_client_privatekey[] = AUTH_CLIENT_PRIVATE_KEY_PEM; + +static struct auth_optional_param dtls_certs_param = { + .param_id = AUTH_DTLS_PARAM, + .param_body = { + .dtls_certs = { + .server_ca_chain_pem = { + .cert = auth_cert_ca_chain, + .cert_size = sizeof(auth_cert_ca_chain), + }, + + .device_cert_pem = { + .cert = auth_dev_client_cert, + .cert_size = sizeof(auth_dev_client_cert), + .priv_key = auth_client_privatekey, + .priv_key_size = sizeof(auth_client_privatekey) + } + } + } +}; +#endif + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) +#define NEW_SHARED_KEY_LEN (32u) + +/* Use a different key than default */ +static uint8_t chal_resp_sharedkey[NEW_SHARED_KEY_LEN] = { + 0x21, 0x8e, 0x37, 0x42, 0x1e, 0xe1, 0x2a, 0x22, 0x7c, 0x4b, 0x3f, 0x3f, 0x07, 0x5e, 0x8a, 0xd8, + 0x24, 0xdf, 0xca, 0xf4, 0x04, 0xd0, 0x3e, 0x22, 0x61, 0x9f, 0x24, 0xa3, 0xc7, 0xf6, 0x5d, 0x66 +}; + +static struct auth_optional_param chal_resp_param = { + .param_id = AUTH_CHALRESP_PARAM, + .param_body = { + .chal_resp = { + .shared_key = chal_resp_sharedkey, + }, + } +}; +#endif + +static struct bt_conn *default_conn; + +/* UUID to discover */ +static struct bt_gatt_discover_params discover_params; +static struct bt_gatt_subscribe_params subscribe_params; + + + +/** + * Authentication connect struct + */ +static struct authenticate_conn central_auth_conn; + + + +/* Auth service, client descriptor, server descriptor */ +#define AUTH_SVC_GATT_COUNT (4u) + +#define AUTH_SVC_INDEX (0u) +#define AUTH_SVC_CLIENT_CHAR_INDEX (1u) + +/* for enable/disable of notification */ +#define AUTH_SVC_CLIENT_CCC_INDEX (2u) +#define AUTH_SVC_SERVER_CHAR_INDEX (3u) + +/** + * Used to store Authentication GATT service and characteristics. + */ +typedef struct { + const struct bt_uuid *uuid; + const struct bt_gatt_attr *attr; + uint16_t handle; + uint16_t value_handle; + uint8_t permissions; /* Bitfields from: BT_GATT_PERM_NONE, in gatt.h */ + const uint32_t gatt_disc_type; +} auth_svc_gatt_t; + + +static uint32_t auth_desc_index; + + +/* Table content should match indexes above */ +static auth_svc_gatt_t auth_svc_gatt_tbl[AUTH_SVC_GATT_COUNT] = { + { (const struct bt_uuid *)&auth_service_uuid, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_PRIMARY }, /* AUTH_SVC_INDEX */ + { (const struct bt_uuid *)&auth_client_char, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_CHARACTERISTIC }, /* AUTH_SVC_CLIENT_CHAR_INDEX */ + { BT_UUID_GATT_CCC, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_DESCRIPTOR }, /*AUTH_SVC_CLIENT_CCC_INDEX CCC for Client char */ + { (const struct bt_uuid *)&auth_server_char, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_CHARACTERISTIC } /* AUTH_SVC_SERVER_CHAR_INDEX */ +}; + + +/** + * Params used to change the connection MTU length. + */ +struct bt_gatt_exchange_params mtu_parms; + +void mtu_change_cb(struct bt_conn *conn, uint8_t err, + struct bt_gatt_exchange_params *params) +{ + if (err) { + LOG_ERR("Failed to set MTU, err: %d", err); + } else { + LOG_DBG("Successfully set MTU to: %d", bt_gatt_get_mtu(conn)); + } +} + + +/** + * Characteristic discovery function + * + * + * @param conn Bluetooth conection. + * @param attr Discovered attribute. NOTE: This pointer will go out fo scope + * do not save pointer for future use. + * @param params Discover params. + * + * @return BT_GATT_ITER_STOP + */ +static uint8_t discover_func(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + int err; + + if (!attr) { + LOG_INF("Discover complete, NULL attribute."); + (void)memset(params, 0, sizeof(*params)); + return BT_GATT_ITER_STOP; + } + + + /* debug output */ + LOG_DBG("====auth_desc_index is: %d=====", auth_desc_index); + LOG_DBG("[ATTRIBUTE] handle 0x%x", attr->handle); + LOG_DBG("[ATTRIBUTE] value handle 0x%x", bt_gatt_attr_value_handle(attr)); + + /* let's get string */ + char uuid_str[50]; + bt_uuid_to_str(attr->uuid, uuid_str, sizeof(uuid_str)); + LOG_DBG("Attribute UUID: %s", log_strdup(uuid_str)); + + /* print attribute UUID */ + bt_uuid_to_str(discover_params.uuid, uuid_str, sizeof(uuid_str)); + LOG_DBG("Discovery UUID: %s", log_strdup(uuid_str)); + + + /** + * Verify the correct UUID was found + */ + if (bt_uuid_cmp(discover_params.uuid, auth_svc_gatt_tbl[auth_desc_index].uuid)) { + + /* Failed, not the UUID we're expecting */ + LOG_ERR("Error Unknown UUID."); + return BT_GATT_ITER_STOP; + } + + /* save off GATT info */ + auth_svc_gatt_tbl[auth_desc_index].attr = NULL; /* NOTE: attr var not used for the Central */ + auth_svc_gatt_tbl[auth_desc_index].handle = attr->handle; + auth_svc_gatt_tbl[auth_desc_index].value_handle = bt_gatt_attr_value_handle(attr); + auth_svc_gatt_tbl[auth_desc_index].permissions = attr->perm; + + auth_desc_index++; + + /* Are all of the characteristics discovered? */ + if (auth_desc_index >= AUTH_SVC_GATT_COUNT) { + + /* we're done */ + LOG_INF("Discover complete"); + + /* save off the server attribute handle */ + + /* setup the subscribe params + * Value handle for the Client characteristic for indication of + * peripheral data. + */ + subscribe_params.notify = auth_xp_bt_central_notify; + subscribe_params.value = BT_GATT_CCC_NOTIFY; + subscribe_params.value_handle = + auth_svc_gatt_tbl[AUTH_SVC_CLIENT_CHAR_INDEX].value_handle; + + /* Handle for the CCC descriptor itself */ + subscribe_params.ccc_handle = + auth_svc_gatt_tbl[AUTH_SVC_CLIENT_CCC_INDEX].handle; + + err = bt_gatt_subscribe(conn, &subscribe_params); + if (err && err != -EALREADY) { + LOG_ERR("Subscribe failed (err %d)", err); + } + + /* Get the server BT characteristic, the central sends data to this characteristic */ + uint16_t server_char_handle = + auth_svc_gatt_tbl[AUTH_SVC_SERVER_CHAR_INDEX].value_handle; + + /* setup the BT transport params */ + struct auth_xp_bt_params xport_params = + { .conn = conn, .is_central = true, + .server_char_hdl = server_char_handle }; + + err = auth_xport_init(¢ral_auth_conn.xport_hdl, + central_auth_conn.instance, + AUTH_XP_TYPE_BLUETOOTH, &xport_params); + + if (err) { + LOG_ERR("Failed to initialize Bluetooth transport, err: %d", err); + return BT_GATT_ITER_STOP; + } + + /* Start auth process */ + err = auth_lib_start(¢ral_auth_conn); + if (err) { + LOG_ERR("Failed to start auth service, err: %d", err); + } else { + LOG_INF("Started auth service."); + } + + return BT_GATT_ITER_STOP; + } + + /* set up the next discovery params */ + discover_params.uuid = auth_svc_gatt_tbl[auth_desc_index].uuid; + discover_params.start_handle = attr->handle + 1; + discover_params.type = auth_svc_gatt_tbl[auth_desc_index].gatt_disc_type; + + + /* Start discovery */ + err = bt_gatt_discover(conn, &discover_params); + if (err) { + LOG_ERR("Discover failed (err %d)", err); + } + + + return BT_GATT_ITER_STOP; +} + +/** + * Connected to the peripheral device + * + * @param conn The Bluetooth connection. + * @param conn_err Connection error, 0 == no error + */ +static void connected(struct bt_conn *conn, uint8_t conn_err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + int err; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (conn_err) { + LOG_ERR("Failed to connect to %s (%u)", log_strdup(addr), conn_err); + return; + } + + LOG_INF("Connected: %s", log_strdup(addr)); + + if (conn == default_conn) { + + /* set the max MTU, only for GATT interface */ + mtu_parms.func = mtu_change_cb; + bt_gatt_exchange_mtu(conn, &mtu_parms); + + /* reset gatt discovery index */ + auth_desc_index = 0; + + discover_params.uuid = auth_svc_gatt_tbl[auth_desc_index].uuid; + discover_params.func = discover_func; + discover_params.start_handle = 0x0001; + discover_params.end_handle = 0xffff; + discover_params.type = auth_svc_gatt_tbl[auth_desc_index].gatt_disc_type; + + /** + * Discover characteristics for the service + */ + err = bt_gatt_discover(default_conn, &discover_params); + if (err) { + LOG_ERR("Discover failed(err %d)", err); + return; + } + } +} + +/** + * Parse through the BLE adv data, looking for our service + * + * @param data Bluetooth data + * @param user_data User data. + * + * @return true on success, else false + */ +static bool bt_adv_data_found(struct bt_data *data, void *user_data) +{ + bt_addr_le_t *addr = user_data; + int err; + struct bt_uuid_128 auth_uuid; + + LOG_DBG("[AD]: %u data_len %u", data->type, data->data_len); + + if (data->type == BT_DATA_UUID128_ALL) { + + if (data->data_len != BT_UUID_SIZE_128) { + LOG_WRN("AD malformed"); + return false; + } + + /* build full UUID to check */ + auth_uuid.uuid.type = BT_UUID_TYPE_128; + memcpy(auth_uuid.val, data->data, BT_UUID_SIZE_128); + + + /** + * Is this the service we're looking for? If not continue + * else stop the scan and connect to the device. + */ + if (bt_uuid_cmp((const struct bt_uuid *)&auth_uuid, + (const struct bt_uuid *)&auth_service_uuid)) { + return true; + } + + /* stop scanning, we've found the service */ + err = bt_le_scan_stop(); + if (err) { + LOG_ERR("Stop LE scan failed (err %d)", err); + return false; + } + + /** + * @brief Connect to the device + */ + struct bt_conn_le_create_param param = BT_CONN_LE_CREATE_PARAM_INIT( + BT_CONN_LE_OPT_NONE, + BT_GAP_SCAN_FAST_INTERVAL, + BT_GAP_SCAN_FAST_INTERVAL); + + if (bt_conn_le_create(addr, ¶m, BT_LE_CONN_PARAM_DEFAULT, + &default_conn)) { + LOG_ERR("Failed to create BLE connection to peripheral."); + } + + return false; + } + + return true; +} + +/** + * Found a device when scanning. + * + * @param addr BT address + * @param rssi Signal strength + * @param type Device type + * @param ad Simple buffer. + */ +static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + char dev[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(addr, dev, sizeof(dev)); + LOG_DBG("[DEVICE]: %s, AD evt type %u, AD data len %u, RSSI %i", + log_strdup(dev), type, ad->len, rssi); + + /* We're only interested in connectable events */ + if (type == BT_HCI_ADV_IND || type == BT_HCI_ADV_DIRECT_IND) { + bt_data_parse(ad, bt_adv_data_found, (void *)addr); + } +} + +/** + * BT disconnect callback. + * + * @param conn The Bluetooth connection. + * @param reason Disconnect reason. + */ +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + LOG_INF("Disconnected: %s (reason 0x%02x)", log_strdup(addr), reason); + + if (default_conn != conn) { + return; + } + + /* de init transport */ + /* Send disconnect event to BT transport. */ + struct auth_xport_evt conn_evt = { .event = XP_EVT_DISCONNECT }; + auth_xport_event(central_auth_conn.xport_hdl, &conn_evt); + + /* Deinit lower transport */ + auth_xport_deinit(central_auth_conn.xport_hdl); + central_auth_conn.xport_hdl = NULL; + + bt_conn_unref(default_conn); + default_conn = NULL; +} + +/** + * (dis)Connection callbacks + */ +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + + +/** + * Authentication status callback. + * + * @param auth_conn Authentication connection. + * @param instance Instance ID. + * @param status Auth status. + * @param context Optional context. + */ +static void auth_status(struct authenticate_conn *auth_conn, enum auth_instance_id instance, + enum auth_status status, void *context) +{ + /* display status */ + printk("Authentication instance (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + +#if defined(CONFIG_AUTH_DTLS) + if (status == AUTH_STATUS_IN_PROCESS) { + printk(" DTLS may take 30-60 seconds.\n"); + } +#endif +} + +/** + * Process log messages. + */ +static void process_log_msgs(void) +{ + while (log_process(false)) { + ; /* intentionally empty statement */ + } +} + +/** + * Idle process for app. + */ +static void idle_function(void) +{ + /* Just spin while the BT modules handle the connection */ + while (true) { + + process_log_msgs(); + + /* Let the handshake thread run */ + k_yield(); + } +} + + +/** + * Central main entry point. + */ +void main(void) +{ + int err = 0; + struct auth_optional_param *opt_parms = NULL; + + log_init(); + + LOG_INF("Client Auth started."); + + + uint32_t flags = AUTH_CONN_CLIENT; + +#if defined(CONFIG_AUTH_DTLS) && defined(CONFIG_AUTH_CHALLENGE_RESPONSE) +#error Invalid authentication config, either DTLS or Challenge-Response, not both. +#endif + +#if defined(CONFIG_AUTH_DTLS) + flags |= AUTH_CONN_DTLS_AUTH_METHOD; + + /* set TLS certs */ + opt_parms = &dtls_certs_param; + printk("Using DTLS authentication method.\n"); +#endif + + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) + flags |= AUTH_CONN_CHALLENGE_AUTH_METHOD; + + /* Use different shared key */ + opt_parms = &chal_resp_param; + printk("Using Challenge-Response authentication method.\n"); +#endif + + + err = auth_lib_init(¢ral_auth_conn, AUTH_INST_1_ID, auth_status, + NULL, opt_parms, flags); + + if (err) { + LOG_ERR("Failed to init authentication service, err: %d.", err); + idle_function(); /* does not return */ + } + + /** + * @brief Enable the Bluetooth module. Passing NULL to bt_enable + * will block while the BLE stack is initialized. + * Enable bluetooth module + */ + err = bt_enable(NULL); + if (err) { + LOG_ERR("Failed to enable the bluetooth module, err: %d", err); + idle_function(); /* does not return */ + } + + /* Register connect/disconnect callbacks */ + bt_conn_cb_register(&conn_callbacks); + + printk("Starting BLE scanning\n"); + + /* start scanning for peripheral */ + err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, device_found); + + if (err) { + printk("Scanning failed to start (err %d)\n", err); + } + + /* does not return */ + idle_function(); + + /* should not reach here */ + +} diff --git a/samples/authentication/bluetooth/peripheral_auth/CMakeLists.txt b/samples/authentication/bluetooth/peripheral_auth/CMakeLists.txt new file mode 100644 index 000000000000..5d88823fd65f --- /dev/null +++ b/samples/authentication/bluetooth/peripheral_auth/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(periph_auth) + +target_include_directories(app PUBLIC ${CMAKE_SOURCE_DIR}) + + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/authentication/bluetooth/peripheral_auth/dtls.prj.conf b/samples/authentication/bluetooth/peripheral_auth/dtls.prj.conf new file mode 100644 index 000000000000..03992cb768e5 --- /dev/null +++ b/samples/authentication/bluetooth/peripheral_auth/dtls.prj.conf @@ -0,0 +1,76 @@ +CONFIG_BT=y +CONFIG_BT_DEBUG_LOG=y +CONFIG_BT_SMP=y +CONFIG_BT_SIGNING=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_DIS=y +CONFIG_BT_ATT_PREPARE_COUNT=5 +CONFIG_BT_PRIVACY=y +CONFIG_BT_DEVICE_NAME="Zephyr Peripheral Auth" +CONFIG_BT_DEVICE_APPEARANCE=833 +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=65 +CONFIG_BT_RX_BUF_LEN=1600 + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +# If not using DTLS, then the main stack size can be reduced to 1024 bytes +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_AUTH_LIB=y +CONFIG_BT_XPORT=y +CONFIG_AUTH_DTLS=y + +CONFIG_AUTH_LOG_LEVEL=0 + +# logging +CONFIG_LOG=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y + +# The assumes the board has a hardware based random +# number generator. +CONFIG_ENTROPY_DEVICE_RANDOM_GENERATOR=y + +# Mbed config'CONFIG_MBEDTLS=y +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h" +CONFIG_MBEDTLS_TLS_VERSION_1_2=y +CONFIG_MBEDTLS_DTLS=y +CONFIG_MBEDTLS_ENTROPY_ENABLED=y + +# Supported key exchange modes +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED=y + +# Supported elliptic curves +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y + +# Supported cipher modes +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y +CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y +CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y + + +# Supported message authentication methods +CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y +CONFIG_MBEDTLS_MAC_CMAC_ENABLED=y + + +# Other configurations +CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1500 +CONFIG_MBEDTLS_DEBUG=y +CONFIG_MBEDTLS_DEBUG_LEVEL=0 +CONFIG_MBEDTLS_ENABLE_HEAP=y + +# Mbed uses a chunk of memory, it might be possible to reduce +# this heap usage. +CONFIG_MBEDTLS_HEAP_SIZE=65535 +CONFIG_APP_LINK_WITH_MBEDTLS=y + + + diff --git a/samples/authentication/bluetooth/peripheral_auth/prj.conf b/samples/authentication/bluetooth/peripheral_auth/prj.conf new file mode 100644 index 000000000000..78ca73f07904 --- /dev/null +++ b/samples/authentication/bluetooth/peripheral_auth/prj.conf @@ -0,0 +1,38 @@ +CONFIG_BT=y +CONFIG_BT_DEBUG_LOG=y +CONFIG_BT_SMP=y +CONFIG_BT_SIGNING=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_DIS=y +CONFIG_BT_ATT_PREPARE_COUNT=5 +CONFIG_BT_PRIVACY=y +CONFIG_BT_DEVICE_NAME="Zephyr Peripheral Auth" +CONFIG_BT_DEVICE_APPEARANCE=833 +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=65 +CONFIG_BT_RX_BUF_LEN=1600 + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +# If not using DTLS, then the main stack size can be reduced to 1024 bytes +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_AUTH_LIB=y +CONFIG_BT_XPORT=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_LOG=y +CONFIG_AUTH_LOG_LEVEL=3 +CONFIG_AUTH_CHALLENGE_RESPONSE=y + +# logging +CONFIG_LOG=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y + + + + diff --git a/samples/authentication/bluetooth/peripheral_auth/sample.yaml b/samples/authentication/bluetooth/peripheral_auth/sample.yaml new file mode 100644 index 000000000000..3efb45478de8 --- /dev/null +++ b/samples/authentication/bluetooth/peripheral_auth/sample.yaml @@ -0,0 +1,8 @@ +sample: + description: Auth peripheral sample app + name: Auth Peripheral +tests: + sample.peripheral_auth: + harness: bluetooth + platform_allow: qemu_x86 + tags: bluetooth authentication \ No newline at end of file diff --git a/samples/authentication/bluetooth/peripheral_auth/src/main.c b/samples/authentication/bluetooth/peripheral_auth/src/main.c new file mode 100644 index 000000000000..da66c91600ca --- /dev/null +++ b/samples/authentication/bluetooth/peripheral_auth/src/main.c @@ -0,0 +1,380 @@ +/* + * Sample authentication BLE peripheral + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + + +LOG_MODULE_REGISTER(periph_auth, CONFIG_AUTH_LOG_LEVEL); + + + +/** + * Declare UUIDs used for the Authentication service and characteristics + */ + +static struct bt_uuid_128 auth_service_uuid = AUTH_SERVICE_UUID; + +static struct bt_uuid_128 auth_client_char = AUTH_SVC_CLIENT_CHAR_UUID; + +static struct bt_uuid_128 auth_server_char = AUTH_SVC_SERVER_CHAR; + + + +#if defined(CONFIG_AUTH_DTLS) +#include "../../../certs/auth_certs.h" +#endif + + +static void client_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value); + +/* AUTH Service Declaration */ +BT_GATT_SERVICE_DEFINE(auth_svc, + BT_GATT_PRIMARY_SERVICE(&auth_service_uuid), + + /** + * Central (client role) bt_gatt_write() ---> server characteristic --> bt_gatt_read() Peripheral (server role) + * + * Central <--- Notification (client characteristic) <--- Peripheral + * + */ + + /** + * Client characteristic, used by the peripheral (server role) to write messages authentication messages + * to the central (client role). The peripheral needs to alert the central a message is + * ready to be read. + */ + BT_GATT_CHARACTERISTIC((const struct bt_uuid*)&auth_client_char, BT_GATT_CHRC_INDICATE, + (BT_GATT_PERM_READ|BT_GATT_PERM_WRITE), NULL, NULL, NULL), + BT_GATT_CCC(client_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + + /** + * Server characteristic, used by the central (client role) to write authentication messages to. + * to the server (peripheral) + */ + BT_GATT_CHARACTERISTIC((const struct bt_uuid*)&auth_server_char, BT_GATT_CHRC_WRITE, + (BT_GATT_PERM_READ|BT_GATT_PERM_WRITE), NULL, auth_xp_bt_central_write, NULL), +); + + + +struct bt_conn *default_conn; + +static bool is_connected = false; + +static struct authenticate_conn auth_conn; + +#if defined(CONFIG_AUTH_DTLS) +/* The Root and Intermediate Certs in a single CA chain. + * plus the server cert. All in PEM format.*/ +static const uint8_t auth_cert_ca_chain[] = AUTH_ROOTCA_CERT_PEM AUTH_INTERMEDIATE_CERT_PEM; +static const uint8_t auth_dev_server_cert[] = AUTH_SERVER_CERT_PEM; +static const uint8_t auth_server_privatekey[] = AUTH_SERVER_PRIVATE_KEY_PEM; + + +static struct auth_optional_param dtls_certs_param = { + .param_id = AUTH_DTLS_PARAM, + .param_body = { + .dtls_certs = { + .server_ca_chain_pem = { + .cert = auth_cert_ca_chain, + .cert_size = sizeof(auth_cert_ca_chain), + }, + + .device_cert_pem = { + .cert = auth_dev_server_cert, + .cert_size = sizeof(auth_dev_server_cert), + .priv_key = auth_server_privatekey, + .priv_key_size = sizeof(auth_server_privatekey) + } + } + } +}; +#endif + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) + +#define NEW_SHARED_KEY_LEN (32u) + +/* Use a different key than default */ +static uint8_t chal_resp_sharedkey[NEW_SHARED_KEY_LEN] = { + 0x21, 0x8e, 0x37, 0x42, 0x1e, 0xe1, 0x2a, 0x22, 0x7c, 0x4b, 0x3f, 0x3f, 0x07, 0x5e, 0x8a, 0xd8, + 0x24, 0xdf, 0xca, 0xf4, 0x04, 0xd0, 0x3e, 0x22, 0x61, 0x9f, 0x24, 0xa3, 0xc7, 0xf6, 0x5d, 0x66 +}; + + +static struct auth_optional_param chal_resp_param = { + .param_id = AUTH_CHALRESP_PARAM, + .param_body = { + .chal_resp = { + .shared_key = chal_resp_sharedkey, + }, + } +}; +#endif + +/** + * Set up the advertising data + */ +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + + /* auth service UUID */ + BT_DATA_BYTES(BT_DATA_UUID128_ALL, AUTH_SERVICE_UUID_BYTES), +}; + +/** + * Connection callback + * + * @param conn The Bluetooth connection. + * @param err Error value, 0 == success + */ +static void connected(struct bt_conn *conn, uint8_t err) +{ + int ret; + struct auth_xport_evt conn_evt; + + if (err) { + printk("Connection failed (err 0x%02x)\n", err); + } else { + default_conn = bt_conn_ref(conn); + printk("Connected\n"); + + struct auth_xp_bt_params xport_param = { .conn = conn, .is_central = false, + .client_attr = &auth_svc.attrs[1] }; + + ret = auth_xport_init(&auth_conn.xport_hdl, auth_conn.instance, + AUTH_XP_TYPE_BLUETOOTH, &xport_param); + + if(ret) { + printk("Failed to initialize BT transport, err: %d", ret); + return; + } + + is_connected = true; + + /* send connection event to BT transport */ + conn_evt.event = XP_EVT_CONNECT; + auth_xport_event(auth_conn.xport_hdl, &conn_evt); + + /* Start authentication */ + ret = auth_lib_start(&auth_conn); + + if(ret) { + printk("Failed to start authentication, err: %d\n", ret); + } + } +} + +/** + * The disconnect callback. + * + * @param conn Bluetooth connection struct + * @param reason Disconnect reason code. + */ +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + struct auth_xport_evt conn_evt; + + printk("Disconnected (reason 0x%02x)\n", reason); + + is_connected = false; + + /* Send disconnect event to BT transport. */ + conn_evt.event = XP_EVT_DISCONNECT; + auth_xport_event(auth_conn.xport_hdl, &conn_evt); + + /* Deinit lower transport */ + auth_xport_deinit(auth_conn.xport_hdl); + auth_conn.xport_hdl = NULL; + + if (default_conn) { + bt_conn_unref(default_conn); + default_conn = NULL; + } +} + +/** + * Connect callbacks + */ +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +/** + * If the pairing was canceled. + * + * @param conn The Bluetooth connection. + */ +static void auth_cancel(struct bt_conn *conn) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + printk("Pairing cancelled: %s\n", addr); +} + +static struct bt_conn_auth_cb auth_cb_display = { + .cancel = auth_cancel, +}; + +/** + * Called after the BT module has initialized or not (error occurred). + * + * @param err Error code, 0 == success + */ +static void bt_ready(int err) +{ + if (err) { + printk("Bluetooth init failed (err %d)\n", err); + return; + } + + printk("Bluetooth initialized\n"); + + /* Start advertising after BT module has initialized OK */ + err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + printk("Advertising failed to start (err %d)\n", err); + return; + } + + printk("Advertising successfully started\n"); +} + +/** + * Authentication status callback + * + * @param auth_conn The authentication connection. + * @param instance Instance ID. + * @param status Status + * @param context Optional context. + */ +static void auth_status(struct authenticate_conn *auth_conn, enum auth_instance_id instance, + enum auth_status status, void *context) +{ + /* display status */ + printk("Authentication instance (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + +#if defined(CONFIG_AUTH_DTLS) + if(status == AUTH_STATUS_IN_PROCESS) { + printk(" DTLS may take 30-60 seconds.\n"); + } +#endif +} + + +/** + * Called when client notification is (dis)enabled by the Central + * + * @param attr GATT attribute. + * @param value BT_GATT_CCC_NOTIFY if changes are notified. + */ +static void client_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + + bool notif_enabled = (value == BT_GATT_CCC_NOTIFY) ? true : false; + + LOG_INF("Client notifications %s", notif_enabled ? "enabled" : "disabled"); +} + +/** + * Process log messages + */ +static void process_log_msgs(void) +{ + while(log_process(false)) { + ; /* intentionally empty statement */ + } +} + +/** + * Handle idle, never returns + */ +static void idle_function(void) +{ + while(true) { + + process_log_msgs(); + + /* give the handshake thread a chance to run */ + k_yield(); + } +} + +void main(void) +{ + int err = 0; + struct auth_optional_param *opt_parms = NULL; + + log_init(); + + uint32_t auth_flags = AUTH_CONN_SERVER; + +#if defined(CONFIG_AUTH_DTLS) + auth_flags |= AUTH_CONN_DTLS_AUTH_METHOD; + + /* Add certificates to authentication instance.*/ + opt_parms = &dtls_certs_param; + + printk("Using DTLS authentication method.\n"); +#endif + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) + auth_flags |= AUTH_CONN_CHALLENGE_AUTH_METHOD; + + /* Use different shared key */ + opt_parms = &chal_resp_param; + + printk("Using Challenge-Response authentication method.\n"); +#endif + + err = auth_lib_init(&auth_conn, AUTH_INST_1_ID, auth_status, NULL, + opt_parms, auth_flags); + + if(err){ + printk("Failed to init authentication service.\n"); + return; + } + + err = bt_enable(bt_ready); + if (err) { + printk("Bluetooth init failed (err %d)\n", err); + idle_function(); + } + + bt_conn_cb_register(&conn_callbacks); + bt_conn_auth_cb_register(&auth_cb_display); + + printk("Peripheral Auth started\n"); + + /* Never returns */ + idle_function(); + +} + diff --git a/samples/authentication/certs/README.rst b/samples/authentication/certs/README.rst new file mode 100644 index 000000000000..2b36e22221de --- /dev/null +++ b/samples/authentication/certs/README.rst @@ -0,0 +1,14 @@ +.. _auth_certs-sample: + +Test X.509 certs for DLTS Authentication examples +################################################# + +Overview +******** + +These test X.509 certificates are used with the DTLS authentication method. The private keys +for each certificate are located in the **keys** directory. + + + + diff --git a/samples/authentication/certs/auth_certs.h b/samples/authentication/certs/auth_certs.h new file mode 100644 index 000000000000..2744ea436124 --- /dev/null +++ b/samples/authentication/certs/auth_certs.h @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_AUTH_CERTS_H_ +#define ZEPHYR_INCLUDE_AUTH_CERTS_H_ + + +#define AUTH_ROOTCA_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\r\n" \ + "MIICeTCCAh+gAwIBAgIUPt+1qmulufx3ze5ZMsGpact9ekwwCgYIKoZIzj0EAwIw\r\n" \ + "gYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlT\r\n" \ + "YW4gRGllZ28xGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNB\r\n" \ + "dXRoZW50aWNhdGlvbiBURVNUMRcwFQYDVQQDDA5BdXRoIFJvb3QgVEVTVDAeFw0y\r\n" \ + "MDA4MjYxOTM5MDJaFw0zNDA1MDUxOTM5MDJaMIGJMQswCQYDVQQGEwJVUzETMBEG\r\n" \ + "A1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU2FuIERpZWdvMRowGAYDVQQKDBFB\r\n" \ + "dXRoIENvbXBhbnkgVGVzdDEcMBoGA1UECwwTQXV0aGVudGljYXRpb24gVEVTVDEX\r\n" \ + "MBUGA1UEAwwOQXV0aCBSb290IFRFU1QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\r\n" \ + "AATCxBhg/MkpI+6PfUg6NUdk03pOd8x0Q8qcJngr4Gje+AzQLFoJHN0NE+PaTf2k\r\n" \ + "ILZX1hT2l39oKFXxhQedPB/uo2MwYTAdBgNVHQ4EFgQU4wEI2+yUj/DsbPmqLakg\r\n" \ + "jsuWUKkwHwYDVR0jBBgwFoAU4wEI2+yUj/DsbPmqLakgjsuWUKkwDwYDVR0TAQH/\r\n" \ + "BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwIDSAAwRQIgT8TvcsCl\r\n" \ + "adPrqWv50U87LON0QMsaTAqvVV6tFd06knICIQCxH+oRA9l0NSiyHJuPHRuKn6a9\r\n" \ + "xfOTq1zTpr6fNu9vuw==\r\n" \ + "-----END CERTIFICATE-----\r\n" + + +#define AUTH_INTERMEDIATE_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\r\n" \ + "MIICXTCCAgOgAwIBAgICEAAwCgYIKoZIzj0EAwIwgYkxCzAJBgNVBAYTAlVTMRMw\r\n" \ + "EQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4gRGllZ28xGjAYBgNVBAoM\r\n" \ + "EUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNBdXRoZW50aWNhdGlvbiBURVNU\r\n" \ + "MRcwFQYDVQQDDA5BdXRoIFJvb3QgVEVTVDAeFw0yMDA4MjYyMDEyMjVaFw0zMTA4\r\n" \ + "MDkyMDEyMjVaMH0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRow\r\n" \ + "GAYDVQQKDBFBdXRoIENvbXBhbnkgVGVzdDEcMBoGA1UECwwTQXV0aGVudGljYXRp\r\n" \ + "b24gVEVTVDEfMB0GA1UEAwwWQXV0aCBJbnRlcm1lZGlhdGUgVEVTVDBZMBMGByqG\r\n" \ + "SM49AgEGCCqGSM49AwEHA0IABICgo5Ku11qZi4vFoTb2HmCShvvpsCM/0U3SDdqF\r\n" \ + "syBUT/cXdlYuqY+DdhM+GpQ0Qd4KNZEjwFWFEMXVH66gJgqjZjBkMB0GA1UdDgQW\r\n" \ + "BBQ2H3A6aIoNgXkAIVzaz+199JaFKjAfBgNVHSMEGDAWgBTjAQjb7JSP8Oxs+aot\r\n" \ + "qSCOy5ZQqTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAKBggq\r\n" \ + "hkjOPQQDAgNIADBFAiB3iPwLrBmMN6erAl+6w96pQJRH62R5s5T59nLP32d5QQIh\r\n" \ + "AMx3N8ijw8cE35Jg5szKy3hYgiN/6VTFEYADSdi737Si\r\n" \ + "-----END CERTIFICATE-----\r\n" + +#define AUTH_SERVER_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\r\n" \ + "MIICNjCCAd2gAwIBAgICIAEwCgYIKoZIzj0EAwIwfTELMAkGA1UEBhMCVVMxEzAR\r\n" \ + "BgNVBAgMCkNhbGlmb3JuaWExGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRww\r\n" \ + "GgYDVQQLDBNBdXRoZW50aWNhdGlvbiBURVNUMR8wHQYDVQQDDBZBdXRoIEludGVy\r\n" \ + "bWVkaWF0ZSBURVNUMB4XDTIwMDgyNjIwMzgzOFoXDTIxMDkwNTIwMzgzOFowgYsx\r\n" \ + "CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4g\r\n" \ + "RGllZ28xGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNBdXRo\r\n" \ + "ZW50aWNhdGlvbiBURVNUMRkwFwYDVQQDDBBBdXRoIFNlcnZlciBURVNUMFkwEwYH\r\n" \ + "KoZIzj0CAQYIKoZIzj0DAQcDQgAEuHrjl6aMJTYsMqOP6cdzQ22pJwBl3Cy698Co\r\n" \ + "SpH6Ek5NMTJbSphGi2QdSCaQFDcq+T5H51j9Ch6m7+sblSDupqM+MDwwCQYDVR0T\r\n" \ + "BAIwADAfBgNVHSMEGDAWgBQ2H3A6aIoNgXkAIVzaz+199JaFKjAOBgNVHQ8BAf8E\r\n" \ + "BAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgAtXzzTzbEc/5yI4AU6+cFoOwaf3RK7/E\r\n" \ + "8kOsxGiemNECICLojqGP5O/tb5F/xfIYjuGvrkdAlY2ykb1btv9DoMUg\r\n" \ + "-----END CERTIFICATE-----\r\n" + + +#define AUTH_CLIENT_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\r\n" \ + "MIICNzCCAd2gAwIBAgICIAAwCgYIKoZIzj0EAwIwfTELMAkGA1UEBhMCVVMxEzAR\r\n" \ + "BgNVBAgMCkNhbGlmb3JuaWExGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRww\r\n" \ + "GgYDVQQLDBNBdXRoZW50aWNhdGlvbiBURVNUMR8wHQYDVQQDDBZBdXRoIEludGVy\r\n" \ + "bWVkaWF0ZSBURVNUMB4XDTIwMDgyNjIwMzgxMFoXDTIxMDkwNTIwMzgxMFowgYsx\r\n" \ + "CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4g\r\n" \ + "RGllZ28xGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNBdXRo\r\n" \ + "ZW50aWNhdGlvbiBURVNUMRkwFwYDVQQDDBBBdXRoIENsaWVudCBURVNUMFkwEwYH\r\n" \ + "KoZIzj0CAQYIKoZIzj0DAQcDQgAEkwj+WKKVM3k9AEiS/efDhXoICq/e8rVKew2q\r\n" \ + "pun6zie8xATw7pyte1Hf59Ga2Vmti/QTvzczZrvj/m0LbKzmn6M+MDwwCQYDVR0T\r\n" \ + "BAIwADAfBgNVHSMEGDAWgBQ2H3A6aIoNgXkAIVzaz+199JaFKjAOBgNVHQ8BAf8E\r\n" \ + "BAMCB4AwCgYIKoZIzj0EAwIDSAAwRQIgfynezJlsshDJnyVVEC/twUHJAyhpIrEU\r\n" \ + "+qomFnsDB20CIQC9FxlDtKVrn4MOfI1yx9grPCN9ztwq1pRTLAh9LB/F9w==\r\n" \ + "-----END CERTIFICATE-----\r\n" + + +/** + * Sever and client private keys + */ + +#define AUTH_SERVER_PRIVATE_KEY_PEM \ + "-----BEGIN EC PRIVATE KEY-----\r\n" \ + "MHcCAQEEIP8LVrSz0CD/KvTTo5/qmOKIFtepGBTDG2odkSnYx/ssoAoGCCqGSM49\r\n" \ + "AwEHoUQDQgAEuHrjl6aMJTYsMqOP6cdzQ22pJwBl3Cy698CoSpH6Ek5NMTJbSphG\r\n" \ + "i2QdSCaQFDcq+T5H51j9Ch6m7+sblSDupg==\r\n" \ + "-----END EC PRIVATE KEY-----\r\n" + + +#define AUTH_CLIENT_PRIVATE_KEY_PEM \ + "-----BEGIN EC PRIVATE KEY-----\r\n" \ + "MHcCAQEEIJ87D6q+Z4ulXG2B9lQblbgCQh4xhMgImQpyKxUCtP3soAoGCCqGSM49\r\n" \ + "AwEHoUQDQgAEkwj+WKKVM3k9AEiS/efDhXoICq/e8rVKew2qpun6zie8xATw7pyt\r\n" \ + "e1Hf59Ga2Vmti/QTvzczZrvj/m0LbKzmnw==\r\n" \ + "-----END EC PRIVATE KEY-----\r\n" + +#endif // ZEPHYR_INCLUDE_AUTH_CERTS_H_ + + diff --git a/samples/authentication/certs/auth_dev_client.crt b/samples/authentication/certs/auth_dev_client.crt new file mode 100644 index 000000000000..3a6dabe7b5ef --- /dev/null +++ b/samples/authentication/certs/auth_dev_client.crt @@ -0,0 +1,48 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 8192 (0x2000) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=US, ST=California, O=Auth Company Test, OU=Authentication TEST, CN=Auth Intermediate TEST + Validity + Not Before: Aug 26 20:38:10 2020 GMT + Not After : Sep 5 20:38:10 2021 GMT + Subject: C=US, ST=California, L=San Diego, O=Auth Company Test, OU=Authentication TEST, CN=Auth Client TEST + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:93:08:fe:58:a2:95:33:79:3d:00:48:92:fd:e7: + c3:85:7a:08:0a:af:de:f2:b5:4a:7b:0d:aa:a6:e9: + fa:ce:27:bc:c4:04:f0:ee:9c:ad:7b:51:df:e7:d1: + 9a:d9:59:ad:8b:f4:13:bf:37:33:66:bb:e3:fe:6d: + 0b:6c:ac:e6:9f + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Authority Key Identifier: + keyid:36:1F:70:3A:68:8A:0D:81:79:00:21:5C:DA:CF:ED:7D:F4:96:85:2A + + X509v3 Key Usage: critical + Digital Signature + Signature Algorithm: ecdsa-with-SHA256 + 30:45:02:20:7f:29:de:cc:99:6c:b2:10:c9:9f:25:55:10:2f: + ed:c1:41:c9:03:28:69:22:b1:14:fa:aa:26:16:7b:03:07:6d: + 02:21:00:bd:17:19:43:b4:a5:6b:9f:83:0e:7c:8d:72:c7:d8: + 2b:3c:23:7d:ce:dc:2a:d6:94:53:2c:08:7d:2c:1f:c5:f7 +-----BEGIN CERTIFICATE----- +MIICNzCCAd2gAwIBAgICIAAwCgYIKoZIzj0EAwIwfTELMAkGA1UEBhMCVVMxEzAR +BgNVBAgMCkNhbGlmb3JuaWExGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRww +GgYDVQQLDBNBdXRoZW50aWNhdGlvbiBURVNUMR8wHQYDVQQDDBZBdXRoIEludGVy +bWVkaWF0ZSBURVNUMB4XDTIwMDgyNjIwMzgxMFoXDTIxMDkwNTIwMzgxMFowgYsx +CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4g +RGllZ28xGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNBdXRo +ZW50aWNhdGlvbiBURVNUMRkwFwYDVQQDDBBBdXRoIENsaWVudCBURVNUMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEkwj+WKKVM3k9AEiS/efDhXoICq/e8rVKew2q +pun6zie8xATw7pyte1Hf59Ga2Vmti/QTvzczZrvj/m0LbKzmn6M+MDwwCQYDVR0T +BAIwADAfBgNVHSMEGDAWgBQ2H3A6aIoNgXkAIVzaz+199JaFKjAOBgNVHQ8BAf8E +BAMCB4AwCgYIKoZIzj0EAwIDSAAwRQIgfynezJlsshDJnyVVEC/twUHJAyhpIrEU ++qomFnsDB20CIQC9FxlDtKVrn4MOfI1yx9grPCN9ztwq1pRTLAh9LB/F9w== +-----END CERTIFICATE----- diff --git a/samples/authentication/certs/auth_dev_server.crt b/samples/authentication/certs/auth_dev_server.crt new file mode 100644 index 000000000000..9e1126a7fa32 --- /dev/null +++ b/samples/authentication/certs/auth_dev_server.crt @@ -0,0 +1,48 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 8193 (0x2001) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=US, ST=California, O=Auth Company Test, OU=Authentication TEST, CN=Auth Intermediate TEST + Validity + Not Before: Aug 26 20:38:38 2020 GMT + Not After : Sep 5 20:38:38 2021 GMT + Subject: C=US, ST=California, L=San Diego, O=Auth Company Test, OU=Authentication TEST, CN=Auth Server TEST + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:b8:7a:e3:97:a6:8c:25:36:2c:32:a3:8f:e9:c7: + 73:43:6d:a9:27:00:65:dc:2c:ba:f7:c0:a8:4a:91: + fa:12:4e:4d:31:32:5b:4a:98:46:8b:64:1d:48:26: + 90:14:37:2a:f9:3e:47:e7:58:fd:0a:1e:a6:ef:eb: + 1b:95:20:ee:a6 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Authority Key Identifier: + keyid:36:1F:70:3A:68:8A:0D:81:79:00:21:5C:DA:CF:ED:7D:F4:96:85:2A + + X509v3 Key Usage: critical + Digital Signature + Signature Algorithm: ecdsa-with-SHA256 + 30:44:02:20:02:d5:f3:cd:3c:db:11:cf:f9:c8:8e:00:53:af: + 9c:16:83:b0:69:fd:d1:2b:bf:c4:f2:43:ac:c4:68:9e:98:d1: + 02:20:22:e8:8e:a1:8f:e4:ef:ed:6f:91:7f:c5:f2:18:8e:e1: + af:ae:47:40:95:8d:b2:91:bd:5b:b6:ff:43:a0:c5:20 +-----BEGIN CERTIFICATE----- +MIICNjCCAd2gAwIBAgICIAEwCgYIKoZIzj0EAwIwfTELMAkGA1UEBhMCVVMxEzAR +BgNVBAgMCkNhbGlmb3JuaWExGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRww +GgYDVQQLDBNBdXRoZW50aWNhdGlvbiBURVNUMR8wHQYDVQQDDBZBdXRoIEludGVy +bWVkaWF0ZSBURVNUMB4XDTIwMDgyNjIwMzgzOFoXDTIxMDkwNTIwMzgzOFowgYsx +CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4g +RGllZ28xGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNBdXRo +ZW50aWNhdGlvbiBURVNUMRkwFwYDVQQDDBBBdXRoIFNlcnZlciBURVNUMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEuHrjl6aMJTYsMqOP6cdzQ22pJwBl3Cy698Co +SpH6Ek5NMTJbSphGi2QdSCaQFDcq+T5H51j9Ch6m7+sblSDupqM+MDwwCQYDVR0T +BAIwADAfBgNVHSMEGDAWgBQ2H3A6aIoNgXkAIVzaz+199JaFKjAOBgNVHQ8BAf8E +BAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgAtXzzTzbEc/5yI4AU6+cFoOwaf3RK7/E +8kOsxGiemNECICLojqGP5O/tb5F/xfIYjuGvrkdAlY2ykb1btv9DoMUg +-----END CERTIFICATE----- diff --git a/samples/authentication/certs/auth_intermediate.crt b/samples/authentication/certs/auth_intermediate.crt new file mode 100644 index 000000000000..db29f1f7e9d5 --- /dev/null +++ b/samples/authentication/certs/auth_intermediate.crt @@ -0,0 +1,51 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4096 (0x1000) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=US, ST=California, L=San Diego, O=Auth Company Test, OU=Authentication TEST, CN=Auth Root TEST + Validity + Not Before: Aug 26 20:12:25 2020 GMT + Not After : Aug 9 20:12:25 2031 GMT + Subject: C=US, ST=California, O=Auth Company Test, OU=Authentication TEST, CN=Auth Intermediate TEST + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:80:a0:a3:92:ae:d7:5a:99:8b:8b:c5:a1:36:f6: + 1e:60:92:86:fb:e9:b0:23:3f:d1:4d:d2:0d:da:85: + b3:20:54:4f:f7:17:76:56:2e:a9:8f:83:76:13:3e: + 1a:94:34:41:de:0a:35:91:23:c0:55:85:10:c5:d5: + 1f:ae:a0:26:0a + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Subject Key Identifier: + 36:1F:70:3A:68:8A:0D:81:79:00:21:5C:DA:CF:ED:7D:F4:96:85:2A + X509v3 Authority Key Identifier: + keyid:E3:01:08:DB:EC:94:8F:F0:EC:6C:F9:AA:2D:A9:20:8E:CB:96:50:A9 + + X509v3 Basic Constraints: critical + CA:TRUE, pathlen:0 + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + Signature Algorithm: ecdsa-with-SHA256 + 30:45:02:20:77:88:fc:0b:ac:19:8c:37:a7:ab:02:5f:ba:c3: + de:a9:40:94:47:eb:64:79:b3:94:f9:f6:72:cf:df:67:79:41: + 02:21:00:cc:77:37:c8:a3:c3:c7:04:df:92:60:e6:cc:ca:cb: + 78:58:82:23:7f:e9:54:c5:11:80:03:49:d8:bb:df:b4:a2 +-----BEGIN CERTIFICATE----- +MIICXTCCAgOgAwIBAgICEAAwCgYIKoZIzj0EAwIwgYkxCzAJBgNVBAYTAlVTMRMw +EQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlTYW4gRGllZ28xGjAYBgNVBAoM +EUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNBdXRoZW50aWNhdGlvbiBURVNU +MRcwFQYDVQQDDA5BdXRoIFJvb3QgVEVTVDAeFw0yMDA4MjYyMDEyMjVaFw0zMTA4 +MDkyMDEyMjVaMH0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRow +GAYDVQQKDBFBdXRoIENvbXBhbnkgVGVzdDEcMBoGA1UECwwTQXV0aGVudGljYXRp +b24gVEVTVDEfMB0GA1UEAwwWQXV0aCBJbnRlcm1lZGlhdGUgVEVTVDBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABICgo5Ku11qZi4vFoTb2HmCShvvpsCM/0U3SDdqF +syBUT/cXdlYuqY+DdhM+GpQ0Qd4KNZEjwFWFEMXVH66gJgqjZjBkMB0GA1UdDgQW +BBQ2H3A6aIoNgXkAIVzaz+199JaFKjAfBgNVHSMEGDAWgBTjAQjb7JSP8Oxs+aot +qSCOy5ZQqTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAgNIADBFAiB3iPwLrBmMN6erAl+6w96pQJRH62R5s5T59nLP32d5QQIh +AMx3N8ijw8cE35Jg5szKy3hYgiN/6VTFEYADSdi737Si +-----END CERTIFICATE----- diff --git a/samples/authentication/certs/auth_rootca.crt b/samples/authentication/certs/auth_rootca.crt new file mode 100644 index 000000000000..2b0e752170d7 --- /dev/null +++ b/samples/authentication/certs/auth_rootca.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICeTCCAh+gAwIBAgIUPt+1qmulufx3ze5ZMsGpact9ekwwCgYIKoZIzj0EAwIw +gYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlT +YW4gRGllZ28xGjAYBgNVBAoMEUF1dGggQ29tcGFueSBUZXN0MRwwGgYDVQQLDBNB +dXRoZW50aWNhdGlvbiBURVNUMRcwFQYDVQQDDA5BdXRoIFJvb3QgVEVTVDAeFw0y +MDA4MjYxOTM5MDJaFw0zNDA1MDUxOTM5MDJaMIGJMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJU2FuIERpZWdvMRowGAYDVQQKDBFB +dXRoIENvbXBhbnkgVGVzdDEcMBoGA1UECwwTQXV0aGVudGljYXRpb24gVEVTVDEX +MBUGA1UEAwwOQXV0aCBSb290IFRFU1QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AATCxBhg/MkpI+6PfUg6NUdk03pOd8x0Q8qcJngr4Gje+AzQLFoJHN0NE+PaTf2k +ILZX1hT2l39oKFXxhQedPB/uo2MwYTAdBgNVHQ4EFgQU4wEI2+yUj/DsbPmqLakg +jsuWUKkwHwYDVR0jBBgwFoAU4wEI2+yUj/DsbPmqLakgjsuWUKkwDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwIDSAAwRQIgT8TvcsCl +adPrqWv50U87LON0QMsaTAqvVV6tFd06knICIQCxH+oRA9l0NSiyHJuPHRuKn6a9 +xfOTq1zTpr6fNu9vuw== +-----END CERTIFICATE----- diff --git a/samples/authentication/certs/keys/auth_dev_client.key b/samples/authentication/certs/keys/auth_dev_client.key new file mode 100644 index 000000000000..2e6b11b1fcd1 --- /dev/null +++ b/samples/authentication/certs/keys/auth_dev_client.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJ87D6q+Z4ulXG2B9lQblbgCQh4xhMgImQpyKxUCtP3soAoGCCqGSM49 +AwEHoUQDQgAEkwj+WKKVM3k9AEiS/efDhXoICq/e8rVKew2qpun6zie8xATw7pyt +e1Hf59Ga2Vmti/QTvzczZrvj/m0LbKzmnw== +-----END EC PRIVATE KEY----- diff --git a/samples/authentication/certs/keys/auth_dev_server.key b/samples/authentication/certs/keys/auth_dev_server.key new file mode 100644 index 000000000000..06968e59a681 --- /dev/null +++ b/samples/authentication/certs/keys/auth_dev_server.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP8LVrSz0CD/KvTTo5/qmOKIFtepGBTDG2odkSnYx/ssoAoGCCqGSM49 +AwEHoUQDQgAEuHrjl6aMJTYsMqOP6cdzQ22pJwBl3Cy698CoSpH6Ek5NMTJbSphG +i2QdSCaQFDcq+T5H51j9Ch6m7+sblSDupg== +-----END EC PRIVATE KEY----- diff --git a/samples/authentication/certs/keys/auth_inter.key b/samples/authentication/certs/keys/auth_inter.key new file mode 100644 index 000000000000..a2d10a601156 --- /dev/null +++ b/samples/authentication/certs/keys/auth_inter.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDV34Cl2CsPrR9VhASoOPRU1ROQ4tUN+z9LuuxkhcgIroAoGCCqGSM49 +AwEHoUQDQgAEgKCjkq7XWpmLi8WhNvYeYJKG++mwIz/RTdIN2oWzIFRP9xd2Vi6p +j4N2Ez4alDRB3go1kSPAVYUQxdUfrqAmCg== +-----END EC PRIVATE KEY----- diff --git a/samples/authentication/certs/keys/auth_rootca.key b/samples/authentication/certs/keys/auth_rootca.key new file mode 100644 index 000000000000..f811aba022fb --- /dev/null +++ b/samples/authentication/certs/keys/auth_rootca.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEbm67ja033uNzPco68U4+UtBjJBoNJMsGJcb6zbi0Z4oAoGCCqGSM49 +AwEHoUQDQgAEwsQYYPzJKSPuj31IOjVHZNN6TnfMdEPKnCZ4K+Bo3vgM0CxaCRzd +DRPj2k39pCC2V9YU9pd/aChV8YUHnTwf7g== +-----END EC PRIVATE KEY----- diff --git a/samples/authentication/multi_instance/README.rst b/samples/authentication/multi_instance/README.rst new file mode 100644 index 000000000000..58f6277fba50 --- /dev/null +++ b/samples/authentication/multi_instance/README.rst @@ -0,0 +1,37 @@ +.. _auth_multi-sample: + +Multiple Authentication Instances +################################# + +Overview +******** + +This sample shows how to use two authentication instances using the serial and Bluetooth transports. In this sample, +DTLS authentication is instance 1 and uses the Bluetooth transport, the Challenge-Response authentication is instance +2 and uses the Serial transport. The choice of instance and authentication methods are just for sample purposes, it is +possible to use any combination of authentication method and transport. For example, two instances of Challenge-Response +using the Serial transport. + +Here is a real world example of multiple authentication instances. A medical device with disposable heart monitor pads and a +Bluetooth connection. The monitor pads are connected via serial link and are authenticated when plugged into the device. +When the pads are replaced, the new pads are authenticated. The mobile app is authenticated over the Bluetooth link. + +.. image:: heart_monitor_device.png + +One authentication instance handles the monitor pads, the second instance handles the mobile app connection. + + +Building and Running +-------------------- +This sample was developed and tested with two Nordic nRF52840 dev +kits (see: https://www.nordicsemi.com/Software-and-Tools/Development-Kits/nRF52840-DK). Two Ubuntu +VMs were used, one running the Central the other VM running the Peripheral. + +To build:|br| +cmake -Bbuild_auth_client -DBOARD=nrf52840dk_nrf52840 samples/authentication/multi_instance/auth_client + +To build using West:|br| +west build -d build_auth_client -b nrf52840dk_nrf52840 samples/authentication/multi_instance/auth_client + + +Replace auth_client with auth_server to build the server. \ No newline at end of file diff --git a/samples/authentication/multi_instance/auth_client/CMakeLists.txt b/samples/authentication/multi_instance/auth_client/CMakeLists.txt new file mode 100644 index 000000000000..5d88823fd65f --- /dev/null +++ b/samples/authentication/multi_instance/auth_client/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(periph_auth) + +target_include_directories(app PUBLIC ${CMAKE_SOURCE_DIR}) + + +target_sources(app PRIVATE + src/main.c +) diff --git a/samples/authentication/multi_instance/auth_client/prj.conf b/samples/authentication/multi_instance/auth_client/prj.conf new file mode 100644 index 000000000000..fd4eba505eaf --- /dev/null +++ b/samples/authentication/multi_instance/auth_client/prj.conf @@ -0,0 +1,95 @@ +CONFIG_BT=y +CONFIG_BT_DEBUG_LOG=y +CONFIG_BT_SMP=y +CONFIG_BT_SIGNING=y +CONFIG_BT_PERIPHERAL=n +CONFIG_BT_DIS=y +CONFIG_BT_ATT_PREPARE_COUNT=5 +CONFIG_BT_PRIVACY=y +CONFIG_BT_DEVICE_NAME="Zephyr Central Auth" +CONFIG_BT_DEVICE_APPEARANCE=833 +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=65 +CONFIG_BT_RX_BUF_LEN=1600 +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_MAX_PAIRED=2 +CONFIG_BT_MAX_CONN=2 + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +CONFIG_MAIN_STACK_SIZE=4096 + +# logging +CONFIG_LOG=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y + + +CONFIG_AUTH_LIB=y +# Enable Bluetooth and serial transports +CONFIG_SERIAL_XPORT=y +CONFIG_BT_XPORT=y + +# Using both DTLS and Challenge-Response authentication methods +# in different instances. +CONFIG_AUTH_DTLS=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y + +# two authentication instance +CONFIG_NUM_AUTH_INSTANCES=2 + +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_LOG=y +CONFIG_AUTH_LOG_LEVEL=0 +CONFIG_AUTH_CHALLENGE_RESPONSE=y + +# Config vars for Challenge-Response +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_UART_INTERRUPT_DRIVEN=y + + +# Mbed config vars +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h" +CONFIG_MBEDTLS_TLS_VERSION_1_2=y +CONFIG_MBEDTLS_DTLS=y +CONFIG_MBEDTLS_ENTROPY_ENABLED=y + +# Supported key exchange modes +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED=y + +# Supported elliptic curves +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y + +# Supported cipher modes +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y +CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y +CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y + + +# Supported message authentication methods +CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y +CONFIG_MBEDTLS_MAC_CMAC_ENABLED=y + + +# Other configurations +CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1500 +CONFIG_MBEDTLS_DEBUG=y +CONFIG_MBEDTLS_DEBUG_LEVEL=0 +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=65535 +CONFIG_APP_LINK_WITH_MBEDTLS=y + + + + + diff --git a/samples/authentication/multi_instance/auth_client/sample.yaml b/samples/authentication/multi_instance/auth_client/sample.yaml new file mode 100644 index 000000000000..eee7e986afb1 --- /dev/null +++ b/samples/authentication/multi_instance/auth_client/sample.yaml @@ -0,0 +1,8 @@ +sample: + description: Auth Multi Instance Client sample app + name: Auth Client Multi Instance +tests: + sample.auth_client_multi: + harness: bluetooth + platform_allow: qemu_x86 + tags: bluetooth authentication diff --git a/samples/authentication/multi_instance/auth_client/src/main.c b/samples/authentication/multi_instance/auth_client/src/main.c new file mode 100644 index 000000000000..63aa78c6ec86 --- /dev/null +++ b/samples/authentication/multi_instance/auth_client/src/main.c @@ -0,0 +1,645 @@ + +/* main.c - Application main entry point + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + + +#include "../../../certs/auth_certs.h" + + +LOG_MODULE_REGISTER(central_auth, CONFIG_AUTH_LOG_LEVEL); + + +/** + * Declare UUIDs used for the Authentication service and characteristics + * see auth_xport.h + */ + +static struct bt_uuid_128 auth_service_uuid = AUTH_SERVICE_UUID; + +static struct bt_uuid_128 auth_client_char = AUTH_SVC_CLIENT_CHAR_UUID; + +static struct bt_uuid_128 auth_server_char = AUTH_SVC_SERVER_CHAR; + + +#define AUTH_DTLS_INST AUTH_INST_1_ID +#define AUTH_CHALLENGE_INST AUTH_INST_2_ID + +/* The Root and Intermediate Certs in a single CA chain. + * plus the server cert. All in PEM format.*/ +static const uint8_t auth_cert_ca_chain[] = AUTH_ROOTCA_CERT_PEM AUTH_INTERMEDIATE_CERT_PEM; +static const uint8_t auth_dev_client_cert[] = AUTH_CLIENT_CERT_PEM; +static const uint8_t auth_client_privatekey[] = AUTH_CLIENT_PRIVATE_KEY_PEM; + +static struct auth_optional_param dtls_certs_param = { + .param_id = AUTH_DTLS_PARAM, + .param_body = { + .dtls_certs = { + .server_ca_chain_pem = { + .cert = auth_cert_ca_chain, + .cert_size = sizeof(auth_cert_ca_chain), + }, + + .device_cert_pem = { + .cert = auth_dev_client_cert, + .cert_size = sizeof(auth_dev_client_cert), + .priv_key = auth_client_privatekey, + .priv_key_size = sizeof(auth_client_privatekey) + } + } + } +}; + + +#define NEW_SHARED_KEY_LEN (32u) + +/* Use a different key than default */ +static uint8_t chal_resp_sharedkey[NEW_SHARED_KEY_LEN] = { + 0x21, 0x8e, 0x37, 0x42, 0x1e, 0xe1, 0x2a, 0x22, 0x7c, 0x4b, 0x3f, 0x3f, 0x07, 0x5e, 0x8a, 0xd8, + 0x24, 0xdf, 0xca, 0xf4, 0x04, 0xd0, 0x3e, 0x22, 0x61, 0x9f, 0x24, 0xa3, 0xc7, 0xf6, 0x5d, 0x66 +}; + +static struct auth_optional_param chal_resp_param = { + .param_id = AUTH_CHALRESP_PARAM, + .param_body = { + .chal_resp = { + .shared_key = chal_resp_sharedkey, + }, + } +}; + + +static struct bt_conn *default_conn; +static struct bt_gatt_discover_params discover_params; +static struct bt_gatt_subscribe_params subscribe_params; + + + +/** + * Authentication connect struct + */ +static struct authenticate_conn auth_conn_bt; +static struct authenticate_conn auth_conn_serial; + + + +/* Auth service, client descriptor, server descriptor */ +#define AUTH_SVC_GATT_COUNT (4u) + +#define AUTH_SVC_INDEX (0u) +#define AUTH_SVC_CLIENT_CHAR_INDEX (1u) +#define AUTH_SVC_CLIENT_CCC_INDEX (2u) +#define AUTH_SVC_SERVER_CHAR_INDEX (3u) + +/** + * Used to store Authentication GATT service and characteristics. + */ +typedef struct { + const struct bt_uuid *uuid; + const struct bt_gatt_attr *attr; + uint16_t handle; + uint16_t value_handle; + uint8_t permissions; /* Bitfields from: BT_GATT_PERM_NONE, BT_GATT_PERM_READ, .. in gatt.h */ + const uint32_t gatt_disc_type; +} auth_svc_gatt_t; + + +static uint32_t auth_desc_index; + +/* Table content should match indexes above */ +static auth_svc_gatt_t auth_svc_gatt_tbl[AUTH_SVC_GATT_COUNT] = { + { (const struct bt_uuid *)&auth_service_uuid, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_PRIMARY }, /* AUTH_SVC_INDEX */ + { (const struct bt_uuid *)&auth_client_char, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_CHARACTERISTIC }, /* AUTH_SVC_CLIENT_CHAR_INDEX */ + { BT_UUID_GATT_CCC, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_DESCRIPTOR }, /* AUTH_SVC_CLIENT_CCC_INDEX CCC for Client char */ + { (const struct bt_uuid *)&auth_server_char, NULL, 0, 0, BT_GATT_PERM_NONE, BT_GATT_DISCOVER_CHARACTERISTIC } /* AUTH_SVC_SERVER_CHAR_INDEX */ +}; + +static const struct device *uart_dev; + +static struct uart_config uart_cfg = { + .baudrate = 115200, + .parity = UART_CFG_PARITY_NONE, + .stop_bits = UART_CFG_STOP_BITS_1, + .data_bits = UART_CFG_DATA_BITS_8, + .flow_ctrl = UART_CFG_FLOW_CTRL_NONE, +}; + + + +/** + * Params used to change the connection MTU length. + */ +struct bt_gatt_exchange_params mtu_parms; + +void mtu_change_cb(struct bt_conn *conn, uint8_t err, struct bt_gatt_exchange_params *params) +{ + if (err) { + LOG_ERR("Failed to set MTU, err: %d", err); + } else { + LOG_DBG("Successfully set MTU to: %d", bt_gatt_get_mtu(conn)); + } +} + +/** + * Start DTLS and Challenge-Response auth methods. + * + * @return 0 on success, else negative error value + */ +static int start_authentication(void) +{ + int err = auth_lib_start(&auth_conn_bt); + + if (err) { + LOG_ERR("Failed to start DTLS auth method, err: %d", err); + return err; + } + + err = auth_lib_start(&auth_conn_serial); + + if (err) { + LOG_ERR("Failed to start Challenge-Response auth method, err: %d", err); + return err; + } + + return 0; +} + +/** + * Characteristic discovery function + * + * + * @param conn Bluetooth connection. + * @param attr Discovered attribute. NOTE: This pointer will go out fo scope + * do not save pointer for future use. + * @param params Discover params. + * + * @return BT_GATT_ITER_STOP + */ +static uint8_t discover_func(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + struct bt_gatt_discover_params *params) +{ + int err; + + if (!attr) { + LOG_INF("Discover complete, NULL attribute."); + (void)memset(params, 0, sizeof(*params)); + return BT_GATT_ITER_STOP; + } + + + /* debug output */ + LOG_DBG("====auth_desc_index is: %d=====", auth_desc_index); + LOG_DBG("[ATTRIBUTE] handle 0x%x", attr->handle); + LOG_DBG("[ATTRIBUTE] value handle 0x%x", bt_gatt_attr_value_handle(attr)); + + /* let's get string */ + char uuid_str[50]; + bt_uuid_to_str(attr->uuid, uuid_str, sizeof(uuid_str)); + LOG_DBG("Attribute UUID: %s", log_strdup(uuid_str)); + + /* print attribute UUID */ + bt_uuid_to_str(discover_params.uuid, uuid_str, sizeof(uuid_str)); + LOG_DBG("Discovery UUID: %s", log_strdup(uuid_str)); + + + /** + * Verify the correct UUID was found + */ + if (bt_uuid_cmp(discover_params.uuid, auth_svc_gatt_tbl[auth_desc_index].uuid)) { + + /* Failed, not the UUID we're expecting */ + LOG_ERR("Error Unknown UUID."); + return BT_GATT_ITER_STOP; + } + + /* save off GATT info */ + auth_svc_gatt_tbl[auth_desc_index].attr = NULL; /* NOTE: attr var not used for the Central */ + auth_svc_gatt_tbl[auth_desc_index].handle = attr->handle; + auth_svc_gatt_tbl[auth_desc_index].value_handle = bt_gatt_attr_value_handle(attr); + auth_svc_gatt_tbl[auth_desc_index].permissions = attr->perm; + + auth_desc_index++; + + /* Are all of the characteristics discovered? */ + if (auth_desc_index >= AUTH_SVC_GATT_COUNT) { + + /* we're done */ + LOG_INF("Discover complete"); + + /* save off the server attribute handle */ + + /* setup the subscribe params + * Value handle for the Client characteristic for indication of + * peripheral data. + */ + subscribe_params.notify = auth_xp_bt_central_notify; + subscribe_params.value = BT_GATT_CCC_NOTIFY; + subscribe_params.value_handle = + auth_svc_gatt_tbl[AUTH_SVC_CLIENT_CHAR_INDEX].value_handle; + + /* Handle for the CCC descriptor itself */ + subscribe_params.ccc_handle = + auth_svc_gatt_tbl[AUTH_SVC_CLIENT_CCC_INDEX].handle; + + err = bt_gatt_subscribe(conn, &subscribe_params); + if (err && err != -EALREADY) { + LOG_ERR("Subscribe failed (err %d)", err); + } + + /* Get the server BT characteristic, the central sends data to this characteristic */ + uint16_t server_char_handle = + auth_svc_gatt_tbl[AUTH_SVC_SERVER_CHAR_INDEX].value_handle; + + /* setup the BT transport params */ + struct auth_xp_bt_params xport_params = + { .conn = conn, .is_central = true, + .server_char_hdl = server_char_handle }; + + err = auth_xport_init(&auth_conn_bt.xport_hdl, + auth_conn_bt.instance, + AUTH_XP_TYPE_BLUETOOTH, &xport_params); + + if (err) { + LOG_ERR("Failed to initialize Bluetooth transport, err: %d", err); + return BT_GATT_ITER_STOP; + } + + /* Start DTLS and Challenge-Response auth process */ + err = start_authentication(); + + if (err == 0) { + LOG_INF("Started auth services."); + } + + return BT_GATT_ITER_STOP; + } + + /* set up the next discovery params */ + discover_params.uuid = auth_svc_gatt_tbl[auth_desc_index].uuid, + discover_params.start_handle = attr->handle + 1; + discover_params.type = auth_svc_gatt_tbl[auth_desc_index].gatt_disc_type; + + + /* Start discovery */ + err = bt_gatt_discover(conn, &discover_params); + if (err) { + LOG_ERR("Discover failed (err %d)", err); + } + + + return BT_GATT_ITER_STOP; +} + +/** + * Connected to the peripheral device + * + * @param conn The Bluetooth connection. + * @param conn_err Connection error, 0 == no error + */ +static void connected(struct bt_conn *conn, uint8_t conn_err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + int err; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (conn_err) { + LOG_ERR("Failed to connect to %s (%u)", log_strdup(addr), conn_err); + return; + } + + LOG_INF("Connected: %s", log_strdup(addr)); + + if (conn == default_conn) { + + /* set the max MTU, only for GATT interface */ + mtu_parms.func = mtu_change_cb; + bt_gatt_exchange_mtu(conn, &mtu_parms); + + /* reset gatt discovery index */ + auth_desc_index = 0; + + discover_params.uuid = auth_svc_gatt_tbl[auth_desc_index].uuid; + discover_params.func = discover_func; + discover_params.start_handle = 0x0001; + discover_params.end_handle = 0xffff; + discover_params.type = auth_svc_gatt_tbl[auth_desc_index].gatt_disc_type; + + /** + * Discover characteristics for the service + */ + err = bt_gatt_discover(default_conn, &discover_params); + if (err) { + LOG_ERR("Discover failed(err %d)", err); + return; + } + } +} + +/** + * Parse through the BLE adv data, looking for our service + * + * @param data Bluetooth data + * @param user_data User data. + * + * @return true on success, else false + */ +static bool bt_adv_data_found(struct bt_data *data, void *user_data) +{ + bt_addr_le_t *addr = user_data; + int err; + struct bt_uuid_128 auth_uuid; + + LOG_DBG("[AD]: %u data_len %u", data->type, data->data_len); + + if (data->type == BT_DATA_UUID128_ALL) { + + if (data->data_len != BT_UUID_SIZE_128) { + LOG_WRN("AD malformed"); + return false; + } + + /* build full UUID to check */ + auth_uuid.uuid.type = BT_UUID_TYPE_128; + memcpy(auth_uuid.val, data->data, BT_UUID_SIZE_128); + + + /** + * Is this the service we're looking for? If not continue + * else stop the scan and connect to the device. + */ + if (bt_uuid_cmp((const struct bt_uuid *)&auth_uuid, + (const struct bt_uuid *)&auth_service_uuid)) { + return true; + } + + /* stop scanning, we've found the service */ + err = bt_le_scan_stop(); + if (err) { + LOG_ERR("Stop LE scan failed (err %d)", err); + return false; + } + + /** + * @brief Connect to the device + */ + struct bt_conn_le_create_param param = BT_CONN_LE_CREATE_PARAM_INIT( + BT_CONN_LE_OPT_NONE, + BT_GAP_SCAN_FAST_INTERVAL, + BT_GAP_SCAN_FAST_INTERVAL); + + if (bt_conn_le_create(addr, ¶m, BT_LE_CONN_PARAM_DEFAULT, + &default_conn)) { + LOG_ERR("Failed to create BLE connection to peripheral."); + } + + return false; + } + + return true; +} + +/** + * Found a device when scanning. + * + * @param addr BT address + * @param rssi Signal strength + * @param type Device type + * @param ad Simple buffer. + */ +static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, + struct net_buf_simple *ad) +{ + char dev[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(addr, dev, sizeof(dev)); + LOG_DBG("[DEVICE]: %s, AD evt type %u, AD data len %u, RSSI %i", + log_strdup(dev), type, ad->len, rssi); + + /* We're only interested in connectable events */ + if (type == BT_HCI_ADV_IND || type == BT_HCI_ADV_DIRECT_IND) { + bt_data_parse(ad, bt_adv_data_found, (void *)addr); + } +} + +/** + * BT disconnect callback. + * + * @param conn The Bluetooth connection. + * @param reason Disconnect reason. + */ +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + LOG_INF("Disconnected: %s (reason 0x%02x)", log_strdup(addr), reason); + + if (default_conn != conn) { + return; + } + + /* de init transport */ + /* Send disconnect event to BT transport. */ + struct auth_xport_evt conn_evt = { .event = XP_EVT_DISCONNECT }; + auth_xport_event(auth_conn_bt.xport_hdl, &conn_evt); + + /* Deinit lower transport */ + auth_xport_deinit(auth_conn_bt.xport_hdl); + auth_conn_bt.xport_hdl = NULL; + + bt_conn_unref(default_conn); + default_conn = NULL; +} + +/** + * (dis)Connection callbacks + */ +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +/** + * Configures the UART + * + * @return 0 on success, else negative error code. + */ +static int config_uart(void) +{ + struct auth_xp_serial_params xp_params; + + uart_dev = device_get_binding(DT_LABEL(DT_NODELABEL(uart0))); + + int err = uart_configure(uart_dev, &uart_cfg); + + if (err) { + LOG_ERR("Failed to configure UART, err: %d", err); + return err; + } + + /* If successful,then init lower transport layer. */ + xp_params.uart_dev = uart_dev; + + err = auth_xport_init(&auth_conn_serial.xport_hdl, auth_conn_serial.instance, + AUTH_XP_TYPE_SERIAL, &xp_params); + + if (err) { + LOG_ERR("Failed to initialize authentication transport, error: %d", err); + } + + return err; +} + + +/** + * Authentication status callback. + * + * @param auth_conn Authentication connection. + * @param instance Instance ID. + * @param status Auth status. + * @param context Optional context. + */ +static void auth_status(struct authenticate_conn *auth_conn, + enum auth_instance_id instance, + enum auth_status status, void *context) +{ + /* display status */ + if (instance == AUTH_DTLS_INST) { + printk("Auth instance DTLS (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + + if (status == AUTH_STATUS_IN_PROCESS) { + printk(" DTLS may take 30-60 seconds.\n"); + } + } + + if (instance == AUTH_CHALLENGE_INST) { + printk("Auth instance Challenge-Response (%d) status: %s\n", + instance, auth_lib_getstatus_str(status)); + } + +} + +/** + * Process log messages. + */ +static void process_log_msgs(void) +{ + while (log_process(false)) { + ; /* intentionally empty statement */ + } +} + +/** + * Idle process for app. + */ +static void idle_function(void) +{ + /* Just spin while the BT modules handle the connection. */ + while (true) { + + process_log_msgs(); + + /* Let the handshake thread run */ + k_yield(); + } +} + + +/** + * main entry point. + */ +void main(void) +{ + int err = 0; + + log_init(); + + + printk("Starting multi auth client.\n"); + + /* Instance 1 uses DTLS auth method over Bluetooth. */ + err = auth_lib_init(&auth_conn_bt, AUTH_DTLS_INST, auth_status, NULL, + &dtls_certs_param, + AUTH_CONN_CLIENT | AUTH_CONN_DTLS_AUTH_METHOD); + + if (err) { + LOG_ERR("Failed to init DTLS authentication service, err: %d.", err); + idle_function(); /* does not return */ + } + + /* Instance 2 uses Challenge-Response auth method over Serial transport. */ + err = auth_lib_init(&auth_conn_serial, AUTH_CHALLENGE_INST, auth_status, + NULL, &chal_resp_param, + AUTH_CONN_CLIENT | AUTH_CONN_CHALLENGE_AUTH_METHOD); + + if (err) { + LOG_ERR("Failed to init Challenge-Response authentication service, err: %d.", err); + idle_function(); /* does not return */ + } + + + /* Configure the UART for serial transport. */ + err = config_uart(); + + if (err) { + LOG_ERR("Failed to configure the UART, err: %d.", err); + idle_function(); /* does not return */ + } + + /** + * @brief Enable the Bluetooth module. Passing NULL to bt_enable + * will block while the BLE stack is initialized. + * Enable bluetooth module + */ + err = bt_enable(NULL); + if (err) { + LOG_ERR("Failed to enable the bluetooth module, err: %d", err); + idle_function(); /* does not return */ + } + + /* Register connect/disconnect callbacks */ + bt_conn_cb_register(&conn_callbacks); + + /* start scanning for peripheral */ + err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, device_found); + + if (err) { + printk("Scanning failed to start (err %d)\n", err); + } + + + /* does not return */ + idle_function(); + + /* should not reach here */ + +} diff --git a/samples/authentication/multi_instance/auth_server/CMakeLists.txt b/samples/authentication/multi_instance/auth_server/CMakeLists.txt new file mode 100644 index 000000000000..524177640d57 --- /dev/null +++ b/samples/authentication/multi_instance/auth_server/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(auth_serial_server) + + +target_include_directories(app PUBLIC ${CMAKE_SOURCE_DIR}) + +zephyr_include_directories(${CMAKE_SOURCE_DIR}/src) + +target_sources(app PRIVATE + src/main.c +) + +#zephyr_library_include_directories(${CMAKE_SOURCE_DIR}) diff --git a/samples/authentication/multi_instance/auth_server/prj.conf b/samples/authentication/multi_instance/auth_server/prj.conf new file mode 100644 index 000000000000..3df813a2bf10 --- /dev/null +++ b/samples/authentication/multi_instance/auth_server/prj.conf @@ -0,0 +1,96 @@ +# +# +# + +# Bluetooth config vars +CONFIG_BT=y +CONFIG_BT_DEBUG_LOG=y +CONFIG_BT_SMP=y +CONFIG_BT_SIGNING=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_DIS=y +CONFIG_BT_ATT_PREPARE_COUNT=5 +CONFIG_BT_PRIVACY=y +CONFIG_BT_DEVICE_NAME="Zephyr Peripheral Auth" +CONFIG_BT_DEVICE_APPEARANCE=833 +CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_DEVICE_NAME_MAX=65 +CONFIG_BT_RX_BUF_LEN=1600 + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +CONFIG_MAIN_STACK_SIZE=4096 + +# Don't use UART backend for logging, will collide with serial link +CONFIG_LOG=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_AUTH_LOG_LEVEL=0 +CONFIG_LOG_PRINTK=y + + +CONFIG_AUTH_LIB=y +# Enable Bluetooth and seiral transports +CONFIG_SERIAL_XPORT=y +CONFIG_BT_XPORT=y + +# Using both DTLS and Challenge-Response authentication methods +# in different instances. +CONFIG_AUTH_DTLS=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y + +# two authentication instance +CONFIG_NUM_AUTH_INSTANCES=2 + +# Config vars for Challenge-Response +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# For production products, should use a real hardware +# random number generator. +#CONFIG_TEST_RANDOM_GENERATOR=y + + +# Mbed config vars +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h" +CONFIG_MBEDTLS_TLS_VERSION_1_2=y +CONFIG_MBEDTLS_DTLS=y +CONFIG_MBEDTLS_ENTROPY_ENABLED=y + +# Supported key exchange modes +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED=y + +# Supported elliptic curves +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y + +# Supported cipher modes +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y +CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y +CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=yy + + +# Supported message authentication methods +CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y +CONFIG_MBEDTLS_MAC_CMAC_ENABLED=y + + +# Other configurations +CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1500 +CONFIG_MBEDTLS_DEBUG=y +CONFIG_MBEDTLS_DEBUG_LEVEL=0 +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=65535 +CONFIG_APP_LINK_WITH_MBEDTLS=y + + + + + + diff --git a/samples/authentication/multi_instance/auth_server/sample.yaml b/samples/authentication/multi_instance/auth_server/sample.yaml new file mode 100644 index 000000000000..6fda38efc3d1 --- /dev/null +++ b/samples/authentication/multi_instance/auth_server/sample.yaml @@ -0,0 +1,8 @@ +sample: + description: Auth Multi Instance Server sample app + name: Auth Server Multi Instance +tests: + sample.auth_server_multi: + harness: bluetooth + platform_allow: qemu_x86 + tags: bluetooth authentication diff --git a/samples/authentication/multi_instance/auth_server/src/main.c b/samples/authentication/multi_instance/auth_server/src/main.c new file mode 100644 index 000000000000..88b8043ebe4b --- /dev/null +++ b/samples/authentication/multi_instance/auth_server/src/main.c @@ -0,0 +1,430 @@ +/* + * Sample authentication BLE peripheral + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + + +LOG_MODULE_REGISTER(periph_auth, CONFIG_AUTH_LOG_LEVEL); + + +#include "../../../certs/auth_certs.h" + +/** + * Declare UUIDs used for the Authentication service and characteristics + */ + +static struct bt_uuid_128 auth_service_uuid = AUTH_SERVICE_UUID; + +static struct bt_uuid_128 auth_client_char = AUTH_SVC_CLIENT_CHAR_UUID; + +static struct bt_uuid_128 auth_server_char = AUTH_SVC_SERVER_CHAR; + + +static void client_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value); + +/* AUTH Service Declaration */ +BT_GATT_SERVICE_DEFINE(auth_svc, + BT_GATT_PRIMARY_SERVICE(&auth_service_uuid), + + /** + * Central (client role) bt_gatt_write() ---> server characteristic --> bt_gatt_read() Peripheral (server role) + * + * Central <--- Notification (client characteristic) <--- Peripheral + * + */ + + /** + * Client characteristic, used by the peripheral (server role) to write messages authentication messages + * to the central (client role). The peripheral needs to alert the central a message is + * ready to be read. + */ + BT_GATT_CHARACTERISTIC((const struct bt_uuid *)&auth_client_char, BT_GATT_CHRC_INDICATE, + (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), NULL, NULL, NULL), + BT_GATT_CCC(client_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + + /** + * Server characteristic, used by the central (client role) to write authentication messages to. + * to the server (peripheral) + */ + BT_GATT_CHARACTERISTIC((const struct bt_uuid *)&auth_server_char, BT_GATT_CHRC_WRITE, + (BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), NULL, auth_xp_bt_central_write, NULL), + ); + + + +struct bt_conn *default_conn; + +static bool is_connected = false; + +/* Authentication connection info for serial and Bluetooth */ +static struct authenticate_conn auth_conn_bt; +static struct authenticate_conn auth_conn_serial; + + +/* The Root and Intermediate Certs in a single CA chain. + * plus the server cert. All in PEM format.*/ +static const uint8_t auth_cert_ca_chain[] = AUTH_ROOTCA_CERT_PEM AUTH_INTERMEDIATE_CERT_PEM; +static const uint8_t auth_dev_server_cert[] = AUTH_SERVER_CERT_PEM; +static const uint8_t auth_server_privatekey[] = AUTH_SERVER_PRIVATE_KEY_PEM; + + +#define AUTH_DTLS_INST AUTH_INST_1_ID +#define AUTH_CHALLENGE_INST AUTH_INST_2_ID + + +static struct auth_optional_param dtls_certs_param = { + .param_id = AUTH_DTLS_PARAM, + .param_body = { + .dtls_certs = { + .server_ca_chain_pem = { + .cert = auth_cert_ca_chain, + .cert_size = sizeof(auth_cert_ca_chain), + }, + + .device_cert_pem = { + .cert = auth_dev_server_cert, + .cert_size = sizeof(auth_dev_server_cert), + .priv_key = auth_server_privatekey, + .priv_key_size = sizeof(auth_server_privatekey) + } + } + } +}; + + +#define NEW_SHARED_KEY_LEN (32u) + +/* Use a different key than default */ +static uint8_t chal_resp_sharedkey[NEW_SHARED_KEY_LEN] = { + 0x21, 0x8e, 0x37, 0x42, 0x1e, 0xe1, 0x2a, 0x22, 0x7c, 0x4b, 0x3f, 0x3f, 0x07, 0x5e, 0x8a, 0xd8, + 0x24, 0xdf, 0xca, 0xf4, 0x04, 0xd0, 0x3e, 0x22, 0x61, 0x9f, 0x24, 0xa3, 0xc7, 0xf6, 0x5d, 0x66 +}; + + +static struct auth_optional_param chal_resp_param = { + .param_id = AUTH_CHALRESP_PARAM, + .param_body = { + .chal_resp = { + .shared_key = chal_resp_sharedkey, + }, + } +}; + +/** + * Set up the advertising data + */ +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID128_ALL, AUTH_SERVICE_UUID_BYTES), /* auth service UUID */ +}; + + +static const struct device *uart_dev; + +static struct uart_config uart_cfg = { + .baudrate = 115200, + .parity = UART_CFG_PARITY_NONE, + .stop_bits = UART_CFG_STOP_BITS_1, + .data_bits = UART_CFG_DATA_BITS_8, + .flow_ctrl = UART_CFG_FLOW_CTRL_NONE, +}; + +/** + * Connection callback + * + * @param conn The Bluetooth connection. + * @param err Error value, 0 == success + */ +static void connected(struct bt_conn *conn, uint8_t err) +{ + int ret; + struct auth_xport_evt conn_evt; + + if (err) { + printk("Connection failed (err 0x%02x)\n", err); + } else { + default_conn = bt_conn_ref(conn); + printk("Connected\n"); + + struct auth_xp_bt_params xport_param = + { .conn = conn, .is_central = false, + .client_attr = &auth_svc.attrs[1] }; + + ret = auth_xport_init(&auth_conn_bt.xport_hdl, auth_conn_bt.instance, + AUTH_XP_TYPE_BLUETOOTH, &xport_param); + + if (ret) { + printk("Failed to initialize BT transport, err: %d", ret); + return; + } + + is_connected = true; + + /* send connection event to BT transport */ + conn_evt.event = XP_EVT_CONNECT; + auth_xport_event(auth_conn_bt.xport_hdl, &conn_evt); + + /* Start Bluetooth authentication */ + ret = auth_lib_start(&auth_conn_bt); + + if (ret) { + printk("Failed to start authentication, err: %d\n", ret); + } + } +} + +/** + * The disconnect callback. + * + * @param conn Bluetooth connection struct + * @param reason Disconnect reason code. + */ +static void disconnected(struct bt_conn *conn, uint8_t reason) +{ + struct auth_xport_evt conn_evt; + + printk("Disconnected (reason 0x%02x)\n", reason); + + is_connected = false; + + /* Send disconnect event to BT transport. */ + conn_evt.event = XP_EVT_DISCONNECT; + auth_xport_event(auth_conn_bt.xport_hdl, &conn_evt); + + /* Deinit lower transport */ + auth_xport_deinit(auth_conn_bt.xport_hdl); + auth_conn_bt.xport_hdl = NULL; + + if (default_conn) { + bt_conn_unref(default_conn); + default_conn = NULL; + } +} + +/** + * Connect callbacks + */ +static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, +}; + +/** + * If the pairing was canceled. + * + * @param conn The Bluetooth connection. + */ +static void auth_cancel(struct bt_conn *conn) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + printk("Pairing cancelled: %s\n", addr); +} + +static struct bt_conn_auth_cb auth_cb_display = { + .cancel = auth_cancel, +}; + +/** + * Called after the BT module has initialized or not (error occurred). + * + * @param err Error code, 0 == success + */ +static void bt_ready(int err) +{ + if (err) { + printk("Bluetooth init failed (err %d)\n", err); + return; + } + + printk("Bluetooth initialized\n"); + + /* Start advertising after BT module has initialized OK */ + err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + printk("Advertising failed to start (err %d)\n", err); + return; + } + + printk("Advertising successfully started\n"); +} + +/** + * Authentication status callback + * + * @param auth_conn The authentication connection. + * @param instance Instance ID. + * @param status Status + * @param context Optional context. + */ +static void auth_status(struct authenticate_conn *auth_conn, + enum auth_instance_id instance, + enum auth_status status, void *context) +{ + /* display status */ + if (instance == AUTH_DTLS_INST) { + printk("Auth instance DTLS (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + + if (status == AUTH_STATUS_IN_PROCESS) { + printk(" DTLS may take 30-60 seconds.\n"); + } + } + + if (instance == AUTH_CHALLENGE_INST) { + printk("Auth instance Challenge-Response (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + } +} + + +/** + * Called when client notification is (dis)enabled by the Central + * + * @param attr GATT attribute. + * @param value BT_GATT_CCC_NOTIFY if changes are notified. + */ +static void client_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) +{ + ARG_UNUSED(attr); + + bool notif_enabled = (value == BT_GATT_CCC_NOTIFY) ? true : false; + + LOG_INF("Client notifications %s", notif_enabled ? "enabled" : "disabled"); +} + +/** + * Configure the UART params. + * + * @return 0 on success, else negative error value. + */ +static int config_uart(void) +{ + struct auth_xp_serial_params xp_params; + + uart_dev = device_get_binding(DT_LABEL(DT_NODELABEL(uart0))); + + int err = uart_configure(uart_dev, &uart_cfg); + + if (err) { + LOG_ERR("Failed to configure UART, err: %d", err); + return err; + } + + /* If successful,then init lower transport layer. */ + xp_params.uart_dev = uart_dev; + + err = auth_xport_init(&auth_conn_serial.xport_hdl, auth_conn_serial.instance, + AUTH_XP_TYPE_SERIAL, &xp_params); + + if (err) { + LOG_ERR("Failed to initialize authentication transport, error: %d", err); + } + + return err; +} + + +/** + * Process log messages + */ +static void process_log_msgs(void) +{ + while (log_process(false)) { + ; /* intentionally empty statement */ + } +} + + +static void idle_function(void) +{ + while (true) { + process_log_msgs(); + k_yield(); + } +} + +void main(void) +{ + int err = 0; + + log_init(); + + printk("Starting multi auth server.\n"); + + /* Instance uses DTLS auth method over Bluetooth transport. */ + err = auth_lib_init(&auth_conn_bt, AUTH_DTLS_INST, auth_status, NULL, + &dtls_certs_param, + AUTH_CONN_SERVER | AUTH_CONN_DTLS_AUTH_METHOD); + + if (err) { + printk("Failed to initialize DTLS auth method.\n"); + idle_function(); + } + + /* Instance 2 uses Challenge-Response method over serial transport */ + err = auth_lib_init(&auth_conn_serial, AUTH_CHALLENGE_INST, auth_status, + NULL, &chal_resp_param, + AUTH_CONN_SERVER | AUTH_CONN_CHALLENGE_AUTH_METHOD); + + if (err) { + printk("Failed to initialize Challenge-Response auth method.\n"); + idle_function(); /* never returns */ + } + + /* Configure the UART for the serial transport. */ + err = config_uart(); + if (err) { + printk("Failed to initialize UART for serial link.\n"); + idle_function(); /* never returns */ + } + + + err = bt_enable(bt_ready); + if (err) { + printk("Bluetooth init failed (err %d)\n", err); + idle_function(); /* never returns */ + } + + bt_conn_cb_register(&conn_callbacks); + bt_conn_auth_cb_register(&auth_cb_display); + + /* Start Challenge-Response auth over serial transport. */ + err = auth_lib_start(&auth_conn_serial); + + if (err) { + printk("Failed to start Challenge-Response authentication, err: %d\n", err); + } + + /* never returns */ + idle_function(); + + /* should never reach here */ +} + diff --git a/samples/authentication/multi_instance/heart_monitor_device.png b/samples/authentication/multi_instance/heart_monitor_device.png new file mode 100644 index 0000000000000000000000000000000000000000..345c6ac25d1821fbc088b3059de823dd934cc6aa GIT binary patch literal 23474 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%0A<{jIpa<$5`<1y8* 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%0DXZ3VGfGDM%O6WGH{Vtl3@sdk}dixut<$JiF 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 +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +#include + +#if defined(CONFIG_AUTH_DTLS) +#include "../../../certs/auth_certs.h" +#endif + +LOG_MODULE_REGISTER(auth_serial_client, CONFIG_AUTH_LOG_LEVEL); + + +static const struct device *uart_dev; + +static struct uart_config uart_cfg = { + .baudrate = 115200, + .parity = UART_CFG_PARITY_NONE, + .stop_bits = UART_CFG_STOP_BITS_1, + .data_bits = UART_CFG_DATA_BITS_8, + .flow_ctrl = UART_CFG_FLOW_CTRL_NONE, +}; + +#if defined(CONFIG_AUTH_DTLS) +/* The Root and Intermediate Certs in a single CA chain. + * plus the server cert. All in PEM format.*/ +static const uint8_t auth_cert_ca_chain[] = AUTH_ROOTCA_CERT_PEM AUTH_INTERMEDIATE_CERT_PEM; +static const uint8_t auth_dev_client_cert[] = AUTH_CLIENT_CERT_PEM; +static const uint8_t auth_client_privatekey[] = AUTH_CLIENT_PRIVATE_KEY_PEM; + +static struct auth_optional_param tls_certs_param = { + .param_id = AUTH_DTLS_PARAM, + .param_body = { + .dtls_certs = { + .server_ca_chain_pem = { + .cert = auth_cert_ca_chain, + .cert_size = sizeof(auth_cert_ca_chain), + }, + + .device_cert_pem = { + .cert = auth_dev_client_cert, + .cert_size = sizeof(auth_dev_client_cert), + .priv_key = auth_client_privatekey, + .priv_key_size = sizeof(auth_client_privatekey) + } + } + } +}; +#endif + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) +#define NEW_SHARED_KEY_LEN (32u) + +/* Use a different key than default */ +static uint8_t chal_resp_sharedkey[NEW_SHARED_KEY_LEN] = { + 0x21, 0x8e, 0x37, 0x42, 0x1e, 0xe1, 0x2a, 0x22, 0x7c, 0x4b, 0x3f, 0x3f, 0x07, 0x5e, 0x8a, 0xd8, + 0x24, 0xdf, 0xca, 0xf4, 0x04, 0xd0, 0x3e, 0x22, 0x61, 0x9f, 0x24, 0xa3, 0xc7, 0xf6, 0x5d, 0x66 +}; + + +static struct auth_optional_param chal_resp_param = { + .param_id = AUTH_CHALRESP_PARAM, + .param_body = { + .chal_resp = { + .shared_key = chal_resp_sharedkey, + }, + } +}; +#endif + + +/* Authentication connection info */ +static struct authenticate_conn auth_conn_serial; + +/** + * Authentication status callback. + * + * @param auth_conn The authentication connection. + * @param instance Authentication instance id. + * @param status Authentication status. + * @param context Optional context + */ +void auth_status_callback(struct authenticate_conn *auth_conn, + enum auth_instance_id instance, + enum auth_status status, void *context) +{ + printk("Authentication instance (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + +#if defined(CONFIG_AUTH_DTLS) + if (status == AUTH_STATUS_IN_PROCESS) { + printk(" DTLS may take 30-60 seconds.\n"); + } +#endif + + + if ((status == AUTH_STATUS_FAILED) || + (status == AUTH_STATUS_AUTHENTICATION_FAILED) || + (status == AUTH_STATUS_SUCCESSFUL)) { + /* Authentication has finished */ + auth_lib_deinit(auth_conn); + } +} + +/** + * Process log messages + */ +static void process_log_msgs(void) +{ + while (log_process(false)) { + ; /* intentionally empty statement */ + } +} + +/** + * Idle processing. + */ +static void idle_process(void) +{ + /* Just spin while the BT modules handle the connection. */ + while (true) { + + process_log_msgs(); + + /* Let the handshake thread run */ + k_yield(); + } +} + + +/** + * Configures the UART + * + * @return 0 on success, else negative error code. + */ +static int config_uart(void) +{ + struct auth_xp_serial_params xp_params; + + uart_dev = device_get_binding(DT_LABEL(DT_NODELABEL(uart0))); + + int err = uart_configure(uart_dev, &uart_cfg); + + if (err) { + LOG_ERR("Failed to configure UART, err: %d", err); + return err; + } + + /* If successful,then init lower transport layer. */ + xp_params.uart_dev = uart_dev; + + err = auth_xport_init(&auth_conn_serial.xport_hdl, + auth_conn_serial.instance, + AUTH_XP_TYPE_SERIAL, &xp_params); + + if (err) { + LOG_ERR("Failed to initialize authentication transport, error: %d", err); + } + + return err; +} + + +void main(void) +{ + struct auth_optional_param *opt_parms = NULL; + + log_init(); + +#if defined(CONFIG_AUTH_DTLS) && defined(CONFIG_AUTH_CHALLENGE_RESPONSE) +#error Invalid authentication config, either DTLS or Challenge-Response, not both. +#endif + + uint32_t flags = AUTH_CONN_CLIENT; + +#if defined(CONFIG_AUTH_DTLS) + flags |= AUTH_CONN_DTLS_AUTH_METHOD; + + /* set TLS certs */ + opt_parms = &tls_certs_param; + printk("Using DTLS authentication method.\n"); +#endif + + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) + flags |= AUTH_CONN_CHALLENGE_AUTH_METHOD; + + /* Use different shared key */ + opt_parms = &chal_resp_param; + printk("Using Challenge-Response authentication method.\n"); +#endif + + + /* init authentication library */ + int err = auth_lib_init(&auth_conn_serial, AUTH_INST_1_ID, + auth_status_callback, NULL, + opt_parms, flags); + + /* If successful, then configure the UAR and start the + * authentication process */ + if (!err) { + + /* configure the UART and init the lower serial transport */ + err = config_uart(); + + /* start authentication */ + if (!err) { + err = auth_lib_start(&auth_conn_serial); + + if (err) { + LOG_ERR("Failed to start authentication, err: %d", err); + } + } + + } + + /* does not return */ + idle_process(); + + /* should not reach here */ +} diff --git a/samples/authentication/serial/auth_server/CMakeLists.txt b/samples/authentication/serial/auth_server/CMakeLists.txt new file mode 100644 index 000000000000..524177640d57 --- /dev/null +++ b/samples/authentication/serial/auth_server/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(auth_serial_server) + + +target_include_directories(app PUBLIC ${CMAKE_SOURCE_DIR}) + +zephyr_include_directories(${CMAKE_SOURCE_DIR}/src) + +target_sources(app PRIVATE + src/main.c +) + +#zephyr_library_include_directories(${CMAKE_SOURCE_DIR}) diff --git a/samples/authentication/serial/auth_server/dtls.prj.conf b/samples/authentication/serial/auth_server/dtls.prj.conf new file mode 100644 index 000000000000..e3e8b581f6f5 --- /dev/null +++ b/samples/authentication/serial/auth_server/dtls.prj.conf @@ -0,0 +1,65 @@ +# +# Project configuration to us if DTLS authentication method +# is enabled. +# + +CONFIG_MAIN_STACK_SIZE=4096 + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +CONFIG_AUTH_LIB=y +CONFIG_SERIAL_XPORT=y +CONFIG_AUTH_DTLS=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# Don't use UART backend for logging, will collide with serial link +CONFIG_LOG=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y + +# For production products, should use a real hardware +# random number generator. +CONFIG_TEST_RANDOM_GENERATOR=y + +# Mbed config'CONFIG_MBEDTLS=y +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h" +CONFIG_MBEDTLS_TLS_VERSION_1_2=y +CONFIG_MBEDTLS_DTLS=y +CONFIG_MBEDTLS_ENTROPY_ENABLED=y + +# Supported key exchange modes +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED=y + +# Supported elliptic curves +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y + +# Supported cipher modes +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y +CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y +CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y + + + +# Supported message authentication methods +CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y +CONFIG_MBEDTLS_MAC_CMAC_ENABLED=y + + +# Other configurations +CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=1500 +CONFIG_MBEDTLS_DEBUG=y +CONFIG_MBEDTLS_DEBUG_LEVEL=0 +CONFIG_MBEDTLS_ENABLE_HEAP=y + +# Mbed uses a chunk of memory, it might be possible to reduce +# this heap usage. +CONFIG_MBEDTLS_HEAP_SIZE=65535 +CONFIG_APP_LINK_WITH_MBEDTLS=y + + diff --git a/samples/authentication/serial/auth_server/prj.conf b/samples/authentication/serial/auth_server/prj.conf new file mode 100644 index 000000000000..34230db03adf --- /dev/null +++ b/samples/authentication/serial/auth_server/prj.conf @@ -0,0 +1,26 @@ +# +# Project configuration to us if Challenge-Response authentication method +# is enabled. +# + + +# Increase stack due to settings API usage +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +CONFIG_MAIN_STACK_SIZE=1024 +CONFIG_AUTH_LIB=y +CONFIG_SERIAL_XPORT=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# For production products, should use a real hardware +# random number generator. +CONFIG_TEST_RANDOM_GENERATOR=y + +# Don't use UART backend for logging, will collide with serial link +CONFIG_LOG=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PRINTK=y \ No newline at end of file diff --git a/samples/authentication/serial/auth_server/sample.yaml b/samples/authentication/serial/auth_server/sample.yaml new file mode 100644 index 000000000000..8fe6dbd28d6b --- /dev/null +++ b/samples/authentication/serial/auth_server/sample.yaml @@ -0,0 +1,8 @@ +sample: + description: Auth Server serial transport sample app + name: Auth Server Serial +tests: + sample.auth_server_serial: + harness: bluetooth + platform_allow: qemu_x86 + tags: authentication \ No newline at end of file diff --git a/samples/authentication/serial/auth_server/src/main.c b/samples/authentication/serial/auth_server/src/main.c new file mode 100644 index 000000000000..774cc3bf96f7 --- /dev/null +++ b/samples/authentication/serial/auth_server/src/main.c @@ -0,0 +1,233 @@ + +/* main.c - Application main entry point + * Sample authenticating over a UART link */ + +/* + * SPDX-License-Identifier: Apache-2.0 + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +#include + + +#if defined(CONFIG_AUTH_DTLS) +#include "../../../certs/auth_certs.h" +#endif + +LOG_MODULE_REGISTER(auth_serial_server, CONFIG_AUTH_LOG_LEVEL); + + + +static struct uart_config uart_cfg = { + .baudrate = 115200, + .parity = UART_CFG_PARITY_NONE, + .stop_bits = UART_CFG_STOP_BITS_1, + .data_bits = UART_CFG_DATA_BITS_8, + .flow_ctrl = UART_CFG_FLOW_CTRL_NONE, +}; + +#if defined(CONFIG_AUTH_DTLS) +/* The Root and Intermediate Certs in a single CA chain. + * plus the server cert. All in PEM format.*/ +static const uint8_t auth_cert_ca_chain[] = AUTH_ROOTCA_CERT_PEM AUTH_INTERMEDIATE_CERT_PEM; +static const uint8_t auth_dev_client_cert[] = AUTH_CLIENT_CERT_PEM; +static const uint8_t auth_client_privatekey[] = AUTH_CLIENT_PRIVATE_KEY_PEM; + +static struct auth_optional_param tls_certs_param = { + .param_id = AUTH_DTLS_PARAM, + .param_body = { + .dtls_certs = { + .server_ca_chain_pem = { + .cert = auth_cert_ca_chain, + .cert_size = sizeof(auth_cert_ca_chain), + }, + + .device_cert_pem = { + .cert = auth_dev_client_cert, + .cert_size = sizeof(auth_dev_client_cert), + .priv_key = auth_client_privatekey, + .priv_key_size = sizeof(auth_client_privatekey) + } + } + } +}; +#endif + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) +#define NEW_SHARED_KEY_LEN (32u) + +/* Use a different key than default */ +static uint8_t chal_resp_sharedkey[NEW_SHARED_KEY_LEN] = { + 0x21, 0x8e, 0x37, 0x42, 0x1e, 0xe1, 0x2a, 0x22, 0x7c, 0x4b, 0x3f, 0x3f, 0x07, 0x5e, 0x8a, 0xd8, + 0x24, 0xdf, 0xca, 0xf4, 0x04, 0xd0, 0x3e, 0x22, 0x61, 0x9f, 0x24, 0xa3, 0xc7, 0xf6, 0x5d, 0x66 +}; + + +static struct auth_optional_param chal_resp_param = { + .param_id = AUTH_CHALRESP_PARAM, + .param_body = { + .chal_resp = { + .shared_key = chal_resp_sharedkey, + }, + } +}; +#endif + +static const struct device *uart_dev; + +/* Authentication connection info */ +static struct authenticate_conn auth_conn_serial; + +/** + * Authentication status callback. + * + * @param auth_conn The authentication connection. + * @param instance Authentication instance. + * @param status Status + * @param context Optional context. + */ +static void auth_status_callback(struct authenticate_conn *auth_conn, + enum auth_instance_id instance, + enum auth_status status, void *context) +{ + printk("Authentication instance (%d) status: %s\n", instance, + auth_lib_getstatus_str(status)); + +#if defined(CONFIG_AUTH_DTLS) + if (status == AUTH_STATUS_IN_PROCESS) { + printk(" DTLS may take 30-60 seconds.\n"); + } +#endif + + if ((status == AUTH_STATUS_FAILED) || + (status == AUTH_STATUS_AUTHENTICATION_FAILED) || + (status == AUTH_STATUS_SUCCESSFUL)) { + /* Authentication has finished */ + auth_lib_deinit(auth_conn); + } +} + +static void process_log_msgs(void) +{ + while (log_process(false)) { + ; /* intentionally empty statement */ + } +} + +static void idle_process(void) +{ + /* Just spin while the BT modules handle the connection. */ + while (true) { + + process_log_msgs(); + + /* Let the handshake thread run */ + k_yield(); + } +} + +/** + * Configure the UART params. + * + * @return 0 on success, else negative error value. + */ +static int config_uart(void) +{ + struct auth_xp_serial_params xp_params; + + uart_dev = device_get_binding(DT_LABEL(DT_NODELABEL(uart0))); + + int err = uart_configure(uart_dev, &uart_cfg); + + if (err) { + LOG_ERR("Failed to configure UART, err: %d", err); + return err; + } + + /* If successful,then init lower transport layer. */ + xp_params.uart_dev = uart_dev; + + err = auth_xport_init(&auth_conn_serial.xport_hdl, + auth_conn_serial.instance, + AUTH_XP_TYPE_SERIAL, &xp_params); + + if (err) { + LOG_ERR("Failed to initialize authentication transport, error: %d", err); + } + + return err; +} + + +void main(void) +{ + struct auth_optional_param *opt_parms = NULL; + + log_init(); + +#if defined(CONFIG_AUTH_DTLS) && defined(CONFIG_AUTH_CHALLENGE_RESPONSE) +#error Invalid authentication config, either DTLS or Challenge-Response, not both. +#endif + + uint32_t flags = AUTH_CONN_SERVER; + + +#if defined(CONFIG_AUTH_DTLS) + flags |= AUTH_CONN_DTLS_AUTH_METHOD; + + /* set TLS certs */ + opt_parms = &tls_certs_param; + printk("Using DTLS authentication method.\n"); +#endif + + +#if defined(CONFIG_AUTH_CHALLENGE_RESPONSE) + flags |= AUTH_CONN_CHALLENGE_AUTH_METHOD; + + /* Use different shared key */ + opt_parms = &chal_resp_param; + printk("Using Challenge-Response authentication method.\n"); +#endif + + + /* init authentication library */ + int err = auth_lib_init(&auth_conn_serial, AUTH_INST_1_ID, + auth_status_callback, NULL, + opt_parms, flags); + + /* If successful, then configure the UAR and start the + * authentication process */ + if (!err) { + + /* configure the UART and init the lower serial transport */ + err = config_uart(); + + /* start authentication */ + if (!err) { + err = auth_lib_start(&auth_conn_serial); + + if (err) { + LOG_ERR("Failed to start authentication, err: %d", err); + } + } + + } + + /* does not return */ + idle_process(); + + /* should not reach here */ +} diff --git a/samples/index.rst b/samples/index.rst index 8581e763db30..51e059508c60 100644 --- a/samples/index.rst +++ b/samples/index.rst @@ -28,6 +28,7 @@ Samples and Demos smp/* tfm_integration/tfm_integration.rst debug/* + authentication/* .. comment To add a new sample document, please use the template available under diff --git a/tests/lib/authentication/CMakeLists.txt b/tests/lib/authentication/CMakeLists.txt new file mode 100644 index 000000000000..445aaf129642 --- /dev/null +++ b/tests/lib/authentication/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(integration) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/authentication/prj.conf b/tests/lib/authentication/prj.conf new file mode 100644 index 000000000000..7d1fac3d043b --- /dev/null +++ b/tests/lib/authentication/prj.conf @@ -0,0 +1,13 @@ +CONFIG_ZTEST=y +CONFIG_AUTH_LIB=y +CONFIG_SERIAL=y +CONFIG_SERIAL_XPORT=y +CONFIG_AUTH_CHALLENGE_RESPONSE=y +CONFIG_TINYCRYPT=y +CONFIG_TINYCRYPT_SHA256=y +CONFIG_LOG=y +CONFIG_AUTH_LOG_LEVEL=3 +CONFIG_AUTH_CHALLENGE_RESPONSE=y +CONFIG_UART_INTERRUPT_DRIVEN=y +CONFIG_TEST_RANDOM_GENERATOR=y + diff --git a/tests/lib/authentication/src/main.c b/tests/lib/authentication/src/main.c new file mode 100644 index 000000000000..5e7e7c500e4b --- /dev/null +++ b/tests/lib/authentication/src/main.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +/** + * @brief Test Asserts + * + * This test verifies various assert macros provided by ztest. + * + */ + + +static void auth_status_callback(struct authenticate_conn *auth_conn, enum auth_instance_id instance, + enum auth_status status, void *context) +{ + // dummy function +} + +static void test_auth_api(void) +{ + int ret_val = 0; + struct authenticate_conn auth_conn; + + /* init library with NULL status function callback */ + ret_val = auth_lib_init(&auth_conn, AUTH_INST_1_ID, NULL, NULL, + NULL, AUTH_CONN_SERVER | AUTH_CONN_CHALLENGE_AUTH_METHOD); + + zassert_equal(ret_val, AUTH_ERROR_INVALID_PARAM, "NULL status function param test failed."); + + /* Verify server and client roles flags fail */ + ret_val = auth_lib_init(&auth_conn, AUTH_INST_1_ID, auth_status_callback, NULL, + NULL, AUTH_CONN_SERVER | AUTH_CONN_CLIENT | AUTH_CONN_CHALLENGE_AUTH_METHOD); + + zassert_equal(ret_val, AUTH_ERROR_INVALID_PARAM, "Invalid flags test failed."); + + /* Verify DLTS and Challenge-Reponse flags fail */ + ret_val = auth_lib_init(&auth_conn, AUTH_INST_1_ID, auth_status_callback, NULL, + NULL, AUTH_CONN_SERVER | AUTH_CONN_DTLS_AUTH_METHOD | AUTH_CONN_CHALLENGE_AUTH_METHOD); + + zassert_equal(ret_val, AUTH_ERROR_INVALID_PARAM, "Invalid flags test failed."); + + // init lib with valid params + ret_val = auth_lib_init(&auth_conn, AUTH_INST_1_ID, auth_status_callback, NULL, + NULL, AUTH_CONN_SERVER | AUTH_CONN_CHALLENGE_AUTH_METHOD); + + zassert_equal(ret_val, AUTH_SUCCESS, "Failed to initialize Authentication library."); + + /* de-init */ + ret_val = auth_lib_deinit(&auth_conn); + + zassert_equal(ret_val, AUTH_SUCCESS, "Failed to start Challenge-Response authentication."); + +} + +void test_main(void) +{ + ztest_test_suite(authentication_tests, + ztest_unit_test(test_auth_api) + ); + + ztest_run_test_suite(authentication_tests); +} diff --git a/tests/lib/authentication/testcase.yaml b/tests/lib/authentication/testcase.yaml new file mode 100644 index 000000000000..463b8a93a391 --- /dev/null +++ b/tests/lib/authentication/testcase.yaml @@ -0,0 +1,4 @@ +tests: + authentication.auth: + tags: authentication + platform_allow: qemu_cortex_m3 qemu_x86