Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
31e6eba
added comment ot start off flag logic
sciros Mar 27, 2026
f029d86
Merge branch 'ds-persistent-dns-bugfix-2' into ds-dcv-case-sensitive-…
sciros Mar 30, 2026
2aedf88
broke out case sensitivity tests to prepare for test checking case se…
sciros Mar 30, 2026
908095c
removed test skip for case sensitive validation methods
sciros Mar 30, 2026
13ff852
added failing test for methods that can allow a require_exact_case flag
sciros Mar 30, 2026
792e86d
Merge branch 'ds-persistent-dns-bugfix-2' into ds-dcv-case-sensitive-…
sciros Mar 31, 2026
a027722
added require_exact_case flag to DCV Validation Parameters (relevant …
sciros Mar 31, 2026
0c3e79b
Merge branch 'ds-persistent-dns-bugfix-2' into ds-dcv-case-sensitive-…
sciros Mar 31, 2026
21bba35
applied formatting to changed files
sciros Mar 31, 2026
c44d364
Merge branch 'ds-persistent-dns-bugfix-2' into ds-dcv-case-sensitive-…
sciros Apr 1, 2026
2017f51
Merge branch 'main' into ds-dcv-case-sensitive-flag
sciros Apr 1, 2026
65e5768
updated aiohttp dependency to address moderate and low vulnerabilities
sciros Apr 3, 2026
3beb18d
added logic to force case insensitivity for DNS Change CNAME and CAA …
sciros Apr 3, 2026
db9510e
updated couple other libraries to address CVEs. reworked a couple of …
sciros Apr 3, 2026
f119fd2
tweaked logic in check_parameters and added tests to force ACME case …
sciros Apr 3, 2026
6867cc6
Update tests/unit/open_mpic_core/test_check_request_parameters.py
sciros Apr 3, 2026
c37b55e
added token to test for acme http 01 case sensitivity
sciros Apr 3, 2026
6417ff4
modified check parameters to more correctly set require_exact_case to…
sciros Apr 6, 2026
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
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ classifiers = [
]
dependencies = [
"pyyaml==6.0.2",
"requests==2.32.4",
"requests==2.33.0",
"dnspython==2.7.0",
"pydantic==2.11.7",
"aiohttp==3.13.3",
"cryptography==46.0.5",
"aiohttp==3.13.4",
"cryptography==46.0.6",
"uritools==6.0.1",
]

Expand Down Expand Up @@ -60,7 +60,7 @@ build.targets.wheel.packages = ["src/open_mpic_core"]
"./tests/unit/test_util" = "open_mpic_core_test/test_util" # include tests in the wheel to facilitate integration testing in wrapper projects

[tool.api]
spec_version = "3.8.0"
spec_version = "3.9.0"
spec_repository = "https://github.com/open-mpic/open-mpic-specification"

[tool.hatch.envs.default]
Expand Down
2 changes: 1 addition & 1 deletion src/open_mpic_core/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "6.3.4"
__version__ = "6.4.0"
23 changes: 21 additions & 2 deletions src/open_mpic_core/common_domain/check_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Literal, Union, Any, Set, Annotated
from uritools import isuri

from pydantic import BaseModel, field_validator, Field
from pydantic import BaseModel, field_validator, Field, model_validator

from open_mpic_core import CertificateType, DnsRecordType, DcvValidationMethod, UrlScheme

Expand All @@ -18,6 +18,7 @@ class CaaCheckParameters(BaseModel):

class DcvValidationParameters(BaseModel, ABC):
validation_method: DcvValidationMethod
require_exact_case: bool
# DNS records have 5 fields: name, ttl, class, type, rdata (which can be multipart itself)
# A or AAAA: name=domain_name type=A <rdata:address> (ip address)
# CNAME: name=domain_name_x type=CNAME <rdata:domain_name>
Expand All @@ -31,6 +32,7 @@ class DcvWebsiteChangeValidationParameters(DcvValidationParameters):
url_scheme: UrlScheme = UrlScheme.HTTP
http_headers: dict[str, Any] | None = None
match_regex: str | None = None
require_exact_case: bool = True
# TODO add optional flag to iterate up through the domain hierarchy


Expand All @@ -42,7 +44,8 @@ class DcvGeneralDnsValidationParameters(DcvValidationParameters, ABC):

class DcvDnsChangeValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.DNS_CHANGE] = DcvValidationMethod.DNS_CHANGE
require_exact_match: bool = False
require_exact_match: bool = False # if False, looks for a matching substring (rather than entire string)
require_exact_case: bool = True

# noinspection PyNestedDecorators
@field_validator("dns_record_type")
Expand All @@ -52,13 +55,20 @@ def validate_record_type(cls, v: DnsRecordType) -> DnsRecordType:
raise ValueError(f"Record type must be one of {DNS_CHANGE_ALLOWED_RECORD_TYPES}, got {v}")
return v

@model_validator(mode="after")
def validate_require_exact_case(self) -> 'DcvDnsChangeValidationParameters':
if self.dns_record_type is not DnsRecordType.TXT:
self.require_exact_case = False # case-sensitivity only applies to TXT records; force to False for others
return self


class DcvDnsPersistentValidationParameters(DcvValidationParameters):
validation_method: Literal[DcvValidationMethod.DNS_PERSISTENT] = DcvValidationMethod.DNS_PERSISTENT
dns_record_type: Literal[DnsRecordType.TXT] = DnsRecordType.TXT
dns_name_prefix: Literal["_validation-persist"] = "_validation-persist"
issuer_domain_names: list[str] # Disclosed issuer domain names from CA's CP/CPS
expected_account_uri: str # The specific account URI to validate
require_exact_case: Literal[False] = False

@field_validator("expected_account_uri")
@classmethod
Expand All @@ -83,27 +93,32 @@ class DcvContactEmailTxtValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.CONTACT_EMAIL_TXT] = DcvValidationMethod.CONTACT_EMAIL_TXT
dns_record_type: Literal[DnsRecordType.TXT] = DnsRecordType.TXT
dns_name_prefix: Literal["_validation-contactemail"] = "_validation-contactemail"
require_exact_case: Literal[False] = False


class DcvContactEmailCaaValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.CONTACT_EMAIL_CAA] = DcvValidationMethod.CONTACT_EMAIL_CAA
dns_record_type: Literal[DnsRecordType.CAA] = DnsRecordType.CAA
require_exact_case: Literal[False] = False


class DcvContactPhoneTxtValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.CONTACT_PHONE_TXT] = DcvValidationMethod.CONTACT_PHONE_TXT
dns_record_type: Literal[DnsRecordType.TXT] = DnsRecordType.TXT
dns_name_prefix: Literal["_validation-contactphone"] = "_validation-contactphone"
require_exact_case: Literal[False] = False


class DcvContactPhoneCaaValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.CONTACT_PHONE_CAA] = DcvValidationMethod.CONTACT_PHONE_CAA
dns_record_type: Literal[DnsRecordType.CAA] = DnsRecordType.CAA
require_exact_case: Literal[False] = False


class DcvIpAddressValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.IP_ADDRESS] = DcvValidationMethod.IP_ADDRESS
dns_record_type: DnsRecordType
require_exact_case: Literal[False] = False

# noinspection PyNestedDecorators
@field_validator("dns_record_type")
Expand All @@ -117,25 +132,29 @@ def validate_record_type(cls, v: DnsRecordType) -> DnsRecordType:
class DcvReverseAddressLookupValidationParameters(DcvGeneralDnsValidationParameters):
validation_method: Literal[DcvValidationMethod.REVERSE_ADDRESS_LOOKUP] = DcvValidationMethod.REVERSE_ADDRESS_LOOKUP
dns_record_type: Literal[DnsRecordType.PTR] = DnsRecordType.PTR
require_exact_case: Literal[False] = False


class DcvAcmeHttp01ValidationParameters(DcvValidationParameters):
validation_method: Literal[DcvValidationMethod.ACME_HTTP_01] = DcvValidationMethod.ACME_HTTP_01
token: str
key_authorization: str
http_headers: dict[str, Any] | None = None
require_exact_case: Literal[True] = True # ACME HTTP-01 validation is always case-sensitive, per RFC8555


class DcvAcmeDns01ValidationParameters(DcvValidationParameters):
validation_method: Literal[DcvValidationMethod.ACME_DNS_01] = DcvValidationMethod.ACME_DNS_01
key_authorization_hash: str
dns_record_type: Literal[DnsRecordType.TXT] = DnsRecordType.TXT
dns_name_prefix: Literal["_acme-challenge"] = "_acme-challenge"
require_exact_case: Literal[True] = True # ACME DNS-01 validation is always case-sensitive


class DcvAcmeTlsAlpn01ValidationParameters(DcvValidationParameters):
validation_method: Literal[DcvValidationMethod.ACME_TLS_ALPN_01] = DcvValidationMethod.ACME_TLS_ALPN_01
key_authorization_hash: str
require_exact_case: Literal[False] = False


DcvCheckParameters = Annotated[
Expand Down
30 changes: 23 additions & 7 deletions src/open_mpic_core/mpic_dcv_checker/mpic_dcv_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ async def perform_general_dns_validation(self, request: DcvCheckRequest) -> DcvC
validation_method, check_parameters
)

require_exact_case = check_parameters.require_exact_case

if validation_method == DcvValidationMethod.DNS_CHANGE: # DNS_CHANGE may allow for non-exact match
exact_match = check_parameters.require_exact_match

Expand All @@ -141,7 +143,13 @@ async def perform_general_dns_validation(self, request: DcvCheckRequest) -> DcvC
):
lookup = await self.perform_dns_resolution(name_to_resolve, validation_method, dns_record_type)
MpicDcvChecker.evaluate_dns_lookup_response(
dcv_check_response, lookup, validation_method, dns_record_type, expected_dns_record_content, exact_match
dcv_check_response,
lookup,
validation_method,
dns_record_type,
expected_dns_record_content,
exact_match,
require_exact_case,
)
except dns.exception.DNSException as e:
log_msg = f"DNS lookup error for {name_to_resolve}: {str(e)}. Trace ID: {request.trace_identifier}"
Expand Down Expand Up @@ -289,11 +297,18 @@ async def evaluate_http_lookup_response(
# ACME requires an exact match
dcv_check_response.check_passed = challenge_value == result
else:
# Case-insensitive substring check for WEBSITE_CHANGE; also checks regex if provided
dcv_check_response.check_passed = challenge_value.lower() in result.lower()
# For WEBSITE_CHANGE: substring check; case-sensitive by default, unless require_exact_case=False
require_exact_case = dcv_check_request.dcv_check_parameters.require_exact_case
if require_exact_case:
dcv_check_response.check_passed = challenge_value in result
else:
dcv_check_response.check_passed = challenge_value.lower() in result.lower()
if match_regex is not None and len(match_regex) > 0:
match = re.search(match_regex, result)
dcv_check_response.check_passed = challenge_value.lower() in result.lower() and match is not None
challenge_value_found = (
challenge_value in result if require_exact_case else challenge_value.lower() in result.lower()
)
dcv_check_response.check_passed = challenge_value_found and match is not None
dcv_check_response.details.response_status_code = http_response.status
dcv_check_response.details.response_url = target_url
dcv_check_response.details.response_history = response_history
Expand Down Expand Up @@ -324,6 +339,7 @@ def evaluate_dns_lookup_response(
dns_record_type: DnsRecordType,
expected_dns_record_content: ExpectedDnsRecordContent | None,
exact_match: bool = True,
require_exact_case: bool = False,
) -> None:
if dns_response is None:
dcv_check_response.check_passed = False
Expand Down Expand Up @@ -374,10 +390,10 @@ def evaluate_dns_lookup_response(
expected_dns_record_content, records_as_strings
)
else:
if validation_method == DcvValidationMethod.ACME_DNS_01:
expected_dns_value = expected_dns_record_content.expected_value # case-sensitive per ACME spec
if require_exact_case:
expected_dns_value = expected_dns_record_content.expected_value
else:
expected_dns_value = expected_dns_record_content.expected_value.lower() # all others case-insensitive
expected_dns_value = expected_dns_record_content.expected_value.lower() # case-insensitive
records_as_strings = [record.lower() for record in records_as_strings]

# exact_match=True requires at least one record matches and will fail even if whitespace is different.
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/open_mpic_core/test_check_request_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
DcvContactPhoneCaaValidationParameters,
DcvIpAddressValidationParameters,
DcvCheckParameters,
DnsRecordType
)


Expand Down Expand Up @@ -94,6 +95,40 @@ def check_request_parameters__should_accept_valid_uri_format_for_expected_accoun
assert isinstance(details_as_object, DcvDnsPersistentValidationParameters)
assert details_as_object.expected_account_uri == account_uri

# fmt: off
@pytest.mark.parametrize("record_type, is_always_case_insensitive", [
(DnsRecordType.CNAME, True), (DnsRecordType.TXT, False), (DnsRecordType.CAA, True)
])
# fmt: on
def check_request_parameters__should_force_case_sensitivity_to_false_for_non_txt_dns_records(
self, record_type, is_always_case_insensitive
):
# notice require_exact_case is true in the serialized JSON; it should be forced to False for non-TXT records
parameters_as_json = f'{{"validation_method": "dns-change", "dns_record_type": "{record_type}", "challenge_value": "test-cv", "require_exact_case": true}}'
type_adapter = TypeAdapter(DcvCheckParameters)
details_as_object: DcvCheckParameters = type_adapter.validate_json(parameters_as_json)
assert isinstance(details_as_object, DcvDnsChangeValidationParameters)
if is_always_case_insensitive:
assert details_as_object.require_exact_case is False # should be forced to False for non-TXT records
else:
assert details_as_object.require_exact_case is True

@staticmethod
def check_request_parameters__should_disallow_setting_case_sensitivity_to_false_for_acme_dns_01():
parameters_as_json = '{"validation_method": "acme-dns-01", "key_authorization_hash": "test-kah", "require_exact_case": false}'
type_adapter = TypeAdapter(DcvCheckParameters)
with pytest.raises(Exception) as validation_error:
type_adapter.validate_json(parameters_as_json)
assert isinstance(validation_error.value, ValueError)

@staticmethod
def check_request_parameters__should_disallow_setting_case_sensitivity_to_false_for_acme_http_01():
parameters_as_json = '{"validation_method": "acme-http-01", "token:" "test", "key_authorization": "test-ka", "require_exact_case": false}'
type_adapter = TypeAdapter(DcvCheckParameters)
with pytest.raises(Exception) as validation_error:
type_adapter.validate_json(parameters_as_json)
assert isinstance(validation_error.value, ValueError)
Comment thread
sciros marked this conversation as resolved.


if __name__ == "__main__":
pytest.main()
Loading
Loading