From 2261951b15f9aa12e226849be04db4a8b745434e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 27 Nov 2025 19:50:53 +0100 Subject: [PATCH] Peppol: Live ID validation (#5602) * Peppol: Live ID validation * Always check both systems * Simplify logic --- pyproject.toml | 1 + src/pretix/base/invoicing/peppol.py | 32 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa82563f35..4d89864e54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "cryptography>=44.0.0", "css-inline==0.18.*", "defusedcsv>=1.1.0", + "dnspython==2.*", "Django[argon2]==4.2.*,>=4.2.26", "django-bootstrap3==25.2", "django-compressor==4.5.1", diff --git a/src/pretix/base/invoicing/peppol.py b/src/pretix/base/invoicing/peppol.py index c8a28752a9..333cb7534d 100644 --- a/src/pretix/base/invoicing/peppol.py +++ b/src/pretix/base/invoicing/peppol.py @@ -19,8 +19,11 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import base64 +import hashlib import re +import dns.resolver from django import forms from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _, pgettext @@ -123,6 +126,9 @@ class PeppolIdValidator: "9959": ".*", } + def __init__(self, validate_online=False): + self.validate_online = validate_online + def __call__(self, value): if ":" not in value: raise ValidationError(_("A Peppol participant ID always starts with a prefix, followed by a colon (:).")) @@ -136,6 +142,28 @@ class PeppolIdValidator: raise ValidationError(_("The Peppol participant ID does not match the validation rules for the prefix " "%(number)s. Please reach out to us if you are sure this ID is correct."), params={"number": prefix}) + + if self.validate_online: + base_hostnames = ['edelivery.tech.ec.europa.eu', 'acc.edelivery.tech.ec.europa.eu'] + smp_id = base64.b32encode(hashlib.sha256(value.lower().encode()).digest()).decode().rstrip("=") + for base_hostname in base_hostnames: + smp_domain = f'{smp_id}.iso6523-actorid-upis.{base_hostname}' + resolver = dns.resolver.Resolver() + try: + answers = resolver.resolve(smp_domain, 'NAPTR', lifetime=1.0) + if answers: + return value + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + # ID not registered, do not set found=True + pass + except Exception: # noqa + # Error likely on our end or infrastructure is down, allow user to proceed + return value + + raise ValidationError( + _("The Peppol participant ID is not registered on the Peppol network."), + ) + return value @@ -155,7 +183,9 @@ class PeppolTransmissionType(TransmissionType): "transmission_peppol_participant_id": forms.CharField( label=_("Peppol participant ID"), validators=[ - PeppolIdValidator(), + PeppolIdValidator( + validate_online=True, + ), ] ), }