Skip to content

Conversation

@HarshvMahawar
Copy link
Collaborator

@HarshvMahawar HarshvMahawar commented Mar 13, 2025

This draft PR includes the implementation up to the structure definition of the EARClaim set, along with mandatory claims definitions.

Follows #12

@HarshvMahawar
Copy link
Collaborator Author

Some linting checks are failing due to misconfigurations between different tools like Black and Pylint. I will open another PR to manage them

@HarshvMahawar HarshvMahawar force-pushed the dev-branch branch 2 times, most recently from 8023a33 to 7b6b71d Compare March 15, 2025 23:50

@classmethod
@abstractmethod
def from_cbor(cls, data: Dict[int, Any]):
Copy link
Member

Choose a reason for hiding this comment

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

All these functions can be implemented in the abstract class.

Copy link
Member

Choose a reason for hiding this comment

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

@HarshvMahawar this is still not implemented

src/claims.py Outdated


@dataclass
class EARClaims:
Copy link
Member

Choose a reason for hiding this comment

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

This should be also CBOR serializable right?



# General
VerifierMalfunctionClaim = TrustClaim(
Copy link
Member

Choose a reason for hiding this comment

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

If this is a constant, this is not pythonic. Capitalize and use underscores.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure

src/claims.py Outdated


@dataclass
class EARClaims:
Copy link
Member

Choose a reason for hiding this comment

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

EARClaims -> AttestationResult: Try to keep the naming either close to the spec or the go implementation.

Also try to reference the spec where possible by adding comments, makes it easier to understand

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure

return AttestationResult.from_dict(payload)


# EXAMPLE USAGE
Copy link
Member

Choose a reason for hiding this comment

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

Maybe put it in an example folder instead of having it as a comment?

return json.dumps(self.to_dict())

@abstractmethod
def to_cbor(self) -> Dict[int, Any]:

Choose a reason for hiding this comment

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

Shouldn't this be returning bytes? It seems weird that to_cbor() method doesn't actually return CBOR (especially since to_json() does in fact return JSON).

return secrets.token_hex(32)


def sign_ear_claims(

Choose a reason for hiding this comment

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

Why the change of terminology? Why not sign_attestation_result?

Also, this could probably just be a method of AttestationResult?

return jwt.encode(payload, secret_key, algorithm=algorithm)


def verify_ear_claims(

Choose a reason for hiding this comment

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

This seems highly misleading, as the function does not verify any claims, just the signature on the JWT. Maybe just decode_jwt?

Or beter yet, just integrate this into decode_ear_claims below, as this doen't really add much over just calling jwt.decode().

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 = {

Choose a reason for hiding this comment

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

The capitalisation here seems inconsitent with the rest of the code. Should be jc_claims.


def to_cbor(self) -> Dict[int, Any]:
return {
self.JC_map["profile"]: self.profile,

Choose a reason for hiding this comment

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

This can largely be replaced with generic code that uses getattr and JC_map keys. (and checking if the obtained values have to_cbor themselves to call recursively. This would allow moving this code up to the base class.

TRUST_TIER_CONTRAINDICATED: TrustTier = TrustTier(96)

# Mapping from TrustTier to string representation
TRUST_TIER_TO_STRING: Dict[TrustTier, str] = {

Choose a reason for hiding this comment

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

Instead of exposing this, why not just implement __str__ for TrustTier?

}

# Reverse mapping from string to TrustTier
STRING_TO_TRUST_TIER: Dict[str, TrustTier] = {

Choose a reason for hiding this comment

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

Similarly, instead of exposing this, add TrustTier.from_str classmethod.

Or you could implement TrustTier.__init__(self, v: str | int) -> TrustTier (i.e. allow both int and str as aguments to __init__ and then handle the accodingly with isinstance)

}

# Mapping from integer value to TrustTier
INT_TO_TRUST_TIER: Dict[int, TrustTier] = {

Choose a reason for hiding this comment

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

I s this necessary? You can get a TrustTier from some int i just by doing TrustTier(i).

storage_opaque: Optional[TrustClaim] = None
sourced_data: Optional[TrustClaim] = None

JC_map = {

Choose a reason for hiding this comment

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

Same comment regarding capitalisation as above -- should be jc_map

pass


def validate_trust_claim(trust_claim: TrustClaim):

Choose a reason for hiding this comment

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

This, and the functions below, could be methods of the corresponding classes; i.e. just implment TrustClaim.validate() etc.

@HarshvMahawar HarshvMahawar merged commit a90e0cd into veraison:dev-branch Mar 18, 2025
3 checks passed
@HarshvMahawar
Copy link
Collaborator Author

HarshvMahawar commented Apr 7, 2025

working towards generalizing the to_ and from_ methods I have come up with this to_cbor method for BaseJCSerializable base class

@setrofim @thomas-fossati @THS-on can you please review this, I'm currently working on the from_cbor part, which is a bit tricky because it needs to handle nested objects and dictionaries properly based on the CBOR format. (having separate implementations were easier but this will definitely improve maintainability)

class BaseJCSerializable(ABC):
    jc_map: Dict[str, int]


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

@setrofim
Copy link

setrofim commented Apr 7, 2025

  • As I think I mentined in a pervious revew, to_cbor should be renamed, as it does not return CBOR but a structure of serialisable types, intended to be based to the actual CBOR serializer (but could also be used for something else -- nothing in your code is actually CBOR-specific).
  • Your logic for JSON would be identical, except for the dict keys, so rather than duplicationg it later, just extend it to contain both (i.e. Dict[str, tuple[int, str]] where the int in the value tuple is used for CBOR, and the str is used for JSON).
  • So the method becomes something like def to_data(self, keys_as_ints=False) -> Dict[int | str, Any]:
  • It is generally considered more Pythonic to check for behaviour rather than specific types, so instead of isinstance(value, BaseJCSerializable) do hasattr(value, "to_cbor"), and instead of isinstace(value, dict) do hasattr(value, "items"). (This is called "duck typing" -- if it walks like a duck, and it quacks like a duck, then its a duck, regardless of what its type hierarchy says).
  • I may be missing something (it's not clear in the code snippet), but why do you need to_dict for trust claim (I assume this is trust vector, or something else?), instead of just having it implement to_cbor?
  • Instead of having nested items delimited with a "." and the associated _serialize_nested_dict, you can just have recurve to_cbor function that makes use of an objects to_cbor method, when necessary. This would also simplfiy the method. Something along the lines of:
from abc import ABC
from typing import Dict, Any

def to_data(value: Any, keys_as_int=False) -> Any:
    if hasattr(value, 'to_data'):
        return value.to_data(keys_as_int)
    elif hasattr(value, 'items'):  # dict-like
        return {
            to_data(k, keys_as_int): to_data(v, keys_as_int)
            for k, v in value.items()
        }
    elif hasattr(value, '__iter__') and not isinstance(value, str):  # list-like
        return [to_data(v, keys_as_int) for v in value]
    else: # scalar and no to_data(), so assume serializable as-is
        return value


class BaseJCSerializable(ABC):
    jc_map: Dict[str, tuple[int, str]]

    def to_data(self, keys_as_int=False) -> Dict[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
        }

(note: this is a rough sketch. I'm probably missing some edge condition that would need to be handled, but hopefully this gives you the generall idea).

@HarshvMahawar
Copy link
Collaborator Author

  • As I think I mentined in a pervious revew, to_cbor should be renamed, as it does not return CBOR but a structure of serialisable types, intended to be based to the actual CBOR serializer (but could also be used for something else -- nothing in your code is actually CBOR-specific).
  • Your logic for JSON would be identical, except for the dict keys, so rather than duplicationg it later, just extend it to contain both (i.e. Dict[str, tuple[int, str]] where the int in the value tuple is used for CBOR, and the str is used for JSON).
  • So the method becomes something like def to_data(self, keys_as_ints=False) -> Dict[int | str, Any]:
  • It is generally considered more Pythonic to check for behaviour rather than specific types, so instead of isinstance(value, BaseJCSerializable) do hasattr(value, "to_cbor"), and instead of isinstace(value, dict) do hasattr(value, "items"). (This is called "duck typing" -- if it walks like a duck, and it quacks like a duck, then its a duck, regardless of what its type hierarchy says).
  • I may be missing something (it's not clear in the code snippet), but why do you need to_dict for trust claim (I assume this is trust vector, or something else?), instead of just having it implement to_cbor?
  • Instead of having nested items delimited with a "." and the associated _serialize_nested_dict, you can just have recurve to_cbor function that makes use of an objects to_cbor method, when necessary. This would also simplfiy the method. Something along the lines of:
from abc import ABC
from typing import Dict, Any

def to_data(value: Any, keys_as_int=False) -> Any:
    if hasattr(value, 'to_data'):
        return value.to_data(keys_as_int)
    elif hasattr(value, 'items'):  # dict-like
        return {
            to_data(k, keys_as_int): to_data(v, keys_as_int)
            for k, v in value.items()
        }
    elif hasattr(value, '__iter__') and not isinstance(value, str):  # list-like
        return [to_data(v, keys_as_int) for v in value]
    else: # scalar and no to_data(), so assume serializable as-is
        return value


class BaseJCSerializable(ABC):
    jc_map: Dict[str, tuple[int, str]]

    def to_data(self, keys_as_int=False) -> Dict[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
        }

(note: this is a rough sketch. I'm probably missing some edge condition that would need to be handled, but hopefully this gives you the generall idea).

thank you @setrofim , I will have a look at it

@HarshvMahawar
Copy link
Collaborator Author

@setrofim to_data() is correctly working, I modified the jc_map defintion as follows for all the sub classes to be able to switch between int_keys or str_keys smoothly

    jc_map = {
        "profile": (265, "profile"),
        "issued_at": (6, "issued_at"),
        "verifier_id": (1004, "verifier_id"),
        "submods": (266, "submods"),
    }


def to_data(value: Any, keys_as_int=False) -> Any:
    if hasattr(value, 'to_data'):
        return value.to_data(keys_as_int)
    elif hasattr(value, 'items'):  # dict-like
        return {
            to_data(k, keys_as_int): to_data(v, keys_as_int)
            for k, v in value.items()
        }
    elif hasattr(value, '__iter__') and not isinstance(value, str):  # list-like
        return [to_data(v, keys_as_int) for v in value]
    else: # scalar and no to_data(), so assume serializable as-is
        return value

class BaseJCSerializable(ABC):
    jc_map

    def to_data(self, keys_as_int=False) ->  Dict[Union[str, int], Any]:
        print(self.jc_map.__str__())
        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()
        }

introduced one more class Submod on the way to simply the implementation for from_ method

@dataclass
class Submod(BaseJCSerializable):
    trust_vector: TrustVector
    status: TrustClaim

    jc_map = {
        "status": (1000, "status"),
        "trust_vector": (1001, "trust_vector")
    }

now working towards the from_ methods I am facing a lot of challenges like deserializing the nested objects for example the TrustClaim for attributes of trust_vector or TrustTier for status, can you please help me on how should I tackle this?, this is what I was able to implement for now

    @classmethod
    def from_data(cls: Type[T], data: dict) -> T:
        init_kwargs = {}
        reverse_map = {v[1]: k for k, v in cls.jc_map.items()} # v[1] for str keys and v[0] for int keys

        for str_key, value in data.items():
            if str_key not in reverse_map:
                continue

            attr = reverse_map[str_key]
            field_type = cls.__annotations__.get(attr)

            if field_type is None:
                continue

            origin = get_origin(field_type)
            args = get_args(field_type)

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

            elif origin == dict and hasattr(args[1], 'from_data'):
                # Dict[str, CustomClass]
                init_kwargs[attr] = {
                    k: args[1].from_data(v) for k, v in value.items()
                }

            else:
                init_kwargs[attr] = value

        return cls(**init_kwargs)

while testing the output I am getting is

       data = {
        "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(),
                },
                "status": TRUST_TIER_AFFIRMING.value,
            }
        },
    }
    
    print(AttestationResult.from_data(data))
    
    AttestationResult(profile='test_profile', issued_at=1234567890, verifier_id=VerifierID(developer='Acme Inc.', build='v1'), submods={'submod1': Submod(trust_vector=TrustVector(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={'value': 2, 'tag': 'approved_config', 'short': 'all recognized and approved', 'long': 'The configuration is a known and approved config.'}, executables={'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.'}, file_system={'value': 2, 'tag': 'approved_fs', 'short': 'all recognized and approved', 'long': 'Only a recognized set of approved files are found.'}, hardware={'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.'}, runtime_opaque={'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."}, storage_opaque={'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.'}, sourced_data={'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.'}), status=2)})

you can see that I am not able to serialize the attrubutes of TrustVector to TrustClaim type and similarly status to TrustTier

@HarshvMahawar HarshvMahawar changed the title Draft PR: Dev branch Draft PR: Adding foundational components to assist in generating EAR Jun 20, 2025
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