-
Notifications
You must be signed in to change notification settings - Fork 276
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
Update TUF to handle HTTPS proxies #781
Update TUF to handle HTTPS proxies #781
Conversation
1e86d74
to
4863868
Compare
Cc @JustinCappos @SantiagoTorres @awwad Need your eyes to carefully review, edit, and merge! 👀 |
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.
Jotted some quick thoughts down. Someone with more recent eyeballs in the code will likely find better things to comment on.
pylint | ||
requests |
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.
Can we just change the requests line in this commit unless the rest are relevant? I don't mind you doing the alpha-reorder, but please let's split the commits so we can see why things are done.vv(Same for requirements.in too please)
I'm happy to make the edits, but want to double check that these files aren't order sensitive...
tuf/exceptions.py
Outdated
@@ -173,7 +173,7 @@ def __init__(self, expected_length, observed_length): | |||
|
|||
def __str__(self): | |||
return 'Observed length (' + repr(self.observed_length)+\ | |||
') <= expected length (' + repr(self.expected_length) + ').' | |||
') != expected length (' + repr(self.expected_length) + ').' |
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 think we had this before because some adopters wanted to effectively turn this off and list max length allowed instead of the expected length for files. Why is this being changed? Is this change needed now?
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 reason why I changed itfrom
<=to
!= is because if there is a bug in the way the download function works, nothing actually prevents it from downloading more than it should. This happened to me, and I found the error message very misleading (e.g., 9 <= 5
).
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.
Behavior here has not changed. It should not be possible to get a length greater than expected due to the way the download code was written (and now as well, I think). If the code is changed and it becomes possible to end up with more, we can change the error message, but '<' currently communicates an expectation that has some value.
# A PEM (RFC 1422) file where you may find SSL certificate authorities | ||
# https://en.wikipedia.org/wiki/Certificate_authority | ||
# http://docs.python.org/2/library/ssl.html#certificates | ||
ssl_certificates = None |
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.
Does this mean that there are no trusted certs by default? What does None do in this context?
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 used to be "use the default bundle that urllib resolves", instead we are going to use the environment variable that requests uses: REQUESTS_CA_BUNDLE
that's 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.
Correct. I figured we don't need two ways to configure the same thing right now.
@@ -89,16 +84,6 @@ | |||
# avoid being considered as a slow retrieval attack. | |||
MIN_AVERAGE_DOWNLOAD_SPEED = 50 #bytes/second | |||
|
|||
# The time (in seconds) we ignore a server with a slow initial retrieval speed. | |||
SLOW_START_GRACE_PERIOD = 0.1 #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.
Seems like a very tight bound. Should this be higher?
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.
It is gone now. I think requests has a built-in start grace period.
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.
Correct, requests
has a connect()
timeout.
tuf/settings.py
Outdated
@@ -80,7 +75,7 @@ | |||
DEFAULT_TARGETS_REQUIRED_LENGTH = 5000000 #bytes | |||
|
|||
# Set a timeout value in seconds (float) for non-blocking socket operations. | |||
SOCKET_TIMEOUT = 4 #seconds | |||
SOCKET_TIMEOUT = 5 #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.
Why did this change?
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.
Because network connections can be quite lossy in the real world. We found a customer who was timing out over HTTP, but that was more likely due to the stringest SLOW_START_GRACE_PERIOD
of 0.1 seconds. This latter setting is gone now, because requests
handles both connect()
and read()
timeouts.
tuf/download.py
Outdated
# mirror, and it would pass the Session to be used in this module, but that | ||
# requires a much more invasive change. | ||
# | ||
_session = requests.Session() |
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.
Key to fix before merging. Using a separate session for each hostname part of the URL should be easy. Perhaps make this a dict?
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.
Each hostname might be a bit overkill --- I was thinking a separate session per repository mirror, but better to be safe than sorry. Let me work on this.
@@ -172,20 +163,6 @@ def unsafe_download(url, required_length): | |||
securesystemslib.formats.URL_SCHEMA.check_match(url) | |||
securesystemslib.formats.LENGTH_SCHEMA.check_match(required_length) | |||
|
|||
# Ensure 'url' specifies one of the URI schemes in |
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.
We probably should retain a comment explaining that this is where the http / https prefix from the settings.py file is checked (assuming that is the case) and that an ???Exception may be thrown as a result...
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 removed the check for whether a given URL matches http
or https
. Originally, we did this to avoid processing file:///
requests, but requests
doesn't open files anyway, so we should be okay. I'm more in favour of simplicity. I can't imagine requests
opening more dangerous protocols than http
or https
, but please correct me if wrong.
tuf/download.py
Outdated
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding#Directives | ||
'Accept-Encoding': 'identity', | ||
# The TUF user agent. | ||
# TODO: Attach version number, which cannot be easily found right now. |
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.
What does this mean? Do you mean that TUF's communication protocol should have a version of its own so we might use different versions at different times? Do we want the version of TUF to be here instead?
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 think it's just overwriting the User-Agent to something that's not requests
or python urrlibX
. It'd be cleaner (and easier to analyze traffic) if we used something like TUF vX
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.
Correct. Sometimes you want to exactly which piece of software was downloading from your server. To be fair, it should be something like tuf-{tuf-version-number}-over-requests-{requests-version-number}
.
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.
There are some very minor details. Otherwise it LGTM once the failing tests are addressed.
tests/test_slow_retrieval_attack.py
Outdated
extra_bytes = 8 | ||
total_bytes = tuf.settings.SLOW_START_GRACE_PERIOD + extra_bytes | ||
total_bytes = 100 * extra_bytes |
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'd be happier if re-used that variable or assigned a constant for this 100
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.
@awwad, could you help me fix this? I think this might be related to the problems you've been seeing with bad hashes, and so on. Thanks in advance!
tuf/download.py
Outdated
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding#Directives | ||
'Accept-Encoding': 'identity', | ||
# The TUF user agent. | ||
# TODO: Attach version number, which cannot be easily found right now. |
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 think it's just overwriting the User-Agent to something that's not requests
or python urrlibX
. It'd be cleaner (and easier to analyze traffic) if we used something like TUF vX
# A PEM (RFC 1422) file where you may find SSL certificate authorities | ||
# https://en.wikipedia.org/wiki/Certificate_authority | ||
# http://docs.python.org/2/library/ssl.html#certificates | ||
ssl_certificates = None |
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 used to be "use the default bundle that urllib resolves", instead we are going to use the environment variable that requests uses: REQUESTS_CA_BUNDLE
that's here
@@ -89,16 +84,6 @@ | |||
# avoid being considered as a slow retrieval attack. | |||
MIN_AVERAGE_DOWNLOAD_SPEED = 50 #bytes/second | |||
|
|||
# The time (in seconds) we ignore a server with a slow initial retrieval speed. | |||
SLOW_START_GRACE_PERIOD = 0.1 #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.
It is gone now. I think requests has a built-in start grace period.
connection.close() | ||
# This else block returns and skips closing the response in the finally | ||
# block, so close the response here. | ||
response.close() |
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.
Not related to this PR, but this finally
clause with the response.close is then useless. I don't think finally
works like this 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.
Resolved now (with the conflict). The strange try/except/finally was removed in #771.
tuf/download.py
Outdated
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding#Directives | ||
'Accept-Encoding': 'identity', | ||
# The TUF user agent. | ||
# TODO: Attach version number, which cannot be easily found right now. |
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.
We definitely need something defined here, and add it to part of our release process...
tests/test_download.py
Outdated
@@ -168,44 +170,29 @@ def test_download_url_to_tempfileobj_and_urls(self): | |||
self.assertRaises(securesystemslib.exceptions.FormatError, | |||
download_file, None, self.target_data_length) | |||
|
|||
self.assertRaises(securesystemslib.exceptions.FormatError, | |||
self.assertRaises(requests.exceptions.MissingSchema, |
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.
Again, not related to this PR, but moving to assertRaises
as a context processor would make this code more readable...
Signed-off-by: Trishank K Kuppusamy <trishank.kuppusamy@datadoghq.com>
4863868
to
b9bc860
Compare
Rebased. Edit: most of the failures I was seeing had to do with the virtual box I was running it in. 😆 Saw 24 failures and balked. My bad. |
(sorry -- just to keep things simple) Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
I'll finish looking through it to make sure I haven't missed anything subtle and then fix the tests. |
@awwad AFAICT with |
063c2c2
to
c3a01bd
Compare
c3a01bd
to
b059fc3
Compare
I have a PR for the sessions issue, but I can't push it now for some network reason, and I gotta run now. See you guys Monday! |
setup.py
Outdated
setup( | ||
name = 'tuf', | ||
version = '0.11.1', | ||
version = find_version("tuf", "__init__.py"), |
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 isn't going to work: read isn't defined. This is code from here and is a little more complicated than is ideal. It'll also match comment lines if they exist. Single-sourcing version number isn't necessary for this pull request, but if I was going to do it, I'd probably add a VERSION file and have tuf/__init__.py
and setup.py
each read that in. There could be problems with that, too. I'm going to punt on this and keep the version in two places and we can fix that less urgently. (Also, the user agent reporting a version seems less critical in any case than the rest of the PR.)
SlowRetrievalError is essentially part of the API (see ATTACKS.md), so I'll adjust this a bit to still raise that when we catch urllib3.exceptions.ReadTimeoutError(). |
I am still having some strange network issues in my VM trying to push to / pull from GitHub. Let me send my updated PR as soon as I figure out what's going on. |
Keep an eye out for any history differences? It wouldn't look like a network issue, but I did have to strip one of your commits (self-merge -- merging remote branch into local branch and then pushing) from the commit history. Meanwhile, I'm looking at why bad hash errors are occurring in one of the tests where I don't expect them to occur. (Slow retrieval should cause an error that indicates that retrieval was slow/disrupted, not a bad hash.) The download code is a little kinky and probably could use some tidying up (in another PR, later, tbc). |
Okay, guys, I've added code to use a different session per hostname. Let me know what you think, and I'm happy to help with anything else! P.S. There is not so much time pressure on this anymore, but the sooner we can fix this, the better :) |
So, unlike the code without this PR merged, this branch downloads a target file all in the code that claims to make a connection. This makes the slow retrieval, endless data, etc. code moot (and also invalidates all the docstrings -- e.g. Using stream=True in the (UPDATE: Hang on, I may have misunderstood something.....) |
08ac96e
to
7df8e46
Compare
We already use |
Fixed some bad assumptions in the tests that resulted -- when the test failed -- in misleading errors (and clarified behavior with comments in the tests). Also raised ReadTimeoutError (from urllib) as an expected tuf.exceptions.SlowRetrievalError. The test fails, however, because a slow retrieval attack is not prevented: streaming one byte per second for 400 (or, previously, 800) seconds is permitted to complete. I'll tinker and figure out how to prevent slow retrieval attacks with the requests calls. |
I wonder what's going on. Have you looked at the doc, Sebastien? |
Yes. The current settings include a chunk size of 40k and a 5s timeout that is now interpreted by requests as a maximum gap between bytes (and also the timeout of the connect interval). This means that a transfer of 40k could take 50+ hours and still pass. We could tinker with those settings
|
I think I sort of understand what's going on. Would you mind terribly much showing what you mean in the updated unit tests? Thanks in advance! |
Not clear on the question, so in case you mean explaining the stack as it is tested, then:
If there had been more than one chunk (if target file size > 40k), then Change Required:The change that's called for (I'll make one or the other in a bit assuming I'm not off my rocker) is to either reduce chunk size and continue to use request.raw.read() or, instead of using request.raw.read(), call request.iter_content() with a much smaller chunk size (I think I prefer this option? Is there a reason raw.read() is preferable?), say 128b, resulting in the largest possible delay that can be caused by an attacker delivering at speeds less than that acceptable ( TBC, ideally we could call request.iter_content(40k, max_duration=X seconds) or something, but I don't see that option anywhere after looking through some feature requests. |
I feel like Vlad toyed with requests and elected not to use it? I don't really recall at this point -- maybe we used to use it and moved away.... This (no total timeout option for an individual read) would be a decent reason for that. Maybe there's a way to do this via urllib3? (don't see that yet, but haven't really looked properly). UPDATE: don't see it in urllib3 after moderate search |
The problem with |
I strongly recommend using |
Having said all this, the way we used to handle slow retrieval errors appears to be broken using |
tests/test_proxy_use.py
Outdated
|
||
|
||
|
||
def test_httpS_dl_via_smart_http_proxy(self): |
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 think I get the idea of capitalization ~ highlighting, but this looks terrible to me.)
tests/test_proxy_use.py
Outdated
# recently overwrote it with. | ||
pass | ||
|
||
elif key in os.environ: |
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.
Perhaps remove the elif
/else
and do self.old_env_values[key] = os.environ.get(key, None)
.
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.
(BTW, what about os.execve
ing things and supplying a modified environment directly, instead of saving/restoring the current one?)
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.
Your suggestion reduced 6 lines to 2. Thanks!
As for execve: seems like a messy option. I'm not sure I'd want to fully replace the environment variables (instead of tweaking them) and I'm not sure I want to close the existing process and transition to a new one (temp files?). Popen execve? Not sure. I think this is probably easier and more acceptable.
tests/test_proxy_use.py
Outdated
# del os.environ[key] should unset the variable. Otherwise, we'll just | ||
# have to settle for setting it to an empty string. | ||
# See os.environ in: | ||
# https://docs.python.org/2/library/os.html#process-parameters) |
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.
Remove trailing )
tests/test_proxy_use.py
Outdated
# This is only relevant if the proxy is in intercept mode. | ||
cls.https_proxy_port = cls.http_port + 3 | ||
cls.https_proxy_proc = subprocess.Popen( | ||
['python', 'proxy_server.py', str(cls.https_proxy_port), 'intercept', |
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.
Use sys.executable
instead of python
. (It could be python3
, python2.7
etc. depending on your setup.)
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.
And/or consider Popen
ing a Python interpreter a common function and spin it off into a separate function. (The code seems to do this a lot, across files in the PR).
- two reversions to unnecessary changes - some typo fixes - capitalization of HTTP/S where reasonable - commenting out code section with ''' rather than # Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
that draws from sys.executable (the currently running Python interpreter) instead of assuming 'python' is correct. Use this function instead of having many individual subprocess calls written out. Slightly simplifies code, too. This should eventually be moved to a common test module instead of appearing in two places in the test code. Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
Hi Sebastien, Sorry, don't mean to press you, but could we please get a dev release at least ASAP? We have the Agent 6.6 release frozen at the end of next week, and I'd like to test this ASAP, so that we can update TUF in the Agent next week if all goes well. Thanks very much in advance! Cc: @nmuesch @olivielpeau |
No problem. I'll merge sometime today (and release). |
@nmuesch Can we get your customer to install the latest stable Agent, and then update TUF manually to see if that fixes the problem? |
to make sure that the test uses the intended certificate. (There's some indirect indication that the updated environment variable might not always have been used.) Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
dbc5338
to
ab8990b
Compare
After seeing some AppVeyor failures, I've increased the wait after starting test HTTP, HTTPS, and proxy servers from 0.5s to 1s, to make it less likely that tests will fail because the servers weren't done starting up yet. After some review comments by @aaaaalbert, I've tightened the logic in aggregate_tests.py around which tests to skip unless a certain Python version is running, and added some consistency checks. This also involved a bit of clarification of comments and variable names. Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
ab8990b
to
01d8d9e
Compare
Merged and an alpha released produced: https://github.com/theupdateframework/tuf/releases/tag/v0.11.2-alpha Oh, I should mention that I wasn't expecting to put this on PyPI until the vulnerability is resolved. I hope that's OK. I could be convinced otherwise, so if this is an issue, LMK. For now, it's just on GitHub. |
@awwad Could we please get a |
https://pypi.org/project/tuf/0.11.2.dev1/ |
Thanks @awwad ! |
Since theupdateframework#781 we only provide limited protection against slow retrieval attacks. So far this has only been discussed in above issue and hinted at by a disabled test and a code comment in that test. This change adds a corresponding disclaimer to a more prominent place, i.e. the list of attacks in SECURITY.md.
Since theupdateframework#781 we only provide limited protection against slow retrieval attacks. So far this has only been discussed in above issue and hinted at by a disabled test and a code comment in that test. This change adds a corresponding disclaimer to a more prominent place, i.e. the list of attacks in SECURITY.md. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Since theupdateframework#781 we only provide limited protection against slow retrieval attacks. So far this has only been discussed in above issue and hinted at by a disabled test and a code comment in that test. This change adds a corresponding disclaimer to a more prominent place, i.e. the list of attacks in SECURITY.md. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu> Co-Authored-By: Trishank K Kuppusamy <33133073+trishankatdatadog@users.noreply.github.com>
Fixes issue #:
Unfortunately, TUF fails to work via HTTPS proxies, whereas the
requests
library does.The problem is this well-intentioned but outdated code.
As the comment hints, I had borrowed that VerifiedHTTPSConnection code from pip back then, because I knew that python2 back then was not verifying hostnames on SSL certs. Unfortunately, this code is now severely outdated.
Description of the changes being introduced by the pull request:
The tuf.download module now uses the
requests
library. I have manually tested that it handles HTTPS proxies.Changes:
requests
and its dependencies by default.requests
instead of custom, outdated networking code.REQUESTS_CA_BUNDLE
environment variable torequests
.tuf.settings.SLOW_START_GRACE_PERIOD
, as this no longer seems terribly useful, becauserequests
allow for setting connect and / or read timeouts.http
andhttps
only). This was originally done to prevent potentially unsafe requests to files on disk, butrequests
does not handle opening local files anyway.TODO:
requests.Session
per repository mirror to avoid sharing state, and accidentally falling for unknown / unforeseen security issues.REQUESTS_CA_BUNDLE
and / orHTTPS_PROXY
variables in order to handle HTTPS proxies, and (2) ensuring that different sessions are used per hostnamePlease verify and check that the pull request fulfills the following
requirements: