1# -*- coding: utf-8 -*-
2"""Validation utilities for acme2certifier"""
3
4import re
5import logging
6import ipaddress
7from email.utils import parseaddr
8from typing import List, Dict, Tuple, Optional
9import idna
10
11
12def dkeys_lower(tree: Dict[str, str]) -> Dict[str, str]:
13 """lower characters in payload string"""
14 if isinstance(tree, dict):
15 result = {k.lower(): dkeys_lower(v) for k, v in tree.items()}
16 elif isinstance(tree, list):
17 result = [dkeys_lower(ele) for ele in tree]
18 else:
19 result = tree
20 return result
21
22
23def fqdn_in_san_check(logger: logging.Logger, san_list: List[str], fqdn: str) -> bool:
24 """check if fqdn is in a list of sans"""
25 logger.debug("Helper.fqdn_in_san_check([%s], %s)", san_list, fqdn)
26
27 result = False
28 if fqdn and san_list:
29 for san in san_list:
30 try:
31 _type, value = san.lower().split(":", 1)
32 if fqdn == value:
33 result = True
34 break
35 except Exception:
36 logger.error("Error during SAN check. SAN split failed: %s", san)
37
38 logger.debug("Helper.fqdn_in_san_check() ended with: %s", result)
39 return result
40
41
42def validate_csr(logger: logging.Logger, order_dic: Dict[str, str], _csr) -> bool:
43 """validate certificate signing request against order"""
44 logger.debug("Helper.validate_csr(%s)", order_dic)
45 return True
46
47
48def normalize_email_address(logger: logging.Logger, raw: str) -> Optional[str]:
49 """Parse and normalize an email address for identity comparison.
50
51 Returns a canonical form with a lowercased local part and an A-label domain,
52 or None if the address cannot be parsed or the domain is not valid IDNA.
53 """
54 logger.debug("Helper.normalize_email_address()")
55 if not raw or not isinstance(raw, str):
56 return None
57
58 _display, addr = parseaddr(raw.strip())
59 if not addr or "@" not in addr:
60 logger.debug("Helper.normalize_email_address(): unparseable From header")
61 return None
62
63 local, _, domain = addr.rpartition("@")
64 local = local.strip()
65 domain = domain.strip()
66 if not local or not domain:
67 return None
68
69 try:
70 domain_ascii = idna.encode(domain).decode("ascii").lower()
71 except idna.IDNAError as err:
72 logger.debug(
73 "Helper.normalize_email_address(): invalid domain %s: %s", domain, err
74 )
75 return None
76
77 result = f"{local.lower()}@{domain_ascii}"
78 logger.debug("Helper.normalize_email_address() ended with: %s", result)
79 return result
80
81
82def validate_email(logger: logging.Logger, contact_list: List[str]) -> bool:
83 """validate contact against RFC608"""
84 logger.debug("Helper.validate_email()")
85 result = True
86 pattern = (
87 r"^[A-Za-z0-9.+_-]+@[A-Za-z][A-Za-z0-9._-]+[A-Za-z0-9]"
88 r"\.[A-Za-z]+(?:\.[A-Za-z]+)*$"
89 )
90 # check if we got a list or single address
91 if isinstance(contact_list, list):
92 for contact in contact_list:
93 contact = contact.replace("mailto:", "")
94 contact = contact.lstrip()
95 tmp_result = bool(re.search(pattern, contact))
96 logger.debug("# validate: %s result: %s", contact, tmp_result)
97 if not tmp_result:
98 result = tmp_result
99 else:
100 logger.debug("Helper.validate_email(): %s is not a list", contact_list)
101 contact_list = contact_list.replace("mailto:", "")
102 contact_list = contact_list.lstrip()
103 result = bool(re.search(pattern, contact_list))
104 logger.debug(
105 "Helper.validate_email() of: %s ended with result: %s", contact_list, result
106 )
107 return result
108
109
110def validate_identifier(
111 logger: logging.Logger,
112 id_type: str,
113 identifier: str,
114 tnauthlist_support: bool = False,
115) -> bool:
116 """validate identifier format"""
117 logger.debug("Helper.validate_identifier()")
118
119 result = False
120 if identifier:
121 if id_type == "dns":
122 result = validate_fqdn(logger, identifier)
123 elif id_type == "ip":
124 result = validate_ip(logger, identifier)
125 elif id_type == "email":
126 result = validate_email(logger, [identifier])
127 elif id_type == "tnauthlist" and tnauthlist_support:
128 result = True
129
130 logger.debug("Helper.validate_identifier() ended with: %s", result)
131 return result
132
133
134def validate_ip(logger: logging.Logger, ip: str, raw=False) -> ipaddress._BaseAddress:
135 """validate ip address"""
136 logger.debug("Helper.validate_ip()")
137 if raw:
138 return ipaddress.ip_address(ip)
139 result = False
140 try:
141 ipaddress.ip_address(ip)
142 result = True
143 except ValueError:
144 result = False
145 logger.debug("Helper.validate_ip() ended with: %s", result)
146 return result
147
148
149def validate_fqdn(logger: logging.Logger, fqdn: str) -> bool:
150 """validate fqdn"""
151 logger.debug("Helper.validate_fqdn()")
152
153 result = False
154 regex = r"^(([a-z0-9]\-*[a-z0-9]*){1,63}\.?){1,255}$"
155 p = re.compile(regex)
156 if re.search(p, fqdn):
157 result = True
158
159 if not result:
160 logger.debug(
161 "Helper.validate_fqdn(): invalid fqdn. Check for wildcard : %s", fqdn
162 )
163 regex = r"^\*\.[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
164 p = re.compile(regex)
165 if re.search(p, fqdn):
166 result = True
167
168 logger.debug("Helper.validate_fqdn() ended with: %s", result)
169 return result
170
171
172def validate_network(logger: logging.Logger, ip_network: str) -> ipaddress._BaseNetwork:
173 """check if an ip address is in a list of ip networks"""
174 logger.debug("Helper.validate_network(%s)", ip_network)
175 return ipaddress.ip_network(ip_network, strict=False)
176
177
178def ip_validate(logger: logging.Logger, ip_addr: str) -> Tuple[str, bool]:
179 """validate ip address"""
180 logger.debug("Helper.ip_validate(%s)", ip_addr)
181
182 try:
183 reverse_pointer = ipaddress.ip_address(ip_addr).reverse_pointer
184 invalid = False
185 except ValueError:
186 reverse_pointer = None
187 invalid = True
188 logger.debug("Helper.ip_validate() ended with: %s:%s", reverse_pointer, invalid)
189 return (reverse_pointer, invalid)
190
191
192def ipv6_chk(logger: logging.Logger, address: str) -> bool:
193 """check if an address is ipv6"""
194 logger.debug("Helper.ipv6_chk(%s)", address)
195
196 try:
197 # we need to set a host header and braces for ipv6 headers and
198 if isinstance(ipaddress.ip_address(address), ipaddress.IPv6Address):
199 logger.debug("Helper.v6_adjust(}): ipv6 address detected")
200 result = True
201 else:
202 result = False
203 except Exception:
204 result = False
205
206 logger.debug("Helper.ipv6_chk() ended with %s", result)
207 return result
208
209
210def cn_validate(logger: logging.Logger, cn: str) -> bool:
211 """validate common name"""
212 logger.debug("Helper.cn_validate(%s)", cn)
213
214 error = False
215 if cn:
216 # check if CN is a valid IP address
217 result = validate_ip(logger, cn)
218 if not result:
219 # check if CN is a valid fqdn
220 result = validate_fqdn(logger, cn)
221 if not result:
222 error = "Profile subject check failed: CN validation failed"
223 else:
224 error = "Profile subject check failed: commonName missing"
225
226 logger.debug("Helper.cn_validate() ended with: %s", error)
227 return error