feat: link local library
This commit is contained in:
@@ -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
|
||||
@@ -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")},
|
||||
},
|
||||
),
|
||||
]
|
||||
+53
@@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
+21
@@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
+30
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+25
@@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
+56
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+37
@@ -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")},
|
||||
),
|
||||
]
|
||||
Vendored
+19
@@ -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,
|
||||
),
|
||||
]
|
||||
Vendored
+29
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
+29
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
Vendored
+30
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
+16
@@ -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',
|
||||
),
|
||||
]
|
||||
+19
@@ -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]),
|
||||
),
|
||||
]
|
||||
+21
@@ -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',
|
||||
),
|
||||
]
|
||||
+26
@@ -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')},
|
||||
),
|
||||
]
|
||||
+21
@@ -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',
|
||||
),
|
||||
]
|
||||
+28
@@ -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'),
|
||||
),
|
||||
]
|
||||
+19
@@ -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,
|
||||
),
|
||||
]
|
||||
+33
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
+33
@@ -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],
|
||||
),
|
||||
),
|
||||
]
|
||||
Vendored
+86
@@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
+79
@@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
+23
@@ -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),
|
||||
),
|
||||
]
|
||||
+17
@@ -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"),
|
||||
),
|
||||
]
|
||||
+17
@@ -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),
|
||||
),
|
||||
]
|
||||
+17
@@ -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"),
|
||||
),
|
||||
]
|
||||
+17
@@ -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"),
|
||||
),
|
||||
]
|
||||
+35
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
+23
@@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
||||
+38
@@ -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.",
|
||||
),
|
||||
),
|
||||
]
|
||||
+19
@@ -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",
|
||||
),
|
||||
]
|
||||
+34
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0032_alter_fetchedentitystatement_jwt.py
Vendored
+20
@@ -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),
|
||||
),
|
||||
]
|
||||
+18
@@ -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,),
|
||||
)
|
||||
+9
@@ -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"
|
||||
+91
@@ -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
|
||||
+97
@@ -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,
|
||||
)
|
||||
+55
@@ -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])
|
||||
+37
@@ -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)
|
||||
|
||||
+43
@@ -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)
|
||||
+12
@@ -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)
|
||||
+96
@@ -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)
|
||||
|
||||
|
||||
|
||||
+59
@@ -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"
|
||||
)
|
||||
@@ -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')]
|
||||
Reference in New Issue
Block a user