Merge branch 'feat/dnssec'

This commit is contained in:
Maël Gangloff 2025-08-26 16:35:42 +02:00
commit fdb29903b1
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
7 changed files with 316 additions and 0 deletions

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250812002458 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add dns_key table';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Config\DnsKey;
/**
* @see https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
*/
enum Algorithm: int
{
case RSAMD5 = 1;
case DH = 2;
case DSA = 3;
// 4 RESERVED
case RSASHA1 = 5;
case DSA_NSEC3_SHA1 = 6;
case RSASHA1_NSEC3_SHA1 = 7;
case RSASHA256 = 8;
// 9 RESERVED
case RSASHA512 = 10;
// 11 RESERVED
case ECC_GOST = 12;
case ECDSAP256SHA256 = 13;
case ECDSAP384SHA384 = 14;
case ED25519 = 15;
case ED448 = 16;
case SM2SM3 = 17;
// 18-22 RESERVED
case ECC_GOST12 = 23;
// 24-122 UNASSIGNED
// 123-251 RESERVED
case INDIRECT = 252;
case PRIVATEDNS = 253;
case PRIVATEOID = 254;
case RESERVED_255 = 255;
// 255 RESERVED
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Config\DnsKey;
/**
* @see https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
*/
enum DigestType: int
{
case RESERVED = 0;
case SHA1 = 1;
case SHA256 = 2;
case GOST_R_34_11_94 = 3;
case SHA384 = 4;
case GOST_R_34_11_2012 = 5;
case SM3 = 6;
// 7-127 UNASSIGNED
// 128-252 RESERVED
// 253-254 RESERVED PRIVATE USE
// 254 UNASSIGNED
}

100
src/Entity/DnsKey.php Normal file
View File

@ -0,0 +1,100 @@
<?php
namespace App\Entity;
use App\Config\DnsKey\Algorithm;
use App\Config\DnsKey\DigestType;
use App\Repository\DnsKeyRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: DnsKeyRepository::class)]
class DnsKey
{
#[ORM\Column(nullable: true, enumType: Algorithm::class)]
#[Groups(['ds:list'])]
#[ORM\Id]
private ?Algorithm $algorithm;
#[ORM\Column(enumType: DigestType::class)]
#[Groups(['ds:list'])]
#[ORM\Id]
private ?DigestType $digestType;
#[ORM\Column(type: Types::BINARY)]
#[Groups(['ds:list'])]
#[ORM\Id]
private $keyTag;
#[ORM\ManyToOne(inversedBy: 'dnsKey')]
#[ORM\JoinColumn(referencedColumnName: 'ldh_name', nullable: false)]
#[Groups(['ds:list', 'ds:item'])]
#[ORM\Id]
private ?Domain $domain = null;
#[ORM\Column(type: Types::BLOB)]
#[Groups(['ds:list'])]
#[ORM\Id]
private $digest;
public function getAlgorithm(): ?Algorithm
{
return $this->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;
}
}

View File

@ -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<int, DnsKey>
*/
#[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<int, DnsKey>
*/
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;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\DnsKey;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<DnsKey>
*/
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()
// ;
// }
}

View File

@ -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();
@ -636,6 +641,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