RST updates and fixes

This commit is contained in:
Pinga 2025-05-12 11:20:23 +03:00
parent 2515b8c6df
commit 1c79be37a6
8 changed files with 231 additions and 49 deletions

View file

@ -61,7 +61,7 @@ class ApplicationsController extends Controller
$contactBilling = $data['contactBilling'] ?? null; $contactBilling = $data['contactBilling'] ?? null;
$phaseType = $data['phaseType'] ?? null; $phaseType = $data['phaseType'] ?? null;
$phaseName = $data['phaseName'] ?? null; $phaseName = isset($data['phaseName']) && trim($data['phaseName']) !== '' ? $data['phaseName'] : null;
$nameservers = !empty($data['nameserver']) ? $data['nameserver'] : null; $nameservers = !empty($data['nameserver']) ? $data['nameserver'] : null;
$nameserver_ipv4 = !empty($data['nameserver_ipv4']) ? $data['nameserver_ipv4'] : null; $nameserver_ipv4 = !empty($data['nameserver_ipv4']) ? $data['nameserver_ipv4'] : null;
@ -74,6 +74,7 @@ class ApplicationsController extends Controller
$smdFile = $uploadedFiles['smd'] ?? null; $smdFile = $uploadedFiles['smd'] ?? null;
$smd = null; $smd = null;
if ($phaseType === 'sunrise' || $phaseType === 'landrush') {
if ($smdFile && $smdFile->getError() === UPLOAD_ERR_OK) { if ($smdFile && $smdFile->getError() === UPLOAD_ERR_OK) {
$smd = $smdFile->getStream()->getContents(); $smd = $smdFile->getStream()->getContents();
$smd_filename = $smdFile->getClientFilename(); $smd_filename = $smdFile->getClientFilename();
@ -86,6 +87,7 @@ class ApplicationsController extends Controller
$this->container->get('flash')->addMessage('error', 'SMD file upload failed'); $this->container->get('flash')->addMessage('error', 'SMD file upload failed');
return $response->withHeader('Location', '/application/create')->withStatus(302); return $response->withHeader('Location', '/application/create')->withStatus(302);
} }
}
if ($phaseType === 'custom' && empty($phaseName)) { if ($phaseType === 'custom' && empty($phaseName)) {
$this->container->get('flash')->addMessage('error', 'Please provide a phase name when selecting the "Custom" phase type.'); $this->container->get('flash')->addMessage('error', 'Please provide a phase name when selecting the "Custom" phase type.');
@ -141,6 +143,10 @@ class ApplicationsController extends Controller
[$tld_id, $phaseType, $currentDate, $currentDate] [$tld_id, $phaseType, $currentDate, $currentDate]
); );
$noticeid = null;
$notafter = null;
$accepted = null;
if ($phase_details !== 'Application') { if ($phase_details !== 'Application') {
$this->container->get('flash')->addMessage('error', 'Error creating application: The launch phase ' . $phaseType . ' is improperly configured. Please check the settings or contact support.'); $this->container->get('flash')->addMessage('error', 'Error creating application: The launch phase ' . $phaseType . ' is improperly configured. Please check the settings or contact support.');
return $response->withHeader('Location', '/application/create')->withStatus(302); return $response->withHeader('Location', '/application/create')->withStatus(302);
@ -157,10 +163,30 @@ class ApplicationsController extends Controller
$noticeid = $data['noticeid']; $noticeid = $data['noticeid'];
$notafter = $data['notafter']; $notafter = $data['notafter'];
$accepted = $data['accepted']; $accepted = $data['accepted'];
} else {
$noticeid = null; // Validate that acceptedDate is before notAfter
$notafter = null; try {
$accepted = null; $acceptedDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['accepted']);
$notAfterDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['notafter']);
if (!$acceptedDate || !$notAfterDate) {
$this->container->get('flash')->addMessage('error', "Invalid date format");
return $response->withHeader('Location', '/application/create')->withStatus(302);
}
if ($acceptedDate >= $notAfterDate) {
$this->container->get('flash')->addMessage('error', "Invalid dates: acceptedDate must be before notAfter");
return $response->withHeader('Location', '/application/create')->withStatus(302);
}
} catch (Exception $e) {
$this->container->get('flash')->addMessage('error', "Invalid date format");
return $response->withHeader('Location', '/application/create')->withStatus(302);
}
if (!validateTcnId($domainName, $noticeid, $data['notafter'])) {
$this->container->get('flash')->addMessage('error', "Invalid TMCH claims noticeID format");
return $response->withHeader('Location', '/application/create')->withStatus(302);
}
} }
if ($phaseType === 'sunrise') { if ($phaseType === 'sunrise') {
@ -192,6 +218,28 @@ class ApplicationsController extends Controller
chunk_split($certBase64, 64, "\n") . chunk_split($certBase64, 64, "\n") .
"-----END CERTIFICATE-----\n"; "-----END CERTIFICATE-----\n";
// Load the SMD certificate
$x509 = new \phpseclib3\File\X509();
$cert = $x509->loadX509($certPem);
$serial = strtoupper($cert['tbsCertificate']['serialNumber']->toHex()); // serial as hex
// Get latest CRL from DB
$crlDer = $db->selectValue('SELECT content FROM tmch_crl ORDER BY update_timestamp DESC LIMIT 1');
// Load and parse the CRL
$crl = new \phpseclib3\File\X509();
$crlData = $crl->loadCRL($crlDer);
// Check revoked serials
$revoked = $crlData['tbsCertList']['revokedCertificates'] ?? [];
foreach ($revoked as $entry) {
$revokedSerial = strtoupper($entry['userCertificate']->toHex());
if ($revokedSerial === $serial) {
$this->container->get('flash')->addMessage('error', 'Error creating application: SMD certificate has been revoked.');
return $response->withHeader('Location', '/application/create')->withStatus(302);
}
}
$notBefore = new \DateTime($xpath->evaluate('string(//smd:notBefore)')); $notBefore = new \DateTime($xpath->evaluate('string(//smd:notBefore)'));
$notafter = new \DateTime($xpath->evaluate('string(//smd:notAfter)')); $notafter = new \DateTime($xpath->evaluate('string(//smd:notAfter)'));
$markName = $xpath->evaluate('string(//mark:markName)'); $markName = $xpath->evaluate('string(//mark:markName)');

View file

@ -144,7 +144,7 @@ class DomainsController extends Controller
$contactBilling = $data['contactBilling'] ?? null; $contactBilling = $data['contactBilling'] ?? null;
$phaseType = $data['phaseType'] ?? 'none'; $phaseType = $data['phaseType'] ?? 'none';
$phaseName = $data['phaseName'] ?? null; $phaseName = isset($data['phaseName']) && trim($data['phaseName']) !== '' ? $data['phaseName'] : null;
$token = $data['token'] ?? null; $token = $data['token'] ?? null;
@ -169,6 +169,7 @@ class DomainsController extends Controller
$smdFile = $uploadedFiles['smd'] ?? null; $smdFile = $uploadedFiles['smd'] ?? null;
$smd = null; $smd = null;
if ($phaseType === 'sunrise' || $phaseType === 'landrush') {
if ($smdFile && $smdFile->getError() === UPLOAD_ERR_OK) { if ($smdFile && $smdFile->getError() === UPLOAD_ERR_OK) {
$smd = $smdFile->getStream()->getContents(); $smd = $smdFile->getStream()->getContents();
$smd_filename = $smdFile->getClientFilename(); $smd_filename = $smdFile->getClientFilename();
@ -181,6 +182,7 @@ class DomainsController extends Controller
$this->container->get('flash')->addMessage('error', 'SMD file upload failed'); $this->container->get('flash')->addMessage('error', 'SMD file upload failed');
return $response->withHeader('Location', '/domain/create')->withStatus(302); return $response->withHeader('Location', '/domain/create')->withStatus(302);
} }
}
if ($invalid_domain) { if ($invalid_domain) {
$this->container->get('flash')->addMessage('error', 'Error creating domain: Invalid domain name'); $this->container->get('flash')->addMessage('error', 'Error creating domain: Invalid domain name');
@ -230,6 +232,10 @@ class DomainsController extends Controller
[$tld_id, $currentDate, $currentDate] [$tld_id, $currentDate, $currentDate]
); );
$noticeid = null;
$notafter = null;
$accepted = null;
// Check if the phase requires application submission // Check if the phase requires application submission
if ($phase_details && $phase_details === 'Application') { if ($phase_details && $phase_details === 'Application') {
$this->container->get('flash')->addMessage('error', 'Domain registration is not allowed for this TLD. You must submit a new application instead.'); $this->container->get('flash')->addMessage('error', 'Domain registration is not allowed for this TLD. You must submit a new application instead.');
@ -267,10 +273,30 @@ class DomainsController extends Controller
$noticeid = $data['noticeid']; $noticeid = $data['noticeid'];
$notafter = $data['notafter']; $notafter = $data['notafter'];
$accepted = $data['accepted']; $accepted = $data['accepted'];
} else {
$noticeid = null; // Validate that acceptedDate is before notAfter
$notafter = null; try {
$accepted = null; $acceptedDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['accepted']);
$notAfterDate = DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['notafter']);
if (!$acceptedDate || !$notAfterDate) {
$this->container->get('flash')->addMessage('error', "Invalid date format");
return $response->withHeader('Location', '/domain/create')->withStatus(302);
}
if ($acceptedDate >= $notAfterDate) {
$this->container->get('flash')->addMessage('error', "Invalid dates: acceptedDate must be before notAfter");
return $response->withHeader('Location', '/domain/create')->withStatus(302);
}
} catch (Exception $e) {
$this->container->get('flash')->addMessage('error', "Invalid date format");
return $response->withHeader('Location', '/domain/create')->withStatus(302);
}
if (!validateTcnId($domainName, $noticeid, $data['notafter'])) {
$this->container->get('flash')->addMessage('error', "Invalid TMCH claims noticeID format");
return $response->withHeader('Location', '/domain/create')->withStatus(302);
}
} }
if ($phaseType === 'sunrise') { if ($phaseType === 'sunrise') {
@ -302,6 +328,28 @@ class DomainsController extends Controller
chunk_split($certBase64, 64, "\n") . chunk_split($certBase64, 64, "\n") .
"-----END CERTIFICATE-----\n"; "-----END CERTIFICATE-----\n";
// Load the SMD certificate
$x509 = new \phpseclib3\File\X509();
$cert = $x509->loadX509($certPem);
$serial = strtoupper($cert['tbsCertificate']['serialNumber']->toHex()); // serial as hex
// Get latest CRL from DB
$crlDer = $db->selectValue('SELECT content FROM tmch_crl ORDER BY update_timestamp DESC LIMIT 1');
// Load and parse the CRL
$crl = new \phpseclib3\File\X509();
$crlData = $crl->loadCRL($crlDer);
// Check revoked serials
$revoked = $crlData['tbsCertList']['revokedCertificates'] ?? [];
foreach ($revoked as $entry) {
$revokedSerial = strtoupper($entry['userCertificate']->toHex());
if ($revokedSerial === $serial) {
$this->container->get('flash')->addMessage('error', 'Error creating domain: SMD certificate has been revoked');
return $response->withHeader('Location', '/domain/create')->withStatus(302);
}
}
$notBefore = new \DateTime($xpath->evaluate('string(//smd:notBefore)')); $notBefore = new \DateTime($xpath->evaluate('string(//smd:notBefore)'));
$notafter = new \DateTime($xpath->evaluate('string(//smd:notAfter)')); $notafter = new \DateTime($xpath->evaluate('string(//smd:notAfter)'));
$markName = $xpath->evaluate('string(//mark:markName)'); $markName = $xpath->evaluate('string(//mark:markName)');
@ -312,14 +360,14 @@ class DomainsController extends Controller
} }
if (!in_array($label, $labels)) { if (!in_array($label, $labels)) {
$this->container->get('flash')->addMessage('error', 'Error creating domain: SMD file is not valid for the domain name being registered.'); $this->container->get('flash')->addMessage('error', 'Error creating domain: SMD file is not valid for the domain name being registered');
return $response->withHeader('Location', '/domain/create')->withStatus(302); return $response->withHeader('Location', '/domain/create')->withStatus(302);
} }
// Check if current date and time is between notBefore and notAfter // Check if current date and time is between notBefore and notAfter
$now = new \DateTime(); $now = new \DateTime();
if (!($now >= $notBefore && $now <= $notafter)) { if (!($now >= $notBefore && $now <= $notafter)) {
$this->container->get('flash')->addMessage('error', 'Error creating domain: Current time is outside the valid range in the SMD.'); $this->container->get('flash')->addMessage('error', 'Error creating domain: Current time is outside the valid range in the SMD');
return $response->withHeader('Location', '/domain/create')->withStatus(302); return $response->withHeader('Location', '/domain/create')->withStatus(302);
} }
@ -339,11 +387,11 @@ class DomainsController extends Controller
$accepted = (new \DateTime())->format('Y-m-d H:i:s.v'); $accepted = (new \DateTime())->format('Y-m-d H:i:s.v');
if (!$dsig->verify($key, $signatureNode)) { if (!$dsig->verify($key, $signatureNode)) {
$this->container->get('flash')->addMessage('error', 'Error creating domain: The XML signature of the SMD file is not valid.'); $this->container->get('flash')->addMessage('error', 'Error creating domain: The XML signature of the SMD file is not valid');
return $response->withHeader('Location', '/domain/create')->withStatus(302); return $response->withHeader('Location', '/domain/create')->withStatus(302);
} }
} else { } else {
$this->container->get('flash')->addMessage('error', "Error creating domain: SMD upload is required in the 'sunrise' phase."); $this->container->get('flash')->addMessage('error', "Error creating domain: SMD upload is required in the 'sunrise' phase");
return $response->withHeader('Location', '/domain/create')->withStatus(302); return $response->withHeader('Location', '/domain/create')->withStatus(302);
} }
} }

View file

@ -1162,3 +1162,33 @@ function getClid($db, string $clid): ?int {
$result = $db->selectValue('SELECT id FROM registrar WHERE clid = ? LIMIT 1', [$clid]); $result = $db->selectValue('SELECT id FROM registrar WHERE clid = ? LIMIT 1', [$clid]);
return $result !== false ? (int)$result : null; return $result !== false ? (int)$result : null;
} }
function validateTcnId(string $domain, string $noticeId, string $notAfterUtc): bool
{
// Ensure ID is at least 9 chars (8 for checksum + 1 for notice number)
if (strlen($noticeId) < 9) return false;
$tcnChecksum = substr($noticeId, 0, 8); // First 8 hex chars
$noticeNumber = substr($noticeId, 8); // Rest is TMDB Notice Identifier
// Validate numeric part
if (!ctype_digit($noticeNumber)) return false;
// Convert domain to ASCII and get leftmost label
$asciiDomain = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
$leftmostLabel = explode('.', $asciiDomain)[0];
// Convert notAfter to Unix time
$notAfterTimestamp = strtotime($notAfterUtc);
if ($notAfterTimestamp === false) return false;
// Build the checksum input string
$input = $leftmostLabel . $notAfterTimestamp . $noticeNumber;
// Compute CRC32 as unsigned int, then format as 8-digit lowercase hex
$crc32 = hash('crc32b', $input);
$crc32Hex = str_pad(strtolower($crc32), 8, '0', STR_PAD_LEFT);
// Compare
return hash_equals($tcnChecksum, $crc32Hex);
}

View file

@ -50,7 +50,8 @@
"moneyphp/money": "^4.6", "moneyphp/money": "^4.6",
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"filips123/monolog-phpmailer": "^2.0", "filips123/monolog-phpmailer": "^2.0",
"robrichards/xmlseclibs": "^3.1" "robrichards/xmlseclibs": "^3.1",
"phpseclib/phpseclib": "^3.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View file

@ -10,6 +10,7 @@
"phpmailer/phpmailer": "^6.9", "phpmailer/phpmailer": "^6.9",
"filips123/monolog-phpmailer": "^2.0", "filips123/monolog-phpmailer": "^2.0",
"ramsey/uuid": "^4.7", "ramsey/uuid": "^4.7",
"robrichards/xmlseclibs": "^3.1" "robrichards/xmlseclibs": "^3.1",
"phpseclib/phpseclib": "^3.0"
} }
} }

View file

@ -88,7 +88,7 @@ function processContactCreate($conn, $db, $xml, $clid, $database_type, $trans) {
if ($postalInfoIntStreet1) { if ($postalInfoIntStreet1) {
if ( if (
preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet1) || preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet1) ||
!preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet1) || !preg_match('/^[a-zA-Z0-9\'\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet1) ||
strlen($postalInfoIntStreet1) > 255 strlen($postalInfoIntStreet1) > 255
) { ) {
sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans); sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans);
@ -99,7 +99,7 @@ function processContactCreate($conn, $db, $xml, $clid, $database_type, $trans) {
if ($postalInfoIntStreet2) { if ($postalInfoIntStreet2) {
if ( if (
preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet2) || preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet2) ||
!preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet2) || !preg_match('/^[a-zA-Z0-9\'\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet2) ||
strlen($postalInfoIntStreet2) > 255 strlen($postalInfoIntStreet2) > 255
) { ) {
sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans); sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans);
@ -110,7 +110,7 @@ function processContactCreate($conn, $db, $xml, $clid, $database_type, $trans) {
if ($postalInfoIntStreet3) { if ($postalInfoIntStreet3) {
if ( if (
preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet3) || preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet3) ||
!preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet3) || !preg_match('/^[a-zA-Z0-9\'\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet3) ||
strlen($postalInfoIntStreet3) > 255 strlen($postalInfoIntStreet3) > 255
) { ) {
sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans); sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans);
@ -690,8 +690,8 @@ function processDomainCreate($conn, $db, $xml, $clid, $database_type, $trans, $m
return; return;
} }
if (strlen($noticeid) !== 27 || !ctype_alnum($noticeid)) { if (!validateTcnId($domainName, $noticeid, $launch_notAfter)) {
sendEppError($conn, $db, 2306, 'Invalid noticeID format', $clTRID, $trans); sendEppError($conn, $db, 2306, 'Invalid TMCH claims noticeID format', $clTRID, $trans);
} }
} elseif ($launch_phase === 'landrush') { } elseif ($launch_phase === 'landrush') {
// Continue // Continue
@ -825,6 +825,30 @@ function processDomainCreate($conn, $db, $xml, $clid, $database_type, $trans, $m
chunk_split($certBase64, 64, "\n") . chunk_split($certBase64, 64, "\n") .
"-----END CERTIFICATE-----\n"; "-----END CERTIFICATE-----\n";
// Load the SMD certificate
$x509 = new \phpseclib3\File\X509();
$cert = $x509->loadX509($certPem);
$serial = strtoupper($cert['tbsCertificate']['serialNumber']->toHex()); // serial as hex
// Get latest CRL from DB
$stmt = $db->query("SELECT content FROM tmch_crl ORDER BY update_timestamp DESC LIMIT 1");
$crlDer = $stmt->fetchColumn();
$stmt->closeCursor();
// Load and parse the CRL
$crl = new \phpseclib3\File\X509();
$crlData = $crl->loadCRL($crlDer);
// Check revoked serials
$revoked = $crlData['tbsCertList']['revokedCertificates'] ?? [];
foreach ($revoked as $entry) {
$revokedSerial = strtoupper($entry['userCertificate']->toHex());
if ($revokedSerial === $serial) {
sendEppError($conn, $db, 2306, 'Error creating domain: SMD certificate has been revoked.', $clTRID, $trans);
return;
}
}
$notBefore = new \DateTime($xpath->evaluate('string(//smd:notBefore)')); $notBefore = new \DateTime($xpath->evaluate('string(//smd:notBefore)'));
$notafter = new \DateTime($xpath->evaluate('string(//smd:notAfter)')); $notafter = new \DateTime($xpath->evaluate('string(//smd:notAfter)'));
$markName = $xpath->evaluate('string(//mark:markName)'); $markName = $xpath->evaluate('string(//mark:markName)');

View file

@ -188,7 +188,7 @@ function processContactUpdate($conn, $db, $xml, $clid, $database_type, $trans) {
if ($postalInfoIntStreet1) { if ($postalInfoIntStreet1) {
if ( if (
preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet1) || preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet1) ||
!preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet1) || !preg_match('/^[a-zA-Z0-9\'\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet1) ||
strlen($postalInfoIntStreet1) > 255 strlen($postalInfoIntStreet1) > 255
) { ) {
sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans); sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans);
@ -199,7 +199,7 @@ function processContactUpdate($conn, $db, $xml, $clid, $database_type, $trans) {
if ($postalInfoIntStreet2) { if ($postalInfoIntStreet2) {
if ( if (
preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet2) || preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet2) ||
!preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet2) || !preg_match('/^[a-zA-Z0-9\'\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet2) ||
strlen($postalInfoIntStreet2) > 255 strlen($postalInfoIntStreet2) > 255
) { ) {
sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans); sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans);
@ -210,7 +210,7 @@ function processContactUpdate($conn, $db, $xml, $clid, $database_type, $trans) {
if ($postalInfoIntStreet3) { if ($postalInfoIntStreet3) {
if ( if (
preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet3) || preg_match('/(^\-)|(^\,)|(^\.)|(\-\-)|(\,\,)|(\.\.)|(\-$)/', $postalInfoIntStreet3) ||
!preg_match('/^[a-zA-Z0-9\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet3) || !preg_match('/^[a-zA-Z0-9\'\-\&\,\.\/\s]{5,}$/', $postalInfoIntStreet3) ||
strlen($postalInfoIntStreet3) > 255 strlen($postalInfoIntStreet3) > 255
) { ) {
sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans); sendEppError($conn, $db, 2005, 'Invalid contact:street', $clTRID, $trans);

View file

@ -1070,3 +1070,33 @@ function normalizeDatetime($input) {
$dt = DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $input); $dt = DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $input);
return $dt ? $dt->format('Y-m-d H:i:s.v') : null; return $dt ? $dt->format('Y-m-d H:i:s.v') : null;
} }
function validateTcnId(string $domain, string $noticeId, string $notAfterUtc): bool
{
// Ensure ID is at least 9 chars (8 for checksum + 1 for notice number)
if (strlen($noticeId) < 9) return false;
$tcnChecksum = substr($noticeId, 0, 8); // First 8 hex chars
$noticeNumber = substr($noticeId, 8); // Rest is TMDB Notice Identifier
// Validate numeric part
if (!ctype_digit($noticeNumber)) return false;
// Convert domain to ASCII and get leftmost label
$asciiDomain = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
$leftmostLabel = explode('.', $asciiDomain)[0];
// Convert notAfter to Unix time
$notAfterTimestamp = strtotime($notAfterUtc);
if ($notAfterTimestamp === false) return false;
// Build the checksum input string
$input = $leftmostLabel . $notAfterTimestamp . $noticeNumber;
// Compute CRC32 as unsigned int, then format as 8-digit lowercase hex
$crc32 = hash('crc32b', $input);
$crc32Hex = str_pad(strtolower($crc32), 8, '0', STR_PAD_LEFT);
// Compare
return hash_equals($tcnChecksum, $crc32Hex);
}