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

pyatls: use urllib3 v2 #12

Merged
merged 11 commits into from
Sep 1, 2023
10 changes: 7 additions & 3 deletions python-package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ conn = HTTPAConnection("my.confidential.service.net", ctx)
conn.request("GET", "/index")

response = conn.getresponse()
code = response.getcode()

print(f"Status: {code}")
print(f"Response: {response.read().decode()}")
print(f"Status: {response.status}")
print(f"Response: {response.data.decode()}")

conn.close()
```
Expand All @@ -110,6 +109,11 @@ print(f"Status: {response.status_code}")
print(f"Response: {response.text}")
```

**Note**: The `requests` library is not marked as a dependency of this package
because it is not required for its operation. As such, if you wish to use
`requests`, install it via `pip install requests` prior to importing
`HTTPAAdapter`.

## Further Reading

If you are unfamiliar with the terms used in this README and would like to learn
Expand Down
4 changes: 2 additions & 2 deletions python-package/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pyatls"
version = "0.0.2"
version = "0.0.3"
description = "A Python package that implements Attested TLS (aTLS)."
readme = "README.md"
authors = [{ name = "Opaque Systems", email = "[email protected]" }]
Expand All @@ -24,7 +24,7 @@ dependencies = [
"cryptography",
"PyJWT",
"pyOpenSSL",
"urllib3 >= 1.26.16, < 2.1.0",
"urllib3 >= 2.0.0, < 2.1.0",
]

[project.urls]
Expand Down
3 changes: 2 additions & 1 deletion python-package/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
cryptography==41.0.3
PyJWT==2.8.0
pyOpenSSL==23.2.0
urllib3==1.26.16
requests==2.31.0
Copy link

Choose a reason for hiding this comment

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

The requests library is not marked as a dependency of this package

do we need it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose it depends on what we use this file for. If it's for development dependencies, then yes; if it's for general dependencies, then I'd say no.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I should rename it to requirements-dev.txt.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is fine to have it here.

urllib3==2.0.4
146 changes: 117 additions & 29 deletions python-package/sample/connect_aci.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@

import argparse
import ast
import warnings
from typing import List, Mapping, Optional

import requests
import urllib3
from atls import ATLSContext, HTTPAConnection
from atls.utils.requests import HTTPAAdapter
from atls.utils.urllib3 import extract_from_urllib3, inject_into_urllib3
from atls.validators import Validator
from atls.validators.azure.aas import AciValidator
from cryptography.x509.oid import ObjectIdentifier

# Parse arguments
parser = argparse.ArgumentParser()
Expand Down Expand Up @@ -81,15 +85,44 @@
"(default: none)",
)

parser.add_argument(
"--loops",
default=1,
help="number of times to perform the request to evaluate the impact of "
"connection pooling (default: 1)",
)

parser.add_argument(
"--use-injection",
action="store_true",
help="inject aTLS support under the urllib3 library to automatically "
"upgade all HTTPS connections into HTTP/aTLS (default: false)",
)

parser.add_argument(
"--use-requests",
action="store_true",
help="use the requests library with the HTTPS/aTLS adapater (default: "
"false)",
)

parser.add_argument(
"--insecure",
action="store_true",
help="disable attestation (testing only) (default false)",
)

args = parser.parse_args()

if args.insecure and (args.policy or args.jku):
raise Exception(
"Cannot specify --policy and/or --jku alongside --insecure"
)

loops: int = int(args.loops)
if loops == 0 or loops < 0:
raise ValueError(f"Invalid loop count: {loops}")

policy_files: Optional[List[str]] = args.policy
jkus: Optional[List[str]] = args.jku

Expand All @@ -101,14 +134,43 @@
with open(filepath) as f:
policies.append(f.read())

# Set up the Azure AAS ACI validator:

class NullValidator(Validator):
"""
A validator that accepts any evidence, effectively bypassing attestation.

This can be useful to evaluate the overhead of the attestation process. For
example, when using AAS, the endpoint may be too far away from where this
script is running and therefore incur significant latency. To test that
hypothesis, it may be valuable to momentarily disable attestation for the
sake of debugging.

Do not use in production.
"""

@staticmethod
def accepts(_oid: ObjectIdentifier) -> bool:
return True

def validate(
self, _document: bytes, _public_key: bytes, _nonce: bytes
) -> bool:
warnings.warn("Skipping attestation...")
return True


# Set up the Azure AAS ACI validator, unless it has been explicitly disabled:
# - The policies array carries all allowed CCE policies, or none if the policy
# should be ignored.
#
# - The JKUs array carries all allowed JWKS URLs, or none if the JKU claim in
# the AAS JWT token sent by the server during the aTLS handshake should not
# be checked.
validator = AciValidator(policies=policies, jkus=jkus)
validator: Validator
if args.insecure:
validator = NullValidator()
else:
validator = AciValidator(policies=policies, jkus=jkus)

# Parse provided headers, if any.
headers: Mapping[str, str] = {}
Expand All @@ -127,50 +189,76 @@ def use_direct() -> None:
# validator (only one need succeed).
ctx = ATLSContext([validator])

# Set up the HTTP request machinery using the aTLS context.
conn = HTTPAConnection(args.server, ctx, args.port)
# Purposefully create a new connection per loop to incur the cost of
# attestation to highlight the added value of connection pooling as
# provided by the urllib3 and requests libraries.
for _ in range(loops):
# Set up the HTTP request machinery using the aTLS context.
conn = HTTPAConnection(args.server, ctx, args.port)

# Send the HTTP request, and read and print the response in the usual way.
conn.request(
args.method,
args.url,
body,
headers,
)
# Send the HTTP request, and read and print the response in the usual
# way.
conn.request(args.method, args.url, body, headers)

response = conn.getresponse()
code = response.getcode()
response = conn.getresponse()

print(f"Status: {code}")
print(f"Response: {response.read().decode()}")
print(f"Status: {response.status}")
print(f"Response: {response.data.decode()}")

conn.close()
conn.close()


def use_injection() -> None:
# Replace urllib3's default HTTPSConnection class with HTTPAConnection.
inject_into_urllib3([validator])

for _ in range(loops):
# The rest of urllib3's usage is as usual.
response = urllib3.request(
"POST",
f"https://{args.server}:{args.port}{args.url}",
body=body,
headers=headers,
)

print(f"Status: {response.status}")
print(f"Response: {response.data.decode()}")

# Restore the default HTTPSConnection class.
extract_from_urllib3()


def use_requests() -> None:
# Note that this is an optional dependency of PyAtls since it is not
# strictly required for its operation.
import requests

session = requests.Session()

# Mount the HTTP/aTLS adapter such that any URL whose scheme is httpa://
# results in an HTTPAConnection object that in turn establishes an aTLS
# connection with the server.
session.mount("httpa://", HTTPAAdapter([validator]))

# The rest of the usage of the requests library is as usual. Do remember to
# use session.request from the session object that has the mounted adapter,
# not requests.request, since that's the global request function and has
# therefore no knowledge of the adapter.
response = session.request(
args.method,
f"httpa://{args.server}:{args.port}{args.url}",
data=body,
headers=headers,
)
for _ in range(loops):
# The rest of the usage of the requests library is as usual. Do
# remember to use session.request from the session object that has the
# mounted adapter, not requests.request, since that's the global
# request function and has therefore no knowledge of the adapter.
response = session.request(
args.method,
f"httpa://{args.server}:{args.port}{args.url}",
data=body,
headers=headers,
)

print(f"Status: {response.status_code}")
print(f"Response: {response.text}")
print(f"Status: {response.status_code}")
print(f"Response: {response.text}")


if args.use_requests:
use_requests()
elif args.use_injection:
use_injection()
else:
use_direct()
3 changes: 3 additions & 0 deletions python-package/src/atls/atls_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def _verify_certificate(
extension: Extension[ExtensionType]
for extension in peer_cert.extensions:
if validator.accepts(extension.oid):
if not hasattr(extension.value, "value"):
continue

document = extension.value.value
pub = peer_cert.public_key()
spki = pub.public_bytes(
Expand Down
34 changes: 22 additions & 12 deletions python-package/src/atls/httpa_connection.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import socket
from http.client import HTTPS_PORT, HTTPConnection
from typing import Optional, Tuple
from typing import ClassVar, Optional, Tuple

from atls import ATLSContext
from urllib3.connection import HTTPConnection, port_by_scheme
from urllib3.util.connection import _TYPE_SOCKET_OPTIONS
from urllib3.util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT

Comment on lines +5 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a huge deal, but importing variables starting with _ feels a bit weird since they are the python equivalent of private variables and methods (and therefore I am not certain if they are protected from changing / breaking between minor version bumps unlike public methods).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't like it either. The alternative though is duplicating them, which felt nastier when I was considering what to do.

Thinking about it more now, maybe I should duplicate them since these end up being part of the aTLS package's public API.


class HTTPAConnection(HTTPConnection):
Expand All @@ -22,8 +23,9 @@ class HTTPAConnection(HTTPConnection):
port : int, optional
Port to connect to.

timeout : int
Timeout for the attempt to connect to the host on the specified port.
timeout : _TYPE_TIMEOUT
Maximum amount of time, in seconds, to await an attempt to connect to
the host on the specified port before timing out.

source_address : tuple of str and int, optional
A pair of (host, port) for the client socket to bind to before
Expand All @@ -32,27 +34,35 @@ class HTTPAConnection(HTTPConnection):
blocksize : int
Size in bytes of blocks when sending and receiving data to and from the
remote host, respectively.

socket_options: _TYPE_SOCKET_OPTIONS, optional
A sequence of socket options to apply to the socket.
"""

default_port = HTTPS_PORT
default_port: ClassVar[int] = port_by_scheme["https"]

def __init__(
self,
host: str,
context: ATLSContext,
port: Optional[int] = None,
timeout: int = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore
timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
source_address: Optional[Tuple[str, int]] = None,
blocksize: int = 8192,
socket_options: Optional[_TYPE_SOCKET_OPTIONS] = None,
) -> None:
super().__init__(host, port, timeout, source_address, blocksize)

if not isinstance(context, ATLSContext):
raise ValueError("context must be an instance of AtlsContext")
super().__init__(
host,
port,
timeout=timeout,
source_address=source_address,
blocksize=blocksize,
socket_options=socket_options,
)

self._context = context

def connect(self) -> None:
super().connect()

self.sock = self._context.wrap_socket(self.sock)
self.sock = self._context.wrap_socket(self.sock) # type: ignore
40 changes: 40 additions & 0 deletions python-package/src/atls/utils/_httpa_connection_shim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Any, ClassVar, Dict, List, Optional, Tuple

from atls import ATLSContext
from atls.httpa_connection import HTTPAConnection
from atls.validators import Validator
from urllib3.util.connection import _TYPE_SOCKET_OPTIONS
from urllib3.util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT


class _HTTPAConnectionShim(HTTPAConnection):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any particular reason this file and class have an _ in front, because my understanding is that means you don't intend to import it anywhere else, but you do end up importing it in other files (I also have never seen a python file that starts with an _, but that might be a convention I am not aware of).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding the file name, I saw it in the requests library:
https://github.com/psf/requests/blob/main/src/requests/_internal_utils.py

As for the class name, the intention is to mark it as internal to the library, but still available to the library itself. Some tools (like VS Code) automatically hide modules (.py files) and types that start with an underscore.

I suppose that if the file starts with an underscore, then there is no point to also the class starting with one.

I can leave the underscore in the file and remove it from the class. Thoughts?

"""
Provides impendance-matching at the interface between urllib3 and the
HTTPAConnection class.
"""

Validators: ClassVar[List[Validator]]

is_verified: bool = True

def __init__(
self,
host: str,
port: Optional[int] = None,
timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
source_address: Optional[Tuple[str, int]] = None,
blocksize: int = 8192,
socket_options: Optional[_TYPE_SOCKET_OPTIONS] = None,
**_kwargs: Dict[str, Any],
) -> None:
context = ATLSContext(self.Validators)

super().__init__(
host,
context,
port,
timeout,
source_address,
blocksize,
socket_options,
)
Loading
Loading