-
Notifications
You must be signed in to change notification settings - Fork 1
Draft PR: Adding foundational components to assist in generating EAR #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Some linting checks are failing due to misconfigurations between different tools like Black and Pylint. I will open another PR to manage them |
8023a33 to
7b6b71d
Compare
|
|
||
| @classmethod | ||
| @abstractmethod | ||
| def from_cbor(cls, data: Dict[int, Any]): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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?
src/trust_claims.py
Outdated
|
|
||
|
|
||
| # General | ||
| VerifierMalfunctionClaim = TrustClaim( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure
src/jwt_handler.py
Outdated
| return AttestationResult.from_dict(payload) | ||
|
|
||
|
|
||
| # EXAMPLE USAGE |
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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] = { |
There was a problem hiding this comment.
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] = { |
There was a problem hiding this comment.
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] = { |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
ab8fd22 to
a90e0cd
Compare
|
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 |
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 |
|
@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 @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 |
This draft PR includes the implementation up to the structure definition of the EARClaim set, along with mandatory claims definitions.
Follows #12