Remove signed HTTP request support (#5137)

This commit is contained in:
Amaury Chamayou 2023-04-19 11:47:44 +01:00 коммит произвёл GitHub
Родитель defca1153f
Коммит 5b1c504cdb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
51 изменённых файлов: 156 добавлений и 2093 удалений

Просмотреть файл

@ -1,5 +1,4 @@
--- ___
(- -) (o o) | Y & +-
( V ) z O z O +---=---'
/--x-m- /--m-m---xXx--/--yY-----
/--x-m- /--m-m---xXx--/--yY-----

Просмотреть файл

@ -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]

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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
----------------

Просмотреть файл

@ -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"``

Просмотреть файл

@ -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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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.

Просмотреть файл

@ -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://<ccf-node-address>/gov/ack --cacert service_cert.pem --data-binary @- -H "content-type: application/cose"
true
Or alternatively:
.. code-block:: bash
$ scurl.sh https://<ccf-node-address>/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

Просмотреть файл

@ -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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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

Просмотреть файл

@ -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 <https://tools.ietf.org/html/draft-cavage-http-signatures-12>`_ scheme):
.. code-block:: bash
# First, retrieve the hash to be signed
$ scurl.sh https://<ccf-node-address>/gov/<endpoint> -X [GET|POST] --signing-cert $IDENTITY_CERT_NAME.pem --print-digest-to-sign
Hash 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="<insert_base64_signature_here>"' # 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": "<hash_to_sign>"}' -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 <https://tools.ietf.org/html/rfc7518#section-3.4>`_ and encoded in `base64url <https://tools.ietf.org/html/rfc4648#section-5>`_ format and are not directly compatible with the signatures supported by CCF.
The :ccf_repo:`jws_to_der.py </doc/governance/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://<ccf-node-address>/gov/<endpoint> -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: <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 <base64_encrypted_share>
# 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`).
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.

Просмотреть файл

@ -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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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).

Просмотреть файл

@ -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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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://<ccf-node-address>/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.
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.

Просмотреть файл

@ -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": []
},

Просмотреть файл

@ -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": []
}

Просмотреть файл

@ -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": {

Просмотреть файл

@ -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 <https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08>`_ , but
supports `ecdsa-sha256` as well as `hs2019` signing algorithms as described in the later `draft 12 <https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-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 <https://www.python-httpx.org/>`_.
.. 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 <https://www.python-httpx.org/>`_.
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.
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.

Просмотреть файл

@ -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 <memory>
@ -27,22 +26,11 @@ namespace ccf
static std::shared_ptr<UserCertAuthnPolicy> user_cert_auth_policy =
std::make_shared<UserCertAuthnPolicy>();
/** Authenticate using HTTP request signature, and
* @c public:ccf.gov.users.certs table */
static std::shared_ptr<UserSignatureAuthnPolicy> user_signature_auth_policy =
std::make_shared<UserSignatureAuthnPolicy>();
/** Authenticate using TLS session identity, and
* @c public:ccf.gov.members.certs table */
static std::shared_ptr<MemberCertAuthnPolicy> member_cert_auth_policy =
std::make_shared<MemberCertAuthnPolicy>();
/** Authenticate using HTTP request signature, and
* @c public:ccf.gov.members.certs table */
static std::shared_ptr<MemberSignatureAuthnPolicy>
member_signature_auth_policy =
std::make_shared<MemberSignatureAuthnPolicy>();
/** 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 */

Просмотреть файл

@ -133,11 +133,9 @@ namespace ccf::endpoints
* retrieved inside the endpoint with ctx.get_caller<IdentType>(),
* @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;
};

Просмотреть файл

@ -43,13 +43,11 @@ namespace ccf
std::span<const uint8_t> 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
{

Просмотреть файл

@ -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<VerifierCache> verifiers;
public:
static constexpr auto SECURITY_SCHEME_NAME = "user_signature";
UserSignatureAuthnPolicy();
virtual ~UserSignatureAuthnPolicy();
std::unique_ptr<AuthnIdentity> authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason) override;
void set_unauthenticated_error(
std::shared_ptr<ccf::RpcContext> ctx,
std::string&& error_reason) override;
std::optional<OpenAPISecuritySchema> 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<uint8_t> request_digest;
};
class MemberSignatureAuthnPolicy : public AuthnPolicy
{
protected:
static const OpenAPISecuritySchema security_schema;
std::unique_ptr<VerifierCache> verifiers;
public:
static constexpr auto SECURITY_SCHEME_NAME = "member_signature";
MemberSignatureAuthnPolicy();
virtual ~MemberSignatureAuthnPolicy();
std::unique_ptr<AuthnIdentity> authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason) override;
void set_unauthenticated_error(
std::shared_ptr<ccf::RpcContext> ctx,
std::string&& error_reason) override;
std::optional<OpenAPISecuritySchema> get_openapi_security_schema()
const override
{
return security_schema;
}
std::string get_security_scheme_name() override
{
return SECURITY_SCHEME_NAME;
}
};
}

Просмотреть файл

@ -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}. */

Просмотреть файл

@ -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<RecordsIndexingStrategy>(
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<ccf::UserSignatureAuthnIdentity>())
{
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<ccf::MemberSignatureAuthnIdentity>())
{
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<ccf::JwtAuthnIdentity>())
{
@ -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<void, std::string>()
@ -1739,22 +1689,6 @@ namespace loggingapp
.set_auto_schema<void, std::string>()
.install();
auto get_signed_request_query = [this](auto& ctx) {
ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
std::vector<uint8_t> 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<void, std::string>()
.install();
auto post_cose_signed_content =
[this](ccf::endpoints::EndpointContext& ctx) {
const auto& caller_identity =

Просмотреть файл

@ -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<ccf::UserSignatureAuthnIdentity>())
{
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<ccf::MemberSignatureAuthnIdentity>())
{
policy_name = get_policy_name_from_ident(member_sig_ident);
id = member_sig_ident->member_id;
is_member = true;
}
if (policy_name == nullptr)
{

Просмотреть файл

@ -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<T, ccf::UserSignatureAuthnIdentity>)
{
return ccf::UserSignatureAuthnPolicy::SECURITY_SCHEME_NAME;
}
else if constexpr (std::is_same_v<T, ccf::MemberCertAuthnIdentity>)
{
return ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME;
}
else if constexpr (std::is_same_v<T, ccf::MemberSignatureAuthnIdentity>)
{
return ccf::MemberSignatureAuthnPolicy::SECURITY_SCHEME_NAME;
}
else if constexpr (std::is_same_v<T, ccf::JwtAuthnIdentity>)
{
return ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME;

Просмотреть файл

@ -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();
}
};

Просмотреть файл

@ -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"

Просмотреть файл

@ -11,7 +11,6 @@
#define FMT_HEADER_ONLY
#include <fmt/format.h>
#include <http/http_sig.h>
#include <nlohmann/json.hpp>
#include <optional>
#include <thread>
@ -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();
}

Просмотреть файл

@ -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<SignedReq> parse_signed_request(
const std::shared_ptr<ccf::RpcContext>& 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<crypto::Pem, crypto::VerifierPtr> verifiers;
VerifierCache(size_t max_verifiers = DEFAULT_MAX_VERIFIERS) :
verifiers(max_verifiers)
{}
crypto::VerifierPtr get_verifier(const crypto::Pem& pem)
{
std::lock_guard<ccf::pal::Mutex> 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<VerifierCache>())
{}
UserSignatureAuthnPolicy::~UserSignatureAuthnPolicy() = default;
std::unique_ptr<AuthnIdentity> UserSignatureAuthnPolicy::authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason)
{
std::optional<SignedReq> 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<UserSignatureAuthnIdentity>();
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<ccf::RpcContext> 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<VerifierCache>())
{}
MemberSignatureAuthnPolicy::~MemberSignatureAuthnPolicy() = default;
std::unique_ptr<AuthnIdentity> MemberSignatureAuthnPolicy::authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason)
{
std::optional<SignedReq> 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<uint8_t> 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<MemberSignatureAuthnIdentity>();
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<ccf::RpcContext> 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."}});
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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 <fmt/format.h>
#include <optional>
#include <string>
namespace http
{
inline std::vector<uint8_t> construct_raw_signed_string(
std::string verb,
const std::string_view& url,
const http::HeaderMap& headers,
const std::vector<std::string_view>& 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<uint8_t>({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<std::string_view>& 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<std::string_view> 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<std::string_view> 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<uint8_t>& 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<std::string_view> parse_delimited_string(
std::string_view& s, const std::string& delimiter)
{
std::vector<std::string_view> 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<ccf::SignedReq> parse(
const std::string& verb,
const std::string_view& url,
const http::HeaderMap& headers,
const std::vector<uint8_t>& 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<std::string> 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;
}
};
}

Просмотреть файл

@ -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<ccf::SignedReq> signed_reqs;
virtual void handle_request(
llhttp_method method,
const std::string_view& url,
http::HeaderMap&& headers,
std::vector<uint8_t>&& 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<uint8_t> 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<std::string_view> 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<std::string_view> 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<std::string_view> 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")
{
{

Просмотреть файл

@ -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<uint8_t> packed = request.build_request();

Просмотреть файл

@ -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<MemberId> 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<ccf::MemberSignatureAuthnIdentity>())
{
return sig_ident->member_id;
}
else if (
const auto* cert_ident =
ctx.try_get_caller<ccf::MemberCertAuthnIdentity>())
@ -638,7 +632,6 @@ namespace ccf
bool authnz_active_member(
ccf::endpoints::EndpointContext& ctx,
std::optional<MemberId>& member_id,
std::optional<ccf::MemberSignatureAuthnIdentity>& sig_auth_id,
std::optional<ccf::MemberCOSESign1AuthnIdentity>& 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<ccf::MemberSignatureAuthnIdentity>())
{
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<MemberCOSESign1AuthnPolicy>(gov_msg_type)};
return {std::make_shared<MemberCOSESign1AuthnPolicy>(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<MemberCOSESign1AuthnPolicy>(gov_msg_type)};
}
@ -701,13 +684,10 @@ namespace ccf
//! A member acknowledges state
auto ack = [this](ccf::endpoints::EndpointContext& ctx) {
std::optional<ccf::MemberSignatureAuthnIdentity> sig_auth_id =
std::nullopt;
std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
std::nullopt;
std::optional<MemberId> 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<uint8_t> 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<ccf::MemberSignatureAuthnIdentity> sig_auth_id =
std::nullopt;
std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
std::nullopt;
std::optional<MemberId> 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<uint8_t> 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<ccf::jsgov::ProposalInfoMap>(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<ccf::MemberSignatureAuthnIdentity> sig_auth_id =
std::nullopt;
std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
std::nullopt;
std::optional<MemberId> 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<ccf::MemberSignatureAuthnIdentity> sig_auth_id =
std::nullopt;
std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
std::nullopt;
std::optional<MemberId> 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(

Просмотреть файл

@ -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

Просмотреть файл

@ -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<uint8_t>* 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<uint8_t>& 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<kv::test::BackupStubConsensus>();
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<ccf::ForwardedHeader_v1>(
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;

Просмотреть файл

@ -89,28 +89,6 @@ std::vector<uint8_t> create_request(
return r.build_request();
}
std::vector<uint8_t> 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<uint8_t>() :
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<uint8_t>& 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<StateDigest>(
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(

Просмотреть файл

@ -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<uint8_t>& r,
const crypto::Pem& i,
jsgov::ProposalInfoSummary& o) {
const auto rs = frontend_process(f, r, i);
o = parse_response_body<jsgov::ProposalInfoSummary>(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<kv::TxID, crypto::Sha256Hash, kv::Term>
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<NullTxHistoryWithOverride>(
*network.tables, kv::test::PrimaryNodeId, *kp);
network.tables->set_history(history);
auto consensus = std::make_shared<kv::test::PrimaryStubConsensus>();
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<jsgov::ProposalInfoSummary>(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;
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -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",
)

Просмотреть файл

@ -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,

Просмотреть файл

@ -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

Просмотреть файл

@ -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 = []

Просмотреть файл

@ -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",

Просмотреть файл

@ -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:

Просмотреть файл

@ -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,

Просмотреть файл

@ -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": {}
}

Просмотреть файл

@ -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(

Просмотреть файл

@ -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")

Просмотреть файл

@ -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

Просмотреть файл

@ -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)

Просмотреть файл

@ -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}" \