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

HTTP signatures #4146

Merged
merged 14 commits into from
Jul 14, 2017
Merged

HTTP signatures #4146

merged 14 commits into from
Jul 14, 2017

Conversation

Gargron
Copy link
Member

@Gargron Gargron commented Jul 10, 2017

Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06

Access control on Atom resource URLs can be implemented with this spec.

TODO:

  • Class to sign requests (Request)
  • Parsing and verifying signatures in controllers (SignatureVerification)
  • Replace direct usage of HTTP with new Request class
  • Test positive cases
  • Test cases when request has been tampered with (signature does not match)

Potential uses in the future:

@Gargron Gargron added api REST API, Streaming API, Web Push API requires in depth work in progress Not to be merged, currently being worked on activitypub Protocol-related changes, federation labels Jul 10, 2017
@Gargron Gargron removed the work in progress Not to be merged, currently being worked on label Jul 13, 2017
Copy link
Contributor

@ClearlyClaire ClearlyClaire left a 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}"
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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)]}"
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing about timeouts.

@Gargron
Copy link
Member Author

Gargron commented Jul 14, 2017

@ThibG Addressed date issue and documented draft. Don't think duplicate headers should occur in our use case.

@ClearlyClaire
Copy link
Contributor

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

@Gargron
Copy link
Member Author

Gargron commented Jul 14, 2017

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

@ClearlyClaire
Copy link
Contributor

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

@Gargron
Copy link
Member Author

Gargron commented Jul 14, 2017

why doesn't it cover signing the request data, but only headers?

@ThibG It delegates that to the Digest header from a different spec

@ClearlyClaire
Copy link
Contributor

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

@Gargron
Copy link
Member Author

Gargron commented Jul 14, 2017

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.

@ClearlyClaire
Copy link
Contributor

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

'hub.callback': api_subscription_url(@account.id),
'hub.verify': 'async',
'hub.secret': @account.secret,
'hub.lease_seconds': 30.days.seconds,
Copy link
Member Author

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.

@Gargron
Copy link
Member Author

Gargron commented Jul 14, 2017

@ThibG

it seems to me it would be quite complicated to get it right with RoR, which joins such values with “, ”

From the spec:

If there are multiple instances of the same header field, all header field values associated with the header field MUST be concatenated, separated by a ASCII comma and an ASCII space , , and used in the order in which they will appear in the transmitted HTTP message

So it works out of the box?

@ClearlyClaire
Copy link
Contributor

@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)
@Gargron
Copy link
Member Author

Gargron commented Jul 14, 2017

@ThibG Fixed!

@Gargron Gargron merged commit 1618b68 into master Jul 14, 2017
@Gargron Gargron deleted the feature-http-signatures branch July 14, 2017 18:41
abcang pushed a commit to pixiv/mastodon that referenced this pull request Aug 22, 2017
* 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
activitypub Protocol-related changes, federation api REST API, Streaming API, Web Push API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants