From befc810d2d157b9e8b81fdb4bc1842cd83997683 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Mon, 29 Sep 2025 14:30:46 +0200 Subject: [PATCH 1/6] Update patch files --- ...-plist.patch => 0001-plist-ubuntu22.patch} | 0 doc/user/0001-plist-ubuntu24.patch | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+) rename doc/user/{0001-plist.patch => 0001-plist-ubuntu22.patch} (100%) create mode 100644 doc/user/0001-plist-ubuntu24.patch diff --git a/doc/user/0001-plist.patch b/doc/user/0001-plist-ubuntu22.patch similarity index 100% rename from doc/user/0001-plist.patch rename to doc/user/0001-plist-ubuntu22.patch diff --git a/doc/user/0001-plist-ubuntu24.patch b/doc/user/0001-plist-ubuntu24.patch new file mode 100644 index 00000000..af368843 --- /dev/null +++ b/doc/user/0001-plist-ubuntu24.patch @@ -0,0 +1,35 @@ +--- libccid_Info.plist_org 2025-09-23 10:35:20.962252106 +0200 ++++ libccid_Info.plist 2025-09-23 10:54:39.042011175 +0200 +@@ -499,6 +499,10 @@ + 0x2C97 + 0x2C97 + 0x2C97 ++ 0x2C97 ++ 0x2C97 ++ 0x2C97 ++ 0x2C97 + 0x17EF + 0x17EF + 0x17EF +@@ -1096,6 +1100,10 @@ + 0x1009 + 0x4009 + 0x5009 ++ 0x5000 ++ 0x6000 ++ 0x7000 ++ 0x8000 + 0x6007 + 0x6055 + 0x6111 +@@ -1693,6 +1701,10 @@ + Ledger Nano S + Ledger Nano X + Ledger Nano S Plus ++ Ledger Nano S Plus Legacy ++ Ledger Flex ++ Ledger Stax ++ Ledger Apex P + Lenovo Lenovo USB Smartcard Keyboard + Lenovo Lenovo USB Smartcard Keyboard + Lenovo Lenovo Smartcard Wired Keyboard II From 2662fdd0ce169211482e582f0d0ad37997965122 Mon Sep 17 00:00:00 2001 From: Gleb Zakhartchenko Date: Tue, 30 Sep 2025 13:49:08 +0400 Subject: [PATCH 2/6] docs(macOS): add IFD CCID toggle path; keep Info.plist edits as legacy; update SIP link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recommend enabling Apple-bundled IFD CCID via `useIFDCCID` (no SIP needed) - Keep Linux/Windows instructions unchanged - Point macOS “Legacy” path to existing “Manual update of CCID” section for XML keys/values - Update SIP documentation link to the current Apple URL --- doc/user/app-openpgp.rst | 50 ++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/doc/user/app-openpgp.rst b/doc/user/app-openpgp.rst index b084a834..02dc7a5d 100644 --- a/doc/user/app-openpgp.rst +++ b/doc/user/app-openpgp.rst @@ -75,16 +75,52 @@ Linux You have to add your devices to ``/etc/libccid_Info.plist`` -MAC -~~~ +macOS +~~~~~ + +Two supported approaches: + +**A: Recommended: enable the bundled IFD CCID driver (no SIP required)** + +On recent macOS releases (Sonoma 14 and Sequoia 15), Apple ships both its own CCID driver and Ludovic Rousseau’s **IFD CCID** (included by Apple; 1.5.1 in current builds). If your Ledger is not detected or behaves oddly with the Apple driver, enable IFD CCID: + +.. code-block:: bash + + # Enable IFD CCID system-wide + sudo defaults write /Library/Preferences/com.apple.security.smartcard useIFDCCID -bool yes + +Verify the setting (``1`` means IFD CCID is enabled; “does not exist” means Apple driver is used): + +.. code-block:: bash + + defaults read /Library/Preferences/com.apple.security.smartcard.plist useIFDCCID + +To revert to the Apple driver later: + +.. code-block:: bash + + sudo defaults delete /Library/Preferences/com.apple.security.smartcard useIFDCCID + # or + sudo defaults write /Library/Preferences/com.apple.security.smartcard useIFDCCID -bool no + +Unplug/replug the device after changing the setting. A reboot is rarely necessary. + +**B: Legacy: manual device list (advanced; may require disabling SIP)** + +Only if you must maintain a custom device list for IFD CCID. Edit: + +``/usr/libexec/SmartCardServices/drivers/ifd-ccid.bundle/Contents/Info.plist`` -1. First it is necessary to disable SIP, that forbid editing files in ``/usr/``. -2. You have to add your devices to ``/usr/libexec/SmartCardServices/drivers/ifd-ccid.bundle/Contents/Info.plist`` -3. Enable SIP +.. warning:: + Editing system files may be blocked by SIP (System Integrity Protection) and is discouraged unless strictly necessary. + Prefer the single-command switch above whenever possible. -Note: See https://developer.apple.com/library/content/documentation/Security/Conceptual/System_Integrity_Protection_Guide/ConfiguringSystemIntegrityProtection/ConfiguringSystemIntegrityProtection.html +.. note:: + If you need to disable/enable SIP to edit system files, see Apple’s documentation: + https://developer.apple.com/documentation/security/disabling-and-enabling-system-integrity-protection -TBC... +See the **Manual update of CCID** section below for the required XML keys (``ifdVendorID``, ``ifdProductID``, ``ifdFriendlyName``) and Ledger device values. +We intentionally keep those details centralized in that section. Windows ~~~~~~~ From c3254649d7f1b7abb029e70b6ad230c37fedde40 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Sep 2025 12:11:10 +0200 Subject: [PATCH 3/6] Adapt to Reusable workflows --- .github/workflows/codeql_checks.yml | 31 ++----------- .github/workflows/python_client_checks.yml | 32 ++++---------- .github/workflows/python_tool_checks.yml | 35 ++++----------- .github/workflows/unit_tests.yml | 51 +++------------------- pytools/gpgapp/gpgcard.py | 7 ++- 5 files changed, 28 insertions(+), 128 deletions(-) diff --git a/.github/workflows/codeql_checks.yml b/.github/workflows/codeql_checks.yml index ff59de9c..9b559d7e 100644 --- a/.github/workflows/codeql_checks.yml +++ b/.github/workflows/codeql_checks.yml @@ -14,31 +14,6 @@ on: jobs: analyse: - name: Analyse - strategy: - fail-fast: false - matrix: - sdk: ["$NANOX_SDK", "$NANOSP_SDK", "$STAX_SDK", "$FLEX_SDK", "$APEX_P_SDK"] - # 'cpp' covers C and C++ - language: ['cpp'] - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest - - steps: - - name: Clone - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - # CodeQL will create the database during the compilation - - name: Build - run: | - make BOLOS_SDK=${{ matrix.sdk }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + name: Call Ledger CodeQL analysis + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_codeql_checks.yml@v1 + secrets: inherit diff --git a/.github/workflows/python_client_checks.yml b/.github/workflows/python_client_checks.yml index e86f3d4b..3640fd55 100644 --- a/.github/workflows/python_client_checks.yml +++ b/.github/workflows/python_client_checks.yml @@ -14,27 +14,11 @@ on: jobs: lint: - name: Client linting - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v4 - - name: Installing PIP dependencies - run: | - pip install pylint - pip install -r tests/ragger/requirements.txt - - name: Lint Python code - run: pylint --rc tests/ragger/setup.cfg tests/ragger/application_client/ - - mypy: - name: Type checking - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v4 - - name: Installing PIP dependencies - run: | - pip install mypy - pip install -r tests/ragger/requirements.txt - - name: Mypy type checking - run: mypy tests/ragger/application_client/ + name: Call Ledger Python linters + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_python_checks.yml@v1 + with: + run_linter: pylint + run_type_check: true + src_directory: application_client + setup_directory: tests/ragger + req_directory: tests/ragger diff --git a/.github/workflows/python_tool_checks.yml b/.github/workflows/python_tool_checks.yml index 01d31c84..2c948b91 100644 --- a/.github/workflows/python_tool_checks.yml +++ b/.github/workflows/python_tool_checks.yml @@ -14,29 +14,12 @@ on: jobs: lint: - name: Client linting - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v4 - - name: Installing PIP dependencies - run: | - sudo apt-get update && sudo apt-get install -y libpcsclite-dev - pip install pylint - pip install -r pytools/requirements.txt - - name: Lint Python code - run: pylint --rc pytools/setup.cfg pytools/ - - mypy: - name: Type checking - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v4 - - name: Installing PIP dependencies - run: | - sudo apt-get update && sudo apt-get install -y libpcsclite-dev - pip install mypy - pip install -r pytools/requirements.txt - - name: Mypy type checking - run: mypy pytools/ + name: Call Ledger Python linters + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_python_checks.yml@v1 + with: + run_linter: pylint + run_type_check: true + src_directory: . + setup_directory: pytools + req_directory: pytools + additional_packages: libpcsclite-dev diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 3dd30a67..acacc350 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -10,49 +10,8 @@ on: jobs: job_unit_test: - name: Unit test - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest - - steps: - - name: Clone - uses: actions/checkout@v4 - - - name: Build unit tests - run: | - cd tests/unit - cmake -Bbuild -H. && make -C build - - - name: Run the tests - run: | - cd tests/unit - make -C build test - - - name: Generate code coverage - run: | - cd tests/unit - lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base - lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture - lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info - lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/tests/unit/*' -o coverage.info - genhtml coverage.info -o coverage - - - uses: actions/upload-artifact@v4 - with: - name: code-coverage - path: tests/unit/coverage - - - name: Install codecov dependencies - run: apt install --no-install-recommends -y curl gpg - - - name: Upload to codecov.io - uses: codecov/codecov-action@v5 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - with: - files: ./tests/unit/coverage.info - flags: unittests - name: codecov-app-openpgp - fail_ci_if_error: true - verbose: true + name: Call Ledger unit_test + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_unit_tests.yml@v1 + secrets: inherit + with: + test_directory: tests/unit diff --git a/pytools/gpgapp/gpgcard.py b/pytools/gpgapp/gpgcard.py index b436a177..abe2d5e2 100644 --- a/pytools/gpgapp/gpgcard.py +++ b/pytools/gpgapp/gpgcard.py @@ -22,15 +22,14 @@ from hashlib import sha1 from typing import Optional, Tuple from dataclasses import dataclass +# pylint: disable=import-error from Crypto.PublicKey.RSA import construct +from ledgercomm import Transport # type: ignore +# pylint: enable=import-error from gpgapp.gpgcmd import DataObject, ErrorCodes, KeyTypes, PassWord, PubkeyAlgo # type: ignore from gpgapp.gpgcmd import KEY_OPERATIONS, KEY_TEMPLATES, USER_SALUTATION # type: ignore -# pylint: disable=import-error -from ledgercomm import Transport # type: ignore -# pylint: enable=import-error - APDU_MAX_SIZE: int = 0xFE APDU_CHAINING_MODE: int = 0x10 From 580b68d18635312831b94db69ab88419966241d6 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Sep 2025 11:43:40 +0200 Subject: [PATCH 4/6] Update nbgl_useCaseKeypadPin usage --- src/gpg_ux_nbgl.c | 46 +++++++--------------------------------------- 1 file changed, 7 insertions(+), 39 deletions(-) diff --git a/src/gpg_ux_nbgl.c b/src/gpg_ux_nbgl.c index 5c752b29..269e7dc9 100644 --- a/src/gpg_ux_nbgl.c +++ b/src/gpg_ux_nbgl.c @@ -1168,12 +1168,6 @@ void ui_menu_pinconfirm_display(unsigned int value) { /* ------------------------------ PIN ENTRY UX ----------------------------- */ -// clang-format off -enum { - TOKEN_PIN_ENTRY_BACK = FIRST_USER_TOKEN, -}; -// clang-format on - static void ui_menu_pinentry_cb(void); /** @@ -1288,22 +1282,6 @@ static void pinback_cb(void) { ui_init(); } -#ifdef SCREEN_SIZE_WALLET -/** - * @brief Pin Entry Action callback - * - * @param[in] token button Id pressed - * @param[in] index widget index on the page - * - */ -static void pinentry_cb(int token, uint8_t index) { - UNUSED(index); - if (token == TOKEN_PIN_ENTRY_BACK) { - pinback_cb(); - } -} -#endif // SCREEN_SIZE_WALLET - /** * @brief Pin Entry page display * @@ -1343,23 +1321,13 @@ void ui_menu_pinentry_display(unsigned int step) { minLen = (G_gpg_vstate.io_p2 == PIN_ID_PW3) ? GPG_MIN_PW3_LENGTH : GPG_MIN_PW1_LENGTH; // Draw the keypad -#ifdef SCREEN_SIZE_WALLET - nbgl_useCaseKeypadPIN(G_gpg_vstate.menu, - minLen, - GPG_MAX_PW_LENGTH, - TOKEN_PIN_ENTRY_BACK, - false, - TUNE_TAP_CASUAL, - pinentry_validate_cb, - pinentry_cb); -#else // SCREEN_SIZE_WALLET - nbgl_useCaseKeypadPIN(G_gpg_vstate.menu, - minLen, - GPG_MAX_PW_LENGTH, - false, - pinentry_validate_cb, - pinback_cb); -#endif // SCREEN_SIZE_WALLET + nbgl_useCaseKeypad(G_gpg_vstate.menu, + minLen, + GPG_MAX_PW_LENGTH, + false, + true, + pinentry_validate_cb, + pinback_cb); } /** From 9b8b997fac72312b1267158b8775bd1a4a4e402f Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Sep 2025 12:10:49 +0200 Subject: [PATCH 5/6] Bump version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 59f7b852..c132b8e9 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ APPNAME = OpenPGP # Application version APPVERSION_M = 2 APPVERSION_N = 5 -APPVERSION_P = 0 +APPVERSION_P = 1 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" DEFINES += SPEC_VERSION='"3.3.1"' From 5ca725231e13ec8dcacfc92ce9706c51760b8cfd Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Tue, 30 Sep 2025 09:23:41 +0200 Subject: [PATCH 6/6] Update snapshots --- .../apex_p/test_menu_settings/00011.png | Bin 3379 -> 3406 bytes .../apex_p/test_menu_settings/00012.png | Bin 3317 -> 3342 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/ragger/snapshots/apex_p/test_menu_settings/00011.png b/tests/ragger/snapshots/apex_p/test_menu_settings/00011.png index bd6b4f305c218e34a9b1d9b101d370f6d286bf0e..ffaaf168f5b581f8e032804a28663f89224a90bb 100644 GIT binary patch literal 3406 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 literal 3379 zcmdT{X*e6$8aAd?YnzU38e7v&Yb`}ljOc`pwu+V#TLfciirNW6s$xvrY1C*LjcvLJ zwM4{P4V`FOMX0JJ2_m*2Bw`m0m*@Vy&olS>cc14yKi>D8?>y(nd*1Jyd)FN8ut5itj2o<&Iq~xN1zyc#d z7{G5b#I28!HkZC!gv3ymR}-!7=GvwUvFnIe2oP`iMSdG8&Q=gv{dJ%o z7n?{O?2y?YS=eZ$YWMvBl^}P<2bl9hC!9NLXDKckmX)s4r;- z!Vr0pHmnFa6WAsfW~2E2-&z$2%hwC>X&0v270(6CdrAi?fW(gV?Jt*Z5GQCS+Qx3Y>k@~ zIiHtc!eF_fZ%=Rn(tVc4kTC$NQxctEO|{8(T(ZN5Yb4~#Y&P{9_NXwXmgt@_MGt@` z-?y_aB_pW9;DVt_2izJ8Ja|{=-V;F`V$aOGZj_4{aFrtSs1+D<-HMd(X-!GZTKH4* zQ>A0=CJd&FB5n=qCSWYJmj(MexmyzmtD2qiAG4+vWeg7cZ_T-cP9)`CDQYr*#4GJ# zNF=|;#o2SP*1=#y)q(;@NlA}_O57FkL23Tb%ZUSqy3x^FJYj7uQKZPb$sk9c?|utt zVLi!N7F}HF=iI71-}?|(-x-REk4L@wC3@`* zI9Zr*a?TisohvM9_O*)<%sJHcKwb6!VCAH1lrjG*tezSQF>WI1zMoK>QQ@_$TR0ch z!GN851E?@oFRYL8LeQlh&fT~4BmZ~CrHM&W2>RDWXH7yK<1Zr|r;9mdVl ztl{lqtXYV6V7K!QxU43!nZ_QClY$GJmlX9a*WFYd@&Sd}655bUnH>g(Rh#!L)^%sH z?VG3V-&0+y6s^6p<0kasN9q~qz-J#AFD|?=dMM$_NA5YSSKuRfHzogNMS0^c`d6ApM;` zk8K2s{@x0rO+(DAn~PcmUiVXtf|F%PUNCt z{ESqqz!bI^#|(jNDD5(K`931(-zB0Xg2K#h5Cy2ahso8*+JUZ0?jQ55F$a&2k%xU7 zoi&np7cbMN{St)?VF z>-DlshS(BP!P0(aXn|i!RntWS+3lrcZLVQuZJ%^p^5c1ghK6lN z==rwC&Lz04kFexN=M}b7PY;i|&MhaBf#0s7axyZW@T$m*&)A@f^)FBCnenxw%otgd z2X;kZ4^;rK(#cNcL9BbBLtH-|xst}fxmBPC?u{^N5j~vfGo_iUve4hvu-!iCtqg*w zPLOxfj#Zb;GR7SW3-1*?*QhN=WTkpPCtJA^5&WBqgfp4>i4tp=)n%b4y5`3$RfG?5 zar}5a0ZcV*i7#8p!+kr86;_0O!Z#xtVt5 z*_O@zKl-G$uz5*@ldB%gm&7iA+Tfbnh)JabuqS_MEqOGE_5JLoWvixM;gKLG{RVoz zr=F7KrQ}!jMDUq2uYM$zFVYr{%ppndU8nU{`Pn)u4NO&DSni^t!6`VqCOVwj`%a2& zj(Btevr_u?yNRborO~Z-!A-RxEdk!%Q=Wo06NDLWY1Ow}1U;Qt;UI0R(|7aQDpq;d zVajTJMJHM8ZUV8j{<$x&Rv5Z$9u#IkFEka81J6LK&PFCeB)RY~8l9XDz30#tF1hUH zy?!g&?+>zW?(Hh6CRMG0L7BXP>PGv>*thf?s`wzb&J7%Ia_ju-`g0MYJfVbIAdgWT zY50a_b0h!rf!jfsoEqm@_d~|4UlncDK^U1e9+V)p0fo%-RyWCgx6vL0relu6BnpQT znnrWI?FDvkmPXKv1Me~8y#@x}kM?8I6i2Lw}R!!7BKuRa_n>|#}E&==J442Y!4&z#*k=BHiZ z+_7965XLQSZ&BNCyTb1Mm_N%EIyRN%0U+f^rBITfbHK-MGXRhWk$WT*_;inq%p3q? z06O4DdxUx}Hto9&0Hk!QbF^_!HW^WJ+$>3|VfKONvLYm62O2f*YV^Xw#?E7W^Iz~T<5cnp7HrJB4O3b230kWnE|DeU3kwc zIJ%Ai+I4txpL?}&+Ci6HDUUh)sU;PZqIkaw!wmp-`Qs=>0-ke_?*@k9=xyRa)lpFT zq!u$|`6Fn@1c>Q7dB$ny4%zI-_2e^mW;>ootWGa+yB)>uWK2MKSnnr^r~B%NI>B@R z3sWz(np*U>H#QjNgl;E(OLBSQzi-iWr4=oRI`+leH4$coL?Fk_9ZsGk1* zi*oT~zJhl)nI(xs-%y)m^v2X4m!HM2nJ!p%m2&V*ruCj9Q?b;f6}J-=ug9%!d=JuH z9ndAHga0t(2s@Xa6t!(Rrt#+6uj{14o`Sv}g5-&<4sX_@?{?VOk-7{uW#DmYE!xg8 zt!U%HPW9!FGZqiIDrhN*h~;outP*b#N{yZG67`gCS=j5+(6FxHhl{fXC| zX>-0aKUPjLOUm?Y6zjI|EF8PC+TbvD3^#^R@t zr%ysr-b%mAZEcF!ctFVv;+9u}r)>v!NZNh(ns0OU&l3YVOV2}u5-yDR#+&TTv4HQd z4|mqSM~g#W)1WjsH2;Jij#`xAOyTWp!*E#{OFQG#m`4P1fQBgLpQ1sk)`ET3Z&UYTC(eUWGCQAF6D;aBs{8yRh;QN3jboAThSkL#w2tEhcxlHKjmG@FT z%ZsAcI5g-3w)!JUPHHNvmbBNWcGSaGbQ~TA6k?O^-1nYb#q4`)BTiNe^P$2NPq_HoNt7TY}4v9P=M*vK0$W9*i)PJ5qDm_BXb^FAa2) z7~t25Y}ylYSQqX0wWEPazhxFi#adh8J*L6b*H~ zennIy{oZ!sz2w0_(ys8hA2Qg<8tMDj2B4lzsE(4=G_xo(GxDRA)q@7+P`_7v4vE>) zL^SemEdANRu7w|<#oW{sg6y7sZ4w9=D^FC9U#ZCSovVhlTIi*kg9%@a3S|%Q_U)CC zt$7%n3lXj_b?QV$PT*3u!)1F6cL|F9Y{B+HI$0;-N#g^A%yKJs`_Kt{eC2U9GMCn8 zNO=CCkv&u8lpQ40btufZdJPDS6x!%sg11c~{rc0e`u$`|!;4p`?7V(4mYhL;VRxt3 zVw{iq>Ap@AMVm9)FuR zTa+Pq&j4_(!ZFf?f@W^>Dvw%3*7hT--TLGr+|jtQO;SEsF?xUlnAK}i`W{KBmS*42 zWnN#KE@!Lv6#E{6MAo;`BaS zH)X;i!4mJ2L)(+6ps;F}C6hG0*Cq7P6QYVnzHny0p*&&a_yg|3+vygH1(`h>_m~Pr zLb1`c5s)XvbhAQhq^5k41+A`fekG@4V{e5CruAJe0Il>`3rnU=TLTUpMFZSRw)_OL z7&JkRXDCiae5#u4tWgISiLPJ84eps}1I`EL?vt#A@z-t)5;v{J_=GDLoHz{w1f>;$ z4imlrp7c>J)2a)ed|*SN>6%|NrvM&ya|Z;?xFcd>%f*+k^55K3?$y7$p#STP-5hcy z;rlFip#R=0G-6j@NLxQb!#xP^wGrwkyXGtUuBrX+;!o1nvV>y)7sO5{wPOZpwe$xm zrg?92bb7SP>>jUIF8?NAMl-ye<$keYf{5m9F2v2pPf|T)pG{H4Jy|ZCt2a0HSjZm| z)#$zcmE<5ZF~2Yy`ck*B2TOVjpBf**ZA9|+Q|2ey(_w;lD+Cco?XXe3;7#5V`{?@4 zEE8my2K6DCUxt_x9ZNvb8*nDLC!3i_KWV-2PzRBPHt8yzG3xkSetywbEQ!1c@w{|- zJX}Gg=*(IG?H9eu80>etMWH@pLcycqZG)iYd1?_pS9>%uk%`&W8tktHds_IV zsQ)jsK+%!(kh8hZ_?X2H=>FM#=FjXx|KGm?#`uD)_WIkD)OP07Qlg6W|6nBYo9?%cs5f?#cG*1KVX6E>T@^|Ub$0FhOvrvdKM z{bf`1&5s{FKA#2R+rE7MD7=FJ3VnQT*a<-FdGW2?CV6yWlLYa*J`>iDMsr|-N0g(g z0Sutl+Xp=VSvYNm9oe-Cf(am&cQnC-(_b}6DwJC!x*vA#+wlB+=cqXU;C8kQ_Scku E0hjBwi5z)*A!4(w{y>{;XaevL+nfYFu1yfZaQlEYa_t zOlQKgXnVXy2KN=vIl)l&S?>-Df7-ZyQv=#eRb81hs*i(h-r?!+w~;7u>!Fr}8{)-K6DPjR*J>eHg!-W(tM{&a>bnc~7;-Kx*$p!|`S3o^RXZuucYF9gkOWhvXESi_^SzrCe1 zj$8t0|5?nl(S{BRR*U0!1-<=JR}%^qkK_J2)(}m*6&Jljsz%yw$?8XTDwyYy>sDlp zfK#3y?Z+Sc#XrtFbutzvYYLna%Q=h(yQD}zuyX(i!&W|DXB=BEehtE5#RinB<x$)F-?$0&ws=djdw*ycCg#J!W-=8`c*k#dnsbS*pv@ZB4fV`LX}h5OK{}m* z@RBtjW;-cxf+Y<6wf-0kPt2sI7x;%RRKivE;_SG6J@5Q97y(aUf|Gnn(OSttZk}=Y zh1#0FLc8nyd-p??`Rig)yXP=v<6{O{i%D6?VCB@4W;Vy*7X7uIpqO0PIg7}#j3Ffk zo$Dp(5}gk#iKkIh2{LCR&Lc1cMoKKDA!yK$1d7&Z^5&Ie zk_0U@noXS@h~$e1Un)rp4|Kx{hI>YIQUVeJh=PIX+xSB>g;^?xwOeODi0~^D=<=JW zwH63Q6mxox_%hwX?K;NDoIf!z;*Pac^SV#rl%lE6Sx8J@U4x$$B9_A8ueb-y5e2`z zGfFjPqj~d$py1w!8=WI4OmkeXcK5<&?xJ}QA~KBJ&F}ZC9&(s-txFRG1zn!XfWk}u|9Mh4EI>`8N$zdMaej7D7s_-YX9hc z-9%P!Z-}ovyq<}@^{RtWbhgMS?YKL#?bb9+0r>GA#S?f4sEs?-e)*4;g?sg8$@0Ec zk_5Qfy`3zs*$&7C=Yw!nTLX+5)=y5!m4=dpQ*f@pUQ>=_J#wwFHineZvCS?CCO1!L zregI8}wqSRpCYoznl@^JE0&)sX^o89g_H_mmOl_5E8bBo<{ zC1=T%!NV-32PQ?_gk;;{eK6KF_;xCMR)NERUz+M3BKG*6?72|EAUCrLW)*3EC~j9} zBj1{KZ;4KT(4t03%iW-Ce&lv$dn4BvB|jTw)w!q%OuBmQ*lD>q7t%C8jgr^ z1ni8Up&Nlvz>U9#?Oa`H^my#vjJ+OSaeDvD%l(dNh`~Nz0U``chwhFW#XqOr5J&ny14uZrk&U*EwK`1X3^hgMp& zU%#>NT_X)n?9;_&az2)gce~$x-QKU`Zs6lEwU+zNjm! ziN2M)_`!*T45X?8iy@CX3JtHXM@2?lndLM0fipF!Xt+;fE#?BZ(Xqps*swkbN|mA( z3T6XuR?BmVT?HcL^%$3^z7zpnZ3s^`;l;Efr3LjloRr4)9@^!NHxdRyJA)z*V0sN{G;u%akix{nisZ6F;C*BHyZ&=)KWV(wL?OGF3EG-%Z zqViNz`~(=v6M*F*s<0)R$%|YKGX7PBe}swGzm*Eo6eMNWNe4oY!F8e zWQgvNi>g2Rb(4H8n}eB|XM7JWuaVgy>_f9c%T|zHu7V1jKpM`url(@*fGMx9hUn{; zTm4wJ$l}50B8Z>e{hx}~9iF9)@Z{ImE^Z{jy6hU}N4-MN7#^{T+jH`Z?tl#m!elk^ z!PW6zLS$z{!+(TmY>M0EA+Ef%I+UrL(;PnJGCIk1;^sUM-xy{-ME0_k#ynNj=LumW%Wo~*>_6<$zc>mR zsi`wrD>$BAvDmXLG|R)#MOi&+dGz@cI7~F$Y}5OC*e|A8JF4?o?9WrFi8@WBSUz#D zw;CpBC&nX&EyT2LTQ9dsDHtheKI)L`Gz7+<+HuW44-Go)`tbwMGU3JXexR)O z9d!3t%a{9Fg{|4iL@2-**;$}SO&>N_h7Oq;?-U$APdNh$28woufTxb`6n0EwuU->H z+tgSI8fbhp)>IpJ;mJ1