Authenticating Clients with HTTP Signature
July 9, 2024 ยท View on GitHub
This document describes how to accomplish EWP client authentication with the use of HTTP Signatures.
Introduction
This client authentication method makes use of:
- The
Digestheader, specified in RFC 3230. - The
SHA-256Digest Algorithm, specified in RFC 5843. - The
Authorizationheader, specified in draft-cavage-http-signatures-07 (presumably, soon to become RFC), along with itsrsa-sha256signature algorithm. - Optional "Nonce & Timestamp" replay attack prevention.
Implementing a server
The order of the following steps seems optimal, but you MAY perform them in different order, if you wish (as long as you perform all of them).
Verify authentication method used
Make sure that:
-
The client's request contains the
Authorizationheader with theSignaturemethod, as specified here. -
The
algorithmparameter of theAuthorizationheader is equal torsa-sha256. You MAY support other algorithms too, butrsa-sha256is (currently) the only one required. -
The
headersparameter of theAuthorizationheader contains at least the following values:(request-target),host,dateororiginal-date(it MUST contain at least one of those, it also MAY contain both),digest,x-request-id.
Note, that you MUST allow
headersparameter to contain more values than the required ones listed here.
If one or more of these conditions is not met, then the client doesn't use this method of authentication, or is using it incorrectly. In this case:
-
If you support other client authentication methods at this endpoint, then you SHOULD check if the client doesn't use any of those.
-
If the client doesn't use any of the supported authentication methods, then you MUST respond with HTTP 400 or HTTP 401 error response, with a proper
<developer-message>(it SHOULD describe the reason why you consider the request to be invalid).If HTTP Signature Authentication is the preferred method of authentication at this endpoint, then is it RECOMMENDED to respond with HTTP 401, and include the following headers:
WWW-Authenticate, as described here. Yourrealmvalue SHOULD beEWP.Want-Digest, as described here.
For example:
WWW-Authenticate: Signature realm="EWP" Want-Digest: SHA-256
Verify the host
You MUST verify that the value of the Host header included in the request
matches your own domain (the one on which the endpoint is served).
This verification is sometimes handled by the web server itself, and you simply won't receive requests which don't match your host. If you're not sure, then verify it in your code anyway.
Look up the key
Extract the keyId from the request's Authorization header. (In case of
problems, respond with HTTP 400 error message.)
The key MUST match at least one of the public client keys published in the
Registry Service. Consult Registry API for
information on how to find a match. If you cannot find a match, then you MUST
respond with HTTP 403 error response. As usual, including a proper
<developer-message> is RECOMMENDED.
Verify the date(s)
You need to parse and verify the values of the Date and
Original-Date headers, if they are included in the
request (at least one of them MUST be). In particular, you MUST verify at least
the ones which have been listed in the request's Authorization header, but it
is RECOMMENDED to verify both (if both are included in the request).
Verification process consists of parsing the date, and matching it against the date reported by your own server clock. The verification fails if:
-
The date cannot be parsed.
-
The date does not match your server clock within a certain threshold of time.
- It is RECOMMENDED to use the 5 minutes threshold.
- You MAY choose a greater threshold than 5 minutes, but you MUST NOT choose a lower threshold than this.
If the verification fails, then you MUST respond with HTTP 400 error response.
Your error response SHOULD include a proper <developer-message>.
Also note, that you MUST make sure that your clock is synchronized (otherwise your clients won't be able to use your service).
Verify the nonce (optional)
This step is OPTIONAL if TLS is used for transport (in EWP, most APIs require TLS to be used). However, if TLS is not used (or cannot be trusted for any reason), then this step is REQUIRED in order to prevent replay attacks. Also see Security Considerations section below.
The X-Request-Id header can be used as a cryptographic
nonce. If you decide to
implement nonce verification, then you MUST reject requests who's nonce has
already been used. You will need to store the set of nonces which have been
used. Thanks to the Date header (or Original-Date header), you don't have
to store the nonces which have been used earlier than 5 minutes ago.
Verify X-Request-Id
Even if you don't verify the nonce in the previous step, it is still
RECOMMENDED to verify the format of the X-Request-Id header. It SHOULD be an
an UUID formatted in a canonical form, e.g. dc05b425-4e86-4106-8dde-1257fccf53e5.
If it's not, then you SHOULD respond with HTTP 400 error response.
If you're wondering why we are recommending this, then it's because we want to
force clients to use proper values in their X-Request-Id header. We expect
that most servers won't be verifying nonces at this time, and without this
recommendation, some clients might be tempted to include "dummy" values in
their X-Request-Id header, and no servers would notice that. We want the
network to collectively prevent that.
Verify the signature
You MUST verify the request's signature, as explained here.
If the signature is invalid, then you MUST respond with a HTTP 400 error
response. Your error response SHOULD include a proper <developer-message>.
Verify the digest
Calculate the Base64-encoded SHA-256 digest of the HTTP request's body,
according to RFC 3230 and RFC 5843. Compare it
to the Digest header which should be present in the request. The values MUST
match.
In case of mismatch, you MUST respond with HTTP 400 error response. Your error
response SHOULD include a proper <developer-message>.
Ignore unsigned headers
Servers MUST ignore all request headers which hadn't been signed by the client. They might have been added by the attacker in transport. This is important especially in cases when TLS is not used in some parts of the transport, or when you don't fully trust the partner's TLS implementation.
The safest way to properly ignore such headers is to modify your Request
object now (during the authentication and authorization process), by either
removing the suspicious headers, or at least changing their name (e.g.
prepending it with Unsigned-). Then, pass the modified Request along as the
result of your authentication, so that the actual API you are implementing
believes that the client didn't supply these headers at all. This approach is
much safer than trusting yourself to remember to verify this every time before
you access every header in every single ones of your APIs (and some APIs might
be dependent on request's headers).
Identify the covered HEI
In most cases, you will also need to identify which HEI is covered by the requester (most APIs will require that). Note that the requester may not cover any HEI.
In the previous steps you have already found a <host> element
bound to the client's public key. Now, you will need to build on that
information, and retrieve the HEI this host covers. Consult
Registry API specification for useful hints (i.e., examples of
XPath expressions).
Implementing a client
Generate a key-pair
You need to generate an RSA key-pair for your client. You MAY use the same key-pair you use for your TLS communication if you want to.
Publish your public key
Each partner declares (in his Manifest file) a list of public keys it will use for communicating with other hosts. This list is later fetched by registry, and the keys (and/or their fingerprints) are served to all other partners see (see Registry API for details).
Public keys are bound to the HEI you cover. Once the server confirms that the client is in possession of a proper private key, it is then able to identify (with the help of the Registry again) which HEI such client covers.
Note that the Registry will verify if your keys meet certain security standards (i.e., their length). These standards MAY change in time.
Include all required headers
-
You MUST include either the
DateorOriginal-Dateheader in your request. You MAY include both of them. The format of theOriginal-Dateheader, if included, MUST match the "regular" format of theDateheader, as defined in RFC 2616. You MUST make sure that your clock is synchronized (otherwise your request may fail). -
You MUST include
X-Request-Idheader in your request. It's value MUST be an UUID (preferably, version 4), unpredictable and unique for each request, formatted in a canonical form, e.g.dc05b425-4e86-4106-8dde-1257fccf53e5. If it's not unique, or it is in different format, then your request MAY fail. -
You MUST include the
Digestheader in your request, as explained here. You MUST use SHA-256 algorithm for generating the digest.
Sign your request
You MUST include the Authorization header in your request, as explained
here. You MUST use the rsa-sha256 signature
algorithm, and you MUST include at least the following values in your
headers parameter:
(request-target),host,dateororiginal-date(it MUST contain at least one of those, it also MAY contain both),digest,x-request-id.
If it is important for the server to recognize any other of your headers, then you MUST sign all these headers too. The headers mentioned above are important for handling authentication, non-repudiation and security described in this document, but other headers also MAY be essential in your case.
One such example would be the Accept-Signature header, as explained
here.
The keyId parameter of the Authorization header MUST contain a
HEX-encoded SHA-256 fingerprint of the public key part of the key-pair which
you have used to sign your request. It MUST match one of the keys you
previously published in your manifest file.
Take care to not modify it again
Many frameworks or proxies might try to automatically modify your request after you sign it. For example, it might add or replace some headers with more proper values. In many cases, this would be a good thing, but in this case, such changes could break your HTTP Signature. Make sure that you try to disable all such automatic modifications when you use HTTP Signatures for signing.
Security Considerations
Replay attacks
HTTP Signatures, on their own, do not protect servers against replay attacks. For this reason, most EWP APIs require all communication to occur over TLS. TLS, when properly deployed, does protect the servers against replay attacks.
Some partners pointed out that in some cases we cannot be 100% sure that TLS
has been securely deployed by all the partners. For example, if the client's
machine is tricked into installing attacker's root certificate,
man-in-the-middle attacks are possible. In this scenario, the use of end-to-end
HTTP signatures prevents the attacker from modifying the message, but it
doesn't prevent him from executing replay-attacks. With help of the Date
(Original-Date) and X-Request-Id headers, partners MAY implement additional
security measures to prevent such attacks.
Non-repudiation
It's worth noting, that servers may store the request, along with its headers, in order to be able to prove in the future that the request actually took place. In some cases, for example approving important documents by the other party, this feature might be handy.
Main security questions
The Authentication and Security document recommends that each client authentication method specification explicitly answers the following questions:
How the client's request must look like? How can the server detect that the client is using this particular method for authentication?
See Implementing a client chapter above. The server detect this method by
checking for the existence of a proper set of headers (in particular, the
Authorization: Signature header).
How can the server verify which HEI is covered by the requester?
This is described in the Identify the covered HEI chapter above (in the Implementing a server section).
How can the server verify that the request has not been tampered with, nor replayed by a third party?
Tampering would invalidate the signature. As to the replay attacks - see a separate chapter above.
Does it provide non-repudiation? Can a server provide a solid proof later on, that a particular request took place, and that it originated from a certain client?
Yes. See Non-repudiation section above.