Skip to content

Conversation

@HarshvMahawar
Copy link
Collaborator

@HarshvMahawar HarshvMahawar commented Mar 18, 2025

This PR introduces the foundational components required for EAT Attestation Results (EAR) generation. It includes:

  • Definition of core data structures such as AttestationResult, TrustVector, TrustClaim, VerifierID, trust_tier and submods
  • Implementation of JWT creation and signing methods within the AttestationResult class
  • Support for CBOR serialization (int-keys) and deserialization for structured data exchange (to_ and from_ methods)
  • Integration of a GitHub Actions CI workflow using tox
  • Documentation enhancements, including an overview of the attestation model and usage examples

Continuation of reviews from #8

HarshvMahawar and others added 9 commits March 18, 2025 18:46
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
Signed-off-by: HarshvMahawar <hv062727@gmail.com>
@HarshvMahawar
Copy link
Collaborator Author

HarshvMahawar commented Apr 17, 2025

@THS-on @thomas-fossati @setrofim can you please review the final to_ and from_ methods

continuation of #8 (comment)

import json
from abc import ABC
from typing import Any, ClassVar, Dict, Tuple, Type, TypeVar, Union, get_args

T = TypeVar("T", bound="BaseJCSerializable")


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]

    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


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_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

            attr = reverse_map[key]
            field_type = getattr(cls, "__annotations__", {}).get(attr)
            if field_type is None:
                continue

            args = get_args(field_type)

            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:
                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

    def to_int_keys(self) -> Dict[Union[str, int], Any]:
        return self.to_data(keys_as_int=True)

    @classmethod
    def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
        return cls.from_data(data)

    @classmethod
    def from_int_keys(cls: Type[T], data: Dict[int, Any]) -> T:
        return cls.from_data(data, keys_as_int=True)

    @classmethod
    def from_json(cls, json_str: str):
        return cls.from_dict(json.loads(json_str))

    def to_json(self):
        return json.dumps(self.to_data())

@THS-on
Copy link
Member

THS-on commented Apr 28, 2025

@THS-on @thomas-fossati @setrofim can you please review the final to_ and from_ methods

continuation of #8 (comment)

import json
from abc import ABC
from typing import Any, ClassVar, Dict, Tuple, Type, TypeVar, Union, get_args

T = TypeVar("T", bound="BaseJCSerializable")


def to_data(value: Any, keys_as_int=False) -> Any:
    if hasattr(value, "to_data"):
        return value.to_data(keys_as_int)

This are objects defined by us, right? So just explicitly check the type

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]

In that case it is probably better to just do:

isinstance(value, Iterable) and not isinstance(value, str):

Though this have also some caveats:
https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable

if hasattr(
    value, "value"
):  # custom classes that have value attr but don't have 'to_data'
    return value.value  # type: ignore[attr-defined]

Also here can you explicitly for the types you expect?

# scalar and no to_data(), so assume serializable as-is
return value> 

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_data(cls: Type[T], data: dict, keys_as_int=False) -> T:

    if keys_as_int:
        index = 0
    else:
        index = 1

Might be worth it to use a namedtuple instead, so that we are not doing random indices.

    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

        attr = reverse_map[key]
        field_type = getattr(cls, "__annotations__", {}).get(attr)
        if field_type is None:
            continue

        args = get_args(field_type)

        if hasattr(field_type, "from_data"):
            # Direct object
            init_kwargs[attr] = field_type.from_data(value, keys_as_int=keys_as_int)

Same notes as above, check for types explicitly.

        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:
            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

def to_int_keys(self) -> Dict[Union[str, int], Any]:
    return self.to_data(keys_as_int=True)

@classmethod
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
    return cls.from_data(data)

@classmethod
def from_int_keys(cls: Type[T], data: Dict[int, Any]) -> T:
    return cls.from_data(data, keys_as_int=True)

@classmethod
def from_json(cls, json_str: str):
    return cls.from_dict(json.loads(json_str))

def to_json(self):
    return json.dumps(self.to_data())

@setrofim can you take also a look?

Signed-off-by: HarshvMahawar <hv062727@gmail.com>
@HarshvMahawar
Copy link
Collaborator Author

@THS-on @thomas-fossati @setrofim can you please review the final to_ and from_ methods
continuation of #8 (comment)
Might be worth it to use a namedtuple instead, so that we are not doing random indices.

    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

        attr = reverse_map[key]
        field_type = getattr(cls, "__annotations__", {}).get(attr)
        if field_type is None:
            continue

        args = get_args(field_type)

        if hasattr(field_type, "from_data"):
            # Direct object
            init_kwargs[attr] = field_type.from_data(value, keys_as_int=keys_as_int)

done

@HarshvMahawar
Copy link
Collaborator Author

@THS-on @thomas-fossati @setrofim can you please review the final to_ and from_ methods
continuation of #8 (comment)

This are objects defined by us, right? So just explicitly check the type

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]

In that case it is probably better to just do:

isinstance(value, Iterable) and not isinstance(value, str):

Though this have also some caveats: https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable

if hasattr(
    value, "value"
):  # custom classes that have value attr but don't have 'to_data'
    return value.value  # type: ignore[attr-defined]

Also here can you explicitly for the types you expect?

# scalar and no to_data(), so assume serializable as-is
return value> 

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_data(cls: Type[T], data: dict, keys_as_int=False) -> T:

    if keys_as_int:
        index = 0
    else:
        index = 1

Same notes as above, check for types explicitly.

        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:
            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

def to_int_keys(self) -> Dict[Union[str, int], Any]:
    return self.to_data(keys_as_int=True)

@classmethod
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
    return cls.from_data(data)

@classmethod
def from_int_keys(cls: Type[T], data: Dict[int, Any]) -> T:
    return cls.from_data(data, keys_as_int=True)

@classmethod
def from_json(cls, json_str: str):
    return cls.from_dict(json.loads(json_str))

def to_json(self):
    return json.dumps(self.to_data())

initially I did the same but as suggested by Sergie here (point 4, #8 (comment)) I tried implementing duck-typing

Copy link

@setrofim setrofim left a comment

Choose a reason for hiding this comment

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

JWT/JSON claim names need to be aligned with the corresponding spec (either the EAR draft or the EAT RFC). Apart from that, looks good.

src/claims.py Outdated
jc_map = {
"profile": KeyMapping(265, "profile"),
"issued_at": KeyMapping(6, "issued_at"),
"verifier_id": KeyMapping(1004, "verifier_id"),

Choose a reason for hiding this comment

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

The string name should be "ear.verifier-id". Please note that JSON field names are defined by draft-fv-rats-ear (see "JWT Claim Name" entry). The same goes for other claim names as well -- please make sure they align with the spec.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

src/claims.py Outdated
}
# https://www.ietf.org/archive/id/draft-ietf-rats-eat-31.html#section-7.2.4
jc_map = {
"profile": KeyMapping(265, "profile"),

Choose a reason for hiding this comment

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

The string name should be "eat_profile". This is a standard [EAT claim])https://www.rfc-editor.org/rfc/rfc9711.html#name-eat_profile-eat-profile-claim). The same goes for other claims as well please check you're using the correct name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@setrofim
Copy link

There are still some field names that are wrong -- please check all JSON field names against the corresponding specs, not just the ones I specifically highlighted (e.g. "issued_at" is still wrong).

@HarshvMahawar
Copy link
Collaborator Author

There are still some field names that are wrong -- please check all JSON field names against the corresponding specs, not just the ones I specifically highlighted (e.g. "issued_at" is still wrong).

oh, totally missed it, will update

@HarshvMahawar
Copy link
Collaborator Author

HarshvMahawar commented Jun 16, 2025

There are still some field names that are wrong -- please check all JSON field names against the corresponding specs, not just the ones I specifically highlighted (e.g. "issued_at" is still wrong).

done, now constructed EAR will look like this

    data = {
        "eat_profile": "test_profile",
        "iat": 1234567890,
        "ear.verifier-id": {"developer": "Acme Inc.", "build": "v1"},
        "submods": {
            "submod1": {
                "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,
                    "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,
                },
                "ear.status": TRUST_TIER_AFFIRMING.value,
            }
        },
    }

Signed-off-by: HarshvMahawar <hv062727@gmail.com>
@HarshvMahawar HarshvMahawar changed the title Dev branch 2 Add foundational components for EAR generation (JWT creation and signing) and CI setup Jun 20, 2025
@HarshvMahawar HarshvMahawar merged commit 8b32a1c into veraison:dev-branch-2 Jun 20, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants