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: urllib3 and requests support with cleanup #10

Merged
merged 8 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ exclude = '''
# https://pycqa.github.io/isort/docs/configuration/profiles.html
profile = "black"
filter_files = true
line_length = 79 # must be to set to the same value as that in black
line_length = 79 # must be to set to the same value as that in black

# ========== mypy - type checker options ==========
# Global options:
Expand All @@ -37,6 +37,12 @@ warn_unreachable = true
# Per-module options:

[[tool.mypy.overrides]]
module = ["requests", "OpenSSL", "OpenSSL.crypto", "OpenSSL.SSL"]
module = [
"requests",
"requests.adapters",
"OpenSSL",
"OpenSSL.crypto",
"OpenSSL.SSL",
]
# Ignore "missing library stubs or py.typed marker" for all the above modules
ignore_missing_imports = true
47 changes: 37 additions & 10 deletions python-package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ This package aims to implement remote attestation for various TEEs in Python.

## Design

The main workhorse of this package is the `AttestedTLSContext` class. Instances
of this class are parameterized with one or more `Validator`s. A `Validator` can
The main workhorse of this package is the `ATLSContext` class. Instances of this
class are parameterized with one or more `Validator`s. A `Validator` can
understand and appraise evidence or attestation results issued by an attester or
verifier, respectively, contained in an attestation document created by an
issuer, itself embedded in a TLS certificate.

The appraisal of an attestation document takes the place of the typical
PKI-based certificate validation performed during regular TLS. By appraising an
attestation document via `Validator`s, the `AttestedTLSContext` class binds the
TLS handshake not to a PKI-backed entity but to a genuine TEE.
attestation document via `Validator`s, the `ATLSContext` class binds the TLS
handshake not to a PKI-backed entity but to a genuine TEE.

## Sample Usage

Expand All @@ -72,15 +72,42 @@ running on a confidential ACI instance with the corresponding attestation
document issuer, and submit an HTTP request:

```python
from atls import AttestedHTTPSConnection, AttestedTLSContext
from atls.validators import AzAasAciValidator
from atls import ATLSContext, HTTPAConnection
from atls.validators.azure.aas import AciValidator

validator = AzAasAciValidator()
ctx = AttestedTLSContext([validator])
conn = AttestedHTTPConnection("my.confidential.service.net", ctx)
validator = AciValidator()
ctx = ATLSContext([validator])
conn = HTTPAConnection("my.confidential.service.net", ctx)

conn.request("GET", "/index")
print(conn.getresponse().read().decode())

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

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

conn.close()
```

Alternatively, this package integrates into the
[`requests`](https://requests.readthedocs.io/) library by using the `httpa://`
scheme in lieu of `https://`, like so:

```python
import requests

from atls.utils.requests import HTTPAAdapter
from atls.validators.azure.aas import AciValidator

validator = AciValidator()
session = requests.Session()
session.mount("httpa://", HTTPAAdapter([validator]))

response = session.request("GET", "httpa://my.confidential.service.net/index")

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

## Further Reading
Expand Down
121 changes: 100 additions & 21 deletions python-package/sample/connect_aci.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/python3

# Sample usage of the atls package
#
# Suppose a simple HTTP server with a single GET endpoint /index is running
Expand All @@ -17,10 +19,13 @@
# --url /index

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

from atls import AttestedHTTPSConnection, AttestedTLSContext
from atls.validators import AzAasAciValidator
import requests
from atls import ATLSContext, HTTPAConnection
from atls.utils.requests import HTTPAAdapter
from atls.validators.azure.aas import AciValidator

# Parse arguments
parser = argparse.ArgumentParser()
Expand All @@ -36,29 +41,51 @@
parser.add_argument(
"--method",
default="GET",
help="HTTP method to use in the request " "(default: GET)",
help="HTTP method to use in the request (default: GET)",
)

parser.add_argument(
"--url",
default="/index",
help="URL to perform the HTTP request against " "(default: /index)",
help="URL to perform the HTTP request against (default: /index)",
)

parser.add_argument(
"--policy",
nargs="*",
help="path to a CCE policy in Rego format, may be "
"specified multiple times, once for each allowed policy "
"(default: ignore)",
help="path to a CCE policy in Rego format, may be specified multiple "
"times, once for each allowed policy (default: ignore)",
)

parser.add_argument(
"--jku",
nargs="*",
help="allowed JWKS URL to verify the JKU claim in the AAS "
"JWT token against, may be specified multiple times, one "
"for each allowed value (default: ignore)",
action="extend",
help="allowed JWKS URL to verify the JKU claim in the AAS JWT token "
"against, may be specified multiple times, one for each allowed value "
"(default: ignore)",
)

parser.add_argument(
"--body",
type=argparse.FileType("r"),
help="path to a file containing the content to include in the request "
"(default: nothing)",
)

parser.add_argument(
"--headers",
type=argparse.FileType("r"),
help="path to a file containing the string representation of a Python "
"dictionary containing the headers to be sent along with the request "
"(default: none)",
)

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

args = parser.parse_args()
Expand All @@ -81,17 +108,69 @@
# - 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 = AzAasAciValidator(policies=policies, jkus=jkus)
validator = AciValidator(policies=policies, jkus=jkus)

# Parse provided headers, if any.
headers: Mapping[str, str] = {}
if args.headers is not None:
raw = args.headers.read()
headers = ast.literal_eval(raw)

# Read in the provided body, if any.
body: Optional[str] = None
if args.body is not None:
body = args.body.read()


def use_direct() -> None:
# Set up the aTLS context, including at least one attestation document
# 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)

# 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()

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

conn.close()


def use_requests() -> None:
session = requests.Session()
Copy link

Choose a reason for hiding this comment

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

nit: possibly low priority but better to add this as pytest.

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'd like to add unit tests that run automatically but we need an ACI-capable server instance. I do want to add tests for things that can be tested without a server-side component, though; that's on my to-do list.


# 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]))
Copy link

Choose a reason for hiding this comment

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

How to set timeout for the connection in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

session.request(..., timeout=)


# Set up the aTLS context, including at least one attestation document
# validator (only one need succeed).
ctx = AttestedTLSContext([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,
)

# Set up the HTTP request machinery using the aTLS context.
conn = AttestedHTTPSConnection(args.server, ctx, args.port)
print(f"Status: {response.status_code}")
print(f"Response: {response.text}")

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

conn.close()
if args.use_requests:
use_requests()
else:
use_direct()
9 changes: 6 additions & 3 deletions python-package/src/atls/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from atls.attested_https_connection import AttestedHTTPSConnection
from atls.attested_tls_context import AttestedTLSContext
from atls.atls_context import ATLSContext
from atls.httpa_connection import HTTPAConnection

__all__ = ["AttestedHTTPSConnection", "AttestedTLSContext"]
__all__ = [
"HTTPAConnection",
"ATLSContext",
]
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from urllib3.contrib.pyopenssl import WrappedSocket # noqa: E402


class AttestedTLSContext(PyOpenSSLContext):
class ATLSContext(PyOpenSSLContext):
"""
An SSL context that supports validation of aTLS certificates.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from http.client import HTTPS_PORT, HTTPConnection
from typing import Optional, Tuple

from atls.attested_tls_context import AttestedTLSContext
from atls import ATLSContext


class AttestedHTTPSConnection(HTTPConnection):
class HTTPAConnection(HTTPConnection):
"""
Performs HTTP requests over an Attested TLS (aTLS) connection. It is
equivalent to HTTPSConnection, but the underlying transport is aTLS instead
Expand All @@ -16,7 +16,7 @@ class AttestedHTTPSConnection(HTTPConnection):
host : str
IP address or hostname to connect to.

context : AttestedTLSContext
context : ATLSContext
An aTLS context that performs the aTLS handshake.

port : int, optional
Expand All @@ -39,15 +39,15 @@ class AttestedHTTPSConnection(HTTPConnection):
def __init__(
self,
host: str,
context: AttestedTLSContext,
context: ATLSContext,
port: Optional[int] = None,
timeout: int = socket._GLOBAL_DEFAULT_TIMEOUT, # type: ignore
source_address: Optional[Tuple[str, int]] = None,
blocksize: int = 8192,
) -> None:
super().__init__(host, port, timeout, source_address, blocksize)

if not isinstance(context, AttestedTLSContext):
if not isinstance(context, ATLSContext):
raise ValueError("context must be an instance of AtlsContext")

self._context = context
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions python-package/src/atls/utils/requests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from atls.utils.requests.adapter import HTTPAAdapter

__all__ = ["HTTPAAdapter"]
Loading
Loading