From abdfa1e133730bcea815f37c4d0e0ba53d39f68a Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Mahawar <114311884+HarshvMahawar@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:52:25 +0530 Subject: [PATCH 01/16] Enhance README.md with detailed technical overview of the project Signed-off-by: HarshvMahawar --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 537468d..1407903 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,68 @@ -# python-ear +# **python-ear** -A python implementation of [draft-fv-rats-ear](https://datatracker.ietf.org/doc/draft-fv-rats-ear/). +A Python library that implements the EAT Attestation Result (EAR) data format, as specified in [draft-fv-rats-ear](https://datatracker.ietf.org/doc/draft-fv-rats-ear/). This library provides implementations for both CBOR-based and JSON-based serialisations. -# Proposal +--- -Following are the tools that will be used in the development of this library +## **Overview** -## CWT and JWT creation +The goal of this project is to standardize attestation results by defining a shared information and data model, enabling seamless integration with other components of the RATS architecture. This focuses specifically on harmonizing attestation results to facilitate interoperability between various verifiers and relying parties. -1. [python-cwt](https://python-cwt.readthedocs.io/en/stable/) -2. [python-jwt](https://pypi.org/project/python-jose/) +This implementation was initiated as part of the **Veraison Mentorship** under the Linux Foundation Mentorship Program (**LFX Mentorship**), focusing on the following capabilities: -## Code formatting and styling +- **Populating EAR Claims-Sets:** Define and populate claims that represent evidence and attestation results. +- **Signing EAR Claims-Sets:** Support signing using private keys, ensuring data integrity and authenticity. +- **Encoding and Decoding:** + - Encode signed EAR claims as **CWT** (Concise Binary Object Representation Web Tokens) or **JWT** (JSON Web Tokens). + - Decode signed EARs from CWT or JWT formats, enabling interoperability between different systems. +- **Signature Verification:** Verify signatures using public keys to ensure the authenticity of claims. +- **Accessing Claims:** Provide interfaces to access and manage EAR claims efficiently. -1. [black](https://pypi.org/project/black/) -2. [isort](https://pypi.org/project/isort/) +This library is developed in Python and makes use of existing packages for CWT and JWT management, static code analysis, and testing. -## Linting and static analysis +--- -1. [flake8](https://pypi.org/project/flake8/) -2. [mypy](https://pypi.org/project/mypy/) +## **Key Features** -## Testing +1. **Standards Compliance:** + Implements draft-fv-rats-ear as per IETF specifications to ensure compatibility with the RATS architecture. -1. [pytest](https://pypi.org/project/pytest/) \ No newline at end of file +2. **Token Management:** + - **CWT Support:** Utilizes [python-cwt](https://python-cwt.readthedocs.io/en/stable/) for handling CBOR Web Tokens. + - **JWT Support:** Uses [python-jose](https://pypi.org/project/python-jose/) for JSON Web Tokens management. + +3. **Security:** + - Supports signing of EAR claims with private keys and verification with public keys. + - Adopts secure cryptographic practices for token creation and verification. + +4. **Static Analysis and Code Quality:** + - Ensures code quality using linters and static analysis tools. + - Maintains type safety and code consistency. + +5. **Testing:** + - Comprehensive unit tests using `pytest` to validate all functionalities. + +--- + +## **Technical Stack** + +### **Token Creation and Management** + +- **CWT:** [python-cwt](https://python-cwt.readthedocs.io/en/stable/) +- **JWT:** [python-jose](https://pypi.org/project/python-jose/) + +### **Code Formatting and Styling** + +- **black:** Ensures consistent code formatting. +- **isort:** Manages import statements. + +### **Linting and Static Analysis** + +- **flake8:** For PEP 8 compliance and linting. +- **mypy:** Static type checking. +- **pyright:** Advanced type checking for Python. +- **pylint:** Code analysis for error detection and enforcing coding standards. + +### **Testing** + +- **pytest:** Framework for writing and executing tests. \ No newline at end of file From b6224c0c15daa74c5514de7fdda5c81032651292 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Thu, 6 Mar 2025 05:03:35 +0530 Subject: [PATCH 02/16] Add GitHub Actions workflow for Tox Signed-off-by: HarshvMahawar --- .github/workflows/tox.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/tox.yml diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml new file mode 100644 index 0000000..d907ca5 --- /dev/null +++ b/.github/workflows/tox.yml @@ -0,0 +1,34 @@ +name: Run Tox on PR + +on: + pull_request: + branches: + - main + - '**' # Run on all branches for PRs + +jobs: + tox-tests: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.11] # Test against multiple Python versions + + steps: + # Checkout the code + - name: Checkout code + uses: actions/checkout@v3 + + # Setup Python + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + # Install tox + - name: Install tox + run: pip install tox + + # Run tox + - name: Run tox + run: tox \ No newline at end of file From f41a8a577773b315600fe60620b54007979399c9 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Thu, 6 Mar 2025 17:05:13 +0530 Subject: [PATCH 03/16] Add support for Python 3.9 in GitHub Actions workflow for Tox Signed-off-by: HarshvMahawar --- .github/workflows/tox.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index d907ca5..f68e652 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - python-version: [3.11] # Test against multiple Python versions + python-version: [3.9, 3.11] # Test against multiple Python versions steps: # Checkout the code From d6e58f2102a953b8ec63647b6b896b56fa53c8c1 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Wed, 12 Mar 2025 15:21:00 +0530 Subject: [PATCH 04/16] Add VerifierID struct with its unit tests Signed-off-by: HarshvMahawar --- src/base.py | 28 +++++++++++++++++++++++++++ src/verifier_id.py | 36 +++++++++++++++++++++++++++++++++++ tests/test_verifier_id.py | 40 +++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 4 files changed, 105 insertions(+) create mode 100644 src/base.py create mode 100644 src/verifier_id.py create mode 100644 tests/test_verifier_id.py diff --git a/src/base.py b/src/base.py new file mode 100644 index 0000000..c7bc1fb --- /dev/null +++ b/src/base.py @@ -0,0 +1,28 @@ +import json +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class BaseJCSerializable(ABC): + JC_map: Dict[str, int] + + @abstractmethod + def to_dict(self) -> Dict[str, Any]: + pass + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + @abstractmethod + def to_cbor(self) -> Dict[int, Any]: + pass + + @classmethod + @abstractmethod + def from_dict(cls, data: Dict[str, Any]): + pass + + @classmethod + @abstractmethod + def from_cbor(cls, data: Dict[int, Any]): + pass diff --git a/src/verifier_id.py b/src/verifier_id.py new file mode 100644 index 0000000..f3d9ca3 --- /dev/null +++ b/src/verifier_id.py @@ -0,0 +1,36 @@ +from dataclasses import asdict, dataclass +from typing import Any, Dict + +from src.base import BaseJCSerializable + + +@dataclass +class VerifierID(BaseJCSerializable): + developer: str + build: str + # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.3 + JC_map = { + "developer": 0, # JC<"developer", 0> + "build": 1, # JC<"build", 1> + } + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + # Convert to a dict with integer keys (for CBOR) + def to_cbor(self) -> Dict[int, str]: + return { + index: getattr(self, field) for field, index in self.JC_map.items() + } # noqa: E501 + + # Create an instance from a dict with string keys + @classmethod + def from_dict(cls, data: Dict[str, str]): + return cls(**data) + + # Create an instance from a CBOR-like dict (integer keys) + @classmethod + def from_cbor(cls, data: Dict[int, str]): + reverse_map = {v: k for k, v in cls.JC_map.items()} + kwargs = {reverse_map[index]: value for index, value in data.items()} + return cls(**kwargs) diff --git a/tests/test_verifier_id.py b/tests/test_verifier_id.py new file mode 100644 index 0000000..372c6c5 --- /dev/null +++ b/tests/test_verifier_id.py @@ -0,0 +1,40 @@ +import json + +import pytest + +from src.verifier_id import VerifierID + + +@pytest.fixture +def verifier(): + # Sample VerifierID object for testing + return VerifierID(developer="Acme Inc.", build="v1.0.0") + + +def test_to_dict(verifier): # pylint: disable=redefined-outer-name + expected = {"developer": "Acme Inc.", "build": "v1.0.0"} + assert verifier.to_dict() == expected + + +def test_to_json(verifier): # pylint: disable=redefined-outer-name + expected = json.dumps({"developer": "Acme Inc.", "build": "v1.0.0"}) + assert verifier.to_json() == expected + + +def test_to_cbor(verifier): # pylint: disable=redefined-outer-name + expected = {0: "Acme Inc.", 1: "v1.0.0"} + assert verifier.to_cbor() == expected + + +def test_from_dict(): + data = {"developer": "Acme Inc.", "build": "v1.0.0"} + sample_verifier = VerifierID.from_dict(data) + assert sample_verifier.developer == "Acme Inc." + assert sample_verifier.build == "v1.0.0" + + +def test_from_cbor(): + cbor_data = {0: "Acme Inc.", 1: "v1.0.0"} + sample_verifier = VerifierID.from_cbor(cbor_data) + assert sample_verifier.developer == "Acme Inc." + assert sample_verifier.build == "v1.0.0" diff --git a/tox.ini b/tox.ini index 2a75fe9..0d461b8 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ deps = mypy==1.5.1 pylint==2.17.5 pyright==1.1.325 + pytest==7.4.2 commands = isort . --check --diff black . --check --diff From ff5410db14e4d4411d805da06286cb94fdb08416 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Thu, 13 Mar 2025 19:51:20 +0530 Subject: [PATCH 05/16] Implementation Overview Signed-off-by: HarshvMahawar --- src/base.py | 2 + src/claims.py | 35 ++++-- src/trust_claims.py | 224 +++++++++++++++++++++++++++++++++++++ src/trust_vector.py | 58 ++++++++++ tests/test_claims.py | 97 ++++++++++++++-- tests/test_trust_claims.py | 19 ++++ tests/test_trust_vector.py | 93 +++++++++++++++ 7 files changed, 511 insertions(+), 17 deletions(-) create mode 100644 src/trust_claims.py create mode 100644 src/trust_vector.py create mode 100644 tests/test_trust_claims.py create mode 100644 tests/test_trust_vector.py diff --git a/src/base.py b/src/base.py index c7bc1fb..3f3a993 100644 --- a/src/base.py +++ b/src/base.py @@ -3,6 +3,7 @@ from typing import Any, Dict +# Abstract class to define structure to subclasses class BaseJCSerializable(ABC): JC_map: Dict[str, int] @@ -10,6 +11,7 @@ class BaseJCSerializable(ABC): def to_dict(self) -> Dict[str, Any]: pass + # Similar for all the subclasses def to_json(self) -> str: return json.dumps(self.to_dict()) diff --git a/src/claims.py b/src/claims.py index 8e69c4d..86eb826 100644 --- a/src/claims.py +++ b/src/claims.py @@ -2,34 +2,51 @@ from dataclasses import dataclass, field from typing import Any, Dict +from src.trust_vector import TrustVector +from src.verifier_id import VerifierID + +# Represents the EAR Claims set that will be populated @dataclass class EARClaims: profile: str issued_at: int - verifier_id: Dict[str, str] = field(default_factory=dict) - submods: Dict[str, Any] = field(default_factory=dict) + verifier_id: VerifierID + submods: Dict[str, Dict[str, Any]] = field(default_factory=dict) + # Returns a python dictionary that will be used for serializing to JWT def to_dict(self) -> Dict[str, Any]: return { "eat_profile": self.profile, "iat": self.issued_at, - "ear.verifier-id": self.verifier_id, - "submods": self.submods, + "ear.verifier-id": self.verifier_id.to_dict(), + "submods": { + key: { + "trust_vector": value["trust_vector"].to_dict(), + "status": value["status"], + } + for key, value in self.submods.items() + }, } + def to_json(self) -> str: + return json.dumps(self.to_dict()) + @classmethod def from_dict(cls, data: Dict[str, Any]): return cls( profile=data.get("eat_profile", ""), issued_at=data.get("iat", 0), - verifier_id=data.get("ear.verifier-id", {}), - submods=data.get("submods", {}), + verifier_id=VerifierID.from_dict(data.get("ear.verifier-id", {})), + submods={ + key: { + "trust_vector": TrustVector.from_dict(value["trust_vector"]), # noqa: E501 + "status": value["status"], + } + for key, value in data.get("submods", {}).items() + }, ) - def to_json(self) -> str: - return json.dumps(self.to_dict()) - @classmethod def from_json(cls, json_str: str): return cls.from_dict(json.loads(json_str)) diff --git a/src/trust_claims.py b/src/trust_claims.py new file mode 100644 index 0000000..0f1e040 --- /dev/null +++ b/src/trust_claims.py @@ -0,0 +1,224 @@ +from dataclasses import asdict, dataclass +from typing import Any, Dict + + +@dataclass +class TrustClaim: + value: int # must be in range -128 to 127 + tag: str = "" + short: str = "" + long: str = "" + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +# General +VerifierMalfunctionClaim = TrustClaim( + value=-1, + tag="verifier_malfunction", + short="verifier malfunction", + long="A verifier malfunction occurred during the Verifier's appraisal processing.", # noqa: E501 +) +NoClaim = TrustClaim( + value=0, + tag="no_claim", + short="no claim being made", + long="The Evidence received is insufficient to make a conclusion.", +) +UnexpectedEvidenceClaim = TrustClaim( + value=1, + tag="unexected_evidence", + short="unexpected evidence", + long="The Evidence received contains unexpected elements which the Verifier is unable to parse.", # noqa: E501 +) +CryptoValidationFailedClaim = TrustClaim( + value=99, + tag="crypto_failed", + short="cryptographic validation failed", + long="Cryptographic validation of the Evidence has failed.", +) + +# Instance Identity +TrustworthyInstanceClaim = TrustClaim( + value=2, + tag="recognized_instance", + short="recognized and not compromised", + long="The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 +) +UntrustworthyInstanceClaim = TrustClaim( + value=96, + tag="untrustworthy_instance", + short="recognized but not trustworthy", + long="The Attesting Environment is recognized, but its unique private key indicates a device which is not trustworthy.", # noqa: E501 +) +UnrecognizedInstanceClaim = TrustClaim( + value=97, + tag="unrecognized_instance", + short="not recognized", + long="The Attesting Environment is not recognized; however the Verifier believes it should be.", # noqa: E501 +) + +# Config +ApprovedConfigClaim = TrustClaim( + value=2, + tag="approved_config", + short="all recognized and approved", + long="The configuration is a known and approved config.", +) +NoConfigVulnsClaim = TrustClaim( + value=3, + tag="safe_config", + short="no known vulnerabilities", + long="The configuration includes or exposes no known vulnerabilities", +) +UnsafeConfigClaim = TrustClaim( + value=32, + tag="unsafe_config", + short="known vulnerabilities", + long="The configuration includes or exposes known vulnerabilities.", +) +UnsupportableConfigClaim = TrustClaim( + value=96, + tag="unsupportable_config", + short="unacceptable security vulnerabilities", + long="The configuration is unsupportable as it exposes unacceptable security vulnerabilities", # noqa: E501 +) + +# Executables & Runtime +ApprovedRuntimeClaim = TrustClaim( + value=2, + tag="approved_rt", + short="recognized and approved boot- and run-time", + long="Only a recognized genuine set of approved executables, scripts, files, and/or objects have been loaded during and after the boot process.", # noqa: E501 +) +ApprovedBootClaim = TrustClaim( + value=3, + tag="approved_boot", + short="recognized and approved boot-time", + long="Only a recognized genuine set of approved executables have been loaded during the boot process.", # noqa: E501 +) +UnsafeRuntimeClaim = TrustClaim( + value=32, + tag="unsafe_rt", + short="recognized but known bugs or vulnerabilities", + long="Only a recognized genuine set of executables, scripts, files, and/or objects have been loaded. However the Verifier cannot vouch for a subset of these due to known bugs or other known vulnerabilities.", # noqa: E501 +) +UnrecognizedRuntimeClaim = TrustClaim( + value=33, + tag="unrecognized_rt", + short="unrecognized run-time", + long="Runtime memory includes executables, scripts, files, and/or objects which are not recognized.", # noqa: E501 +) +ContraindicatedRuntimeClaim = TrustClaim( + value=96, + tag="contraindicated_rt", + short="contraindicated run-time", + long="Runtime memory includes executables, scripts, files, and/or object which are contraindicated.", # noqa: E501 +) + +# File System +ApprovedFilesClaim = TrustClaim( + value=2, + tag="approved_fs", + short="all recognized and approved", + long="Only a recognized set of approved files are found.", +) +UnrecognizedFilesClaim = TrustClaim( + value=32, + tag="unrecognized_fs", + short="unrecognized item(s) found", + long="The file system includes unrecognized executables, scripts, or files.", # noqa: E501 +) +ContraindicatedFilesClaim = TrustClaim( + value=96, + tag="contraindicated_fs", + short="contraindicated item(s) found", + long="The file system includes contraindicated executables, scripts, or files.", # noqa: E501 +) + +# Hardware +GenuineHardwareClaim = TrustClaim( + value=2, + tag="genuine_hw", + short="genuine", + long="An Attester has passed its hardware and/or firmware verifications needed to demonstrate that these are genuine/supported.", # noqa: E501 +) +UnsafeHardwareClaim = TrustClaim( + value=32, + tag="unsafe_hw", + short="genuine but known bugs or vulnerabilities", + long="An Attester contains only genuine/supported hardware and/or firmware, but there are known security vulnerabilities.", # noqa: E501 +) +ContraindicatedHardwareClaim = TrustClaim( + value=96, + tag="contraindicated_hw", + short="genuine but contraindicated", + long="Attester hardware and/or firmware is recognized, but its trustworthiness is contraindicated.", # noqa: E501 +) +UnrecognizedHardwareClaim = TrustClaim( + value=97, + tag="unrecognized_hw", + short="unrecognized", + long="A Verifier does not recognize an Attester's hardware or firmware, but it should be recognized.", # noqa: E501 +) + +# Opaque Runtime +EncryptedMemoryRuntimeClaim = TrustClaim( + value=2, + tag="encrypted_rt", + short="memory encryption", + long="the Attester's executing Target Environment and Attesting Environments are encrypted and within Trusted Execution Environment(s) opaque to the operating system, virtual machine manager, and peer applications.", # noqa: E501 +) +IsolatedMemoryRuntimeClaim = TrustClaim( + value=32, + tag="isolated_rt", + short="memory isolation", + long="the Attester's executing Target Environment and Attesting Environments are inaccessible from any other parallel application or Guest VM running on the Attester's physical device.", # noqa: E501 +) +VisibleMemoryRuntimeClaim = TrustClaim( + value=96, + tag="visible_rt", + short="visible", + long="The Verifier has concluded that in memory objects are unacceptably visible within the physical host that supports the Attester.", # noqa: E501 +) + +# Opaque Storage +HwKeysEncryptedSecretsClaim = TrustClaim( + value=2, + tag="hw_encrypted_secrets", + short="encrypted secrets with HW-backed keys", + long="the Attester encrypts all secrets in persistent storage via using keys which are never visible outside an HSM or the Trusted Execution Environment hardware.", # noqa: E501 +) +SwKeysEncryptedSecretsClaim = TrustClaim( + value=32, + tag="sw_encrypted_secrets", + short="encrypted secrets with non HW-backed keys", + long="the Attester encrypts all persistently stored secrets, but without using hardware backed keys.", # noqa: E501 +) +UnencryptedSecretsClaim = TrustClaim( + value=96, + tag="unencrypted_secrets", + short="unencrypted secrets", + long="There are persistent secrets which are stored unencrypted in an Attester.", # noqa: E501 +) + +# Sourced Data +TrustedSourcesClaim = TrustClaim( + value=2, + tag="trusted_sources", + short="from attesters in the affirming tier", + long='All essential Attester source data objects have been provided by other Attester(s) whose most recent appraisal(s) had both no Trustworthiness Claims of "0" where the current Trustworthiness Claim is "Affirming", as well as no "Warning" or "Contraindicated" Trustworthiness Claims.', # noqa: E501 +) +UntrustedSourcesClaim = TrustClaim( + value=32, + tag="untrusted_sources", + short="from unattested sources or attesters in the warning tier", + long='Attester source data objects come from unattested sources, or attested sources with "Warning" type Trustworthiness Claims', # noqa: E501 +) +ContraindicatedSourcesClaim = TrustClaim( + value=96, + tag="contraindicated_sources", + short="from attesters in the contraindicated tier", + long="Attester source data objects come from contraindicated sources.", +) diff --git a/src/trust_vector.py b/src/trust_vector.py new file mode 100644 index 0000000..9c80972 --- /dev/null +++ b/src/trust_vector.py @@ -0,0 +1,58 @@ +from dataclasses import asdict, dataclass +from typing import Any, Dict, Optional + +from src.base import BaseJCSerializable +from src.trust_claims import TrustClaim + + +# TrustVector class to represent the trustworthiness vector +@dataclass +class TrustVector(BaseJCSerializable): + instance_identity: Optional[TrustClaim] = None + configuration: Optional[TrustClaim] = None + executables: Optional[TrustClaim] = None + file_system: Optional[TrustClaim] = None + hardware: Optional[TrustClaim] = None + runtime_opaque: Optional[TrustClaim] = None + storage_opaque: Optional[TrustClaim] = None + sourced_data: Optional[TrustClaim] = None + + # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.1 + JC_map = { + "instance_identity": 0, + "configuration": 1, + "executables": 2, + "file_system": 3, + "hardware": 4, + "runtime_opaque": 5, + "storage_opaque": 6, + "sourced_data": 7, + } + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + def to_cbor(self) -> Dict[int, Dict[str, Any]]: + return { + index: getattr(self, field).to_dict() + for field, index in self.JC_map.items() + if getattr(self, field) + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]): + kwargs = { + field: TrustClaim(**data[field]) if data.get(field) else None + for field in cls.JC_map + } + return cls(**kwargs) + + @classmethod + def from_cbor(cls, data: Dict[int, Dict[str, Any]]): + reverse_map = {v: k for k, v in cls.JC_map.items()} + kwargs = { + reverse_map[index]: TrustClaim(**value) + for index, value in data.items() + if index in reverse_map + } + return cls(**kwargs) diff --git a/tests/test_claims.py b/tests/test_claims.py index 06fac4b..5d15f34 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -1,13 +1,94 @@ +import pytest + from src.claims import EARClaims +from src.trust_claims import ( + ApprovedConfigClaim, + ApprovedFilesClaim, + ApprovedRuntimeClaim, + EncryptedMemoryRuntimeClaim, + GenuineHardwareClaim, + HwKeysEncryptedSecretsClaim, + TrustedSourcesClaim, + TrustworthyInstanceClaim, +) +from src.trust_vector import TrustVector +from src.verifier_id import VerifierID -def test_ear_claims(): - claims = EARClaims( - "test_profile", - 1234567890, - {"build": "v1"}, - {"submods1": {"status": "affirming"}}, +@pytest.fixture +def sample_ear_claims(): + return EARClaims( + profile="test_profile", + issued_at=1234567890, + verifier_id=VerifierID(developer="Acme Inc.", build="v1"), + submods={ + "submod1": { + "trust_vector": TrustVector( + instance_identity=TrustworthyInstanceClaim, + configuration=ApprovedConfigClaim, + executables=ApprovedRuntimeClaim, + file_system=ApprovedFilesClaim, + hardware=GenuineHardwareClaim, + runtime_opaque=EncryptedMemoryRuntimeClaim, + storage_opaque=HwKeysEncryptedSecretsClaim, + sourced_data=TrustedSourcesClaim, + ), + "status": "affirming", + } + }, ) - json_str = claims.to_json() + + +def test_ear_claims_to_dict(sample_ear_claims): + expected = { + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"}, + "submods": { + "submod1": { + "trust_vector": { + "instance_identity": TrustworthyInstanceClaim.to_dict(), + "configuration": ApprovedConfigClaim.to_dict(), + "executables": ApprovedRuntimeClaim.to_dict(), + "file_system": ApprovedFilesClaim.to_dict(), + "hardware": GenuineHardwareClaim.to_dict(), + "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), + "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), + "sourced_data": TrustedSourcesClaim.to_dict(), + }, + "status": "affirming", + } + }, + } + assert sample_ear_claims.to_dict() == expected + + +def test_ear_claims_to_json(sample_ear_claims): + json_str = sample_ear_claims.to_json() parsed_claims = EARClaims.from_json(json_str) - assert parsed_claims.to_dict() == claims.to_dict() + assert parsed_claims.to_dict() == sample_ear_claims.to_dict() + + +def test_ear_claims_from_dict(): + data = { + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"}, + "submods": { + "submod1": { + "trust_vector": { + "instance_identity": TrustworthyInstanceClaim.to_dict(), + "configuration": ApprovedConfigClaim.to_dict(), + "executables": ApprovedRuntimeClaim.to_dict(), + "file_system": ApprovedFilesClaim.to_dict(), + "hardware": GenuineHardwareClaim.to_dict(), + "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), + "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), + "sourced_data": TrustedSourcesClaim.to_dict(), + }, + "status": "affirming", + } + }, + } + parsed_claims = EARClaims.from_dict(data) + assert parsed_claims.to_dict() == data diff --git a/tests/test_trust_claims.py b/tests/test_trust_claims.py new file mode 100644 index 0000000..6594179 --- /dev/null +++ b/tests/test_trust_claims.py @@ -0,0 +1,19 @@ +import pytest + +from src.trust_claims import TrustworthyInstanceClaim + + +@pytest.fixture +def trust_claim(): + # Sample TrustClaim object for testing + return TrustworthyInstanceClaim + + +def test_to_dict(trust_claim): + expected = { + "value": 2, + "tag": "recognized_instance", + "short": "recognized and not compromised", + "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 + } + assert trust_claim.to_dict() == expected diff --git a/tests/test_trust_vector.py b/tests/test_trust_vector.py new file mode 100644 index 0000000..33e5a8c --- /dev/null +++ b/tests/test_trust_vector.py @@ -0,0 +1,93 @@ +import json + +import pytest + +from src.trust_claims import ( + ApprovedFilesClaim, + ApprovedRuntimeClaim, + EncryptedMemoryRuntimeClaim, + GenuineHardwareClaim, + HwKeysEncryptedSecretsClaim, + TrustedSourcesClaim, + TrustworthyInstanceClaim, + UnsafeConfigClaim, +) +from src.trust_vector import TrustVector + + +@pytest.fixture +def sample_trust_vector(): + return TrustVector( + instance_identity=TrustworthyInstanceClaim, + configuration=UnsafeConfigClaim, + executables=ApprovedRuntimeClaim, + file_system=ApprovedFilesClaim, + hardware=GenuineHardwareClaim, + runtime_opaque=EncryptedMemoryRuntimeClaim, + storage_opaque=HwKeysEncryptedSecretsClaim, + sourced_data=TrustedSourcesClaim, + ) + + +def test_trust_vector_to_dict(sample_trust_vector): + expected = { + "instance_identity": TrustworthyInstanceClaim.to_dict(), + "configuration": UnsafeConfigClaim.to_dict(), + "executables": ApprovedRuntimeClaim.to_dict(), + "file_system": ApprovedFilesClaim.to_dict(), + "hardware": GenuineHardwareClaim.to_dict(), + "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), + "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), + "sourced_data": TrustedSourcesClaim.to_dict(), + } + assert sample_trust_vector.to_dict() == expected + + +def test_trust_vector_to_json(sample_trust_vector): + json_str = sample_trust_vector.to_json() + parsed_vector = TrustVector.from_dict(json.loads(json_str)) + assert parsed_vector.to_dict() == sample_trust_vector.to_dict() + + +def test_trust_vector_to_cbor(sample_trust_vector): + expected = { + 0: TrustworthyInstanceClaim.to_dict(), + 1: UnsafeConfigClaim.to_dict(), + 2: ApprovedRuntimeClaim.to_dict(), + 3: ApprovedFilesClaim.to_dict(), + 4: GenuineHardwareClaim.to_dict(), + 5: EncryptedMemoryRuntimeClaim.to_dict(), + 6: HwKeysEncryptedSecretsClaim.to_dict(), + 7: TrustedSourcesClaim.to_dict(), + } + assert sample_trust_vector.to_cbor() == expected + + +def test_trust_vector_from_dict(): + data = { + "instance_identity": TrustworthyInstanceClaim.to_dict(), + "configuration": UnsafeConfigClaim.to_dict(), + "executables": ApprovedRuntimeClaim.to_dict(), + "file_system": ApprovedFilesClaim.to_dict(), + "hardware": GenuineHardwareClaim.to_dict(), + "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), + "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), + "sourced_data": TrustedSourcesClaim.to_dict(), + } + parsed_vector = TrustVector.from_dict(data) + assert parsed_vector.to_dict() == data + + +def test_trust_vector_from_cbor(): + cbor_data = { + 0: TrustworthyInstanceClaim.to_dict(), + 1: UnsafeConfigClaim.to_dict(), + 2: ApprovedRuntimeClaim.to_dict(), + 3: ApprovedFilesClaim.to_dict(), + 4: GenuineHardwareClaim.to_dict(), + 5: EncryptedMemoryRuntimeClaim.to_dict(), + 6: HwKeysEncryptedSecretsClaim.to_dict(), + 7: TrustedSourcesClaim.to_dict(), + } + parsed_vector = TrustVector.from_cbor(cbor_data) + assert parsed_vector.to_dict() == TrustVector.from_cbor(cbor_data).to_dict() # noqa: E501 From 4234f627470f36125fe01ba3ab56ad58338ed80e Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Sun, 16 Mar 2025 04:28:16 +0530 Subject: [PATCH 06/16] Add trust_tier type for status alongside trust_vector Signed-off-by: HarshvMahawar --- src/claims.py | 9 ++-- src/trust_tier.py | 42 ++++++++++++++++ src/validation.py | 79 ++++++++++++++++++++++++++++++ tests/test_claims.py | 23 ++++----- tests/test_trust_tier.py | 34 +++++++++++++ tests/test_trust_vector.py | 19 ++++---- tests/test_validation.py | 98 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 275 insertions(+), 29 deletions(-) create mode 100644 src/trust_tier.py create mode 100644 src/validation.py create mode 100644 tests/test_trust_tier.py create mode 100644 tests/test_validation.py diff --git a/src/claims.py b/src/claims.py index 86eb826..49787c0 100644 --- a/src/claims.py +++ b/src/claims.py @@ -2,11 +2,11 @@ from dataclasses import dataclass, field from typing import Any, Dict +from src.trust_tier import to_trust_tier from src.trust_vector import TrustVector from src.verifier_id import VerifierID -# Represents the EAR Claims set that will be populated @dataclass class EARClaims: profile: str @@ -14,7 +14,6 @@ class EARClaims: verifier_id: VerifierID submods: Dict[str, Dict[str, Any]] = field(default_factory=dict) - # Returns a python dictionary that will be used for serializing to JWT def to_dict(self) -> Dict[str, Any]: return { "eat_profile": self.profile, @@ -23,7 +22,7 @@ def to_dict(self) -> Dict[str, Any]: "submods": { key: { "trust_vector": value["trust_vector"].to_dict(), - "status": value["status"], + "status": value["status"].value, } for key, value in self.submods.items() }, @@ -40,8 +39,8 @@ def from_dict(cls, data: Dict[str, Any]): verifier_id=VerifierID.from_dict(data.get("ear.verifier-id", {})), submods={ key: { - "trust_vector": TrustVector.from_dict(value["trust_vector"]), # noqa: E501 - "status": value["status"], + "trust_vector": TrustVector.from_dict(value["trust_vector"]), + "status": to_trust_tier(value["status"]), } for key, value in data.get("submods", {}).items() }, diff --git a/src/trust_tier.py b/src/trust_tier.py new file mode 100644 index 0000000..2d83e02 --- /dev/null +++ b/src/trust_tier.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass +from typing import Any, Dict + + +@dataclass(frozen=True) +class TrustTier: + value: int + + +def to_trust_tier(value: Any) -> TrustTier: + # Converts an integer or string to a TrustTier instance, defaulting to TrustTierNone on failure + if isinstance(value, int): + return IntToTrustTier.get(value, TrustTierNone) + if isinstance(value, str): + return StringToTrustTier.get(value, TrustTierNone) + raise ValueError(f"Cannot convert {value} (type {type(value)}) to TrustTier") + + +# Defining trust tiers +TrustTierNone = TrustTier(0) +TrustTierAffirming = TrustTier(2) +TrustTierWarning = TrustTier(32) +TrustTierContraindicated = TrustTier(96) + +# Mapping from TrustTier to string representation +TrustTierToString: Dict[TrustTier, str] = { + TrustTierNone: "none", + TrustTierAffirming: "affirming", + TrustTierWarning: "warning", + TrustTierContraindicated: "contraindicated", +} + +# Reverse mapping from string to TrustTier +StringToTrustTier: Dict[str, TrustTier] = {v: k for k, v in TrustTierToString.items()} + +# Mapping from integer value to TrustTier +IntToTrustTier: Dict[int, TrustTier] = { + TrustTierNone.value: TrustTierNone, + TrustTierAffirming.value: TrustTierAffirming, + TrustTierWarning.value: TrustTierWarning, + TrustTierContraindicated.value: TrustTierContraindicated, +} diff --git a/src/validation.py b/src/validation.py new file mode 100644 index 0000000..8c4b984 --- /dev/null +++ b/src/validation.py @@ -0,0 +1,79 @@ +from typing import Any, Dict + +from src.claims import EARClaims +from src.trust_claims import TrustClaim +from src.trust_tier import TrustTier, TrustTierNone, to_trust_tier +from src.trust_vector import TrustVector +from src.verifier_id import VerifierID + + +class EARValidationError(Exception): + # Custom exception for validation errors in EARClaims + pass + + +def validate_trust_claim(trust_claim: TrustClaim): + # Validates a TrustClaim object + if not isinstance(trust_claim.value, int) or not -128 <= trust_claim.value <= 127: + raise EARValidationError( + f"Invalid value in TrustClaim: {trust_claim.value}. Must be in range [-128, 127]" + ) + if not isinstance(trust_claim.tag, str): + raise EARValidationError("TrustClaim tag must be a string") + if not isinstance(trust_claim.short, str): + raise EARValidationError("TrustClaim short description must be a string") + if not isinstance(trust_claim.long, str): + raise EARValidationError("TrustClaim long description must be a string") + + +def validate_trust_vector(trust_vector: TrustVector): + # Validates a TrustVector object + if not isinstance(trust_vector, TrustVector): + raise EARValidationError("Invalid TrustVector object") + + for claim in trust_vector.__dict__.values(): + if claim is not None: + validate_trust_claim(claim) + + +def validate_verifier_id(verifier_id: VerifierID): + # Validates a VerifierID object + if not isinstance(verifier_id, VerifierID): + raise EARValidationError("Invalid VerifierID object") + if not verifier_id.developer or not isinstance(verifier_id.developer, str): + raise EARValidationError("VerifierID developer must be a non-empty string") + if not verifier_id.build or not isinstance(verifier_id.build, str): + raise EARValidationError("VerifierID build must be a non-empty string") + + +def validate_ear_claims(ear_claims: EARClaims): + # Validates an EARClaims object + if not isinstance(ear_claims, EARClaims): + raise EARValidationError("Invalid EARClaims object") + if not isinstance(ear_claims.profile, str) or not ear_claims.profile: + raise EARValidationError("EARClaims profile must be a non-empty string") + if not isinstance(ear_claims.issued_at, int) or ear_claims.issued_at <= 0: + raise EARValidationError("EARClaims issued_at must be a positive integer") + + validate_verifier_id(ear_claims.verifier_id) + + for submod, details in ear_claims.submods.items(): + if ( + not isinstance(details, Dict) + or "trust_vector" not in details + or "status" not in details + ): + raise EARValidationError( + f"Submodule {submod} must contain a valid trust_vector and status" + ) + + validate_trust_vector(details["trust_vector"]) + + +def validate_all(ear_claims: EARClaims): + # Runs all validation checks on the provided EARClaims object + try: + validate_ear_claims(ear_claims) + print("EARClaims validation successful.") + except EARValidationError as e: + print(f"Validation failed: {e}") diff --git a/tests/test_claims.py b/tests/test_claims.py index 5d15f34..2a60a94 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -1,16 +1,13 @@ import pytest from src.claims import EARClaims -from src.trust_claims import ( - ApprovedConfigClaim, - ApprovedFilesClaim, - ApprovedRuntimeClaim, - EncryptedMemoryRuntimeClaim, - GenuineHardwareClaim, - HwKeysEncryptedSecretsClaim, - TrustedSourcesClaim, - TrustworthyInstanceClaim, -) +from src.trust_claims import (ApprovedConfigClaim, ApprovedFilesClaim, + ApprovedRuntimeClaim, + EncryptedMemoryRuntimeClaim, + GenuineHardwareClaim, + HwKeysEncryptedSecretsClaim, TrustedSourcesClaim, + TrustworthyInstanceClaim) +from src.trust_tier import TrustTierAffirming from src.trust_vector import TrustVector from src.verifier_id import VerifierID @@ -33,7 +30,7 @@ def sample_ear_claims(): storage_opaque=HwKeysEncryptedSecretsClaim, sourced_data=TrustedSourcesClaim, ), - "status": "affirming", + "status": TrustTierAffirming, } }, ) @@ -56,7 +53,7 @@ def test_ear_claims_to_dict(sample_ear_claims): "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), "sourced_data": TrustedSourcesClaim.to_dict(), }, - "status": "affirming", + "status": TrustTierAffirming.value, } }, } @@ -86,7 +83,7 @@ def test_ear_claims_from_dict(): "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), "sourced_data": TrustedSourcesClaim.to_dict(), }, - "status": "affirming", + "status": TrustTierAffirming.value, } }, } diff --git a/tests/test_trust_tier.py b/tests/test_trust_tier.py new file mode 100644 index 0000000..4530caf --- /dev/null +++ b/tests/test_trust_tier.py @@ -0,0 +1,34 @@ +import pytest + +from src.trust_tier import (TrustTierAffirming, TrustTierContraindicated, + TrustTierNone, TrustTierWarning, to_trust_tier) + + +def test_to_trust_tier_valid_int(): + assert to_trust_tier(0) == TrustTierNone + assert to_trust_tier(2) == TrustTierAffirming + assert to_trust_tier(32) == TrustTierWarning + assert to_trust_tier(96) == TrustTierContraindicated + + +def test_to_trust_tier_valid_str(): + assert to_trust_tier("none") == TrustTierNone + assert to_trust_tier("affirming") == TrustTierAffirming + assert to_trust_tier("warning") == TrustTierWarning + assert to_trust_tier("contraindicated") == TrustTierContraindicated + + +def test_to_trust_tier_invalid_int(): + assert to_trust_tier(100) == TrustTierNone # Default fallback + + +def test_to_trust_tier_invalid_str(): + assert to_trust_tier("invalid_string") == TrustTierNone # Default fallback + + +def test_to_trust_tier_invalid_type(): + with pytest.raises(ValueError): + to_trust_tier([1, 2, 3]) + + with pytest.raises(ValueError): + to_trust_tier({"tier": "affirming"}) diff --git a/tests/test_trust_vector.py b/tests/test_trust_vector.py index 33e5a8c..a65086a 100644 --- a/tests/test_trust_vector.py +++ b/tests/test_trust_vector.py @@ -2,16 +2,11 @@ import pytest -from src.trust_claims import ( - ApprovedFilesClaim, - ApprovedRuntimeClaim, - EncryptedMemoryRuntimeClaim, - GenuineHardwareClaim, - HwKeysEncryptedSecretsClaim, - TrustedSourcesClaim, - TrustworthyInstanceClaim, - UnsafeConfigClaim, -) +from src.trust_claims import (ApprovedFilesClaim, ApprovedRuntimeClaim, + EncryptedMemoryRuntimeClaim, + GenuineHardwareClaim, + HwKeysEncryptedSecretsClaim, TrustedSourcesClaim, + TrustworthyInstanceClaim, UnsafeConfigClaim) from src.trust_vector import TrustVector @@ -90,4 +85,6 @@ def test_trust_vector_from_cbor(): 7: TrustedSourcesClaim.to_dict(), } parsed_vector = TrustVector.from_cbor(cbor_data) - assert parsed_vector.to_dict() == TrustVector.from_cbor(cbor_data).to_dict() # noqa: E501 + assert ( + parsed_vector.to_dict() == TrustVector.from_cbor(cbor_data).to_dict() + ) # noqa: E501 diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..fde170b --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,98 @@ +import pytest + +from src.claims import EARClaims +from src.trust_claims import (TrustClaim, TrustworthyInstanceClaim, + UnsafeConfigClaim) +from src.trust_vector import TrustVector +from src.validation import (EARValidationError, validate_ear_claims, + validate_trust_claim, validate_trust_vector, + validate_verifier_id) +from src.verifier_id import VerifierID + + +@pytest.fixture +def valid_trust_claim(): + return TrustClaim( + value=2, + tag="approved_config", + short="Approved", + long="Configuration is approved.", + ) + + +@pytest.fixture +def valid_trust_vector(): + return TrustVector( + instance_identity=TrustworthyInstanceClaim, + configuration=UnsafeConfigClaim, + ) + + +@pytest.fixture +def valid_verifier_id(): + return VerifierID(developer="Acme Inc.", build="v1.0.0") + + +@pytest.fixture +def valid_ear_claims(valid_trust_vector, valid_verifier_id): + return EARClaims.from_dict( + { + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.verifier-id": valid_verifier_id.to_dict(), + "submods": { + "submod1": { + "trust_vector": valid_trust_vector.to_dict(), + "status": "affirming", + } + }, + } + ) + + +def test_validate_trust_claim(valid_trust_claim): + # Should not raise an error + validate_trust_claim(valid_trust_claim) + + +def test_validate_trust_claim_invalid(): + with pytest.raises(EARValidationError): + validate_trust_claim( + TrustClaim(value=200, tag="invalid", short="", long="") + ) # Invalid value (>127) + + +def test_validate_trust_vector(valid_trust_vector): + # Should not raise an error + validate_trust_vector(valid_trust_vector) + + +def test_validate_trust_vector_invalid(): + with pytest.raises(EARValidationError): + invalid_vector = TrustVector( + configuration=TrustClaim(value=200, tag="invalid", short="", long="") + ) + validate_trust_vector(invalid_vector) + + +def test_validate_verifier_id(valid_verifier_id): + # Should not raise an error + validate_verifier_id(valid_verifier_id) + + +def test_validate_verifier_id_invalid(): + with pytest.raises(EARValidationError): + validate_verifier_id(VerifierID(developer="", build="")) # Invalid empty fields + + +def test_validate_ear_claims(valid_ear_claims): + # Should not raise an error + validate_ear_claims(valid_ear_claims) + + +def test_validate_ear_claims_invalid(): + with pytest.raises(EARValidationError): + invalid_claims = EARClaims( + profile="", issued_at=-1, verifier_id=VerifierID(developer="", build="") + ) + validate_ear_claims(invalid_claims) From fb1846c2dd75d8ce378d2455e3a5243ca0627674 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Mon, 17 Mar 2025 21:22:01 +0530 Subject: [PATCH 07/16] Modify code according to feedback Signed-off-by: HarshvMahawar --- .flake8 | 2 + .pylintrc | 3 +- src/claims.py | 46 +++++++- src/jwt_handler.py | 230 +++++++++++++++++++++++++++++++++++++ src/trust_claims.py | 148 ++++++++++++++---------- src/trust_tier.py | 40 ++++--- src/trust_vector.py | 2 +- src/validation.py | 34 +++--- src/verifier_id.py | 2 +- tests/test_claims.py | 145 +++++++++++++++-------- tests/test_trust_claims.py | 6 +- tests/test_trust_tier.py | 29 +++-- tests/test_trust_vector.py | 95 +++++++-------- tests/test_validation.py | 23 ++-- tox.ini | 2 +- 15 files changed, 593 insertions(+), 214 deletions(-) create mode 100644 .flake8 create mode 100644 src/jwt_handler.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..1d36346 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 88 \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index ee29c99..73c840f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,8 +1,9 @@ [MESSAGES CONTROL] -disable = C0114, C0115, C0116 ; Disable missing module/class/function docstring warnings +disable = C0114, C0115, C0116, redefined-outer-name, duplicate-code ; Disable missing module/class/function docstring warnings [FORMAT] max-line-length = 88 ; Match Black's default line length +max-attributes=10 [MASTER] ignore = venv ; Ignore virtual environment folder diff --git a/src/claims.py b/src/claims.py index 49787c0..08e8077 100644 --- a/src/claims.py +++ b/src/claims.py @@ -2,18 +2,30 @@ from dataclasses import dataclass, field from typing import Any, Dict +from src.base import BaseJCSerializable from src.trust_tier import to_trust_tier from src.trust_vector import TrustVector from src.verifier_id import VerifierID +# https://datatracker.ietf.org/doc/draft-fv-rats-ear/ @dataclass -class EARClaims: +class AttestationResult(BaseJCSerializable): profile: str issued_at: int verifier_id: VerifierID submods: Dict[str, Dict[str, Any]] = field(default_factory=dict) + # https://www.ietf.org/archive/id/draft-ietf-rats-eat-31.html#section-7.2.4 + JC_map = { + "profile": 265, + "issued_at": 6, + "verifier_id": 1004, + "submods": 266, + "submod.trust_vector": 1001, + "submod.status": 1000, + } + def to_dict(self) -> Dict[str, Any]: return { "eat_profile": self.profile, @@ -28,8 +40,19 @@ def to_dict(self) -> Dict[str, Any]: }, } - def to_json(self) -> str: - return json.dumps(self.to_dict()) + def to_cbor(self) -> Dict[int, Any]: + return { + self.JC_map["profile"]: self.profile, + self.JC_map["issued_at"]: self.issued_at, + self.JC_map["verifier_id"]: self.verifier_id.to_cbor(), + self.JC_map["submods"]: { + key: { + self.JC_map["submod.trust_vector"]: value["trust_vector"].to_cbor(), + self.JC_map["submod.status"]: value["status"].value, + } + for key, value in self.submods.items() + }, + } @classmethod def from_dict(cls, data: Dict[str, Any]): @@ -49,3 +72,20 @@ def from_dict(cls, data: Dict[str, Any]): @classmethod def from_json(cls, json_str: str): return cls.from_dict(json.loads(json_str)) + + @classmethod + def from_cbor(cls, data: Dict[int, Any]): + return cls( + profile=data.get(cls.JC_map["profile"], ""), + issued_at=data.get(cls.JC_map["issued_at"], 0), + verifier_id=VerifierID.from_cbor(data.get(cls.JC_map["verifier_id"], {})), + submods={ + key: { + "trust_vector": TrustVector.from_cbor( + value.get(cls.JC_map["submod.trust_vector"], {}) + ), + "status": to_trust_tier(value.get(cls.JC_map["submod.status"], 0)), + } + for key, value in data.get(cls.JC_map["submods"], {}).items() + }, + ) diff --git a/src/jwt_handler.py b/src/jwt_handler.py new file mode 100644 index 0000000..9775548 --- /dev/null +++ b/src/jwt_handler.py @@ -0,0 +1,230 @@ +import secrets +from datetime import datetime, timedelta +from typing import Any, Dict + +from jose import jwt # type: ignore # pylint: disable=import-error + +from src.claims import AttestationResult + +# Default cryptographic settings +DEFAULT_ALGORITHM = "HS256" +DEFAULT_EXPIRATION_MINUTES = 60 + + +def generate_secret_key() -> str: + # Generates a secure random secret key for JWT signing. + return secrets.token_hex(32) + + +def sign_ear_claims( + ear_claims: AttestationResult, + secret_key: str, + algorithm: str = DEFAULT_ALGORITHM, + expiration_minutes: int = DEFAULT_EXPIRATION_MINUTES, +) -> str: + + # Signs an AttestationResult object and returns a JWT. + payload = ear_claims.to_dict() + payload["exp"] = int( + datetime.timestamp(datetime.now() + timedelta(minutes=expiration_minutes)) + ) + return jwt.encode(payload, secret_key, algorithm=algorithm) + + +def verify_ear_claims( + token: str, secret_key: str, algorithm: str = DEFAULT_ALGORITHM +) -> Dict[str, Any]: + + # Verifies a JWT and returns the decoded AttestationResult payload. + try: + return jwt.decode(token, secret_key, algorithms=[algorithm]) + except Exception as exc: + raise ValueError(f"JWT decoding failed: {exc}") from exc + + +def decode_ear_claims( + token: str, secret_key: str, algorithm: str = DEFAULT_ALGORITHM +) -> AttestationResult: + + # Decodes and reconstructs an AttestationResult object from a JWT. + payload = verify_ear_claims(token, secret_key, algorithm) + return AttestationResult.from_dict(payload) + + +# EXAMPLE USAGE + +# Generate a secret key +# secret_key = generate_secret_key() +# print(f"Generated Secret Key: {secret_key}") + +# # Create an AttestationResult object +# attestation_result = AttestationResult( +# profile="test_profile", +# issued_at=int(datetime.timestamp(datetime.now())), +# verifier_id=VerifierID(developer="Acme Inc.", build="v1"), +# submods={ +# "submod1": { +# "trust_vector": TrustVector(instance_identity=UNRECOGNIZED_INSTANCE_CLAIM), # noqa: E501 # pylint: disable=line-too-long +# "status": TRUST_TIER_AFFIRMING, +# }, +# "submod2": { +# "trust_vector": TrustVector(instance_identity=TRUSTWORTHY_INSTANCE_CLAIM), # noqa: E501 # pylint: disable=line-too-long +# "status": TRUST_TIER_CONTRAINDICATED, +# }, +# }, +# ) + +# # Print the original AttestationResult object +# print("\n Original AttestationResult Dictionary - JSON:") +# print(json.dumps(attestation_result.to_dict(), indent=4)) + +# print("\n Original AttestationResult Dictionary - CBOR:") +# print(json.dumps(attestation_result.to_cbor(), indent=4)) + +# # Sign the AttestationResult and generate a JWT +# jwt_token = sign_ear_claims(attestation_result, secret_key) +# print("\n Signed JWT Token:") +# print(jwt_token) + +# # Decode and verify the JWT +# decoded_claims = decode_ear_claims(jwt_token, secret_key) +# print("\n Decoded AttestationResult Dictionary:") +# print(json.dumps(decoded_claims.to_dict(), indent=4)) + + +# OUTPUT + +# Generated Secret Key: dadb1756080cabf4c0...d097a705b39c259be3d3 + +# Original AttestationResult Dictionary - JSON: +# { +# "eat_profile": "test_profile", +# "iat": 1742225266, +# "ear.verifier-id": { +# "developer": "Acme Inc.", +# "build": "v1" +# }, +# "submods": { +# "submod1": { +# "trust_vector": { +# "instance_identity": { +# "value": 97, +# "tag": "unrecognized_instance", +# "short": "not recognized", +# "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." # noqa: E501 # pylint: disable=line-too-long +# }, +# "configuration": null, +# "executables": null, +# "file_system": null, +# "hardware": null, +# "runtime_opaque": null, +# "storage_opaque": null, +# "sourced_data": null +# }, +# "status": 2 +# }, +# "submod2": { +# "trust_vector": { +# "instance_identity": { +# "value": 2, +# "tag": "recognized_instance", +# "short": "recognized and not compromised", +# "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." # noqa: E501 # pylint: disable=line-too-long +# }, +# "configuration": null, +# "executables": null, +# "file_system": null, +# "hardware": null, +# "runtime_opaque": null, +# "storage_opaque": null, +# "sourced_data": null +# }, +# "status": 96 +# } +# } +# } + +# Original AttestationResult Dictionary - CBOR: +# { +# "265": "test_profile", +# "6": 1742225266, +# "1004": { +# "0": "Acme Inc.", +# "1": "v1" +# }, +# "266": { +# "submod1": { +# "1001": { +# "0": { +# "value": 97, +# "tag": "unrecognized_instance", +# "short": "not recognized", +# "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." # noqa: E501 # pylint: disable=line-too-long +# } +# }, +# "1000": 2 +# }, +# "submod2": { +# "1001": { +# "0": { +# "value": 2, +# "tag": "recognized_instance", +# "short": "recognized and not compromised", +# "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." # noqa: E501 # pylint: disable=line-too-long +# } +# }, +# "1000": 96 +# } +# } +# } + +# Signed JWT Token: +# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Nq0aWU0BDg + +# Decoded AttestationResult Dictionary: +# { +# "eat_profile": "test_profile", +# "iat": 1742225266, +# "ear.verifier-id": { +# "developer": "Acme Inc.", +# "build": "v1" +# }, +# "submods": { +# "submod1": { +# "trust_vector": { +# "instance_identity": { +# "value": 97, +# "tag": "unrecognized_instance", +# "short": "not recognized", +# "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." # noqa: E501 # pylint: disable=line-too-long +# }, +# "configuration": null, +# "executables": null, +# "file_system": null, +# "hardware": null, +# "runtime_opaque": null, +# "storage_opaque": null, +# "sourced_data": null +# }, +# "status": 2 +# }, +# "submod2": { +# "trust_vector": { +# "instance_identity": { +# "value": 2, +# "tag": "recognized_instance", +# "short": "recognized and not compromised", +# "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." # noqa: E501 # pylint: disable=line-too-long +# }, +# "configuration": null, +# "executables": null, +# "file_system": null, +# "hardware": null, +# "runtime_opaque": null, +# "storage_opaque": null, +# "sourced_data": null +# }, +# "status": 96 +# } +# } +# } diff --git a/src/trust_claims.py b/src/trust_claims.py index 0f1e040..80086e7 100644 --- a/src/trust_claims.py +++ b/src/trust_claims.py @@ -2,6 +2,7 @@ from typing import Any, Dict +# https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-2.3 @dataclass class TrustClaim: value: int # must be in range -128 to 127 @@ -14,209 +15,240 @@ def to_dict(self) -> Dict[str, Any]: # General -VerifierMalfunctionClaim = TrustClaim( +VERIFIER_MALFUNCTION_CLAIM = TrustClaim( value=-1, tag="verifier_malfunction", short="verifier malfunction", - long="A verifier malfunction occurred during the Verifier's appraisal processing.", # noqa: E501 + long="A verifier malfunction occurred during the Verifier's appraisal processing.", # noqa: E501 # pylint: disable=line-too-long ) -NoClaim = TrustClaim( + +NO_CLAIM = TrustClaim( value=0, tag="no_claim", short="no claim being made", long="The Evidence received is insufficient to make a conclusion.", ) -UnexpectedEvidenceClaim = TrustClaim( + +UNEXPECTED_EVIDENCE_CLAIM = TrustClaim( value=1, tag="unexected_evidence", short="unexpected evidence", - long="The Evidence received contains unexpected elements which the Verifier is unable to parse.", # noqa: E501 + long="The Evidence received contains unexpected elements which the Verifier is unable to parse.", # noqa: E501 # pylint: disable=line-too-long ) -CryptoValidationFailedClaim = TrustClaim( + +CRYPTO_VALIDATION_FAILED_CLAIM = TrustClaim( value=99, tag="crypto_failed", short="cryptographic validation failed", long="Cryptographic validation of the Evidence has failed.", ) + # Instance Identity -TrustworthyInstanceClaim = TrustClaim( +TRUSTWORTHY_INSTANCE_CLAIM = TrustClaim( value=2, tag="recognized_instance", short="recognized and not compromised", - long="The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 + long="The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 # pylint: disable=line-too-long ) -UntrustworthyInstanceClaim = TrustClaim( + +UNTRUSTWORTHY_INSTANCE_CLAIM = TrustClaim( value=96, tag="untrustworthy_instance", short="recognized but not trustworthy", - long="The Attesting Environment is recognized, but its unique private key indicates a device which is not trustworthy.", # noqa: E501 + long="The Attesting Environment is recognized, but its unique private key indicates a device which is not trustworthy.", # noqa: E501 # pylint: disable=line-too-long ) -UnrecognizedInstanceClaim = TrustClaim( + +UNRECOGNIZED_INSTANCE_CLAIM = TrustClaim( value=97, tag="unrecognized_instance", short="not recognized", - long="The Attesting Environment is not recognized; however the Verifier believes it should be.", # noqa: E501 + long="The Attesting Environment is not recognized; however the Verifier believes it should be.", # noqa: E501 # pylint: disable=line-too-long ) + # Config -ApprovedConfigClaim = TrustClaim( +APPROVED_CONFIG_CLAIM = TrustClaim( value=2, tag="approved_config", short="all recognized and approved", long="The configuration is a known and approved config.", ) -NoConfigVulnsClaim = TrustClaim( + +NO_CONFIG_VULNS_CLAIM = TrustClaim( value=3, tag="safe_config", short="no known vulnerabilities", - long="The configuration includes or exposes no known vulnerabilities", + long="The configuration includes or exposes no known vulnerabilities", # noqa: E501 # pylint: disable=line-too-long ) -UnsafeConfigClaim = TrustClaim( + +UNSAFE_CONFIG_CLAIM = TrustClaim( value=32, tag="unsafe_config", short="known vulnerabilities", long="The configuration includes or exposes known vulnerabilities.", ) -UnsupportableConfigClaim = TrustClaim( + +UNSUPPORTABLE_CONFIG_CLAIM = TrustClaim( value=96, tag="unsupportable_config", short="unacceptable security vulnerabilities", - long="The configuration is unsupportable as it exposes unacceptable security vulnerabilities", # noqa: E501 + long="The configuration is unsupportable as it exposes unacceptable security vulnerabilities", # noqa: E501 # pylint: disable=line-too-long ) + # Executables & Runtime -ApprovedRuntimeClaim = TrustClaim( +APPROVED_RUNTIME_CLAIM = TrustClaim( value=2, tag="approved_rt", short="recognized and approved boot- and run-time", - long="Only a recognized genuine set of approved executables, scripts, files, and/or objects have been loaded during and after the boot process.", # noqa: E501 + long="Only a recognized genuine set of approved executables, scripts, files, and/or objects have been loaded during and after the boot process.", # noqa: E501 # pylint: disable=line-too-long ) -ApprovedBootClaim = TrustClaim( + +APPROVED_BOOT_CLAIM = TrustClaim( value=3, tag="approved_boot", short="recognized and approved boot-time", - long="Only a recognized genuine set of approved executables have been loaded during the boot process.", # noqa: E501 + long="Only a recognized genuine set of approved executables have been loaded during the boot process.", # noqa: E501 # pylint: disable=line-too-long ) -UnsafeRuntimeClaim = TrustClaim( + +UNSAFE_RUNTIME_CLAIM = TrustClaim( value=32, tag="unsafe_rt", short="recognized but known bugs or vulnerabilities", - long="Only a recognized genuine set of executables, scripts, files, and/or objects have been loaded. However the Verifier cannot vouch for a subset of these due to known bugs or other known vulnerabilities.", # noqa: E501 + long="Only a recognized genuine set of executables, scripts, files, and/or objects have been loaded. However the Verifier cannot vouch for a subset of these due to known bugs or other known vulnerabilities.", # noqa: E501 # pylint: disable=line-too-long ) -UnrecognizedRuntimeClaim = TrustClaim( + +UNRECOGNIZED_RUNTIME_CLAIM = TrustClaim( value=33, tag="unrecognized_rt", short="unrecognized run-time", - long="Runtime memory includes executables, scripts, files, and/or objects which are not recognized.", # noqa: E501 + long="Runtime memory includes executables, scripts, files, and/or objects which are not recognized.", # noqa: E501 # pylint: disable=line-too-long ) -ContraindicatedRuntimeClaim = TrustClaim( + +CONTRAINDICATED_RUNTIME_CLAIM = TrustClaim( value=96, tag="contraindicated_rt", short="contraindicated run-time", - long="Runtime memory includes executables, scripts, files, and/or object which are contraindicated.", # noqa: E501 + long="Runtime memory includes executables, scripts, files, and/or object which are contraindicated.", # noqa: E501 # pylint: disable=line-too-long ) + # File System -ApprovedFilesClaim = TrustClaim( +APPROVED_FILES_CLAIM = TrustClaim( value=2, tag="approved_fs", short="all recognized and approved", long="Only a recognized set of approved files are found.", ) -UnrecognizedFilesClaim = TrustClaim( + +UNRECOGNIZED_FILES_CLAIM = TrustClaim( value=32, tag="unrecognized_fs", short="unrecognized item(s) found", - long="The file system includes unrecognized executables, scripts, or files.", # noqa: E501 + long="The file system includes unrecognized executables, scripts, or files.", # noqa: E501 # pylint: disable=line-too-long ) -ContraindicatedFilesClaim = TrustClaim( + +CONTRAINDICATED_FILES_CLAIM = TrustClaim( value=96, tag="contraindicated_fs", short="contraindicated item(s) found", - long="The file system includes contraindicated executables, scripts, or files.", # noqa: E501 + long="The file system includes contraindicated executables, scripts, or files.", # noqa: E501 # pylint: disable=line-too-long ) + # Hardware -GenuineHardwareClaim = TrustClaim( +GENUINE_HARDWARE_CLAIM = TrustClaim( value=2, tag="genuine_hw", short="genuine", - long="An Attester has passed its hardware and/or firmware verifications needed to demonstrate that these are genuine/supported.", # noqa: E501 + long="An Attester has passed its hardware and/or firmware verifications needed to demonstrate that these are genuine/supported.", # noqa: E501 # pylint: disable=line-too-long ) -UnsafeHardwareClaim = TrustClaim( + +UNSAFE_HARDWARE_CLAIM = TrustClaim( value=32, tag="unsafe_hw", short="genuine but known bugs or vulnerabilities", - long="An Attester contains only genuine/supported hardware and/or firmware, but there are known security vulnerabilities.", # noqa: E501 + long="An Attester contains only genuine/supported hardware and/or firmware, but there are known security vulnerabilities.", # noqa: E501 # pylint: disable=line-too-long ) -ContraindicatedHardwareClaim = TrustClaim( + +CONTRAINDICATED_HARDWARE_CLAIM = TrustClaim( value=96, tag="contraindicated_hw", short="genuine but contraindicated", - long="Attester hardware and/or firmware is recognized, but its trustworthiness is contraindicated.", # noqa: E501 + long="Attester hardware and/or firmware is recognized, but its trustworthiness is contraindicated.", # noqa: E501 # pylint: disable=line-too-long ) -UnrecognizedHardwareClaim = TrustClaim( + +UNRECOGNIZED_HARDWARE_CLAIM = TrustClaim( value=97, tag="unrecognized_hw", short="unrecognized", - long="A Verifier does not recognize an Attester's hardware or firmware, but it should be recognized.", # noqa: E501 + long="A Verifier does not recognize an Attester's hardware or firmware, but it should be recognized.", # noqa: E501 # pylint: disable=line-too-long ) + # Opaque Runtime -EncryptedMemoryRuntimeClaim = TrustClaim( +ENCRYPTED_MEMORY_RUNTIME_CLAIM = TrustClaim( value=2, tag="encrypted_rt", short="memory encryption", - long="the Attester's executing Target Environment and Attesting Environments are encrypted and within Trusted Execution Environment(s) opaque to the operating system, virtual machine manager, and peer applications.", # noqa: E501 + long="the Attester's executing Target Environment and Attesting Environments are encrypted and within Trusted Execution Environment(s) opaque to the operating system, virtual machine manager, and peer applications.", # noqa: E501 # pylint: disable=line-too-long ) -IsolatedMemoryRuntimeClaim = TrustClaim( + +ISOLATED_MEMORY_RUNTIME_CLAIM = TrustClaim( value=32, tag="isolated_rt", short="memory isolation", - long="the Attester's executing Target Environment and Attesting Environments are inaccessible from any other parallel application or Guest VM running on the Attester's physical device.", # noqa: E501 + long="the Attester's executing Target Environment and Attesting Environments are inaccessible from any other parallel application or Guest VM running on the Attester's physical device.", # noqa: E501 # pylint: disable=line-too-long ) -VisibleMemoryRuntimeClaim = TrustClaim( + +VISIBLE_MEMORY_RUNTIME_CLAIM = TrustClaim( value=96, tag="visible_rt", short="visible", - long="The Verifier has concluded that in memory objects are unacceptably visible within the physical host that supports the Attester.", # noqa: E501 + long="The Verifier has concluded that in memory objects are unacceptably visible within the physical host that supports the Attester.", # noqa: E501 # pylint: disable=line-too-long ) + # Opaque Storage -HwKeysEncryptedSecretsClaim = TrustClaim( +HW_KEYS_ENCRYPTED_SECRETS_CLAIM = TrustClaim( value=2, tag="hw_encrypted_secrets", short="encrypted secrets with HW-backed keys", - long="the Attester encrypts all secrets in persistent storage via using keys which are never visible outside an HSM or the Trusted Execution Environment hardware.", # noqa: E501 + long="the Attester encrypts all secrets in persistent storage via using keys which are never visible outside an HSM or the Trusted Execution Environment hardware.", # noqa: E501 # pylint: disable=line-too-long ) -SwKeysEncryptedSecretsClaim = TrustClaim( + +SW_KEYS_ENCRYPTED_SECRETS_CLAIM = TrustClaim( value=32, tag="sw_encrypted_secrets", short="encrypted secrets with non HW-backed keys", - long="the Attester encrypts all persistently stored secrets, but without using hardware backed keys.", # noqa: E501 + long="the Attester encrypts all persistently stored secrets, but without using hardware backed keys.", # noqa: E501 # pylint: disable=line-too-long ) -UnencryptedSecretsClaim = TrustClaim( + +UNENCRYPTED_SECRETS_CLAIM = TrustClaim( value=96, tag="unencrypted_secrets", short="unencrypted secrets", - long="There are persistent secrets which are stored unencrypted in an Attester.", # noqa: E501 + long="There are persistent secrets which are stored unencrypted in an Attester.", ) + # Sourced Data -TrustedSourcesClaim = TrustClaim( +TRUSTED_SOURCES_CLAIM = TrustClaim( value=2, tag="trusted_sources", short="from attesters in the affirming tier", - long='All essential Attester source data objects have been provided by other Attester(s) whose most recent appraisal(s) had both no Trustworthiness Claims of "0" where the current Trustworthiness Claim is "Affirming", as well as no "Warning" or "Contraindicated" Trustworthiness Claims.', # noqa: E501 + long='All essential Attester source data objects have been provided by other Attester(s) whose most recent appraisal(s) had both no Trustworthiness Claims of "0" where the current Trustworthiness Claim is "Affirming", as well as no "Warning" or "Contraindicated" Trustworthiness Claims.', # noqa: E501 # pylint: disable=line-too-long ) -UntrustedSourcesClaim = TrustClaim( + +UNTRUSTED_SOURCES_CLAIM = TrustClaim( value=32, tag="untrusted_sources", short="from unattested sources or attesters in the warning tier", - long='Attester source data objects come from unattested sources, or attested sources with "Warning" type Trustworthiness Claims', # noqa: E501 + long='Attester source data objects come from unattested sources, or attested sources with "Warning" type Trustworthiness Claims', # noqa: E501 # pylint: disable=line-too-long ) -ContraindicatedSourcesClaim = TrustClaim( + +CONTRAINDICATED_SOURCES_CLAIM = TrustClaim( value=96, tag="contraindicated_sources", short="from attesters in the contraindicated tier", diff --git a/src/trust_tier.py b/src/trust_tier.py index 2d83e02..2f186f7 100644 --- a/src/trust_tier.py +++ b/src/trust_tier.py @@ -2,41 +2,45 @@ from typing import Any, Dict +# https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.2 @dataclass(frozen=True) class TrustTier: value: int def to_trust_tier(value: Any) -> TrustTier: - # Converts an integer or string to a TrustTier instance, defaulting to TrustTierNone on failure + # Converts an integer or string to a TrustTier instance, + # defaulting to TrustTierNone on failure if isinstance(value, int): - return IntToTrustTier.get(value, TrustTierNone) + return INT_TO_TRUST_TIER.get(value, TRUST_TIER_NONE) if isinstance(value, str): - return StringToTrustTier.get(value, TrustTierNone) + return STRING_TO_TRUST_TIER.get(value, TRUST_TIER_NONE) raise ValueError(f"Cannot convert {value} (type {type(value)}) to TrustTier") # Defining trust tiers -TrustTierNone = TrustTier(0) -TrustTierAffirming = TrustTier(2) -TrustTierWarning = TrustTier(32) -TrustTierContraindicated = TrustTier(96) +TRUST_TIER_NONE: TrustTier = TrustTier(0) +TRUST_TIER_AFFIRMING: TrustTier = TrustTier(2) +TRUST_TIER_WARNING: TrustTier = TrustTier(32) +TRUST_TIER_CONTRAINDICATED: TrustTier = TrustTier(96) # Mapping from TrustTier to string representation -TrustTierToString: Dict[TrustTier, str] = { - TrustTierNone: "none", - TrustTierAffirming: "affirming", - TrustTierWarning: "warning", - TrustTierContraindicated: "contraindicated", +TRUST_TIER_TO_STRING: Dict[TrustTier, str] = { + TRUST_TIER_NONE: "none", + TRUST_TIER_AFFIRMING: "affirming", + TRUST_TIER_WARNING: "warning", + TRUST_TIER_CONTRAINDICATED: "contraindicated", } # Reverse mapping from string to TrustTier -StringToTrustTier: Dict[str, TrustTier] = {v: k for k, v in TrustTierToString.items()} +STRING_TO_TRUST_TIER: Dict[str, TrustTier] = { + v: k for k, v in TRUST_TIER_TO_STRING.items() +} # Mapping from integer value to TrustTier -IntToTrustTier: Dict[int, TrustTier] = { - TrustTierNone.value: TrustTierNone, - TrustTierAffirming.value: TrustTierAffirming, - TrustTierWarning.value: TrustTierWarning, - TrustTierContraindicated.value: TrustTierContraindicated, +INT_TO_TRUST_TIER: Dict[int, TrustTier] = { + TRUST_TIER_NONE.value: TRUST_TIER_NONE, + TRUST_TIER_AFFIRMING.value: TRUST_TIER_AFFIRMING, + TRUST_TIER_WARNING.value: TRUST_TIER_WARNING, + TRUST_TIER_CONTRAINDICATED.value: TRUST_TIER_CONTRAINDICATED, } diff --git a/src/trust_vector.py b/src/trust_vector.py index 9c80972..bd87239 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -5,6 +5,7 @@ from src.trust_claims import TrustClaim +# https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.1 # TrustVector class to represent the trustworthiness vector @dataclass class TrustVector(BaseJCSerializable): @@ -17,7 +18,6 @@ class TrustVector(BaseJCSerializable): storage_opaque: Optional[TrustClaim] = None sourced_data: Optional[TrustClaim] = None - # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.1 JC_map = { "instance_identity": 0, "configuration": 1, diff --git a/src/validation.py b/src/validation.py index 8c4b984..afed6b6 100644 --- a/src/validation.py +++ b/src/validation.py @@ -1,14 +1,13 @@ -from typing import Any, Dict +from typing import Dict -from src.claims import EARClaims +from src.claims import AttestationResult from src.trust_claims import TrustClaim -from src.trust_tier import TrustTier, TrustTierNone, to_trust_tier from src.trust_vector import TrustVector from src.verifier_id import VerifierID class EARValidationError(Exception): - # Custom exception for validation errors in EARClaims + # Custom exception for validation errors in AttestationResult pass @@ -16,7 +15,8 @@ def validate_trust_claim(trust_claim: TrustClaim): # Validates a TrustClaim object if not isinstance(trust_claim.value, int) or not -128 <= trust_claim.value <= 127: raise EARValidationError( - f"Invalid value in TrustClaim: {trust_claim.value}. Must be in range [-128, 127]" + f"""Invalid value in TrustClaim: {trust_claim.value}. + Must be in range [-128, 127]""" ) if not isinstance(trust_claim.tag, str): raise EARValidationError("TrustClaim tag must be a string") @@ -46,14 +46,16 @@ def validate_verifier_id(verifier_id: VerifierID): raise EARValidationError("VerifierID build must be a non-empty string") -def validate_ear_claims(ear_claims: EARClaims): - # Validates an EARClaims object - if not isinstance(ear_claims, EARClaims): - raise EARValidationError("Invalid EARClaims object") +def validate_ear_claims(ear_claims: AttestationResult): + # Validates an AttestationResult object + if not isinstance(ear_claims, AttestationResult): + raise EARValidationError("Invalid AttestationResult object") if not isinstance(ear_claims.profile, str) or not ear_claims.profile: - raise EARValidationError("EARClaims profile must be a non-empty string") + raise EARValidationError("AttestationResult profile must be a non-empty string") if not isinstance(ear_claims.issued_at, int) or ear_claims.issued_at <= 0: - raise EARValidationError("EARClaims issued_at must be a positive integer") + raise EARValidationError( + "AttestationResult issued_at must be a positive integer" + ) validate_verifier_id(ear_claims.verifier_id) @@ -70,10 +72,10 @@ def validate_ear_claims(ear_claims: EARClaims): validate_trust_vector(details["trust_vector"]) -def validate_all(ear_claims: EARClaims): - # Runs all validation checks on the provided EARClaims object +def validate_all(ear_claims: AttestationResult): + # Runs all validation checks on the provided AttestationResult object try: validate_ear_claims(ear_claims) - print("EARClaims validation successful.") - except EARValidationError as e: - print(f"Validation failed: {e}") + print("AttestationResult validation successful.") + except EARValidationError as error_message: + print(f"Validation failed: {error_message}") diff --git a/src/verifier_id.py b/src/verifier_id.py index f3d9ca3..e035cc4 100644 --- a/src/verifier_id.py +++ b/src/verifier_id.py @@ -4,11 +4,11 @@ from src.base import BaseJCSerializable +# https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.3 @dataclass class VerifierID(BaseJCSerializable): developer: str build: str - # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.3 JC_map = { "developer": 0, # JC<"developer", 0> "build": 1, # JC<"build", 1> diff --git a/tests/test_claims.py b/tests/test_claims.py index 2a60a94..5452ab0 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -1,42 +1,46 @@ import pytest -from src.claims import EARClaims -from src.trust_claims import (ApprovedConfigClaim, ApprovedFilesClaim, - ApprovedRuntimeClaim, - EncryptedMemoryRuntimeClaim, - GenuineHardwareClaim, - HwKeysEncryptedSecretsClaim, TrustedSourcesClaim, - TrustworthyInstanceClaim) -from src.trust_tier import TrustTierAffirming +from src.claims import AttestationResult +from src.trust_claims import ( + APPROVED_CONFIG_CLAIM, + APPROVED_FILES_CLAIM, + APPROVED_RUNTIME_CLAIM, + ENCRYPTED_MEMORY_RUNTIME_CLAIM, + GENUINE_HARDWARE_CLAIM, + HW_KEYS_ENCRYPTED_SECRETS_CLAIM, + TRUSTED_SOURCES_CLAIM, + TRUSTWORTHY_INSTANCE_CLAIM, +) +from src.trust_tier import TRUST_TIER_AFFIRMING from src.trust_vector import TrustVector from src.verifier_id import VerifierID @pytest.fixture -def sample_ear_claims(): - return EARClaims( +def sample_attestation_result(): + return AttestationResult( profile="test_profile", issued_at=1234567890, verifier_id=VerifierID(developer="Acme Inc.", build="v1"), submods={ "submod1": { "trust_vector": TrustVector( - instance_identity=TrustworthyInstanceClaim, - configuration=ApprovedConfigClaim, - executables=ApprovedRuntimeClaim, - file_system=ApprovedFilesClaim, - hardware=GenuineHardwareClaim, - runtime_opaque=EncryptedMemoryRuntimeClaim, - storage_opaque=HwKeysEncryptedSecretsClaim, - sourced_data=TrustedSourcesClaim, + instance_identity=TRUSTWORTHY_INSTANCE_CLAIM, + configuration=APPROVED_CONFIG_CLAIM, + executables=APPROVED_RUNTIME_CLAIM, + file_system=APPROVED_FILES_CLAIM, + hardware=GENUINE_HARDWARE_CLAIM, + runtime_opaque=ENCRYPTED_MEMORY_RUNTIME_CLAIM, + storage_opaque=HW_KEYS_ENCRYPTED_SECRETS_CLAIM, + sourced_data=TRUSTED_SOURCES_CLAIM, ), - "status": TrustTierAffirming, + "status": TRUST_TIER_AFFIRMING, } }, ) -def test_ear_claims_to_dict(sample_ear_claims): +def test_attestation_result_to_dict(sample_attestation_result): expected = { "eat_profile": "test_profile", "iat": 1234567890, @@ -44,29 +48,29 @@ def test_ear_claims_to_dict(sample_ear_claims): "submods": { "submod1": { "trust_vector": { - "instance_identity": TrustworthyInstanceClaim.to_dict(), - "configuration": ApprovedConfigClaim.to_dict(), - "executables": ApprovedRuntimeClaim.to_dict(), - "file_system": ApprovedFilesClaim.to_dict(), - "hardware": GenuineHardwareClaim.to_dict(), - "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), - "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), - "sourced_data": TrustedSourcesClaim.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + "configuration": APPROVED_CONFIG_CLAIM.to_dict(), + "executables": APPROVED_RUNTIME_CLAIM.to_dict(), + "file_system": APPROVED_FILES_CLAIM.to_dict(), + "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), }, - "status": TrustTierAffirming.value, + "status": TRUST_TIER_AFFIRMING.value, } }, } - assert sample_ear_claims.to_dict() == expected + assert sample_attestation_result.to_dict() == expected -def test_ear_claims_to_json(sample_ear_claims): - json_str = sample_ear_claims.to_json() - parsed_claims = EARClaims.from_json(json_str) - assert parsed_claims.to_dict() == sample_ear_claims.to_dict() +def test_attestation_result_to_json(sample_attestation_result): + json_str = sample_attestation_result.to_json() + parsed_claims = AttestationResult.from_json(json_str) + assert parsed_claims.to_dict() == sample_attestation_result.to_dict() -def test_ear_claims_from_dict(): +def test_attestation_result_from_dict(): data = { "eat_profile": "test_profile", "iat": 1234567890, @@ -74,18 +78,69 @@ def test_ear_claims_from_dict(): "submods": { "submod1": { "trust_vector": { - "instance_identity": TrustworthyInstanceClaim.to_dict(), - "configuration": ApprovedConfigClaim.to_dict(), - "executables": ApprovedRuntimeClaim.to_dict(), - "file_system": ApprovedFilesClaim.to_dict(), - "hardware": GenuineHardwareClaim.to_dict(), - "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), - "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), - "sourced_data": TrustedSourcesClaim.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + "configuration": APPROVED_CONFIG_CLAIM.to_dict(), + "executables": APPROVED_RUNTIME_CLAIM.to_dict(), + "file_system": APPROVED_FILES_CLAIM.to_dict(), + "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), }, - "status": TrustTierAffirming.value, + "status": TRUST_TIER_AFFIRMING.value, } }, } - parsed_claims = EARClaims.from_dict(data) + parsed_claims = AttestationResult.from_dict(data) assert parsed_claims.to_dict() == data + + +def test_attestation_result_to_cbor(sample_attestation_result): + cbor_data = sample_attestation_result.to_cbor() + expected_cbor = { + 265: "test_profile", + 6: 1234567890, + 1004: {0: "Acme Inc.", 1: "v1"}, + 266: { + "submod1": { + 1001: { + 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + 1: APPROVED_CONFIG_CLAIM.to_dict(), + 2: APPROVED_RUNTIME_CLAIM.to_dict(), + 3: APPROVED_FILES_CLAIM.to_dict(), + 4: GENUINE_HARDWARE_CLAIM.to_dict(), + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + 7: TRUSTED_SOURCES_CLAIM.to_dict(), + }, + 1000: TRUST_TIER_AFFIRMING.value, + } + }, + } + assert cbor_data == expected_cbor + + +def test_attestation_result_from_cbor(): + cbor_data = { + 265: "test_profile", + 6: 1234567890, + 1004: {0: "Acme Inc.", 1: "v1"}, + 266: { + "submod1": { + 1001: { + 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + 1: APPROVED_CONFIG_CLAIM.to_dict(), + 2: APPROVED_RUNTIME_CLAIM.to_dict(), + 3: APPROVED_FILES_CLAIM.to_dict(), + 4: GENUINE_HARDWARE_CLAIM.to_dict(), + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + 7: TRUSTED_SOURCES_CLAIM.to_dict(), + }, + 1000: TRUST_TIER_AFFIRMING.value, + } + }, + } + + parsed_claims = AttestationResult.from_cbor(cbor_data) + assert parsed_claims.to_cbor() == cbor_data diff --git a/tests/test_trust_claims.py b/tests/test_trust_claims.py index 6594179..f5e134f 100644 --- a/tests/test_trust_claims.py +++ b/tests/test_trust_claims.py @@ -1,12 +1,12 @@ import pytest -from src.trust_claims import TrustworthyInstanceClaim +from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM @pytest.fixture def trust_claim(): # Sample TrustClaim object for testing - return TrustworthyInstanceClaim + return TRUSTWORTHY_INSTANCE_CLAIM def test_to_dict(trust_claim): @@ -14,6 +14,6 @@ def test_to_dict(trust_claim): "value": 2, "tag": "recognized_instance", "short": "recognized and not compromised", - "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 + "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 # pylint: disable=line-too-long } assert trust_claim.to_dict() == expected diff --git a/tests/test_trust_tier.py b/tests/test_trust_tier.py index 4530caf..69287e3 100644 --- a/tests/test_trust_tier.py +++ b/tests/test_trust_tier.py @@ -1,29 +1,34 @@ import pytest -from src.trust_tier import (TrustTierAffirming, TrustTierContraindicated, - TrustTierNone, TrustTierWarning, to_trust_tier) +from src.trust_tier import ( + TRUST_TIER_AFFIRMING, + TRUST_TIER_CONTRAINDICATED, + TRUST_TIER_NONE, + TRUST_TIER_WARNING, + to_trust_tier, +) def test_to_trust_tier_valid_int(): - assert to_trust_tier(0) == TrustTierNone - assert to_trust_tier(2) == TrustTierAffirming - assert to_trust_tier(32) == TrustTierWarning - assert to_trust_tier(96) == TrustTierContraindicated + assert to_trust_tier(0) == TRUST_TIER_NONE + assert to_trust_tier(2) == TRUST_TIER_AFFIRMING + assert to_trust_tier(32) == TRUST_TIER_WARNING + assert to_trust_tier(96) == TRUST_TIER_CONTRAINDICATED def test_to_trust_tier_valid_str(): - assert to_trust_tier("none") == TrustTierNone - assert to_trust_tier("affirming") == TrustTierAffirming - assert to_trust_tier("warning") == TrustTierWarning - assert to_trust_tier("contraindicated") == TrustTierContraindicated + assert to_trust_tier("none") == TRUST_TIER_NONE + assert to_trust_tier("affirming") == TRUST_TIER_AFFIRMING + assert to_trust_tier("warning") == TRUST_TIER_WARNING + assert to_trust_tier("contraindicated") == TRUST_TIER_CONTRAINDICATED def test_to_trust_tier_invalid_int(): - assert to_trust_tier(100) == TrustTierNone # Default fallback + assert to_trust_tier(100) == TRUST_TIER_NONE # Default fallback def test_to_trust_tier_invalid_str(): - assert to_trust_tier("invalid_string") == TrustTierNone # Default fallback + assert to_trust_tier("invalid_string") == TRUST_TIER_NONE # Default fallback def test_to_trust_tier_invalid_type(): diff --git a/tests/test_trust_vector.py b/tests/test_trust_vector.py index a65086a..5070037 100644 --- a/tests/test_trust_vector.py +++ b/tests/test_trust_vector.py @@ -2,38 +2,43 @@ import pytest -from src.trust_claims import (ApprovedFilesClaim, ApprovedRuntimeClaim, - EncryptedMemoryRuntimeClaim, - GenuineHardwareClaim, - HwKeysEncryptedSecretsClaim, TrustedSourcesClaim, - TrustworthyInstanceClaim, UnsafeConfigClaim) +from src.trust_claims import ( + APPROVED_FILES_CLAIM, + APPROVED_RUNTIME_CLAIM, + ENCRYPTED_MEMORY_RUNTIME_CLAIM, + GENUINE_HARDWARE_CLAIM, + HW_KEYS_ENCRYPTED_SECRETS_CLAIM, + TRUSTED_SOURCES_CLAIM, + TRUSTWORTHY_INSTANCE_CLAIM, + UNSAFE_CONFIG_CLAIM, +) from src.trust_vector import TrustVector @pytest.fixture def sample_trust_vector(): return TrustVector( - instance_identity=TrustworthyInstanceClaim, - configuration=UnsafeConfigClaim, - executables=ApprovedRuntimeClaim, - file_system=ApprovedFilesClaim, - hardware=GenuineHardwareClaim, - runtime_opaque=EncryptedMemoryRuntimeClaim, - storage_opaque=HwKeysEncryptedSecretsClaim, - sourced_data=TrustedSourcesClaim, + instance_identity=TRUSTWORTHY_INSTANCE_CLAIM, + configuration=UNSAFE_CONFIG_CLAIM, + executables=APPROVED_RUNTIME_CLAIM, + file_system=APPROVED_FILES_CLAIM, + hardware=GENUINE_HARDWARE_CLAIM, + runtime_opaque=ENCRYPTED_MEMORY_RUNTIME_CLAIM, + storage_opaque=HW_KEYS_ENCRYPTED_SECRETS_CLAIM, + sourced_data=TRUSTED_SOURCES_CLAIM, ) def test_trust_vector_to_dict(sample_trust_vector): expected = { - "instance_identity": TrustworthyInstanceClaim.to_dict(), - "configuration": UnsafeConfigClaim.to_dict(), - "executables": ApprovedRuntimeClaim.to_dict(), - "file_system": ApprovedFilesClaim.to_dict(), - "hardware": GenuineHardwareClaim.to_dict(), - "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), - "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), - "sourced_data": TrustedSourcesClaim.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + "configuration": UNSAFE_CONFIG_CLAIM.to_dict(), + "executables": APPROVED_RUNTIME_CLAIM.to_dict(), + "file_system": APPROVED_FILES_CLAIM.to_dict(), + "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), } assert sample_trust_vector.to_dict() == expected @@ -46,28 +51,28 @@ def test_trust_vector_to_json(sample_trust_vector): def test_trust_vector_to_cbor(sample_trust_vector): expected = { - 0: TrustworthyInstanceClaim.to_dict(), - 1: UnsafeConfigClaim.to_dict(), - 2: ApprovedRuntimeClaim.to_dict(), - 3: ApprovedFilesClaim.to_dict(), - 4: GenuineHardwareClaim.to_dict(), - 5: EncryptedMemoryRuntimeClaim.to_dict(), - 6: HwKeysEncryptedSecretsClaim.to_dict(), - 7: TrustedSourcesClaim.to_dict(), + 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + 1: UNSAFE_CONFIG_CLAIM.to_dict(), + 2: APPROVED_RUNTIME_CLAIM.to_dict(), + 3: APPROVED_FILES_CLAIM.to_dict(), + 4: GENUINE_HARDWARE_CLAIM.to_dict(), + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + 7: TRUSTED_SOURCES_CLAIM.to_dict(), } assert sample_trust_vector.to_cbor() == expected def test_trust_vector_from_dict(): data = { - "instance_identity": TrustworthyInstanceClaim.to_dict(), - "configuration": UnsafeConfigClaim.to_dict(), - "executables": ApprovedRuntimeClaim.to_dict(), - "file_system": ApprovedFilesClaim.to_dict(), - "hardware": GenuineHardwareClaim.to_dict(), - "runtime_opaque": EncryptedMemoryRuntimeClaim.to_dict(), - "storage_opaque": HwKeysEncryptedSecretsClaim.to_dict(), - "sourced_data": TrustedSourcesClaim.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + "configuration": UNSAFE_CONFIG_CLAIM.to_dict(), + "executables": APPROVED_RUNTIME_CLAIM.to_dict(), + "file_system": APPROVED_FILES_CLAIM.to_dict(), + "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), } parsed_vector = TrustVector.from_dict(data) assert parsed_vector.to_dict() == data @@ -75,14 +80,14 @@ def test_trust_vector_from_dict(): def test_trust_vector_from_cbor(): cbor_data = { - 0: TrustworthyInstanceClaim.to_dict(), - 1: UnsafeConfigClaim.to_dict(), - 2: ApprovedRuntimeClaim.to_dict(), - 3: ApprovedFilesClaim.to_dict(), - 4: GenuineHardwareClaim.to_dict(), - 5: EncryptedMemoryRuntimeClaim.to_dict(), - 6: HwKeysEncryptedSecretsClaim.to_dict(), - 7: TrustedSourcesClaim.to_dict(), + 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), + 1: UNSAFE_CONFIG_CLAIM.to_dict(), + 2: APPROVED_RUNTIME_CLAIM.to_dict(), + 3: APPROVED_FILES_CLAIM.to_dict(), + 4: GENUINE_HARDWARE_CLAIM.to_dict(), + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), + 7: TRUSTED_SOURCES_CLAIM.to_dict(), } parsed_vector = TrustVector.from_cbor(cbor_data) assert ( diff --git a/tests/test_validation.py b/tests/test_validation.py index fde170b..7e9d395 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,12 +1,15 @@ import pytest -from src.claims import EARClaims -from src.trust_claims import (TrustClaim, TrustworthyInstanceClaim, - UnsafeConfigClaim) +from src.claims import AttestationResult +from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM, UNSAFE_CONFIG_CLAIM, TrustClaim from src.trust_vector import TrustVector -from src.validation import (EARValidationError, validate_ear_claims, - validate_trust_claim, validate_trust_vector, - validate_verifier_id) +from src.validation import ( + EARValidationError, + validate_ear_claims, + validate_trust_claim, + validate_trust_vector, + validate_verifier_id, +) from src.verifier_id import VerifierID @@ -23,8 +26,8 @@ def valid_trust_claim(): @pytest.fixture def valid_trust_vector(): return TrustVector( - instance_identity=TrustworthyInstanceClaim, - configuration=UnsafeConfigClaim, + instance_identity=TRUSTWORTHY_INSTANCE_CLAIM, + configuration=UNSAFE_CONFIG_CLAIM, ) @@ -35,7 +38,7 @@ def valid_verifier_id(): @pytest.fixture def valid_ear_claims(valid_trust_vector, valid_verifier_id): - return EARClaims.from_dict( + return AttestationResult.from_dict( { "eat_profile": "test_profile", "iat": 1234567890, @@ -92,7 +95,7 @@ def test_validate_ear_claims(valid_ear_claims): def test_validate_ear_claims_invalid(): with pytest.raises(EARValidationError): - invalid_claims = EARClaims( + invalid_claims = AttestationResult( profile="", issued_at=-1, verifier_id=VerifierID(developer="", build="") ) validate_ear_claims(invalid_claims) diff --git a/tox.ini b/tox.ini index 0d461b8..d015e1b 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ deps = pyright==1.1.325 pytest==7.4.2 commands = - isort . --check --diff + isort . --profile=black black . --check --diff flake8 . mypy . From 1c86eb90626c4057805fd66713a6a3afca3e8f60 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Tue, 18 Mar 2025 14:28:24 +0530 Subject: [PATCH 08/16] Add example usage of jwt Signed-off-by: HarshvMahawar --- src/example/__init__.py | 0 src/example/jwt_example.py | 50 ++++++++++ src/example/jwt_output.json | 128 ++++++++++++++++++++++++++ src/jwt_handler.py | 179 ------------------------------------ 4 files changed, 178 insertions(+), 179 deletions(-) create mode 100644 src/example/__init__.py create mode 100644 src/example/jwt_example.py create mode 100644 src/example/jwt_output.json diff --git a/src/example/__init__.py b/src/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/example/jwt_example.py b/src/example/jwt_example.py new file mode 100644 index 0000000..45d5fdd --- /dev/null +++ b/src/example/jwt_example.py @@ -0,0 +1,50 @@ +from datetime import datetime + +from src.claims import AttestationResult +from src.jwt_handler import decode_ear_claims, generate_secret_key, sign_ear_claims +from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM, UNRECOGNIZED_INSTANCE_CLAIM +from src.trust_tier import TRUST_TIER_AFFIRMING, TRUST_TIER_CONTRAINDICATED +from src.trust_vector import TrustVector +from src.verifier_id import VerifierID + +# Generate a secret key for signing +secret_key = generate_secret_key() + +# Create an AttestationResult object +attestation_result = AttestationResult( + profile="test_profile", + issued_at=int(datetime.timestamp(datetime.now())), + verifier_id=VerifierID(developer="Acme Inc.", build="v1"), + submods={ + "submod1": { + "trust_vector": TrustVector(instance_identity=UNRECOGNIZED_INSTANCE_CLAIM), + "status": TRUST_TIER_AFFIRMING, + }, + "submod2": { + "trust_vector": TrustVector(instance_identity=TRUSTWORTHY_INSTANCE_CLAIM), + "status": TRUST_TIER_CONTRAINDICATED, + }, + }, +) + +signed_jwt_token = sign_ear_claims(attestation_result, secret_key) + +# Prepare data to be written to a JSON file +output_data = { + "generated_secret_key": secret_key, + "original_attestation_result_json": attestation_result.to_dict(), + "original_attestation_result_cbor": attestation_result.to_cbor(), + "signed_jwt_token": signed_jwt_token, +} + +# Decode the JWT and add decoded claims +decoded_claims = decode_ear_claims(signed_jwt_token, secret_key) +output_data["decoded_attestation_result"] = decoded_claims.to_dict() + +# Save to output.json +""" +with open("jwt_output.json", "w", encoding="utf-8") as f: + json.dump(output_data, f, indent=4) + +print("Output successfully written to output.json") +""" diff --git a/src/example/jwt_output.json b/src/example/jwt_output.json new file mode 100644 index 0000000..16fd603 --- /dev/null +++ b/src/example/jwt_output.json @@ -0,0 +1,128 @@ +{ + "generated_secret_key": "d09f32...50287c7a7a6", + "original_attestation_result_json": { + "eat_profile": "test_profile", + "iat": 1742287718, + "ear.verifier-id": { + "developer": "Acme Inc.", + "build": "v1" + }, + "submods": { + "submod1": { + "trust_vector": { + "instance_identity": { + "value": 97, + "tag": "unrecognized_instance", + "short": "not recognized", + "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." + }, + "configuration": null, + "executables": null, + "file_system": null, + "hardware": null, + "runtime_opaque": null, + "storage_opaque": null, + "sourced_data": null + }, + "status": 2 + }, + "submod2": { + "trust_vector": { + "instance_identity": { + "value": 2, + "tag": "recognized_instance", + "short": "recognized and not compromised", + "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." + }, + "configuration": null, + "executables": null, + "file_system": null, + "hardware": null, + "runtime_opaque": null, + "storage_opaque": null, + "sourced_data": null + }, + "status": 96 + } + } + }, + "original_attestation_result_cbor": { + "265": "test_profile", + "6": 1742287718, + "1004": { + "0": "Acme Inc.", + "1": "v1" + }, + "266": { + "submod1": { + "1001": { + "0": { + "value": 97, + "tag": "unrecognized_instance", + "short": "not recognized", + "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." + } + }, + "1000": 2 + }, + "submod2": { + "1001": { + "0": { + "value": 2, + "tag": "recognized_instance", + "short": "recognized and not compromised", + "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." + } + }, + "1000": 96 + } + } + }, + "signed_jwt_token": "eyJhbGciOiJIU...LWmmfns12U", + "decoded_attestation_result": { + "eat_profile": "test_profile", + "iat": 1742287718, + "ear.verifier-id": { + "developer": "Acme Inc.", + "build": "v1" + }, + "submods": { + "submod1": { + "trust_vector": { + "instance_identity": { + "value": 97, + "tag": "unrecognized_instance", + "short": "not recognized", + "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." + }, + "configuration": null, + "executables": null, + "file_system": null, + "hardware": null, + "runtime_opaque": null, + "storage_opaque": null, + "sourced_data": null + }, + "status": 2 + }, + "submod2": { + "trust_vector": { + "instance_identity": { + "value": 2, + "tag": "recognized_instance", + "short": "recognized and not compromised", + "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." + }, + "configuration": null, + "executables": null, + "file_system": null, + "hardware": null, + "runtime_opaque": null, + "storage_opaque": null, + "sourced_data": null + }, + "status": 96 + } + } + } +} \ No newline at end of file diff --git a/src/jwt_handler.py b/src/jwt_handler.py index 9775548..a198571 100644 --- a/src/jwt_handler.py +++ b/src/jwt_handler.py @@ -49,182 +49,3 @@ def decode_ear_claims( # Decodes and reconstructs an AttestationResult object from a JWT. payload = verify_ear_claims(token, secret_key, algorithm) return AttestationResult.from_dict(payload) - - -# EXAMPLE USAGE - -# Generate a secret key -# secret_key = generate_secret_key() -# print(f"Generated Secret Key: {secret_key}") - -# # Create an AttestationResult object -# attestation_result = AttestationResult( -# profile="test_profile", -# issued_at=int(datetime.timestamp(datetime.now())), -# verifier_id=VerifierID(developer="Acme Inc.", build="v1"), -# submods={ -# "submod1": { -# "trust_vector": TrustVector(instance_identity=UNRECOGNIZED_INSTANCE_CLAIM), # noqa: E501 # pylint: disable=line-too-long -# "status": TRUST_TIER_AFFIRMING, -# }, -# "submod2": { -# "trust_vector": TrustVector(instance_identity=TRUSTWORTHY_INSTANCE_CLAIM), # noqa: E501 # pylint: disable=line-too-long -# "status": TRUST_TIER_CONTRAINDICATED, -# }, -# }, -# ) - -# # Print the original AttestationResult object -# print("\n Original AttestationResult Dictionary - JSON:") -# print(json.dumps(attestation_result.to_dict(), indent=4)) - -# print("\n Original AttestationResult Dictionary - CBOR:") -# print(json.dumps(attestation_result.to_cbor(), indent=4)) - -# # Sign the AttestationResult and generate a JWT -# jwt_token = sign_ear_claims(attestation_result, secret_key) -# print("\n Signed JWT Token:") -# print(jwt_token) - -# # Decode and verify the JWT -# decoded_claims = decode_ear_claims(jwt_token, secret_key) -# print("\n Decoded AttestationResult Dictionary:") -# print(json.dumps(decoded_claims.to_dict(), indent=4)) - - -# OUTPUT - -# Generated Secret Key: dadb1756080cabf4c0...d097a705b39c259be3d3 - -# Original AttestationResult Dictionary - JSON: -# { -# "eat_profile": "test_profile", -# "iat": 1742225266, -# "ear.verifier-id": { -# "developer": "Acme Inc.", -# "build": "v1" -# }, -# "submods": { -# "submod1": { -# "trust_vector": { -# "instance_identity": { -# "value": 97, -# "tag": "unrecognized_instance", -# "short": "not recognized", -# "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." # noqa: E501 # pylint: disable=line-too-long -# }, -# "configuration": null, -# "executables": null, -# "file_system": null, -# "hardware": null, -# "runtime_opaque": null, -# "storage_opaque": null, -# "sourced_data": null -# }, -# "status": 2 -# }, -# "submod2": { -# "trust_vector": { -# "instance_identity": { -# "value": 2, -# "tag": "recognized_instance", -# "short": "recognized and not compromised", -# "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." # noqa: E501 # pylint: disable=line-too-long -# }, -# "configuration": null, -# "executables": null, -# "file_system": null, -# "hardware": null, -# "runtime_opaque": null, -# "storage_opaque": null, -# "sourced_data": null -# }, -# "status": 96 -# } -# } -# } - -# Original AttestationResult Dictionary - CBOR: -# { -# "265": "test_profile", -# "6": 1742225266, -# "1004": { -# "0": "Acme Inc.", -# "1": "v1" -# }, -# "266": { -# "submod1": { -# "1001": { -# "0": { -# "value": 97, -# "tag": "unrecognized_instance", -# "short": "not recognized", -# "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." # noqa: E501 # pylint: disable=line-too-long -# } -# }, -# "1000": 2 -# }, -# "submod2": { -# "1001": { -# "0": { -# "value": 2, -# "tag": "recognized_instance", -# "short": "recognized and not compromised", -# "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." # noqa: E501 # pylint: disable=line-too-long -# } -# }, -# "1000": 96 -# } -# } -# } - -# Signed JWT Token: -# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Nq0aWU0BDg - -# Decoded AttestationResult Dictionary: -# { -# "eat_profile": "test_profile", -# "iat": 1742225266, -# "ear.verifier-id": { -# "developer": "Acme Inc.", -# "build": "v1" -# }, -# "submods": { -# "submod1": { -# "trust_vector": { -# "instance_identity": { -# "value": 97, -# "tag": "unrecognized_instance", -# "short": "not recognized", -# "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." # noqa: E501 # pylint: disable=line-too-long -# }, -# "configuration": null, -# "executables": null, -# "file_system": null, -# "hardware": null, -# "runtime_opaque": null, -# "storage_opaque": null, -# "sourced_data": null -# }, -# "status": 2 -# }, -# "submod2": { -# "trust_vector": { -# "instance_identity": { -# "value": 2, -# "tag": "recognized_instance", -# "short": "recognized and not compromised", -# "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." # noqa: E501 # pylint: disable=line-too-long -# }, -# "configuration": null, -# "executables": null, -# "file_system": null, -# "hardware": null, -# "runtime_opaque": null, -# "storage_opaque": null, -# "sourced_data": null -# }, -# "status": 96 -# } -# } -# } From 31de3a14b053ecd476777253dcd805ad741d6f89 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Tue, 18 Mar 2025 18:41:30 +0530 Subject: [PATCH 09/16] Update .pylintrc Signed-off-by: HarshvMahawar --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 73c840f..ce00b4d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ [MESSAGES CONTROL] -disable = C0114, C0115, C0116, redefined-outer-name, duplicate-code ; Disable missing module/class/function docstring warnings +disable = C0114, C0115, C0116, redefined-outer-name, duplicate-code [FORMAT] max-line-length = 88 ; Match Black's default line length From d7d2aacc8b7d86479459ce47981d9f68eb038792 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Fri, 21 Mar 2025 20:26:57 +0530 Subject: [PATCH 10/16] Rename JC_map to jc_map to maintain consistency Signed-off-by: HarshvMahawar --- src/base.py | 2 +- src/claims.py | 26 +++++++++++++------------- src/trust_vector.py | 8 ++++---- src/verifier_id.py | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/base.py b/src/base.py index 3f3a993..faea172 100644 --- a/src/base.py +++ b/src/base.py @@ -5,7 +5,7 @@ # Abstract class to define structure to subclasses class BaseJCSerializable(ABC): - JC_map: Dict[str, int] + jc_map: Dict[str, int] @abstractmethod def to_dict(self) -> Dict[str, Any]: diff --git a/src/claims.py b/src/claims.py index 08e8077..9f4f8bf 100644 --- a/src/claims.py +++ b/src/claims.py @@ -17,7 +17,7 @@ class AttestationResult(BaseJCSerializable): submods: Dict[str, Dict[str, Any]] = field(default_factory=dict) # https://www.ietf.org/archive/id/draft-ietf-rats-eat-31.html#section-7.2.4 - JC_map = { + jc_map = { "profile": 265, "issued_at": 6, "verifier_id": 1004, @@ -42,13 +42,13 @@ def to_dict(self) -> Dict[str, Any]: def to_cbor(self) -> Dict[int, Any]: return { - self.JC_map["profile"]: self.profile, - self.JC_map["issued_at"]: self.issued_at, - self.JC_map["verifier_id"]: self.verifier_id.to_cbor(), - self.JC_map["submods"]: { + self.jc_map["profile"]: self.profile, + self.jc_map["issued_at"]: self.issued_at, + self.jc_map["verifier_id"]: self.verifier_id.to_cbor(), + self.jc_map["submods"]: { key: { - self.JC_map["submod.trust_vector"]: value["trust_vector"].to_cbor(), - self.JC_map["submod.status"]: value["status"].value, + self.jc_map["submod.trust_vector"]: value["trust_vector"].to_cbor(), + self.jc_map["submod.status"]: value["status"].value, } for key, value in self.submods.items() }, @@ -76,16 +76,16 @@ def from_json(cls, json_str: str): @classmethod def from_cbor(cls, data: Dict[int, Any]): return cls( - profile=data.get(cls.JC_map["profile"], ""), - issued_at=data.get(cls.JC_map["issued_at"], 0), - verifier_id=VerifierID.from_cbor(data.get(cls.JC_map["verifier_id"], {})), + profile=data.get(cls.jc_map["profile"], ""), + issued_at=data.get(cls.jc_map["issued_at"], 0), + verifier_id=VerifierID.from_cbor(data.get(cls.jc_map["verifier_id"], {})), submods={ key: { "trust_vector": TrustVector.from_cbor( - value.get(cls.JC_map["submod.trust_vector"], {}) + value.get(cls.jc_map["submod.trust_vector"], {}) ), - "status": to_trust_tier(value.get(cls.JC_map["submod.status"], 0)), + "status": to_trust_tier(value.get(cls.jc_map["submod.status"], 0)), } - for key, value in data.get(cls.JC_map["submods"], {}).items() + for key, value in data.get(cls.jc_map["submods"], {}).items() }, ) diff --git a/src/trust_vector.py b/src/trust_vector.py index bd87239..dd142e3 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -18,7 +18,7 @@ class TrustVector(BaseJCSerializable): storage_opaque: Optional[TrustClaim] = None sourced_data: Optional[TrustClaim] = None - JC_map = { + jc_map = { "instance_identity": 0, "configuration": 1, "executables": 2, @@ -35,7 +35,7 @@ def to_dict(self) -> Dict[str, Any]: def to_cbor(self) -> Dict[int, Dict[str, Any]]: return { index: getattr(self, field).to_dict() - for field, index in self.JC_map.items() + for field, index in self.jc_map.items() if getattr(self, field) } @@ -43,13 +43,13 @@ def to_cbor(self) -> Dict[int, Dict[str, Any]]: def from_dict(cls, data: Dict[str, Any]): kwargs = { field: TrustClaim(**data[field]) if data.get(field) else None - for field in cls.JC_map + for field in cls.jc_map } return cls(**kwargs) @classmethod def from_cbor(cls, data: Dict[int, Dict[str, Any]]): - reverse_map = {v: k for k, v in cls.JC_map.items()} + reverse_map = {v: k for k, v in cls.jc_map.items()} kwargs = { reverse_map[index]: TrustClaim(**value) for index, value in data.items() diff --git a/src/verifier_id.py b/src/verifier_id.py index e035cc4..e088ca5 100644 --- a/src/verifier_id.py +++ b/src/verifier_id.py @@ -9,7 +9,7 @@ class VerifierID(BaseJCSerializable): developer: str build: str - JC_map = { + jc_map = { "developer": 0, # JC<"developer", 0> "build": 1, # JC<"build", 1> } @@ -20,7 +20,7 @@ def to_dict(self) -> Dict[str, Any]: # Convert to a dict with integer keys (for CBOR) def to_cbor(self) -> Dict[int, str]: return { - index: getattr(self, field) for field, index in self.JC_map.items() + index: getattr(self, field) for field, index in self.jc_map.items() } # noqa: E501 # Create an instance from a dict with string keys @@ -31,6 +31,6 @@ def from_dict(cls, data: Dict[str, str]): # Create an instance from a CBOR-like dict (integer keys) @classmethod def from_cbor(cls, data: Dict[int, str]): - reverse_map = {v: k for k, v in cls.JC_map.items()} + reverse_map = {v: k for k, v in cls.jc_map.items()} kwargs = {reverse_map[index]: value for index, value in data.items()} return cls(**kwargs) From 6da16f4e5d3cbde010948b0aebd0ff5b60cc81c7 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Thu, 27 Mar 2025 15:29:29 +0530 Subject: [PATCH 11/16] Move validation checks from individual file to class defintions Signed-off-by: HarshvMahawar --- src/claims.py | 27 ++++++++++ src/errors.py | 3 ++ src/trust_claims.py | 16 ++++++ src/trust_vector.py | 7 +++ src/validation.py | 81 ----------------------------- src/verifier_id.py | 8 +++ tests/test_claims.py | 14 +++++ tests/test_trust_claims.py | 14 ++++- tests/test_trust_vector.py | 15 ++++++ tests/test_validation.py | 101 ------------------------------------- tests/test_verifier_id.py | 12 +++++ 11 files changed, 115 insertions(+), 183 deletions(-) create mode 100644 src/errors.py delete mode 100644 src/validation.py delete mode 100644 tests/test_validation.py diff --git a/src/claims.py b/src/claims.py index 9f4f8bf..bbae954 100644 --- a/src/claims.py +++ b/src/claims.py @@ -3,6 +3,7 @@ from typing import Any, Dict from src.base import BaseJCSerializable +from src.errors import EARValidationError from src.trust_tier import to_trust_tier from src.trust_vector import TrustVector from src.verifier_id import VerifierID @@ -89,3 +90,29 @@ def from_cbor(cls, data: Dict[int, Any]): for key, value in data.get(cls.jc_map["submods"], {}).items() }, ) + + def validate(self): + # Validates an AttestationResult object + if not isinstance(self.profile, str) or not self.profile: + raise EARValidationError( + "AttestationResult profile must be a non-empty string" + ) + if not isinstance(self.issued_at, int) or self.issued_at <= 0: + raise EARValidationError( + "AttestationResult issued_at must be a positive integer" + ) + + self.verifier_id.validate() + + for submod, details in self.submods.items(): + if ( + not isinstance(details, Dict) + or "trust_vector" not in details + or "status" not in details + ): + raise EARValidationError( + f"Submodule {submod} must contain a valid trust_vector and status" + ) + + trust_vector = details["trust_vector"] + trust_vector.validate() diff --git a/src/errors.py b/src/errors.py new file mode 100644 index 0000000..cd5747e --- /dev/null +++ b/src/errors.py @@ -0,0 +1,3 @@ +class EARValidationError(Exception): + # Custom exception for validation errors in AttestationResult + pass diff --git a/src/trust_claims.py b/src/trust_claims.py index 80086e7..39f4d01 100644 --- a/src/trust_claims.py +++ b/src/trust_claims.py @@ -1,6 +1,8 @@ from dataclasses import asdict, dataclass from typing import Any, Dict +from src.errors import EARValidationError + # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-2.3 @dataclass @@ -13,6 +15,20 @@ class TrustClaim: def to_dict(self) -> Dict[str, Any]: return asdict(self) + def validate(self): + # Validates a TrustClaim object + if not isinstance(self.value, int) or not -128 <= self.value <= 127: + raise EARValidationError( + f"""Invalid value in TrustClaim: {self.value}. + Must be in range [-128, 127]""" + ) + if not isinstance(self.tag, str): + raise EARValidationError("TrustClaim tag must be a string") + if not isinstance(self.short, str): + raise EARValidationError("TrustClaim short description must be a string") + if not isinstance(self.long, str): + raise EARValidationError("TrustClaim long description must be a string") + # General VERIFIER_MALFUNCTION_CLAIM = TrustClaim( diff --git a/src/trust_vector.py b/src/trust_vector.py index dd142e3..6942ce0 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -56,3 +56,10 @@ def from_cbor(cls, data: Dict[int, Dict[str, Any]]): if index in reverse_map } return cls(**kwargs) + + def validate(self): + # Validates a TrustVector object + + for claim in self.__dict__.values(): + if claim is not None: + claim.validate() diff --git a/src/validation.py b/src/validation.py deleted file mode 100644 index afed6b6..0000000 --- a/src/validation.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Dict - -from src.claims import AttestationResult -from src.trust_claims import TrustClaim -from src.trust_vector import TrustVector -from src.verifier_id import VerifierID - - -class EARValidationError(Exception): - # Custom exception for validation errors in AttestationResult - pass - - -def validate_trust_claim(trust_claim: TrustClaim): - # Validates a TrustClaim object - if not isinstance(trust_claim.value, int) or not -128 <= trust_claim.value <= 127: - raise EARValidationError( - f"""Invalid value in TrustClaim: {trust_claim.value}. - Must be in range [-128, 127]""" - ) - if not isinstance(trust_claim.tag, str): - raise EARValidationError("TrustClaim tag must be a string") - if not isinstance(trust_claim.short, str): - raise EARValidationError("TrustClaim short description must be a string") - if not isinstance(trust_claim.long, str): - raise EARValidationError("TrustClaim long description must be a string") - - -def validate_trust_vector(trust_vector: TrustVector): - # Validates a TrustVector object - if not isinstance(trust_vector, TrustVector): - raise EARValidationError("Invalid TrustVector object") - - for claim in trust_vector.__dict__.values(): - if claim is not None: - validate_trust_claim(claim) - - -def validate_verifier_id(verifier_id: VerifierID): - # Validates a VerifierID object - if not isinstance(verifier_id, VerifierID): - raise EARValidationError("Invalid VerifierID object") - if not verifier_id.developer or not isinstance(verifier_id.developer, str): - raise EARValidationError("VerifierID developer must be a non-empty string") - if not verifier_id.build or not isinstance(verifier_id.build, str): - raise EARValidationError("VerifierID build must be a non-empty string") - - -def validate_ear_claims(ear_claims: AttestationResult): - # Validates an AttestationResult object - if not isinstance(ear_claims, AttestationResult): - raise EARValidationError("Invalid AttestationResult object") - if not isinstance(ear_claims.profile, str) or not ear_claims.profile: - raise EARValidationError("AttestationResult profile must be a non-empty string") - if not isinstance(ear_claims.issued_at, int) or ear_claims.issued_at <= 0: - raise EARValidationError( - "AttestationResult issued_at must be a positive integer" - ) - - validate_verifier_id(ear_claims.verifier_id) - - for submod, details in ear_claims.submods.items(): - if ( - not isinstance(details, Dict) - or "trust_vector" not in details - or "status" not in details - ): - raise EARValidationError( - f"Submodule {submod} must contain a valid trust_vector and status" - ) - - validate_trust_vector(details["trust_vector"]) - - -def validate_all(ear_claims: AttestationResult): - # Runs all validation checks on the provided AttestationResult object - try: - validate_ear_claims(ear_claims) - print("AttestationResult validation successful.") - except EARValidationError as error_message: - print(f"Validation failed: {error_message}") diff --git a/src/verifier_id.py b/src/verifier_id.py index e088ca5..f3b6128 100644 --- a/src/verifier_id.py +++ b/src/verifier_id.py @@ -2,6 +2,7 @@ from typing import Any, Dict from src.base import BaseJCSerializable +from src.errors import EARValidationError # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.3 @@ -34,3 +35,10 @@ def from_cbor(cls, data: Dict[int, str]): reverse_map = {v: k for k, v in cls.jc_map.items()} kwargs = {reverse_map[index]: value for index, value in data.items()} return cls(**kwargs) + + def validate(self): + # Validates a VerifierID object + if not self.developer or not isinstance(self.developer, str): + raise EARValidationError("VerifierID developer must be a non-empty string") + if not self.build or not isinstance(self.build, str): + raise EARValidationError("VerifierID build must be a non-empty string") diff --git a/tests/test_claims.py b/tests/test_claims.py index 5452ab0..24041af 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -1,6 +1,7 @@ import pytest from src.claims import AttestationResult +from src.errors import EARValidationError from src.trust_claims import ( APPROVED_CONFIG_CLAIM, APPROVED_FILES_CLAIM, @@ -144,3 +145,16 @@ def test_attestation_result_from_cbor(): parsed_claims = AttestationResult.from_cbor(cbor_data) assert parsed_claims.to_cbor() == cbor_data + + +def test_validate_ear_claims(sample_attestation_result): + # Should not raise an error + sample_attestation_result.validate() + + +def test_validate_ear_claims_invalid(): + with pytest.raises(EARValidationError): + invalid_attestation_result = AttestationResult( + profile="", issued_at=-1, verifier_id=VerifierID(developer="", build="") + ) + invalid_attestation_result.validate() diff --git a/tests/test_trust_claims.py b/tests/test_trust_claims.py index f5e134f..1c477b7 100644 --- a/tests/test_trust_claims.py +++ b/tests/test_trust_claims.py @@ -1,6 +1,7 @@ import pytest -from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM +from src.errors import EARValidationError +from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM, TrustClaim @pytest.fixture @@ -17,3 +18,14 @@ def test_to_dict(trust_claim): "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised.", # noqa: E501 # pylint: disable=line-too-long } assert trust_claim.to_dict() == expected + + +def test_validate_trust_claim_valid(trust_claim): + # Should not raise an error + trust_claim.validate() + + +def test_validate_trust_claim_invalid(): + with pytest.raises(EARValidationError): + invalid_trust_claim = TrustClaim(value=200, tag="invalid", short="", long="") + invalid_trust_claim.validate() diff --git a/tests/test_trust_vector.py b/tests/test_trust_vector.py index 5070037..249ca48 100644 --- a/tests/test_trust_vector.py +++ b/tests/test_trust_vector.py @@ -2,6 +2,7 @@ import pytest +from src.errors import EARValidationError from src.trust_claims import ( APPROVED_FILES_CLAIM, APPROVED_RUNTIME_CLAIM, @@ -11,6 +12,7 @@ TRUSTED_SOURCES_CLAIM, TRUSTWORTHY_INSTANCE_CLAIM, UNSAFE_CONFIG_CLAIM, + TrustClaim, ) from src.trust_vector import TrustVector @@ -93,3 +95,16 @@ def test_trust_vector_from_cbor(): assert ( parsed_vector.to_dict() == TrustVector.from_cbor(cbor_data).to_dict() ) # noqa: E501 + + +def test_validate_trust_vector(sample_trust_vector): + # Should not raise an error + sample_trust_vector.validate() + + +def test_validate_trust_vector_invalid(): + with pytest.raises(EARValidationError): + invalid_vector = TrustVector( + configuration=TrustClaim(value=200, tag="invalid", short="", long="") + ) + invalid_vector.validate() diff --git a/tests/test_validation.py b/tests/test_validation.py deleted file mode 100644 index 7e9d395..0000000 --- a/tests/test_validation.py +++ /dev/null @@ -1,101 +0,0 @@ -import pytest - -from src.claims import AttestationResult -from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM, UNSAFE_CONFIG_CLAIM, TrustClaim -from src.trust_vector import TrustVector -from src.validation import ( - EARValidationError, - validate_ear_claims, - validate_trust_claim, - validate_trust_vector, - validate_verifier_id, -) -from src.verifier_id import VerifierID - - -@pytest.fixture -def valid_trust_claim(): - return TrustClaim( - value=2, - tag="approved_config", - short="Approved", - long="Configuration is approved.", - ) - - -@pytest.fixture -def valid_trust_vector(): - return TrustVector( - instance_identity=TRUSTWORTHY_INSTANCE_CLAIM, - configuration=UNSAFE_CONFIG_CLAIM, - ) - - -@pytest.fixture -def valid_verifier_id(): - return VerifierID(developer="Acme Inc.", build="v1.0.0") - - -@pytest.fixture -def valid_ear_claims(valid_trust_vector, valid_verifier_id): - return AttestationResult.from_dict( - { - "eat_profile": "test_profile", - "iat": 1234567890, - "ear.verifier-id": valid_verifier_id.to_dict(), - "submods": { - "submod1": { - "trust_vector": valid_trust_vector.to_dict(), - "status": "affirming", - } - }, - } - ) - - -def test_validate_trust_claim(valid_trust_claim): - # Should not raise an error - validate_trust_claim(valid_trust_claim) - - -def test_validate_trust_claim_invalid(): - with pytest.raises(EARValidationError): - validate_trust_claim( - TrustClaim(value=200, tag="invalid", short="", long="") - ) # Invalid value (>127) - - -def test_validate_trust_vector(valid_trust_vector): - # Should not raise an error - validate_trust_vector(valid_trust_vector) - - -def test_validate_trust_vector_invalid(): - with pytest.raises(EARValidationError): - invalid_vector = TrustVector( - configuration=TrustClaim(value=200, tag="invalid", short="", long="") - ) - validate_trust_vector(invalid_vector) - - -def test_validate_verifier_id(valid_verifier_id): - # Should not raise an error - validate_verifier_id(valid_verifier_id) - - -def test_validate_verifier_id_invalid(): - with pytest.raises(EARValidationError): - validate_verifier_id(VerifierID(developer="", build="")) # Invalid empty fields - - -def test_validate_ear_claims(valid_ear_claims): - # Should not raise an error - validate_ear_claims(valid_ear_claims) - - -def test_validate_ear_claims_invalid(): - with pytest.raises(EARValidationError): - invalid_claims = AttestationResult( - profile="", issued_at=-1, verifier_id=VerifierID(developer="", build="") - ) - validate_ear_claims(invalid_claims) diff --git a/tests/test_verifier_id.py b/tests/test_verifier_id.py index 372c6c5..22ac7a5 100644 --- a/tests/test_verifier_id.py +++ b/tests/test_verifier_id.py @@ -2,6 +2,7 @@ import pytest +from src.errors import EARValidationError from src.verifier_id import VerifierID @@ -38,3 +39,14 @@ def test_from_cbor(): sample_verifier = VerifierID.from_cbor(cbor_data) assert sample_verifier.developer == "Acme Inc." assert sample_verifier.build == "v1.0.0" + + +def test_validate_verifier_id(verifier): + # Should not raise an error + verifier.validate() + + +def test_validate_verifier_id_invalid(): + with pytest.raises(EARValidationError): + invalid_verifier_id = VerifierID(developer="", build="") # Invalid empty fields + invalid_verifier_id.validate() From 2187370fee39f61196a35bc50d949c44d2169c33 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Fri, 28 Mar 2025 04:14:34 +0530 Subject: [PATCH 12/16] Add encode_jwt and decode_jwt methods to AttestationResult class Signed-off-by: HarshvMahawar --- src/claims.py | 28 +++++++ src/example/jwt_example.py | 29 +++---- src/example/jwt_output.json | 154 +++++++++--------------------------- src/jwt_config.py | 10 +++ src/jwt_handler.py | 51 ------------ tox.ini | 2 + 6 files changed, 86 insertions(+), 188 deletions(-) create mode 100644 src/jwt_config.py delete mode 100644 src/jwt_handler.py diff --git a/src/claims.py b/src/claims.py index bbae954..fd7bfe4 100644 --- a/src/claims.py +++ b/src/claims.py @@ -1,9 +1,13 @@ import json from dataclasses import dataclass, field +from datetime import datetime, timedelta from typing import Any, Dict +from jose import jwt # type: ignore # pylint: disable=import-error + from src.base import BaseJCSerializable from src.errors import EARValidationError +from src.jwt_config import DEFAULT_ALGORITHM, DEFAULT_EXPIRATION_MINUTES from src.trust_tier import to_trust_tier from src.trust_vector import TrustVector from src.verifier_id import VerifierID @@ -116,3 +120,27 @@ def validate(self): trust_vector = details["trust_vector"] trust_vector.validate() + + def encode_jwt( + self, + secret_key: str, + algorithm: str = DEFAULT_ALGORITHM, + expiration_minutes: int = DEFAULT_EXPIRATION_MINUTES, + ) -> str: + # Signs an AttestationResult object and returns a JWT + payload = self.to_dict() + payload["exp"] = int( + datetime.timestamp(datetime.now() + timedelta(minutes=expiration_minutes)) + ) + return jwt.encode(payload, secret_key, algorithm=algorithm) + + @classmethod + def decode_jwt( + cls, token: str, secret_key: str, algorithm: str = DEFAULT_ALGORITHM + ): + # Verifies a JWT and returns the decoded AttestationResult object. + try: + payload = jwt.decode(token, secret_key, algorithms=[algorithm]) + return cls.from_dict(payload) + except Exception as exc: + raise ValueError(f"JWT decoding failed: {exc}") from exc diff --git a/src/example/jwt_example.py b/src/example/jwt_example.py index 45d5fdd..e413a3a 100644 --- a/src/example/jwt_example.py +++ b/src/example/jwt_example.py @@ -1,12 +1,14 @@ from datetime import datetime from src.claims import AttestationResult -from src.jwt_handler import decode_ear_claims, generate_secret_key, sign_ear_claims +from src.jwt_config import generate_secret_key from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM, UNRECOGNIZED_INSTANCE_CLAIM from src.trust_tier import TRUST_TIER_AFFIRMING, TRUST_TIER_CONTRAINDICATED from src.trust_vector import TrustVector from src.verifier_id import VerifierID +# import json + # Generate a secret key for signing secret_key = generate_secret_key() @@ -27,24 +29,13 @@ }, ) -signed_jwt_token = sign_ear_claims(attestation_result, secret_key) - -# Prepare data to be written to a JSON file -output_data = { - "generated_secret_key": secret_key, - "original_attestation_result_json": attestation_result.to_dict(), - "original_attestation_result_cbor": attestation_result.to_cbor(), - "signed_jwt_token": signed_jwt_token, -} +# payload = attestation_result.encode_jwt(secret_key=secret_key) +# print(payload) -# Decode the JWT and add decoded claims -decoded_claims = decode_ear_claims(signed_jwt_token, secret_key) -output_data["decoded_attestation_result"] = decoded_claims.to_dict() +# decoded = AttestationResult.decode_jwt(token=payload, secret_key=secret_key) +# output_data = decoded.to_dict() -# Save to output.json -""" -with open("jwt_output.json", "w", encoding="utf-8") as f: - json.dump(output_data, f, indent=4) +# with open("jwt_output.json", "w", encoding="utf-8") as f: +# json.dump(output_data, f, indent=4) -print("Output successfully written to output.json") -""" +# print("Output successfully written to output.json") diff --git a/src/example/jwt_output.json b/src/example/jwt_output.json index 16fd603..1fe24a7 100644 --- a/src/example/jwt_output.json +++ b/src/example/jwt_output.json @@ -1,128 +1,46 @@ { - "generated_secret_key": "d09f32...50287c7a7a6", - "original_attestation_result_json": { - "eat_profile": "test_profile", - "iat": 1742287718, - "ear.verifier-id": { - "developer": "Acme Inc.", - "build": "v1" - }, - "submods": { - "submod1": { - "trust_vector": { - "instance_identity": { - "value": 97, - "tag": "unrecognized_instance", - "short": "not recognized", - "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." - }, - "configuration": null, - "executables": null, - "file_system": null, - "hardware": null, - "runtime_opaque": null, - "storage_opaque": null, - "sourced_data": null - }, - "status": 2 - }, - "submod2": { - "trust_vector": { - "instance_identity": { - "value": 2, - "tag": "recognized_instance", - "short": "recognized and not compromised", - "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." - }, - "configuration": null, - "executables": null, - "file_system": null, - "hardware": null, - "runtime_opaque": null, - "storage_opaque": null, - "sourced_data": null - }, - "status": 96 - } - } + "eat_profile": "test_profile", + "iat": 1743076236, + "ear.verifier-id": { + "developer": "Acme Inc.", + "build": "v1" }, - "original_attestation_result_cbor": { - "265": "test_profile", - "6": 1742287718, - "1004": { - "0": "Acme Inc.", - "1": "v1" - }, - "266": { - "submod1": { - "1001": { - "0": { - "value": 97, - "tag": "unrecognized_instance", - "short": "not recognized", - "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." - } + "submods": { + "submod1": { + "trust_vector": { + "instance_identity": { + "value": 97, + "tag": "unrecognized_instance", + "short": "not recognized", + "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." }, - "1000": 2 + "configuration": null, + "executables": null, + "file_system": null, + "hardware": null, + "runtime_opaque": null, + "storage_opaque": null, + "sourced_data": null }, - "submod2": { - "1001": { - "0": { - "value": 2, - "tag": "recognized_instance", - "short": "recognized and not compromised", - "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." - } - }, - "1000": 96 - } - } - }, - "signed_jwt_token": "eyJhbGciOiJIU...LWmmfns12U", - "decoded_attestation_result": { - "eat_profile": "test_profile", - "iat": 1742287718, - "ear.verifier-id": { - "developer": "Acme Inc.", - "build": "v1" + "status": 2 }, - "submods": { - "submod1": { - "trust_vector": { - "instance_identity": { - "value": 97, - "tag": "unrecognized_instance", - "short": "not recognized", - "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." - }, - "configuration": null, - "executables": null, - "file_system": null, - "hardware": null, - "runtime_opaque": null, - "storage_opaque": null, - "sourced_data": null + "submod2": { + "trust_vector": { + "instance_identity": { + "value": 2, + "tag": "recognized_instance", + "short": "recognized and not compromised", + "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." }, - "status": 2 + "configuration": null, + "executables": null, + "file_system": null, + "hardware": null, + "runtime_opaque": null, + "storage_opaque": null, + "sourced_data": null }, - "submod2": { - "trust_vector": { - "instance_identity": { - "value": 2, - "tag": "recognized_instance", - "short": "recognized and not compromised", - "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." - }, - "configuration": null, - "executables": null, - "file_system": null, - "hardware": null, - "runtime_opaque": null, - "storage_opaque": null, - "sourced_data": null - }, - "status": 96 - } + "status": 96 } } } \ No newline at end of file diff --git a/src/jwt_config.py b/src/jwt_config.py new file mode 100644 index 0000000..af17f0e --- /dev/null +++ b/src/jwt_config.py @@ -0,0 +1,10 @@ +import secrets + +# Default cryptographic settings for JWT +DEFAULT_ALGORITHM = "HS256" +DEFAULT_EXPIRATION_MINUTES = 60 + + +def generate_secret_key() -> str: + # Generates a secure random secret key for JWT signing. + return secrets.token_hex(32) diff --git a/src/jwt_handler.py b/src/jwt_handler.py deleted file mode 100644 index a198571..0000000 --- a/src/jwt_handler.py +++ /dev/null @@ -1,51 +0,0 @@ -import secrets -from datetime import datetime, timedelta -from typing import Any, Dict - -from jose import jwt # type: ignore # pylint: disable=import-error - -from src.claims import AttestationResult - -# Default cryptographic settings -DEFAULT_ALGORITHM = "HS256" -DEFAULT_EXPIRATION_MINUTES = 60 - - -def generate_secret_key() -> str: - # Generates a secure random secret key for JWT signing. - return secrets.token_hex(32) - - -def sign_ear_claims( - ear_claims: AttestationResult, - secret_key: str, - algorithm: str = DEFAULT_ALGORITHM, - expiration_minutes: int = DEFAULT_EXPIRATION_MINUTES, -) -> str: - - # Signs an AttestationResult object and returns a JWT. - payload = ear_claims.to_dict() - payload["exp"] = int( - datetime.timestamp(datetime.now() + timedelta(minutes=expiration_minutes)) - ) - return jwt.encode(payload, secret_key, algorithm=algorithm) - - -def verify_ear_claims( - token: str, secret_key: str, algorithm: str = DEFAULT_ALGORITHM -) -> Dict[str, Any]: - - # Verifies a JWT and returns the decoded AttestationResult payload. - try: - return jwt.decode(token, secret_key, algorithms=[algorithm]) - except Exception as exc: - raise ValueError(f"JWT decoding failed: {exc}") from exc - - -def decode_ear_claims( - token: str, secret_key: str, algorithm: str = DEFAULT_ALGORITHM -) -> AttestationResult: - - # Decodes and reconstructs an AttestationResult object from a JWT. - payload = verify_ear_claims(token, secret_key, algorithm) - return AttestationResult.from_dict(payload) diff --git a/tox.ini b/tox.ini index d015e1b..4f4f283 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ deps = pylint==2.17.5 pyright==1.1.325 pytest==7.4.2 + python-jose==3.4.0 commands = isort . --profile=black black . --check --diff @@ -21,4 +22,5 @@ commands = [testenv:test] deps = pytest==7.4.2 + python-jose==3.4.0 commands = pytest From 3c3d806a631b6c03f87109fe0badaf1c8c6fcd04 Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Tue, 8 Apr 2025 00:34:37 +0530 Subject: [PATCH 13/16] Add to_cbor and from_cbor generalized Signed-off-by: HarshvMahawar --- src/base.py | 107 +++++++++++++++++++++++++++++++++++++------ src/claims.py | 62 ++++++++++++------------- src/trust_vector.py | 30 ++++++------ src/verifier_id.py | 18 ++++---- tests/test_claims.py | 5 +- 5 files changed, 151 insertions(+), 71 deletions(-) diff --git a/src/base.py b/src/base.py index faea172..8b3709e 100644 --- a/src/base.py +++ b/src/base.py @@ -1,30 +1,109 @@ -import json from abc import ABC, abstractmethod -from typing import Any, Dict +from typing import Any, Dict, Type, TypeVar, get_type_hints +import json +T = TypeVar("T", bound="BaseJCSerializable") -# Abstract class to define structure to subclasses class BaseJCSerializable(ABC): jc_map: Dict[str, int] - @abstractmethod def to_dict(self) -> Dict[str, Any]: - pass + raise NotImplementedError("to_dict must be implemented in subclasses.") - # Similar for all the subclasses def to_json(self) -> str: return json.dumps(self.to_dict()) - @abstractmethod def to_cbor(self) -> Dict[int, Any]: - pass + cbor_data = {} + for attr, cbor_key in self.jc_map.items(): + if "." in attr: # skiped nested keys, cuz, they will be processed when we go inside submods(not a nested key as in jc_map) + continue + value = getattr(self, attr, None) + if isinstance(value, BaseJCSerializable): # trust_vector and status will be processed here + cbor_data[cbor_key] = value.to_cbor() + elif isinstance(value, dict): # submods will be processed here + nested = {} + for k, v in value.items(): + nested[k] = self._serialize_nested_dict(attr, v) + cbor_data[cbor_key] = nested + elif hasattr(value, "to_dict"): # for trust_claim + cbor_data[cbor_key] = value.to_dict() + else: + cbor_data[cbor_key] = value + return cbor_data + + def _serialize_nested_dict(self, prefix: str, d: dict) -> dict: + out = {} + for subkey, val in d.items(): + if hasattr(val, "to_cbor"): + out[self.jc_map[f"{prefix}.{subkey}"]] = val.to_cbor() + elif hasattr(val, "value"): # status with trust_tier + out[self.jc_map[f"{prefix}.{subkey}"]] = val.value + else: + out[self.jc_map.get(f"{prefix}.{subkey}", subkey)] = val + return out + + @classmethod + def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: + raise NotImplementedError("from_dict must be implemented in subclasses.") @classmethod - @abstractmethod - def from_dict(cls, data: Dict[str, Any]): - pass + def from_cbor(cls: Type[T], data: Dict[int, Any]) -> T: + kwargs = {} + reverse_map = {v: k for k, v in cls.jc_map.items()} + type_hints = get_type_hints(cls) + + for key, val in data.items(): + attr = reverse_map.get(key) + if attr is None or "." in attr: + continue + + hint = type_hints.get(attr) + + # Handle BaseJCSerializable directly + if isinstance(val, dict) and hasattr(hint, "from_cbor"): + kwargs[attr] = hint.from_cbor(val) + + # Handle Dict[str, BaseJCSerializable] or Dict[str, Any] with nested mapping + elif isinstance(val, dict) and isinstance(hint, type) and issubclass(hint, dict): + sub_hint = None + if hasattr(hint, "__args__") and len(hint.__args__) > 1: + sub_hint = hint.__args__[1] + kwargs[attr] = { + k: cls._deserialize_nested_dict(attr, v, sub_hint=sub_hint) + for k, v in val.items() + } + + else: + kwargs[attr] = val + + return cls(**kwargs) @classmethod - @abstractmethod - def from_cbor(cls, data: Dict[int, Any]): - pass + def _deserialize_nested_dict(cls, prefix: str, d: dict, sub_hint=None) -> dict: + out = {} + + # If sub_hint isn't given, try to get it from type hints + if sub_hint is None: + type_hints = get_type_hints(cls) + hint = type_hints.get(prefix) + if hasattr(hint, '__args__') and len(hint.__args__) > 1: + sub_hint = hint.__args__[1] + + for map_key, jc_key in cls.jc_map.items(): + if not map_key.startswith(f"{prefix}."): + continue + + field_name = map_key.split(".")[-1] + if jc_key in d: + val = d[jc_key] + + # Handle BaseJCSerializable subclasses inside subdict + if hasattr(sub_hint, 'from_cbor') and isinstance(val, dict): + out[field_name] = sub_hint.from_cbor(val) + elif callable(sub_hint): + out[field_name] = sub_hint(val) + else: + out[field_name] = val + + return out \ No newline at end of file diff --git a/src/claims.py b/src/claims.py index fd7bfe4..cd15a98 100644 --- a/src/claims.py +++ b/src/claims.py @@ -27,8 +27,8 @@ class AttestationResult(BaseJCSerializable): "issued_at": 6, "verifier_id": 1004, "submods": 266, - "submod.trust_vector": 1001, - "submod.status": 1000, + "submods.trust_vector": 1001, + "submods.status": 1000, } def to_dict(self) -> Dict[str, Any]: @@ -45,19 +45,19 @@ def to_dict(self) -> Dict[str, Any]: }, } - def to_cbor(self) -> Dict[int, Any]: - return { - self.jc_map["profile"]: self.profile, - self.jc_map["issued_at"]: self.issued_at, - self.jc_map["verifier_id"]: self.verifier_id.to_cbor(), - self.jc_map["submods"]: { - key: { - self.jc_map["submod.trust_vector"]: value["trust_vector"].to_cbor(), - self.jc_map["submod.status"]: value["status"].value, - } - for key, value in self.submods.items() - }, - } + # def to_cbor(self) -> Dict[int, Any]: + # return { + # self.jc_map["profile"]: self.profile, + # self.jc_map["issued_at"]: self.issued_at, + # self.jc_map["verifier_id"]: self.verifier_id.to_cbor(), + # self.jc_map["submods"]: { + # key: { + # self.jc_map["submod.trust_vector"]: value["trust_vector"].to_cbor(), + # self.jc_map["submod.status"]: value["status"].value, + # } + # for key, value in self.submods.items() + # }, + # } @classmethod def from_dict(cls, data: Dict[str, Any]): @@ -78,22 +78,22 @@ def from_dict(cls, data: Dict[str, Any]): def from_json(cls, json_str: str): return cls.from_dict(json.loads(json_str)) - @classmethod - def from_cbor(cls, data: Dict[int, Any]): - return cls( - profile=data.get(cls.jc_map["profile"], ""), - issued_at=data.get(cls.jc_map["issued_at"], 0), - verifier_id=VerifierID.from_cbor(data.get(cls.jc_map["verifier_id"], {})), - submods={ - key: { - "trust_vector": TrustVector.from_cbor( - value.get(cls.jc_map["submod.trust_vector"], {}) - ), - "status": to_trust_tier(value.get(cls.jc_map["submod.status"], 0)), - } - for key, value in data.get(cls.jc_map["submods"], {}).items() - }, - ) + # @classmethod + # def from_cbor(cls, data: Dict[int, Any]): + # return cls( + # profile=data.get(cls.jc_map["profile"], ""), + # issued_at=data.get(cls.jc_map["issued_at"], 0), + # verifier_id=VerifierID.from_cbor(data.get(cls.jc_map["verifier_id"], {})), + # submods={ + # key: { + # "trust_vector": TrustVector.from_cbor( + # value.get(cls.jc_map["submod.trust_vector"], {}) + # ), + # "status": to_trust_tier(value.get(cls.jc_map["submod.status"], 0)), + # } + # for key, value in data.get(cls.jc_map["submods"], {}).items() + # }, + # ) def validate(self): # Validates an AttestationResult object diff --git a/src/trust_vector.py b/src/trust_vector.py index 6942ce0..89882e2 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -32,12 +32,12 @@ class TrustVector(BaseJCSerializable): def to_dict(self) -> Dict[str, Any]: return asdict(self) - def to_cbor(self) -> Dict[int, Dict[str, Any]]: - return { - index: getattr(self, field).to_dict() - for field, index in self.jc_map.items() - if getattr(self, field) - } + # def to_cbor(self) -> Dict[int, Dict[str, Any]]: + # return { + # index: getattr(self, field).to_dict() + # for field, index in self.jc_map.items() + # if getattr(self, field) + # } @classmethod def from_dict(cls, data: Dict[str, Any]): @@ -47,15 +47,15 @@ def from_dict(cls, data: Dict[str, Any]): } return cls(**kwargs) - @classmethod - def from_cbor(cls, data: Dict[int, Dict[str, Any]]): - reverse_map = {v: k for k, v in cls.jc_map.items()} - kwargs = { - reverse_map[index]: TrustClaim(**value) - for index, value in data.items() - if index in reverse_map - } - return cls(**kwargs) + # @classmethod + # def from_cbor(cls, data: Dict[int, Dict[str, Any]]): + # reverse_map = {v: k for k, v in cls.jc_map.items()} + # kwargs = { + # reverse_map[index]: TrustClaim(**value) + # for index, value in data.items() + # if index in reverse_map + # } + # return cls(**kwargs) def validate(self): # Validates a TrustVector object diff --git a/src/verifier_id.py b/src/verifier_id.py index f3b6128..ee6cead 100644 --- a/src/verifier_id.py +++ b/src/verifier_id.py @@ -19,10 +19,10 @@ def to_dict(self) -> Dict[str, Any]: return asdict(self) # Convert to a dict with integer keys (for CBOR) - def to_cbor(self) -> Dict[int, str]: - return { - index: getattr(self, field) for field, index in self.jc_map.items() - } # noqa: E501 + # def to_cbor(self) -> Dict[int, str]: + # return { + # index: getattr(self, field) for field, index in self.jc_map.items() + # } # noqa: E501 # Create an instance from a dict with string keys @classmethod @@ -30,11 +30,11 @@ def from_dict(cls, data: Dict[str, str]): return cls(**data) # Create an instance from a CBOR-like dict (integer keys) - @classmethod - def from_cbor(cls, data: Dict[int, str]): - reverse_map = {v: k for k, v in cls.jc_map.items()} - kwargs = {reverse_map[index]: value for index, value in data.items()} - return cls(**kwargs) + # @classmethod + # def from_cbor(cls, data: Dict[int, str]): + # reverse_map = {v: k for k, v in cls.jc_map.items()} + # kwargs = {reverse_map[index]: value for index, value in data.items()} + # return cls(**kwargs) def validate(self): # Validates a VerifierID object diff --git a/tests/test_claims.py b/tests/test_claims.py index 24041af..ab40445 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -121,7 +121,7 @@ def test_attestation_result_to_cbor(sample_attestation_result): assert cbor_data == expected_cbor -def test_attestation_result_from_cbor(): +def test_attestation_result_from_cbor(sample_attestation_result): cbor_data = { 265: "test_profile", 6: 1234567890, @@ -142,9 +142,10 @@ def test_attestation_result_from_cbor(): } }, } + import json parsed_claims = AttestationResult.from_cbor(cbor_data) - assert parsed_claims.to_cbor() == cbor_data + assert json.dumps(parsed_claims.to_cbor(), sort_keys=True) == json.dumps(cbor_data, sort_keys=True) def test_validate_ear_claims(sample_attestation_result): From b6ed9ecbd9e53c9e4c8827b94b71b3be9769292c Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Mon, 14 Apr 2025 05:07:00 +0530 Subject: [PATCH 14/16] Make from_ method more generalizable for custom classes Signed-off-by: HarshvMahawar --- src/base.py | 160 +++++++++++++++++------------------- src/claims.py | 94 +++------------------ src/example/jwt_example.py | 17 ++-- src/example/jwt_output.json | 28 ++----- src/submod.py | 13 +++ src/trust_claims.py | 4 +- src/trust_vector.py | 48 +++-------- src/verifier_id.py | 28 +------ tests/test_claims.py | 125 ++++++++++++++-------------- tests/test_trust_vector.py | 60 +++++++------- tests/test_verifier_id.py | 10 +-- 11 files changed, 233 insertions(+), 354 deletions(-) create mode 100644 src/submod.py diff --git a/src/base.py b/src/base.py index 8b3709e..7dcef08 100644 --- a/src/base.py +++ b/src/base.py @@ -1,109 +1,97 @@ -from abc import ABC, abstractmethod -from typing import Any, Dict, Type, TypeVar, get_type_hints import json +from abc import ABC +from typing import Any, ClassVar, Dict, Tuple, Type, TypeVar, Union, get_args T = TypeVar("T", bound="BaseJCSerializable") -class BaseJCSerializable(ABC): - jc_map: Dict[str, int] - def to_dict(self) -> Dict[str, Any]: - raise NotImplementedError("to_dict must be implemented in subclasses.") +def to_data(value: Any, keys_as_int=False) -> Any: + if hasattr(value, "to_data"): + return value.to_data(keys_as_int) + if hasattr(value, "items"): # dict-like + return { + to_data(k, keys_as_int): to_data(v, keys_as_int) for k, v in value.items() + } + if hasattr(value, "__iter__") and not isinstance(value, str): # list-like + return [to_data(v, keys_as_int) for v in value] - def to_json(self) -> str: - return json.dumps(self.to_dict()) + if hasattr( + value, "value" + ): # custom classes that have value attr but don't have 'to_data' + return value.value # type: ignore[attr-defined] + # scalar and no to_data(), so assume serializable as-is + return value - def to_cbor(self) -> Dict[int, Any]: - cbor_data = {} - for attr, cbor_key in self.jc_map.items(): - if "." in attr: # skiped nested keys, cuz, they will be processed when we go inside submods(not a nested key as in jc_map) - continue - value = getattr(self, attr, None) - if isinstance(value, BaseJCSerializable): # trust_vector and status will be processed here - cbor_data[cbor_key] = value.to_cbor() - elif isinstance(value, dict): # submods will be processed here - nested = {} - for k, v in value.items(): - nested[k] = self._serialize_nested_dict(attr, v) - cbor_data[cbor_key] = nested - elif hasattr(value, "to_dict"): # for trust_claim - cbor_data[cbor_key] = value.to_dict() - else: - cbor_data[cbor_key] = value - return cbor_data - - def _serialize_nested_dict(self, prefix: str, d: dict) -> dict: - out = {} - for subkey, val in d.items(): - if hasattr(val, "to_cbor"): - out[self.jc_map[f"{prefix}.{subkey}"]] = val.to_cbor() - elif hasattr(val, "value"): # status with trust_tier - out[self.jc_map[f"{prefix}.{subkey}"]] = val.value - else: - out[self.jc_map.get(f"{prefix}.{subkey}", subkey)] = val - return out - @classmethod - def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: - raise NotImplementedError("from_dict must be implemented in subclasses.") +class BaseJCSerializable(ABC): + jc_map: ClassVar[Dict[str, Tuple[int, str]]] + + def to_data(self, keys_as_int=False) -> Dict[Union[str, int], Any]: + return { + (int_key if keys_as_int else str_key): to_data( + getattr(self, attr), keys_as_int + ) + for attr, (int_key, str_key) in self.jc_map.items() + } @classmethod - def from_cbor(cls: Type[T], data: Dict[int, Any]) -> T: - kwargs = {} - reverse_map = {v: k for k, v in cls.jc_map.items()} - type_hints = get_type_hints(cls) - - for key, val in data.items(): - attr = reverse_map.get(key) - if attr is None or "." in attr: + def from_data(cls: Type[T], data: dict, keys_as_int=False) -> T: + + if keys_as_int: + index = 0 + else: + index = 1 + init_kwargs = {} + reverse_map = {v[index]: k for k, v in cls.jc_map.items()} + for key, value in data.items(): + if key not in reverse_map: continue - hint = type_hints.get(attr) + attr = reverse_map[key] + field_type = getattr(cls, "__annotations__", {}).get(attr) + if field_type is None: + continue - # Handle BaseJCSerializable directly - if isinstance(val, dict) and hasattr(hint, "from_cbor"): - kwargs[attr] = hint.from_cbor(val) + args = get_args(field_type) - # Handle Dict[str, BaseJCSerializable] or Dict[str, Any] with nested mapping - elif isinstance(val, dict) and isinstance(hint, type) and issubclass(hint, dict): - sub_hint = None - if hasattr(hint, "__args__") and len(hint.__args__) > 1: - sub_hint = hint.__args__[1] - kwargs[attr] = { - k: cls._deserialize_nested_dict(attr, v, sub_hint=sub_hint) - for k, v in val.items() + if hasattr(field_type, "from_data"): + # Direct object + init_kwargs[attr] = field_type.from_data(value, keys_as_int=keys_as_int) + + elif hasattr(field_type, "items") and hasattr(args[1], "from_data"): + # Dict[str | int, CustomClass] + init_kwargs[attr] = { + k: args[1].from_data(v, keys_as_int=keys_as_int) + for k, v in value.items() } + elif args: + # custom classes that dont have 'from_data' + init_kwargs[attr] = args[0](value) + else: - kwargs[attr] = val + init_kwargs[attr] = field_type(value) + + return cls(**init_kwargs) + + def to_dict(self) -> Dict[str, Any]: + # default str_keys + return self.to_data() # type: ignore[return-value] # pyright: ignore[reportGeneralTypeIssues] # noqa: E501 # pylint: disable=line-too-long - return cls(**kwargs) + def to_int_keys(self) -> Dict[Union[str, int], Any]: + return self.to_data(keys_as_int=True) @classmethod - def _deserialize_nested_dict(cls, prefix: str, d: dict, sub_hint=None) -> dict: - out = {} - - # If sub_hint isn't given, try to get it from type hints - if sub_hint is None: - type_hints = get_type_hints(cls) - hint = type_hints.get(prefix) - if hasattr(hint, '__args__') and len(hint.__args__) > 1: - sub_hint = hint.__args__[1] - - for map_key, jc_key in cls.jc_map.items(): - if not map_key.startswith(f"{prefix}."): - continue + def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: + return cls.from_data(data) - field_name = map_key.split(".")[-1] - if jc_key in d: - val = d[jc_key] + @classmethod + def from_int_keys(cls: Type[T], data: Dict[int, Any]) -> T: + return cls.from_data(data, keys_as_int=True) - # Handle BaseJCSerializable subclasses inside subdict - if hasattr(sub_hint, 'from_cbor') and isinstance(val, dict): - out[field_name] = sub_hint.from_cbor(val) - elif callable(sub_hint): - out[field_name] = sub_hint(val) - else: - out[field_name] = val + @classmethod + def from_json(cls, json_str: str): + return cls.from_dict(json.loads(json_str)) - return out \ No newline at end of file + def to_json(self): + return json.dumps(self.to_data()) diff --git a/src/claims.py b/src/claims.py index cd15a98..77e122c 100644 --- a/src/claims.py +++ b/src/claims.py @@ -1,15 +1,13 @@ -import json from dataclasses import dataclass, field from datetime import datetime, timedelta -from typing import Any, Dict +from typing import Dict from jose import jwt # type: ignore # pylint: disable=import-error from src.base import BaseJCSerializable from src.errors import EARValidationError from src.jwt_config import DEFAULT_ALGORITHM, DEFAULT_EXPIRATION_MINUTES -from src.trust_tier import to_trust_tier -from src.trust_vector import TrustVector +from src.submod import Submod from src.verifier_id import VerifierID @@ -19,82 +17,16 @@ class AttestationResult(BaseJCSerializable): profile: str issued_at: int verifier_id: VerifierID - submods: Dict[str, Dict[str, Any]] = field(default_factory=dict) + submods: Dict[str, Submod] = field(default_factory=dict) # https://www.ietf.org/archive/id/draft-ietf-rats-eat-31.html#section-7.2.4 jc_map = { - "profile": 265, - "issued_at": 6, - "verifier_id": 1004, - "submods": 266, - "submods.trust_vector": 1001, - "submods.status": 1000, + "profile": (265, "profile"), + "issued_at": (6, "issued_at"), + "verifier_id": (1004, "verifier_id"), + "submods": (266, "submods"), } - def to_dict(self) -> Dict[str, Any]: - return { - "eat_profile": self.profile, - "iat": self.issued_at, - "ear.verifier-id": self.verifier_id.to_dict(), - "submods": { - key: { - "trust_vector": value["trust_vector"].to_dict(), - "status": value["status"].value, - } - for key, value in self.submods.items() - }, - } - - # def to_cbor(self) -> Dict[int, Any]: - # return { - # self.jc_map["profile"]: self.profile, - # self.jc_map["issued_at"]: self.issued_at, - # self.jc_map["verifier_id"]: self.verifier_id.to_cbor(), - # self.jc_map["submods"]: { - # key: { - # self.jc_map["submod.trust_vector"]: value["trust_vector"].to_cbor(), - # self.jc_map["submod.status"]: value["status"].value, - # } - # for key, value in self.submods.items() - # }, - # } - - @classmethod - def from_dict(cls, data: Dict[str, Any]): - return cls( - profile=data.get("eat_profile", ""), - issued_at=data.get("iat", 0), - verifier_id=VerifierID.from_dict(data.get("ear.verifier-id", {})), - submods={ - key: { - "trust_vector": TrustVector.from_dict(value["trust_vector"]), - "status": to_trust_tier(value["status"]), - } - for key, value in data.get("submods", {}).items() - }, - ) - - @classmethod - def from_json(cls, json_str: str): - return cls.from_dict(json.loads(json_str)) - - # @classmethod - # def from_cbor(cls, data: Dict[int, Any]): - # return cls( - # profile=data.get(cls.jc_map["profile"], ""), - # issued_at=data.get(cls.jc_map["issued_at"], 0), - # verifier_id=VerifierID.from_cbor(data.get(cls.jc_map["verifier_id"], {})), - # submods={ - # key: { - # "trust_vector": TrustVector.from_cbor( - # value.get(cls.jc_map["submod.trust_vector"], {}) - # ), - # "status": to_trust_tier(value.get(cls.jc_map["submod.status"], 0)), - # } - # for key, value in data.get(cls.jc_map["submods"], {}).items() - # }, - # ) - def validate(self): # Validates an AttestationResult object if not isinstance(self.profile, str) or not self.profile: @@ -109,16 +41,12 @@ def validate(self): self.verifier_id.validate() for submod, details in self.submods.items(): - if ( - not isinstance(details, Dict) - or "trust_vector" not in details - or "status" not in details - ): + if not isinstance(details, Submod): raise EARValidationError( f"Submodule {submod} must contain a valid trust_vector and status" ) - trust_vector = details["trust_vector"] + trust_vector = details.trust_vector trust_vector.validate() def encode_jwt( @@ -132,7 +60,9 @@ def encode_jwt( payload["exp"] = int( datetime.timestamp(datetime.now() + timedelta(minutes=expiration_minutes)) ) - return jwt.encode(payload, secret_key, algorithm=algorithm) + return jwt.encode( + payload, secret_key, algorithm=algorithm + ) # pyright: ignore[reportGeneralTypeIssues] @classmethod def decode_jwt( diff --git a/src/example/jwt_example.py b/src/example/jwt_example.py index e413a3a..8a8dde3 100644 --- a/src/example/jwt_example.py +++ b/src/example/jwt_example.py @@ -2,6 +2,7 @@ from src.claims import AttestationResult from src.jwt_config import generate_secret_key +from src.submod import Submod from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM, UNRECOGNIZED_INSTANCE_CLAIM from src.trust_tier import TRUST_TIER_AFFIRMING, TRUST_TIER_CONTRAINDICATED from src.trust_vector import TrustVector @@ -18,14 +19,14 @@ issued_at=int(datetime.timestamp(datetime.now())), verifier_id=VerifierID(developer="Acme Inc.", build="v1"), submods={ - "submod1": { - "trust_vector": TrustVector(instance_identity=UNRECOGNIZED_INSTANCE_CLAIM), - "status": TRUST_TIER_AFFIRMING, - }, - "submod2": { - "trust_vector": TrustVector(instance_identity=TRUSTWORTHY_INSTANCE_CLAIM), - "status": TRUST_TIER_CONTRAINDICATED, - }, + "submod1": Submod( + trust_vector=TrustVector(instance_identity=UNRECOGNIZED_INSTANCE_CLAIM), + status=TRUST_TIER_AFFIRMING, + ), + "submod2": Submod( + trust_vector=TrustVector(instance_identity=TRUSTWORTHY_INSTANCE_CLAIM), + status=TRUST_TIER_CONTRAINDICATED, + ), }, ) diff --git a/src/example/jwt_output.json b/src/example/jwt_output.json index 1fe24a7..c87caea 100644 --- a/src/example/jwt_output.json +++ b/src/example/jwt_output.json @@ -1,19 +1,15 @@ { - "eat_profile": "test_profile", - "iat": 1743076236, - "ear.verifier-id": { + "profile": "test_profile", + "issued_at": 1744932192, + "verifier_id": { "developer": "Acme Inc.", "build": "v1" }, "submods": { "submod1": { + "status": 2, "trust_vector": { - "instance_identity": { - "value": 97, - "tag": "unrecognized_instance", - "short": "not recognized", - "long": "The Attesting Environment is not recognized; however the Verifier believes it should be." - }, + "instance_identity": 97, "configuration": null, "executables": null, "file_system": null, @@ -21,17 +17,12 @@ "runtime_opaque": null, "storage_opaque": null, "sourced_data": null - }, - "status": 2 + } }, "submod2": { + "status": 96, "trust_vector": { - "instance_identity": { - "value": 2, - "tag": "recognized_instance", - "short": "recognized and not compromised", - "long": "The Attesting Environment is recognized, and the associated instance of the Attester is not known to be compromised." - }, + "instance_identity": 2, "configuration": null, "executables": null, "file_system": null, @@ -39,8 +30,7 @@ "runtime_opaque": null, "storage_opaque": null, "sourced_data": null - }, - "status": 96 + } } } } \ No newline at end of file diff --git a/src/submod.py b/src/submod.py new file mode 100644 index 0000000..c4bdbf9 --- /dev/null +++ b/src/submod.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + +from src.base import BaseJCSerializable +from src.trust_tier import TrustTier +from src.trust_vector import TrustVector + + +@dataclass +class Submod(BaseJCSerializable): + trust_vector: TrustVector + status: TrustTier + + jc_map = {"status": (1000, "status"), "trust_vector": (1001, "trust_vector")} diff --git a/src/trust_claims.py b/src/trust_claims.py index 39f4d01..9ac7fa6 100644 --- a/src/trust_claims.py +++ b/src/trust_claims.py @@ -7,7 +7,8 @@ # https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-2.3 @dataclass class TrustClaim: - value: int # must be in range -128 to 127 + # every trustclaim will be transported in form of its value only + value: int # must be in range -128 to 127, tag: str = "" short: str = "" long: str = "" @@ -30,6 +31,7 @@ def validate(self): raise EARValidationError("TrustClaim long description must be a string") +# Mapping value to TrustClaim types # General VERIFIER_MALFUNCTION_CLAIM = TrustClaim( value=-1, diff --git a/src/trust_vector.py b/src/trust_vector.py index 89882e2..91a8866 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -1,5 +1,5 @@ -from dataclasses import asdict, dataclass -from typing import Any, Dict, Optional +from dataclasses import dataclass +from typing import Optional from src.base import BaseJCSerializable from src.trust_claims import TrustClaim @@ -19,44 +19,16 @@ class TrustVector(BaseJCSerializable): sourced_data: Optional[TrustClaim] = None jc_map = { - "instance_identity": 0, - "configuration": 1, - "executables": 2, - "file_system": 3, - "hardware": 4, - "runtime_opaque": 5, - "storage_opaque": 6, - "sourced_data": 7, + "instance_identity": (0, "instance_identity"), + "configuration": (1, "configuration"), + "executables": (2, "executables"), + "file_system": (3, "file_system"), + "hardware": (4, "hardware"), + "runtime_opaque": (5, "runtime_opaque"), + "storage_opaque": (6, "storage_opaque"), + "sourced_data": (7, "sourced_data"), } - def to_dict(self) -> Dict[str, Any]: - return asdict(self) - - # def to_cbor(self) -> Dict[int, Dict[str, Any]]: - # return { - # index: getattr(self, field).to_dict() - # for field, index in self.jc_map.items() - # if getattr(self, field) - # } - - @classmethod - def from_dict(cls, data: Dict[str, Any]): - kwargs = { - field: TrustClaim(**data[field]) if data.get(field) else None - for field in cls.jc_map - } - return cls(**kwargs) - - # @classmethod - # def from_cbor(cls, data: Dict[int, Dict[str, Any]]): - # reverse_map = {v: k for k, v in cls.jc_map.items()} - # kwargs = { - # reverse_map[index]: TrustClaim(**value) - # for index, value in data.items() - # if index in reverse_map - # } - # return cls(**kwargs) - def validate(self): # Validates a TrustVector object diff --git a/src/verifier_id.py b/src/verifier_id.py index ee6cead..686d735 100644 --- a/src/verifier_id.py +++ b/src/verifier_id.py @@ -1,5 +1,4 @@ -from dataclasses import asdict, dataclass -from typing import Any, Dict +from dataclasses import dataclass from src.base import BaseJCSerializable from src.errors import EARValidationError @@ -11,31 +10,10 @@ class VerifierID(BaseJCSerializable): developer: str build: str jc_map = { - "developer": 0, # JC<"developer", 0> - "build": 1, # JC<"build", 1> + "developer": (0, "developer"), # JC<"developer", 0> + "build": (1, "build"), # JC<"build", 1> } - def to_dict(self) -> Dict[str, Any]: - return asdict(self) - - # Convert to a dict with integer keys (for CBOR) - # def to_cbor(self) -> Dict[int, str]: - # return { - # index: getattr(self, field) for field, index in self.jc_map.items() - # } # noqa: E501 - - # Create an instance from a dict with string keys - @classmethod - def from_dict(cls, data: Dict[str, str]): - return cls(**data) - - # Create an instance from a CBOR-like dict (integer keys) - # @classmethod - # def from_cbor(cls, data: Dict[int, str]): - # reverse_map = {v: k for k, v in cls.jc_map.items()} - # kwargs = {reverse_map[index]: value for index, value in data.items()} - # return cls(**kwargs) - def validate(self): # Validates a VerifierID object if not self.developer or not isinstance(self.developer, str): diff --git a/tests/test_claims.py b/tests/test_claims.py index ab40445..211d489 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -1,7 +1,10 @@ +import json + import pytest from src.claims import AttestationResult from src.errors import EARValidationError +from src.submod import Submod from src.trust_claims import ( APPROVED_CONFIG_CLAIM, APPROVED_FILES_CLAIM, @@ -11,6 +14,7 @@ HW_KEYS_ENCRYPTED_SECRETS_CLAIM, TRUSTED_SOURCES_CLAIM, TRUSTWORTHY_INSTANCE_CLAIM, + TrustClaim, ) from src.trust_tier import TRUST_TIER_AFFIRMING from src.trust_vector import TrustVector @@ -24,39 +28,39 @@ def sample_attestation_result(): issued_at=1234567890, verifier_id=VerifierID(developer="Acme Inc.", build="v1"), submods={ - "submod1": { - "trust_vector": TrustVector( - instance_identity=TRUSTWORTHY_INSTANCE_CLAIM, - configuration=APPROVED_CONFIG_CLAIM, - executables=APPROVED_RUNTIME_CLAIM, - file_system=APPROVED_FILES_CLAIM, - hardware=GENUINE_HARDWARE_CLAIM, - runtime_opaque=ENCRYPTED_MEMORY_RUNTIME_CLAIM, - storage_opaque=HW_KEYS_ENCRYPTED_SECRETS_CLAIM, - sourced_data=TRUSTED_SOURCES_CLAIM, + "submod1": Submod( + trust_vector=TrustVector( + instance_identity=TrustClaim(2), + configuration=TrustClaim(2), + executables=TrustClaim(2), + file_system=TrustClaim(2), + hardware=TrustClaim(2), + runtime_opaque=TrustClaim(2), + storage_opaque=TrustClaim(2), + sourced_data=TrustClaim(2), ), - "status": TRUST_TIER_AFFIRMING, - } + status=TRUST_TIER_AFFIRMING, + ), }, ) def test_attestation_result_to_dict(sample_attestation_result): expected = { - "eat_profile": "test_profile", - "iat": 1234567890, - "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"}, + "profile": "test_profile", + "issued_at": 1234567890, + "verifier_id": {"developer": "Acme Inc.", "build": "v1"}, "submods": { "submod1": { "trust_vector": { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - "configuration": APPROVED_CONFIG_CLAIM.to_dict(), - "executables": APPROVED_RUNTIME_CLAIM.to_dict(), - "file_system": APPROVED_FILES_CLAIM.to_dict(), - "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "configuration": APPROVED_CONFIG_CLAIM.value, + "executables": APPROVED_RUNTIME_CLAIM.value, + "file_system": APPROVED_FILES_CLAIM.value, + "hardware": GENUINE_HARDWARE_CLAIM.value, + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced_data": TRUSTED_SOURCES_CLAIM.value, }, "status": TRUST_TIER_AFFIRMING.value, } @@ -67,26 +71,26 @@ def test_attestation_result_to_dict(sample_attestation_result): def test_attestation_result_to_json(sample_attestation_result): json_str = sample_attestation_result.to_json() - parsed_claims = AttestationResult.from_json(json_str) + parsed_claims = AttestationResult.from_json(json_str=json_str) assert parsed_claims.to_dict() == sample_attestation_result.to_dict() def test_attestation_result_from_dict(): data = { - "eat_profile": "test_profile", - "iat": 1234567890, - "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"}, + "profile": "test_profile", + "issued_at": 1234567890, + "verifier_id": {"developer": "Acme Inc.", "build": "v1"}, "submods": { "submod1": { "trust_vector": { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - "configuration": APPROVED_CONFIG_CLAIM.to_dict(), - "executables": APPROVED_RUNTIME_CLAIM.to_dict(), - "file_system": APPROVED_FILES_CLAIM.to_dict(), - "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "configuration": APPROVED_CONFIG_CLAIM.value, + "executables": APPROVED_RUNTIME_CLAIM.value, + "file_system": APPROVED_FILES_CLAIM.value, + "hardware": GENUINE_HARDWARE_CLAIM.value, + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced_data": TRUSTED_SOURCES_CLAIM.value, }, "status": TRUST_TIER_AFFIRMING.value, } @@ -96,56 +100,57 @@ def test_attestation_result_from_dict(): assert parsed_claims.to_dict() == data -def test_attestation_result_to_cbor(sample_attestation_result): - cbor_data = sample_attestation_result.to_cbor() - expected_cbor = { +def test_attestation_result_to_int_keys(sample_attestation_result): + int_keys_data = sample_attestation_result.to_int_keys() + expected_int_keys = { 265: "test_profile", 6: 1234567890, 1004: {0: "Acme Inc.", 1: "v1"}, 266: { "submod1": { 1001: { - 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - 1: APPROVED_CONFIG_CLAIM.to_dict(), - 2: APPROVED_RUNTIME_CLAIM.to_dict(), - 3: APPROVED_FILES_CLAIM.to_dict(), - 4: GENUINE_HARDWARE_CLAIM.to_dict(), - 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - 7: TRUSTED_SOURCES_CLAIM.to_dict(), + 0: TRUSTWORTHY_INSTANCE_CLAIM.value, + 1: APPROVED_CONFIG_CLAIM.value, + 2: APPROVED_RUNTIME_CLAIM.value, + 3: APPROVED_FILES_CLAIM.value, + 4: GENUINE_HARDWARE_CLAIM.value, + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + 7: TRUSTED_SOURCES_CLAIM.value, }, 1000: TRUST_TIER_AFFIRMING.value, } }, } - assert cbor_data == expected_cbor + assert int_keys_data == expected_int_keys -def test_attestation_result_from_cbor(sample_attestation_result): - cbor_data = { +def test_attestation_result_from_int_keys(): + int_keys_data = { 265: "test_profile", 6: 1234567890, 1004: {0: "Acme Inc.", 1: "v1"}, 266: { "submod1": { 1001: { - 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - 1: APPROVED_CONFIG_CLAIM.to_dict(), - 2: APPROVED_RUNTIME_CLAIM.to_dict(), - 3: APPROVED_FILES_CLAIM.to_dict(), - 4: GENUINE_HARDWARE_CLAIM.to_dict(), - 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - 7: TRUSTED_SOURCES_CLAIM.to_dict(), + 0: TRUSTWORTHY_INSTANCE_CLAIM.value, + 1: APPROVED_CONFIG_CLAIM.value, + 2: APPROVED_RUNTIME_CLAIM.value, + 3: APPROVED_FILES_CLAIM.value, + 4: GENUINE_HARDWARE_CLAIM.value, + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + 7: TRUSTED_SOURCES_CLAIM.value, }, 1000: TRUST_TIER_AFFIRMING.value, } }, } - import json - parsed_claims = AttestationResult.from_cbor(cbor_data) - assert json.dumps(parsed_claims.to_cbor(), sort_keys=True) == json.dumps(cbor_data, sort_keys=True) + parsed_claims = AttestationResult.from_int_keys(int_keys_data) + assert json.dumps(parsed_claims.to_int_keys(), sort_keys=True) == json.dumps( + int_keys_data, sort_keys=True + ) def test_validate_ear_claims(sample_attestation_result): diff --git a/tests/test_trust_vector.py b/tests/test_trust_vector.py index 249ca48..a758c21 100644 --- a/tests/test_trust_vector.py +++ b/tests/test_trust_vector.py @@ -33,14 +33,14 @@ def sample_trust_vector(): def test_trust_vector_to_dict(sample_trust_vector): expected = { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - "configuration": UNSAFE_CONFIG_CLAIM.to_dict(), - "executables": APPROVED_RUNTIME_CLAIM.to_dict(), - "file_system": APPROVED_FILES_CLAIM.to_dict(), - "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "configuration": UNSAFE_CONFIG_CLAIM.value, + "executables": APPROVED_RUNTIME_CLAIM.value, + "file_system": APPROVED_FILES_CLAIM.value, + "hardware": GENUINE_HARDWARE_CLAIM.value, + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced_data": TRUSTED_SOURCES_CLAIM.value, } assert sample_trust_vector.to_dict() == expected @@ -51,37 +51,37 @@ def test_trust_vector_to_json(sample_trust_vector): assert parsed_vector.to_dict() == sample_trust_vector.to_dict() -def test_trust_vector_to_cbor(sample_trust_vector): +def test_trust_vector_to_int_keys(sample_trust_vector): expected = { - 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - 1: UNSAFE_CONFIG_CLAIM.to_dict(), - 2: APPROVED_RUNTIME_CLAIM.to_dict(), - 3: APPROVED_FILES_CLAIM.to_dict(), - 4: GENUINE_HARDWARE_CLAIM.to_dict(), - 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - 7: TRUSTED_SOURCES_CLAIM.to_dict(), + 0: TRUSTWORTHY_INSTANCE_CLAIM.value, + 1: UNSAFE_CONFIG_CLAIM.value, + 2: APPROVED_RUNTIME_CLAIM.value, + 3: APPROVED_FILES_CLAIM.value, + 4: GENUINE_HARDWARE_CLAIM.value, + 5: ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + 7: TRUSTED_SOURCES_CLAIM.value, } - assert sample_trust_vector.to_cbor() == expected + assert sample_trust_vector.to_int_keys() == expected def test_trust_vector_from_dict(): data = { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), - "configuration": UNSAFE_CONFIG_CLAIM.to_dict(), - "executables": APPROVED_RUNTIME_CLAIM.to_dict(), - "file_system": APPROVED_FILES_CLAIM.to_dict(), - "hardware": GENUINE_HARDWARE_CLAIM.to_dict(), - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.to_dict(), - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), - "sourced_data": TRUSTED_SOURCES_CLAIM.to_dict(), + "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "configuration": UNSAFE_CONFIG_CLAIM.value, + "executables": APPROVED_RUNTIME_CLAIM.value, + "file_system": APPROVED_FILES_CLAIM.value, + "hardware": GENUINE_HARDWARE_CLAIM.value, + "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced_data": TRUSTED_SOURCES_CLAIM.value, } parsed_vector = TrustVector.from_dict(data) assert parsed_vector.to_dict() == data -def test_trust_vector_from_cbor(): - cbor_data = { +def test_trust_vector_from_int_keys(): + int_keys_data = { 0: TRUSTWORTHY_INSTANCE_CLAIM.to_dict(), 1: UNSAFE_CONFIG_CLAIM.to_dict(), 2: APPROVED_RUNTIME_CLAIM.to_dict(), @@ -91,9 +91,9 @@ def test_trust_vector_from_cbor(): 6: HW_KEYS_ENCRYPTED_SECRETS_CLAIM.to_dict(), 7: TRUSTED_SOURCES_CLAIM.to_dict(), } - parsed_vector = TrustVector.from_cbor(cbor_data) + parsed_vector = TrustVector.from_int_keys(int_keys_data) assert ( - parsed_vector.to_dict() == TrustVector.from_cbor(cbor_data).to_dict() + parsed_vector.to_dict() == TrustVector.from_int_keys(int_keys_data).to_dict() ) # noqa: E501 diff --git a/tests/test_verifier_id.py b/tests/test_verifier_id.py index 22ac7a5..5476203 100644 --- a/tests/test_verifier_id.py +++ b/tests/test_verifier_id.py @@ -22,9 +22,9 @@ def test_to_json(verifier): # pylint: disable=redefined-outer-name assert verifier.to_json() == expected -def test_to_cbor(verifier): # pylint: disable=redefined-outer-name +def test_to_int_keys(verifier): # pylint: disable=redefined-outer-name expected = {0: "Acme Inc.", 1: "v1.0.0"} - assert verifier.to_cbor() == expected + assert verifier.to_int_keys() == expected def test_from_dict(): @@ -34,9 +34,9 @@ def test_from_dict(): assert sample_verifier.build == "v1.0.0" -def test_from_cbor(): - cbor_data = {0: "Acme Inc.", 1: "v1.0.0"} - sample_verifier = VerifierID.from_cbor(cbor_data) +def test_from_int_keys(): + int_keys_data = {0: "Acme Inc.", 1: "v1.0.0"} + sample_verifier = VerifierID.from_int_keys(int_keys_data) assert sample_verifier.developer == "Acme Inc." assert sample_verifier.build == "v1.0.0" From bc092272698f6519146f0ade4cfb340b300ab8bb Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Tue, 10 Jun 2025 12:05:27 +0530 Subject: [PATCH 15/16] Add namedtuple for jc_map entries Signed-off-by: HarshvMahawar --- src/base.py | 14 ++++++++------ src/claims.py | 10 +++++----- src/submod.py | 7 +++++-- src/trust_vector.py | 18 +++++++++--------- src/verifier_id.py | 6 +++--- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/base.py b/src/base.py index 7dcef08..ae506f1 100644 --- a/src/base.py +++ b/src/base.py @@ -1,9 +1,12 @@ import json from abc import ABC +from collections import namedtuple from typing import Any, ClassVar, Dict, Tuple, Type, TypeVar, Union, get_args T = TypeVar("T", bound="BaseJCSerializable") +KeyMapping = namedtuple("KeyMapping", ["int_key", "str_key"]) + def to_data(value: Any, keys_as_int=False) -> Any: if hasattr(value, "to_data"): @@ -36,13 +39,12 @@ def to_data(self, keys_as_int=False) -> Dict[Union[str, int], Any]: @classmethod def from_data(cls: Type[T], data: dict, keys_as_int=False) -> T: - - if keys_as_int: - index = 0 - else: - index = 1 + key_attr = "int_key" if keys_as_int else "str_key" init_kwargs = {} - reverse_map = {v[index]: k for k, v in cls.jc_map.items()} + reverse_map = { + getattr(mapping, key_attr): attr for attr, mapping in cls.jc_map.items() + } + for key, value in data.items(): if key not in reverse_map: continue diff --git a/src/claims.py b/src/claims.py index 77e122c..6b4e477 100644 --- a/src/claims.py +++ b/src/claims.py @@ -4,7 +4,7 @@ from jose import jwt # type: ignore # pylint: disable=import-error -from src.base import BaseJCSerializable +from src.base import BaseJCSerializable, KeyMapping from src.errors import EARValidationError from src.jwt_config import DEFAULT_ALGORITHM, DEFAULT_EXPIRATION_MINUTES from src.submod import Submod @@ -21,10 +21,10 @@ class AttestationResult(BaseJCSerializable): # https://www.ietf.org/archive/id/draft-ietf-rats-eat-31.html#section-7.2.4 jc_map = { - "profile": (265, "profile"), - "issued_at": (6, "issued_at"), - "verifier_id": (1004, "verifier_id"), - "submods": (266, "submods"), + "profile": KeyMapping(265, "profile"), + "issued_at": KeyMapping(6, "issued_at"), + "verifier_id": KeyMapping(1004, "verifier_id"), + "submods": KeyMapping(266, "submods"), } def validate(self): diff --git a/src/submod.py b/src/submod.py index c4bdbf9..f40f4c4 100644 --- a/src/submod.py +++ b/src/submod.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from src.base import BaseJCSerializable +from src.base import BaseJCSerializable, KeyMapping from src.trust_tier import TrustTier from src.trust_vector import TrustVector @@ -10,4 +10,7 @@ class Submod(BaseJCSerializable): trust_vector: TrustVector status: TrustTier - jc_map = {"status": (1000, "status"), "trust_vector": (1001, "trust_vector")} + jc_map = { + "status": KeyMapping(1000, "status"), + "trust_vector": KeyMapping(1001, "trust_vector"), + } diff --git a/src/trust_vector.py b/src/trust_vector.py index 91a8866..eb6ef21 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Optional -from src.base import BaseJCSerializable +from src.base import BaseJCSerializable, KeyMapping from src.trust_claims import TrustClaim @@ -19,14 +19,14 @@ class TrustVector(BaseJCSerializable): sourced_data: Optional[TrustClaim] = None jc_map = { - "instance_identity": (0, "instance_identity"), - "configuration": (1, "configuration"), - "executables": (2, "executables"), - "file_system": (3, "file_system"), - "hardware": (4, "hardware"), - "runtime_opaque": (5, "runtime_opaque"), - "storage_opaque": (6, "storage_opaque"), - "sourced_data": (7, "sourced_data"), + "instance_identity": KeyMapping(0, "instance_identity"), + "configuration": KeyMapping(1, "configuration"), + "executables": KeyMapping(2, "executables"), + "file_system": KeyMapping(3, "file_system"), + "hardware": KeyMapping(4, "hardware"), + "runtime_opaque": KeyMapping(5, "runtime_opaque"), + "storage_opaque": KeyMapping(6, "storage_opaque"), + "sourced_data": KeyMapping(7, "sourced_data"), } def validate(self): diff --git a/src/verifier_id.py b/src/verifier_id.py index 686d735..f450f01 100644 --- a/src/verifier_id.py +++ b/src/verifier_id.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from src.base import BaseJCSerializable +from src.base import BaseJCSerializable, KeyMapping from src.errors import EARValidationError @@ -10,8 +10,8 @@ class VerifierID(BaseJCSerializable): developer: str build: str jc_map = { - "developer": (0, "developer"), # JC<"developer", 0> - "build": (1, "build"), # JC<"build", 1> + "developer": KeyMapping(0, "developer"), # JC<"developer", 0> + "build": KeyMapping(1, "build"), # JC<"build", 1> } def validate(self): From 3e6eb747378050ed8081bbd0f367f5e0e6855e8d Mon Sep 17 00:00:00 2001 From: HarshvMahawar Date: Mon, 16 Jun 2025 15:42:18 +0530 Subject: [PATCH 16/16] Align claim names with RFC spec Signed-off-by: HarshvMahawar --- src/claims.py | 6 +++--- src/submod.py | 4 ++-- src/trust_vector.py | 10 +++++----- tests/test_claims.py | 40 +++++++++++++++++++------------------- tests/test_trust_vector.py | 20 +++++++++---------- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/claims.py b/src/claims.py index 6b4e477..e5416fb 100644 --- a/src/claims.py +++ b/src/claims.py @@ -21,9 +21,9 @@ class AttestationResult(BaseJCSerializable): # https://www.ietf.org/archive/id/draft-ietf-rats-eat-31.html#section-7.2.4 jc_map = { - "profile": KeyMapping(265, "profile"), - "issued_at": KeyMapping(6, "issued_at"), - "verifier_id": KeyMapping(1004, "verifier_id"), + "profile": KeyMapping(265, "eat_profile"), + "issued_at": KeyMapping(6, "iat"), + "verifier_id": KeyMapping(1004, "ear.verifier-id"), "submods": KeyMapping(266, "submods"), } diff --git a/src/submod.py b/src/submod.py index f40f4c4..7ba1608 100644 --- a/src/submod.py +++ b/src/submod.py @@ -11,6 +11,6 @@ class Submod(BaseJCSerializable): status: TrustTier jc_map = { - "status": KeyMapping(1000, "status"), - "trust_vector": KeyMapping(1001, "trust_vector"), + "status": KeyMapping(1000, "ear.status"), + "trust_vector": KeyMapping(1001, "ear.trustworthiness-vector"), } diff --git a/src/trust_vector.py b/src/trust_vector.py index eb6ef21..a55f1a8 100644 --- a/src/trust_vector.py +++ b/src/trust_vector.py @@ -19,14 +19,14 @@ class TrustVector(BaseJCSerializable): sourced_data: Optional[TrustClaim] = None jc_map = { - "instance_identity": KeyMapping(0, "instance_identity"), + "instance_identity": KeyMapping(0, "instance-identity"), "configuration": KeyMapping(1, "configuration"), "executables": KeyMapping(2, "executables"), - "file_system": KeyMapping(3, "file_system"), + "file_system": KeyMapping(3, "file-system"), "hardware": KeyMapping(4, "hardware"), - "runtime_opaque": KeyMapping(5, "runtime_opaque"), - "storage_opaque": KeyMapping(6, "storage_opaque"), - "sourced_data": KeyMapping(7, "sourced_data"), + "runtime_opaque": KeyMapping(5, "runtime-opaque"), + "storage_opaque": KeyMapping(6, "storage-opaque"), + "sourced_data": KeyMapping(7, "sourced-data"), } def validate(self): diff --git a/tests/test_claims.py b/tests/test_claims.py index 211d489..15b6737 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -47,22 +47,22 @@ def sample_attestation_result(): def test_attestation_result_to_dict(sample_attestation_result): expected = { - "profile": "test_profile", - "issued_at": 1234567890, - "verifier_id": {"developer": "Acme Inc.", "build": "v1"}, + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"}, "submods": { "submod1": { - "trust_vector": { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "ear.trustworthiness-vector": { + "instance-identity": TRUSTWORTHY_INSTANCE_CLAIM.value, "configuration": APPROVED_CONFIG_CLAIM.value, "executables": APPROVED_RUNTIME_CLAIM.value, - "file_system": APPROVED_FILES_CLAIM.value, + "file-system": APPROVED_FILES_CLAIM.value, "hardware": GENUINE_HARDWARE_CLAIM.value, - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, - "sourced_data": TRUSTED_SOURCES_CLAIM.value, + "runtime-opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage-opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced-data": TRUSTED_SOURCES_CLAIM.value, }, - "status": TRUST_TIER_AFFIRMING.value, + "ear.status": TRUST_TIER_AFFIRMING.value, } }, } @@ -77,22 +77,22 @@ def test_attestation_result_to_json(sample_attestation_result): def test_attestation_result_from_dict(): data = { - "profile": "test_profile", - "issued_at": 1234567890, - "verifier_id": {"developer": "Acme Inc.", "build": "v1"}, + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"}, "submods": { "submod1": { - "trust_vector": { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "ear.trustworthiness-vector": { + "instance-identity": TRUSTWORTHY_INSTANCE_CLAIM.value, "configuration": APPROVED_CONFIG_CLAIM.value, "executables": APPROVED_RUNTIME_CLAIM.value, - "file_system": APPROVED_FILES_CLAIM.value, + "file-system": APPROVED_FILES_CLAIM.value, "hardware": GENUINE_HARDWARE_CLAIM.value, - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, - "sourced_data": TRUSTED_SOURCES_CLAIM.value, + "runtime-opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage-opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced-data": TRUSTED_SOURCES_CLAIM.value, }, - "status": TRUST_TIER_AFFIRMING.value, + "ear.status": TRUST_TIER_AFFIRMING.value, } }, } diff --git a/tests/test_trust_vector.py b/tests/test_trust_vector.py index a758c21..890a261 100644 --- a/tests/test_trust_vector.py +++ b/tests/test_trust_vector.py @@ -33,14 +33,14 @@ def sample_trust_vector(): def test_trust_vector_to_dict(sample_trust_vector): expected = { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "instance-identity": TRUSTWORTHY_INSTANCE_CLAIM.value, "configuration": UNSAFE_CONFIG_CLAIM.value, "executables": APPROVED_RUNTIME_CLAIM.value, - "file_system": APPROVED_FILES_CLAIM.value, + "file-system": APPROVED_FILES_CLAIM.value, "hardware": GENUINE_HARDWARE_CLAIM.value, - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, - "sourced_data": TRUSTED_SOURCES_CLAIM.value, + "runtime-opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage-opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced-data": TRUSTED_SOURCES_CLAIM.value, } assert sample_trust_vector.to_dict() == expected @@ -67,14 +67,14 @@ def test_trust_vector_to_int_keys(sample_trust_vector): def test_trust_vector_from_dict(): data = { - "instance_identity": TRUSTWORTHY_INSTANCE_CLAIM.value, + "instance-identity": TRUSTWORTHY_INSTANCE_CLAIM.value, "configuration": UNSAFE_CONFIG_CLAIM.value, "executables": APPROVED_RUNTIME_CLAIM.value, - "file_system": APPROVED_FILES_CLAIM.value, + "file-system": APPROVED_FILES_CLAIM.value, "hardware": GENUINE_HARDWARE_CLAIM.value, - "runtime_opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, - "storage_opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, - "sourced_data": TRUSTED_SOURCES_CLAIM.value, + "runtime-opaque": ENCRYPTED_MEMORY_RUNTIME_CLAIM.value, + "storage-opaque": HW_KEYS_ENCRYPTED_SECRETS_CLAIM.value, + "sourced-data": TRUSTED_SOURCES_CLAIM.value, } parsed_vector = TrustVector.from_dict(data) assert parsed_vector.to_dict() == data