Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move away from http request signing #3875

Closed
achamayou opened this issue May 20, 2022 · 8 comments · Fixed by #4506
Closed

Move away from http request signing #3875

achamayou opened this issue May 20, 2022 · 8 comments · Fixed by #4506
Assignees
Milestone

Comments

@achamayou
Copy link
Member

Governance currently uses http request signing to authenticate governance commands. This causes two main problems:

  1. the draft keeps changing, causing churn (scurl.sh docs: signature_algorithm vs algorithm #3011, Support for shareless members #1866 etc)
  2. client support is (very) poor

So we would like to investigate alternatives, in particular the possibility of using https://cose-wg.github.io/cose-spec/#rfc.section.4.2, and setting the result in a CCF-specific header.

The expected benefits are:

  1. stability, since COSE is published
  2. shorter client code, thanks to COSE libraries, independent from the HTTP layer

We want to validate 2. by writing some sample client code.

@achamayou achamayou self-assigned this May 20, 2022
@achamayou achamayou added the 3.x label May 23, 2022
@shokouedamsr shokouedamsr added this to the 3.x milestone Jul 25, 2022
@achamayou achamayou removed their assignment Aug 10, 2022
@achamayou achamayou self-assigned this Sep 2, 2022
@achamayou
Copy link
Member Author

There are two main top-level possibilities to decouple signing from transport:

  1. Include the signature in the message body itself, using a format like COSE Sign1
  2. Include the signature as a HTTP header, for example as a JWT or a CWT

The benefit of the first approach is complete transport independence. Transports that do not support headers would still work, and programming contexts that do not allow clients to easily control header setting and formatting aren't problematic. Because the body can encoded arbitrarily, this is also an efficient choice.

The second approach inevitably requires base64 encoding, which is somewhat wasteful, and the use of the Authorization header in HTTP. JWTs are however more widespread than COSE, and library support is more widely available.

@achamayou
Copy link
Member Author

achamayou commented Sep 6, 2022

In the case of governance, the signed payload is used repeatedly across transactions, as the proposal or the vote is being evaluated. For that reason, it is extracted and stored in its own table.

In approach 1. where the request body contains the payload inline, this potentially leads to duplication in the KV store: the proposal would be stored once, parsed out, and again, in the middle of a COSE Sign1 frame, for offline audit purposes.

To alleviate this problem, CCF could make use of detached content, as described in https://cose-wg.github.io/cose-spec/#rfc.section.2: the headers and signatures could be store in the evidence table, away from the proposal itself. A verifier would treat the proposal as detached content.

Library support for detached content in COSE seems limited today, from a quick survey:

  • COSE-JAVA: yes
  • t_cose: yes
  • veraison/go-cose: yes
  • COSE-csharp: no
  • pycose: no
  • cose-js: no

This isn't a substantial issue for CCF itself, but may be a hurdle for potential ledger auditors.

@achamayou
Copy link
Member Author

achamayou commented Sep 7, 2022

Proposed format for proposals and ballots:

label = int / tstr
values = any
empty_map = bstr .size 0

Generic_Headers = (
    ? 1 => int / tstr,  ; algorithm identifier
    ? 2 => [+label],    ; criticality
    ? 3 => tstr / int,  ; content type
    ? 4 => bstr,        ; key identifier
    ? 5 => bstr,        ; IV
    ? 6 => bstr,        ; Partial IV
    ? 7 => COSE_Signature / [+COSE_Signature] ; Counter signature
)

CCF_Governance_Headers = (
  "ccf_governance_action" => tstr,    ; "proposal" / "ballot" / "withdrawal" / "ack" / "recovery_share"
  ? "ccf_governance_proposal_id" => tstr ; Proposal id in CCF, set for ballots and withdrawals, but not proposals
)

header_map = {
    Generic_Headers,
    CCF_Governance_Headers
}

Headers = (
    protected : header_map,
    unprotected : empty_map
)

COSE_Sign1 = [
    Headers,
    payload : bstr / nil,   ; JSON payload
    signature : bstr
]

@achamayou
Copy link
Member Author

Python experiments: https://github.com/achamayou/CCF/blob/cose_signing_authn/tests/signing.py#L188

A source of awkwardness compared to HTTP request signing is the need to redundantly indicate what the verb/url already encode, for example in the case of a POST /gov/proposals/{proposal_id}/withdraw. This seems like an unavoidable consequence of transport-independence however.

We could wonder if it is then necessary to have separate endpoints for governance submissions. That seems obviously good on the read side (eg. GET /gov/proposals/{proposal_id}/ballots/{member_id}), and staying symmetrical and compatible seems reasonable.

Another negative point is the lack of support for embedding CDDL in OpenAPI, which means that the schema and the documentation are going to get worse for these endpoints unless we do more things manually/in a non standard way. This would also have been true for JWS/JWT because of the base64 encoding, however.

@achamayou
Copy link
Member Author

achamayou commented Sep 8, 2022

List of endpoints that only accept signed requests by members:

  • POST /ack: JSON input, no response payload link
  • POST /proposals: JSON input, JSON response payload link
  • POST /proposals/{proposal_id}/withdraw no input, JSON response payload link
  • POST /proposals/{proposal_id}/ballots JSON input, JSON response payload link

Other gov endpoints additionally accept signed requests, but also allow session-authed requests.

@achamayou
Copy link
Member Author

I think the way this would look is, there would be a:

class MemberCOSESign1AuthnPolicy : public AuthnPolicy

Which would:

  1. Parse the COSE Sign1 body
  2. Verify the signature, after looking up the member by KID
  3. Expose ccf_governance_action, ?ccf_governance_proposal_id and the content (ideally a std::span into the body) in a MemberCOSESign1AuthnIdentity.

Endpoints listed above would also allow this policy, next to MemberSignatureAuthnPolicy, for the time being (with an eventual deprecation deadline). Other endpoints would drop authentication requirements.

Everything else would remain unchanged, except the public:ccf.gov.history, which would allow a variant value, or more likely would be continued in a new public:ccf.gov.cose_history. Same for public:ccf.gov.acks.

@achamayou
Copy link
Member Author

In anticipation of #4213, we will add a mandatory timestamp in the protected headers.

@achamayou achamayou mentioned this issue Oct 4, 2022
4 tasks
@achamayou achamayou changed the title Investigate moving away from http request signing Move away from http request signing Oct 7, 2022
@achamayou achamayou added M and removed S labels Oct 7, 2022
@achamayou
Copy link
Member Author

achamayou commented Oct 14, 2022

Still to be done:

  • conversion of existing endpoints
  • user-quality Python library support
  • documentation + scurl sample replacements

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants