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,6 @@
from enum import Enum
class KeyUsage(str, Enum):
signature = "sig"
encryption = "enc"
@@ -0,0 +1,30 @@
from django.contrib.auth import get_user_model
from django.db import models
class TimeStampedModel(models.Model):
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class CreatedModifiedBy(models.Model):
created_by = models.ForeignKey(
get_user_model(),
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="%(class)s_created_by",
)
modified_by = models.ForeignKey(
get_user_model(),
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="%(class)s_modified_by",
)
class Meta:
abstract = True
@@ -0,0 +1,202 @@
import logging
from django.contrib import admin
from django.utils.safestring import mark_safe
from django.contrib import messages
# from prettyjson import PrettyJSONWidget
from django.conf import settings
from .jwtse import unpad_jwt_payload
from .statements import EntityConfiguration, get_entity_configurations, get_entity_statements
from .models import (
FederationEntityConfiguration,
FederationHistoricalKey,
FetchedEntityStatement,
TrustChain,
StaffToken
)
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
from . settings import HTTPC_PARAMS
logger = logging.getLogger(__name__)
@admin.register(FederationHistoricalKey)
class FederationHistoricalKey(admin.ModelAdmin):
readonly_fields = ('jwk', "as_json")
list_display = ("entity", "kid", "inactive_from")
list_filter = ("created", "modified", "inactive_from")
search_fields = ("entity",)
@admin.register(FederationEntityConfiguration)
class FederationEntityConfigurationAdmin(admin.ModelAdmin):
# formfield_overrides = {
# JSONField: {
# "widget": PrettyJSONWidget(
# attrs={"initial": "parsed", "disabled": True}
# )
# }
# }
@admin.action(description='update trust marks')
def update_trust_marks(modeladmin, request, queryset): # pragma: no cover
"""
fetch trust marks from all the authorities
"""
trust_marks = {}
for obj in queryset:
jwts = get_entity_configurations(obj.authority_hints, HTTPC_PARAMS)
for jwt in jwts:
try:
ec = EntityConfiguration(jwt, httpc_params=HTTPC_PARAMS)
except Exception as e:
_msg = f"Failed getting Entity Configuration for {jwt}: {e}"
logger.warning(_msg)
messages.error(_msg)
continue
try:
# get superior fetch url
fetch_api_url = ec.payload["metadata"]["federation_entity"][
"federation_fetch_endpoint"
]
except KeyError:
_msg = (
"Missing federation_fetch_endpoint in "
f"federation_entity metadata for {obj.sub} by {ec.sub}."
)
logger.warning(_msg)
messages.error(_msg)
continue
_url = f"{fetch_api_url}?sub={obj.sub}"
try:
logger.info(f"Getting entity statements from {_url}")
_jwts = get_entity_statements([_url], HTTPC_PARAMS)
payload = unpad_jwt_payload(_jwts[0])
for i in payload.get("trust_marks", []):
trust_marks[i['id']] = i['trust_mark']
except Exception as e:
_msg = f"Error getting entity statements from {_url}: {e}"
logger.warning(_msg)
messages.error(_msg)
continue
positions = {}
count = 0
for i in obj.trust_marks:
positions[i['id']] = count
count += 1
if positions:
for k,v in trust_marks.items():
if positions.get(k, None):
obj.trust_marks[positions[k]] = {k:v}
else:
obj.trust_marks.append({k:v})
else:
obj.trust_marks = [
{"trust_mark_id":k, "trust_mark":v} for k,v in trust_marks.items()
]
obj.save()
messages.success(
request,
f"Trust mark reloaded succesfully: {', '.join(trust_marks.keys())}"
)
list_display = (
"sub",
"type",
"kids",
"is_active",
"created",
)
list_filter = ("created", "modified", "is_active")
# search_fields = ('command__name',)
readonly_fields = (
"created",
"modified",
"entity_configuration_as_json",
"pems_as_html",
"kids",
"type",
)
actions = [update_trust_marks]
def pems_as_html(self, obj):
res = ""
data = dict()
for k, v in obj.pems_as_dict.items():
data[k] = {}
for i in ("public", "private"):
data[k][i] = v[i].replace("\n", "<br>")
res += (
f"<b>{k}</b><br><br>"
f"{data[k]['public']}<br>"
f"{data[k]['private']}<br><hr>"
)
return mark_safe(res) # nosec
@admin.register(TrustChain)
class TrustChainAdmin(admin.ModelAdmin):
@admin.action(description='reload trust chain')
def update_trust_chain(modeladmin, request, queryset): # pragma: no cover
for tc in queryset:
sub = tc.sub
ta = tc.trust_anchor.sub
try :
get_or_create_trust_chain(
subject=sub,
trust_anchor=ta,
httpc_params=settings.HTTPC_PARAMS,
required_trust_marks=getattr(
settings, "OIDCFED_REQUIRED_TRUST_MARKS", [],
),
force=True
)
messages.success(
request, f"Trust chain successfully reloaded for {sub}"
)
except Exception as e:
messages.error(request, f"Failed to update {sub} due to: {e}")
continue
list_display = ("sub", "exp", "modified", "is_valid")
list_filter = ("exp", "modified", "is_active")
search_fields = ("sub",)
readonly_fields = (
"created",
"modified",
"parties_involved",
"status",
"log",
"chain",
"iat",
)
actions = [update_trust_chain]
@admin.register(FetchedEntityStatement)
class FetchedEntityStatementAdmin(admin.ModelAdmin):
list_display = ("sub", "iss", "exp", "iat", "created", "modified")
list_filter = ("created", "modified", "exp", "iat")
search_fields = ("sub", "iss")
readonly_fields = ("sub", "statement", "created", "modified", "iat", "exp", "iss")
@admin.register(StaffToken)
class StaffTokenAdmin(admin.ModelAdmin):
list_display = ("user", "expire_at", "is_valid")
list_filter = ("created", "modified", "expire_at")
search_fields = ("token", )
readonly_fields = ("is_valid",)
raw_id_fields = ('user',)
@@ -0,0 +1,8 @@
from django.apps import AppConfig
class SpidCieFederationEntityConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "spid_cie_oidc.entity"
verbose_name = "SPID/CIE OIDC Federation Entity"
label = "spid_cie_oidc_entity"
@@ -0,0 +1,53 @@
from django.core.exceptions import ValidationError
class HttpError(Exception):
pass
class TrustChainHttpError(HttpError):
pass
class UnknownKid(Exception):
pass
class MissingJwksClaim(ValidationError):
pass
class MissingAuthorityHintsClaim(ValidationError):
pass
class NotDescendant(ValidationError):
pass
class TrustAnchorNeeded(ValidationError):
pass
class MetadataDiscoveryException(ValidationError):
pass
class MissingTrustMark(ValidationError):
pass
class InvalidRequiredTrustMark(ValidationError):
pass
class InvalidTrustchain(ValidationError):
pass
class TrustchainMissingMetadata(ValidationError):
pass
class InvalidEntityConfiguration(ValidationError):
pass
@@ -0,0 +1,44 @@
import aiohttp
import asyncio
async def fetch(session, url, httpc_params: dict = {}):
async with session.get(url, **httpc_params.get("connection", {})) as response:
if response.status != 200: # pragma: no cover
# response.raise_for_status()
return ""
return await response.text()
async def fetch_all(session, urls, httpc_params):
tasks = []
for url in urls:
task = asyncio.create_task(fetch(session, url, httpc_params))
tasks.append(task)
results = await asyncio.gather(*tasks)
return results
async def http_get(urls, httpc_params: dict = {}):
_con = aiohttp.TCPConnector(**httpc_params.get("connection", {}))
async with aiohttp.ClientSession(
connector=_con,
**httpc_params.get("session", {})
) as session:
text = await fetch_all(session, urls, httpc_params)
return text
if __name__ == "__main__": # pragma: no cover
httpc_params = {
"connection": {"ssl": True},
"session": {"timeout": aiohttp.ClientTimeout(total=4)},
}
urls = [
"http://127.0.0.1:8001/.well-known/openid-federation",
"http://127.0.0.1:8000/.well-known/openid-federation",
"http://asdasd.it",
"http://127.0.0.1:8001/.well-known/openid-federation",
"http://google.it",
]
asyncio.run(http_get(urls, httpc_params=httpc_params))
@@ -0,0 +1,86 @@
import base64
import json
from urllib.parse import urlparse
from typing import Union
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, ed448, rsa
from cryptojwt.jwk.jwk import key_from_jwk_dict
from spid_cie_oidc.entity.x509 import X509Issuer
def _normalize_jwks(jwks):
"""
Normalize jwks to a list of JWK dicts. Handles:
- {"keys": [...]} (JWKS structure from entity config)
- [...] (list of keys)
- Single JWK dict (as item that may be JSON string)
"""
if isinstance(jwks, dict):
if "keys" in jwks:
jwks = jwks["keys"]
else:
jwks = [jwks]
if not isinstance(jwks, list):
jwks = [jwks]
result = []
for item in jwks:
if isinstance(item, dict):
result.append(item)
elif isinstance(item, str):
result.append(json.loads(item))
else:
raise TypeError(f"JWK item must be dict or JSON string, got {type(item)}")
return result
def _public_key_from_jwk_dict(jwk_dict: dict):
"""
Extract public key from a JWK dict. Handles keys with x5c but no kty
(e.g. from entity configurations that publish keys via x5c only).
"""
if "kty" in jwk_dict:
return key_from_jwk_dict(jwk_dict).public_key()
# JWK has x5c but no kty - derive from certificate
x5c = jwk_dict.get("x5c")
if not x5c:
raise ValueError("JWK must have either kty or x5c")
der = base64.b64decode(x5c[0])
cert = x509.load_der_x509_certificate(der)
pub_key = cert.public_key()
# Add kty so the JWK is valid for downstream consumers
if isinstance(pub_key, rsa.RSAPublicKey):
jwk_dict["kty"] = "RSA"
elif isinstance(pub_key, ec.EllipticCurvePublicKey):
jwk_dict["kty"] = "EC"
elif isinstance(pub_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
jwk_dict["kty"] = "OKP"
else:
jwk_dict["kty"] = "RSA" # fallback for unknown types
return pub_key
def update_jwks_with_x5c(
jwks: list,
private_key: bytes,
subject: str,
is_ca_or_int: bool,
path_length: Union[int, None] = None,
) -> dict:
jwks = _normalize_jwks(jwks)
subject_data: dict = dict(
X509_COMMON_NAME=urlparse(subject).hostname,
# TODO: add COUNTRY_NAME, X509_STATE_OR_PROVINCE_NAME, X509_LOCALITY_NAME, X509_ORGANIZATION_NAME
entity_id=subject
)
for i in jwks:
i['x5c'] = X509Issuer(
private_key = private_key,
public_key = _public_key_from_jwk_dict(i),
subject_data = subject_data,
is_ca_or_int = is_ca_or_int,
path_length = path_length
).x5c
return jwks
+121
View File
@@ -0,0 +1,121 @@
from cryptojwt.jwk.jwk import key_from_jwk_dict
from cryptojwt.jwk.rsa import new_rsa_key
from cryptojwt.jwk.ec import new_ec_key
from cryptography.hazmat.primitives import serialization
from cryptojwt.jwk.rsa import RSAKey
from cryptojwt.jwk.ec import ECKey
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from django.conf import settings
from . import settings as local_settings
DEFAULT_HASH_FUNC = getattr(
settings, "DEFAULT_HASH_FUNC", local_settings.DEFAULT_HASH_FUNC
)
def create_jwk(key=None, hash_func=None, typ: str = getattr(settings, "PRIVATE_KEY_TYPE", None)):
if key:
key = key
elif typ and typ.lower() == 'ec':
key = new_ec_key(crv="P-256")
else:
key = new_rsa_key()
thumbprint = key.thumbprint(hash_function=hash_func or DEFAULT_HASH_FUNC)
jwk = key.to_dict()
jwk["kid"] = thumbprint.decode()
return jwk
def public_jwk_from_private_jwk(jwk_dict: dict):
# exports public
_k = key_from_jwk_dict(jwk_dict)
jwk = _k.serialize()
jwk["kid"] = jwk_dict['kid']
return jwk
def private_pem_from_jwk(jwk_dict: dict):
# exports private
_k = key_from_jwk_dict(jwk_dict)
pk = _k.private_key()
pem = pk.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
return pem.decode()
def public_pem_from_jwk(jwk_dict: dict):
# exports private
_k = key_from_jwk_dict(jwk_dict)
pk = _k.public_key()
cert = pk.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
return cert.decode()
def serialize_key(key, kind="public", hash_func="SHA-256"):
"""
key can be:
- cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey
- cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey
- cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey
- cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
- str or bytes (PEM format)
"""
data = {}
# Handle RSA keys
if isinstance(key, rsa.RSAPublicKey):
data = {"pub_key": key}
elif isinstance(key, rsa.RSAPrivateKey):
data = {"priv_key": key}
# Handle EC keys
elif isinstance(key, ec.EllipticCurvePublicKey):
data = {"pub_key": key}
elif isinstance(key, ec.EllipticCurvePrivateKey):
data = {"priv_key": key}
# Handle PEM strings/bytes
elif isinstance(key, (str, bytes)):
if kind == "private":
if isinstance(key, rsa.RSAPrivateKey) or (isinstance(key, str) and "RSA" in key):
data = {"priv_key": private_jwk_from_pem(key)}
else:
data = {"priv_key": private_jwk_from_pem(key)}
else:
if isinstance(key, rsa.RSAPublicKey) or (isinstance(key, str) and "RSA" in key):
data = {"pub_key": public_jwk_from_pem(key)}
else:
data = {"pub_key": public_jwk_from_pem(key)}
# Create appropriate JWK object based on key type
if isinstance(key, (rsa.RSAPublicKey, rsa.RSAPrivateKey)) or (isinstance(key, str) and "RSA" in key):
jwk_obj = RSAKey(**data)
else:
jwk_obj = ECKey(**data)
thumbprint = jwk_obj.thumbprint(hash_function=hash_func)
jwk = jwk_obj.to_dict()
jwk["kid"] = thumbprint.hex() # Use hex() instead of decode()
return jwk
def private_jwk_from_pem(content:str, password:str = None):
content = content.encode() if isinstance(content, str) else content
key = serialization.load_pem_private_key(content, password=password)
return serialize_key(key, kind='private')
def public_jwk_from_pem(content:str, password:str = None):
content = content.encode() if isinstance(content, str) else content
key = serialization.load_pem_public_key(content)
return serialize_key(key, kind='public')
@@ -0,0 +1,137 @@
import base64
import binascii
import json
import logging
import cryptojwt
from cryptojwt.exception import UnsupportedAlgorithm, VerificationError
from cryptojwt.jwe.jwe import factory
from cryptojwt.jwe.jwe_ec import JWE_EC
from cryptojwt.jwe.jwe_rsa import JWE_RSA
from cryptojwt.jwk.jwk import key_from_jwk_dict
from cryptojwt.jws.jws import JWS
from cryptojwt.jws.utils import left_hash
from typing import Union
from .settings import (
DEFAULT_JWE_ALG,
DEFAULT_JWE_ENC,
ENCRYPTION_ALG_VALUES_SUPPORTED,
SIGNING_ALG_VALUES_SUPPORTED,
)
logger = logging.getLogger(__name__)
def unpad_jwt_element(jwt: str, position: int) -> dict:
b = jwt.split(".")[position]
padded = f"{b}{'=' * divmod(len(b), 4)[1]}"
data = json.loads(base64.urlsafe_b64decode(padded))
return data
def unpad_jwt_head(jwt: str) -> dict:
return unpad_jwt_element(jwt, position=0)
def unpad_jwt_payload(jwt: str) -> dict:
return unpad_jwt_element(jwt, position=1)
def create_jwe(plain_dict: Union[dict, str, int, None], jwk_dict: dict, **kwargs) -> str:
logger.debug(f"Encrypting dict as JWE: " f"{plain_dict}")
_key = key_from_jwk_dict(jwk_dict)
if isinstance(_key, cryptojwt.jwk.rsa.RSAKey):
JWE_CLASS = JWE_RSA
elif isinstance(_key, cryptojwt.jwk.ec.ECKey):
JWE_CLASS = JWE_EC
if isinstance(plain_dict, dict):
_payload = json.dumps(plain_dict).encode()
elif not plain_dict:
logger.warning("create_jwe with null payload!")
_payload = ""
elif isinstance(plain_dict, (str, int)):
_payload = plain_dict
else:
logger.error("create_jwe with unsupported payload type!")
_payload = ""
_keyobj = JWE_CLASS(
_payload,
alg=DEFAULT_JWE_ALG,
enc=DEFAULT_JWE_ENC,
kid=_key.kid,
**kwargs
)
jwe = _keyobj.encrypt(_key.public_key())
logger.debug(f"Encrypted dict as JWE: {jwe}")
return jwe
def decrypt_jwe(jwe: str, jwk_dict: dict) -> dict:
# get header
try:
jwe_header = unpad_jwt_head(jwe)
except (binascii.Error, Exception) as e: # pragma: no cover
logger.error(f"Failed to extract JWT header: {e}")
raise VerificationError("The JWT is not valid")
_alg = jwe_header.get("alg", DEFAULT_JWE_ALG)
_enc = jwe_header.get("enc", DEFAULT_JWE_ENC)
jwe_header.get("kid")
if _alg not in ENCRYPTION_ALG_VALUES_SUPPORTED: # pragma: no cover
raise UnsupportedAlgorithm(f"{_alg} has beed disabled for security reason")
_decryptor = factory(jwe, alg=_alg, enc=_enc)
# _dkey = RSAKey(priv_key=PRIV_KEY)
_dkey = key_from_jwk_dict(jwk_dict)
msg = _decryptor.decrypt(jwe, [_dkey])
try:
msg_dict = json.loads(msg)
logger.debug(f"Decrypted JWT as: {json.dumps(msg_dict, indent=2)}")
except json.decoder.JSONDecodeError:
msg_dict = msg
logger.debug(f"Decrypted JWT as: {msg_dict}")
return msg_dict
def create_jws(payload: dict, jwk_dict: dict, alg: str = "RS256", protected:dict = {}, **kwargs) -> str:
_key = key_from_jwk_dict(jwk_dict)
_signer = JWS(payload, alg=alg, **kwargs)
signature = _signer.sign_compact([_key], protected=protected, **kwargs)
return signature
def verify_jws(jws: str, pub_jwk: dict, **kwargs) -> str:
_key = key_from_jwk_dict(pub_jwk)
_head = unpad_jwt_head(jws)
if _head.get("kid") != pub_jwk["kid"]: # pragma: no cover
raise Exception(
f"kid error: {_head.get('kid')} != {pub_jwk['kid']}"
)
_alg = _head["alg"]
if _alg not in SIGNING_ALG_VALUES_SUPPORTED or not _alg: # pragma: no cover
raise UnsupportedAlgorithm(f"{_alg} has beed disabled for security reason")
verifier = JWS(alg=_head["alg"], **kwargs)
msg = verifier.verify_compact(jws, [_key])
return msg
def verify_at_hash(id_token, access_token) -> bool:
id_token_at_hash = id_token['at_hash']
at_hash = left_hash(access_token, "HS256")
if at_hash != id_token_at_hash:
raise Exception(
f"at_hash error: {at_hash} != {id_token_at_hash}"
)
return True
@@ -0,0 +1,243 @@
# Generated by Django 4.0.2 on 2022-02-13 09:06
from django.db import migrations, models
import spid_cie_oidc.entity.models
import spid_cie_oidc.entity.validators
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="FederationEntityConfiguration",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
(
"uuid",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
(
"sub",
models.URLField(
help_text="URL that identifies this Entity in the Federation. This value and iss are the same in the Entity Configuration.",
max_length=255,
),
),
(
"default_exp",
models.PositiveIntegerField(
default=33,
help_text="how many minutes from now() an issued statement must expire",
),
),
(
"default_signature_alg",
models.CharField(
default="RS256",
help_text="default signature algorithm, eg: RS256",
max_length=16,
),
),
(
"authority_hints",
models.JSONField(
blank=True,
default=list,
help_text="only required if this Entity is an intermediary",
),
),
(
"jwks",
models.JSONField(
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
help_text="a list of public keys",
),
),
(
"trust_marks",
models.JSONField(
blank=True,
default=list,
help_text="which trust marks MUST be exposed in its entity configuration",
),
),
(
"trust_mark_issuers",
models.JSONField(
blank=True,
default=dict,
help_text='Only usable for Trust Anchors and intermediates. Which issuers are allowed to issue trust marks for the descendants. Example: {"https://www.spid.gov.it/certification/rp": ["https://registry.spid.agid.gov.it", "https://intermediary.spid.it"],"https://sgd.aa.it/onboarding": ["https://sgd.aa.it", ]}',
),
),
(
"metadata",
models.JSONField(
default=dict,
help_text='federation_entity metadata, eg: {"federation_entity": { ... },"openid_provider": { ... },"openid_relying_party": { ... },"oauth_resource": { ... }}',
validators=[
spid_cie_oidc.entity.validators.validate_entity_metadata
],
),
),
(
"is_active",
models.BooleanField(
default=False,
help_text="If this configuration is active. At least one configuration must be active",
),
),
],
options={
"verbose_name": "Federation Entity Configuration",
"verbose_name_plural": "Federation Entity Configurations",
},
),
migrations.CreateModel(
name="PublicJwk",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
(
"kid",
models.CharField(
help_text="unique code that identifies this jwks. ",
max_length=1024,
unique=True,
),
),
(
"jwk",
models.JSONField(
default=dict,
help_text="Public jwk",
validators=[
spid_cie_oidc.entity.validators.validate_public_jwks
],
),
),
],
options={
"verbose_name": "Public JWKs",
"verbose_name_plural": "Public JWKs",
},
),
migrations.CreateModel(
name="TrustChain",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
(
"sub",
models.URLField(
help_text="URL that identifies this Entity in the Federation. ",
max_length=255,
),
),
(
"type",
models.CharField(
blank=True,
choices=[
("federation_entity", "federation_entity"),
("openid_relying_party", "openid_relying_party"),
("openid_provider", "openid_provider"),
("oauth_resource", "oauth_resource"),
],
default="openid_provider",
help_text="OpenID Connect Federation entity type",
max_length=33,
),
),
("exp", models.DateTimeField()),
("iat", models.DateTimeField()),
(
"chain",
models.JSONField(
blank=True,
default=list,
help_text="A list of entity statements collected during the metadata discovery",
),
),
(
"resultant_metadata",
models.JSONField(
default=dict,
help_text="The final metadata applied with the metadata policy built over the chain",
),
),
(
"parties_involved",
models.JSONField(
blank=True,
default=list,
help_text="subjects involved in the metadata discovery",
),
),
(
"status",
models.CharField(
choices=[
("unreachable", "unreachable"),
("valid", "valid"),
("signature_failed", "signature_failed"),
("not_valid", "not_valid"),
("unknown", "unknown"),
("expired", "expired"),
],
default=False,
help_text="Status of this trust chain, on each update.",
max_length=33,
),
),
(
"status_log",
models.JSONField(blank=True, default=dict, help_text="status log"),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="If you need to disable the trust to this subject, deacticate this",
),
),
],
options={
"verbose_name": "Trust Chain",
"verbose_name_plural": "Trust Chains",
"unique_together": {("sub", "type")},
},
),
]
@@ -0,0 +1,53 @@
# Generated by Django 4.0.2 on 2022-02-13 11:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="FetchedEntityStatement",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
(
"iss",
models.URLField(
help_text="URL that identifies the issuer of this statement in the Federation. ",
max_length=255,
),
),
(
"sub",
models.URLField(
help_text="URL that identifies this Entity in the Federation. ",
max_length=255,
),
),
("exp", models.DateTimeField()),
("iat", models.DateTimeField()),
(
"statement",
models.JSONField(default=dict, help_text="Entity statement"),
),
],
options={
"verbose_name": "Fetched Entity Statement",
"verbose_name_plural": "Fetched Entity Statement",
},
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.0.2 on 2022-02-13 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0002_fetchedentitystatement"),
]
operations = [
migrations.AddField(
model_name="federationentityconfiguration",
name="constraints",
field=models.JSONField(
default=dict,
help_text='\n{\n "naming_constraints": {\n "permitted": [\n "https://.example.com"\n ],\n "excluded": [\n "https://east.example.com"\n ]\n },\n "max_path_length": 2\n}\n',
),
),
]
@@ -0,0 +1,30 @@
# Generated by Django 4.0.2 on 2022-02-14 15:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0003_federationentityconfiguration_constraints"),
]
operations = [
migrations.AlterField(
model_name="federationentityconfiguration",
name="authority_hints",
field=models.JSONField(
blank=True,
default=list,
help_text="only required if this Entity is an intermediary or leaf.",
),
),
migrations.AlterField(
model_name="federationentityconfiguration",
name="default_exp",
field=models.PositiveIntegerField(
default=2880,
help_text="how many minutes from now() an issued statement must expire",
),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 4.0.2 on 2022-02-14 23:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0004_alter_federationentityconfiguration_authority_hints_and_more",
),
]
operations = [
migrations.AlterField(
model_name="federationentityconfiguration",
name="constraints",
field=models.JSONField(
blank=True,
default=dict,
help_text='\n{\n "naming_constraints": {\n "permitted": [\n "https://.example.com"\n ],\n "excluded": [\n "https://east.example.com"\n ]\n },\n "max_path_length": 2\n}\n',
),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 4.0.2 on 2022-02-22 00:15
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0005_alter_federationentityconfiguration_constraints",
),
]
operations = [
migrations.RemoveField(
model_name="trustchain",
name="resultant_metadata",
),
migrations.RemoveField(
model_name="trustchain",
name="status_log",
),
migrations.AddField(
model_name="trustchain",
name="log",
field=models.TextField(blank=True, default="", help_text="status log"),
),
migrations.AddField(
model_name="trustchain",
name="metadata",
field=models.JSONField(
blank=True,
default=dict,
help_text="The final metadata applied with the metadata policy built over the chain",
null=True,
),
),
migrations.AddField(
model_name="trustchain",
name="processing_start",
field=models.DateTimeField(
default=django.utils.timezone.localtime,
help_text="When the metadata discovery started for this Trust Chain. It should prevent concurrent processing for the same sub/type.",
),
),
migrations.AlterField(
model_name="trustchain",
name="is_active",
field=models.BooleanField(
default=True,
help_text="If you need to disable the trust to this subject, uncheck this",
),
),
]
@@ -0,0 +1,37 @@
# Generated by Django 4.0.2 on 2022-02-25 14:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0006_remove_trustchain_resultant_metadata_and_more"),
]
operations = [
migrations.AlterUniqueTogether(
name="trustchain",
unique_together=set(),
),
migrations.AddField(
model_name="trustchain",
name="trust_anchor",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to="spid_cie_oidc_entity.fetchedentitystatement",
),
preserve_default=False,
),
migrations.AlterField(
model_name="trustchain",
name="iat",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterUniqueTogether(
name="trustchain",
unique_together={("sub", "type", "trust_anchor")},
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.0.2 on 2022-02-25 14:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0007_alter_trustchain_unique_together_and_more"),
]
operations = [
migrations.AddField(
model_name="fetchedentitystatement",
name="jwt",
field=models.CharField(default="", max_length=2048),
preserve_default=False,
),
]
@@ -0,0 +1,29 @@
# Generated by Django 4.0.2 on 2022-02-26 18:26
from django.db import migrations, models
import spid_cie_oidc.entity.models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0008_fetchedentitystatement_jwt"),
]
operations = [
migrations.AddField(
model_name="trustchain",
name="trust_marks",
field=models.JSONField(
blank=True, default=list, help_text="verified trust marks"
),
),
migrations.AlterField(
model_name="federationentityconfiguration",
name="jwks",
field=models.JSONField(
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
help_text="a list of private keys",
),
),
]
@@ -0,0 +1,29 @@
# Generated by Django 4.0.2 on 2022-02-26 23:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0009_trustchain_trust_marks_and_more"),
]
operations = [
migrations.AddField(
model_name="federationentityconfiguration",
name="entity_type",
field=models.CharField(
blank=True,
choices=[
("federation_entity", "federation_entity"),
("openid_relying_party", "openid_relying_party"),
("openid_provider", "openid_provider"),
("oauth_resource", "oauth_resource"),
],
default="openid_relying_party",
help_text="OpenID Connect Federation entity type",
max_length=33,
),
),
]
@@ -0,0 +1,30 @@
# Generated by Django 4.0.2 on 2022-03-01 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0010_federationentityconfiguration_entity_type"),
]
operations = [
migrations.AlterField(
model_name="trustchain",
name="status",
field=models.CharField(
choices=[
("unreachable", "unreachable"),
("valid", "valid"),
("signature_failed", "signature_failed"),
("not_valid", "not_valid"),
("unknown", "unknown"),
("expired", "expired"),
],
default="unreachable",
help_text="Status of this trust chain, on each update.",
max_length=33,
),
),
]
@@ -0,0 +1,16 @@
# Generated by Django 4.0.2 on 2022-03-06 22:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0011_alter_trustchain_status'),
]
operations = [
migrations.DeleteModel(
name='PublicJwk',
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.0.2 on 2022-03-13 00:41
from django.db import migrations, models
import spid_cie_oidc.entity.validators
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0012_delete_publicjwk'),
]
operations = [
migrations.AlterField(
model_name='federationentityconfiguration',
name='metadata',
field=models.JSONField(default=dict, help_text='federation_entity metadata, eg: {"federation_entity": { ... },"openid_provider": { ... },"openid_relying_party": { ... },"oauth_resource": { ... }}', validators=[spid_cie_oidc.entity.validators.validate_entity_metadata, spid_cie_oidc.entity.validators.validate_metadata_algs]),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.0.3 on 2022-03-20 13:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0013_alter_federationentityconfiguration_metadata'),
]
operations = [
migrations.AlterUniqueTogether(
name='trustchain',
unique_together={('sub', 'trust_anchor')},
),
migrations.RemoveField(
model_name='trustchain',
name='type',
),
]
@@ -0,0 +1,26 @@
# Generated by Django 4.0.3 on 2022-03-20 13:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0014_alter_trustchain_unique_together_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='trustchain',
unique_together=set(),
),
migrations.AddField(
model_name='trustchain',
name='type',
field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('oauth_resource', 'oauth_resource')], default='openid_provider', help_text='OpenID Connect Federation entity type', max_length=33),
),
migrations.AlterUniqueTogether(
name='trustchain',
unique_together={('sub', 'type', 'trust_anchor')},
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.0.3 on 2022-03-20 14:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0015_alter_trustchain_unique_together_trustchain_type_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='trustchain',
unique_together={('sub', 'trust_anchor')},
),
migrations.RemoveField(
model_name='trustchain',
name='type',
),
]
@@ -0,0 +1,28 @@
# Generated by Django 4.0.3 on 2022-04-23 14:48
from django.db import migrations, models
import spid_cie_oidc.entity.models
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0016_alter_trustchain_unique_together_and_more'),
]
operations = [
migrations.RemoveField(
model_name='federationentityconfiguration',
name='jwks',
),
migrations.AddField(
model_name='federationentityconfiguration',
name='jwks_core',
field=models.JSONField(default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, help_text='a list of private keys for Core ops'),
),
migrations.AddField(
model_name='federationentityconfiguration',
name='jwks_fed',
field=models.JSONField(default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, help_text='a list of private keys for Federation ops'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.0.5 on 2022-07-03 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0017_remove_federationentityconfiguration_jwks_and_more'),
]
operations = [
migrations.AddField(
model_name='trustchain',
name='jwks',
field=models.JSONField(default=[], help_text='jwks of this federation entity'),
preserve_default=False,
),
]
@@ -0,0 +1,33 @@
# Generated by Django 4.0.5 on 2022-10-18 09:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import spid_cie_oidc.entity.utils
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('spid_cie_oidc_entity', '0018_trustchain_jwks'),
]
operations = [
migrations.CreateModel(
name='StaffToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('token', models.CharField(default=spid_cie_oidc.entity.utils.random_token, help_text='it will be generated automatically.', max_length=255)),
('expire_at', models.DateTimeField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
('user', models.ForeignKey(help_text='The user responsible of thi token', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Staff Token',
'verbose_name_plural': 'Staff Tokens',
},
),
]
@@ -0,0 +1,33 @@
# Generated by Django 4.2.1 on 2023-07-08 11:35
from django.db import migrations, models
import spid_cie_oidc.entity.models
import spid_cie_oidc.entity.validators
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0019_stafftoken"),
]
operations = [
migrations.AlterField(
model_name="federationentityconfiguration",
name="jwks_core",
field=models.JSONField(
blank=True,
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
help_text="a list of private keys for Core ops",
validators=[spid_cie_oidc.entity.validators.validate_private_jwks],
),
),
migrations.AlterField(
model_name="federationentityconfiguration",
name="jwks_fed",
field=models.JSONField(
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
help_text="a list of private keys for Federation ops",
validators=[spid_cie_oidc.entity.validators.validate_private_jwks],
),
),
]
@@ -0,0 +1,86 @@
# Generated by Django 4.2.1 on 2023-07-08 13:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0020_alter_federationentityconfiguration_jwks_core_and_more",
),
]
operations = [
migrations.CreateModel(
name="FederationHistoricalKey",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
(
"kid",
models.CharField(
choices=[
(0, "unspecified"),
(1, "keyCompromise"),
(2, "cACompromise"),
(3, "affiliationChanged"),
(4, "superseded"),
(5, "cessationOfOperation"),
(6, "certificateHold"),
(8, "removeFromCRL"),
(9, "privilegeWithdrawn"),
(10, "aACompromise"),
],
max_length=33,
),
),
(
"inactive_from",
models.DateTimeField(
help_text="Expired or Revocation date if revocation motivation is configured"
),
),
(
"revocation_motivation",
models.CharField(
blank=True,
choices=[
(0, "unspecified"),
(1, "keyCompromise"),
(2, "cACompromise"),
(3, "affiliationChanged"),
(4, "superseded"),
(5, "cessationOfOperation"),
(6, "certificateHold"),
(8, "removeFromCRL"),
(9, "privilegeWithdrawn"),
(10, "aACompromise"),
],
max_length=33,
),
),
(
"entity",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="spid_cie_oidc_entity.federationentityconfiguration",
),
),
],
options={
"verbose_name": "Federation Historical Key",
"verbose_name_plural": "Federation Historical Keys",
},
),
]
@@ -0,0 +1,79 @@
# Generated by Django 4.2.1 on 2023-07-08 14:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
replaces = [
("spid_cie_oidc_entity", "0021_federationhistoricalkey"),
("spid_cie_oidc_entity", "0022_federationhistoricalkey_jwk_and_more"),
("spid_cie_oidc_entity", "0023_alter_federationhistoricalkey_jwk"),
("spid_cie_oidc_entity", "0024_alter_federationhistoricalkey_kid"),
("spid_cie_oidc_entity", "0025_alter_federationhistoricalkey_jwk"),
("spid_cie_oidc_entity", "0026_alter_federationhistoricalkey_jwk"),
]
dependencies = [
(
"spid_cie_oidc_entity",
"0020_alter_federationentityconfiguration_jwks_core_and_more",
),
]
operations = [
migrations.CreateModel(
name="FederationHistoricalKey",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
("kid", models.CharField(max_length=128)),
(
"inactive_from",
models.DateTimeField(
help_text="Expired or Revocation date if revocation motivation is configured"
),
),
(
"revocation_motivation",
models.CharField(
blank=True,
choices=[
(0, "unspecified"),
(1, "keyCompromise"),
(2, "cACompromise"),
(3, "affiliationChanged"),
(4, "superseded"),
(5, "cessationOfOperation"),
(6, "certificateHold"),
(8, "removeFromCRL"),
(9, "privilegeWithdrawn"),
(10, "aACompromise"),
],
max_length=33,
),
),
(
"entity",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="spid_cie_oidc_entity.federationentityconfiguration",
),
),
("jwk", models.JSONField(default=dict, help_text="private jwk")),
],
options={
"verbose_name": "Federation Historical Key",
"verbose_name_plural": "Federation Historical Keys",
},
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.1 on 2023-07-08 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0021_federationhistoricalkey"),
]
operations = [
migrations.AddField(
model_name="federationhistoricalkey",
name="jwk",
field=models.TextField(default={}, help_text="private jwk"),
preserve_default=False,
),
migrations.AlterField(
model_name="federationhistoricalkey",
name="kid",
field=models.CharField(max_length=33),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-07-08 13:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0022_federationhistoricalkey_jwk_and_more"),
]
operations = [
migrations.AlterField(
model_name="federationhistoricalkey",
name="jwk",
field=models.TextField(default={}, help_text="private jwk"),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-07-08 13:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0023_alter_federationhistoricalkey_jwk"),
]
operations = [
migrations.AlterField(
model_name="federationhistoricalkey",
name="kid",
field=models.CharField(max_length=128),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-07-08 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0024_alter_federationhistoricalkey_kid"),
]
operations = [
migrations.AlterField(
model_name="federationhistoricalkey",
name="jwk",
field=models.JSONField(default={}, help_text="private jwk"),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-07-08 14:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0025_alter_federationhistoricalkey_jwk"),
]
operations = [
migrations.AlterField(
model_name="federationhistoricalkey",
name="jwk",
field=models.JSONField(default=dict, help_text="private jwk"),
),
]
@@ -0,0 +1,35 @@
# Generated by Django 4.2.1 on 2023-07-08 14:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk",
),
]
operations = [
migrations.AlterField(
model_name="federationhistoricalkey",
name="revocation_motivation",
field=models.CharField(
blank=True,
choices=[
("0", "unspecified"),
("1", "keyCompromise"),
("2", "cACompromise"),
("3", "affiliationChanged"),
("4", "superseded"),
("5", "cessationOfOperation"),
("6", "certificateHold"),
("8", "removeFromCRL"),
("9", "privilegeWithdrawn"),
("10", "aACompromise"),
],
max_length=33,
),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.1 on 2023-07-09 15:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0027_alter_federationhistoricalkey_revocation_motivation",
),
]
operations = [
migrations.AddField(
model_name="federationentityconfiguration",
name="issue_x509",
field=models.BooleanField(
default=False,
help_text="If TA/Intermediate: issues x509 in both entity statements and configuration. if leaf, issues x509 certificates in configuration.",
),
),
]
@@ -0,0 +1,38 @@
# Generated by Django 4.2.1 on 2023-07-09 15:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("spid_cie_oidc_entity", "0028_federationentityconfiguration_issue_x509"),
]
operations = [
migrations.AlterField(
model_name="federationentityconfiguration",
name="entity_type",
field=models.CharField(
blank=True,
choices=[
("federation_entity", "federation_entity"),
("openid_relying_party", "openid_relying_party"),
("openid_provider", "openid_provider"),
("oauth_resource", "oauth_resource"),
("wallet_provider", "wallet_provider"),
("wallet_relying_party", "wallet_relying_party"),
],
default="openid_relying_party",
help_text="OpenID Connect Federation entity type",
max_length=33,
),
),
migrations.AlterField(
model_name="federationentityconfiguration",
name="issue_x509",
field=models.BooleanField(
default=False,
help_text="Issues x509 certificates in the Entity Configuration.",
),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 4.2.1 on 2023-07-13 04:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0029_alter_federationentityconfiguration_entity_type_and_more",
),
]
operations = [
migrations.RemoveField(
model_name="federationentityconfiguration",
name="issue_x509",
),
]
@@ -0,0 +1,34 @@
# Generated by Django 4.2.7 on 2023-11-14 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0030_remove_federationentityconfiguration_issue_x509",
),
]
operations = [
migrations.AlterField(
model_name="federationentityconfiguration",
name="entity_type",
field=models.CharField(
blank=True,
choices=[
("federation_entity", "federation_entity"),
("openid_relying_party", "openid_relying_party"),
("openid_provider", "openid_provider"),
("openid_credential_issuer", "openid_credential_issuer"),
("oauth_resource", "oauth_resource"),
("wallet_provider", "wallet_provider"),
("wallet_relying_party", "wallet_relying_party"),
],
default="openid_relying_party",
help_text="OpenID Connect Federation entity type",
max_length=33,
),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 4.2.3 on 2023-12-13 14:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"spid_cie_oidc_entity",
"0031_alter_federationentityconfiguration_entity_type",
),
]
operations = [
migrations.AlterField(
model_name="fetchedentitystatement",
name="jwt",
field=models.TextField(blank=False, null=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.0.5 on 2026-03-23 15:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spid_cie_oidc_entity', '0032_alter_fetchedentitystatement_jwt'),
]
operations = [
migrations.AlterField(
model_name='federationentityconfiguration',
name='entity_type',
field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], default='openid_relying_party', help_text='OpenID Connect Federation entity type', max_length=33),
),
]
@@ -0,0 +1,550 @@
import json
import logging
from typing import Union
import uuid
from django.contrib.auth import get_user_model
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
from spid_cie_oidc.entity.abstract_models import TimeStampedModel
from cryptojwt.jwk.jwk import key_from_jwk_dict
from spid_cie_oidc.entity.jwks import (
create_jwk,
private_pem_from_jwk,
public_pem_from_jwk,
)
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.settings import (
ENTITY_STATUS,
ENTITY_TYPE_LEAFS,
ENTITY_TYPES,
FEDERATION_DEFAULT_EXP
)
from spid_cie_oidc.entity.statements import EntityConfiguration
from spid_cie_oidc.entity.utils import exp_from_now, iat_now, random_token
from spid_cie_oidc.entity.validators import (
validate_entity_metadata,
validate_metadata_algs,
validate_private_jwks
)
from .jwk_x5c import update_jwks_with_x5c
from .jwks import public_jwk_from_private_jwk
logger = logging.getLogger(__name__)
def is_leaf(statement_metadata):
for _typ in ENTITY_TYPE_LEAFS:
if _typ in statement_metadata:
return True # pragma: no cover
class FederationEntityConfiguration(TimeStampedModel):
"""
Federation Authority configuration.
"""
def _create_jwks():
return [create_jwk()]
uuid = models.UUIDField(
blank=False, null=False, default=uuid.uuid4, unique=True, editable=False
)
sub = models.URLField(
max_length=255,
blank=False,
null=False,
help_text=_(
"URL that identifies this Entity in the Federation. "
"This value and iss are the same in the Entity Configuration."
),
)
default_exp = models.PositiveIntegerField(
default=FEDERATION_DEFAULT_EXP,
help_text=_("how many minutes from now() an issued statement must expire"),
)
default_signature_alg = models.CharField(
max_length=16,
default=(
"ES256" if (
hasattr(settings, "PRIVATE_KEY_TYPE")
and getattr(settings, "PRIVATE_KEY_TYPE") == "EC"
) else "RS256"
),
blank=False,
null=False,
help_text=_("default signature algorithm, eg: RS256"),
)
authority_hints = models.JSONField(
blank=True,
null=False,
help_text=_("only required if this Entity is an intermediary or leaf."),
default=list,
)
jwks_fed = models.JSONField(
blank=False,
null=False,
help_text=_("a list of private keys for Federation ops"),
default=_create_jwks,
validators = [validate_private_jwks],
)
jwks_core = models.JSONField(
blank=True,
null=False,
help_text=_("a list of private keys for Core ops"),
default=_create_jwks,
validators = [validate_private_jwks],
)
trust_marks = models.JSONField(
blank=True,
help_text=_("which trust marks MUST be exposed in its entity configuration"),
default=list,
)
trust_mark_issuers = models.JSONField(
blank=True,
help_text=_(
"Only usable for Trust Anchors and intermediates. "
"Which issuers are allowed to issue trust marks for the descendants. "
'Example: {"https://www.spid.gov.it/certification/rp": '
'["https://registry.spid.agid.gov.it", "https://intermediary.spid.it"],'
'"https://sgd.aa.it/onboarding": ["https://sgd.aa.it", ]}'
),
default=dict,
)
entity_type = models.CharField(
max_length=33,
blank=True,
default="openid_relying_party",
choices=[(i, i) for i in ENTITY_TYPES],
help_text=_("OpenID Connect Federation entity type"),
)
metadata = models.JSONField(
blank=False,
null=False,
help_text=_(
"federation_entity metadata, eg: "
'{"federation_entity": { ... },'
'"openid_provider": { ... },'
'"openid_relying_party": { ... },'
'"oauth_resource": { ... }'
"}"
),
default=dict,
validators=[
validate_entity_metadata,
validate_metadata_algs
],
)
constraints = models.JSONField(
blank=True,
help_text=_(
"""
{
"naming_constraints": {
"permitted": [
"https://.example.com"
],
"excluded": [
"https://east.example.com"
]
},
"max_path_length": 2
}
"""
),
default=dict,
# TODO
# validators=[validate_entity_metadata,]
)
is_active = models.BooleanField(
default=False,
help_text=_(
"If this configuration is active. "
"At least one configuration must be active"
),
)
class Meta:
verbose_name = "Federation Entity Configuration"
verbose_name_plural = "Federation Entity Configurations"
@classmethod
def get_active_conf(cls):
"""
returns the first available active acsia engine configuration found
"""
return cls.objects.filter(is_active=True).first()
@property
def public_jwks(self):
"""
Export public JWKs from private jwks_fed.
Uses public_jwk_from_private_jwk to avoid SerializationNotPossible
when serialize_rsa_key+RSAKey.thumbprint() fails with cryptojwt.
"""
res = []
for i in self.jwks_fed:
skey = public_jwk_from_private_jwk(i)
res.append(skey)
return res
@property
def pems_as_dict(self):
res = {}
for i in self.jwks_fed:
res[i["kid"]] = {
"private": private_pem_from_jwk(i),
"public": public_pem_from_jwk(i),
}
return res
@property
def pems_as_json(self):
return json.dumps(self.pems_as_dict, indent=2)
@property
def kids(self) -> list:
return [i["kid"] for i in self.jwks_fed]
@property
def type(self) -> list:
return [i for i in self.metadata.keys()]
@property
def is_leaf(self):
return is_leaf(self.metadata)
@property
def entity_configuration_as_dict(self):
conf = {
"exp": exp_from_now(self.default_exp),
"iat": iat_now(),
"iss": self.sub,
"sub": self.sub,
"jwks": {"keys": self.public_jwks},
"metadata": self.metadata,
}
if self.trust_mark_issuers:
conf["trust_mark_issuers"] = self.trust_mark_issuers
if self.trust_marks:
conf["trust_marks"] = self.trust_marks
if self.constraints:
conf["constraints"] = self.constraints
if self.authority_hints:
conf["authority_hints"] = self.authority_hints
elif self.is_leaf: # pragma: no cover
_msg = f"Entity {self.sub} is a leaf and requires authority_hints valued"
logger.error(_msg)
if hasattr(settings, "X509_COMMON_NAME"):
conf['jwks'] = {"keys": update_jwks_with_x5c(
jwks=self.public_jwks,
private_key=key_from_jwk_dict(self.jwks_fed[0]).private_key(),
subject=self.sub,
is_ca_or_int=True
)}
return conf
@property
def entity_configuration_as_json(self):
return json.dumps(self.entity_configuration_as_dict)
@property
def entity_configuration_as_jws(self, **kwargs):
return create_jws(
self.entity_configuration_as_dict,
self.jwks_fed[0],
alg=self.default_signature_alg,
typ="entity-statement+jwt",
**kwargs,
)
@property
def fetch_endpoint(self) -> Union[str, None]:
metadata = self.entity_configuration_as_dict.get('metadata', {})
if 'federation_entity' in metadata:
return metadata['federation_entity'].get("federation_fetch_endpoint", None)
def set_jwks_as_array(self):
for i in ('jwks_fed','jwks_core'):
value = getattr(self, i)
if not isinstance(value, list):
setattr(self, i, [value])
def save(self, *args, **kwargs):
self.set_jwks_as_array()
super().save(*args, **kwargs)
def __str__(self):
return "{} [{}]".format(
self.sub, "active" if self.is_active else "--"
)
class FetchedEntityStatement(TimeStampedModel):
"""
Entity Statement acquired by a third party
"""
iss = models.URLField(
max_length=255,
blank=False,
help_text=_(
"URL that identifies the issuer of this statement in the Federation. "
),
)
sub = models.URLField(
max_length=255,
blank=False,
help_text=_("URL that identifies this Entity in the Federation. "),
)
exp = models.DateTimeField()
iat = models.DateTimeField()
statement = models.JSONField(
blank=False, null=False, help_text=_("Entity statement"), default=dict
)
jwt = models.TextField(null=False, blank=False)
class Meta:
verbose_name = "Fetched Entity Statement"
verbose_name_plural = "Fetched Entity Statement"
def get_entity_configuration_as_obj(self):
return EntityConfiguration(self.jwt)
@property
def is_expired(self):
return self.exp <= timezone.localtime()
def __str__(self):
return f"{self.sub} issued by {self.iss}"
class TrustChain(TimeStampedModel):
"""
Federation Trust Chain
"""
sub = models.URLField(
max_length=255,
blank=False,
help_text=_("URL that identifies this Entity in the Federation. "),
)
trust_anchor = models.ForeignKey(FetchedEntityStatement, on_delete=models.CASCADE)
exp = models.DateTimeField()
iat = models.DateTimeField(auto_now_add=True)
chain = models.JSONField(
blank=True,
help_text=_(
"A list of entity statements collected during the metadata discovery"
),
default=list,
)
jwks = models.JSONField(
blank=False,
null=False,
help_text=_("jwks of this federation entity")
)
metadata = models.JSONField(
blank=True,
null=True,
help_text=_(
"The final metadata applied with the metadata policy built over the chain"
),
default=dict,
)
trust_marks = models.JSONField(
blank=True, help_text=_("verified trust marks"), default=list
)
parties_involved = models.JSONField(
blank=True,
help_text=_("subjects involved in the metadata discovery"),
default=list,
)
status = models.CharField(
max_length=33,
default="unreachable",
help_text=_("Status of this trust chain, on each update."),
choices=[(i, i) for i in list(ENTITY_STATUS.keys())],
)
log = models.TextField(blank=True, help_text=_("status log"), default="")
processing_start = models.DateTimeField(
help_text=_(
"When the metadata discovery started for this Trust Chain. "
"It should prevent concurrent processing for the same sub/type."
),
default=timezone.localtime,
)
is_active = models.BooleanField(
default=True,
help_text=_("If you need to disable the trust to this subject, uncheck this"),
)
class Meta:
verbose_name = "Trust Chain"
verbose_name_plural = "Trust Chains"
unique_together = ("sub", "trust_anchor")
@property
def subject(self):
return self.sub # pragma: no cover
@property
def is_expired(self):
return self.exp <= timezone.localtime()
@property
def iat_as_timestamp(self):
return int(self.iat.timestamp())
@property
def exp_as_timestamp(self):
return int(self.exp.timestamp())
@property
def is_valid(self):
return self.is_active and ENTITY_STATUS[self.status]
def __str__(self):
return "{} [{}] [{}]".format(
self.sub, self.trust_anchor, self.is_valid
)
class StaffToken(TimeStampedModel):
"""
Token provisioned to staffs operators for protected resources
"""
user = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
help_text=_("The user responsible of thi token"),
)
token = models.CharField(
max_length=255,
blank=False,
null=False,
default = random_token,
help_text=_("it will be generated automatically."),
)
expire_at = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(
default=True,
blank=False,
null=False
)
class Meta:
verbose_name = "Staff Token"
verbose_name_plural = "Staff Tokens"
@property
def is_valid(self):
if self.is_active and not self.expire_at:
return True
elif self.is_active and self.expire_at > timezone.localtime():
return True
else:
return False
def __str__(self):
return f"{self.user} {self.is_active}"
class FederationHistoricalKey(TimeStampedModel):
"""
https://openid.net/specs/openid-connect-federation-1_0.html#name-federation-historical-keys-
"""
REVOCATION_REASONS_MAP = (
("0", 'unspecified'),
("1", 'keyCompromise'),
("2", 'cACompromise'),
("3", 'affiliationChanged'),
("4", 'superseded'),
("5", 'cessationOfOperation'),
("6", 'certificateHold'),
# 7 is unused in rfc5280
("8", 'removeFromCRL'),
("9", 'privilegeWithdrawn'),
("10", 'aACompromise')
)
# REVOCATION_REASONS_CODES = {k:k for k in REVOCATION_REASONS_MAP.keys()}
entity = models.ForeignKey(FederationEntityConfiguration, on_delete=models.CASCADE)
kid = models.CharField(
blank=False, null=False, max_length=128
)
inactive_from = models.DateTimeField(help_text=_(
"Expired or Revocation date if revocation motivation is configured"
))
revocation_motivation = models.CharField(
blank=True, null=False, max_length=33, choices=REVOCATION_REASONS_MAP
)
jwk = models.JSONField(help_text=_("private jwk"), default=dict)
class Meta:
verbose_name = "Federation Historical Key"
verbose_name_plural = "Federation Historical Keys"
@property
def as_dict(self):
if not self.jwk:
return {}
kdict = public_jwk_from_private_jwk(self.jwk)
if self.revocation_motivation:
kdict["revoked"] = {
"revoked_at": int(self.inactive_from.timestamp()),
"reason": dict(self.REVOCATION_REASONS_MAP)[self.revocation_motivation],
"reason_code": self.revocation_motivation
}
else:
kdict["exp"] = int(self.inactive_from.timestamp())
return kdict
@property
def as_json(self):
return json.dumps(self.as_dict)
def save(self, *args, **kwargs):
if self.kid:
for jwk in self.entity.jwks_fed:
if not jwk.get('kid', None):
continue
elif self.kid == jwk['kid']:
_indx = self.entity.jwks_fed.index(jwk)
self.jwk = self.entity.jwks_fed.pop(_indx)
self.entity.save()
break
super().save(*args, **kwargs)
def __str__(self):
return f"{self.entity} {self.kid}"
def get_first_self_trust_anchor(
sub: str = None,
) -> Union[FederationEntityConfiguration, None]:
"""
get the first available Trust Anchor that represent self
as a qualified issuer
"""
lk = dict(metadata__federation_entity__isnull=False, is_active=True)
if sub:
lk["sub"] = sub
return FederationEntityConfiguration.objects.filter(**lk).first()
@@ -0,0 +1,355 @@
__author__ = "Roland Hedberg"
__license__ = "Apache 2.0"
__version__ = ""
import logging
logger = logging.getLogger(__name__)
class PolicyError(Exception):
pass
def combine_subset_of(s1, s2): # pragma: no cover
return list(set(s1).intersection(set(s2)))
def combine_superset_of(s1, s2): # pragma: no cover
return list(set(s1).intersection(set(s2)))
def combine_one_of(s1, s2): # pragma: no cover
return list(set(s1).intersection(set(s2)))
def combine_add(s1, s2): # pragma: no cover
if isinstance(s1, list):
set1 = set(s1)
else:
set1 = {s1}
if isinstance(s2, list):
set2 = set(s2)
else:
set2 = {s2}
return list(set1.union(set2))
POLICY_FUNCTIONS = {
"subset_of",
"superset_of",
"one_of",
"add",
"value",
"default",
"essential",
}
OP2FUNC = {
"subset_of": combine_subset_of,
"superset_of": combine_superset_of,
"one_of": combine_one_of,
"add": combine_add,
}
def do_sub_one_super_add(superior, child, policy): # pragma: no cover
if policy in superior and policy in child:
comb = OP2FUNC[policy](superior[policy], child[policy])
if comb:
return comb
else:
raise PolicyError("Value sets doesn't overlap")
elif policy in superior:
return superior[policy]
elif policy in child:
return child[policy]
def do_value(superior, child, policy): # pragma: no cover
if policy in superior and policy in child:
if superior[policy] == child[policy]:
return superior[policy]
else:
raise PolicyError("Not allowed to combine values")
elif policy in superior:
return superior[policy]
elif policy in child:
return child[policy]
def do_default(superior, child, policy): # pragma: no cover
# A child's default can not override a superiors
if policy in superior and policy in child:
if superior["default"] == child["default"]:
return superior["default"]
else:
raise PolicyError("Not allowed to change default")
elif policy in superior:
return superior[policy]
elif policy in child:
return child[policy]
def do_essential(superior, child, policy): # pragma: no cover
# essential: an child can make it True if a superior has states False
# but not the other way around
if policy in superior and policy in child:
if not superior[policy] and child["essential"]:
return True
else:
return superior[policy]
elif policy in superior:
return superior[policy]
elif policy in child: # Not in superior is the same as essential=True
return True
DO_POLICY = {
"superset_of": do_sub_one_super_add,
"subset_of": do_sub_one_super_add,
"one_of": do_sub_one_super_add,
"add": do_sub_one_super_add,
"value": do_value,
"default": do_default,
"essential": do_essential,
}
def combine_claim_policy(superior, child): # noqa: C901 # pragma: no cover
"""
Combine policy rules.
Applying the child policy can only make the combined policy more restrictive.
:param superior: Superior policy
:param child: Intermediates policy
"""
# weed out everything I don't recognize
superior_set = set(superior).intersection(POLICY_FUNCTIONS)
child_set = set(child).intersection(POLICY_FUNCTIONS)
if "value" in superior_set: # An exact value can not be restricted.
if child_set:
if "essential" in child_set:
if len(child_set) == 1:
return {"value": superior["value"], "essential": child["essential"]}
else:
raise PolicyError(
"value can only be combined with essential, not {}".format(
child_set
)
)
elif "value" in child_set:
if child["value"] != superior["value"]: # Not OK
raise PolicyError("Child can not set another value then superior")
else:
return superior
else:
raise PolicyError(
"Not allowed combination of policies: {} + {}".format(
superior, child
)
)
return superior
else:
if "essential" in superior_set and "essential" in child_set:
# can only go from False to True
if (
superior["essential"] != child["essential"]
and child["essential"] is False
):
raise PolicyError("Essential can not go from True to False")
comb_policy = superior_set.union(child_set)
if "one_of" in comb_policy:
if "subset_of" in comb_policy or "superset_of" in comb_policy:
raise PolicyError(
"one_of can not be combined with subset_of/superset_of"
)
rule = {}
for policy in comb_policy:
rule[policy] = DO_POLICY[policy](superior, child, policy)
if comb_policy == {"superset_of", "subset_of"}:
# make sure the subset_of is a superset of superset_of.
if set(rule["superset_of"]).difference(set(rule["subset_of"])):
raise PolicyError("superset_of not a super set of subset_of")
elif comb_policy == {"superset_of", "subset_of", "default"}:
# make sure the subset_of is a superset of superset_of.
if set(rule["superset_of"]).difference(set(rule["subset_of"])):
raise PolicyError("superset_of not a super set of subset_of")
if set(rule["default"]).difference(set(rule["subset_of"])):
raise PolicyError("default not a sub set of subset_of")
if set(rule["superset_of"]).difference(set(rule["default"])):
raise PolicyError("default not a super set of subset_of")
elif comb_policy == {"subset_of", "default"}:
if set(rule["default"]).difference(set(rule["subset_of"])):
raise PolicyError("default not a sub set of subset_of")
elif comb_policy == {"superset_of", "default"}:
if set(rule["superset_of"]).difference(set(rule["default"])):
raise PolicyError("default not a super set of subset_of")
elif comb_policy == {"one_of", "default"}:
if isinstance(rule["default"], list):
if set(rule["default"]).difference(set(rule["one_of"])):
raise PolicyError("default not a super set of one_of")
else:
if {rule["default"]}.difference(set(rule["one_of"])):
raise PolicyError("default not a super set of one_of")
return rule
def combine_policy(superior, child):
res = {}
sup_set = set(superior.keys())
chi_set = set(child.keys())
for claim in set(sup_set).intersection(chi_set):
res[claim] = combine_claim_policy(superior[claim], child[claim])
for claim in sup_set.difference(chi_set):
res[claim] = superior[claim]
for claim in chi_set.difference(sup_set):
res[claim] = child[claim]
return res
def gather_policies(chain, entity_type):
"""
Gather and combine all the metadata policies that are defined in the trust chain
:param chain: A list of Entity Statements
:return: The combined metadata policy
"""
try:
combined_policy = chain[0]["metadata_policy"][entity_type]
except KeyError:
combined_policy = {}
for es in chain[1:]:
try:
child = es["metadata_policy"][entity_type]
except KeyError:
pass
else:
combined_policy = combine_policy(combined_policy, child)
return combined_policy
def union(val1, val2):
if isinstance(val1, list):
base = set(val1)
else:
base = {val1}
if isinstance(val2, list):
ext = set(val2)
else:
ext = {val2}
return base.union(ext)
def apply_policy(metadata, policy):
"""
Apply a metadata policy to a metadata statement.
The order is value, add, default and then the checks subset_of/superset_of and one_of
:param metadata: A metadata statement
:param policy: A metadata policy
:return: A metadata statement that adheres to a metadata policy
"""
metadata_set = set(metadata.keys())
policy_set = set(policy.keys())
# Metadata claims that there exists a policy for
for claim in metadata_set.intersection(policy_set):
if "value" in policy[claim]: # value overrides everything
metadata[claim] = policy[claim]["value"]
else:
if "one_of" in policy[claim]:
# The is for claims that can have only one value
if isinstance(metadata[claim], list): # Should not be but ...
_claim = [
c for c in metadata[claim] if c in policy[claim]["one_of"]
]
if _claim:
metadata[claim] = _claim[0]
else:
raise PolicyError(
"{}: None of {} among {}".format(
claim, metadata[claim], policy[claim]["one_of"]
)
)
else:
if metadata[claim] in policy[claim]["one_of"]:
pass
else:
raise PolicyError(
"{} not among {}".format(
metadata[claim], policy[claim]["one_of"]
)
)
else:
# The following is for claims that can have lists of values
if "add" in policy[claim]:
metadata[claim] = list(union(metadata[claim], policy[claim]["add"]))
if "subset_of" in policy[claim]:
_val = set(policy[claim]["subset_of"]).intersection(
set(metadata[claim])
)
if _val:
metadata[claim] = list(_val)
else:
raise PolicyError(
"{} not subset of {}".format(
metadata[claim], policy[claim]["subset_of"]
)
)
if "superset_of" in policy[claim]:
if set(policy[claim]["superset_of"]).difference(
set(metadata[claim])
):
raise PolicyError(
"{} not superset of {}".format(
metadata[claim], policy[claim]["superset_of"]
)
)
else:
pass
# In policy but not in metadata
for claim in policy_set.difference(metadata_set):
if "value" in policy[claim]:
metadata[claim] = policy[claim]["value"]
elif "add" in policy[claim]:
metadata[claim] = policy[claim]["add"]
elif "default" in policy[claim]:
metadata[claim] = policy[claim]["default"]
if claim not in metadata:
if "essential" in policy[claim] and policy[claim]["essential"]:
raise PolicyError("Essential claim '{}' missing".format(claim))
# All that are in metadata but not in policy should just remain
return metadata
def diff2policy(new, old):
res = {}
for claim in set(new).intersection(set(old)):
if new[claim] == old[claim]:
continue
else:
res[claim] = {"value": new[claim]}
for claim in set(new).difference(set(old)):
if claim in ["contacts"]:
res[claim] = {"add": new[claim]}
else:
res[claim] = {"value": new[claim]}
return res
@@ -0,0 +1,12 @@
from typing import List
from pydantic import BaseModel, HttpUrl, EmailStr
class FAMetadata(BaseModel):
contacts: List[EmailStr]
federation_fetch_endpoint: HttpUrl
federation_resolve_endpoint: HttpUrl
federation_list_endpoint: HttpUrl
homepage_uri: HttpUrl
organization_name: str
@@ -0,0 +1,72 @@
from typing import List, Literal, Optional
from pydantic import BaseModel, validator
class Jwk(BaseModel):
kid: str # Base64url-encoded thumbprint string
kty: Literal["EC", "RSA"]
# TODO: verify if is optional
alg: Optional[
Literal[
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512",
"PS256",
"PS384",
"PS512",
"RSA-OAEP",
"RSA-OAEP-256",
"ECDH-ES",
"ECDH-ES+A128KW",
"ECDH-ES+A256KW"
]
]
use: Optional[Literal["sig", "enc"]]
n: Optional[str] # Base64urlUInt-encoded
e: Optional[str] # Base64urlUInt-encoded
def check_value_for_rsa(value, name, values):
if "EC" == values.get("kty") and value:
raise ValueError(f"{name} must be present only for kty = RSA")
def check_value_for_ec(value, name, values):
if "RSA" == values.get("kty") and value:
raise ValueError(f"{name} must be present only for kty = EC")
@validator("n")
def validate_n(cls, n_value, values):
cls.check_value_for_rsa(n_value, "n", values)
@validator("e")
def validate_e(cls, e_value, values):
cls.check_value_for_rsa(e_value, "e", values)
class JwkCie(Jwk):
x: Optional[str] # Base64url-encoded
y: Optional[str] # Base64url-encoded
crv: Optional[Literal["P-256", "P-384", "P-521"]]
@validator("x")
def validate_x(cls, x_value, values):
cls.check_value_for_ec(x_value, "x", values)
@validator("y")
def validate_y(cls, y_value, values):
cls.check_value_for_ec(y_value, "y", values)
@validator("crv")
def validate_crv(cls, crv_value, values):
cls.check_value_for_ec(crv_value, "crv", values)
class JwksCie(BaseModel):
keys: List[JwkCie]
class JwksSpid(BaseModel):
keys: List[Jwk]
@@ -0,0 +1,138 @@
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, HttpUrl, root_validator
from .jwks import JwksCie
class ScopeSupported(str, Enum):
openid = "openid"
offline_access = "offline_access"
profile = "profile"
email = "email"
class ResponseModesSupported(str, Enum):
form_post = ("form_post",)
query = "query"
class GrantTypeSupported(str, Enum):
refresh_token = "refresh_token" # nosec - B105
authorization_code = "authorization_code"
class AcrValuesSupported(str, Enum):
spid_l1 = "https://www.spid.gov.it/SpidL1"
spid_l2 = "https://www.spid.gov.it/SpidL2"
spid_l3 = "https://www.spid.gov.it/SpidL3"
class SigningAlgValuesSupported(str, Enum):
es256 = "ES256"
es384 = "ES384"
es512 = "ES512"
rs256 = "RS256"
rs384 = "RS384"
rs512 = "RS512"
# TODO: to be confirmed
# ps256 = "PS256"
# ps384 = "PS384"
# ps512 = "PS512"
class EncryptionAlgValuesSupported(str, Enum):
rsa_oaep = "RSA-OAEP"
ras_oaep_256 = "RSA-OAEP-256"
class EncryptionEncValuesSupported(str, Enum):
a128cbc_hs256 = "A128CBC-HS256"
a192cbc_hs384 = "A192CBC-HS384"
a256cbc_hs512 = "A256CBC-HS512"
a128gcm = "A128GCM"
a192gcm = "A192GCM"
a256gcm = "A256GCM"
rsa_oaep_256 = "RSA-OAEP-256"
class ClaimsSupported(str, Enum):
given_name = "given_name"
family_name = "family_name"
email = "email"
email_verified = "email_verified"
gender = "gender"
birthdate = "birthdate"
phone_number = "phone_number"
phone_number_verified = "phone_number_verified"
place_of_birth = "place_of_birth"
address = "address"
document_details = "document_details"
https_attributes_eid_gov_it_e_delivery_service = (
"https://attributes.eid.gov.it/e_delivery_service"
)
https_attributes_eid_gov_it_fiscal_number = (
"https://attributes.eid.gov.it/fiscal_number"
)
https_attributes_eid_gov_it_idANPR = "https://attributes.eid.gov.it/idANPR"
https_attributes_eid_gov_it_physical_phone_number = (
"https://attributes.eid.gov.it/physical_phone_number"
)
class OPMetadata(BaseModel):
organization_name: str
issuer: HttpUrl
authorization_endpoint: HttpUrl
token_endpoint: HttpUrl
userinfo_endpoint: HttpUrl
introspection_endpoint: HttpUrl
revocation_endpoint: HttpUrl
token_endpoint_auth_signing_alg_values_supported: List[SigningAlgValuesSupported]
id_token_encryption_alg_values_supported: List[EncryptionAlgValuesSupported]
id_token_encryption_enc_values_supported: List[EncryptionEncValuesSupported]
userinfo_encryption_alg_values_supported: List[EncryptionAlgValuesSupported]
userinfo_encryption_enc_values_supported: List[EncryptionEncValuesSupported]
request_object_encryption_alg_values_supported: List[EncryptionAlgValuesSupported]
request_object_encryption_enc_values_supported: List[EncryptionEncValuesSupported]
id_token_signing_alg_values_supported: List[SigningAlgValuesSupported]
userinfo_signing_alg_values_supported: List[SigningAlgValuesSupported]
request_object_signing_alg_values_supported: List[SigningAlgValuesSupported]
token_endpoint_auth_methods_supported = ["private_key_jwt"]
subject_types_supported = ["pairwise"]
request_parameter_supported = True
acr_values_supported: List[AcrValuesSupported]
signed_jwks_uri: Optional[HttpUrl]
jwks_uri: Optional[HttpUrl]
jwks: Optional[JwksCie]
@root_validator(pre=False)
def validate(cls, values):
jwks_there = False
for i in ("jwks_uri", "jwks", "signed_jwks_uri"):
if values.get(i):
jwks_there = True
break
if not jwks_there:
raise ValueError(
"one of signed_jwks_uri or jwks_uri or jwks must be set"
)
return values
class OPMetadataCie(OPMetadata):
scopes_supported: List[ScopeSupported]
response_types_supported = ["code"]
response_modes_supported: List[ResponseModesSupported]
grant_types_supported: List[GrantTypeSupported]
claims_supported: List[ClaimsSupported]
claims_parameter_supported = True
tls_client_certificate_bound_access_tokens = True
authorization_response_iss_parameter_supported = True
class OPMetadataSpid(OPMetadata):
op_name: Optional[str]
op_uri: Optional[str]
@@ -0,0 +1,53 @@
from typing import List, Literal, Optional
from pydantic import BaseModel, HttpUrl, constr
class ResolveResponse(BaseModel):
jose: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
class ResolveErrorResponse(BaseModel):
operation: Optional[str]
error: Literal[
"invalid_request",
"invalid_subject",
"invalid_trust_anchor",
"invalid_trust_chain",
"invalid_metadata",
"not_found",
"server_error",
"temporarily_unavailable",
"unsupported_parameter",
]
error_description: str
class ResolveRequest(BaseModel):
sub: HttpUrl
trust_anchor: HttpUrl
# OpenID Federation 8.3.1: entity_type OPTIONAL, may occur multiple times
entity_type: Optional[List[str]] = None
format: Optional[Literal["json", "jose"]] = None
def example(): # pragma: no cover
return ResolveRequest(
sub="http://127.0.0.1:8000/oidc/rp",
trust_anchor="http://127.0.0.1:8000",
format="json",
)
# class ResolveResponse(BaseModel):
# iss : HttpUrl
# sub : HttpUrl
# iat : int
# exp: int
# trust_marks : Json
# metadata: Json
# trust_chain: List
class ResolveHeader(BaseModel):
Authorization : Optional[str]
@@ -0,0 +1,46 @@
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, HttpUrl, root_validator
from .jwks import JwksSpid
class GrantTypeSupported(str, Enum):
refresh_token = "refresh_token" # nosec - B105
authorization_code = "authorization_code"
class RPMetadata(BaseModel):
organization_name: str
redirect_uris: List[HttpUrl]
response_types = ["code"]
grant_types: List[GrantTypeSupported]
client_id: HttpUrl
# TODO: Could be specified in multiple languages
client_name: str
signed_jwks_uri: Optional[HttpUrl]
jwks_uri: Optional[HttpUrl]
jwks: Optional[JwksSpid]
@root_validator(pre=False)
def validate(cls, values):
jwks_there = False
for i in ("jwks_uri", "jwks", "signed_jwks_uri"):
if values.get(i):
jwks_there = True
break
if not jwks_there:
raise ValueError(
"one of signed_jwks_uri or jwks_uri or jwks must be set"
)
return values
class RPMetadataSpid(RPMetadata):
pass
class RPMetadataCie(RPMetadataSpid):
application_type = "web"
tls_client_certificate_bound_access_tokens: Optional[bool]
@@ -0,0 +1,93 @@
from django.conf import settings
import aiohttp
ENCRYPTION_ENC_SUPPORTED = [
"A128CBC-HS256",
"A192CBC-HS384",
"A256CBC-HS512",
"A128GCM",
"A192GCM",
"A256GCM",
]
DEFAULT_HASH_FUNC = "SHA-256"
DEFAULT_JWS_ALG = getattr(settings, "DEFAULT_JWS_ALG", "RS256")
DEFAULT_JWE_ALG = getattr(settings, "DEFAULT_JWE_ALG", "RSA-OAEP")
DEFAULT_JWE_ENC = getattr(settings, "DEFAULT_JWE_ENC", "A256CBC-HS512")
SIGNING_ALG_VALUES_SUPPORTED = getattr(
settings,
"SIGNING_ALG_VALUES_SUPPORTED",
["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"],
)
ENCRYPTION_ALG_VALUES_SUPPORTED = getattr(
settings,
"ENCRYPTION_ALG_VALUES_SUPPORTED",
[
"RSA-OAEP",
"RSA-OAEP-256",
"ECDH-ES",
"ECDH-ES+A128KW",
"ECDH-ES+A192KW",
"ECDH-ES+A256KW",
],
)
# This is required in general project settings
# OIDCFED_TRUST_ANCHORS = [https://..., ]
OIDCFED_ALLOWED_TRUST_MARKS = []
OIDCFED_FILTER_BY_TRUST_MARKS = True
# the metadata discovery will only processes the first MAXIMUM_AUTHORITY_HINTS
OIDCFED_MAX_PATH_LEN = 1
# old, for requests
# HTTPC_PARAMS = {
# "verify": True,
# "timeout": 4
# }
# for aiohttp
HTTPC_TIMEOUT = getattr(settings, "HTTPC_TIMEOUT", 12)
HTTPC_PARAMS = getattr(
settings,
"HTTPC_PARAMS",
{
"connection": {"ssl": True},
"session": {"timeout": aiohttp.ClientTimeout(total=HTTPC_TIMEOUT)},
},
)
# in minutes
MAX_ACCEPTED_TIMEDIFF = 5
OIDCFED_MAXIMUM_AUTHORITY_HINTS = getattr(
settings,
"OIDCFED_MAXIMUM_AUTHORITY_HINTS",
2,
)
FEDERATION_DEFAULT_EXP = getattr(settings, "FEDERATION_DEFAULT_EXP", 2880)
ENTITY_TYPE_LEAFS = getattr(settings, "ENTITY_TYPE_LEAFS", [
"openid_relying_party",
"openid_provider",
"openid_credential_issuer",
"oauth_resource",
"wallet_provider",
"openid_credential_verifier"
])
ENTITY_TYPES = ["federation_entity"] + ENTITY_TYPE_LEAFS
ENTITY_STATUS = {
"unreachable": False,
"valid": True,
"signature_failed": False,
"not_valid": False,
"unknown": None,
"expired": None,
}
@@ -0,0 +1,486 @@
from copy import deepcopy
from .exceptions import (
UnknownKid,
MissingJwksClaim,
MissingTrustMark,
TrustAnchorNeeded,
)
from .http_client import http_get
from .jwtse import verify_jws, unpad_jwt_head, unpad_jwt_payload
import asyncio
import json
import logging
import requests
try:
from django.conf import settings
except ImportError: # pragma: no cover
from . import settings
OIDCFED_FEDERATION_WELLKNOWN_URL = ".well-known/openid-federation"
logger = logging.getLogger(__name__)
def jwks_from_jwks_uri(jwks_uri: str, httpc_params: dict = {}) -> list:
return [json.loads(asyncio.run(http_get([jwks_uri], httpc_params)))] # pragma: no cover
def get_federation_jwks(jwt_payload: dict, httpc_params: dict = {}):
return (
jwt_payload.get("jwks", {}).get("keys", [])
)
def get_http_url(urls: list, httpc_params: dict = {}) -> list:
if getattr(settings, "HTTP_CLIENT_SYNC", False):
responses = []
for i in urls:
res = requests.get(i, **httpc_params) # nosec - B113
responses.append(res.content.decode())
else:
responses = asyncio.run(http_get(urls, httpc_params)) # pragma: no cover
return responses
def get_entity_statements(urls: list, httpc_params: dict = {}) -> list:
"""
Fetches an entity statement/configuration
"""
if isinstance(urls, str):
urls = [urls] # pragma: no cover
for url in urls:
logger.debug(f"Starting Entity Statement Request to {url}")
return get_http_url(urls, httpc_params)
def get_entity_configurations(subjects: list, httpc_params: dict = {}):
if isinstance(subjects, str):
subjects = [subjects]
urls = []
for subject in subjects:
if subject[-1] != "/":
subject = f"{subject}/"
url = f"{subject}{OIDCFED_FEDERATION_WELLKNOWN_URL}"
urls.append(url)
logger.info(f"Starting Entity Configuration Request for {url}")
return get_http_url(urls, httpc_params)
def validate_entity_statement_header(header: dict) -> None:
"""
OpenID Federation 3.2: Entity Statements MUST have typ entity-statement+jwt
and kid (Key ID) in the header. Trust Marks are not validated here (typ can be customized).
"""
if header.get("typ") != "entity-statement+jwt":
raise ValueError(
f"Entity Statement must have typ=entity-statement+jwt, got {header.get('typ')}"
)
kid = header.get("kid")
if not kid or not isinstance(kid, str) or not kid.strip():
raise ValueError("Entity Statement must have non-empty kid header")
def get_trust_mark_type_id(payload_or_obj) -> str:
"""
Return trust mark type identifier from a Trust Mark JWT payload or from
a trust_marks array element (dict with trust_mark_type, trust_mark_id, or id).
Retrocompat: accepts trust_mark_type (draft 48), trust_mark_id, or id.
"""
if hasattr(payload_or_obj, "get"):
return (
payload_or_obj.get("trust_mark_type")
or payload_or_obj.get("trust_mark_id")
or payload_or_obj.get("id")
)
return None
class TrustMark:
def __init__(self, jwt: str, httpc_params: dict = {}):
self.jwt = jwt
self.header = unpad_jwt_head(jwt)
self.payload = unpad_jwt_payload(jwt)
# Retrocompat: id from trust_mark_type (draft 48), trust_mark_id, or id
self.id = get_trust_mark_type_id(self.payload)
if not self.id:
raise KeyError(
"Trust Mark payload must contain one of: "
"trust_mark_type, trust_mark_id, id"
)
self.sub = self.payload["sub"]
self.iss = self.payload["iss"]
self.is_valid = False
self.issuer_entity_configuration = None
self.httpc_params = httpc_params
def validate_by(self, ec) -> bool:
# TODO: pydantic entity configuration validation here
if self.header.get("kid") not in ec.kids:
raise UnknownKid( # pragma: no cover
f"Trust Mark validation failed: "
f"{self.header.get('kid')} not found in {ec.jwks}"
)
# verify signature
payload = verify_jws(self.jwt, ec.jwks[ec.kids.index(self.header["kid"])])
self.is_valid = True
return payload
def validate_by_its_issuer(self) -> bool:
if not self.issuer_entity_configuration:
self.issuer_entity_configuration = get_entity_configurations(
self.iss, self.httpc_params
)
try:
ec = EntityConfiguration(self.issuer_entity_configuration[0])
ec.validate_by_itself()
except UnknownKid:
logger.warning(
f"Trust Mark validation failed by its Issuer: "
f"{self.header.get('kid')} not found in "
f"{self.issuer_entity_configuration.jwks}")
return False
except Exception:
logger.warning(f"Issuer {self.iss} of trust mark {self.id} is not valid.")
self.is_valid = False
return False
# verify signature
payload = verify_jws(
self.jwt,
ec.jwks[
ec.kids.index(self.header["kid"])
],
)
self.is_valid = True
return payload
def __repr__(self) -> str:
return f"{self.id} to {self.sub} issued by {self.iss}"
class EntityConfiguration:
"""
The self issued/signed statement of a federation entity
"""
def __init__(
self,
jwt: str,
httpc_params: dict = {},
filter_by_allowed_trust_marks: list = [],
trust_anchor_entity_conf=None,
trust_mark_issuers_entity_confs: dict = [],
):
self.jwt = jwt
self.header = unpad_jwt_head(jwt)
validate_entity_statement_header(self.header)
self.payload = unpad_jwt_payload(jwt)
self.sub = self.payload["sub"]
self.iss = self.payload["iss"]
self.jwks = get_federation_jwks(self.payload, httpc_params)
if not self.jwks or not self.jwks[0]:
_msg = f"Missing jwks in the statement for {self.sub}"
logger.error(_msg)
raise MissingJwksClaim(_msg)
self.kids = [i.get("kid") for i in self.jwks]
self.httpc_params = httpc_params
self.filter_by_allowed_trust_marks = filter_by_allowed_trust_marks
self.trust_anchor_entity_conf = trust_anchor_entity_conf
self.trust_mark_issuers_entity_confs = trust_mark_issuers_entity_confs
# a dict with sup_sub : superior entity configuration
self.verified_superiors = {}
# as previous but with superiors with invalid entity configurations
self.failed_superiors = {}
# a dict with sup_sub : entity statement issued for self
self.verified_by_superiors = {}
self.failed_by_superiors = {}
# a dict with the paylaod of valid entity statements for each descendant subject
self.verified_descendant_statements = {}
self.failed_descendant_statements = {}
# a dict with the RAW JWT of valid entity statements for each descendant subject
self.verified_descendant_statements_as_jwt = {}
self.verified_trust_marks = []
self.is_valid = False
def validate_by_itself(self) -> bool:
"""
validates the entity configuration by it self
"""
# TODO: pydantic entity configuration validation here
if self.header.get("kid") not in self.kids:
raise UnknownKid(f"{self.header.get('kid')} not found in {self.jwks}") # pragma: no cover
# verify signature
verify_jws(self.jwt, self.jwks[self.kids.index(self.header["kid"])])
self.is_valid = True
return True
def validate_by_allowed_trust_marks(self) -> bool:
"""
validate the entity configuration ony if marked by a well known
trust mark, issued by a trusted issuer
"""
if not self.trust_anchor_entity_conf:
raise TrustAnchorNeeded(
"To validate the trust marks the "
"Trust Anchor Entity Configuration "
"is needed."
)
if not self.filter_by_allowed_trust_marks:
return True
if not self.payload.get("trust_marks"):
logger.warning(
f"{self.sub} doesn't have the trust marks claim "
"in its Entity Configuration"
)
return False
trust_marks = []
is_valid = False
for tm in self.payload["trust_marks"]:
# Retrocompat: accept trust_mark_type, trust_mark_id, or id
tm_type_id = get_trust_mark_type_id(tm)
if tm_type_id not in self.filter_by_allowed_trust_marks:
continue
try:
trust_mark = TrustMark(tm["trust_mark"])
except KeyError:
logger.warning(
f"Trust Mark decoding failed on [{tm}]. "
"Missing 'trust_mark' claim in it"
)
except Exception:
logger.warning(f"Trust Mark decoding failed on [{tm}]")
continue
else:
trust_marks.append(trust_mark)
if not trust_marks:
raise MissingTrustMark("Required Trust marks are missing.") # pragma: no cover
trust_mark_issuers_by_id = self.trust_anchor_entity_conf.payload.get(
"trust_mark_issuers", {}
)
# TODO : cache of issuers -> it would be better to have a proxy function
#
# required_issuer_ecs = []
# for trust_mark in trust_marks:
# if trust_mark.iss not in [
# i.payload.get('iss', None)
# for i in self.trust_mark_issuers_entity_confs
# ]:
# required_issuer_ecs.append(trust_mark.iss)
# TODO: snippet for CACHE
# if required_issuer_ec:
# ## fetch the issuer entity configuration and validate it
# iecs = get_entity_configurations(
# [required_issuer_ecs], self.httpc_params
# )
# for jwt in iecs:
# try:
# ec = self.__class__(jwt, httpc_params=self.httpc_params)
# ec.validate_by_itself()
# except Exception as e:
# logger.warning(
# "Trust Marks issuer Entity Configuration "
# f"failed for {jwt}: {e}"
# )
# continue
# self.trust_mark_issuers_entity_confs.append(ec)
for trust_mark in trust_marks:
id_issuers = trust_mark_issuers_by_id.get(trust_mark.id, None)
if id_issuers and trust_mark.iss not in id_issuers:
is_valid = False
elif id_issuers and trust_mark.iss in id_issuers:
is_valid = trust_mark.validate_by_its_issuer()
elif not id_issuers:
is_valid = trust_mark.validate_by(self.trust_anchor_entity_conf)
if not trust_mark.is_valid:
is_valid = False
if is_valid:
logger.info(f"Trust Mark {trust_mark} is valid")
self.verified_trust_marks.append(trust_mark)
else:
logger.warning(f"Trust Mark {trust_mark} is not valid")
return is_valid
def get_superiors(
self,
authority_hints: list = [],
max_authority_hints: int = 0,
superiors_hints: list = [],
) -> dict:
"""
get superiors entity configurations
"""
# apply limits if defined
authority_hints = authority_hints or deepcopy(self.payload.get("authority_hints", []))
if (
max_authority_hints
and authority_hints != authority_hints[:max_authority_hints]
):
logger.warning(
f"Found {len(authority_hints)} but "
f"authority maximum hints is set to {max_authority_hints}. "
"the following authorities will be ignored: "
f"{', '.join(authority_hints[max_authority_hints:])}"
)
authority_hints = authority_hints[:max_authority_hints]
for sup in superiors_hints:
if sup.sub in authority_hints:
logger.info(
"Getting Cached Entity Configurations for "
f"{[i.sub for i in superiors_hints]}"
)
authority_hints.pop(authority_hints.index(sup.sub))
self.verified_superiors[sup.sub] = sup
logger.debug(f"Getting Entity Configurations for {authority_hints}")
jwts = get_entity_configurations(authority_hints, self.httpc_params)
for jwt in jwts:
try:
ec = self.__class__(jwt, httpc_params=self.httpc_params)
except Exception as e:
logger.warning(f"Get Entity Configuration for {jwt}: {e}")
continue
if ec.validate_by_itself():
target = self.verified_superiors
else:
target = self.failed_superiors
target[ec.payload["sub"]] = ec
for ahints in authority_hints:
if not self.verified_superiors.get(ahints, None):
logger.warning(
f"{ahints} is not available, missing or not valid authority hint"
)
continue
return self.verified_superiors
def validate_descendant_statement(self, jwt: str) -> bool:
"""
jwt is a descendant entity statement issued by self
"""
header = unpad_jwt_head(jwt)
validate_entity_statement_header(header)
payload = unpad_jwt_payload(jwt)
if header.get("kid") not in self.kids:
raise UnknownKid(f"{self.header.get('kid')} not found in {self.jwks}")
# verify signature
payload = verify_jws(jwt, self.jwks[self.kids.index(header["kid"])])
self.verified_descendant_statements[payload["sub"]] = payload
self.verified_descendant_statements_as_jwt[payload["sub"]] = jwt
return self.verified_descendant_statements
def validate_by_superior_statement(self, jwt: str, ec):
"""
jwt is a statement issued by a superior
ec is a superior entity configuration
this method validates self with the jwks contained in statement
of the superior
"""
is_valid = None
payload = {}
try:
payload = unpad_jwt_payload(jwt)
ec.validate_by_itself()
ec.validate_descendant_statement(jwt)
_jwks = get_federation_jwks(payload, self.httpc_params)
_kids = [i.get("kid") for i in _jwks]
verify_jws(self.jwt, _jwks[_kids.index(self.header["kid"])])
is_valid = True
except Exception as e:
logger.warning(
f"{self.sub} failed validation with "
f"{ec.sub}'s superior statement '{payload or jwt}'. "
f"Exception: {e}"
)
is_valid = False
if is_valid:
target = self.verified_by_superiors
ec.verified_descendant_statements[self.sub] = payload
ec.verified_descendant_statements_as_jwt[self.sub] = jwt
target[payload["iss"]] = ec
self.is_valid = True
return self.verified_by_superiors.get(ec.sub)
else:
target = self.failed_superiors
ec.failed_descendant_statements[self.sub] = payload
self.is_valid = False
def validate_by_superiors(
self,
superiors_entity_configurations: dict = {},
): # -> dict[str, EntityConfiguration]:
"""
validates the entity configuration with the entity statements
issued by its superiors
this methods create self.verified_superiors and failed ones
and self.verified_by_superiors and failed ones
"""
for ec in superiors_entity_configurations:
if ec.sub in ec.verified_by_superiors:
# already fetched and cached
continue
try:
# get superior fetch url
fetch_api_url = ec.payload["metadata"]["federation_entity"][
"federation_fetch_endpoint"
]
except KeyError:
logger.warning(
"Missing federation_fetch_endpoint in "
f"federation_entity metadata for {self.sub} by {ec.sub}."
)
self.failed_superiors[ec.sub] = None
continue
else:
_url = f"{fetch_api_url}?sub={self.sub}"
logger.info(f"Getting entity statements from {_url}")
jwts = get_entity_statements([_url], self.httpc_params)
jwt = jwts[0]
if jwt:
self.validate_by_superior_statement(jwt, ec)
else:
logger.error(
f"Empty response for {_url}"
)
return self.verified_by_superiors
def __repr__(self) -> str:
return f"{self.sub} valid {self.is_valid}"
@@ -0,0 +1,8 @@
from django.urls import reverse
def get_admin_change_view_url(obj: object) -> str:
return reverse(
"admin:{}_{}_change".format(obj._meta.app_label, type(obj).__name__.lower()),
args=(obj.pk,),
)
@@ -0,0 +1,9 @@
FA_METADATA = {
"contacts": ["[email protected]"],
"federation_fetch_endpoint": "https://registry.spid.agid.gov.it/oidc/fedapi/",
"federation_list_endpoint": "https://registry.spid.agid.gov.it/oidc/list/",
"federation_trust_mark_status_endpoint": "https://registry.spid.agid.gov.it/oidc/status/",
"federation_resolve_endpoint": "https://registry.spid.agid.gov.it/oidc/resolve/",
"homepage_uri": "https://spid.agid.gov.it/",
"organization_name": "Tal dei tali"
}
@@ -0,0 +1,33 @@
from copy import deepcopy
JWKS = {
"keys": [
{
"kty": "EC",
"kid": "sig-ec256-0",
"use": "sig",
"crv": "P-256",
"x": "2jM2df3IjB9VYQ0yz373-6EEot_1TBuTRaRYafMi5K0",
"y": "h6Zlz6XReK0L-iu4ZgxlozJEXgTGUFuuDl7o8b_8JnM",
},
{
"kty": "EC",
"kid": "enc-ec256-1",
"use": "enc",
"crv": "P-256",
"x": "QI31cvWP4GwnWIi-Z0IYHauQ4nPCk8Vf1BHoPazGqEc",
"y": "DBwf8t9-abpXGtTDlZ8njjxAb33kOMrOqiGsd9oRxr0",
},
]
}
JWKS_WITH_N_AND_EC_NO_CORRECT = deepcopy(JWKS)
JWKS_WITH_N_AND_EC_NO_CORRECT["keys"][0][
"n"
] = "iu4ZgxlozJEXgTGUFuuDl7o8b_8JnMswwddaQjhFkKk"
JWKS_WITH_X_AND_RSA_NO_CORRECT = deepcopy(JWKS)
JWKS_WITH_X_AND_RSA_NO_CORRECT["keys"][0][
"x"
] = "iu4ZgxlozJEXgTGUFuuDl7o8b_8JnMswwddaQjhFkKk"
JWKS_WITH_X_AND_RSA_NO_CORRECT["keys"][0]["kty"] = "RSA"
@@ -0,0 +1,91 @@
from copy import deepcopy
from spid_cie_oidc.entity.tests.jwks_settings import JWKS, JWKS_WITH_N_AND_EC_NO_CORRECT
OP_METADATA = {
"issuer": "https://idserver.servizicie.interno.gov.it/op/",
"authorization_endpoint": "https://idserver.servizicie.interno.gov.it/op/auth",
"token_endpoint": "https://idserver.servizicie.interno.gov.it/op/token",
"userinfo_endpoint": "https://idserver.servizicie.interno.gov.it/op/userinfo",
"introspection_endpoint": "https://idserver.servizicie.interno.gov.it/op/introspect",
"revocation_endpoint": "https://idserver.servizicie.interno.gov.it/op/revoke",
"jwks_uri": "https://idserver.servizicie.interno.gov.it/.well-known/jwks.json",
"subject_types_supported": ["pairwise"],
"token_endpoint_auth_signing_alg_values_supported" : ["ES256"],
"id_token_encryption_alg_values_supported": ["RSA-OAEP"],
"request_object_encryption_enc_values_supported": ["A256CBC-HS512"],
"token_endpoint_auth_methods_supported": ["private_key_jwt"],
"userinfo_encryption_alg_values_supported": ["RSA-OAEP"],
"id_token_encryption_enc_values_supported": ["A256CBC-HS512"],
"id_token_signing_alg_values_supported": ["ES256"],
"userinfo_signing_alg_values_supported": ["ES256"],
"request_object_encryption_alg_values_supported": ["RSA-OAEP"],
"request_object_signing_alg_values_supported": ["ES256"],
"userinfo_encryption_enc_values_supported": ["A256CBC-HS512"],
"request_parameter_supported": True,
"subject_types_supported": ["pairwise"],
"token_endpoint_auth_methods_supported": ["private_key_jwt"],
"op_name": "Agenzia per l’Italia Digitale",
"op_uri": "https://www.agid.gov.it",
"acr_values_supported":[
"https://www.spid.gov.it/SpidL1",
"https://www.spid.gov.it/SpidL2",
"https://www.spid.gov.it/SpidL3"
],
"organization_name": "Tal dei tali"
}
OP_METADATA_CIE = deepcopy(OP_METADATA)
OP_METADATA_CIE["scopes_supported"] = ["openid", "offline_access", "email"]
OP_METADATA_CIE["response_types_supported"] = ["code"]
OP_METADATA_CIE["response_modes_supported"] = ["query", "form_post"]
OP_METADATA_CIE["grant_types_supported"] = ["authorization_code", "refresh_token"]
OP_METADATA_CIE["claims_supported"] = [
"given_name",
"family_name",
"email",
"email_verified",
"gender",
"birthdate",
"phone_number",
"phone_number_verified",
"place_of_birth",
"address",
"document_details",
"https://attributes.eid.gov.it/e_delivery_service",
"https://attributes.eid.gov.it/fiscal_number",
"https://attributes.eid.gov.it/idANPR",
"https://attributes.eid.gov.it/physical_phone_number",
]
OP_METADATA_CIE["claims_parameter_supported"] = True
OP_METADATA_CIE["tls_client_certificate_bound_access_tokens"] = True
OP_METADATA_CIE["authorization_response_iss_parameter_supported"] = True
OP_METADATA_CIE["userinfo_signing_alg_values_supported"] = ["ES256"]
OP_METADATA_CIE_JWKS_AND_JWKS_URI = deepcopy(OP_METADATA_CIE)
OP_METADATA_CIE_JWKS_AND_JWKS_URI["jwks"] = JWKS
OP_METADATA_CIE_JWKS_URI_NO_CORRECT = deepcopy(OP_METADATA_CIE)
OP_METADATA_CIE_JWKS_URI_NO_CORRECT[
"jwks_uri"
] = "https://registry.cie.gov.it/.well-known/"
OP_METADATA_SPID = deepcopy(OP_METADATA)
OP_METADATA_SPID["op_name"] = "Agenzia per l’Italia Digitale"
OP_METADATA_SPID["op_uri"] = "https://www.agid.gov.it"
OP_METADATA_SPID["userinfo_signing_alg_values_supported"] = ["RS384"]
OP_METADATA_SPID["acr_values_supported"] = [
"https://www.spid.gov.it/SpidL1",
"https://www.spid.gov.it/SpidL2",
"https://www.spid.gov.it/SpidL3",
]
OP_METADATA_SPID_JWKS_AND_JWKS_URI = deepcopy(OP_METADATA_SPID)
OP_METADATA_SPID_JWKS_AND_JWKS_URI["jwks"] = JWKS
OP_METADATA_SPID_JWKS_NO_JWKS_URI = deepcopy(OP_METADATA_SPID_JWKS_AND_JWKS_URI)
OP_METADATA_SPID_JWKS_NO_JWKS_URI.pop("jwks_uri")
OP_METADATA_SPID_JWKS_EC_NO_CORRECT = deepcopy(OP_METADATA_SPID_JWKS_NO_JWKS_URI)
OP_METADATA_SPID_JWKS_EC_NO_CORRECT["jwks"] = JWKS_WITH_N_AND_EC_NO_CORRECT
@@ -0,0 +1,97 @@
from copy import deepcopy
from spid_cie_oidc.entity.tests.jwks_settings import JWKS
RP_METADATA = {
"redirect_uris": [
"https://rp.example.it/spid/callback1/",
"https://rp.example.it/spid/callback2/",
],
"jwks_uri": "https://registry.cie.gov.it/keys.json",
"response_types": ["code"],
"grant_types": ["authorization_code", "refresh_token"],
"client_id": "https://rp.example.it/spid",
"client_name": "Nome RP",
"contacts": ["[email protected]"],
"subject_type": "pairwise",
"logo_uri": [
"https://rp.example.it/logo_small.jpg",
"https://rp.example.it/logo_big.jpg"
],
"scope" : ["openid", "offline_access", "profile"],
"organization_name": "Tal dei tali"
}
RP_METADATA_CIE = deepcopy(RP_METADATA)
RP_METADATA_CIE["tls_client_certificate_bound_access_tokens"] = True
RP_METADATA_CIE["application_type"] = "web"
RP_METADATA_SPID = deepcopy(RP_METADATA)
RP_METADATA_SPID_NO_REDIRECT_URIS = deepcopy(RP_METADATA_SPID)
RP_METADATA_SPID_NO_REDIRECT_URIS.pop("redirect_uris")
RP_METADATA_SPID_JWKS_AND_JWKS_URI = deepcopy(RP_METADATA_SPID)
RP_METADATA_SPID_JWKS_AND_JWKS_URI["jwks"] = JWKS
RP_METADATA_CIE_JWKS_AND_JWKS_URI = deepcopy(RP_METADATA_CIE)
RP_METADATA_CIE_JWKS_AND_JWKS_URI["jwks"] = JWKS
RP_METADATA_CIE_NO_REDIRECT_URIS = deepcopy(RP_METADATA_CIE)
RP_METADATA_CIE_NO_REDIRECT_URIS.pop("redirect_uris")
RP_METADATA_SPID_NOJWKS_NOJWKS_URI = deepcopy(RP_METADATA_SPID)
RP_METADATA_SPID_NOJWKS_NOJWKS_URI.pop("jwks_uri")
RP_METADATA_CIE_NOJWKS_NOJWKS_URI = deepcopy(RP_METADATA_CIE)
RP_METADATA_CIE_NOJWKS_NOJWKS_URI.pop("jwks_uri")
RP_METADATA_JWK1 = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
RP_METADATA_JWK1_pub = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
RP_METADATA_JWK2 = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
RP_METADATA_JWK2_pub = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
rp_onboarding_data = dict(
name="RP Test",
sub="http://rp-test.it/oidc/rp/",
type="openid_relying_party",
metadata_policy={"openid_relying_party": {"scope": {"value": ["openid"]}}},
is_active=True,
jwks = [RP_METADATA_JWK1_pub]
)
rp_conf = {
"sub": rp_onboarding_data["sub"],
"jwks_fed" : [RP_METADATA_JWK1],
"jwks_core" : [RP_METADATA_JWK1, RP_METADATA_JWK2],
"metadata": {
"openid_relying_party": {
"application_type": "web",
"client_registration_types": ["automatic"],
"client_name": "Name of this service called http://rp-test.it/oidc/rp/",
"contacts": ["[email protected]"],
"grant_types": ["refresh_token", "authorization_code"],
"redirect_uris": ["http://rp-test.it/oidc/rp/callback/"],
"response_types": ["code"],
"subject_type": "pairwise",
"client_id": "http://rp-test.it/oidc/rp/",
"jwks": {"keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub]},
}
},
"authority_hints": ["http://testserver/"],
"is_active": True,
}
RP_CONF_AS_JSON = {
"iss": rp_conf["sub"],
"sub": rp_conf["sub"],
"jwks": {
"keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub]
},
"metadata": rp_conf["metadata"],
"authority_hints":rp_conf["authority_hints"]
}
@@ -0,0 +1,44 @@
TA_SUB = "http://testserver/"
FA_METADATA = {
"federation_entity": {
"contacts": ["ops@localhost"],
"federation_fetch_endpoint": f"{TA_SUB}/fetch",
"federation_list_endpoint": f"{TA_SUB}/list/",
"homepage_uri": f"{TA_SUB}",
"name": "example TA",
}
}
TM_ISSUERS = {
"https://www.spid.gov.it/certification/rp/public": [
TA_SUB,
"https://public.intermediary.spid.it",
],
"https://www.spid.gov.it/certification/rp/private": [
TA_SUB,
"https://private.other.intermediary.it",
],
"https://sgd.aa.it/onboarding": ["https://sgd.aa.it"],
}
FA_CONSTRAINTS = {"max_path_length": 1}
ta_conf_data = dict(
sub=TA_SUB,
metadata=FA_METADATA,
constraints=FA_CONSTRAINTS,
is_active=1,
trust_mark_issuers=TM_ISSUERS,
)
TA_JWK_PRIVATE = {'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
TA_JWK_PUBLIC = {'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
ta_conf_data_as_json = dict(
sub=TA_SUB,
iss=TA_SUB,
metadata=FA_METADATA,
jwks ={
"keys": [TA_JWK_PUBLIC]
},
constraints=FA_CONSTRAINTS,
is_active=1,
trust_mark_issuers=TM_ISSUERS,
)
@@ -0,0 +1,55 @@
from django.contrib.auth import get_user_model
from django.test import TestCase, Client
from django.urls import reverse
from spid_cie_oidc.entity.models import *
from spid_cie_oidc.entity.jwtse import verify_jws
from . import get_admin_change_view_url
from .settings import *
class EntityConfigurationTest(TestCase):
def setUp(self):
self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data)
self.admin_user = get_user_model().objects.create_superuser(
username="superuser", password="secret", email="[email protected]"
)
def test_entity_configuration_admin(self):
c = Client()
c.login(username="superuser", password="secret")
# run test
response = c.get(get_admin_change_view_url(self.ta_conf))
self.assertEqual(response.status_code, 200)
def test_entity_configuration(self):
self.assertTrue(
isinstance(
FederationEntityConfiguration.get_active_conf(),
FederationEntityConfiguration,
)
)
self.assertTrue(isinstance(self.ta_conf, FederationEntityConfiguration))
self.assertFalse(self.ta_conf.is_leaf)
for i in (
"public_jwks",
"pems_as_json",
"kids",
"type",
"entity_configuration_as_jws",
):
attr = getattr(self.ta_conf, i)
self.assertTrue(attr)
md = self.ta_conf.entity_configuration_as_dict["metadata"]["federation_entity"]
for i in ("federation_fetch_endpoint",):
self.assertTrue(md.get(i))
# dulcis in fundo -> test the .well-knwon/openid-federation
wk_url = reverse("entity_configuration")
c = Client()
res = c.get(wk_url)
verify_jws(res.content.decode(), self.ta_conf.jwks_fed[0])
@@ -0,0 +1,37 @@
import logging
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.entity.schemas.op_metadata import OPMetadataCie, OPMetadataSpid
from spid_cie_oidc.entity.tests.op_metadata_settings import (
OP_METADATA_CIE,
OP_METADATA_CIE_JWKS_AND_JWKS_URI,
OP_METADATA_SPID,
OP_METADATA_SPID_JWKS_AND_JWKS_URI,
OP_METADATA_SPID_JWKS_EC_NO_CORRECT,
OP_METADATA_SPID_JWKS_NO_JWKS_URI,
)
logger = logging.getLogger(__name__)
class OpMetadataTest(TestCase):
def test_op_metatada_cie(self):
OPMetadataCie(**OP_METADATA_CIE)
def test_op_metatada_cie_jwks_and_jwks_uri(self):
self.assertTrue(OPMetadataCie(**OP_METADATA_CIE_JWKS_AND_JWKS_URI))
def test_op_metatada_spid(self):
OPMetadataSpid(**OP_METADATA_SPID)
def test_op_metatada_spid_jwks_and_jwks_uri(self):
self.assertTrue(OPMetadataSpid(**OP_METADATA_SPID_JWKS_AND_JWKS_URI))
def test_op_metatada_spid_jwks_no_jwks_uri(self):
OPMetadataSpid(**OP_METADATA_SPID_JWKS_NO_JWKS_URI)
def test_op_metatada_spid_jws_ec_no_correct(self):
with self.assertRaises(ValidationError):
OPMetadataSpid(**OP_METADATA_SPID_JWKS_EC_NO_CORRECT)
@@ -0,0 +1,43 @@
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.entity.schemas.rp_metadata import RPMetadataCie, RPMetadataSpid
from spid_cie_oidc.entity.tests.rp_metadata_settings import (
RP_METADATA_CIE,
RP_METADATA_SPID,
RP_METADATA_SPID_JWKS_AND_JWKS_URI,
RP_METADATA_SPID_NO_REDIRECT_URIS,
RP_METADATA_CIE_JWKS_AND_JWKS_URI,
RP_METADATA_CIE_NO_REDIRECT_URIS,
RP_METADATA_SPID_NOJWKS_NOJWKS_URI,
RP_METADATA_CIE_NOJWKS_NOJWKS_URI
)
class RPMetadataTest(TestCase):
def test_rp_metadataCie(self):
RPMetadataCie(**RP_METADATA_CIE)
def test_rp_metadataSpid(self):
RPMetadataSpid(**RP_METADATA_SPID)
def test_rp_metadataSpid_no_redirect_uris(self):
with self.assertRaises(ValidationError):
RPMetadataSpid(**RP_METADATA_SPID_NO_REDIRECT_URIS)
def test_rp_metadataSpid_jwks_and_jwks_uri(self):
self.assertTrue(RPMetadataSpid(**RP_METADATA_SPID_JWKS_AND_JWKS_URI))
def test_rp_metadataSpid_no_jwks_and_no_jwks_uri(self):
with self.assertRaises(ValidationError):
RPMetadataSpid(**RP_METADATA_SPID_NOJWKS_NOJWKS_URI)
def test_rp_metadataCie_jwks_and_jwks_uri(self):
self.assertTrue(RPMetadataCie(**RP_METADATA_CIE_JWKS_AND_JWKS_URI))
def test_rp_metadataCie_no_redirect_uris(self):
with self.assertRaises(ValidationError):
RPMetadataCie(**RP_METADATA_CIE_NO_REDIRECT_URIS)
def test_rp_metadataCie_no_jwks_and_no_jwks_uri(self):
with self.assertRaises(ValidationError):
RPMetadataCie(**RP_METADATA_CIE_NOJWKS_NOJWKS_URI)
@@ -0,0 +1,24 @@
from django.test import TestCase
from pydantic import ValidationError
from spid_cie_oidc.entity.schemas.jwks import JwksCie, JwksSpid
from spid_cie_oidc.entity.tests.jwks_settings import (
JWKS,
JWKS_WITH_N_AND_EC_NO_CORRECT,
JWKS_WITH_X_AND_RSA_NO_CORRECT,
)
class JwksTest(TestCase):
def test_jwks_spid(self):
JwksSpid(**JWKS)
def test_jwks_cie(self):
JwksCie(**JWKS)
def test_jwks_with_n_and_ec_no_correct(self):
with self.assertRaises(ValidationError):
JwksSpid(**JWKS_WITH_N_AND_EC_NO_CORRECT)
def test_jwks_with_e_and_rsa_no_correct(self):
with self.assertRaises(ValidationError):
JwksCie(**JWKS_WITH_X_AND_RSA_NO_CORRECT)
@@ -0,0 +1,8 @@
from django.test import TestCase
from spid_cie_oidc.entity.schemas.fa_metadata import FAMetadata
from spid_cie_oidc.entity.tests.fa_metadata_settings import FA_METADATA
class FAMetadatTest(TestCase):
def test_fa_metadata(self):
FAMetadata(**FA_METADATA)
@@ -0,0 +1,91 @@
from copy import deepcopy
from django.test import TestCase
from spid_cie_oidc.entity.policy import (
apply_policy,
diff2policy,
gather_policies,
PolicyError
)
from spid_cie_oidc.entity.tests.rp_metadata_settings import RP_METADATA
from spid_cie_oidc.authority.tests.settings import rp_onboarding_data
class PolicyTest(TestCase):
def setUp(self):
pass
def test_gather_polices(self):
combined_policy = gather_policies([{}], "openid_relying_party")
self.assertTrue(combined_policy == {})
def test_gather_polices_rp(self):
combined_policy = gather_policies([{},rp_onboarding_data], "openid_relying_party")
self.assertTrue(combined_policy == {'scope': {'value': ['openid']}})
def test_apply_policy(self):
fa_policy = {}
fa_policy["contacts"] = {"add": "[email protected]"}
combined_policy = apply_policy(deepcopy(RP_METADATA), fa_policy)
combined_contacts = combined_policy["contacts"]
self.assertTrue("[email protected]" in combined_contacts)
self.assertTrue("[email protected]" in combined_contacts)
def test_apply_policy_one_of(self):
fa_policy = {}
fa_policy["logo_uri"] = {"one_of": ["logo1", "logo2"]}
with self.assertRaises(PolicyError):
apply_policy(deepcopy(RP_METADATA), fa_policy)
def test_apply_policy_subset_of(self):
fa_policy = {}
fa_policy["scope"] = {"subset_of": ["openid", "offline_access"]}
combined_contacts = apply_policy(deepcopy(RP_METADATA), fa_policy)
self.assertTrue('profile' not in combined_contacts["scope"])
def test_apply_policy_superset_of(self):
fa_policy = {}
fa_policy["scope"] = {"superset_of": ["openid"]}
RP_METADATA_LOCAL = deepcopy(RP_METADATA)
RP_METADATA_LOCAL["scope"] = ["offline_access"]
with self.assertRaises(PolicyError):
apply_policy(deepcopy(RP_METADATA_LOCAL), fa_policy)
def test_apply_policy_difference_value(self):
fa_policy = {}
fa_policy["client_id"] = {"value": "https://rp.example.it/spid"}
RP_METADATA_LOCAL = deepcopy(RP_METADATA)
RP_METADATA_LOCAL.pop("client_id")
combined_contacts = apply_policy(deepcopy(RP_METADATA_LOCAL), fa_policy)
self.assertTrue(combined_contacts["client_id"])
def test_apply_policy_difference_add(self):
fa_policy = {}
fa_policy["contacts"] = {"add": "[email protected]"}
RP_METADATA_LOCAL = deepcopy(RP_METADATA)
RP_METADATA_LOCAL.pop("contacts")
combined_contacts = apply_policy(deepcopy(RP_METADATA_LOCAL), fa_policy)
self.assertTrue(combined_contacts["contacts"])
self.assertTrue("[email protected]" in combined_contacts["contacts"])
def test_apply_policy_add(self):
fa_policy = {}
fa_policy["contacts"] = {"add": "[email protected]"}
combined_contacts = apply_policy(deepcopy(RP_METADATA), fa_policy)
self.assertTrue("[email protected]" in combined_contacts["contacts"])
self.assertTrue("[email protected]" in combined_contacts["contacts"])
def test_diff_two_policy(self):
fa_policy_old = {}
fa_policy_new = deepcopy(fa_policy_old)
fa_policy_new["contacts"] = {"add": "[email protected]"}
result = diff2policy(fa_policy_new, fa_policy_old)
diff = {
"contacts": {
"add": {
"add": "[email protected]"
}
}
}
self.assertTrue(result == diff)
@@ -0,0 +1,12 @@
import asyncio
from django.test import TestCase
from spid_cie_oidc.entity.http_client import http_get
class HttpClient(TestCase):
def test_http_client(self):
url = "https://icanhazip.com/"
get = http_get([url], {})
value = asyncio.run(get)
self.assertFalse(len(value) == 0)
@@ -0,0 +1,96 @@
import json
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import (
Client,
RequestFactory,
TestCase,
override_settings
)
from django.urls import reverse
from spid_cie_oidc.entity.tests.rp_metadata_settings import rp_conf
from spid_cie_oidc.entity.views import resolve_entity_statement
from spid_cie_oidc.entity.models import (
FederationEntityConfiguration,
FetchedEntityStatement,
TrustChain,
StaffToken
)
from spid_cie_oidc.entity.tests.settings import ta_conf_data
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
def create_tc():
TA_FES = FetchedEntityStatement.objects.create(
sub="http://testserver/",
iss="http://testserver/",
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
return TrustChain.objects.create(
sub="http://rp-test.it/oidc/rp/",
exp=datetime_from_timestamp(exp_from_now(33)),
jwks = [],
metadata=[],
status="valid",
trust_anchor=TA_FES,
is_active=True,
)
class ResolveEntityStatementTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = get_user_model().objects.create(
username="test",
first_name="test",
last_name="test",
email="[email protected]",
is_staff = True
)
self.user.set_password("test")
self.user.save()
StaffToken.objects.create(
user = self.user,
token = "secret-token",
is_active = True
)
self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data)
self.sub_conf = FederationEntityConfiguration.objects.create(**rp_conf)
self.ta_fes = FetchedEntityStatement.objects.create(
sub=ta_conf_data["sub"],
iss=ta_conf_data["sub"],
exp=datetime_from_timestamp(exp_from_now(33)),
iat=datetime_from_timestamp(iat_now()),
)
@override_settings(HTTP_CLIENT_SYNC=True)
def test_resolve_entity_statement(self):
client = Client()
url = reverse("oidcfed_resolve")
client.login(username="test", password="test")
data = {"sub" : rp_conf["sub"], "trust_anchor" : ta_conf_data["sub"]}
request = self.factory.get(url, data, **{'HTTP_AUTHORIZATION': "secret-token"})
self.patcher = patch(
"spid_cie_oidc.entity.trust_chain_operations.get_or_create_trust_chain",
return_value = create_tc()
)
self.patcher.start()
res = resolve_entity_statement(request, format='json')
self.patcher.stop()
_json = json.loads(res.content.decode())
self.assertTrue(_json.get("iss") == ta_conf_data["sub"])
self.assertTrue(_json.get("sub") == rp_conf["sub"])
self.assertTrue("trust_chain" in _json)
self.assertTrue("metadata" in _json)
@@ -0,0 +1,81 @@
from copy import deepcopy
from django.core.exceptions import ValidationError
from django.test import TestCase
from spid_cie_oidc.authority.tests.settings import (
RP_METADATA_JWK1,
RP_METADATA_JWK1_pub
)
from spid_cie_oidc.entity.tests.op_metadata_settings import OP_METADATA
from spid_cie_oidc.entity.tests.rp_metadata_settings import RP_METADATA_SPID
from spid_cie_oidc.entity.validators import (
validate_entity_metadata,
validate_metadata_algs,
validate_private_jwks,
validate_public_jwks
)
class ValidatrTest(TestCase):
def setUp(self):
pass
def test_validate_public_jwks_with_private_key(self):
with self.assertRaises(Exception):
validate_public_jwks(RP_METADATA_JWK1)
def test_validate_private_jwks_with_public_key(self):
with self.assertRaises(Exception):
validate_private_jwks(RP_METADATA_JWK1_pub)
def test_validate_public_jwks(self):
validate_public_jwks(RP_METADATA_JWK1_pub)
def test_validate_private_jwks(self):
validate_private_jwks(RP_METADATA_JWK1)
def test_validate_metadata_algs_ok(self):
op_md = {}
op_md["openid_provider"] = OP_METADATA
validate_metadata_algs(op_md)
def test_validate_metadata_algs_with_error(self):
OP_METADATA_NO_CORRECT = deepcopy(OP_METADATA)
OP_METADATA_NO_CORRECT["token_endpoint_auth_signing_alg_values_supported"] = ["ciao"]
op_md = {}
op_md["openid_provider"] = OP_METADATA_NO_CORRECT
with self.assertRaises(ValidationError):
validate_metadata_algs(op_md)
def test_validate_entity_op_metadata(self):
op_md = {}
op_md["openid_provider"] = deepcopy(OP_METADATA)
validate_entity_metadata(op_md)
def test_validate_entity_no_valid_op_metadata(self):
op_md = {}
op_md["openid_provider"] = deepcopy(OP_METADATA)
op_md["openid_provider"].pop("issuer")
with self.assertRaises(ValidationError):
validate_entity_metadata(op_md)
def test_validate_entity_rp_metadata(self):
rp_md = {}
rp_md["openid_relying_party"] = deepcopy(RP_METADATA_SPID)
validate_entity_metadata(rp_md)
def test_validate_entity_no_valid_rp_metadata(self):
rp_md = {}
rp_md["openid_relying_party"] = deepcopy(RP_METADATA_SPID)
rp_md["openid_relying_party"].pop("grant_types")
with self.assertRaises(ValidationError):
validate_entity_metadata(rp_md)
def test_validate_entity_no_entity_type(self):
rp_md = {}
rp_md = deepcopy(RP_METADATA_SPID)
with self.assertRaises(ValidationError):
validate_entity_metadata(rp_md)
@@ -0,0 +1,40 @@
from unittest.mock import patch
from django.test import TestCase, override_settings
from spid_cie_oidc.authority.models import FederationDescendant, FederationEntityAssignedProfile, FederationEntityProfile
from spid_cie_oidc.authority.tests.mocked_responses import EntityResponseNoIntermediate
from spid_cie_oidc.entity.models import FederationEntityConfiguration
from spid_cie_oidc.entity.tests.settings import ta_conf_data
from spid_cie_oidc.authority.tests.settings import (
rp_conf,
RP_PROFILE,
rp_onboarding_data
)
from spid_cie_oidc.entity.statements import TrustMark
class StatementTest(TestCase):
def setUp(self):
self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data)
self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf)
self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE)
self.rp = FederationDescendant.objects.create(**rp_onboarding_data)
self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create(
descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf
)
self.trust_mark = self.rp_assigned_profile.trust_mark_as_jws
@override_settings(HTTP_CLIENT_SYNC=True)
@patch("requests.get", return_value=EntityResponseNoIntermediate())
def test_validate_by_its_issuer(self, mocked):
self.tm = TrustMark(self.trust_mark)
TrustMark.validate_by_its_issuer(self.tm)
self.assertTrue(self.tm.is_valid)
@@ -0,0 +1,59 @@
from unittest.mock import patch
from django.test import TestCase
from spid_cie_oidc.authority.tests.settings import (
RP_CONF_AS_JSON,
RP_METADATA_JWK1,
rp_conf
)
from spid_cie_oidc.entity.exceptions import InvalidEntityConfiguration
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.tests.settings import (
TA_JWK_PRIVATE,
ta_conf_data,
ta_conf_data_as_json
)
from spid_cie_oidc.entity.trust_chain import TrustChainBuilder
from spid_cie_oidc.entity.utils import (
datetime_from_timestamp,
exp_from_now,
iat_now
)
EXP = datetime_from_timestamp(exp_from_now(33))
NOW = datetime_from_timestamp(iat_now())
def create_entity_config():
JWS_RP = create_jws(RP_CONF_AS_JSON, RP_METADATA_JWK1, typ="entity-statement+jwt")
JWS_TA = create_jws(ta_conf_data_as_json, TA_JWK_PRIVATE, typ="entity-statement+jwt")
return [JWS_RP, JWS_TA]
class TrustChainTest(TestCase):
def setUp(self):
self.tcb = TrustChainBuilder(
subject = rp_conf["sub"],
trust_anchor = ta_conf_data["sub"]
)
def test_get_trust_anchor_configuration(self):
self.patcher = patch(
"spid_cie_oidc.entity.trust_chain.get_entity_configurations",
return_value = create_entity_config()
)
self.patcher.start()
TrustChainBuilder.get_trust_anchor_configuration(self.tcb)
self.patcher.stop()
def test_get_subject_configuration_failed(self):
self.patcher = patch(
"spid_cie_oidc.entity.trust_chain.get_entity_configurations",
return_value = []
)
self.patcher.start()
with self.assertRaises(InvalidEntityConfiguration):
TrustChainBuilder.get_subject_configuration(self.tcb)
self.patcher.stop()
@@ -0,0 +1,21 @@
from django.test import TestCase
from spid_cie_oidc.entity.schemas.resolve_endpoint import ResolveRequest
from .rp_metadata_settings import rp_conf
TA_SUB = "http://testserver.it/"
RESOLVE_REQUEST = {
"sub": rp_conf["sub"],
"trust_anchor": TA_SUB,
"format": "json",
}
class SchemaTest(TestCase):
def setUp(self) -> None:
return super().setUp()
def test_resolve_request(self):
ResolveRequest(**RESOLVE_REQUEST)
@@ -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
@@ -0,0 +1,203 @@
import logging
from django.utils import timezone
from typing import Union
from .exceptions import InvalidTrustchain, TrustchainMissingMetadata
from .models import FetchedEntityStatement, TrustChain
from .statements import EntityConfiguration, get_entity_configurations
from .settings import HTTPC_PARAMS
from .trust_chain import TrustChainBuilder
from .utils import datetime_from_timestamp
logger = logging.getLogger(__name__)
def trust_chain_builder(
subject: str,
trust_anchor: EntityConfiguration,
httpc_params: dict = HTTPC_PARAMS,
required_trust_marks: list = []
) -> Union[TrustChainBuilder, bool]:
"""
Trust Chain builder
"""
tc = TrustChainBuilder(
subject,
trust_anchor=trust_anchor,
required_trust_marks=required_trust_marks,
httpc_params=httpc_params
)
tc.start()
if not tc.is_valid:
logger.error(
"The tree of trust cannot be validated for "
f"{tc.subject}: {tc.tree_of_trust}"
)
return False
else:
return tc
def dumps_statements_from_trust_chain_to_db(trust_chain: TrustChainBuilder) -> list:
entity_statements = []
for stat in trust_chain.trust_path:
data = dict(
exp=datetime_from_timestamp(stat.payload["exp"]),
iat=datetime_from_timestamp(stat.payload["iat"]),
statement=stat.payload,
jwt=stat.jwt,
)
fes = FetchedEntityStatement.objects.filter(sub=stat.sub, iss=stat.iss)
if fes:
fes.update(**data)
else:
fes = FetchedEntityStatement.objects.create(
sub=stat.sub, iss=stat.iss, **data
)
entity_statements.append(fes)
if stat.verified_descendant_statements:
for desc_stat_sub in stat.verified_descendant_statements:
payload = stat.verified_descendant_statements[desc_stat_sub]
jwt = stat.verified_descendant_statements_as_jwt[desc_stat_sub]
_data = dict(
exp=datetime_from_timestamp(payload["exp"]),
iat=datetime_from_timestamp(payload["iat"]),
statement=payload,
jwt=jwt,
)
desc_fes = FetchedEntityStatement.objects.filter(
sub=payload["sub"], iss=payload["iss"]
)
if desc_fes:
desc_fes.update(**_data)
else:
desc_fes = FetchedEntityStatement.objects.create(
sub=payload["sub"], iss=payload["iss"], **_data
)
entity_statements.append(desc_fes)
return entity_statements
def get_or_create_trust_chain(
subject: str,
trust_anchor: str,
httpc_params: dict = HTTPC_PARAMS,
required_trust_marks: list = [],
force: bool = False,
) -> Union[TrustChain, None]:
"""
returns a TrustChain model object if any available
if available it return it
if not available it create a new one
if available and expired it return the expired one
if flag force is set to True -> renew the trust chain, update it and
return the updated one
"""
fetched_trust_anchor = FetchedEntityStatement.objects.filter(
sub=trust_anchor, iss=trust_anchor
)
if not fetched_trust_anchor or fetched_trust_anchor.first().is_expired or force:
jwts = get_entity_configurations([trust_anchor], httpc_params=httpc_params)
ta_conf = EntityConfiguration(jwts[0], httpc_params=httpc_params)
data = dict(
exp=datetime_from_timestamp(ta_conf.payload["exp"]),
iat=datetime_from_timestamp(ta_conf.payload["iat"]),
statement=ta_conf.payload,
jwt=ta_conf.jwt,
)
if not fetched_trust_anchor:
# trust to the anchor should be absolute trusted!
# ta_conf.validate_by_itself()
fetched_trust_anchor = FetchedEntityStatement.objects.create(
sub=ta_conf.sub, iss=ta_conf.iss, **data
)
else:
fetched_trust_anchor.update(
exp=datetime_from_timestamp(ta_conf.payload["exp"]),
iat=datetime_from_timestamp(ta_conf.payload["iat"]),
statement=ta_conf.payload,
jwt=ta_conf.jwt,
)
fetched_trust_anchor = fetched_trust_anchor.first()
else:
fetched_trust_anchor = fetched_trust_anchor.first()
ta_conf = fetched_trust_anchor.get_entity_configuration_as_obj()
tc = TrustChain.objects.filter(sub=subject, trust_anchor__sub=trust_anchor).first()
if tc and not tc.is_active:
# if manualy disabled by staff
return None
elif force or not tc or tc.is_expired:
trust_chain = trust_chain_builder(
subject=subject,
trust_anchor=ta_conf,
required_trust_marks=required_trust_marks
)
if not trust_chain:
raise InvalidTrustchain(
f"Trust chain for subject {subject} and "
f"trust_anchor {trust_anchor} is not found"
)
elif not trust_chain.is_valid:
raise InvalidTrustchain(
f"Trust chain for subject {subject} and "
f"trust_anchor {trust_anchor} is not valid"
)
elif not trust_chain.final_metadata:
raise TrustchainMissingMetadata(
f"Trust chain for subject {subject} and "
f"trust_anchor {trust_anchor} doesn't have any metadata"
)
dumps_statements_from_trust_chain_to_db(trust_chain)
tc = TrustChain.objects.filter(
sub=subject, trust_anchor__sub=trust_anchor
)
data = dict(
exp=trust_chain.exp_datetime,
processing_start = timezone.localtime(),
chain=trust_chain.serialize(),
jwks = trust_chain.subject_configuration.jwks,
metadata=trust_chain.final_metadata,
parties_involved=[i.sub for i in trust_chain.trust_path],
status="valid",
trust_marks=[
{"trust_mark_type": i.id, "trust_mark": i.jwt, "trust_mark_id": i.id}
for i in trust_chain.verified_trust_marks
],
is_active=True,
)
if tc:
tc.update(**data)
tc = tc.first()
else:
tc = TrustChain.objects.create(
sub=subject,
trust_anchor=fetched_trust_anchor,
**data,
)
return tc
@@ -0,0 +1,39 @@
"""acsia_agent_proj URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.urls import path
from .views import (
entity_configuration,
historical_keys,
resolve_entity_statement
)
_PREF = getattr(settings, "OIDC_PREFIX", "")
urlpatterns = [
path(
f"{_PREF}.well-known/openid-federation",
entity_configuration,
name="entity_configuration",
),
path(f"{_PREF}resolve", resolve_entity_statement, name="oidcfed_resolve"),
path(
f"{_PREF}.well-known/openid-federation-historical-jwks",
historical_keys,
name="oidcfed_historical_keys"
)
]
@@ -0,0 +1,79 @@
from django.utils import timezone
from django.utils.timezone import make_aware
from secrets import token_hex
from spid_cie_oidc.entity.jwtse import unpad_jwt_head
from spid_cie_oidc.entity.settings import HTTPC_PARAMS
from spid_cie_oidc.entity.statements import get_http_url
from . import KeyUsage
import datetime
import json
import logging
logger = logging.getLogger(__name__)
def get_key(jwks, use=KeyUsage.signature):
for jwk in jwks:
if jwk['use'] == use:
return jwk
return jwks[0]
def iat_now() -> int:
return int(datetime.datetime.now().timestamp())
def exp_from_now(minutes: int = 33) -> int:
_now = timezone.localtime()
return int((_now + datetime.timedelta(minutes=minutes)).timestamp())
def datetime_from_timestamp(value) -> datetime.datetime:
return make_aware(datetime.datetime.fromtimestamp(value))
def get_jwks(metadata: dict, federation_jwks: list = []) -> dict:
"""
get jwks or jwks_uri or signed_jwks_uri
"""
jwks_list = []
if metadata.get('jwks'):
jwks_list = metadata["jwks"]["keys"]
elif metadata.get('jwks_uri'):
try:
jwks_uri = metadata["jwks_uri"]
jwks_list = get_http_url(
[jwks_uri], httpc_params=HTTPC_PARAMS
)
jwks_list = json.loads(jwks_list[0])
except Exception as e:
logger.error(f"Failed to download jwks from {jwks_uri}: {e}")
elif metadata.get('signed_jwks_uri'):
try:
signed_jwks_uri = metadata["signed_jwks_uri"]
jwks_list = get_http_url(
[signed_jwks_uri], httpc_params=HTTPC_PARAMS
)[0]
except Exception as e:
logger.error(f"Failed to download jwks from {signed_jwks_uri}: {e}")
return jwks_list
def get_jwk_from_jwt(jwt: str, provider_jwks: dict) -> dict:
"""
docs here
"""
head = unpad_jwt_head(jwt)
kid = head["kid"]
if isinstance(provider_jwks, dict) and provider_jwks.get('keys'):
provider_jwks = provider_jwks['keys']
for jwk in provider_jwks:
if jwk["kid"] == kid:
return jwk
return {}
def random_token(n=254):
return token_hex(n)
@@ -0,0 +1,104 @@
import json
from typing import Union
from cryptojwt.jwk.jwk import key_from_jwk_dict
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from spid_cie_oidc.provider.settings import (
OIDCFED_DEFAULT_PROVIDER_PROFILE,
OIDCFED_PROVIDER_PROFILES
)
from spid_cie_oidc.relying_party.settings import (
RP_DEFAULT_PROVIDER_PROFILES,
RP_PROVIDER_PROFILES
)
from .jwks import serialize_key
from .settings import (
ENCRYPTION_ALG_VALUES_SUPPORTED,
ENCRYPTION_ENC_SUPPORTED,
ENTITY_TYPES,
SIGNING_ALG_VALUES_SUPPORTED
)
def validate_public_jwks(values: Union[dict, list]):
if isinstance(values, dict):
values = [values]
try:
for jwk_dict in values:
_k = key_from_jwk_dict(jwk_dict)
if _k.private_key():
_pub = serialize_key(_k.public_key())
raise ValidationError(
f"This JWK is is private {json.dumps(jwk_dict)}. "
f"It MUST be public instead, like this: {json.dumps([_pub])}."
)
except Exception as e:
raise ValidationError(f"Not valid: {e}")
def validate_metadata_algs(metadata: dict):
amap = dict(
id_token_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED,
id_token_encryption_alg_values_supported = ENCRYPTION_ALG_VALUES_SUPPORTED,
id_token_encryption_enc_values_supported = ENCRYPTION_ENC_SUPPORTED,
token_endpoint_auth_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED,
userinfo_encryption_alg_values_supported = ENCRYPTION_ALG_VALUES_SUPPORTED,
userinfo_encryption_enc_values_supported = ENCRYPTION_ENC_SUPPORTED,
userinfo_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED,
request_object_encryption_alg_values_supported = ENCRYPTION_ALG_VALUES_SUPPORTED,
request_object_encryption_enc_values_supported = ENCRYPTION_ENC_SUPPORTED,
request_object_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED,
)
if metadata.get("openid_provider", None):
md = metadata["openid_provider"]
for k, v in amap.items():
if k in md:
for alg in md[k]:
if alg not in v:
raise ValidationError(
f"{k} has an unsupported value {alg}. "
f"Supported algs are {v}"
)
def validate_entity_metadata(value):
status = False
for i in ENTITY_TYPES:
if i in value:
status = True
if not status:
raise ValidationError(
_(f'Need to specify one of {", ".join(ENTITY_TYPES)}')
)
if "openid_provider" in value:
schema = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE]
try:
schema["op_metadata"](**value["openid_provider"])
except Exception as e:
raise ValidationError(
f"OP metadata fail {e}. "
)
if "openid_relying_party" in value:
schema = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES]
try:
schema["rp_metadata"](**value["openid_relying_party"])
except Exception as e:
raise ValidationError(
f"RP metadata fail {e}. "
)
# TODO - add wallet_provider and wallet_relying_party once standardized
def validate_private_jwks(values: Union[dict, list]):
if isinstance(values, dict):
values = [values]
try:
for jwk_dict in values:
_k = key_from_jwk_dict(jwk_dict)
if not _k.private_key():
raise ValidationError(f"Can't extract a private JWK from {jwk_dict}")
except Exception as e:
raise ValidationError(f"Not valid: {e}")
@@ -0,0 +1,257 @@
import logging
from urllib.parse import urlsplit, urlunsplit
from django.conf import settings
from djagger.decorators import schema
from django.http import Http404
from django.http import HttpResponse
from django.http import JsonResponse
from spid_cie_oidc.entity.jwtse import create_jws
from spid_cie_oidc.entity.schemas.resolve_endpoint import (
ResolveRequest, ResolveResponse, ResolveErrorResponse
)
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
from .models import (
FederationEntityConfiguration,
FederationHistoricalKey,
TrustChain,
StaffToken
)
from .settings import HTTPC_PARAMS
from .statements import OIDCFED_FEDERATION_WELLKNOWN_URL
from .utils import iat_now
logger = logging.getLogger(__name__)
def _swap_scheme(url: str) -> str:
parts = urlsplit(url)
if parts.scheme == "http":
return urlunsplit(("https", parts.netloc, parts.path, parts.query, parts.fragment))
if parts.scheme == "https":
return urlunsplit(("http", parts.netloc, parts.path, parts.query, parts.fragment))
return url
def get_subs_from_wellknown(request, wkuri :str):
sub = request.build_absolute_uri().split(wkuri)[0].rstrip("/")
sub_values = []
for candidate in (sub, _swap_scheme(sub)):
for value in (candidate, f"{candidate}/"):
if value not in sub_values:
sub_values.append(value)
return sub_values
def entity_configuration(request):
"""
OIDC Federation Entity Configuration at
.well-known/openid-federation
"""
_sub_values = get_subs_from_wellknown(request, OIDCFED_FEDERATION_WELLKNOWN_URL)
conf = FederationEntityConfiguration.objects.filter(
# TODO: check for reverse proxy and forwarders ...
sub__in=_sub_values,
is_active=True,
).first()
if not conf: # pragma: no cover
raise Http404()
if request.GET.get("format") == "json": # pragma: no cover
return JsonResponse(conf.entity_configuration_as_dict, safe=False)
else:
return HttpResponse(
conf.entity_configuration_as_jws, content_type="application/entity-statement+jwt"
)
@schema(
methods=['GET'],
get_request_schema = {
"application/x-www-form-urlencoded": ResolveRequest
},
get_response_schema = {
"400": ResolveErrorResponse,
"404": ResolveErrorResponse,
"200": ResolveResponse
},
tags = ['Federation API']
)
def resolve_entity_statement(request, format: str = "jose"):
"""
resolves the final metadata of its descendants
In this implementation we only returns a preexisting
Metadata if it's valid
we avoid any possibility to trigger a new Metadata discovery if
"""
if not all((request.GET.get("sub"), request.GET.get("trust_anchor"))):
return JsonResponse(
{
"error": "invalid_request",
"error_description": "sub and trust_anchor parameters are REQUIRED."
},
status=400
)
iss = FederationEntityConfiguration.objects.filter(is_active=True).first()
_q = dict(
sub=request.GET["sub"],
trust_anchor__sub=request.GET["trust_anchor"],
is_active=True
)
# gets the cached one
entity = TrustChain.objects.filter(**_q).first()
# only with privileged actors with staff token can triggers a new trust chain
staff_token_head = request.headers.get("Authorization", None)
if staff_token_head:
staff_token = StaffToken.objects.filter(
token = staff_token_head
).first()
if staff_token.is_valid:
try:
# a staff token get a fresh trust chain on each call
entity = get_or_create_trust_chain(
httpc_params = HTTPC_PARAMS,
required_trust_marks = getattr(
settings, "OIDCFED_REQUIRED_TRUST_MARKS", []
),
subject=_q["sub"],
trust_anchor=_q["trust_anchor__sub"],
force = True
)
except Exception as e:
logger.error(
f"Failed privileged Trust Chain creation for {_q['sub']}: {e}"
)
if not entity:
return JsonResponse(
{
"error": "not_found",
"error_description": "entity not found"
},
status=404
)
metadata = entity.metadata
# Filter by entity_type if requested (OpenID Federation 8.3.1)
entity_types = request.GET.getlist("entity_type")
if entity_types:
metadata = {
k: v for k, v in (metadata or {}).items()
if k in entity_types
}
res = {
"iss": iss.sub,
"sub": request.GET["sub"],
"iat": entity.iat_as_timestamp,
"exp": entity.exp_as_timestamp,
"trust_marks": entity.trust_marks,
"metadata": metadata,
"trust_chain": entity.chain
}
if request.GET.get("format") == "json" or format == "json":
return JsonResponse(res, safe=False)
jwk = iss.jwks_fed[0]
signed = create_jws(
res,
jwk,
alg=iss.default_signature_alg,
protected={"typ": "resolve-response+jwt", "kid": jwk.get("kid")},
)
return HttpResponse(
signed,
content_type="application/resolve-response+jwt",
)
def openid_jwks(request, metadata_type:str, resource_type:str):
"""
resource_tytpe = set(jwks_uri, jwks.jose)
"""
_sub = request.build_absolute_uri().rsplit(resource_type)[0]
_lookup = _sub.replace(f"/{metadata_type}/", "").rstrip("/")
_lookup_values = [_lookup, _swap_scheme(_lookup)]
conf = FederationEntityConfiguration.objects.filter(
# TODO: check for reverse proxy and forwarders ...
sub__in=_lookup_values,
is_active=True,
).first()
if not conf: # pragma: no cover
raise Http404()
content = conf.entity_configuration_as_dict['metadata'].get(
metadata_type, {}
).get("jwks", {})
if not content:
raise Http404()
if resource_type == 'jwks.json':
return JsonResponse(content, safe=False)
elif resource_type == 'jwks.jose':
return HttpResponse(
create_jws(content, conf.jwks_fed[0]),
content_type="application/jose"
)
else:
raise Http404()
def openid_connect_jwks_uri(request, metadata_type, resource_type="jwks.json"):
"""
OpenID Connect default jwks_uri
"""
return openid_jwks(request, metadata_type, resource_type)
def openid_connect_signed_jwks_uri(request, metadata_type, resource_type="jwks.jose"):
"""
OpenID Federation signed_jwks_uri
"""
return openid_jwks(request, metadata_type, resource_type)
def historical_keys(request):
"""
OIDC Federation Entity Configuration at
.well-known/openid-federation
"""
sub_values = get_subs_from_wellknown(request, '.well-known/openid-federation-historical-jwks')
keys = FederationHistoricalKey.objects.filter(entity__sub__in=sub_values)
entity = keys.first().entity if keys.first() else None
reg = {
"iss": entity.sub if entity else sub_values[0],
"iat": iat_now(),
"keys": []
}
for i in keys:
reg["keys"].append(i.as_dict)
if request.GET.get("format") == "json": # pragma: no cover
return JsonResponse(reg, safe=False)
else:
res = create_jws(
reg,
entity.jwks_fed[0],
typ="jwk-set+jwt",
# **kwargs,
)
return HttpResponse(
res, content_type="application/jwk-set+jwt"
)
+101
View File
@@ -0,0 +1,101 @@
import base64
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from django.utils import timezone
from typing import Union
from django.conf import settings
class X509Issuer:
def __init__(
self,
private_key: bytes,
public_key: bytes,
subject_data: Union[bytes, None] = None,
is_ca_or_int: bool = False,
path_length: Union[int, None] = None
) -> x509.CertificateBuilder:
"""
returns an X.509 certificate
"""
# private_key = COSEKey.from_bytes(self.private_key.encode())
# TODO: add CDP using CRL
if subject_data.get("X509_COMMON_NAME"):
cn = subject_data["X509_COMMON_NAME"]
else:
cn = getattr(settings, "X509_COMMON_NAME")
subject_name = [
x509.NameAttribute(NameOID.COMMON_NAME, cn)
] + [
x509.NameAttribute(NameOID.getattr(i), subject_data[i])
for i in (
'COUNTRY_NAME',
'X509_STATE_OR_PROVINCE_NAME',
'X509_LOCALITY_NAME',
'X509_ORGANIZATION_NAME',
) if subject_data.get(i)
]
_basic_constraints = dict(ca=is_ca_or_int)
_basic_constraints["path_length"] = path_length
self.cert = (
x509.CertificateBuilder()
.issuer_name(
x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, settings.X509_COUNTRY_NAME),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, settings.X509_STATE_OR_PROVINCE_NAME),
x509.NameAttribute(NameOID.LOCALITY_NAME, settings.X509_LOCALITY_NAME),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, settings.X509_ORGANIZATION_NAME),
x509.NameAttribute(NameOID.COMMON_NAME, settings.X509_COMMON_NAME),
])
)
.subject_name(x509.Name(subject_name))
.public_key(public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(
getattr(settings, "X509_NOT_VALID_BEFORE", None) or
subject_data.get('X509_NOT_VALID_BEFORE') or
(timezone.localtime() - timezone.timedelta(minutes=12))
)
.not_valid_after(
getattr(settings, "X509_NOT_VALID_AFTER", None) or
subject_data.get('X509_NOT_VALID_AFTER') or
(timezone.localtime() + timezone.timedelta(days=365))
)
.add_extension(
x509.BasicConstraints(**_basic_constraints),
critical=True,
)
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName(cn),
x509.UniformResourceIdentifier(subject_data.get('entity_id')),
]),
critical=False
)
.sign(private_key, hashes.SHA256())
)
@property
def der(self):
return self.cert.public_bytes(
getattr(serialization.Encoding, 'DER')
)
@property
def pem(self):
return self.cert.public_bytes(
getattr(serialization.Encoding, 'PEM')
)
@property
def x5c(self):
return [base64.b64encode(self.der).decode('utf-8')]