Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 88
34 changes: 34 additions & 0 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -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
72 changes: 57 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/)
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.
30 changes: 30 additions & 0 deletions src/base.py
Original file line number Diff line number Diff line change
@@ -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
76 changes: 66 additions & 10 deletions src/claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,90 @@
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
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()
},
)
Empty file added src/example/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions src/example/jwt_example.py
Original file line number Diff line number Diff line change
@@ -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")
"""
Loading