Skip to content

Commit

Permalink
[SPE-925] Add pcr provider manager for cage pcrs (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
donaltuohy committed Nov 2, 2023
1 parent f390e37 commit 1d49c2b
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/warm-bears-compare.md
@@ -0,0 +1,5 @@
---
"evervault-python": minor
---

Cage PCR Provider: publish new PCRs to public source which SDKs can pull from for attestation
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -12,4 +12,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/everva

## Feedback

Questions or feedback? [Let us know](mailto:support@evervault.com).
Questions or feedback? [Let us know](mailto:support@evervault.com).
10 changes: 9 additions & 1 deletion evervault/__init__.py
Expand Up @@ -7,6 +7,7 @@
from warnings import warn
import warnings
from evervault.http.attestationdoc import AttestationDoc
from evervault.http.cagePcrManager import CagePcrManager
from importlib import metadata

__version__ = metadata.version(__package__ or __name__)
Expand All @@ -25,6 +26,7 @@
CAGES_BETA_HOST_DEFAULT = "cages.evervault.com"
CAGES_GA_HOST_DEFAULT = "cage.evervault.com"
MAX_FILE_SIZE_IN_MB_DEFAULT = 25
DEFAULT_PCR_PROVIDER_POLL_INTERVAL = 300

SUPPORTED_CURVES = ["SECP256K1", "SECP256R1"]

Expand Down Expand Up @@ -97,7 +99,13 @@ def cage_requests_session(cage_attestation_data={}):
def attestable_cage_session(cage_attestation_data={}):
cage_host = os.environ.get("EV_CAGES_HOST_GA", CAGES_GA_HOST_DEFAULT)
cache = AttestationDoc(_app_uuid, cage_attestation_data.keys(), cage_host)
return CageRequestsSession(cage_attestation_data, cage_host, cache)
pcr_manager = CagePcrManager(
cage_attestation_data,
os.environ.get(
"EV_PCR_PROVIDER_POLL_INTERVAL", DEFAULT_PCR_PROVIDER_POLL_INTERVAL
),
)
return CageRequestsSession(pcr_manager, cage_host, cache)


def create_run_token(function_name, data={}):
Expand Down
22 changes: 10 additions & 12 deletions evervault/cages_v2.py
Expand Up @@ -3,6 +3,7 @@
import evervault_attestation_bindings
from types import MethodType
from evervault.errors.evervault_errors import EvervaultError
from evervault.http.cagePcrManager import CagePcrManager
import tempfile
import base64

Expand Down Expand Up @@ -75,8 +76,8 @@ def _validate_conn_override(self, conn):


class CageHTTPAdapter(requests.adapters.HTTPAdapter):
def __init__(self, cage_attestation_data, cages_host, cache):
self.attestation_data = cage_attestation_data
def __init__(self, cage_pcr_manager: CagePcrManager, cages_host: str, cache):
self.cage_pcr_manager = cage_pcr_manager
self.cages_host = cages_host
self.cache = cache
super().__init__()
Expand All @@ -91,14 +92,12 @@ def get_connection(self, url, proxies=None):

def add_attestation_check_to_conn_validation(self, conn, cage_name):
cache = self.cache

pcrs_from_manager = self.cage_pcr_manager.get(cage_name)
expected_pcrs = []
if cage_name in self.attestation_data:
given_pcrs = self.attestation_data[cage_name]
# if the user only supplied a single set of PCRs, convert it to a list
if not isinstance(given_pcrs, list):
given_pcrs = [given_pcrs]

for pcrs in given_pcrs:
if pcrs_from_manager is not None:
for pcrs in pcrs_from_manager:
expected_pcrs.append(
evervault_attestation_bindings.PCRs(
pcrs.get("pcr_0"),
Expand All @@ -107,6 +106,7 @@ def add_attestation_check_to_conn_validation(self, conn, cage_name):
pcrs.get("pcr_8"),
)
)

original_validate_conn = (
urllib3.connectionpool.HTTPSConnectionPool._validate_conn
)
Expand Down Expand Up @@ -174,11 +174,9 @@ def request(self, *args, headers={}, **kwargs):


class CageRequestsSession(requests.Session):
def __init__(self, cage_attestation_data, cages_host, cache):
def __init__(self, cage_pcr_manager, cages_host, cache):
super().__init__()
self.mount(
"https://", CageHTTPAdapter(cage_attestation_data, cages_host, cache)
)
self.mount("https://", CageHTTPAdapter(cage_pcr_manager, cages_host, cache))

def request(self, *args, headers={}, **kwargs):
return super().request(*args, headers=headers, **kwargs)
2 changes: 1 addition & 1 deletion evervault/http/attestationdoc.py
Expand Up @@ -9,7 +9,7 @@


class AttestationDoc:
def __init__(self, app_uuid, cage_names, cage_host, poll_interval=2700):
def __init__(self, app_uuid, cage_names, cage_host, poll_interval=300):
self.cage_host = cage_host
self.app_uuid = app_uuid.replace("_", "-")
self.cage_names = cage_names
Expand Down
123 changes: 123 additions & 0 deletions evervault/http/cagePcrManager.py
@@ -0,0 +1,123 @@
import logging
from evervault.threading.repeatedtimer import RepeatedTimer
import threading
import warnings
import time


logger = logging.getLogger(__name__)


class CagePcrManager:
def __init__(self, attestation_data, poll_interval=300):
self.attestation_data = attestation_data
self.poll_interval = poll_interval
self.lock = threading.Lock()
self.store = {}

self.__load_providers(attestation_data)
self.__fetch_all_pcrs()

logger.debug(
"EVERVAULT :: Cage PCR manager starting polling for PCRs every {self.poll_interval} seconds"
)

self.repeated_timer = RepeatedTimer(
self.poll_interval,
self.__fetch_all_pcrs,
)

def get(self, cage_name) -> str:
with self.lock:
try:
pcrs = self.store[cage_name]["pcrs"]
if pcrs is None:
raise KeyError
return pcrs
except KeyError:
pcrs = self.__fetch_pcrs(cage_name)
self.store[cage_name]["pcrs"] = pcrs
return pcrs

def get_poll_interval(self) -> list:
return self.repeated_timer.get_interval()

def disable_polling(self):
if self.repeated_timer is not None:
self.repeated_timer.stop()
self.repeated_timer = None

def clear_store(self):
with self.lock:
self.store = {}

def remove_pcrs_for_cage(self, cage_name):
with self.lock:
self.store[cage_name]["pcrs"] = None

def __create_provider_from_static_pcrs(self, pcrs):
def provider():
return pcrs

return provider

def __load_providers(self, attestation_data):
for cage_name, value in attestation_data.items():
if callable(value):
self.store[cage_name] = {"pcrs": None, "provider": value}
elif isinstance(value, list):
self.store[cage_name] = {
"pcrs": value,
"provider": self.__create_provider_from_static_pcrs(value),
}
elif isinstance(value, dict):
self.store[cage_name] = {
"pcrs": [value],
"provider": self.__create_provider_from_static_pcrs([value]),
}

else:
raise Exception("EVERVAULT :: Invalid PCR data. Cannot create provider")

def __fetch_all_pcrs(self):
logger.debug("EVERVAULT :: Retrieving Cage PCRs from providers")
for cage_name, provider in self.store.items():
pcrs = self.__fetch_pcrs(cage_name)
self.store[cage_name]["pcrs"] = pcrs

def __fetch_pcrs(self, cage_name):
try:
provider = self.store[cage_name]["provider"]

if provider is None:
warnings.warn(
f"EVERVAULT :: No PCR provider registered for {cage_name}. Cannot fetch PCRs"
)
return None

retries = 3
delay = 0.5

while retries > 0:
try:
pcrs = provider()
logger.debug(
"EVERVAULT :: Retrieved PCRs from provider for {cage_name}"
)
return pcrs
except Exception as e:
retries -= 1
if retries == 0:
raise e
else:
time.sleep(delay)
delay *= 2
warnings.warn(
f"EVERVAULT :: Could not get PCR for {cage_name} {e}"
)
continue
except KeyError:
warnings.warn(
f"EVERVAULT :: No PCR provider registered for {cage_name}. Cannot fetch PCRs"
)
return None
55 changes: 55 additions & 0 deletions tests/test_attestatable_session.py
@@ -1,4 +1,6 @@
import unittest

import requests
import pytest
import importlib
import evervault
Expand Down Expand Up @@ -27,6 +29,59 @@ def test_valid_pcrs(self):
)
assert response.status_code == 401

def test_valid_pcrs_from_array(self):

attested_session = self.evervault.attestable_cage_session(
{
self.cage_name: [
{
"pcr_8": "invalid00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"pcr_8": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
]
}
)
response = attested_session.get(
f"https://{self.cage_name}.{self.app_uuid}.cage.evervault.com/echo"
)
assert response.status_code == 401

def test_valid_pcrs_from_provider(self):
def provider():
pcrs = requests.get(
"https://gist.githubusercontent.com/donaltuohy/5dbc1c175bcd0f0a9a621184cf3c78dc/raw/df25a123dea6424fb630ea80f241c676931728da/pcrs.json"
).json()
return pcrs

attested_session = self.evervault.attestable_cage_session(
{self.cage_name: provider}
)
response = attested_session.get(
f"https://{self.cage_name}.{self.app_uuid}.cage.evervault.com/echo"
)
assert response.status_code == 401

def test_invalid_pcrs_from_provider(self):
def provider():
pcrs = requests.get(
"https://gist.githubusercontent.com/hanneary/d076f6702c1694d29c117a1e00f1957e/raw/2c4999694e302cedb6283ba531dcdab45d556bcc/invalidpcr.json"
).json()
return pcrs

with pytest.raises(
CageVerificationException,
match="The PCRs found were different to the expected values",
):
attested_session = self.evervault.attestable_cage_session(
{self.cage_name: provider}
)

attested_session.get(
f"https://{self.cage_name}.{self.app_uuid}.cage.evervault.com/echo"
)

def test_invalid_pcrs(self):
with pytest.raises(
CageVerificationException,
Expand Down
72 changes: 72 additions & 0 deletions tests/test_pcrs_manager.py
@@ -0,0 +1,72 @@
import unittest
from evervault.http.cagePcrManager import CagePcrManager


class TestCagePcrManager(unittest.TestCase):
def setUp(self):
self.app_uuid = "app-123"
self.cage_1 = "cage_1"
self.cage_2 = "cage_2"

def test_get_pcrs(self):
test_pcrs1 = [
{
"pcr_8": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
]
test_pcrs2 = [
{
"pcr_8": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
}
]

def test_provider1():
return test_pcrs1

def test_provider2():
return test_pcrs2

attestation_data = {self.cage_1: test_provider1, self.cage_2: test_provider2}

manager = CagePcrManager(attestation_data)

assert manager.get(self.cage_1) == test_pcrs1
assert manager.get(self.cage_2) == test_pcrs2

def test_get_hardcoded_pcrs(self):
test_pcrs_object = {
"pcr_8": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
test_pcrs_array = [
{
"pcr_8": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"pcr_8": "111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011"
},
]
attestation_data = {self.cage_1: test_pcrs_object, self.cage_2: test_pcrs_array}

manager = CagePcrManager(attestation_data)

assert manager.get(self.cage_1) == [test_pcrs_object]
assert manager.get(self.cage_2) == test_pcrs_array

def test_get_pcrs_reload_on_missing(self):
test_pcrs1 = [
{
"pcr_8": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
]

def test_provider1():
return test_pcrs1

attestation_data = {self.cage_1: test_provider1}

manager = CagePcrManager(attestation_data)

# Remove pcrs for cage_1 so that we test the reload on a miss
manager.remove_pcrs_for_cage(self.cage_1)

assert manager.get(self.cage_1) == test_pcrs1

0 comments on commit 1d49c2b

Please sign in to comment.