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/.github/workflows/tox.yml b/.github/workflows/tox.yml new file mode 100644 index 0000000..f68e652 --- /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.9, 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 diff --git a/.pylintrc b/.pylintrc index ee29c99..ce00b4d 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 [FORMAT] max-line-length = 88 ; Match Black's default line length +max-attributes=10 [MASTER] ignore = venv ; Ignore virtual environment folder 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 diff --git a/src/base.py b/src/base.py new file mode 100644 index 0000000..3f3a993 --- /dev/null +++ b/src/base.py @@ -0,0 +1,30 @@ +import json +from abc import ABC, abstractmethod +from typing import Any, Dict + + +# Abstract class to define structure to subclasses +class BaseJCSerializable(ABC): + JC_map: Dict[str, int] + + @abstractmethod + def to_dict(self) -> Dict[str, Any]: + pass + + # 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 + + @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/claims.py b/src/claims.py index 8e69c4d..08e8077 100644 --- a/src/claims.py +++ b/src/claims.py @@ -2,20 +2,56 @@ 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: 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) + + # 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, "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"].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 @@ -23,13 +59,33 @@ 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"]), + "status": to_trust_tier(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)) + + @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/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 new file mode 100644 index 0000000..a198571 --- /dev/null +++ b/src/jwt_handler.py @@ -0,0 +1,51 @@ +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/src/trust_claims.py b/src/trust_claims.py new file mode 100644 index 0000000..80086e7 --- /dev/null +++ b/src/trust_claims.py @@ -0,0 +1,256 @@ +from dataclasses import asdict, dataclass +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 + tag: str = "" + short: str = "" + long: str = "" + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +# General +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 # pylint: disable=line-too-long +) + +NO_CLAIM = TrustClaim( + value=0, + tag="no_claim", + short="no claim being made", + long="The Evidence received is insufficient to make a conclusion.", +) + +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 # pylint: disable=line-too-long +) + +CRYPTO_VALIDATION_FAILED_CLAIM = TrustClaim( + value=99, + tag="crypto_failed", + short="cryptographic validation failed", + long="Cryptographic validation of the Evidence has failed.", +) + + +# Instance Identity +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + + +# Config +APPROVED_CONFIG_CLAIM = TrustClaim( + value=2, + tag="approved_config", + short="all recognized and approved", + long="The configuration is a known and approved config.", +) + +NO_CONFIG_VULNS_CLAIM = TrustClaim( + value=3, + tag="safe_config", + short="no known vulnerabilities", + long="The configuration includes or exposes no known vulnerabilities", # noqa: E501 # pylint: disable=line-too-long +) + +UNSAFE_CONFIG_CLAIM = TrustClaim( + value=32, + tag="unsafe_config", + short="known vulnerabilities", + long="The configuration includes or exposes known vulnerabilities.", +) + +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 # pylint: disable=line-too-long +) + + +# Executables & Runtime +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + + +# File System +APPROVED_FILES_CLAIM = TrustClaim( + value=2, + tag="approved_fs", + short="all recognized and approved", + long="Only a recognized set of approved files are found.", +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + + +# Hardware +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + + +# Opaque Runtime +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + + +# Opaque Storage +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +UNENCRYPTED_SECRETS_CLAIM = TrustClaim( + value=96, + tag="unencrypted_secrets", + short="unencrypted secrets", + long="There are persistent secrets which are stored unencrypted in an Attester.", +) + + +# Sourced Data +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 # pylint: disable=line-too-long +) + +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 # pylint: disable=line-too-long +) + +CONTRAINDICATED_SOURCES_CLAIM = 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_tier.py b/src/trust_tier.py new file mode 100644 index 0000000..2f186f7 --- /dev/null +++ b/src/trust_tier.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass +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 + if isinstance(value, int): + return INT_TO_TRUST_TIER.get(value, TRUST_TIER_NONE) + if isinstance(value, str): + return STRING_TO_TRUST_TIER.get(value, TRUST_TIER_NONE) + raise ValueError(f"Cannot convert {value} (type {type(value)}) to TrustTier") + + +# Defining trust tiers +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 +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 +STRING_TO_TRUST_TIER: Dict[str, TrustTier] = { + v: k for k, v in TRUST_TIER_TO_STRING.items() +} + +# Mapping from integer value to TrustTier +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 new file mode 100644 index 0000000..bd87239 --- /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 + + +# 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): + 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 + + 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/src/validation.py b/src/validation.py new file mode 100644 index 0000000..afed6b6 --- /dev/null +++ b/src/validation.py @@ -0,0 +1,81 @@ +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 new file mode 100644 index 0000000..e035cc4 --- /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 + + +# https://www.ietf.org/archive/id/draft-ietf-rats-ar4si-08.html#section-3.3 +@dataclass +class VerifierID(BaseJCSerializable): + developer: str + build: str + 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_claims.py b/tests/test_claims.py index 06fac4b..5452ab0 100644 --- a/tests/test_claims.py +++ b/tests/test_claims.py @@ -1,13 +1,146 @@ -from src.claims import EARClaims +import pytest +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 -def test_ear_claims(): - claims = EARClaims( - "test_profile", - 1234567890, - {"build": "v1"}, - {"submods1": {"status": "affirming"}}, + +@pytest.fixture +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=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": TRUST_TIER_AFFIRMING, + } + }, ) - json_str = claims.to_json() - parsed_claims = EARClaims.from_json(json_str) - assert parsed_claims.to_dict() == claims.to_dict() + + +def test_attestation_result_to_dict(sample_attestation_result): + expected = { + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.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(), + }, + "status": TRUST_TIER_AFFIRMING.value, + } + }, + } + assert sample_attestation_result.to_dict() == expected + + +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_attestation_result_from_dict(): + data = { + "eat_profile": "test_profile", + "iat": 1234567890, + "ear.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(), + }, + "status": TRUST_TIER_AFFIRMING.value, + } + }, + } + 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 new file mode 100644 index 0000000..f5e134f --- /dev/null +++ b/tests/test_trust_claims.py @@ -0,0 +1,19 @@ +import pytest + +from src.trust_claims import TRUSTWORTHY_INSTANCE_CLAIM + + +@pytest.fixture +def trust_claim(): + # Sample TrustClaim object for testing + return TRUSTWORTHY_INSTANCE_CLAIM + + +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 # 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 new file mode 100644 index 0000000..69287e3 --- /dev/null +++ b/tests/test_trust_tier.py @@ -0,0 +1,39 @@ +import pytest + +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) == 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") == 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) == TRUST_TIER_NONE # Default fallback + + +def test_to_trust_tier_invalid_str(): + assert to_trust_tier("invalid_string") == TRUST_TIER_NONE # 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 new file mode 100644 index 0000000..5070037 --- /dev/null +++ b/tests/test_trust_vector.py @@ -0,0 +1,95 @@ +import json + +import pytest + +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=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": 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 + + +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: 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": 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 + + +def test_trust_vector_from_cbor(): + cbor_data = { + 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 ( + 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..7e9d395 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,101 @@ +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 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..d015e1b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,9 @@ deps = mypy==1.5.1 pylint==2.17.5 pyright==1.1.325 + pytest==7.4.2 commands = - isort . --check --diff + isort . --profile=black black . --check --diff flake8 . mypy .