Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make eIDAS LoA configurable #64

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions digid_eherkenning/migrations/0008_update_loa_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Generated by Django 4.2.10 on 2024-03-08 08:45

from django.db import migrations, models

import digid_eherkenning.choices


class Migration(migrations.Migration):

dependencies = [
(
"digid_eherkenning",
"0007_eherkenningconfiguration_service_description_url",
),
]

operations = [
migrations.RemoveConstraint(
model_name="eherkenningconfiguration",
name="valid_loa",
),
migrations.RenameField(
model_name="eherkenningconfiguration",
old_name="loa",
new_name="eh_loa",
),
migrations.AlterField(
model_name="eherkenningconfiguration",
name="eh_loa",
field=models.CharField(
choices=[
("urn:etoegang:core:assurance-class:loa1", "Non existent (1)"),
("urn:etoegang:core:assurance-class:loa2", "Low (2)"),
("urn:etoegang:core:assurance-class:loa2plus", "Low (2+)"),
("urn:etoegang:core:assurance-class:loa3", "Substantial (3)"),
("urn:etoegang:core:assurance-class:loa4", "High (4)"),
],
default="urn:etoegang:core:assurance-class:loa3",
help_text="Level of Assurance (LoA) to use for the eHerkenning service.",
max_length=100,
verbose_name="eHerkenning LoA",
),
),
migrations.AddField(
model_name="eherkenningconfiguration",
name="eidas_loa",
field=models.CharField(
choices=[
("urn:etoegang:core:assurance-class:loa1", "Non existent (1)"),
("urn:etoegang:core:assurance-class:loa2", "Low (2)"),
("urn:etoegang:core:assurance-class:loa2plus", "Low (2+)"),
("urn:etoegang:core:assurance-class:loa3", "Substantial (3)"),
("urn:etoegang:core:assurance-class:loa4", "High (4)"),
],
default="urn:etoegang:core:assurance-class:loa3",
help_text="Level of Assurance (LoA) to use for the eIDAS service.",
max_length=100,
verbose_name="eIDAS LoA",
),
),
migrations.AddConstraint(
model_name="eherkenningconfiguration",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(
("eh_loa__in", digid_eherkenning.choices.AssuranceLevels),
("eidas_loa__in", digid_eherkenning.choices.AssuranceLevels),
)
),
name="valid_loa",
),
),
]
25 changes: 19 additions & 6 deletions digid_eherkenning/models/eherkenning.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.utils.translation import gettext_lazy as _

from ..choices import AssuranceLevels
from ..types import EHerkenningConfig
from ..validators import oin_validator
from .base import BaseConfiguration

Expand Down Expand Up @@ -60,11 +61,11 @@ def get_default_requested_attributes_eidas():


class EherkenningConfiguration(BaseConfiguration):
loa = models.CharField(
_("LoA"),
eh_loa = models.CharField(
_("eHerkenning LoA"),
choices=AssuranceLevels.choices,
default=AssuranceLevels.substantial,
help_text=_("Level of Assurance (LoA) to use for all the services."),
help_text=_("Level of Assurance (LoA) to use for the eHerkenning service."),
max_length=100,
)
eh_attribute_consuming_service_index = models.CharField(
Expand Down Expand Up @@ -99,6 +100,13 @@ class EherkenningConfiguration(BaseConfiguration):
"changing the value is a manual process."
),
)
eidas_loa = models.CharField(
_("eIDAS LoA"),
choices=AssuranceLevels.choices,
default=AssuranceLevels.substantial,
help_text=_("Level of Assurance (LoA) to use for the eIDAS service."),
max_length=100,
)
eidas_attribute_consuming_service_index = models.CharField(
_("eIDAS attribute consuming service index"),
blank=True,
Expand Down Expand Up @@ -176,11 +184,15 @@ class Meta:
verbose_name = _("Eherkenning/eIDAS configuration")
constraints = [
models.constraints.CheckConstraint(
name="valid_loa", check=models.Q(loa__in=AssuranceLevels)
name="valid_loa",
check=models.Q(
models.Q(eh_loa__in=AssuranceLevels)
& models.Q(eidas_loa__in=AssuranceLevels)
),
),
]

def as_dict(self) -> dict:
def as_dict(self) -> EHerkenningConfig:
"""
Emit the configuration as a dictionary compatible with the old settings format.
"""
Expand Down Expand Up @@ -215,6 +227,7 @@ def as_dict(self) -> dict:
"service_description": self.service_description,
"service_description_url": self.service_description_url,
"service_url": self.base_url,
"loa": self.eh_loa,
"privacy_policy_url": self.privacy_policy,
"herkenningsmakelaars_id": self.makelaar_id,
"requested_attributes": self.eh_requested_attributes,
Expand Down Expand Up @@ -247,6 +260,7 @@ def as_dict(self) -> dict:
"service_description": self.service_description,
"service_description_url": self.service_description_url,
"service_url": self.base_url,
"loa": self.eidas_loa,
"privacy_policy_url": self.privacy_policy,
"herkenningsmakelaars_id": self.makelaar_id,
"requested_attributes": self.eidas_requested_attributes,
Expand All @@ -270,7 +284,6 @@ def as_dict(self) -> dict:
"service_entity_id": self.idp_service_entity_id,
"oin": self.oin,
"services": services,
"loa": self.loa,
# optional in runtime code
"want_assertions_encrypted": self.want_assertions_encrypted,
"want_assertions_signed": self.want_assertions_signed,
Expand Down
32 changes: 17 additions & 15 deletions digid_eherkenning/saml2/eherkenning.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import binascii
from base64 import b64encode
from io import BytesIO
from typing import List, Literal, Union
from typing import Literal, Union
from uuid import uuid4

from django.urls import reverse
Expand All @@ -12,10 +12,12 @@
from furl.furl import furl
from lxml.builder import ElementMaker
from lxml.etree import Element, tostring
from onelogin.saml2.settings import OneLogin_Saml2_Settings

from ..choices import AssuranceLevels
from ..models import EherkenningConfiguration
from ..settings import EHERKENNING_DS_XSD
from ..types import EHerkenningConfig, EHerkenningSAMLConfig, ServiceConfig
from ..utils import validate_xml
from .base import BaseSaml2Client, get_service_description, get_service_name

Expand All @@ -36,7 +38,7 @@

def generate_dienst_catalogus_metadata(eherkenning_config=None):
eherkenning_config = eherkenning_config or EherkenningConfiguration.get_solo()
settings = eherkenning_config.as_dict()
settings: EHerkenningConfig = eherkenning_config.as_dict()
# ensure that the single language strings are output in both nl and en
for service in settings["services"]:
name = service["service_name"]
Expand Down Expand Up @@ -295,7 +297,7 @@ def create_classifiers_element(classifiers: list) -> ElementMaker:
return ESC("Classifiers", *classifiers_elements)


def create_key_descriptor(x509_certificate_content: bytes):
def create_key_descriptor(x509_certificate_content: bytes) -> ElementMaker:
certificate = load_pem_x509_certificate(x509_certificate_content)
key_name = binascii.hexlify(
certificate.fingerprint(certificate.signature_hash_algorithm)
Expand All @@ -317,7 +319,7 @@ def create_key_descriptor(x509_certificate_content: bytes):
return MD("KeyDescriptor", *args, **kwargs)


def create_service_catalogus(conf, validate=True):
def create_service_catalogus(conf: EHerkenningConfig, validate: bool = True) -> bytes:
"""
https://afsprakenstelsel.etoegang.nl/display/as/Service+catalog
"""
Expand Down Expand Up @@ -366,7 +368,7 @@ def create_service_catalogus(conf, validate=True):
service_description,
service_description_url,
# https://afsprakenstelsel.etoegang.nl/display/as/Level+of+assurance
conf["loa"],
service["loa"],
entity_concerned_types_allowed,
requested_attributes,
herkenningsmakelaars_id,
Expand Down Expand Up @@ -403,8 +405,8 @@ def create_service_catalogus(conf, validate=True):


def get_metadata_eherkenning_requested_attributes(
conf: dict, service_id: str
) -> List[dict]:
conf: ServiceConfig, service_id: str
) -> list[dict]:
# There needs to be a RequestedAttribute element where the name is the ServiceID
# https://afsprakenstelsel.etoegang.nl/display/as/DV+metadata+for+HM
requested_attributes = [{"name": service_id, "isRequired": False}]
Expand All @@ -427,7 +429,7 @@ def get_metadata_eherkenning_requested_attributes(
return requested_attributes


def create_attribute_consuming_services(conf: dict) -> list:
def create_attribute_consuming_services(conf: EHerkenningConfig) -> list[dict]:
attribute_consuming_services = []

for service in conf["services"]:
Expand Down Expand Up @@ -466,15 +468,15 @@ def __init__(
self.loa = loa

@property
def conf(self) -> dict:
def conf(self) -> EHerkenningConfig:
if not hasattr(self, "_conf"):
db_config = EherkenningConfiguration.get_solo()
self._conf = db_config.as_dict()
self._conf.setdefault("acs_path", reverse("eherkenning:acs"))
return self._conf

def create_config_dict(self, conf):
config_dict = super().create_config_dict(conf)
def create_config_dict(self, conf: EHerkenningConfig) -> EHerkenningSAMLConfig:
config_dict: EHerkenningSAMLConfig = super().create_config_dict(conf)

attribute_consuming_services = create_attribute_consuming_services(conf)
with conf["cert_file"].open("r") as cert_file, conf["key_file"].open(
Expand Down Expand Up @@ -504,7 +506,9 @@ def create_config_dict(self, conf):
)
return config_dict

def create_config(self, config_dict):
def create_config(
self, config_dict: EHerkenningSAMLConfig
) -> OneLogin_Saml2_Settings:
config_dict["security"].update(
{
# See comment in the python3-saml for in OneLogin_Saml2_Response.validate_num_assertions (onelogin/saml2/response.py)
Expand All @@ -515,9 +519,7 @@ def create_config(self, config_dict):
"metadataValidUntil": "",
"metadataCacheDuration": "",
"requestedAuthnContextComparison": "minimum",
"requestedAuthnContext": [
self.loa or self.conf["loa"],
],
"requestedAuthnContext": False if not self.loa else [self.loa],
}
)
return super().create_config(config_dict)
Expand Down
95 changes: 95 additions & 0 deletions digid_eherkenning/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from pathlib import Path
from typing import Optional, TypedDict, Union


class ServiceConfig(TypedDict):
service_uuid: str
service_name: str
attribute_consuming_service_index: str
service_instance_uuid: str
service_description: str
service_description_url: str
service_url: str
loa: str
privacy_policy_url: str
herkenningsmakelaars_id: str
requested_attributes: str
service_restrictions_allowed: str
entity_concerned_types_allowed: list[dict]
language: str
classifiers: Optional[list[str]]


class EHerkenningConfig(TypedDict):
base_url: str
acs_path: str
entity_id: str
metadata_file: str
cert_file: Path
key_file: Path
service_entity_id: str
oin: str
services: list[ServiceConfig]
want_assertions_encrypted: str
want_assertions_signed: str
key_passphrase: str
signature_algorithm: str
digest_algorithm: str
technical_contact_person_telephone: Optional[str]
technical_contact_person_email: Optional[str]
organization: str
organization_name: str
artifact_resolve_content_type: str


class ServiceProviderSAMLConfig(TypedDict):
entityId: str
assertionConsumerService: dict
singleLogoutService: dict
attributeConsumingServices: list[dict]
NameIDFormat: str
x509cert: str
privateKey: str
privateKeyPassphrase: Optional[str]


class IdentityProviderSAMLConfig(TypedDict):
entityId: str
singleSignOnService: dict
singleLogoutService: dict
x509cert: str


class SecuritySAMLConfig(TypedDict):
nameIdEncrypted: bool
authnRequestsSigned: bool
logoutRequestSigned: bool
logoutResponseSigned: bool
signMetadata: bool
wantMessagesSigned: bool
wantAssertionsSigned: bool
wantAssertionsEncrypted: bool
wantNameId: bool
wantNameIdEncrypted: bool
wantAttributeStatement: bool
requestedAuthnContext: Union[bool, list[str]]
requestedAuthnContextComparison: str
failOnAuthnContextMismatch: bool
metadataValidUntil: Optional[str]
metadataCacheDuration: Optional[str]
allowSingleLabelDomains: bool
signatureAlgorithm: str
digestAlgorithm: str
allowRepeatAttributeName: bool
rejectDeprecatedAlgorithm: bool
disableSignatureWrappingProtection: bool


class EHerkenningSAMLConfig(TypedDict):
strict: bool
debug: bool
sp: ServiceProviderSAMLConfig
idp: IdentityProviderSAMLConfig
security: SecuritySAMLConfig
contactPerson: dict
organization: dict
2 changes: 1 addition & 1 deletion digid_eherkenning/views/eherkenning.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_attribute_consuming_service_index(self) -> Optional[str]:

#
# TODO: It might be a good idea to change this to a post-verb.
# I can't think of any realy attack-vectors, but seems like a good
# I can't think of any relay attack-vectors, but seems like a good
# idea anyways.
#
def get_context_data(self, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"service_name": "Example eHerkenning", # TODO: eidas variant?
"want_assertions_signed": False,
"organization_name": "Example",
"loa": "urn:etoegang:core:assurance-class:loa3",
"eh_loa": "urn:etoegang:core:assurance-class:loa3",
"eidas_loa": "urn:etoegang:core:assurance-class:loa3",
"eh_attribute_consuming_service_index": "1",
"eidas_attribute_consuming_service_index": "2",
"oin": "00000000000000000000",
Expand Down
Loading
Loading