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

Improve handling of native dependencies #200

Merged
merged 8 commits into from
Jan 23, 2020

Conversation

joshuagl
Copy link
Collaborator

@joshuagl joshuagl commented Jan 6, 2020

Please fill in the fields below to submit a pull request. The more information
that is provided, the better.

Fixes issue: #179 (partial fix, securesystemslib.gpg still needs to be addressed)

Description of the changes being introduced by the pull request:

  • Makes colorama optional, add a warning to the log when it's not available but otherwise ssl functions as normal
  • rename pyca_crypto_keys -> rsa_keys
  • By default fallback to pure Python ed25519 certificate verification when the nacl module isn't available, rather than making the use of nacl an option at function call time.
  • Ensures the public facing modules interface and keys are importable even when the native dependencies are not available
  • Ensure that public functions using features of the native dependencies provide an actionable error message when they can't be used (due to the absence of native dependencies)

Please verify and check that the pull request fulfils the following
requirements
:

  • The code follows the Code Style Guidelines
  • Tests have been added for the bug fix or new feature
  • Docs have been added for the bug fix or new feature

@joshuagl
Copy link
Collaborator Author

joshuagl commented Jan 6, 2020

I need to figure out how to cleanly test interface and keys without native dependencies installed.

Does it make sense to define a different test suite with a test for the public interfaces that verifies that all public functions either return successfully or return the expected UnsupportedLibraryError, we could then have a separate Tox environment without the native dependencies installed that runs this suite.

Is there a cleaner/simpler way to do it?

@joshuagl joshuagl mentioned this pull request Jan 7, 2020
3 tasks
@joshuagl
Copy link
Collaborator Author

joshuagl commented Jan 7, 2020

I've got a partial implementation of a mechanism to test the public interfaces in 0ca75ef, does that look reasonable to you @lukpueh? If so I'll finish off that patch (it's currently incomplete and has some hard-coded paths).

@lukpueh
Copy link
Member

lukpueh commented Jan 7, 2020

Thanks, @joshuagl! This looks very reasonable to me.

If colorama isn't present warn (via the logger) that terminal output
won't be coloured and continue without.

Signed-off-by: Joshua Lock <[email protected]>
@joshuagl joshuagl force-pushed the joshuagl/issue179 branch 2 times, most recently from eeabc4a to b3c9a42 Compare January 8, 2020 14:54
@lukpueh
Copy link
Member

lukpueh commented Jan 8, 2020

So I just looked closer at your changes and they still seem very sensible. Thanks a lot, @joshuagl!

I realized that the name of pyca_crypto_keys.py is quite misleading. It suggests that it is securesystemslib's interface to pyca/cryptography, although other modules (ecdsa_keys.py, hash.py) also interface that library. Maybe it would be clearer and more accurate to call it rsa_keys.py, this would also make it consistent with the other low-level modules in securesystemslib, which both encapsulate key algorithms and not crypto libraries.

Another thing that I noticed is that pynacl import error handling is repeated on two levels of abstractions, i.e. in keys.py and in ed25519_keys.py. Do you think we could do it only once? Maybe we can draw the line that I described in #179 (comment) one level of abstraction further down. That is e.g.:

# in ed25519_keys.py

NACL = True
try:
  import nacl

except ImportError:
  NACL = False

# ed25519_keys.sign() will raise an error if nacl does not exist
def sign():
  if not NACL:
    raise UnsupportedLibraryError()
  else: 
    nacl.ed25519_sign()

# ed25519_keys.verify() has a pure-python fallback. 
def verify():
  if not NACL:
    pure_python_ed25519_verify()
  else: 
    nacl.ed25519_sign()

This would make keys.py completely oblivious to the whole import story. And we could do the same thing for ecdsa_keys.py and rsa_keys.py (sic!), albeit without the pure-python fallback.

In your approach, OTOH, I see the advantage of failing early, i.e. raising an UnsupportedLibraryError before doing unnecessary work in any of the functions in keys.py.

I hope you don't mind my brainstorming here. I really appreciate your efforts and am curious about your thoughts. (cc @SantiagoTorres).

@lukpueh
Copy link
Member

lukpueh commented Jan 8, 2020

Btw. since securesystemslib.gpg requires gpg and pyca/cryptography I also mentioned it in #179. But I suggest that we deal with it in a separate PR.

@joshuagl
Copy link
Collaborator Author

joshuagl commented Jan 9, 2020

Thanks for the constructive feedback @lukpueh

I realized that the name of pyca_crypto_keys.py is quite misleading. It suggests that it is securesystemslib's interface to pyca/cryptography, although other modules (ecdsa_keys.py, hash.py) also interface that library.

Would you like to see that rename here, or in a separate PR?

Another thing that I noticed is that pynacl import error handling is repeated on two levels of abstractions, i.e. in keys.py and in ed25519_keys.py. Do you think we could do it only once? Maybe we can draw the line that I described in #179 (comment) one level of abstraction further down. That is e.g.:

# in ed25519_keys.py

NACL = True
try:
  import nacl

except ImportError:
  NACL = False

# ed25519_keys.sign() will raise an error if nacl does not exist
def sign():
  if not NACL:
    raise UnsupportedLibraryError()
  else: 
    nacl.ed25519_sign()

# ed25519_keys.verify() has a pure-python fallback. 
def verify():
  if not NACL:
    pure_python_ed25519_verify()
  else: 
    nacl.ed25519_sign()

This would make keys.py completely oblivious to the whole import story. And we could do the same thing for ecdsa_keys.py and rsa_keys.py (sic!), albeit without the pure-python fallback.

In your approach, OTOH, I see the advantage of failing early, i.e. raising an UnsupportedLibraryError before doing unnecessary work in any of the functions in keys.py.

I was torn on this. Doing the import handling a level lower has the benefits of:

  • not handling the import at two levels
  • keeping the code in the public interfaces simpler and moving the complexity (or at least ugly code) to the lower level modules

One downside to the approach is that the unit tests become more complex, as in many places we'll need to provide valid (enough) inputs to the higher-level functions so that the "unnecessary work" can be completed before the UnsupportedLibraryError is thrown by the lower level wrappers.

I hope you don't mind my brainstorming here. I really appreciate your efforts and am curious about your thoughts. (cc @SantiagoTorres).

I don't mind at all. I very much appreciate the constructive discussion around these changes.
I think the more effort/cleaner high-level interfaces approach is the better one.

Assuming you agree, I'll revise this PR to:

  • move the pynacl import error handling only to the lower-level modules
  • rename pyca_crypto_keys.py -> rsa_keys.py

I'll also take a look at adding similar logic to securesystemslib.gpg, depending on how much work it looks to be I may handle it here or in a separate PR.

Does that seem like a reasonable approach?

This better reflects the modules intent, it encapsulates RSA key
algorithms not uses of pyca/cryptography.

Signed-off-by: Joshua Lock <[email protected]>
Instead of having the caller of ed25519_keys.verify_signature() choose
whether to use PyNaCl or not, default to always using nacl when it is
available and otherwise fallback to the vendored copy of the pure
Python ed25519 implementation.

Signed-off-by: Joshua Lock <[email protected]>
@joshuagl
Copy link
Collaborator Author

I didn't get around to addressing securesystemslib.gpg yet, but I think the rest of this PR is in reasonable shape for review.

@joshuagl joshuagl marked this pull request as ready for review January 10, 2020 12:42
Copy link
Member

@lukpueh lukpueh left a comment

Choose a reason for hiding this comment

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

Excellent work, @joshuagl. Thanks a lot for taking a stab at this task. I have left a few very minor comments. Let me know when you want me to take another look.

.travis.yml Outdated Show resolved Hide resolved
CHANGELOG.md Show resolved Hide resolved
securesystemslib/ecdsa_keys.py Outdated Show resolved Hide resolved
@@ -121,6 +125,10 @@ def generate_public_and_private(scheme='ecdsa-sha2-nistp256'):
'securesystemslib.formats.PEMECDSA_SCHEMA', respectively.
"""

if not CRYPTO: # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

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

It would be cool to configure coverage to exclude these kinds of branches from py{27, 35, 36, 37, 38} only, and do the inverse for purepy{27, 36}. Do you want to look into it? We can also ticketize it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is technically easily handled thanks to the versatility of coverage[1], but I held off on doing it because I wasn't sure how valuable coverage results would be for the purepy environments - at least in the current setup with only check_public_interfaces.py run in the purepy Tox environments we won't cover much of the code at all.

Copy link
Member

Choose a reason for hiding this comment

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

I was thinking of a setup with mutual exclusive coverage exemption. Something like a # pragma: purepy, to mark lines/branches that are excluded from py{27, 35, 36, 37, 38}, but are also the only lines/branches considered for purepy{27, 38}, so that all lines must be tested by either one or the other. But let's do this in a separate PR (I'll add a note on #179).

securesystemslib/keys.py Outdated Show resolved Hide resolved
tests/check_public_interfaces.py Show resolved Hide resolved
tests/check_public_interfaces.py Outdated Show resolved Hide resolved
tests/test_ed25519_keys.py Show resolved Hide resolved
securesystemslib/ed25519_keys.py Show resolved Hide resolved
securesystemslib/ed25519_keys.py Show resolved Hide resolved
Raise UnsupportedLibraryError from each function in the low-level modules
which encapsulate key algorithms (ecdsa_keys, ed25519_keys and rsa_keys)
when the libraries needed by that function aren't available.

Signed-off-by: Joshua Lock <[email protected]>
It should be possible to use securesystemslib in a pure Python mode, i.e.
without any native dependencies installed, in which case any functions in
the public interface should provide meaningful error messages when they
aren't usable due to absent dependencies.

Signed-off-by: Joshua Lock <[email protected]>
Ensure the pure Python fallback for ed25519 signature validation is tested
by adding it to the check_public_interfaces test module which is run in
an environment without any of the cryptographic library dependencies
installed.

Signed-off-by: Joshua Lock <[email protected]>
Now that the low-level modules handling interaction with the cryptographic
libraries handle missing imports and throw UnsupportedLibraryException
at the function level it's no longer required to wrap their import in a
try/except block.

Signed-off-by: Joshua Lock <[email protected]>
@joshuagl
Copy link
Collaborator Author

Thanks for the review @lukpueh. I think I've addressed all of your review comments except configuring coverage differently in the purepy case. I'd appreciate your thoughts on that one, I commented in-line.

Copy link
Member

@lukpueh lukpueh left a comment

Choose a reason for hiding this comment

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

Thanks for addressing all the comments! LGTM.

@lukpueh lukpueh merged commit bbcbeca into secure-systems-lab:master Jan 23, 2020
@joshuagl joshuagl deleted the joshuagl/issue179 branch January 28, 2020 10:02
@lukpueh lukpueh mentioned this pull request Jan 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants