-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
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
HTTP signatures #4146
HTTP signatures #4146
Conversation
Accidentally fix lease_seconds not being set and sent properly, and change the new minimum subscription duration to 1 day
b1775c4
to
d2dbb4d
Compare
8310a56
to
0eb1957
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worried about how signed date
HTTP headers are handled, and about handling of multiple headers with the same fields (although it seems like Mastodon-originating requests won't have multiple occurrences of the same header field). Seems ok otherwise.
if signed_header == Request::REQUEST_TARGET | ||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" | ||
elsif signed_header == 'date' | ||
"date: #{Time.now.utc.httpdate}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, am I missing something? How can there be any chance that Time.now.utc
matches the request's Date
field? I don't think this should be special-cased at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How else are you supposed to prevent replay attacks? You need to check that a request that says it comes at a specific time did come at the specific time. This is an honest question - the spec itself is extremely blurry about this, though it does mandate the "date" header for some kind of verification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Date
header could indeed be used to avoid replay attacks, but it is completely out of this draft's scope (the Date
header is actually a SHOULD, not a MUST). It is very unlikely that the request's Date
header matches the time it is processed on the server down to the second.
If you are worried about replay attacks, you could verify that the difference between the time of the Date
header and the time it is processed does not exceed some reasonable bound. I'm not sure what a reasonable bound would be, as many servers are slightly out of date and do not run ntp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So signing the date header makes sense to make every signature different, but verifying its value is not important, is that right? We could give it like a minute drift window, I think that's what TOTP does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I guess the reason they recommend signing Date
is to do that kind of verification, but it's a separate step.
elsif signed_header == 'date' | ||
"date: #{Time.now.utc.httpdate}" | ||
else | ||
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not confident you are handling “duplicate” headers correctly.
Cf. point 2.3.2. of https://tools.ietf.org/html/draft-cavage-http-signatures-06
@@ -0,0 +1,77 @@ | |||
# frozen_string_literal: true | |||
|
|||
module SignatureVerification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole module should probably be documented with the spec you are implementing, especially since it is not standard. In the review, I will assume you are implementing https://tools.ietf.org/html/draft-cavage-http-signatures-06
end | ||
|
||
def signed_headers | ||
@headers.keys.join(' ').downcase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I can't see any particular issue with that, I am not convinced signing all headers is useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, by default we only have user-agent, date and host headers. Places that make use of add_headers
only add headers that are required by e.g. spec. So I don't think there is an issue here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see no use for stuff like user-agent to be signed, but I have no real opposition to it either, so I'm fine with this.
HTTP.headers(user_agent: 'Mastodon/PubSubHubbub') | ||
.timeout(:per_operation, write: 20, connect: 20, read: 50) | ||
.get(subscription.callback_url, params: callback_params) | ||
Request.new(:get, subscription.callback_url, params: callback_params).perform |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are effectively changing the timeout parameters here. Not sure if it's a problem, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am unifying them in one place, which I think is the primary benefit of this. Also, letting a request stall for 90 seconds wasn't good behaviour, I think. We'd rather fail and retry later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right.
HTTP.timeout(:per_operation, write: 50, connect: 20, read: 50) | ||
.headers(headers) | ||
.post(subscription.callback_url, body: payload) | ||
request = Request.new(:post, subscription.callback_url, body: payload) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing about timeouts.
@ThibG Addressed date issue and documented draft. Don't think duplicate headers should occur in our use case. |
@Gargron I'm still worried a 30s window is much too tight: I routinely see time discrepancies of the order of minutes between Mastodon instances. Furthermore, I think at least in the case of PuSH subscriptions, replay attacks are a non-issue. Can't really say for ActivityPub, though. Regarding multiple headers for the same field, it will not be an issue between Mastodon instances, but this means you don't actually verify signatures according to the spec, and any software that would sign multiple headers of the same name for whatever reason won't correctly interoperate with Mastodon. |
@ThibG I am worried that if someone gets their hands on such requests in real time, they could replay them and the server would genuinely respond to them, within that time window. If we want to use this for access control, that's risky. That being said, SSL should protect the requests from being caught by a 3rd party, so I don't know if the risk is real. |
@Gargron well, to be honest, there are things I don't get with the spec itself: why doesn't it cover signing the request data, but only headers? That doesn't really protect you from MITM attacks that would just change the request data while keeping the request headers intact. Regarding replay attacks, as you said, TLS should protect the requests from being caught by a 3rd party. And if not, you have worse problems (namely, the aforementioned one, or tampering with the key exchange in the first place, etc.) |
@ThibG It delegates that to the Digest header from a different spec |
@Gargron thanks, I missed that, that makes sense! So, assuming you follow RFC 3230 and sign the Digest header, replay attacks are just replay attacks. How much of an issue they are depends on the kind of messages you're dealing with, and I would argue they are not much of an issue for things like PuSH subscriptions. I think I would make the validity window an optional parameter, make it default to something like a few minutes, and specifically log requests that fail validating because of this. |
@ThibG I'd like to leave it as-is. Nothing right now actually makes use of the value yet, and even if verification fails, all we get is the status quo, it's not like the request gets denied. We'll have time to figure this out in future PRs. |
@Gargron I would still argue it should be an optional parameter, and such failures should be logged. As for my other gripe with this PR, the handling of multiple headers sharing the same field-name… it seems to me it would be quite complicated to get it right with RoR, which joins such values with “, ” (which is sound with regards to the HTTP specs, but is a problem for the HTTP Signature one). That should probably be explained somewhere in this module. |
app/services/subscribe_service.rb
Outdated
'hub.callback': api_subscription_url(@account.id), | ||
'hub.verify': 'async', | ||
'hub.secret': @account.secret, | ||
'hub.lease_seconds': 30.days.seconds, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually a change, I think, because through a bug it was 7 days previously. I think we might want to keep it to 7 days instead of 30, because while 30 is more efficient, 7 means less "black hole" time if somebody messes up and loses their DB with all subscription data.
From the spec:
So it works out of the box? |
@Gargron ah yes, sorry, indeed, it should work out of the box then! So the only change I'm suggesting now is the time window as parameter, and some logging when it doesn't match the window. |
as PuSH subscription duration (which was previous default due to a bug)
@ThibG Fixed! |
* Add Request class with HTTP signature generator Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06 * Add HTTP signature verification concern * Add test for SignatureVerification concern * Add basic test for Request class * Make PuSH subscribe/unsubscribe requests use new Request class Accidentally fix lease_seconds not being set and sent properly, and change the new minimum subscription duration to 1 day * Make all PuSH workers use new Request class * Make Salmon sender use new Request class * Make FetchLinkService use new Request class * Make FetchAtomService use the new Request class * Make Remotable use the new Request class * Make ResolveRemoteAccountService use the new Request class * Add more tests * Allow +-30 seconds window for signed request to remain valid * Disable time window validation for signed requests, restore 7 days as PuSH subscription duration (which was previous default due to a bug)
Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06
Access control on Atom resource URLs can be implemented with this spec.
TODO:
Request
)SignatureVerification
)Potential uses in the future: