From 5b1c504cdbdd12850a3272dd2e13a64a5026f555 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Wed, 19 Apr 2023 11:47:44 +0100 Subject: [PATCH] Remove signed HTTP request support (#5137) --- .daily_canary | 3 +- CHANGELOG.md | 1 + CMakeLists.txt | 15 - cmake/common.cmake | 1 - doc/build_apps/api.rst | 7 - doc/build_apps/js_app_bundle.rst | 2 - doc/governance/accept_recovery.rst | 28 -- doc/governance/adding_member.rst | 9 +- doc/governance/common_member_operations.rst | 56 --- doc/governance/hsm_keys.rst | 43 +- doc/governance/open_network.rst | 53 --- doc/governance/proposals.rst | 62 +-- doc/schemas/app_openapi.json | 45 +- doc/schemas/gov_openapi.json | 28 +- doc/schemas/node_openapi.json | 2 +- doc/use_apps/issue_commands.rst | 23 +- include/ccf/common_auth_policies.h | 12 - include/ccf/endpoint.h | 2 - .../ccf/endpoints/authentication/cose_auth.h | 6 +- .../ccf/endpoints/authentication/sig_auth.h | 104 ----- js/ccf-app/src/endpoints.ts | 12 - samples/apps/logging/logging.cpp | 68 +-- src/apps/js_generic/js_generic_base.cpp | 16 - src/apps/js_generic/named_auth_policies.h | 14 - src/apps/tpcc/app/tpcc.cpp | 20 +- src/clients/perf/perf_client.h | 1 + src/clients/rpc_tls_client.h | 6 - src/endpoints/authentication/sig_auth.cpp | 221 ---------- src/http/http_rpc_context.h | 1 - src/http/http_sig.h | 405 ------------------ src/http/test/http_test.cpp | 179 -------- src/node/http_node_client.h | 4 - src/node/rpc/member_frontend.h | 76 +--- src/node/rpc/node_frontend.h | 2 +- src/node/rpc/test/frontend_test.cpp | 153 ------- src/node/rpc/test/frontend_test_infra.h | 38 -- src/node/rpc/test/proposal_id_test.cpp | 210 --------- tests/e2e_logging.py | 34 -- tests/governance.py | 95 +--- tests/governance_js.py | 62 +-- tests/infra/clients.py | 57 ++- tests/infra/consortium.py | 3 +- tests/infra/e2e_args.py | 5 - tests/infra/member.py | 2 + tests/infra/network.py | 8 +- tests/js-authentication/app.json | 9 +- tests/js-authentication/src/endpoints.js | 14 - tests/jwt_test.py | 3 +- tests/lts_compatibility.py | 18 +- tests/memberclient.py | 10 +- tests/sandbox/sandbox.sh | 1 - 51 files changed, 156 insertions(+), 2093 deletions(-) delete mode 100644 include/ccf/endpoints/authentication/sig_auth.h delete mode 100644 src/endpoints/authentication/sig_auth.cpp delete mode 100644 src/http/http_sig.h delete mode 100644 src/node/rpc/test/proposal_id_test.cpp diff --git a/.daily_canary b/.daily_canary index 60a84164f9..09f85a339f 100644 --- a/.daily_canary +++ b/.daily_canary @@ -1,5 +1,4 @@ --- ___ (- -) (o o) | Y & +- ( V ) z O z O +---=---' -/--x-m- /--m-m---xXx--/--yY----- - +/--x-m- /--m-m---xXx--/--yY----- \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e525fdea..3a6f1a5f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Removed +- Support for HTTP request signing has been removed (#5137). Governance requests must use COSE Sign1 signing instead, see [documentation](https://microsoft.github.io/CCF/main/use_apps/issue_commands.html#cose-sign1) for details. - Removed experimental 2tx reconfiguration mode, and the associated "reconfiguration_type" config option (#5179). ## [4.0.0-rc1] diff --git a/CMakeLists.txt b/CMakeLists.txt index bfd0a6bcf1..8dd465f0bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -512,21 +512,6 @@ if(BUILD_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/tx_status_test.cpp ) - add_unit_test( - proposal_id_test ${CMAKE_CURRENT_SOURCE_DIR}/src/js/wrap.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/proposal_id_test.cpp - ) - target_link_libraries( - proposal_id_test - PRIVATE ${CMAKE_THREAD_LIBS_INIT} - http_parser.host - sss.host - ccf_endpoints.host - ccfcrypto.host - quickjs.host - ccf_kv.host - ) - add_unit_test( node_frontend_test ${CMAKE_CURRENT_SOURCE_DIR}/src/js/wrap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/node_frontend_test.cpp diff --git a/cmake/common.cmake b/cmake/common.cmake index 57a9c44213..aacc86bd36 100644 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -179,7 +179,6 @@ set(CCF_ENDPOINTS_SOURCES ${CCF_DIR}/src/endpoints/authentication/cert_auth.cpp ${CCF_DIR}/src/endpoints/authentication/empty_auth.cpp ${CCF_DIR}/src/endpoints/authentication/jwt_auth.cpp - ${CCF_DIR}/src/endpoints/authentication/sig_auth.cpp ${CCF_DIR}/src/enclave/enclave_time.cpp ${CCF_DIR}/src/indexing/strategies/seqnos_by_key_bucketed.cpp ${CCF_DIR}/src/indexing/strategies/seqnos_by_key_in_memory.cpp diff --git a/doc/build_apps/api.rst b/doc/build_apps/api.rst index cc40d12a10..02efb9d9b1 100644 --- a/doc/build_apps/api.rst +++ b/doc/build_apps/api.rst @@ -60,9 +60,6 @@ Policies .. doxygenvariable:: ccf::user_cert_auth_policy :project: CCF -.. doxygenvariable:: ccf::user_signature_auth_policy - :project: CCF - .. doxygenvariable:: ccf::jwt_auth_policy :project: CCF @@ -77,10 +74,6 @@ Identities :project: CCF :members: -.. doxygenstruct:: ccf::UserSignatureAuthnIdentity - :project: CCF - :members: - Supporting Types ---------------- diff --git a/doc/build_apps/js_app_bundle.rst b/doc/build_apps/js_app_bundle.rst index 7ea576305a..cb0b6cc550 100644 --- a/doc/build_apps/js_app_bundle.rst +++ b/doc/build_apps/js_app_bundle.rst @@ -69,9 +69,7 @@ Each endpoint object contains the following information: is executed. An empty list indicates an unauthenticated endpoint which can be called by anyone. Possible entries are: - ``"user_cert"`` - - ``"user_signature"`` - ``"member_cert"`` - - ``"member_signature"`` - ``"jwt"`` - ``"no_auth"`` diff --git a/doc/governance/accept_recovery.rst b/doc/governance/accept_recovery.rst index 6341e5dcb9..770f140731 100644 --- a/doc/governance/accept_recovery.rst +++ b/doc/governance/accept_recovery.rst @@ -54,34 +54,6 @@ A member proposes to recover the network and other members can vote on the propo "state": "Accepted" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem --data-binary @transition_service_to_open.json -H "content-type: application/json" - { - "ballot_count": 0, - "proposal_id": "1b7cae1585077104e99e1860ad740efe28ebd498dbf9988e0e7b299e720c5377", - "proposer_id": "d5d7d5fed6f839028456641ad5c3df18ce963bd329bd8a21df16ccdbdbba1eb1", - "state": "Open" - } - - $ scurl.sh https:///gov/proposals/1b7cae1585077104e99e1860ad740efe28ebd498dbf9988e0e7b299e720c5377/ballots --cacert service_cert.pem --signing-key member2_privk.pem --signing-cert member2_cert.pem --data-binary @vote_accept.json -H "content-type: application/json" - { - "ballot_count": 1, - "proposal_id": "1b7cae1585077104e99e1860ad740efe28ebd498dbf9988e0e7b299e720c5377", - "proposer_id": "d5d7d5fed6f839028456641ad5c3df18ce963bd329bd8a21df16ccdbdbba1eb1", - "state": "Open" - } - - $ scurl.sh https:///gov/proposals/1b7cae1585077104e99e1860ad740efe28ebd498dbf9988e0e7b299e720c5377/ballots --cacert service_cert.pem --signing-key member3_privk.pem --signing-cert member3_cert.pem --data-binary @vote_accept.json -H "content-type: application/json" - { - "ballot_count": 2, - "proposal_id": "1b7cae1585077104e99e1860ad740efe28ebd498dbf9988e0e7b299e720c5377", - "proposer_id": "d5d7d5fed6f839028456641ad5c3df18ce963bd329bd8a21df16ccdbdbba1eb1", - "state": "Accepted" - } - Once the proposal to recover the network has passed under the rules of the :term:`Constitution`, the recovered service is ready for members to submit their recovery shares. Note that the ``transition_service_to_open`` proposal takes two parameters: the previous and the next service identity (x509 certificates in PEM format). This is to ensure that the correct network is recovered and to facilitate auditing, as well as to avoid forks. The previous service identity is used to validate the snapshot the recovery node is started from; CCF will refuse to start from a snapshot where the signing node certificate is not endorsed by the previous service identity. Since both identities are recorded on the ledger with the proposal, it is always clear at which point the identity changed. diff --git a/doc/governance/adding_member.rst b/doc/governance/adding_member.rst index 42ce38dda5..0f1eacb82c 100644 --- a/doc/governance/adding_member.rst +++ b/doc/governance/adding_member.rst @@ -62,7 +62,7 @@ First, the new member should update and retrieve the latest state digest via the } -Then, the new member should sign the state digest returned by the :http:POST:`/gov/ack/update_state_digest` via the :http:POST:`/gov/ack` endpoint, using either the ``ccf_cose_sign1`` or ``scurl.sh`` utilities: +Then, the new member should sign the state digest returned by the :http:POST:`/gov/ack/update_state_digest` via the :http:POST:`/gov/ack` endpoint, using the ``ccf_cose_sign1`` utility: .. code-block:: bash @@ -70,13 +70,6 @@ Then, the new member should sign the state digest returned by the :http:POST:`/g curl https:///gov/ack --cacert service_cert.pem --data-binary @- -H "content-type: application/cose" true -Or alternatively: - -.. code-block:: bash - - $ scurl.sh https:///gov/ack --cacert service_cert.pem --signing-key new_member_privk.pem --signing-cert new_member_cert.pem --header "Content-Type: application/json" --data-binary @request.json - true - Once the command completes, the new member becomes active and can take part in governance operations (e.g. creating a new proposal or voting for an existing one). You can verify the activation of the member at `/gov/members`. .. code-block:: bash diff --git a/doc/governance/common_member_operations.rst b/doc/governance/common_member_operations.rst index ebca3ef921..845f1fe27d 100644 --- a/doc/governance/common_member_operations.rst +++ b/doc/governance/common_member_operations.rst @@ -80,34 +80,6 @@ To limit the scope of key compromise, members of the consortium can refresh the "state": "Accepted" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem --data-binary @trigger_ledger_rekey.json -H "content-type: application/json" - { - "ballot_count": 0, - "proposal_id": "2f739d154b8cddacd7fc6d03cc8d4d20626e477ec4b1af10a74c670bb38bed5e", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - - $ scurl.sh https:///gov/proposals/2f739d154b8cddacd7fc6d03cc8d4d20626e477ec4b1af10a74c670bb38bed5e/ballots --cacert service_cert.pem --signing-key member2_privk.pem --signing-cert member2_cert.pem --data-binary @vote_accept_1.json -H "content-type: application/json" - { - "ballot_count": 1, - "proposal_id": "2f739d154b8cddacd7fc6d03cc8d4d20626e477ec4b1af10a74c670bb38bed5e", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - - $ scurl.sh https:///gov/proposals/2f739d154b8cddacd7fc6d03cc8d4d20626e477ec4b1af10a74c670bb38bed5e/ballots --cacert service_cert.pem --signing-key member3_privk --signing-cert member3_cert.pem --data-binary @vote_accept_1.json -H "content-type: application/json" - { - "ballot_count": 2, - "proposal_id": "2f739d154b8cddacd7fc6d03cc8d4d20626e477ec4b1af10a74c670bb38bed5e", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Accepted" - } - Once the proposal is accepted (``"state": "Accepted"``) it is immediately enacted. All subsequent transactions will be encrypted with a fresh new ledger encryption key. Updating Recovery Threshold @@ -162,34 +134,6 @@ The number of member shares required to restore the private ledger (``recovery_t "state": "Accepted" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem --data-binary @set_recovery_threshold.json -H "content-type: application/json" - { - "ballot_count": 0, - "proposal_id": "b9c08b3861395eca904d913427dcb436136e277cf4712eb14e9e9cddf9d231a8", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - - $ scurl.sh https:///gov/proposals/b9c08b3861395eca904d913427dcb436136e277cf4712eb14e9e9cddf9d231a8/ballots --cacert service_cert.pem --signing-key member2_privk.pem --signing-cert member2_cert.pem --data-binary @vote_accept_1.json -H "content-type: application/json" - { - "ballot_count": 1, - "proposal_id": "b9c08b3861395eca904d913427dcb436136e277cf4712eb14e9e9cddf9d231a8", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - - $ scurl.sh https:///gov/proposals/b9c08b3861395eca904d913427dcb436136e277cf4712eb14e9e9cddf9d231a8/ballots --cacert service_cert.pem --signing-key member3_privk.pem --signing-cert member3_cert.pem --data-binary @vote_accept_1.json -H "content-type: application/json" - { - "ballot_count": 2, - "proposal_id": "b9c08b3861395eca904d913427dcb436136e277cf4712eb14e9e9cddf9d231a8", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Accepted" - } - .. note:: The new recovery threshold has to be in the range between 1 and the current number of active recovery members. Renewing Node Certificate diff --git a/doc/governance/hsm_keys.rst b/doc/governance/hsm_keys.rst index 3e847d55be..862e52b140 100644 --- a/doc/governance/hsm_keys.rst +++ b/doc/governance/hsm_keys.rst @@ -117,45 +117,6 @@ Like ``ccf_cose_sign1``, the output can be sent directly to the service via curl "state": "Open" } -HTTP Signing -~~~~~~~~~~~~ - -The ``scurl.sh`` script can be used with the ``--print-digest-to-sign`` option to print the SHA384 to be signed as well as the required headers for HTTP signatures (following the `draft-cavage-http-signatures-12 `_ scheme): - -.. code-block:: bash - - # First, retrieve the hash to be signed - $ scurl.sh https:///gov/ -X [GET|POST] --signing-cert $IDENTITY_CERT_NAME.pem --print-digest-to-sign - Hash to sign: # To be signed by AKV - Request headers: - -H 'Digest: SHA-256=...' - -H 'Authorization: Signature keyId="...",algorithm="hs2019",headers="(request-target) digest content-length",signature=""' # Replace signature with AKV signature here - -H 'content-length: 0' - - # Then, retrieve the kid url for the identity key - $ export IDENTITY_AKV_KID=$(az keyvault key show --vault-name $VAULT_NAME --name $IDENTITY_CERT_NAME --query key.kid --output tsv) - - # Then, sign the request hash to be signed (as output by scurl.sh --print-digest-to-sign) - $ export base64url_signature=$(curl -s -X POST $IDENTITY_AKV_KID/sign?api-version=7.1 --data '{alg: "ES384", "value": ""}' -H "Authorization: Bearer ${AZ_TOKEN}" -H "Content-Type: application/json" | jq -r .value) - -.. note:: The signatures returned by AKV are returned as a `JWS signature `_ and encoded in `base64url `_ format and are not directly compatible with the signatures supported by CCF. - -The :ccf_repo:`jws_to_der.py ` Python script can be used to convert a JWS signature generated by AKV to a DER signature compatible with CCF: - -.. code-block:: bash - - $ pip install pyasn1 - $ export ccf_signature=$(python3.8 jws_to_der.py $base64url_signature) - -Finally, the signed HTTP request can be issued, using the request headers printed by ``scurl.sh --print-digest-to-sign``: - -.. code-block:: bash - - $ curl https:///gov/ -X [GET|POST] --cert $IDENTITY_CERT_NAME.pem \ - -H 'Digest: SHA-256=...' \ - -H 'Authorization: Signature keyId="...",algorithm="hs2019",headers="(request-target) digest content-length",signature="$ccf_signature"' \ - -H 'content-length: ' - Recovery Share Decryption ------------------------- @@ -168,4 +129,6 @@ The retrieved encrypted recovery share can be decrypted with the encryption key $ az keyvault key decrypt --vault-name $VAULT_NAME --name $ENCRYPTION_KEY_NAME --algorithm RSA-OAEP-256 --value # Outputs base64 decrypted share -The decrypted recovery share can then be submitted to the CCF recovered service (see :ref:`governance/accept_recovery:Submitting Recovery Shares`). \ No newline at end of file +The decrypted recovery share can then be submitted to the CCF recovered service (see :ref:`governance/accept_recovery:Submitting Recovery Shares`). + +.. warning:: HTTP request signing could be used in previous versions of CCF, but has been removed as of 4.0, in favour of COSE Sign1. \ No newline at end of file diff --git a/doc/governance/open_network.rst b/doc/governance/open_network.rst index 6da4ff01f3..e75688df0b 100644 --- a/doc/governance/open_network.rst +++ b/doc/governance/open_network.rst @@ -36,18 +36,6 @@ Then, the certificates of trusted users should be registered in CCF via the memb "state": "Open" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals --cacert service_cert.pem --signing-key member0_privk.pem --signing-cert member0_cert.pem --data-binary @set_user.json -H "content-type: application/json" - { - "ballot_count": 0, - "proposal_id": "f665047e3d1eb184a7b7921944a8ab543cfff117aab5b6358dc87f9e70278253", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - Other members are then allowed to vote for the proposal, using the proposal id returned to the proposer member. They may submit an unconditional approval, or their vote may query the current state and the proposed actions. These votes `must` be signed. .. code-block:: bash @@ -68,23 +56,6 @@ Other members are then allowed to vote for the proposal, using the proposal id r "state": "Open" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals/f665047e3d1eb184a7b7921944a8ab543cfff117aab5b6358dc87f9e70278253/ballots --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem --data-binary @vote_accept.json -H "content-type: application/json" - { - "ballot_count": 1, - "proposal_id": "f665047e3d1eb184a7b7921944a8ab543cfff117aab5b6358dc87f9e70278253", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - - $ cat vote_conditional.json - { - "ballot": "export function vote (proposal, proposerId) { return proposerId == \"2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0\" }" - } - .. code-block:: bash $ ccf_cose_sign1 --ccf-gov-msg-type ballot --ccf-gov-msg-created_at `date -Is` --ccf-gov-msg-proposal_id f665047e3d1eb184a7b7921944a8ab543cfff117aab5b6358dc87f9e70278253 --signing-key member0_privk.pem --signing-cert member0_cert.pem --content vote_conditional.json | \ @@ -96,18 +67,6 @@ Or alternatively, with the old signature method: "state": "Accepted" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals/f665047e3d1eb184a7b7921944a8ab543cfff117aab5b6358dc87f9e70278253/ballots --cacert service_cert.pem --signing-key member2_privk.pem --signing-cert member2_cert.pem --data-binary @vote_conditional.json -H "content-type: application/json" - { - "ballot_count": 2, - "proposal_id": "f665047e3d1eb184a7b7921944a8ab543cfff117aab5b6358dc87f9e70278253", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Accepted" - } - The user is successfully added once the proposal has received enough votes under the rules of the :term:`Constitution` (indicated by the response body showing a transition to state ``Accepted``). The user can then make user RPCs. @@ -193,18 +152,6 @@ Once users are added to the opening network, members should create a proposal to "state": "Open" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals --cacert service_cert.pem --signing-key member0_privk.pem --signing-cert member0_cert.pem --data-binary @transition_service_to_open.json -H "content-type: application/json" - { - "ballot_count": 0, - "proposal_id": "77374e16de0b2d61f58aec84d01e6218205d19c9401d2df127d893ce62576b81", - "proposer_id": "2af6cb6c0af07818186f7ef7151061174c3cb74b4a4c30a04a434f0c2b00a8c0", - "state": "Open" - } - Other members are then able to vote for the proposal using the returned proposal id. Once the proposal has received enough votes under the rules of the :term:`Constitution` (ie. ballots which evaluate to ``true``), the network is opened to users. It is only then that users are able to execute transactions on the business logic defined by the enclave file (``enclave.file`` configuration entry). diff --git a/doc/governance/proposals.rst b/doc/governance/proposals.rst index d577196182..33f146d932 100644 --- a/doc/governance/proposals.rst +++ b/doc/governance/proposals.rst @@ -278,18 +278,6 @@ For example, ``member1`` may submit a proposal to add a new member (``member4``) "state": "Open" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem --data-binary @add_member.json -H "content-type: application/json" - { - "ballot_count": 0, - "proposal_id": "d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd", - "proposer_id": "52af2620fa1b005a93d55d7d819a249ee2cb79f5262f54e8db794c5281a0ce73", - "state": "Open" - } - Here a new proposal has successfully been created, and nobody has yet voted for it. The proposal is in state ``Open``, meaning it will can receive additional votes. Members can then vote to accept or reject the proposal: .. code-block:: bash @@ -338,40 +326,6 @@ Here a new proposal has successfully been created, and nobody has yet voted for # As a majority of members have accepted the proposal, member 4 is added to the consortium -Or alternatively, with the old signature method: - -.. code-block:: bash - - # Member 1 approves the proposal (votes in favour: 1/3) - $ scurl.sh https:///gov/proposals/d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd/ballots --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem --data-binary @vote_accept.json -H "content-type: application/json" - { - "ballot_count": 1, - "proposal_id": "d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd", - "proposer_id": "52af2620fa1b005a93d55d7d819a249ee2cb79f5262f54e8db794c5281a0ce73", - "state": "Open" - } - - - # Member 2 rejects the proposal (votes in favour: 1/3) - $ scurl.sh https:///gov/proposals/d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd/ballots --cacert service_cert.pem --signing-key member2_privk.pem --signing-cert member2_cert.pem --data-binary @vote_reject.json -H "content-type: application/json" - { - "ballot_count": 2, - "proposal_id": "d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd", - "proposer_id": "52af2620fa1b005a93d55d7d819a249ee2cb79f5262f54e8db794c5281a0ce73", - "state": "Open" - } - - # Member 3 accepts the proposal (votes in favour: 2/3) - $ scurl.sh https:///gov/proposals/d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd/ballots --cacert service_cert.pem --signing-key member3_privk.pem --signing-cert member3_cert.pem --data-binary @vote_accept.json -H "content-type: application/json" - { - "ballot_count": 3, - "proposal_id": "d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd", - "proposer_id": "52af2620fa1b005a93d55d7d819a249ee2cb79f5262f54e8db794c5281a0ce73", - "state": "Accepted" - } - - # As a majority of members have accepted the proposal, member 4 is added to the consortium - As soon as ``member3`` accepts the proposal, a majority (2 out of 3) of members has been reached and the proposal completes, successfully adding ``member4``. The response shows this, as the proposal's state is now ``Accepted``. .. note:: Once a new member has been accepted to the consortium, the new member must acknowledge that it is active by sending a :http:POST:`/gov/ack` request. See :ref:`governance/adding_member:Activating a New Member`. @@ -415,18 +369,6 @@ At any stage during the voting process, before the proposal is accepted, the pro "state": "Withdrawn" } -Or alternatively, with the old signature method: - -.. code-block:: bash - - $ scurl.sh https:///gov/proposals/d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd/withdraw --cacert service_cert.pem --signing-key member1_privk.pem --signing-cert member1_cert.pem -H "content-type: application/json" - { - "ballot_count": 1, - "proposal_id": "d4ec2de82267f97d3d1b464020af0bd3241f1bedf769f0fee73cd00f08e9c7fd", - "proposer_id": "52af2620fa1b005a93d55d7d819a249ee2cb79f5262f54e8db794c5281a0ce73", - "state": "Withdrawn" - } - This means future votes will be rejected, and the proposal will never be accepted. However it remains visible as a proposal so members can easily audit historic proposals. Binding a Proposal @@ -449,4 +391,6 @@ The `assert_service_identity` action, provided as a sample, illustrates how this ] } -A constitution wishing to enforce that all proposals must be specific to a service could enforce the presence of this action in its ``validate()`` implementation. \ No newline at end of file +A constitution wishing to enforce that all proposals must be specific to a service could enforce the presence of this action in its ``validate()`` implementation. + +.. warning:: HTTP request signing could be used in previous versions of CCF, but has been removed as of 4.0, in favour of COSE Sign1. \ No newline at end of file diff --git a/doc/schemas/app_openapi.json b/doc/schemas/app_openapi.json index 39f79fad02..12dd11b301 100644 --- a/doc/schemas/app_openapi.json +++ b/doc/schemas/app_openapi.json @@ -286,16 +286,6 @@ "description": "Request payload must be a COSE Sign1 document, with expected protected headers. Signer must be a member identity registered with this service.", "scheme": "cose_sign1", "type": "http" - }, - "member_signature": { - "description": "Request must be signed according to the HTTP Signature scheme. The signer must be a member identity registered with this service.", - "scheme": "signature", - "type": "http" - }, - "user_signature": { - "description": "Request must be signed according to the HTTP Signature scheme. The signer must be a user identity registered with this service.", - "scheme": "signature", - "type": "http" } }, "x-ccf-forwarding": { @@ -316,7 +306,7 @@ "info": { "description": "This CCF sample app implements a simple logging application, securely recording messages at client-specified IDs. It demonstrates most of the features available to CCF apps.", "title": "CCF Sample Logging App", - "version": "1.20.0" + "version": "2.0.0" }, "openapi": "3.0.0", "paths": { @@ -1265,33 +1255,6 @@ } } }, - "/app/log/signed_request_query": { - "get": { - "responses": { - "200": { - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/string" - } - } - }, - "description": "Default response description" - }, - "default": { - "$ref": "#/components/responses/default" - } - }, - "security": [ - { - "user_signature": [] - } - ], - "x-ccf-forwarding": { - "$ref": "#/components/x-ccf-forwarding/always" - } - } - }, "/app/multi_auth": { "get": { "responses": { @@ -1310,12 +1273,6 @@ } }, "security": [ - { - "user_signature": [] - }, - { - "member_signature": [] - }, { "jwt": [] }, diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index 974f1ca63c..b3680466d7 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -1246,11 +1246,6 @@ "description": "Request payload must be a COSE Sign1 document, with expected protected headers. Signer must be a member identity registered with this service.", "scheme": "cose_sign1", "type": "http" - }, - "member_signature": { - "description": "Request must be signed according to the HTTP Signature scheme. The signer must be a member identity registered with this service.", - "scheme": "signature", - "type": "http" } }, "x-ccf-forwarding": { @@ -1271,7 +1266,7 @@ "info": { "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", "title": "CCF Governance API", - "version": "2.25.0" + "version": "3.0.0" }, "openapi": "3.0.0", "paths": { @@ -1296,9 +1291,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } @@ -1327,9 +1319,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } @@ -2265,9 +2254,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } @@ -2382,9 +2368,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } @@ -2464,9 +2447,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } @@ -2530,9 +2510,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } @@ -2569,9 +2546,6 @@ } }, "security": [ - { - "member_signature": [] - }, { "member_cose_sign1": [] } diff --git a/doc/schemas/node_openapi.json b/doc/schemas/node_openapi.json index df46ce216a..e5d417df3d 100644 --- a/doc/schemas/node_openapi.json +++ b/doc/schemas/node_openapi.json @@ -914,7 +914,7 @@ "info": { "description": "This API provides public, uncredentialed access to service and node state.", "title": "CCF Public Node API", - "version": "2.42.0" + "version": "3.0.0" }, "openapi": "3.0.0", "paths": { diff --git a/doc/use_apps/issue_commands.rst b/doc/use_apps/issue_commands.rst index 33f8c7300d..3cc10c2282 100644 --- a/doc/use_apps/issue_commands.rst +++ b/doc/use_apps/issue_commands.rst @@ -30,24 +30,7 @@ The response body (the JSON value ``true``) indicates that the request was execu Signing ------- -In some situations CCF requires signed requests, for example for member votes. Two signing schemes are supported as of 3.x. - -HTTP Signatures -~~~~~~~~~~~~~~~ - -An implementation of `IETF HTTP Signatures draft RFC `_ , but -supports `ecdsa-sha256` as well as `hs2019` signing algorithms as described in the later `draft 12 `_. -We provide a wrapper script (``scurl.sh``) around ``curl`` to submit signed requests from the command line. -This passes most args verbatim to ``curl``, but expects additional ``--signing-cert`` and ``--signing-key`` args which specify the identity used to sign the request. -These are distinct from the ``--cert`` and ``--key`` args which are passed to ``curl`` as the client TLS identity, and may specify a different identity. - -CCF identifies the signing identity for a request via the SHA-256 digest of its certificate, represented as a hex string. -That value must be set in the ``keyId`` field of the ``Authorization`` HTTP header for a signed request. - -These commands can also be signed and transmitted by external libraries. -For example, the CCF test infrastructure uses a custom authentication provider for `Python HTTPX `_. - -.. note:: This signing mechanism is still supported for the duration of 3.x, but will be dropped in 4.0 because it is coupled to HTTP, and has not reached adoption as a standard or in libraries. +In some situations CCF requires signed requests, for example for member votes. Only one signing scheme is supported as of 4.x: COSE Sign1 ~~~~~~~~~~ @@ -62,4 +45,6 @@ A signing script (``ccf_cose_sign1``) is provided as part of the `ccf Python pac Commands can also be signed using the pycose library, and sent with any standard HTTP library such as `Python HTTPX `_. -The ``ccf.gov.msg.created_at`` header parameter is used by governance to prevent proposal replay. A fixed-sized window of proposal request digests is kept, and newly submitted proposal requests must not collide, or be older than the median proposal request in that window. \ No newline at end of file +The ``ccf.gov.msg.created_at`` header parameter is used by governance to prevent proposal replay. A fixed-sized window of proposal request digests is kept, and newly submitted proposal requests must not collide, or be older than the median proposal request in that window. + +.. warning:: HTTP request signing could be used in previous versions of CCF, but has been removed as of 4.0, in favour of COSE Sign1. \ No newline at end of file diff --git a/include/ccf/common_auth_policies.h b/include/ccf/common_auth_policies.h index 8d7d3ec95b..2df6827825 100644 --- a/include/ccf/common_auth_policies.h +++ b/include/ccf/common_auth_policies.h @@ -6,7 +6,6 @@ #include "ccf/endpoints/authentication/cose_auth.h" #include "ccf/endpoints/authentication/empty_auth.h" #include "ccf/endpoints/authentication/jwt_auth.h" -#include "ccf/endpoints/authentication/sig_auth.h" #include @@ -27,22 +26,11 @@ namespace ccf static std::shared_ptr user_cert_auth_policy = std::make_shared(); - /** Authenticate using HTTP request signature, and - * @c public:ccf.gov.users.certs table */ - static std::shared_ptr user_signature_auth_policy = - std::make_shared(); - /** Authenticate using TLS session identity, and * @c public:ccf.gov.members.certs table */ static std::shared_ptr member_cert_auth_policy = std::make_shared(); - /** Authenticate using HTTP request signature, and - * @c public:ccf.gov.members.certs table */ - static std::shared_ptr - member_signature_auth_policy = - std::make_shared(); - /** Authenticate using JWT, validating the token using the * @c public:ccf.gov.jwt.public_signing_key_issuer and * @c public:ccf.gov.jwt.public_signing_keys tables */ diff --git a/include/ccf/endpoint.h b/include/ccf/endpoint.h index ca8da5ab91..09026539b5 100644 --- a/include/ccf/endpoint.h +++ b/include/ccf/endpoint.h @@ -133,11 +133,9 @@ namespace ccf::endpoints * retrieved inside the endpoint with ctx.get_caller(), * @see ccf::UserCertAuthnIdentity * @see ccf::JwtAuthnIdentity - * @see ccf::UserSignatureAuthnIdentity * * @see ccf::empty_auth_policy * @see ccf::user_cert_auth_policy - * @see ccf::user_signature_auth_policy */ AuthnPolicies authn_policies; }; diff --git a/include/ccf/endpoints/authentication/cose_auth.h b/include/ccf/endpoints/authentication/cose_auth.h index c60d11c830..034ce6f95a 100644 --- a/include/ccf/endpoints/authentication/cose_auth.h +++ b/include/ccf/endpoints/authentication/cose_auth.h @@ -43,13 +43,11 @@ namespace ccf std::span signature; }; - /** Experimental COSE Sign1 Authentication Policy + /** COSE Sign1 Authentication Policy * * Allows ccf.gov.msg.type and ccf.gov.msg.proposal_id protected header * entries, to specify the type of governance action, and which proposal - * it refers to. The plan is to offer this authentication method as an - * alternative to MemberSignatureAuthnPolicy for governance in the future, - * and perhaps as a generic authentication method as well. + * it refers to. */ class MemberCOSESign1AuthnPolicy : public AuthnPolicy { diff --git a/include/ccf/endpoints/authentication/sig_auth.h b/include/ccf/endpoints/authentication/sig_auth.h deleted file mode 100644 index 221f244bee..0000000000 --- a/include/ccf/endpoints/authentication/sig_auth.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache 2.0 License. -#pragma once - -#include "ccf/crypto/pem.h" -#include "ccf/endpoints/authentication/authentication_types.h" -#include "ccf/entity_id.h" -#include "ccf/service/signed_req.h" - -namespace ccf -{ - struct UserSignatureAuthnIdentity : public AuthnIdentity - { - /** CCF user ID */ - UserId user_id; - /** User certificate, used to sign this request, described by keyId */ - crypto::Pem user_cert; - /** Canonicalised request and associated signature */ - SignedReq signed_request; - }; - - struct VerifierCache; - - class UserSignatureAuthnPolicy : public AuthnPolicy - { - protected: - static const OpenAPISecuritySchema security_schema; - std::unique_ptr verifiers; - - public: - static constexpr auto SECURITY_SCHEME_NAME = "user_signature"; - - UserSignatureAuthnPolicy(); - virtual ~UserSignatureAuthnPolicy(); - - std::unique_ptr authenticate( - kv::ReadOnlyTx& tx, - const std::shared_ptr& ctx, - std::string& error_reason) override; - - void set_unauthenticated_error( - std::shared_ptr ctx, - std::string&& error_reason) override; - - std::optional get_openapi_security_schema() - const override - { - return security_schema; - } - - std::string get_security_scheme_name() override - { - return SECURITY_SCHEME_NAME; - } - }; - - struct MemberSignatureAuthnIdentity : public AuthnIdentity - { - /** CCF member ID */ - MemberId member_id; - - /** Member certificate, used to sign this request, described by keyId */ - crypto::Pem member_cert; - - /** Canonicalised request and associated signature */ - SignedReq signed_request; - - /** Digest of request */ - std::vector request_digest; - }; - - class MemberSignatureAuthnPolicy : public AuthnPolicy - { - protected: - static const OpenAPISecuritySchema security_schema; - std::unique_ptr verifiers; - - public: - static constexpr auto SECURITY_SCHEME_NAME = "member_signature"; - - MemberSignatureAuthnPolicy(); - virtual ~MemberSignatureAuthnPolicy(); - - std::unique_ptr authenticate( - kv::ReadOnlyTx& tx, - const std::shared_ptr& ctx, - std::string& error_reason) override; - - void set_unauthenticated_error( - std::shared_ptr ctx, - std::string&& error_reason) override; - - std::optional get_openapi_security_schema() - const override - { - return security_schema; - } - - std::string get_security_scheme_name() override - { - return SECURITY_SCHEME_NAME; - } - }; -} diff --git a/js/ccf-app/src/endpoints.ts b/js/ccf-app/src/endpoints.ts index d672d758bc..a44cce6f79 100644 --- a/js/ccf-app/src/endpoints.ts +++ b/js/ccf-app/src/endpoints.ts @@ -141,16 +141,6 @@ export interface MemberCertAuthnIdentity extends UserMemberAuthnIdentityCommon { policy: "member_cert"; } -export interface UserSignatureAuthnIdentity - extends UserMemberAuthnIdentityCommon { - policy: "user_signature"; -} - -export interface MemberSignatureAuthnIdentity - extends UserMemberAuthnIdentityCommon { - policy: "member_signature"; -} - export interface JwtAuthnIdentity extends AuthnIdentityCommon { policy: "jwt"; @@ -185,8 +175,6 @@ export type AuthnIdentity = | EmptyAuthnIdentity | UserCertAuthnIdentity | MemberCertAuthnIdentity - | UserSignatureAuthnIdentity - | MemberSignatureAuthnIdentity | JwtAuthnIdentity; /** See {@linkcode Response.body}. */ diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index 6d22972815..97f83734b1 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -312,7 +312,7 @@ namespace loggingapp "recording messages at client-specified IDs. It demonstrates most of " "the features available to CCF apps."; - openapi_info.document_version = "1.20.0"; + openapi_info.document_version = "2.0.0"; index_per_public_key = std::make_shared( PUBLIC_RECORDS, context, 10000, 20); @@ -896,54 +896,6 @@ namespace loggingapp ctx.rpc_ctx->set_response_body(std::move(response)); return; } - else if ( - auto user_sig_ident = - ctx.template try_get_caller()) - { - auto response = std::string("User HTTP signature"); - response += fmt::format( - "\nThe caller is a user with ID: {}", user_sig_ident->user_id); - response += fmt::format( - "\nThe caller's cert is:\n{}", user_sig_ident->user_cert.str()); - - nlohmann::json user_data = nullptr; - if ( - get_user_data_v1(ctx.tx, user_sig_ident->user_id, user_data) == - ccf::ApiResult::OK) - { - response += - fmt::format("\nThe caller's user data is: {}", user_data.dump()); - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_body(std::move(response)); - return; - } - else if ( - auto member_sig_ident = - ctx.template try_get_caller()) - { - auto response = std::string("Member HTTP signature"); - response += fmt::format( - "\nThe caller is a member with ID: {}", - member_sig_ident->member_id); - response += fmt::format( - "\nThe caller's cert is:\n{}", member_sig_ident->member_cert.str()); - - nlohmann::json member_data = nullptr; - if ( - get_member_data_v1( - ctx.tx, member_sig_ident->member_id, member_data) == - ccf::ApiResult::OK) - { - response += fmt::format( - "\nThe caller's member data is: {}", member_data.dump()); - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_body(std::move(response)); - return; - } else if ( auto jwt_ident = ctx.template try_get_caller()) { @@ -982,9 +934,7 @@ namespace loggingapp HTTP_GET, multi_auth, {ccf::user_cert_auth_policy, - ccf::user_signature_auth_policy, ccf::member_cert_auth_policy, - ccf::member_signature_auth_policy, ccf::jwt_auth_policy, ccf::empty_auth_policy}) .set_auto_schema() @@ -1739,22 +1689,6 @@ namespace loggingapp .set_auto_schema() .install(); - auto get_signed_request_query = [this](auto& ctx) { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - std::vector rq( - ctx.rpc_ctx->get_request_query().begin(), - ctx.rpc_ctx->get_request_query().end()); - ctx.rpc_ctx->set_response_body(rq); - }; - - make_endpoint( - "/log/signed_request_query", - HTTP_GET, - get_signed_request_query, - {ccf::user_signature_auth_policy}) - .set_auto_schema() - .install(); - auto post_cose_signed_content = [this](ccf::endpoints::EndpointContext& ctx) { const auto& caller_identity = diff --git a/src/apps/js_generic/js_generic_base.cpp b/src/apps/js_generic/js_generic_base.cpp index 0e4c58e512..729210dda7 100644 --- a/src/apps/js_generic/js_generic_base.cpp +++ b/src/apps/js_generic/js_generic_base.cpp @@ -91,22 +91,6 @@ namespace ccfapp id = member_cert_ident->member_id; is_member = true; } - else if ( - auto user_sig_ident = - endpoint_ctx.try_get_caller()) - { - policy_name = get_policy_name_from_ident(user_sig_ident); - id = user_sig_ident->user_id; - is_member = false; - } - else if ( - auto member_sig_ident = - endpoint_ctx.try_get_caller()) - { - policy_name = get_policy_name_from_ident(member_sig_ident); - id = member_sig_ident->member_id; - is_member = true; - } if (policy_name == nullptr) { diff --git a/src/apps/js_generic/named_auth_policies.h b/src/apps/js_generic/named_auth_policies.h index 42fcfc95e9..d7759df6a8 100644 --- a/src/apps/js_generic/named_auth_policies.h +++ b/src/apps/js_generic/named_auth_policies.h @@ -19,16 +19,10 @@ namespace ccfapp policies.emplace( ccf::UserCertAuthnPolicy::SECURITY_SCHEME_NAME, ccf::user_cert_auth_policy); - policies.emplace( - ccf::UserSignatureAuthnPolicy::SECURITY_SCHEME_NAME, - ccf::user_signature_auth_policy); policies.emplace( ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME, ccf::member_cert_auth_policy); - policies.emplace( - ccf::MemberSignatureAuthnPolicy::SECURITY_SCHEME_NAME, - ccf::member_signature_auth_policy); policies.emplace( ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME, ccf::jwt_auth_policy); @@ -60,18 +54,10 @@ namespace ccfapp { return ccf::UserCertAuthnPolicy::SECURITY_SCHEME_NAME; } - else if constexpr (std::is_same_v) - { - return ccf::UserSignatureAuthnPolicy::SECURITY_SCHEME_NAME; - } else if constexpr (std::is_same_v) { return ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME; } - else if constexpr (std::is_same_v) - { - return ccf::MemberSignatureAuthnPolicy::SECURITY_SCHEME_NAME; - } else if constexpr (std::is_same_v) { return ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME; diff --git a/src/apps/tpcc/app/tpcc.cpp b/src/apps/tpcc/app/tpcc.cpp index 7a4f7644e7..c11d69a3d1 100644 --- a/src/apps/tpcc/app/tpcc.cpp +++ b/src/apps/tpcc/app/tpcc.cpp @@ -115,21 +115,21 @@ namespace ccfapp set_no_content_status(ctx); }; - const ccf::AuthnPolicies user_sig_or_cert = { - user_signature_auth_policy, user_cert_auth_policy}; - - make_endpoint("/tpcc_create", HTTP_POST, create, user_sig_or_cert) - .install(); - make_endpoint("/stock_level", HTTP_POST, do_stock_level, user_sig_or_cert) + make_endpoint("/tpcc_create", HTTP_POST, create, {user_cert_auth_policy}) .install(); make_endpoint( - "/order_status", HTTP_POST, do_order_status, user_sig_or_cert) + "/stock_level", HTTP_POST, do_stock_level, {user_cert_auth_policy}) .install(); - make_endpoint("/delivery", HTTP_POST, do_delivery, user_sig_or_cert) + make_endpoint( + "/order_status", HTTP_POST, do_order_status, {user_cert_auth_policy}) .install(); - make_endpoint("/payment", HTTP_POST, do_payment, user_sig_or_cert) + make_endpoint( + "/delivery", HTTP_POST, do_delivery, {user_cert_auth_policy}) .install(); - make_endpoint("/new_order", HTTP_POST, do_new_order, user_sig_or_cert) + make_endpoint("/payment", HTTP_POST, do_payment, {user_cert_auth_policy}) + .install(); + make_endpoint( + "/new_order", HTTP_POST, do_new_order, {user_cert_auth_policy}) .install(); } }; diff --git a/src/clients/perf/perf_client.h b/src/clients/perf/perf_client.h index 08eb8b7f9d..5a40d3fcbf 100644 --- a/src/clients/perf/perf_client.h +++ b/src/clients/perf/perf_client.h @@ -6,6 +6,7 @@ #include "timing.h" // CCF +#include "ccf/crypto/sha256_hash.h" #include "ccf/crypto/verifier.h" #include "ccf/ds/logger.h" #include "clients/rpc_tls_client.h" diff --git a/src/clients/rpc_tls_client.h b/src/clients/rpc_tls_client.h index aeffa13f41..5f1544bccb 100644 --- a/src/clients/rpc_tls_client.h +++ b/src/clients/rpc_tls_client.h @@ -11,7 +11,6 @@ #define FMT_HEADER_ONLY #include -#include #include #include #include @@ -67,11 +66,6 @@ namespace client http::headers::AUTHORIZATION, fmt::format("Bearer {}", auth_token)); } - if (key_pair != nullptr) - { - http::sign_request(r, key_pair, key_id); - } - return r.build_request(); } diff --git a/src/endpoints/authentication/sig_auth.cpp b/src/endpoints/authentication/sig_auth.cpp deleted file mode 100644 index eefdb36d9f..0000000000 --- a/src/endpoints/authentication/sig_auth.cpp +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache 2.0 License. - -#include "ccf/endpoints/authentication/sig_auth.h" - -#include "ccf/crypto/verifier.h" -#include "ccf/pal/locking.h" -#include "ccf/rpc_context.h" -#include "ccf/service/tables/members.h" -#include "ccf/service/tables/users.h" -#include "ds/lru.h" -#include "http/http_sig.h" - -namespace ccf -{ - static std::optional parse_signed_request( - const std::shared_ptr& ctx) - { - return http::HttpSignatureVerifier::parse( - ctx->get_request_verb().c_str(), - ctx->get_request_url(), - ctx->get_request_headers(), - ctx->get_request_body()); - } - - struct VerifierCache - { - static constexpr size_t DEFAULT_MAX_VERIFIERS = 50; - - ccf::pal::Mutex verifiers_lock; - LRU verifiers; - - VerifierCache(size_t max_verifiers = DEFAULT_MAX_VERIFIERS) : - verifiers(max_verifiers) - {} - - crypto::VerifierPtr get_verifier(const crypto::Pem& pem) - { - std::lock_guard guard(verifiers_lock); - - auto it = verifiers.find(pem); - if (it == verifiers.end()) - { - it = verifiers.insert(pem, crypto::make_verifier(pem)); - } - - return it->second; - } - }; - - UserSignatureAuthnPolicy::UserSignatureAuthnPolicy() : - verifiers(std::make_unique()) - {} - - UserSignatureAuthnPolicy::~UserSignatureAuthnPolicy() = default; - - std::unique_ptr UserSignatureAuthnPolicy::authenticate( - kv::ReadOnlyTx& tx, - const std::shared_ptr& ctx, - std::string& error_reason) - { - std::optional signed_request = std::nullopt; - - try - { - signed_request = parse_signed_request(ctx); - } - catch (const std::exception& e) - { - error_reason = e.what(); - return nullptr; - } - - if (signed_request.has_value()) - { - UserCerts users_certs_table(Tables::USER_CERTS); - auto users_certs = tx.ro(users_certs_table); - auto user_cert = users_certs->get(signed_request->key_id); - if (user_cert.has_value()) - { - auto verifier = verifiers->get_verifier(user_cert.value()); - if (verifier->verify( - signed_request->req, signed_request->sig, signed_request->md)) - { - auto identity = std::make_unique(); - identity->user_id = signed_request->key_id; - identity->user_cert = user_cert.value(); - identity->signed_request = signed_request.value(); - return identity; - } - else - { - error_reason = "Signature is invalid"; - } - } - else - { - error_reason = "Signer is not a known user"; - } - } - else - { - error_reason = "Missing signature"; - } - - return nullptr; - } - - void UserSignatureAuthnPolicy::set_unauthenticated_error( - std::shared_ptr ctx, std::string&& error_reason) - { - ctx->set_error( - HTTP_STATUS_UNAUTHORIZED, - ccf::errors::InvalidAuthenticationInfo, - std::move(error_reason)); - ctx->set_response_header( - http::headers::WWW_AUTHENTICATE, - fmt::format( - "Signature realm=\"Signed request access\", " - "headers=\"{}\"", - fmt::join(http::required_signature_headers, " "))); - } - - const OpenAPISecuritySchema UserSignatureAuthnPolicy::security_schema = - std::make_pair( - UserSignatureAuthnPolicy::SECURITY_SCHEME_NAME, - nlohmann::json{ - {"type", "http"}, - {"scheme", "signature"}, - {"description", - "Request must be signed according to the HTTP Signature scheme. The " - "signer must be a user identity registered with this service."}}); - - MemberSignatureAuthnPolicy::MemberSignatureAuthnPolicy() : - verifiers(std::make_unique()) - {} - - MemberSignatureAuthnPolicy::~MemberSignatureAuthnPolicy() = default; - - std::unique_ptr MemberSignatureAuthnPolicy::authenticate( - kv::ReadOnlyTx& tx, - const std::shared_ptr& ctx, - std::string& error_reason) - { - std::optional signed_request = std::nullopt; - - try - { - signed_request = parse_signed_request(ctx); - } - catch (const std::exception& e) - { - error_reason = e.what(); - return nullptr; - } - - if (signed_request.has_value()) - { - MemberCerts members_certs_table(Tables::MEMBER_CERTS); - auto member_certs = tx.ro(members_certs_table); - auto member_cert = member_certs->get(signed_request->key_id); - if (member_cert.has_value()) - { - std::vector digest; - auto verifier = verifiers->get_verifier(member_cert.value()); - if (verifier->verify( - signed_request->req, - signed_request->sig, - signed_request->md, - digest)) - { - auto identity = std::make_unique(); - identity->member_id = signed_request->key_id; - identity->member_cert = member_cert.value(); - identity->signed_request = signed_request.value(); - identity->request_digest = std::move(digest); - return identity; - } - else - { - error_reason = "Signature is invalid"; - } - } - else - { - error_reason = "Signer is not a known member"; - } - } - else - { - error_reason = "Missing signature"; - } - - return nullptr; - } - - void MemberSignatureAuthnPolicy::set_unauthenticated_error( - std::shared_ptr ctx, std::string&& error_reason) - { - ctx->set_error( - HTTP_STATUS_UNAUTHORIZED, - ccf::errors::InvalidAuthenticationInfo, - std::move(error_reason)); - ctx->set_response_header( - http::headers::WWW_AUTHENTICATE, - fmt::format( - "Signature realm=\"Signed request access\", " - "headers=\"{}\"", - fmt::join(http::required_signature_headers, " "))); - } - - const OpenAPISecuritySchema MemberSignatureAuthnPolicy::security_schema = - std::make_pair( - MemberSignatureAuthnPolicy::SECURITY_SCHEME_NAME, - nlohmann::json{ - {"type", "http"}, - {"scheme", "signature"}, - {"description", - "Request must be signed according to the HTTP Signature scheme. The " - "signer must be a member identity registered with this service."}}); -} diff --git a/src/http/http_rpc_context.h b/src/http/http_rpc_context.h index 20978340c9..c1496f4f39 100644 --- a/src/http/http_rpc_context.h +++ b/src/http/http_rpc_context.h @@ -7,7 +7,6 @@ #include "ccf/odata_error.h" #include "ccf/rpc_context.h" #include "http_parser.h" -#include "http_sig.h" #include "node/rpc/rpc_context_impl.h" namespace http diff --git a/src/http/http_sig.h b/src/http/http_sig.h deleted file mode 100644 index d12d2d6f0b..0000000000 --- a/src/http/http_sig.h +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache 2.0 License. -#pragma once - -#include "ccf/crypto/base64.h" -#include "ccf/crypto/key_pair.h" -#include "ccf/crypto/sha256_hash.h" -#include "ccf/http_consts.h" -#include "ccf/service/signed_req.h" -#include "http_parser.h" - -#define FMT_HEADER_ONLY -#include -#include -#include - -namespace http -{ - inline std::vector construct_raw_signed_string( - std::string verb, - const std::string_view& url, - const http::HeaderMap& headers, - const std::vector& headers_to_sign) - { - std::string signed_string = {}; - std::string value = {}; - bool first = true; - - for (const auto f : headers_to_sign) - { - if (f == auth::SIGN_HEADER_REQUEST_TARGET) - { - // Store verb as lowercase - nonstd::to_lower(verb); - value = fmt::format("{} {}", verb, url); - } - else - { - const auto h = headers.find(f); - if (h == headers.end()) - { - throw std::logic_error( - fmt::format("Signed header '{}' does not exist", f)); - } - - value = h->second; - } - - if (!first) - { - signed_string.append("\n"); - } - first = false; - - signed_string.append(f); - signed_string.append(": "); - signed_string.append(value); - } - - return std::vector({signed_string.begin(), signed_string.end()}); - } - - inline void add_digest_header(http::Request& request) - { - // Ensure digest is present and up-to-date - crypto::Sha256Hash body_digest( - request.get_content_data(), request.get_content_length()); - request.set_header( - headers::DIGEST, - fmt::format( - "{}={}", - "SHA-256", - crypto::b64_from_raw(body_digest.h.data(), body_digest.SIZE))); - } - - inline void sign_request( - http::Request& request, - const crypto::KeyPairPtr& kp, - const std::string& key_id, - const std::vector& headers_to_sign) - { - add_digest_header(request); - - const auto to_sign = construct_raw_signed_string( - llhttp_method_name(request.get_method()), - fmt::format("{}{}", request.get_path(), request.get_formatted_query()), - request.get_headers(), - headers_to_sign); - - const auto signature = kp->sign(to_sign); - - auto auth_value = fmt::format( - "Signature " - "keyId=\"{}\",algorithm=\"{}\",headers=\"{}\",signature=" - "\"{}\"", - key_id, - auth::SIGN_ALGORITHM_HS_2019, - fmt::format("{}", fmt::join(headers_to_sign, " ")), - crypto::b64_from_raw(signature.data(), signature.size())); - - request.set_header(headers::AUTHORIZATION, auth_value); - } - - inline void sign_request( - http::Request& request, - const crypto::KeyPairPtr& kp, - const std::string& key_id) - { - std::vector headers_to_sign; - headers_to_sign.emplace_back(auth::SIGN_HEADER_REQUEST_TARGET); - headers_to_sign.emplace_back(headers::DIGEST); - headers_to_sign.emplace_back(headers::CONTENT_LENGTH); - - sign_request(request, kp, key_id, headers_to_sign); - } - - // Implements verification of "Signature" scheme from - // https://tools.ietf.org/html/draft-cavage-http-signatures-12 - // - // Notes: - // - Only supports public key crytography (i.e. no HMAC) - // - Only supports SHA-256 as request digest algorithm - // - Only supports ecdsa-sha256 and hs2019 as signature algorithms - // - keyId can be set to a SHA-256 digest of a cert against which the - // signature verifies - class HttpSignatureVerifier - { - public: - struct SignatureParams - { - std::string_view signature = {}; - std::string_view signature_algorithm = {}; - std::vector signed_headers; - std::string key_id = {}; - }; - - static void parse_auth_scheme(std::string_view& auth_header_value) - { - auto next_space = auth_header_value.find(" "); - if (next_space == std::string::npos) - { - throw std::logic_error(fmt::format( - "'{}' header only contains one field", headers::AUTHORIZATION)); - } - auto auth_scheme = auth_header_value.substr(0, next_space); - if (auth_scheme != auth::SIGN_AUTH_SCHEME) - { - throw std::logic_error(fmt::format( - "'{}' scheme for signature should be '{}'", - headers::AUTHORIZATION, - auth::SIGN_AUTH_SCHEME)); - } - auth_header_value = auth_header_value.substr(next_space + 1); - } - - static bool verify_digest( - const http::HeaderMap& headers, - const std::vector& body, - std::string& error_reason) - { - // First, retrieve digest from header - auto digest = headers.find(headers::DIGEST); - if (digest == headers.end()) - { - error_reason = fmt::format("Missing '{}' header", headers::DIGEST); - return false; - } - - auto equal_pos = digest->second.find("="); - if (equal_pos == std::string::npos) - { - error_reason = fmt::format( - "'{}' header does not contain key=value", headers::DIGEST); - return false; - } - - auto sha_key = digest->second.substr(0, equal_pos); - if (sha_key != auth::DIGEST_SHA256) - { - error_reason = fmt::format( - "'{}' for request digest is not supported, allowed: '{}'", - sha_key, - auth::DIGEST_SHA256); - return false; - } - - auto raw_digest = - crypto::raw_from_b64(digest->second.substr(equal_pos + 1)); - - // Then, hash the request body - crypto::Sha256Hash body_digest({body.data(), body.size()}); - - if (!std::equal( - raw_digest.begin(), - raw_digest.end(), - body_digest.h.begin(), - body_digest.h.end())) - { - error_reason = fmt::format( - "Request body does not match '{}' header, calculated body " - "digest = {:02x}", - headers::DIGEST, - fmt::join(body_digest.h, "")); - return false; - } - - return true; - } - - // Parses a delimited string with no delimiter at the end - // (e.g. "foo,bar,baz") and returns a vector parsed string views (e.g. - // ["foo", "bar", "baz"]) - static std::vector parse_delimited_string( - std::string_view& s, const std::string& delimiter) - { - std::vector strings; - bool last_string = false; - - auto next_delimiter = s.find(delimiter); - while (next_delimiter != std::string::npos || !last_string) - { - auto token = s.substr(0, next_delimiter); - if (next_delimiter == std::string::npos) - { - last_string = true; - } - - strings.emplace_back(token); - - if (!last_string) - { - s = s.substr(next_delimiter + 1); - next_delimiter = s.find(delimiter); - } - } - - return strings; - } - - static SignatureParams parse_signature_params( - std::string_view& auth_header_value) - { - SignatureParams sig_params = {}; - - auto parsed_params = - parse_delimited_string(auth_header_value, auth::SIGN_PARAMS_DELIMITER); - - for (auto& p : parsed_params) - { - auto eq_pos = p.find("="); - if (eq_pos != std::string::npos) - { - auto k = p.substr(0, eq_pos); - auto v = p.substr(eq_pos + 1); - - // Remove quotes around value, if present - const bool begins_with_quote = v.front() == '"'; - const bool ends_with_quote = v.back() == '"'; - if (v.size() >= 2 && (begins_with_quote || ends_with_quote)) - { - if (!(begins_with_quote && ends_with_quote)) - { - throw std::logic_error(fmt::format( - "Unbalanced quotes in '{}' header: {}", - headers::AUTHORIZATION, - p)); - } - - v = v.substr(1, v.size() - 2); - } - - if (k == auth::SIGN_PARAMS_KEYID) - { - sig_params.key_id = v; - } - else if (k == auth::SIGN_PARAMS_ALGORITHM) - { - sig_params.signature_algorithm = v; - if ( - v != auth::SIGN_ALGORITHM_ECDSA_SHA256 && - v != auth::SIGN_ALGORITHM_HS_2019) - { - throw std::logic_error( - fmt::format("Signature algorithm '{}' is not supported", v)); - } - } - else if (k == auth::SIGN_PARAMS_SIGNATURE) - { - sig_params.signature = v; - } - else if (k == auth::SIGN_PARAMS_HEADERS) - { - auto parsed_signed_headers = - parse_delimited_string(v, auth::SIGN_PARAMS_HEADERS_DELIMITER); - - if (parsed_signed_headers.size() == 0) - { - throw std::logic_error(fmt::format( - "No headers specified in '{}' field", - auth::SIGN_PARAMS_HEADERS)); - } - - for (const auto& h : parsed_signed_headers) - { - sig_params.signed_headers.emplace_back(h); - } - } - } - else - { - throw std::logic_error(fmt::format( - "authorization parameter '{}' does not contain \"=\"", p)); - } - } - - // If any sig params were not found, this is invalid - if (sig_params.key_id.empty()) - { - throw std::logic_error(fmt::format( - "Signature params: Missing '{}'", auth::SIGN_PARAMS_KEYID)); - } - if (sig_params.signature_algorithm.empty()) - { - throw std::logic_error(fmt::format( - "Signature params: Missing '{}'", auth::SIGN_PARAMS_ALGORITHM)); - } - if (sig_params.signature.empty()) - { - throw std::logic_error(fmt::format( - "Signature params: Missing '{}'", auth::SIGN_PARAMS_SIGNATURE)); - } - if (sig_params.signed_headers.empty()) - { - throw std::logic_error(fmt::format( - "Signature params: Missing '{}'", auth::SIGN_PARAMS_HEADERS)); - } - - return sig_params; - } - - static std::optional parse( - const std::string& verb, - const std::string_view& url, - const http::HeaderMap& headers, - const std::vector& body) - { - auto auth = headers.find(headers::AUTHORIZATION); - if (auth != headers.end()) - { - std::string_view authz_header = auth->second; - - parse_auth_scheme(authz_header); - - std::string verify_error_reason; - if (!verify_digest(headers, body, verify_error_reason)) - { - throw std::logic_error(fmt::format( - "Error verifying HTTP '{}' header: {}", - headers::DIGEST, - verify_error_reason)); - } - - auto parsed_sign_params = parse_signature_params(authz_header); - - const auto& signed_headers = parsed_sign_params.signed_headers; - std::vector missing_required_headers; - for (const auto& required_header : http::required_signature_headers) - { - const auto it = std::find( - signed_headers.begin(), signed_headers.end(), required_header); - if (it == signed_headers.end()) - { - missing_required_headers.push_back(required_header); - } - } - - if (!missing_required_headers.empty()) - { - throw std::logic_error(fmt::format( - "HTTP signature does not cover required fields: '{}'", - fmt::join(missing_required_headers, ", "))); - } - - auto signed_raw = - construct_raw_signed_string(verb, url, headers, signed_headers); - - auto sig_raw = crypto::raw_from_b64(parsed_sign_params.signature); - - crypto::MDType md_type = crypto::MDType::NONE; - if ( - parsed_sign_params.signature_algorithm == - auth::SIGN_ALGORITHM_ECDSA_SHA256) - { - md_type = crypto::MDType::SHA256; - } - - return ccf::SignedReq{ - sig_raw, signed_raw, body, md_type, parsed_sign_params.key_id}; - } - - // Request does not contain authorization header - return std::nullopt; - } - }; -} diff --git a/src/http/test/http_test.cpp b/src/http/test/http_test.cpp index 28f0f75f77..898567dec3 100644 --- a/src/http/test/http_test.cpp +++ b/src/http/test/http_test.cpp @@ -6,7 +6,6 @@ #include "http/http_accept.h" #include "http/http_builder.h" #include "http/http_parser.h" -#include "http/http_sig.h" #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #define DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES @@ -563,184 +562,6 @@ DOCTEST_TEST_CASE("Query parser") } } -struct SignedRequestProcessor : public http::SimpleRequestProcessor -{ - std::queue signed_reqs; - - virtual void handle_request( - llhttp_method method, - const std::string_view& url, - http::HeaderMap&& headers, - std::vector&& body, - int32_t stream_id = 0) override - { - const auto signed_req = http::HttpSignatureVerifier::parse( - llhttp_method_name(method), url, headers, body); - - if (signed_req.has_value()) - { - signed_reqs.push(signed_req.value()); - } - - http::SimpleRequestProcessor::handle_request( - method, url, std::move(headers), std::move(body), stream_id); - } -}; - -DOCTEST_TEST_CASE("Signatures") -{ - // Produce signed requests with some formatting variations, ensure we can - // parse them - auto kp = crypto::make_key_pair(); - const std::string key_id = "UniqueIdentifierForThisKeypair"; - - http::Request request("/foo", HTTP_POST); - request.set_query_param("param", "value"); - request.set_query_param("pet", "dog"); - request.set_header("Host", "example.com"); - request.set_header("Date", "Sun, 05 Jan 2014 21:31:40 GMT"); - request.set_header("Content-Type", "application/json"); - - const std::string body_s("{\"hello\": \"world\"}"); - const std::vector body_v(body_s.begin(), body_s.end()); - - request.set_body(body_v.data(), body_v.size()); - - http::add_digest_header(request); - - { - const auto& headers = request.get_headers(); - const auto it = headers.find(http::headers::DIGEST); - DOCTEST_REQUIRE(it != headers.end()); - - constexpr auto expected_digest_value = - "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="; - DOCTEST_REQUIRE(it->second == expected_digest_value); - } - - DOCTEST_SUBCASE("Some headers") - { - std::vector headers_to_sign; - headers_to_sign.emplace_back(http::auth::SIGN_HEADER_REQUEST_TARGET); - headers_to_sign.emplace_back(http::headers::DIGEST); - - http::sign_request(request, kp, key_id, headers_to_sign); - - const auto serial_request = request.build_request(); - - SignedRequestProcessor sp; - http::RequestParser p(sp); - - p.execute(serial_request.data(), serial_request.size()); - DOCTEST_REQUIRE(sp.signed_reqs.size() == 1); - const auto& sr = sp.signed_reqs.back(); - DOCTEST_REQUIRE(sr.key_id == key_id); - sp.signed_reqs.pop(); - } - - DOCTEST_SUBCASE("All headers") - { - std::vector headers_to_sign; - headers_to_sign.emplace_back(http::auth::SIGN_HEADER_REQUEST_TARGET); - for (const auto& header_it : request.get_headers()) - { - headers_to_sign.emplace_back(header_it.first); - } - - // Try all permutations to test order-independence - std::sort(headers_to_sign.begin(), headers_to_sign.end()); - while (true) - { - http::sign_request(request, kp, key_id, headers_to_sign); - - const auto serial_request = request.build_request(); - - SignedRequestProcessor sp; - http::RequestParser p(sp); - - p.execute(serial_request.data(), serial_request.size()); - DOCTEST_REQUIRE(sp.signed_reqs.size() == 1); - const auto& sr = sp.signed_reqs.back(); - DOCTEST_REQUIRE(sr.key_id == key_id); - sp.signed_reqs.pop(); - - const bool was_last_permutation = - !std::next_permutation(headers_to_sign.begin(), headers_to_sign.end()); - if (was_last_permutation) - { - break; - } - } - } - - DOCTEST_SUBCASE("Unquoted auth values") - { - std::vector headers_to_sign; - headers_to_sign.emplace_back(http::auth::SIGN_HEADER_REQUEST_TARGET); - for (const auto& header_it : request.get_headers()) - { - headers_to_sign.emplace_back(header_it.first); - } - - http::sign_request(request, kp, key_id, headers_to_sign); - - const auto& headers = request.get_headers(); - const auto auth_it = headers.find(http::headers::AUTHORIZATION); - DOCTEST_REQUIRE(auth_it != headers.end()); - - DOCTEST_SUBCASE("Unbalanced quotes") - { - std::string original = auth_it->second; - - std::string missing_first_quote = original; - const auto first_quote = missing_first_quote.find_first_of('"'); - missing_first_quote.erase(missing_first_quote.begin() + first_quote); - - { - request.set_header(http::headers::AUTHORIZATION, missing_first_quote); - const auto serial_request = request.build_request(); - - SignedRequestProcessor sp; - http::RequestParser p(sp); - DOCTEST_REQUIRE_THROWS( - p.execute(serial_request.data(), serial_request.size())); - } - - std::string missing_second_quote = original; - const auto second_quote = - missing_second_quote.find_first_of('"', first_quote + 1); - missing_second_quote.erase(missing_second_quote.begin() + second_quote); - - { - request.set_header(http::headers::AUTHORIZATION, missing_second_quote); - const auto serial_request = request.build_request(); - - SignedRequestProcessor sp; - http::RequestParser p(sp); - DOCTEST_REQUIRE_THROWS( - p.execute(serial_request.data(), serial_request.size())); - } - } - - DOCTEST_SUBCASE("No quotes") - { - std::string auth_value = auth_it->second; - const auto new_end = - std::remove(auth_value.begin(), auth_value.end(), '"'); - auth_value.erase(new_end, auth_value.end()); - - request.set_header(http::headers::AUTHORIZATION, auth_value); - - const auto serial_request = request.build_request(); - - SignedRequestProcessor sp; - http::RequestParser p(sp); - p.execute(serial_request.data(), serial_request.size()); - DOCTEST_REQUIRE(sp.signed_reqs.size() == 1); - } - } -} - DOCTEST_TEST_CASE("Parse Accept header") { { diff --git a/src/node/http_node_client.h b/src/node/http_node_client.h index 47076304d5..ca1191533b 100644 --- a/src/node/http_node_client.h +++ b/src/node/http_node_client.h @@ -27,10 +27,6 @@ namespace ccf const auto& node_cert = endorsed_node_cert.has_value() ? endorsed_node_cert.value() : self_signed_node_cert; - auto node_cert_der = crypto::cert_pem_to_der(node_cert); - const auto key_id = crypto::Sha256Hash(node_cert_der).hex_str(); - - http::sign_request(request, node_sign_kp, key_id); std::vector packed = request.build_request(); diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index a2f764209d..a64f0be9dc 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -466,9 +466,9 @@ namespace ccf cose_recent_proposals->put(key, proposal_id); // Only keep the most recent window_size proposals, to avoid // unbounded memory usage - if (replay_keys.size() >= window_size) + if (replay_keys.size() >= (window_size - 1) /* We just added one */) { - for (size_t i = 0; i < (replay_keys.size() - window_size); i++) + for (size_t i = 0; i < (replay_keys.size() - (window_size - 1)); i++) { cose_recent_proposals->remove(replay_keys[i]); } @@ -606,7 +606,7 @@ namespace ccf openapi_info.description = "This API is used to submit and query proposals which affect CCF's " "public governance tables."; - openapi_info.document_version = "2.25.0"; + openapi_info.document_version = "3.0.0"; } static std::optional get_caller_member_id( @@ -618,12 +618,6 @@ namespace ccf { return cose_ident->member_id; } - else if ( - const auto* sig_ident = - ctx.try_get_caller()) - { - return sig_ident->member_id; - } else if ( const auto* cert_ident = ctx.try_get_caller()) @@ -638,7 +632,6 @@ namespace ccf bool authnz_active_member( ccf::endpoints::EndpointContext& ctx, std::optional& member_id, - std::optional& sig_auth_id, std::optional& cose_auth_id, bool must_be_active = true) { @@ -649,13 +642,6 @@ namespace ccf member_id = cose_ident->member_id; cose_auth_id = *cose_ident; } - else if ( - const auto* sig_ident = - ctx.try_get_caller()) - { - member_id = sig_ident->member_id; - sig_auth_id = *sig_ident; - } else { set_gov_error( @@ -682,16 +668,13 @@ namespace ccf AuthnPolicies member_sig_only_policies(const std::string& gov_msg_type) { - return { - member_signature_auth_policy, - std::make_shared(gov_msg_type)}; + return {std::make_shared(gov_msg_type)}; } AuthnPolicies member_cert_or_sig_policies(const std::string& gov_msg_type) { return { member_cert_auth_policy, - member_signature_auth_policy, std::make_shared(gov_msg_type)}; } @@ -701,13 +684,10 @@ namespace ccf //! A member acknowledges state auto ack = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = - std::nullopt; std::optional cose_auth_id = std::nullopt; std::optional member_id = std::nullopt; - if (!authnz_active_member( - ctx, member_id, sig_auth_id, cose_auth_id, false)) + if (!authnz_active_member(ctx, member_id, cose_auth_id, false)) { return; } @@ -742,21 +722,6 @@ namespace ccf auto sig = ctx.tx.rw(this->network.signatures); const auto s = sig->get(); - if (sig_auth_id.has_value()) - { - if (!s) - { - mas->put( - member_id.value(), MemberAck({}, sig_auth_id->signed_request)); - } - else - { - mas->put( - member_id.value(), - MemberAck(s->root, sig_auth_id->signed_request)); - } - } - if (cose_auth_id.has_value()) { std::vector cose_sign1 = { @@ -1163,12 +1128,10 @@ namespace ccf #pragma clang diagnostic ignored "-Wc99-extensions" auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = - std::nullopt; std::optional cose_auth_id = std::nullopt; std::optional member_id = std::nullopt; - if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) + if (!authnz_active_member(ctx, member_id, cose_auth_id)) { return; } @@ -1184,10 +1147,6 @@ namespace ccf } std::vector request_digest; - if (sig_auth_id.has_value()) - { - request_digest = sig_auth_id->request_digest; - } if (cose_auth_id.has_value()) { request_digest = crypto::sha256( @@ -1332,11 +1291,6 @@ namespace ccf ctx.tx.rw(jsgov::Tables::PROPOSALS_INFO); pi->put(proposal_id, {member_id.value(), ccf::ProposalState::OPEN, {}}); - if (sig_auth_id.has_value()) - { - record_voting_history( - ctx.tx, member_id.value(), sig_auth_id->signed_request); - } if (cose_auth_id.has_value()) { record_cose_governance_history( @@ -1523,12 +1477,10 @@ namespace ccf .install(); auto withdraw_js = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = - std::nullopt; std::optional cose_auth_id = std::nullopt; std::optional member_id = std::nullopt; - if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) + if (!authnz_active_member(ctx, member_id, cose_auth_id)) { return; } @@ -1610,11 +1562,6 @@ namespace ccf pi->put(proposal_id, pi_.value()); remove_all_other_non_open_proposals(ctx.tx, proposal_id); - if (sig_auth_id.has_value()) - { - record_voting_history( - ctx.tx, member_id.value(), sig_auth_id->signed_request); - } if (cose_auth_id.has_value()) { record_cose_governance_history( @@ -1682,12 +1629,10 @@ namespace ccf .install(); auto vote_js = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = - std::nullopt; std::optional cose_auth_id = std::nullopt; std::optional member_id = std::nullopt; - if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) + if (!authnz_active_member(ctx, member_id, cose_auth_id)) { return; } @@ -1797,11 +1742,6 @@ namespace ccf pi_->ballots[member_id.value()] = params["ballot"]; pi->put(proposal_id, pi_.value()); - if (sig_auth_id.has_value()) - { - record_voting_history( - ctx.tx, member_id.value(), sig_auth_id->signed_request); - } if (cose_auth_id.has_value()) { record_cose_governance_history( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index dfb2cb71f6..4e5cdebd2f 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -389,7 +389,7 @@ namespace ccf openapi_info.description = "This API provides public, uncredentialed access to service and node " "state."; - openapi_info.document_version = "2.42.0"; + openapi_info.document_version = "3.0.0"; } void init_handlers() override diff --git a/src/node/rpc/test/frontend_test.cpp b/src/node/rpc/test/frontend_test.cpp index 04a3d14e8e..4aa74bc64e 100644 --- a/src/node/rpc/test/frontend_test.cpp +++ b/src/node/rpc/test/frontend_test.cpp @@ -81,17 +81,6 @@ public: .set_forwarding_required(ccf::endpoints::ForwardingRequired::Sometimes) .install(); - auto empty_function_signed = [this](auto& ctx) { - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - }; - make_endpoint( - "/empty_function_signed", - HTTP_POST, - empty_function_signed, - {user_signature_auth_policy}) - .set_forwarding_required(ccf::endpoints::ForwardingRequired::Sometimes) - .install(); - auto empty_function_no_auth = [this](auto& ctx) { ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); }; @@ -433,23 +422,6 @@ auto create_simple_request( return request; } -http::Request create_signed_request( - const crypto::Pem& caller_cert, - const http::Request& r = create_simple_request(), - const std::vector* body = nullptr) -{ - http::Request s(r); - - s.set_body(body); - - auto caller_cert_der = crypto::cert_pem_to_der(caller_cert); - const auto key_id = crypto::Sha256Hash(caller_cert_der).hex_str(); - - http::sign_request(s, kp, key_id); - - return s; -} - http::SimpleResponseProcessor::Response parse_response(const vector& v) { http::SimpleResponseProcessor processor; @@ -532,90 +504,6 @@ TEST_CASE("SignedReq to and from json") REQUIRE(sr.req.empty()); } -TEST_CASE("process with signatures") -{ - NetworkState network; - prepare_callers(network); - TestUserFrontend frontend(*network.tables); - - SUBCASE("missing rpc") - { - for (const std::string& rpc_name : - {"", "/", "/this_rpc_doesnt_exist", "/this/rpc/doesnt/exist"}) - { - const auto invalid_call = create_simple_request(rpc_name); - const auto serialized_call = invalid_call.build_request(); - auto rpc_ctx = ccf::make_rpc_context(user_session, serialized_call); - - frontend.process(rpc_ctx); - const auto serialized_response = rpc_ctx->serialise_response(); - auto response = parse_response(serialized_response); - REQUIRE(response.status == HTTP_STATUS_NOT_FOUND); - } - } - - SUBCASE("endpoint does not require signature") - { - const auto simple_call = create_simple_request(); - const auto signed_call = create_signed_request(user_caller, simple_call); - const auto serialized_simple_call = simple_call.build_request(); - const auto serialized_signed_call = signed_call.build_request(); - - auto simple_rpc_ctx = - ccf::make_rpc_context(user_session, serialized_simple_call); - auto signed_rpc_ctx = - ccf::make_rpc_context(user_session, serialized_signed_call); - - INFO("Unsigned RPC"); - { - frontend.process(simple_rpc_ctx); - const auto serialized_response = simple_rpc_ctx->serialise_response(); - auto response = parse_response(serialized_response); - REQUIRE(response.status == HTTP_STATUS_OK); - } - - INFO("Signed RPC"); - { - frontend.process(signed_rpc_ctx); - const auto serialized_response = signed_rpc_ctx->serialise_response(); - auto response = parse_response(serialized_response); - REQUIRE(response.status == HTTP_STATUS_OK); - } - } - - SUBCASE("endpoint requires signature") - { - const auto simple_call = create_simple_request("/empty_function_signed"); - const auto signed_call = create_signed_request(user_caller, simple_call); - const auto serialized_simple_call = simple_call.build_request(); - const auto serialized_signed_call = signed_call.build_request(); - - auto simple_rpc_ctx = - ccf::make_rpc_context(user_session, serialized_simple_call); - auto signed_rpc_ctx = - ccf::make_rpc_context(user_session, serialized_signed_call); - - INFO("Unsigned RPC"); - { - frontend.process(simple_rpc_ctx); - const auto serialized_response = simple_rpc_ctx->serialise_response(); - auto response = parse_response(serialized_response); - - CHECK(response.status == HTTP_STATUS_UNAUTHORIZED); - const std::string error_msg(response.body.begin(), response.body.end()); - CHECK(error_msg.find("Missing signature") != std::string::npos); - } - - INFO("Signed RPC"); - { - frontend.process(signed_rpc_ctx); - const auto serialized_response = signed_rpc_ctx->serialise_response(); - auto response = parse_response(serialized_response); - REQUIRE(response.status == HTTP_STATUS_OK); - } - } -} - TEST_CASE("process with caller") { NetworkState network; @@ -1161,23 +1049,6 @@ TEST_CASE("Decoded Templated paths") } } -TEST_CASE("Signed read requests can be executed on backup") -{ - NetworkState network; - prepare_callers(network); - TestUserFrontend frontend(*network.tables); - - auto backup_consensus = std::make_shared(); - network.tables->set_consensus(backup_consensus); - - auto signed_call = create_signed_request(user_caller); - auto serialized_signed_call = signed_call.build_request(); - auto rpc_ctx = ccf::make_rpc_context(user_session, serialized_signed_call); - frontend.process(rpc_ctx); - auto response = parse_response(rpc_ctx->serialise_response()); - CHECK(response.status == HTTP_STATUS_OK); -} - TEST_CASE("Forwarding" * doctest::test_suite("forwarding")) { NetworkState network_primary; @@ -1305,30 +1176,6 @@ TEST_CASE("Forwarding" * doctest::test_suite("forwarding")) channel_stub->clear(); } - { - INFO("Client signature on forwarded RPC is recorded by primary"); - - REQUIRE(channel_stub->is_empty()); - auto signed_call = create_signed_request(user_caller); - auto serialized_signed_call = signed_call.build_request(); - auto signed_ctx = - ccf::make_rpc_context(user_session, serialized_signed_call); - user_frontend_backup.process(signed_ctx); - REQUIRE(signed_ctx->response_is_pending); - REQUIRE(channel_stub->size() == 1); - - auto forwarded_msg = channel_stub->get_pop_back(); - auto fwd_ctx = - backup_forwarder->recv_forwarded_command( - kv::test::FirstBackupNodeId, - forwarded_msg.data(), - forwarded_msg.size()); - - user_frontend_primary.process_forwarded(fwd_ctx); - auto response = parse_response(fwd_ctx->serialise_response()); - CHECK(response.status == HTTP_STATUS_OK); - } - // On a session that was previously forwarded, and is now primary, // commands should still succeed ctx->get_session_context()->is_forwarding = true; diff --git a/src/node/rpc/test/frontend_test_infra.h b/src/node/rpc/test/frontend_test_infra.h index b294ee9676..3fb89e910b 100644 --- a/src/node/rpc/test/frontend_test_infra.h +++ b/src/node/rpc/test/frontend_test_infra.h @@ -89,28 +89,6 @@ std::vector create_request( return r.build_request(); } -std::vector create_signed_request( - const json& params, - const string& method_name, - const crypto::KeyPairPtr& kp_, - const crypto::Pem& caller, - llhttp_method verb = HTTP_POST) -{ - http::Request r(fmt::format("/gov/{}", method_name), verb); - - const auto body = params.is_null() ? std::vector() : - serdes::pack(params, default_pack); - r.set_body(&body); - - const auto contents = caller.raw(); - auto caller_der = crypto::cert_pem_to_der(caller); - const auto key_id = crypto::Sha256Hash(caller_der).hex_str(); - - http::sign_request(r, kp_, key_id); - - return r.build_request(); -} - auto frontend_process( MemberRpcFrontend& frontend, const std::vector& serialized_request, @@ -134,22 +112,6 @@ auto frontend_process( return processor.received.front(); } -auto activate( - MemberRpcFrontend& frontend, - const crypto::KeyPairPtr& kp, - const crypto::Pem& caller) -{ - const auto state_digest_req = - create_request(nullptr, "ack/update_state_digest"); - const auto ack = parse_response_body( - frontend_process(frontend, state_digest_req, caller)); - - StateDigest params; - params.state_digest = ack.state_digest; - const auto ack_req = create_signed_request(params, "ack", kp, caller); - return frontend_process(frontend, ack_req, caller); -} - auto get_cert(uint64_t member_id, crypto::KeyPairPtr& kp_mem) { return kp_mem->self_sign( diff --git a/src/node/rpc/test/proposal_id_test.cpp b/src/node/rpc/test/proposal_id_test.cpp deleted file mode 100644 index f2a1ac2d60..0000000000 --- a/src/node/rpc/test/proposal_id_test.cpp +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache 2.0 License. -#include "node/rpc/test/frontend_test_infra.h" - -constexpr auto test_constitution = R"xxx( -export function validate(input) { - return { valid: true, description: "All good" }; -} -export function resolve(proposal, proposerId, votes) { - // Busy wait - let u = 0; - for (let i = 0; i < 1000000; i++) { - u = i ^ 0.5; - } - return "Open"; -} -export function apply(proposal, proposalId) { -} -)xxx"; - -DOCTEST_TEST_CASE("Unique proposal ids") -{ - NetworkState network; - init_network(network); - auto gen_tx = network.tables->create_tx(); - GenesisGenerator gen(network, gen_tx); - gen.create_service(network.identity->cert, ccf::TxID{}); - - const auto proposer_cert = get_cert(0, kp); - const auto proposer_id = gen.add_member(proposer_cert); - gen.activate_member(proposer_id); - const auto voter_cert = get_cert(1, kp); - const auto voter_id = gen.add_member(voter_cert); - gen.activate_member(voter_id); - - gen.set_constitution(test_constitution); - - DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - - ShareManager share_manager(network); - StubNodeContext context; - MemberRpcFrontend frontend(network, context, share_manager); - - frontend.open(); - const auto proposed_member = get_cert(2, kp); - - nlohmann::json proposal_body = "Ignored"; - const auto propose = - create_signed_request(proposal_body, "proposals", kp, proposer_cert); - - jsgov::ProposalInfoSummary out1; - jsgov::ProposalInfoSummary out2; - - auto fn = []( - MemberRpcFrontend& f, - const std::vector& r, - const crypto::Pem& i, - jsgov::ProposalInfoSummary& o) { - const auto rs = frontend_process(f, r, i); - o = parse_response_body(rs); - }; - - auto t1 = std::thread( - fn, - std::ref(frontend), - std::ref(propose), - std::ref(proposer_cert), - std::ref(out1)); - auto t2 = std::thread( - fn, - std::ref(frontend), - std::ref(propose), - std::ref(proposer_cert), - std::ref(out2)); - t1.join(); - t2.join(); - - DOCTEST_CHECK(out1.state == ProposalState::OPEN); - DOCTEST_CHECK(out2.state == ProposalState::OPEN); - DOCTEST_CHECK(out1.proposal_id != out2.proposal_id); - - // Count retries to confirm that these proposals conflicted and one was - // retried (potentially multiple times, if very unlucky and gets a retried - // root before the earlier transaction has set it) - auto metrics_req = create_request(nlohmann::json(), "api/metrics", HTTP_GET); - auto metrics = frontend_process(frontend, metrics_req, proposer_cert); - auto metrics_json = serdes::unpack(metrics.body, serdes::Pack::Text); - for (auto& row : metrics_json["metrics"]) - { - if (row["path"] == "proposals") - { - DOCTEST_CHECK(row["retries"] >= 1); - } - } -} - -class NullTxHistoryWithOverride : public ccf::NullTxHistory -{ - kv::Version forced_version; - bool forced = false; - -public: - NullTxHistoryWithOverride( - kv::Store& store_, const NodeId& id_, crypto::KeyPair& kp_) : - ccf::NullTxHistory(store_, id_, kp_) - {} - - void force_version(kv::Version v) - { - forced_version = v; - forced = true; - } - - std::tuple - get_replicated_state_txid_and_root() override - { - if (forced) - { - forced = false; - return { - {term_of_last_version, forced_version}, - crypto::Sha256Hash(std::to_string(version)), - term_of_next_version}; - } - else - { - return { - {term_of_last_version, version}, - crypto::Sha256Hash(std::to_string(version)), - term_of_next_version}; - } - } -}; - -DOCTEST_TEST_CASE("Compaction conflict") -{ - NetworkState network; - init_network(network); - network.tables->set_encryptor(encryptor); - auto history = std::make_shared( - *network.tables, kv::test::PrimaryNodeId, *kp); - network.tables->set_history(history); - auto consensus = std::make_shared(); - network.tables->set_consensus(consensus); - auto gen_tx = network.tables->create_tx(); - GenesisGenerator gen(network, gen_tx); - gen.create_service(network.identity->cert, ccf::TxID{}); - - const auto proposer_cert = get_cert(0, kp); - const auto proposer_id = gen.add_member(proposer_cert); - gen.activate_member(proposer_id); - const auto voter_cert = get_cert(1, kp); - const auto voter_id = gen.add_member(voter_cert); - gen.activate_member(voter_id); - - gen.set_constitution(test_constitution); - - DOCTEST_REQUIRE(gen_tx.commit() == kv::CommitResult::SUCCESS); - - // Stub transaction, at which we can compact. Write to a table which the - // proposal execution will try to read, so that it tries to retrieve a - // MapHandle at this forced compacted version - auto tx = network.tables->create_tx(); - tx.rw(network.member_info)->put({}, {}); - DOCTEST_CHECK(tx.commit() == kv::CommitResult::SUCCESS); - auto cv = tx.commit_version(); - network.tables->compact(cv); - - ShareManager share_manager(network); - StubNodeContext context; - MemberRpcFrontend frontend(network, context, share_manager); - - frontend.open(); - const auto proposed_member = get_cert(2, kp); - - nlohmann::json proposal_body = "Ignored"; - const auto propose = - create_signed_request(proposal_body, "proposals", kp, proposer_cert); - - // Force history version to an already compacted version to trigger compaction - // conflict - history->force_version(cv - 1); - - const auto rs = frontend_process(frontend, propose, proposer_cert); - const auto out = parse_response_body(rs); - DOCTEST_CHECK(out.state == ProposalState::OPEN); - - auto metrics_req = create_request(nlohmann::json(), "api/metrics", HTTP_GET); - auto metrics = frontend_process(frontend, metrics_req, proposer_cert); - auto metrics_json = serdes::unpack(metrics.body, serdes::Pack::Text); - for (auto& row : metrics_json["metrics"]) - { - if (row["path"] == "proposals") - { - DOCTEST_CHECK(row["retries"] == 1); - } - } -} - -int main(int argc, char** argv) -{ - js::register_class_ids(); - - doctest::Context context; - context.applyCommandLine(argc, argv); - int res = context.run(); - if (context.shouldExit()) - return res; - return res; -} diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index dfdacaac43..1e3ead78a5 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -622,28 +622,6 @@ def test_multi_auth(network, args): r = c.get("/app/multi_auth") require_new_response(r) - LOG.info("Authenticate as a user, via HTTP signature") - with primary.client(None, user.local_id) as c: - r = c.get("/app/multi_auth") - require_new_response(r) - - LOG.info("Authenticate as a member, via HTTP signature") - with primary.client(None, member.local_id) as c: - r = c.get("/app/multi_auth") - require_new_response(r) - - LOG.info("Authenticate as user2 but sign as user1") - with primary.client("user2", "user1") as c: - r = c.get("/app/multi_auth") - require_new_response(r) - - network.create_user("user5", args.participants_curve, record=False) - - LOG.info("Authenticate as invalid user5 but sign as valid user3") - with primary.client("user5", "user3") as c: - r = c.get("/app/multi_auth") - require_new_response(r) - LOG.info("Authenticate via JWT token") jwt_issuer = infra.jwt_issuer.JwtIssuer() jwt_issuer.register(network) @@ -1268,17 +1246,6 @@ def test_long_lived_forwarding(network, args): return network -@reqs.description("Testing signed queries with escaped queries") -@reqs.installed_package("samples/apps/logging/liblogging") -@reqs.at_least_n_nodes(2) -@reqs.no_http2() -def test_signed_escapes(network, args): - node = network.find_node_by_role() - with node.client("user0", "user0") as c: - escaped_query_tests(c, "signed_request_query") - return network - - @reqs.description("Test user-data used for access permissions") @reqs.supports_methods("/app/log/private/admin_only") def test_user_data_ACL(network, args): @@ -1817,7 +1784,6 @@ def run(args): test_forwarding_frontends(network, args) test_forwarding_frontends_without_app_prefix(network, args) test_long_lived_forwarding(network, args) - test_signed_escapes(network, args) test_user_data_ACL(network, args) test_cert_prefix(network, args) test_anonymous_caller(network, args) diff --git a/tests/governance.py b/tests/governance.py index 3685ed8c96..fe8f274080 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -13,7 +13,6 @@ import suite.test_requirements as reqs import infra.logging_app as app import json import jinja2 -import requests import infra.crypto from datetime import datetime import governance_js @@ -361,45 +360,6 @@ def test_ack_state_digest_update(network, args): return network -@reqs.description("Test invalid client signatures") -def test_invalid_client_signature(network, args): - primary, _ = network.find_primary() - - def post_proposal_request_raw(node, headers=None, expected_error_msg=None): - r = requests.post( - f"https://{node.get_public_rpc_host()}:{node.get_public_rpc_port()}/gov/proposals", - headers=headers, - verify=os.path.join(node.common_dir, "service_cert.pem"), - timeout=3, - ).json() - assert r["error"]["code"] == "InvalidAuthenticationInfo" - assert ( - expected_error_msg in r["error"]["details"][0]["message"] - ), f"Expected error message '{expected_error_msg}' not in '{r['error']['details'][0]['message']}'" - - # Verify that _some_ HTTP signature parsing errors are communicated back to the client - post_proposal_request_raw( - primary, - headers=None, - expected_error_msg="Missing signature", - ) - post_proposal_request_raw( - primary, - headers={"Authorization": "invalid"}, - expected_error_msg="'authorization' header only contains one field", - ) - post_proposal_request_raw( - primary, - headers={"Authorization": "invalid invalid"}, - expected_error_msg="'authorization' scheme for signature should be 'Signature", - ) - post_proposal_request_raw( - primary, - headers={"Authorization": "Signature invalid"}, - expected_error_msg="Error verifying HTTP 'digest' header: Missing 'digest' header", - ) - - @reqs.description("Renew certificates of all nodes, one by one") def test_each_node_cert_renewal(network, args): primary, _ = network.find_primary() @@ -635,7 +595,6 @@ def gov(args): test_no_quote(network, args) test_node_data(network, args) test_ack_state_digest_update(network, args) - test_invalid_client_signature(network, args) test_each_node_cert_renewal(network, args) test_binding_proposal_to_service_identity(network, args) test_all_nodes_cert_renewal(network, args) @@ -784,8 +743,8 @@ def single_node(args): # And we approve 2 proposals while this proposal is active ("just_log", and "set_constitution" to the original) assert info_counts[eval_info] == 10 - assert info_counts[validate_error] == 1 - assert info_counts[apply_error] == 1 + assert info_counts[validate_error] == 1, info_counts + assert info_counts[apply_error] == 1, info_counts def js_gov(args): @@ -800,23 +759,25 @@ def js_gov(args): governance_js.test_proposal_withdrawal(network, args) governance_js.test_ballot_storage(network, args) governance_js.test_pure_proposals(network, args) - if args.authenticate_session == "COSE": - governance_js.test_proposal_replay_protection(network, args) - governance_js.test_cose_msg_type_validation(network, args) - # This test sends proposals identical in content to those sent by - # test_read_write_restrictions, so if it run too soon before or after, it - # risks signing them in the same second and hitting the replay protection. governance_js.test_set_constitution(network, args) governance_js.test_proposals_with_votes(network, args) governance_js.test_vote_failure_reporting(network, args) governance_js.test_operator_proposals_and_votes(network, args) governance_js.test_operator_provisioner_proposals_and_votes(network, args) governance_js.test_apply(network, args) - # See above for why this test needs to be run sufficiently later - # than test_set_constitution governance_js.test_read_write_restrictions(network, args) +def gov_replay(args): + with infra.network.network( + args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb + ) as network: + network.start_and_open(args) + network.consortium.set_authenticate_session(args.authenticate_session) + governance_js.test_proposal_replay_protection(network, args) + governance_js.test_cose_msg_type_validation(network, args) + + if __name__ == "__main__": def add(parser): @@ -845,36 +806,18 @@ if __name__ == "__main__": authenticate_session="COSE", ) - cr.add( - "session_auth", - gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=True, - ) - - cr.add( - "session_noauth", - gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=False, - ) - cr.add( "js", js_gov, package="samples/apps/logging/liblogging", nodes=infra.e2e_args.max_nodes(cr.args, f=0), initial_user_count=3, - authenticate_session=True, + authenticate_session="COSE", ) cr.add( - "js_cose", - js_gov, + "replay", + gov_replay, package="samples/apps/logging/liblogging", nodes=infra.e2e_args.max_nodes(cr.args, f=0), initial_user_count=3, @@ -886,14 +829,6 @@ if __name__ == "__main__": governance_history.run, package="samples/apps/logging/liblogging", nodes=infra.e2e_args.max_nodes(cr.args, f=0), - authenticate_session=False, - ) - - cr.add( - "cose_history", - governance_history.run, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), authenticate_session="COSE", ) diff --git a/tests/governance_js.py b/tests/governance_js.py index 6f814160ba..fdccf90078 100644 --- a/tests/governance_js.py +++ b/tests/governance_js.py @@ -12,9 +12,8 @@ import pprint from contextlib import contextmanager import dataclasses import tempfile -from datetime import datetime import uuid -import time +import infra.clients def action(name, **args): @@ -99,7 +98,6 @@ def test_cose_msg_type_validation(network, args): ("POST", "/gov/ack", "ack"), ("POST", "/gov/ack/update_state_digest", "state_digest"), ("POST", "/gov/recovery_share", "recovery_share"), - ("GET", "/gov/recovery_share", "encrypted_recovery_share"), ] for verb, path, name in to_be_checked: @@ -125,7 +123,7 @@ def test_proposal_validation(network, args): ) ), r.body.text() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post( "/gov/proposals", b"{ not valid json", @@ -214,7 +212,7 @@ def test_proposal_validation(network, args): def test_proposal_storage(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.get("/gov/proposals/42") assert r.status_code == 404, r.body.text() @@ -247,8 +245,9 @@ def test_proposal_storage(network, args): @reqs.description("Test proposal withdrawal") def test_proposal_withdrawal(network, args): node = network.find_random_node() + infra.clients.CLOCK.advance() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: for prop in (valid_set_recovery_threshold, valid_set_recovery_threshold_twice): r = c.post("/gov/proposals/42/withdraw") assert r.status_code == 400, r.body.text() @@ -257,7 +256,7 @@ def test_proposal_withdrawal(network, args): assert r.status_code == 200, r.body.text() proposal_id = r.body.json()["proposal_id"] - with node.client(None, "member1") as oc: + with node.client(None, None, "member1") as oc: r = oc.post(f"/gov/proposals/{proposal_id}/withdraw") assert r.status_code == 403, r.body.text() @@ -293,7 +292,9 @@ def test_proposal_withdrawal(network, args): def test_ballot_storage(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + infra.clients.CLOCK.advance() + + with node.client(None, None, "member0") as c: r = c.post("/gov/proposals", valid_set_recovery_threshold) assert r.status_code == 200, r.body.text() proposal_id = r.body.json()["proposal_id"] @@ -314,7 +315,7 @@ def test_ballot_storage(network, args): assert r.status_code == 200, r.body.text() assert r.body.json() == ballot, r.body.json() - with node.client(None, "member1") as c: + with node.client(None, None, "member1") as c: ballot = ballot_no r = c.post(f"/gov/proposals/{proposal_id}/ballots", ballot) assert r.status_code == 200, r.body.text() @@ -330,7 +331,7 @@ def test_ballot_storage(network, args): def test_pure_proposals(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: for prop, state in [ (always_accept_noop, "Accepted"), (always_reject_noop, "Rejected"), @@ -363,9 +364,10 @@ def test_proposal_replay_protection(network, args): and r.body.json()["error"]["code"] == "InvalidCreatedAt" ), r.body.text() + infra.clients.CLOCK.advance() # Fill window size with proposals window_size = 100 - now = int(datetime.now().timestamp()) - 500 + now = infra.clients.CLOCK.count() submitted = [] for i in range(window_size): c.set_created_at_override(now + i) @@ -424,7 +426,7 @@ def test_proposal_replay_protection(network, args): @reqs.description("Test open proposals") def test_all_open_proposals(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post("/gov/proposals", always_accept_noop) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Accepted", r.body.json() @@ -456,7 +458,7 @@ def opposite(js_bool): @reqs.description("Test vote proposals") def test_proposals_with_votes(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: for prop, state, direction in [ (always_accept_with_one_vote, "Accepted", "true"), (always_reject_with_one_vote, "Rejected", "false"), @@ -471,6 +473,8 @@ def test_proposals_with_votes(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == state, r.body.json() + infra.clients.CLOCK.advance() + r = c.post("/gov/proposals", prop) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Open", r.body.json() @@ -484,7 +488,7 @@ def test_proposals_with_votes(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == state, r.body.json() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: for prop, state, ballot in [ (always_accept_with_two_votes, "Accepted", ballot_yes), (always_reject_with_two_votes, "Rejected", ballot_no), @@ -498,7 +502,7 @@ def test_proposals_with_votes(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Open", r.body.json() - with node.client(None, "member1") as oc: + with node.client(None, None, "member1") as oc: r = oc.post(f"/gov/proposals/{proposal_id}/ballots", ballot) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == state, r.body.json() @@ -509,7 +513,7 @@ def test_proposals_with_votes(network, args): @reqs.description("Test vote failure reporting") def test_vote_failure_reporting(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post("/gov/proposals", always_accept_with_one_vote) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Open", r.body.json() @@ -520,7 +524,7 @@ def test_vote_failure_reporting(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Open", r.body.json() - with node.client(None, "member1") as c: + with node.client(None, None, "member1") as c: ballot = ballot_yes r = c.post(f"/gov/proposals/{proposal_id}/ballots", ballot) assert r.status_code == 200, r.body.text() @@ -538,7 +542,7 @@ def test_vote_failure_reporting(network, args): @reqs.description("Test operator proposals and votes") def test_operator_proposals_and_votes(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post("/gov/proposals", always_accept_if_voted_by_operator) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Open", r.body.json() @@ -549,7 +553,7 @@ def test_operator_proposals_and_votes(network, args): assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Accepted", r.body.json() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post("/gov/proposals", always_accept_if_proposed_by_operator) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Accepted", r.body.json() @@ -563,7 +567,7 @@ def test_operator_provisioner_proposals_and_votes(network, args): node = network.find_random_node() def propose_and_assert_accepted(signer_id, proposal): - with node.client(None, signer_id) as c: + with node.client(None, None, signer_id) as c: r = c.post("/gov/proposals", proposal) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] == "Accepted", r.body.json() @@ -623,7 +627,7 @@ def test_operator_provisioner_proposals_and_votes(network, args): member_id=network.consortium.get_member_by_local_id("member0").service_id, member_data={}, ) - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post("/gov/proposals", illegal_proposal) assert r.status_code == 200, r.body.text() assert r.body.json()["state"] != "Accepted", r.body.json() @@ -742,7 +746,7 @@ def test_actions(network, args): def test_apply(network, args): node = network.find_random_node() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post( "/gov/proposals", proposal(action("always_throw_in_apply")), @@ -754,7 +758,7 @@ def test_apply(network, args): == "Failed to apply(): Error: Error message" ), r.body.json() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: pprint.pprint( proposal(action("always_accept_noop"), action("always_throw_in_apply")) ) @@ -767,7 +771,7 @@ def test_apply(network, args): r = c.post(f"/gov/proposals/{proposal_id}/ballots", ballot_yes) assert r.status_code == 200, r.body().text() - with node.client(None, "member1") as c: + with node.client(None, None, "member1") as c: r = c.post(f"/gov/proposals/{proposal_id}/ballots", ballot_yes) assert r.body.json()["error"]["code"] == "InternalError", r.body.json() assert ( @@ -777,7 +781,7 @@ def test_apply(network, args): "Error: Error message" in r.body.json()["error"]["message"] ), r.body.json() - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post( "/gov/proposals", proposal(action("always_throw_in_resolve")), @@ -798,9 +802,10 @@ def test_apply(network, args): def test_set_constitution(network, args): node = network.find_random_node() + infra.clients.CLOCK.advance() # Create some open proposals pending_proposals = [] - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: r = c.post( "/gov/proposals", valid_set_recovery_threshold, @@ -830,7 +835,7 @@ def test_set_constitution(network, args): ] network.consortium.set_constitution(node, modified_constitution) - with node.client(None, "member0") as c: + with node.client(None, None, "member0") as c: # Check all other proposals were dropped for proposal_id in pending_proposals: r = c.get(f"/gov/proposals/{proposal_id}") @@ -862,6 +867,7 @@ def test_set_constitution(network, args): and r.body.json()["error"]["code"] == "ProposalFailedToValidate" ), r.body.text() + infra.clients.CLOCK.advance() # Confirm modified constitution can still accept valid proposals r = c.post( "/gov/proposals", @@ -1008,8 +1014,6 @@ if (args.try.includes("write_during_{kind}")) {{ table.delete(getSingletonKvKey( for test in tests: LOG.info(test.description) - # Make sure iterations are at least a second apart, to avoid replay protection - time.sleep(1) with temporary_constitution( network, args, diff --git a/tests/infra/clients.py b/tests/infra/clients.py index 6d083fadd9..0b98eb601d 100644 --- a/tests/infra/clients.py +++ b/tests/infra/clients.py @@ -28,6 +28,7 @@ import socket import urllib.parse import httpx +import threading from loguru import logger as LOG # type: ignore import infra.commit @@ -35,6 +36,21 @@ from infra.log_capture import flush_info import ccf.cose +class OffSettableSecondsSinceEpoch: + offset = 0 + + def count(self): + return self.offset + int(datetime.now().timestamp()) + + def advance(self, amount=1): + LOG.info(f"Advancing clock by {amount} seconds") + self.offset += amount + + +CLOCK = threading.local() +CLOCK = OffSettableSecondsSinceEpoch() + + class HttpSig(httpx.Auth): requires_request_body = True @@ -329,7 +345,7 @@ def unpack_seqno_or_view(data): def cose_protected_headers(request_path, created_at=None): - phdr = {"ccf.gov.msg.created_at": created_at or int(datetime.now().timestamp())} + phdr = {"ccf.gov.msg.created_at": created_at or CLOCK.count()} if request_path.endswith("gov/ack/update_state_digest"): phdr["ccf.gov.msg.type"] = "state_digest" elif request_path.endswith("gov/ack"): @@ -344,6 +360,8 @@ def cose_protected_headers(request_path, created_at=None): pid = request_path.split("/")[-2] phdr["ccf.gov.msg.type"] = "withdrawal" phdr["ccf.gov.msg.proposal_id"] = pid + elif request_path.endswith("gov/recovery_share"): + phdr["ccf.gov.msg.type"] = "encrypted_recovery_share" LOG.info(phdr) return phdr @@ -369,11 +387,8 @@ class CurlClient: self.hostname = hostname self.ca = ca self.session_auth = session_auth - self.signing_auth = signing_auth + assert signing_auth is None, signing_auth self.cose_signing_auth = cose_signing_auth - if os.getenv("CURL_CLIENT_USE_COSE"): - self.cose_signing_auth = self.signing_auth - self.signing_auth = None self.common_headers = common_headers or {} self.ca_curve = get_curve(self.ca) self.protocol = kwargs.get("protocol") if "protocol" in kwargs else "https" @@ -388,10 +403,7 @@ class CurlClient: cose_header_parameters_override=None, ): with tempfile.NamedTemporaryFile() as nf: - if self.signing_auth: - cmd = ["scurl.sh"] - else: - cmd = ["curl"] + cmd = ["curl"] url = f"{self.protocol}://{self.hostname}{request.path}" @@ -408,7 +420,7 @@ class CurlClient: content_path = None - if request.body is not None: + if (request.body is not None) or self.cose_signing_auth: if isinstance(request.body, str) and request.body.startswith("@"): # Request is already a file path - pass it directly content_path = request.body @@ -424,6 +436,8 @@ class CurlClient: elif isinstance(request.body, bytes): msg_bytes = request.body content_type = CONTENT_TYPE_BINARY + elif request.body is None: + msg_bytes = b"" else: msg_bytes = json.dumps(request.body).encode() content_type = CONTENT_TYPE_JSON @@ -431,13 +445,10 @@ class CurlClient: nf.write(msg_bytes) nf.flush() content_path = f"@{nf.name}" - if not "content-type" in headers and len(request.body) > 0: + if not "content-type" in headers and request.body: headers["content-type"] = content_type - if self.signing_auth: - cmd = ["scurl.sh"] - else: - cmd = ["curl"] + cmd = ["curl"] if self.cose_signing_auth: pre_cmd = ["ccf_cose_sign1"] @@ -462,10 +473,10 @@ class CurlClient: if request.allow_redirects: cmd.append("-L") - if request.body is not None: - if self.cose_signing_auth: - cmd.extend(["--data-binary", "@-"]) - else: + if self.cose_signing_auth: + cmd.extend(["--data-binary", "@-"]) + else: + if request.body is not None: cmd.extend(["--data-binary", content_path]) # Set requested headers first - so they take precedence over defaults @@ -477,9 +488,6 @@ class CurlClient: if self.session_auth: cmd.extend(["--key", self.session_auth.key]) cmd.extend(["--cert", self.session_auth.cert]) - if self.signing_auth: - cmd.extend(["--signing-key", self.signing_auth.key]) - cmd.extend(["--signing-cert", self.signing_auth.cert]) for arg in self.extra_args: cmd.append(arg) @@ -540,6 +548,7 @@ class HttpxClient: _auth_provider = HttpSig created_at_override = None + _corrupt_signature = False def __init__( self, @@ -629,7 +638,7 @@ class HttpxClient: if not "content-type" in request.headers and len(request.body) > 0: extra_headers["content-type"] = content_type - if self.cose_signing_auth is not None: + if self.cose_signing_auth is not None and request.http_verb != "GET": key = open(self.cose_signing_auth.key, encoding="utf-8").read() cert = open(self.cose_signing_auth.cert, encoding="utf-8").read() phdr = cose_protected_headers(request.path, self.created_at_override) @@ -637,6 +646,8 @@ class HttpxClient: request_body = ccf.cose.create_cose_sign1( request_body or b"", key, cert, phdr ) + if self._corrupt_signature: + request_body = request_body[:-5] + b"0" + request_body[-4:] extra_headers["content-type"] = CONTENT_TYPE_COSE diff --git a/tests/infra/consortium.py b/tests/infra/consortium.py index 71c5d5baa6..c3d9f16178 100644 --- a/tests/infra/consortium.py +++ b/tests/infra/consortium.py @@ -18,6 +18,7 @@ import shutil import tempfile import glob import datetime +import infra.clients from cryptography import x509 import cryptography.hazmat.backends as crypto_backends @@ -54,7 +55,7 @@ class Consortium: members_info=None, curve=None, public_state=None, - authenticate_session=True, + authenticate_session="COSE", ): self.common_dir = common_dir self.members = [] diff --git a/tests/infra/e2e_args.py b/tests/infra/e2e_args.py index aecab5ff30..d920a547c7 100644 --- a/tests/infra/e2e_args.py +++ b/tests/infra/e2e_args.py @@ -298,11 +298,6 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False): type=int, default=1800, ) - parser.add_argument( - "--disable-member-session-auth", - help="Disable session auth for members", - action="store_true", - ) parser.add_argument( "--common-read-only-ledger-dir", help="Location of read-only ledger directory available to all nodes", diff --git a/tests/infra/member.py b/tests/infra/member.py index e37cfdd4b8..60e4a8c685 100644 --- a/tests/infra/member.py +++ b/tests/infra/member.py @@ -57,6 +57,7 @@ class Member: self.is_recovery_member = is_recovery_member self.is_retired = False self.authenticate_session = authenticate_session + assert self.authenticate_session == "COSE", self.authenticate_session self.member_info = {} self.member_info["certificate_file"] = f"{self.local_id}_cert.pem" @@ -137,6 +138,7 @@ class Member: self.is_retired = True def propose(self, remote_node, proposal): + infra.clients.CLOCK.advance() with remote_node.client(*self.auth(write=True)) as mc: r = mc.post("/gov/proposals", proposal) if r.status_code != http.HTTPStatus.OK.value: diff --git a/tests/infra/network.py b/tests/infra/network.py index 28ff6d3784..eb6f5bb6d7 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -532,8 +532,10 @@ class Network: args.consensus, initial_members_info, args.participants_curve, - authenticate_session=not args.disable_member_session_auth, ) + set_authenticate_session = kwargs.pop("set_authenticate_session", None) + if set_authenticate_session is not None: + self.consortium.set_authenticate_session(set_authenticate_session) primary = self._start_all_nodes(args, **kwargs) self.wait_for_all_nodes_to_commit(primary=primary) @@ -592,6 +594,7 @@ class Network: committed_ledger_dirs=None, snapshots_dir=None, common_dir=None, + set_authenticate_session=None, **kwargs, ): """ @@ -629,6 +632,9 @@ class Network: public_state=public_state, ) + if set_authenticate_session is not None: + self.consortium.set_authenticate_session(set_authenticate_session) + for node in self.get_joined_nodes(): self.wait_for_state( node, diff --git a/tests/js-authentication/app.json b/tests/js-authentication/app.json index 18448c5b9a..318f44253d 100644 --- a/tests/js-authentication/app.json +++ b/tests/js-authentication/app.json @@ -25,14 +25,7 @@ "js_module": "endpoints.js", "js_function": "multi_auth", "forwarding_required": "sometimes", - "authn_policies": [ - "user_cert", - "user_signature", - "member_cert", - "member_signature", - "jwt", - "no_auth" - ], + "authn_policies": ["user_cert", "member_cert", "jwt", "no_auth"], "mode": "readonly", "openapi": {} } diff --git a/tests/js-authentication/src/endpoints.js b/tests/js-authentication/src/endpoints.js index 6fed986058..16bcdfa914 100644 --- a/tests/js-authentication/src/endpoints.js +++ b/tests/js-authentication/src/endpoints.js @@ -20,13 +20,6 @@ export function multi_auth(request) { `The caller's user data is: ${JSON.stringify(request.caller.data)}` ); lines.push(`The caller's cert is:\n${request.caller.cert}`); - } else if (request.caller.policy === "user_signature") { - lines.push("User HTTP signature"); - lines.push(`The caller is a user with ID: ${request.caller.id}`); - lines.push( - `The caller's user data is: ${JSON.stringify(request.caller.data)}` - ); - lines.push(`The caller's cert is:\n${request.caller.cert}`); } else if (request.caller.policy === "member_cert") { lines.push("Member TLS cert"); lines.push(`The caller is a member with ID: ${request.caller.id}`); @@ -34,13 +27,6 @@ export function multi_auth(request) { `The caller's user data is: ${JSON.stringify(request.caller.data)}` ); lines.push(`The caller's cert is:\n${request.caller.cert}`); - } else if (request.caller.policy === "member_signature") { - lines.push("Member HTTP signature"); - lines.push(`The caller is a member with ID: ${request.caller.id}`); - lines.push( - `The caller's user data is: ${JSON.stringify(request.caller.data)}` - ); - lines.push(`The caller's cert is:\n${request.caller.cert}`); } else if (request.caller.policy === "jwt") { lines.push("JWT"); lines.push( diff --git a/tests/jwt_test.py b/tests/jwt_test.py index e793fd6c3b..2a9ffde6a0 100644 --- a/tests/jwt_test.py +++ b/tests/jwt_test.py @@ -18,6 +18,7 @@ import ssl import socket import ccf.ledger from ccf.tx_id import TxID +import infra.clients from loguru import logger as LOG @@ -196,7 +197,7 @@ def test_jwt_with_sgx_key_policy(network, args): oe_cert_pem = f.read() kid = "my_kid_with_policy" - issuer = infra.jwt_issuer.JwtIssuer("my_issuer", oe_cert_pem) + issuer = infra.jwt_issuer.JwtIssuer("my_issuer_with_policy", oe_cert_pem) oesign = os.path.join(args.oe_binary, "oesign") oeutil_enc = os.path.join(args.oe_binary, "oeutil_enc.signed") diff --git a/tests/lts_compatibility.py b/tests/lts_compatibility.py index 6d0a97acc7..fb17528e7d 100644 --- a/tests/lts_compatibility.py +++ b/tests/lts_compatibility.py @@ -38,6 +38,16 @@ LOCAL_CHECKOUT_DIRECTORY = "." DEFAULT_NODE_CERTIFICATE_VALIDITY_DAYS = 365 +def update_gov_authn(version): + rv = None + if not infra.node.version_after(version, "ccf-3.0.0"): + rv = False + if infra.node.version_after(version, "ccf-4.0.0-rc0"): + rv = "COSE" + LOG.info(f"Setting gov authn to {rv} because version is {version}") + return rv + + def issue_activity_on_live_service(network, args): log_capture = [] network.txs.issue( @@ -500,10 +510,15 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): kwargs = {} if not infra.node.version_after(version, "ccf-4.0.0-rc1"): kwargs["reconfiguration_type"] = "OneTransaction" + if idx == 0: LOG.info(f"Starting new service (version: {version})") network = infra.network.Network(**network_args) - network.start_and_open(args, **kwargs) + network.start_and_open( + args, + set_authenticate_session=update_gov_authn(version), + **kwargs, + ) else: LOG.info(f"Recovering service (new version: {version})") network = infra.network.Network( @@ -514,6 +529,7 @@ def run_ledger_compatibility_since_first(args, local_branch, use_snapshot): ledger_dir, committed_ledger_dirs, snapshots_dir=snapshots_dir, + set_authenticate_session=update_gov_authn(version), **kwargs, ) # Recovery count is not stored in pre-2.0.3 ledgers diff --git a/tests/memberclient.py b/tests/memberclient.py index b5e1618577..d65878c958 100644 --- a/tests/memberclient.py +++ b/tests/memberclient.py @@ -96,12 +96,10 @@ def test_corrupted_signature(network, args): nc.wait_for_commit(r) with node.client(*member.auth(write=True)) as mc: - # Override the auth provider with invalid ones - for fn in (missing_signature, empty_signature, modified_signature): - # pylint: disable=protected-access - mc.client_impl._auth_provider = make_signature_corrupter(fn) - r = mc.post("/gov/proposals", '{"actions": []}') - assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code + # pylint: disable=protected-access + mc.client_impl._corrupt_signature = True + r = mc.post("/gov/proposals", '{"actions": []}') + assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code # Remove the new member once we're done with them network.consortium.remove_member(node, member) diff --git a/tests/sandbox/sandbox.sh b/tests/sandbox/sandbox.sh index 067099c610..add7c345d3 100755 --- a/tests/sandbox/sandbox.sh +++ b/tests/sandbox/sandbox.sh @@ -99,7 +99,6 @@ fi echo "Python environment successfully setup" export CURL_CLIENT=ON -export CURL_CLIENT_USE_COSE=ON export INITIAL_MEMBER_COUNT=1 exec python "${START_NETWORK_SCRIPT}" \ --binary-dir "${BINARY_DIR}" \