From 5640e0d05e2565961b5ed3d33d71005ca2f91b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Tue, 26 Aug 2025 16:18:29 +0200 Subject: [PATCH 1/2] feat: add dns_key table --- migrations/Version20250812002458.php | 35 ++++++++++ src/Config/DnsKey/Algorithm.php | 36 ++++++++++ src/Config/DnsKey/DigestType.php | 22 ++++++ src/Entity/DnsKey.php | 100 +++++++++++++++++++++++++++ src/Entity/Domain.php | 39 +++++++++++ src/Repository/DnsKeyRepository.php | 43 ++++++++++++ src/Service/RDAPService.php | 41 +++++++++++ 7 files changed, 316 insertions(+) create mode 100644 migrations/Version20250812002458.php create mode 100644 src/Config/DnsKey/Algorithm.php create mode 100644 src/Config/DnsKey/DigestType.php create mode 100644 src/Entity/DnsKey.php create mode 100644 src/Repository/DnsKeyRepository.php diff --git a/migrations/Version20250812002458.php b/migrations/Version20250812002458.php new file mode 100644 index 0000000..5d05d71 --- /dev/null +++ b/migrations/Version20250812002458.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE dns_key (algorithm INT NOT NULL, digest_type INT NOT NULL, key_tag BYTEA NOT NULL, digest BYTEA NOT NULL, domain_id VARCHAR(255) NOT NULL, PRIMARY KEY(algorithm, digest_type, key_tag, domain_id, digest))'); + $this->addSql('CREATE INDEX IDX_88A62EF2115F0EE5 ON dns_key (domain_id)'); + $this->addSql('ALTER TABLE dns_key ADD CONSTRAINT FK_88A62EF2115F0EE5 FOREIGN KEY (domain_id) REFERENCES domain (ldh_name) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE dns_key DROP CONSTRAINT FK_88A62EF2115F0EE5'); + $this->addSql('DROP TABLE dns_key'); + } +} diff --git a/src/Config/DnsKey/Algorithm.php b/src/Config/DnsKey/Algorithm.php new file mode 100644 index 0000000..c62d04a --- /dev/null +++ b/src/Config/DnsKey/Algorithm.php @@ -0,0 +1,36 @@ +algorithm; + } + + public function setAlgorithm(?Algorithm $algorithm): static + { + $this->algorithm = $algorithm; + + return $this; + } + + public function getDigestType(): ?DigestType + { + return $this->digestType; + } + + public function setDigestType(DigestType $digestType): static + { + $this->digestType = $digestType; + + return $this; + } + + public function getKeyTag() + { + return unpack('n', $this->keyTag)[1]; + } + + public function setKeyTag($keyTag): static + { + $this->keyTag = $keyTag; + + return $this; + } + + public function getDomain(): ?Domain + { + return $this->domain; + } + + public function setDomain(?Domain $domain): static + { + $this->domain = $domain; + + return $this; + } + + public function getDigest() + { + return strtoupper(bin2hex($this->digest)); + } + + public function setDigest($digest): static + { + $this->digest = $digest; + + return $this; + } +} diff --git a/src/Entity/Domain.php b/src/Entity/Domain.php index f4d837f..7511f8b 100644 --- a/src/Entity/Domain.php +++ b/src/Entity/Domain.php @@ -39,6 +39,7 @@ use Symfony\Component\Serializer\Attribute\SerializedName; 'nameserver-entity:nameserver', 'nameserver-entity:entity', 'tld:item', + 'ds:list', ], ], read: false @@ -124,6 +125,13 @@ class Domain #[Groups(['domain:item', 'domain:list'])] private ?bool $delegationSigned = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DnsKey::class, mappedBy: 'domain', orphanRemoval: true)] + #[Groups(['domain:item'])] + private Collection $dnsKey; + private const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value]; private const IMPORTANT_STATUS = [ 'redemption period', @@ -146,6 +154,7 @@ class Domain $this->createdAt = $this->updatedAt; $this->deleted = false; $this->domainStatuses = new ArrayCollection(); + $this->dnsKey = new ArrayCollection(); } public function getLdhName(): ?string @@ -594,4 +603,34 @@ class Domain $this->calculateDaysFromStatus($now), ]); } + + /** + * @return Collection + */ + public function getDnsKey(): Collection + { + return $this->dnsKey; + } + + public function addDnsKey(DnsKey $dnsKey): static + { + if (!$this->dnsKey->contains($dnsKey)) { + $this->dnsKey->add($dnsKey); + $dnsKey->setDomain($this); + } + + return $this; + } + + public function removeDnsKey(DnsKey $dnsKey): static + { + if ($this->dnsKey->removeElement($dnsKey)) { + // set the owning side to null (unless already changed) + if ($dnsKey->getDomain() === $this) { + $dnsKey->setDomain(null); + } + } + + return $this; + } } diff --git a/src/Repository/DnsKeyRepository.php b/src/Repository/DnsKeyRepository.php new file mode 100644 index 0000000..481f92e --- /dev/null +++ b/src/Repository/DnsKeyRepository.php @@ -0,0 +1,43 @@ + + */ +class DnsKeyRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, DnsKey::class); + } + + // /** + // * @return DnsKey[] Returns an array of DnsKey objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('d.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?DnsKey + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index c4d8288..bc17650 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -2,8 +2,11 @@ namespace App\Service; +use App\Config\DnsKey\Algorithm; +use App\Config\DnsKey\DigestType; use App\Config\EventAction; use App\Config\TldType; +use App\Entity\DnsKey; use App\Entity\Domain; use App\Entity\DomainEntity; use App\Entity\DomainEvent; @@ -31,6 +34,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; @@ -171,6 +175,7 @@ readonly class RDAPService $this->updateDomainEvents($domain, $rdapData); $this->updateDomainEntities($domain, $rdapData); $this->updateDomainNameservers($domain, $rdapData); + $this->updateDomainDsData($domain, $rdapData); $domain->setDeleted(false)->updateTimestamps(); @@ -625,6 +630,42 @@ readonly class RDAPService return $entity; } + private function updateDomainDsData(Domain $domain, array $rdapData): void + { + $domain->getDnsKey()->clear(); + $this->em->persist($domain); + $this->em->flush(); + + if (array_key_exists('secureDNS', $rdapData) && array_key_exists('dsData', $rdapData['secureDNS']) && is_array($rdapData['secureDNS']['dsData'])) { + foreach ($rdapData['secureDNS']['dsData'] as $rdapDsData) { + $dsData = new DnsKey(); + if (array_key_exists('keyTag', $rdapDsData)) { + $dsData->setKeyTag(pack('n', $rdapDsData['keyTag'])); + } + if (array_key_exists('algorithm', $rdapDsData)) { + $dsData->setAlgorithm(Algorithm::from($rdapDsData['algorithm'])); + } + if (array_key_exists('digest', $rdapDsData)) { + $blob = hex2bin($rdapDsData['digest']); + if (false === $blob) { + throw new ServiceUnavailableHttpException('DNSSEC digest is not a valid hexadecimal value.'); + } + $dsData->setDigest($blob); + } + if (array_key_exists('digestType', $rdapDsData)) { + $dsData->setDigestType(DigestType::from($rdapDsData['digestType'])); + } + + $domain->addDnsKey($dsData); + $this->em->persist($dsData); + } + } else { + $this->logger->warning('The domain name {idnDomain} has no DS record.', [ + 'idnDomain' => $domain->getLdhName(), + ]); + } + } + /** * @throws TransportExceptionInterface * @throws ServerExceptionInterface From c1fa5c571f00275f8ba1bd0ce894fa11e4cced65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Tue, 26 Aug 2025 16:34:37 +0200 Subject: [PATCH 2/2] feat: add domain_status when domain is deleted --- src/Service/RDAPService.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index c4d8288..270809a 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -268,7 +268,18 @@ readonly class RDAPService 'idnDomain' => $idnDomain, ]); - $domain->setDeleted(true)->updateTimestamps(); + $domain->updateTimestamps(); + + if (!$domain->getDeleted() && $domain->getUpdatedAt() !== $domain->getCreatedAt()) { + $this->em->persist((new DomainStatus()) + ->setDomain($domain) + ->setCreatedAt($domain->getUpdatedAt()) + ->setDate($domain->getUpdatedAt()) + ->setAddStatus([]) + ->setDeleteStatus($domain->getStatus())); + } + + $domain->setDeleted(true); $this->em->persist($domain); $this->em->flush(); }