feat: link local library

This commit is contained in:
gabdevele
2026-06-17 20:43:56 +02:00 Unverified
parent 9a770a2af0
commit db59882f33
327 changed files with 22274 additions and 1 deletions
@@ -0,0 +1,317 @@
import datetime
import logging
from collections import OrderedDict
from typing import Union, Optional
from spid_cie_oidc.entity.policy import apply_policy
from .exceptions import (
InvalidEntityConfiguration,
InvalidRequiredTrustMark,
MetadataDiscoveryException,
)
from .statements import (
get_entity_configurations,
EntityConfiguration,
)
from .utils import datetime_from_timestamp
logger = logging.getLogger(__name__)
class TrustChainBuilder:
"""
A trust walker that fetches statements and evaluate the evaluables
max_intermediaries means how many hops are allowed to the trust anchor
max_authority_hints means how much authority_hints to follow on each hop
required_trust_marks means all the trsut marks needed to start a metadata discovery
at least one of the required trust marks is needed to start a metadata discovery
if this param if absent the filter won't be considered.
"""
def __init__(
self,
subject: str,
trust_anchor: Union[str, EntityConfiguration],
httpc_params: dict = {},
max_authority_hints: int = 10,
subject_configuration: EntityConfiguration = None,
required_trust_marks: list = [],
# TODO - prefetch cache?
# pre_fetched_entity_configurations = {},
# pre_fetched_statements = {},
#
**kwargs,
) -> None:
self.subject = subject
self.subject_configuration = subject_configuration
self.httpc_params = httpc_params
self.trust_anchor = trust_anchor
self.trust_anchor_configuration = None
self.required_trust_marks = required_trust_marks
self.is_valid = False
self.tree_of_trust = OrderedDict()
self.trust_path = [] # list of valid subjects up to trust anchor
self.max_authority_hints = max_authority_hints
# dynamically valued
self.max_path_len = 0
self.final_metadata: dict = {}
self.verified_trust_marks = []
self.exp = 0
def apply_metadata_policy(self) -> dict:
"""
Starts the search for a trust path and, if found, applies the
metadata policies to generate the final metadata.
"""
logger.info(
f"Starting trust path search for {self.subject} to "
f"{self.trust_anchor_configuration.sub}"
)
start_path = [self.subject_configuration]
valid_path = self._find_trust_path_recursive(start_path)
if valid_path:
logger.info(f"Found a valid trust path: {[e.sub for e in valid_path]}")
self.trust_path = valid_path
final_metadata = self.subject_configuration.payload.get("metadata", {})
if not final_metadata:
logger.error(f"Missing metadata in subject {self.subject}")
return {}
for i in range(len(self.trust_path) - 1, 0, -1):
superior_ec = self.trust_path[i]
subordinate_ec = self.trust_path[i - 1]
statement = superior_ec.verified_descendant_statements.get(subordinate_ec.sub, {})
policy = statement.get("metadata_policy", {})
for md_type, pol_value in policy.items():
if final_metadata.get(md_type):
final_metadata[md_type] = apply_policy(
final_metadata[md_type], pol_value
)
self.final_metadata = final_metadata
self.set_exp()
return self.final_metadata
else:
logger.warning(
f"Could not find a valid trust path for {self.subject} "
f"to {self.trust_anchor_configuration.sub}"
)
self.trust_path = []
return {}
def _find_trust_path_recursive(self, current_path: list) -> Optional[list]:
"""
Recursively searches for a valid trust path (Depth-First Search).
Args:
current_path: The list of EntityConfigurations in the current path.
Returns:
The list representing the complete path if found, otherwise None.
"""
last_entity = current_path[-1]
# Base case (SUCCESS): we have reached the trust anchor
if last_entity.sub == self.trust_anchor_configuration.sub:
return current_path
# Base case (FAILURE): path has exceeded the maximum allowed length
# The +1 is because max_path_len is the number of intermediaries
if len(current_path) > self.max_path_len + 1:
logger.warning(
f"Path length ({len(current_path)}) exceeds max_path_len "
f"({self.max_path_len}). Backtracking."
)
return None
for superior_ec in last_entity.verified_by_superiors.values():
# Loop prevention: do not visit an entity that is already in the path
if superior_ec.sub in [e.sub for e in current_path]:
logger.warning(f"Loop detected with entity {superior_ec.sub}. Skipping path.")
continue
new_path = current_path + [superior_ec]
result = self._find_trust_path_recursive(new_path)
# If the recursive call found a path, propagate the result upwards
if result:
return result
# If the loop finishes, it means no superior led to a solution
return None
@property
def exp_datetime(self) -> datetime.datetime:
if self.exp: # pragma: no cover
return datetime_from_timestamp(self.exp)
def set_exp(self) -> int:
exps = [i.payload["exp"] for i in self.trust_path]
if exps:
self.exp = min(exps)
def discovery(self) -> bool:
"""
return a chain of verified statements
from the lower up to the trust anchor
"""
logger.info(f"Starting a Walk into Metadata Discovery for {self.subject}")
self.tree_of_trust[0] = [self.subject_configuration]
ecs_history = []
while (len(self.tree_of_trust) - 2) < self.max_path_len:
last_path_n = list(self.tree_of_trust.keys())[-1]
last_ecs = self.tree_of_trust[last_path_n]
sup_ecs = []
for last_ec in last_ecs:
# Metadata discovery loop prevention
if last_ec.sub in ecs_history:
logger.warning(
f"Metadata discovery loop detection for {last_ec.sub}. "
f"Already present in {ecs_history}. "
"Discovery blocked for this path."
)
continue
try:
superiors = last_ec.get_superiors(
max_authority_hints=self.max_authority_hints,
superiors_hints=[self.trust_anchor_configuration],
)
validated_by = last_ec.validate_by_superiors(
superiors_entity_configurations=superiors.values()
)
vbv = list(validated_by.values())
sup_ecs.extend(vbv)
ecs_history.append(last_ec)
except MetadataDiscoveryException as e:
logger.exception(
f"Metadata discovery exception for {last_ec.sub}: {e}"
)
if sup_ecs:
self.tree_of_trust[last_path_n + 1] = sup_ecs
else:
break
last_path = list(self.tree_of_trust.keys())[-1]
if (
self.tree_of_trust[0][0].is_valid
and self.tree_of_trust[last_path][0].is_valid
):
self.is_valid = True
self.apply_metadata_policy()
return self.is_valid
def get_trust_anchor_configuration(self) -> None:
if isinstance(self.trust_anchor, EntityConfiguration):
self.trust_anchor_configuration = self.trust_anchor
elif not self.trust_anchor_configuration and isinstance(self.trust_anchor, str):
logger.info(f"Starting Metadata Discovery for {self.subject}")
ta_jwt = get_entity_configurations(
self.trust_anchor, httpc_params=self.httpc_params
)[0]
self.trust_anchor_configuration = EntityConfiguration(ta_jwt)
try:
self.trust_anchor_configuration.validate_by_itself()
except Exception as e: # pragma: no cover
_msg = (
f"Trust Anchor Entity Configuration failed for {self.trust_anchor}. "
f"{e}"
)
logger.error(_msg)
raise Exception(_msg)
if self.trust_anchor_configuration.payload.get("constraints", {}).get(
"max_path_length"
):
self.max_path_len = int(
self.trust_anchor_configuration.payload["constraints"][
"max_path_length"
]
)
def get_subject_configuration(self) -> None:
if not self.subject_configuration:
try:
jwt = get_entity_configurations(
self.subject, httpc_params=self.httpc_params
)
self.subject_configuration = EntityConfiguration(
jwt[0], trust_anchor_entity_conf=self.trust_anchor_configuration
)
self.subject_configuration.validate_by_itself()
except Exception as e:
_msg = f"Entity Configuration for {self.subject} failed: {e}"
logger.error(_msg)
raise InvalidEntityConfiguration(_msg)
# Trust Mark filter
if self.required_trust_marks:
sc = self.subject_configuration
sc.filter_by_allowed_trust_marks = self.required_trust_marks
# TODO: create a proxy function that gets tm issuers ec from
# a previously populated cache
# sc.trust_mark_issuers_entity_confs = [
# trust_mark_issuers_entity_confs
# ]
if not sc.validate_by_allowed_trust_marks():
raise InvalidRequiredTrustMark(
"The required Trust Marks are not valid"
)
else:
self.verified_trust_marks.extend(sc.verified_trust_marks)
def serialize(self):
res = []
# we have only the leaf's and TA's EC, all the intermediate EC will be dropped
ta_ec: str = ""
for stat in self.trust_path:
if not isinstance(self.trust_anchor, str):
if (self.subject == stat.sub == stat.iss):
res.append(stat.jwt)
elif (self.trust_anchor.sub == stat.sub == stat.iss):
ta_ec = stat.jwt
if stat.verified_descendant_statements:
res.extend(
[
i for i in stat.verified_descendant_statements_as_jwt.values()
]
)
if ta_ec:
res.append(ta_ec)
return res
def start(self):
try:
self.get_trust_anchor_configuration()
self.get_subject_configuration()
self.discovery()
except Exception as e:
self.is_valid = False
logger.error(f"{e}")
raise e