Skip to content

Commit

Permalink
Update macsesh documentation and session types for release.
Browse files Browse the repository at this point in the history
  • Loading branch information
sheagcraig committed May 22, 2020
1 parent e5d7311 commit fbb222b
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 78 deletions.
15 changes: 12 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] 2020-05-22
### Added
- Added client cert auth using keychain identities to `SecureTransportAdapter`.

### Changed
- Renamed `SecureTransportSession` to `Session` to indicate that it's the one you probably want to use.
- Updated documentation accordingly.

## [0.2.1] 2020-04-28
### Added
- Reworked to simplify usage, and add alternate strategies.
Expand All @@ -14,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Initial MacKeychainTransportAdapter source.

[Unreleased]: https://github.com/sheagcraig/MacKeychainTransportAdapter/compare/v0.2.1...HEAD
[0.2.0]: https://github.com/sheagcraig/MacKeychainTransportAdapter/compare/v0.1.0...v0.2.1
[0.1.0]: https://github.com/sheagcraig/MacKeychainTransportAdapter/releases/tag/v0.1.0
[Unreleased]: https://github.com/sheagcraig/MacSesh/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/sheagcraig/MacSesh/compare/v0.2.1...v0.3.0
[0.2.1]: https://github.com/sheagcraig/MacSesh/compare/v0.1.0...v0.2.1
[0.1.0]: https://github.com/sheagcraig/MacSesh/releases/tag/v0.1.0
117 changes: 81 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,69 @@
# MacSesh
This package allows requests to verify certs with the macOS keychain,
rather than using certifi. It also includes some tools for easily
hooking up a `SecureTransport` adapter (a la Pip) and then later
undoing all of the sneaky infiltrations required to set this up.

## Which certs?
It uses any of the trusted certs from keychains included in the current
user's keychain search list, as well as the system roots. Typically,
this is the user's default at `~/Library/Keychains/login.keychain`,
the system keychain at `/Library/Keychains/System.keychain`, and the
System Roots keychain at
`/System/Library/Keychains/SystemRootCertificates.keychain`.

To achieve this, one of three different strategies can be employed:
1. `KeychainSession` uses a custom SSLContext, requests Adapter, and
requests Session, and injects the SSLContext into urllib3. This
approach is the recommendation.
2. `SecureTransportSession` uses the urllib3 contrib module for injecting
SecureTransport equivalents into stock urllib3. While this approach
uses more of the native networking framework, it also seems to be
written primarily with the goal of solving the issues with macOS and
aging OpenSSL versions to ensure that Macs could still use pip.
Therefore, it's not entirely feature-complete in providing a full
requests Adapter. It's definitely worth experimenting with.
3. `SimpleKeychainSession` circumvents the normal flow of session
startup, and tells the SSLContext to load its trust information
early; in this case from certs dumped from the keychain.
This package allows the popular requests library to use the macOS
keychain for both validating a server, and for doing client cert auth.
Its original use-case was for Mac admins wanting to use python requests
and certs provided by an MDM for TLS, Specifically, SCEP certs client
cert auth and x509 payloads for server validation.

## Example Usage:
Validate using a trusted cert from the keychain:

```
>>> import macsesh
>>> sesh = macsesh.KeychainSession()
>>> sesh = macsesh.Session()
>>> response = sesh.get('https://nethack.org')
```
If you want to use the "basic" requests API without creating a session:

```
>>> macsesh.inject_into_requests()
>>> requests.get('https://en.wikipedia.org/wiki/Taco') # Uses keychain
```

Client cert auth:

```
>>> import macsesh
>>> sesh = macsesh.Session()
>>> response = sesh.get('https://nethack.org', cert='My Identity Cert')
```

## Validating a server
macsesh uses any of the _trusted_ certs from keychains included in the
current user's keychain search list, as well as the system roots.
Typically, this search list consists of:
- The user's default keychain at `~/Library/Keychains/login.keychain`
- The system keychain at `/Library/Keychains/System.keychain`
- The System Roots keychain at
`/System/Library/Keychains/SystemRootCertificates.keychain`.

Certs in the system roots are implicitly trusted. Certs from other
keychains must be marked as trusted for this purpose or they won't be
included.

When using macsesh, just leave the requests `verify` at its default of
`True` and macsesh will do the rest.

## Client cert auth
macsesh can also do client cert auth from the keychain, currently only
with the `SecureTransportAdapter`. The other adapter types can do client
cert auth as well of course, but they require the identity to be
available on the filesystem just like regular requests.

To specify a certificate to use, provide the Common Name of the cert to
requests' normal `cert` argument as in the example above. As the keychain
may have multiple certs which match , macsesh performs some additional
filtering.
1. The query does a "subject contains" search with the CN provided, so
for exmaple `cert='taco'` could match a CN of `taco truck` or `taco
party`. macsesh will drop anything that is not an exact match.
2. The resulting certs may have been renewed, and the old certs are still
in the keychain. macsesh sorts them by "not valid after" date and
picks the one with the longest lifespan.

## Advanced

### Cleanup
If for some reason you want to revert to "normal" requests (probably
using certifi), in the same python process, you'll need to remove this
module's injected stuff from urllib3 or requests.
Expand All @@ -53,13 +77,34 @@ Any certs added to the keychains after starting a session will
not be available. Digging down in and updating the SSLContext is rough;
just make a new session if you have this need!

### What about mutual TLS certs?
### Choosing a session type
macsesh provides three different types of requests `Session` classes.
While we probably only need the `Session`, the other two are included for
posterity since they are interesting and potentially useful.

1. If in doubt, use `Session` It uses the securetransport module
contributed to urllib3 as a base SSLContext. Pip, for example, uses
this on macOS. The securetransport module uses an entirely different
`SSLContext`, using ctypes to connect to the macOS `Security`
framework. macsesh then injects additional code into the urllib3
code to use the keychain instead of conforming to the OpenSSL approach
of using paths to files. If you need to do client cert auth from
keychain identities, this is the one you want.
1. `KeychainSession` uses a custom SSLContext, requests Adapter, and
requests Session, and injects the SSLContext into urllib3. The certs
for validation are dumped from the keychain and held in memory. The
goal of this approach is to use the minimum amount of messing about to
achieve cert validation.
3. `SimpleKeychainSession` circumvents the normal flow of session
startup, and tells the SSLContext to load its trust information
up front rather than waiting for a bunch of internal checks to decide
the context needs to load the trust store. It uses the same method
as the `KeychainSession` and functionally shouldn't be any different.

I have MDM-provided SCEP certs or non-exportable certs:
This should be doable with the `SecureTransportSession`. Please let me
know if you have tried it and it works; I don't have a way to test this
at the moment.
### What about cert auth for `SimpleKeyhainSession` and
`KeychainSession`?

Also, I can implement looking up and retrieving certs by name for the
other "strategies", but again, I don't have anything to test against,
so I haven't written it yet.
I can try implementing looking up and retrieving certs by name for the
other "strategies", but I'm not sure how much utility there is for that,
as the keys would have to be exportable. At that point, just export them
and use regular old requests.
62 changes: 27 additions & 35 deletions macsesh/__init__.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,40 @@
"""This package allows requests to verify certs with the macOS keychain
It uses any trusted certs from keychains included in the current
user's keychain search list, as well as the system roots. Typically,
this is the user's default at ~/Library/Keychains/login.keychain,
the system keychain at /Library/Keychains/System.keychain, and the
System Roots keychain at
/System/Library/Keychains/SystemRootCertificates.keychain.
To achieve this, one of three different strategies can be employed:
1. KeychainSession uses a custom SSLContext, requests Adapter, and
requests Session, and injects the SSLContext into urllib3. This
approach is the recommendation.
2. SecureTransportSession uses the urllib3 contrib module for injecting
SecureTransport equivalents into stock urllib3. While this approach
uses more of the native networking framework, it also seems to be
written primarily with the goal of solving the issues with macOS and
aging OpenSSL versions to ensure that Macs could still use pip.
Therefore, it's not entirely feature-complete in providing a full
requests Adapter. It's definitely worth experimenting with.
3. SimpleKeychainSession circumvents the normal flow of session
startup, and tells the SSLContext to load its trust information
early; in this case from certs dumped from the keychain.
Example Usage:
"""MacSesh
This package allows the popular requests library to use the macOS
keychain for both validating a server, and for doing client cert auth.
Its original use-case was for Mac admins wanting to use python requests
and certs provided by an MDM for TLS, Specifically, SCEP certs client
cert auth and x509 payloads for server validation.
## Example Usage:
Validate using a trusted cert from the keychain:
```
>>> import macsesh
>>> sesh = macsesh.KeychainSession()
>>> sesh = macsesh.Session()
>>> response = sesh.get('https://nethack.org')
```
Note: if you want to revert to "normal" requests (probably using
certifi), in the same python process, you'll need to remove this
module's injected stuff from urllib3:
If you want to use the "basic" requests API without creating a session:
```
>>> macsesh.extract_from_urllib3()
>>> macsesh.inject_into_requests()
>>> requests.get('https://en.wikipedia.org/wiki/Taco') # Uses keychain
```
Finally, any certs added to the keychains after starting a session will
not be available. The sessions and adapters all have an update_truststore
method for re-dumping the trust.
Client cert auth:
```
>>> import macsesh
>>> sesh = macsesh.Session()
>>> response = sesh.get('https://nethack.org', cert='My Identity Cert')
```
"""
from .keychain import get_trusted_certs, get_truststore_data, get_system_roots


from .injected_adapter import KeychainAdapter, KeychainContext
from .keychain import get_trusted_certs, get_truststore_data, get_system_roots
from .secure_transport_adapter import SecureTransportAdapter
from .session import KeychainSession, SecureTransportSession, SimpleKeychainSession
from .session import KeychainSession, Session, SimpleKeychainSession
from .simple_adapter import SimpleKeychainAdapter
from .util import extract_from_urllib3, inject_into_requests, extract_from_requests
from .version import __version__
2 changes: 1 addition & 1 deletion macsesh/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class KeychainSession(BaseKeychainSession):
_adapter_class = KeychainAdapter


class SecureTransportSession(BaseKeychainSession):
class Session(BaseKeychainSession):
"""Requests session using the SecureTransport"""

_adapter_class = SecureTransportAdapter
Expand Down
4 changes: 2 additions & 2 deletions macsesh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def inject_into_requests(session_class=None):
# Handle the default class here by importing late to avoid circular
# import issues.
if not session_class:
from .session import KeychainSession
session_class = KeychainSession
from .session import Session
session_class = Session
# Create a func that replicates requests.api.request, just subbing
# in the passed session class as a kind of closure.
def request(method, url, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion macsesh/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.2.1'
__version__ = '0.3.0'

0 comments on commit fbb222b

Please sign in to comment.