Skip to content

Práctica hmailServer

Configuración del servidor Bind9 en Debian 13 Trixie

Section titled “Configuración del servidor Bind9 en Debian 13 Trixie”

Comenzaremos esta práctica por instalar un servidor Bind9 en un Debian,

Terminal window
sudo apt update -y && sudo apt upgrade -y && sudo apt install bind9 bind9-utils -y

Este comando lo que hace es primero actualizar los repositorios, después upgradear/actualizar los paquetes que se puedan actualizar y por último instalar bind9 y las utilidades del bind9 como nslookup, dig etc.

Ahora configuraremos el archivo named.conf.local, el cual pondremos el siguiente contenido:

Acto seguido debemos configurar el archivo de la zona y tendremos que poner el siguiente contenido:

Una vez hemos configurado la zona directa, configuraremos la zona inversa de la siguiente forma:

Una vez tenemos esto, reiniciamos el bind9, y con el comando nslookup o dig, comprobamos que realmente funciona, en mi caso voy a utilizar nslookup que es más sencillo de usar:

Terminal window
nslookup mail.josedaniel0375asixc.com 172.16.1.1 # Comprobar la zona directa
nslookup 172.16.1.2 172.16.1.1 # Comprobar la zona inversa

Una vez tenemos configurado el servidor Bind9, debemos configurar correctamente el servidor Windows para que sea capaz de resolver el dominio que hemos creado con el Bind9, para ello simplemente nos aseguraremos de que la configuración sea la correcta:

Configuramos la IP y el DNS para que apunte a la red interna, en mi caso el servidor DNS tiene la IP 172.16.1.1 entonces el DNS apuntará hacia esa IP y el servidor tendrá la IP 172.16.1.2. Comprobamos que podamos hacer ping y resuelva correctamente los dominios:

Acto seguido, instalaremos hmailserver, para ello debemos descargarlo desde la página oficial:

Una vez se ha descargado lo ejecutamos y nos saldrá lo siguiente:

La instalación es bastante sencilla, es todo siguiente siguiente instalar: Nos pedirá que especifiquemos una contraseña, la ponemos y le damos siguiente:

Aquí en la instalación si estás en la versión de Windows Server 2025 e intentas instalar el servidor hmailserver, seguramente no te funcionará y cuando te pida instalar el .NET 2.0 te saltará un error:

El error en cuestión es el siguiente:

Para solucionar el problema, debemos instalar la dependencia .NET 2.0, la cual está ya deprecada y no es recomendable usarla por sus problemas de seguridad, pero para realizar esta práctica, necesitamos instalar esa dependencia, para instalarla usaremos el siguiente comando:

Terminal window
DISM /Online /Enable-Feature /FeatureName:NetFx3 /All /LimitAccess /Source:D:\sources\sxs

Esperaremos a que se complete la acción y una vez se ha completado, nos dejará terminar la instalación de hmailserver:

Una vez instalado, se nos abrirá el hmailserver y le tendremos que dar a Connect y nos saltará un cuadro en el que tendremos que poner la contraseña que hemos puesto en el instalador:

Una vez hemos metido la contraseña y le hemos dado a OK, tendremos que añadir el dominio que hemos configurado previamente en el Bind9, normalmente cuando inicias sesión con las credenciales, te salta ya la pestaña de añadir dominio como se muestra en la siguiente captura:

Ahora le damos a añadir dominio, y escribimos nuestro dominio:

Una vez hemos hecho esto, según el enunciado de esta práctica debemos crear 2 usuarios:

  1. [email protected]
  2. [email protected]

Para configurar estos usuarios, simplemente en el menú lateral, nos dirigimos a Domains -> josedaniel0375asixc.com (en tu caso el dominio cambiará) y le damos a Accounts, damos click derecho encima de accounts y le damos a añadir cuenta:

Haremos el mismo procedimiento con el usuario2:

Acto seguido, según la práctica debemos configurar el local hostname, el cual se encuentra ubicado en el menú lateral Settings -> Protocols -> SMTP, nos saldrá dentro del SMTP varias pestañas, nosotros nos dirigimos a la que dice Delivery of e-mail y ponemos en el campo de local hostname el siguiente contenido, que será básicamente mail.josedaniel0375asixc.com:

Cabe mencionar que según el enunciado, nos pide también configurar el SMTP Relayer, cosa que no voy a hacer ya que no es tan simple como lo que dice el enunciado a día de hoy, para poder realizar este paso, requerimos de entrar en la consola de Google Cloud y crear una cuenta de servicio la cual tendremos que usar para autenticarnos contra Gmail para poder enviar correos a través de sus servidores.

Lo siguiente que nos pide es revisar el apartado de los IP Ranges, la cual en la práctica nos menciona que estas 2 IPs en el IP range son correctas ya que son las que nos permiten comunicarnos con el servidor de correo.

Ahora nos pide configurar el cortafuegos, el cual tendremos que configurar reglas para los siguientes protocolos:

  1. SMTP
  2. POP3
  3. IMAP

Del cual debemos incluir los puertos que van en texto plano y cifrados:

  1. SMTP: 25 (Plain text), 587 (Cifrado explícito), 465 (Cifrado implícito)
  2. POP3: 110 (Plain text), 995 (Cifrado implícito)
  3. IMAP: 143 (Plain text), 993 (Cifrado implícito)

Para configurar las reglas del cortafuegos, simplemente en el buscador de Windows escribimos Firewall, luego nos vamos a configuración avanzada y luego le damos click en Reglas de entrada y le damos a Nueva regla, cabe mencionar que haremos exactamente lo mismo con las reglas de salida:

Ahora configuraremos las reglas de la misma forma pero las de POP3:

Ahora configuraremos las reglas de la misma manera pero para IMAP:

Ahora para habilitar el servicio POP3 nos vamos en menú lateral Settings -> Protocols y nos aseguramos de que esté habilitado el POP3:

Para instalar el thunderbird actualizamos repositorios y ejecutamos el siguiente comando:

Terminal window
sudo apt update -y && sudo apt install thunderbird -y

Ahora abrimos el thunderbird:

Rellenamos los campos del formulario y si lo hemos hecho todo bien, nos tendría que salir en verde. Una vez le damos a continuar nos saldrá lo siguiente:

Ahora probamos que podemos enviar un correo y como podemos ver, podemos enviar un correo:

Por último antes de saltar a la seguridad, configuraremos los logs ya que los IP ranges que tenemos por defecto ya son correctos y no es necesario modificarlos, para habilitar el logging simplemente en el menú lateral nos vamos a Settings -> Logging y habilitamos los logs como se ve en la siguiente captura:

Para ver los logs simplemente le damos a Show logs y se nos abrirá la carpeta con los logs.

Primeramente nos pide habilitar una opción en deliver of e-mail, dicha opción se llama “Require authentication for deliveries” esta opción no se puede tocar ya que no te deja desde la configuración, concluyendo que esta opción viene habilitada por defecto y no se puede ni habilitar ni deshabilitar.

La foto del envío del correo:

Filtrado de IPs y llistas negras (Anti-spam)

Section titled “Filtrado de IPs y llistas negras (Anti-spam)”

Ahora habilitamos las DNS blacklists de la siguiente forma:

Hacemos lo mismo con la otra opción:

Ahora añadiremos nuestra red en el IP Ranges:

La opción de límites lo más cercano a poder limitar la cantidad de correos que envía una persona es en el Anti-spam, usando el spamassasin y configurar el spam threshold.

Ahora nos pide configurar el bloqueo de ficheros, para habilitarlo nos vamos a Anti-virus en el menú lateral y después nos vamos a la última pestaña que se llama Block attachments y la habilitamos, podemos poner extensiones de archivos adicionales, pero yo para mantenerlo simple y hacerlo rápido lo dejare por defecto tal y como viene:

Para la seguridad adicional, se nos pide primero crear un certificado SSL con OpenSSL, en mi caso como no dispongo de OpenSSL y no lo he instalado, he generado los certificados usando PowerShell, con el siguiente script:

Terminal window
New-Item -ItemType Directory -Force -Path "C:\certificado\cert" | Out-Null
Get-ChildItem "cert:\LocalMachine\My" | Where-Object { $_.Subject -match "josedaniel0375asixc" } | Remove-Item -ErrorAction SilentlyContinue
$cert = New-SelfSignedCertificate `
-DnsName "mail.josedaniel0375asixc.com" `
-CertStoreLocation "cert:\LocalMachine\My" `
-NotAfter (Get-Date).AddYears(2) `
-KeyExportPolicy Exportable `
-KeyAlgorithm RSA `
-KeyLength 2048
$pwd = ConvertTo-SecureString -String "hola123" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath "C:\certificado\cert\mail.pfx" -Password $pwd | Out-Null
function Write-AsciiNoBom([string]$path, [string]$content) {
$content = $content -replace "`r`n", "`n"
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $content, $enc)
}
$certB64 = [Convert]::ToBase64String($cert.RawData)
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("-----BEGIN CERTIFICATE-----")
for ($i = 0; $i -lt $certB64.Length; $i += 64) {
$len = [Math]::Min(64, $certB64.Length - $i)
[void]$sb.AppendLine($certB64.Substring($i, $len))
}
[void]$sb.Append("-----END CERTIFICATE-----")
Write-AsciiNoBom "C:\certificado\cert\mail.pem" $sb.ToString()
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
function Encode-Length([int]$len) {
if ($len -lt 0x80) { return ,([byte]$len) }
$tmp = @()
$v = $len
while ($v -gt 0) { $tmp = ,([byte]($v -band 0xFF)) + $tmp; $v = $v -shr 8 }
return ,([byte](0x80 -bor $tmp.Length)) + $tmp
}
function Encode-Integer([byte[]]$data) {
$i = 0
while ($i -lt ($data.Length - 1) -and $data[$i] -eq 0) { $i++ }
$data = $data[$i..($data.Length - 1)]
if ($data[0] -band 0x80) { $data = ,([byte]0) + $data }
return ,([byte]0x02) + (Encode-Length $data.Length) + $data
}
$p = $rsa.ExportParameters($true)
$body = @()
$body += Encode-Integer ([byte[]]@(0))
$body += Encode-Integer $p.Modulus
$body += Encode-Integer $p.Exponent
$body += Encode-Integer $p.D
$body += Encode-Integer $p.P
$body += Encode-Integer $p.Q
$body += Encode-Integer $p.DP
$body += Encode-Integer $p.DQ
$body += Encode-Integer $p.InverseQ
$body = [byte[]]$body
$der = [byte[]](,([byte]0x30) + (Encode-Length $body.Length) + $body)
$keyB64 = [Convert]::ToBase64String($der)
$sb2 = New-Object System.Text.StringBuilder
[void]$sb2.AppendLine("-----BEGIN RSA PRIVATE KEY-----")
for ($i = 0; $i -lt $keyB64.Length; $i += 64) {
$len = [Math]::Min(64, $keyB64.Length - $i)
[void]$sb2.AppendLine($keyB64.Substring($i, $len))
}
[void]$sb2.Append("-----END RSA PRIVATE KEY-----")
Write-AsciiNoBom "C:\certificado\cert\mail.key" $sb2.ToString()
Write-Host "Listo, archivos en C:\certificado\cert\"
Write-Host " mail.pem -> Certificate file"
Write-Host " mail.key -> Private key file"

Este script de powershell tiene como objetivo la generación completa de un certificado SSL autofirmado para hMailServer.

Un certificado SSL autofirmado es un certificado digital que no ha sido emitido por una Autoridad de Certificación (CA) de confianza pública, sino generado y firmado por el propio sistema.

El script está diseñado para ejecutarse en un entorno de Windows con PowerShell y no requiere herramientas externas. Al finalizar, produce 3 archivos:

  1. mail.pfx: Certificado completo con clave privada, protegido por contraseña y en formato PKCS#12.
  2. mail.pem: Certificado público en formato PEM.
  3. mail.key: Clave privada RSA en formato PEM (PKCS#1), necesaria para que el servidor pueda descifrar las comunicaciones.

El script primero crea la carpeta en la que se van a almacenar los certificados:

Terminal window
New-Item -ItemType Directory -Force -Path "C:\certificado\cert" | Out-Null

El parámetro -Force hace que el comando no falle si la carpeta ya existe. La redirección a Out-Null suprime cualquier salida por pantalla de este comando, manteniendo la consola limpia.

Acto seguido, el script busca y elimina cualquier certificado que ya exista en el almacenamiento del sistema con el mismo nombre, evitando duplicados:

Terminal window
Get-ChildItem "cert:\LocalMachine\My" | Where-Object { $_.Subject -match "josedaniel0375asixc" } | Remove-Item -ErrorAction SilentlyContinue

Este comando realiza tres operaciones encadenadas mediante pipes o tuberías (|):

  1. Get-ChildItem "cert:\LocalMachine\My": Obtiene todos los certificados instalados en el almacén de certificados de la máquina local (no del usuario).

  2. Where-Object { $_.Subject -match "josedaniel0375asixc" }: Filtra solo aquellos certificados cuyo campo Subject contiene el texto indicado. El operador -match usa expresiones regulares, aunque en este caso se usa como una simple búsqueda de subcadena.

  3. Remove-Item -ErrorAction SilentlyContinue: Elimina los certificados encontrados. El parámetro -ErrorAction SilentlyContinue evita que el script se detenga si no encuentra ningún certificado que borrar.

Seguidamente, generaremos el certificado, se usa el cmdlet nativo de PowerShell New-SelfSignedCertificate para generar un certificado SSL completo:

Terminal window
$cert = New-SelfSignedCertificate `
-DnsName "mail.josedaniel0375asixc.com" `
-CertStoreLocation "cert:\LocalMachine\My" `
-NotAfter (Get-Date).AddYears(2) `
-KeyExportPolicy Exportable `
-KeyAlgorithm RSA `
-KeyLength 2048

El resultado del cmdlet se almacena en la variable $cert, que se reutilizará en los pasos siguientes. Cada parámetro tiene un propósito específico:

  1. -DnsName mail.josedaniel0375asixc.com: Es el nombre del dominio para el qu se expide el certificado. Se incluirá en el campo Subject Alternative Name (SAN)

  2. -CertStoreLocation "cert:\LocalMachine\My": Ubicación del almacén donde se instalará el certificado. LocalMachine\My es el almacén personal de la máquina.

  3. -NotAfter (Get-Date).AddYears(2): Fecha de expiración del certificado, dos años desde el momento de la ejecución del script.

  4. -KeyExportPolicy Exportable: Permite que la clave privada pueda exportarse fuera del almacén de Windows. Sin esto no sería posible generar el archivo .key.

  5. -KeyAlgorithm RSA: Algoritmo de cifrado asimétrico utilizado. RSA es el estándar más ampliamente soportado.

  6. -KeyLength 2048: Longitud de la clave RSA en bits. 2048 es el mínimo recomendado actualmente.

Internamente, al ejecutar este cmdlet, Windows genera un par de claves RSA:

  1. Una clave pública
  2. Una clave privada

La clave privada queda almacenada de forma segura en el sistema de claves de Windows (CNG: Cryptography Next Generation), mientras que el certificado con la clave pública se instala en el almacenamiento especificado.

Seguidamente, la exportación al formato PFX. El formato PFX (también conocido como PKCS#12) es un contenedor binario que agrupa en un solo archivo tanto el certificado público como la clave privada, protegidos mediante una contraseña.

Terminal window
$pwd = ConvertTo-SecureString -String "hola123" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath "C:\certificado\cert\mail.pfx" -Password $pwd | Out-Null

PowerShell requiere que las contraseñas se pasen como objetos SecureString, como texto plano. El cmdlet ConvertTo-SecureString convierte la cadena “hola123” a ese formato seguro. Los parámetros -Force y -AsPlainText son necesarios para indicar explícitamente que se está convirtiendo texto en claro (algo que no es recomendable), aquí lo hacemos para simplificar el script.

Para la exportación, el cmdlet Export-PfxCertificate toma el objeto certificado almacenado en $cert, lo combina con su clave privada del almacén de Windows, y lo guarda en el archivo especificado protegido con la contraseña indicada.

El archivo PFX resultante puede importarse directamente en servidores Windows, en otros navegadores o convertirse a otros formatos usando herramientas como OpenSSL.

Ahora pasamos a la exportación certificado en formato PEM. El formato PEM (Privacy Enhanced Mail) es el estándar de facto en sistemas Unix/Linux. Es un archivo de texto que contiene los datos del certificado codificados en Base64, delimitados por cabeceras reconocibles.

Antes de exportar, en el script definimos una función auxiliar para escribir archivos de texto sin BOM (Byte Order Mark):

Terminal window
function Write-AsciiNoBom([string]$path, [string]$content) {
$content = $content -replace "`r`n", "`n"
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $content, $enc)
}

Esta función es importante por dos razones:

  1. Sin BOM: Windows añade por defecto una marca BOM al inicio de los archivos UTF-8. Esta marca (que son 3 bytes invisibles) puede causar errores de header too long.

  2. Saltos de línea Unix (LF): El reemplazo de \r\n por \n convierte los saltos de línea de Windows (CRLF) al formato Unix (LF), necesario para que hmailserver interprete correctamente el archivo.

Una vez hecho esto, pasamos a la construcción del archivo PEM. El certificado público se extrae de la propiedad RawData del objeto $cert, que contiene los bytes en formato DER (binario). Estos bytes se convierten a Base64 y se dividen en lineas de 64 caracteres exactamente, que es el ancho estándar del formato PEM:

Terminal window
$certB64 = [Convert]::ToBase64String($cert.RawData)
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("-----BEGIN CERTIFICATE-----")
for ($i = 0; $i -lt $certB64.Length; $i += 64) {
$len = [Math]::Min(64, $certB64.Length - $i)
[void]$sb.AppendLine($certB64.Substring($i, $len))
}
[void]$sb.Append("-----END CERTIFICATE-----")
Write-AsciiNoBom "C:\certificado\cert\mail.pem" $sb.ToString()

El resultado es un archivo de texto con la estructura típica PEM, reconocible por sus cabeceras -----BEGIN CERTIFICATE----- y -----END CERTIFICATE-----

Esta fase del script es la más compleja. PowerShell no dispone de un cmdlet nativo para exportar la clave privada en formato PEM, por lo que en el script lo implementamos manualmente siguiendo los estándares ASN.1 y DER.

Primeramente, obtenemos la clave privada:

Terminal window
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)

Esta llamada contiene el objeto RSA que contiene la clave privada asociada al certificado. A partir de él se pueden extraer todos los parámetros matemáticos que componen la clave.

El formato PKCS#1 (usado en el archivo .key) requiere codificar los datos en ASN.1/DER, que es una notación binaria estándar para representar estructuras de datos criptográficos. En el script implementamos dos funciones para esto:

  1. Función Encode-Length: Codifica la longitud de un campo según las reglas DER. Si la longitud es menor de 128, se representa como un solo byte. Si es mayor, se usan múltiples bytes con un byte de control que indica cuántos bytes de longitud siguen:
Terminal window
function Encode-Length([int]$len) {
if ($len -lt 0x80) { return ,([byte]$len) }
$tmp = @()
$v = $len
while ($v -gt 0) { $tmp = ,([byte]($v -band 0xFF)) + $tmp; $v = $v -shr 8 }
return ,([byte](0x80 -bor $tmp.Length)) + $tmp
}
  1. Función Encode-Integer: Codifica un número entero grande (big integer) según el tipo ASN.1 INTEGER (tag 0x02). Elimina los ceros iniciales innecesarios y, si el byte más significativo tiene el bit 7 activado (lo que indicaría un número negativo en complemento a dos), añade un byte 0x00 de relleno para mantener la interpretación positiva:
Terminal window
function Encode-Integer([byte[]]$data) {
$i = 0
while ($i -lt ($data.Length - 1) -and $data[$i] -eq 0) { $i++ }
$data = $data[$i..($data.Length - 1)]
if ($data[0] -band 0x80) { $data = ,([byte]0) + $data }
return ,([byte]0x02) + (Encode-Length $data.Length) + $data
}

Detallemos más sobre los parámetros RSA y la estructura PKCS#1. La clave privada RSA se compone de nueve parámetros matemáticos. En el script los extraemos y los codificamos uno a uno:

ParámetroNombre matemático
0 (versión)versionSiempre 0 para claves RSA de dos primos
Modulus (n)$n = p \times q$Producto de los dos números primos. Define el tamaño de la clave (2048 bits).
Exponent (e)$e$Exponente público. Habitualmente el valor 65537 (0x10001).
D$d$Exponente privado. La clave de descifrado principal.
P$p$Primer número primo grande, factor de $n$.
Q$q$Segundo número primo grande, factor de $n$.
DP$d \pmod{p-1}$Exponente privado reducido módulo $p-1$. Optimiza el descifrado con CRT.
DQ$d \pmod{q-1}$Exponente privado reducido módulo $q-1$. Optimiza el descifrado con CRT.
InverseQ$q^{-1} \pmod{p}$Inverso modular de $q$ respecto a $p$. Necesario para el Teorema Chino del Resto.
Terminal window
$p = $rsa.ExportParameters($true)
$body = @()
$body += Encode-Integer ([byte[]]@(0))
$body += Encode-Integer $p.Modulus
$body += Encode-Integer $p.Exponent
$body += Encode-Integer $p.D
$body += Encode-Integer $p.P
$body += Encode-Integer $p.Q
$body += Encode-Integer $p.DP
$body += Encode-Integer $p.DQ
$body += Encode-Integer $p.InverseQ

Todos estos parámetros se ensamblan en una estructura SEQUENCE de ASN.1 (tag 0x30), que es la envoltura estándar para una secuencia de campos en DER:

Terminal window
$body = [byte[]]$body
$der = [byte[]](,([byte]0x30) + (Encode-Length $body.Length) + $body)

La estructura DER resultante se convierte a Base64 y se formatea igual que el certificado. Líneas de 64 caracteres entre las cabeceras de formato PKCS#1:

Terminal window
$keyB64 = [Convert]::ToBase64String($der)
$sb2 = New-Object System.Text.StringBuilder
[void]$sb2.AppendLine("-----BEGIN RSA PRIVATE KEY-----")
for ($i = 0; $i -lt $keyB64.Length; $i += 64) {
$len = [Math]::Min(64, $keyB64.Length - $i)
[void]$sb2.AppendLine($keyB64.Substring($i, $len))
}
[void]$sb2.Append("-----END RSA PRIVATE KEY-----")
Write-AsciiNoBom "C:\certificado\cert\mail.key" $sb2.ToString()

La cabecera BEGIN RSA PRIVATE KEY indica específicamente que se trata del formato PKCS#1 (a diferencia de BEGIN PRIVATE KEY), que correspondería a PKCS#8, un formato más moderno y genérico.

Una vez tenemos esto, comprobamos que realmente se han creado los certificados en la carpeta que hemos especificado en el script:

Acto seguido, en el sidebar nos dirigimos a Settings -> Advanced -> SSL Certificates. Crearemos uno especificando la ruta donde hemos generado los certificados autofirmados:

Le damos a Save y una vez hemos hecho esto, nos tenemos que dirigir dentro del mismo apartado de Advanced, pero ahora irnos a TCP/IP ports y habilitar el puerto 465 y 993:

Dentro de los puertos debemos específicar que queremos utilizar SSL/TLS y especificaremos el certificado que hemos generado anteriormente:

Algo muy importante, dentro del apartado de SSL/TLS debemos habilitar la opción TLS v1.3:

Una vez tenemos esto hecho, tendremos que configurar los clientes para que usen SSL/TLS:

Dentro de Account Settings en Server Settings nos debemos asegurar de que Connection Security esté en SSL/TLS en ambas cuentas. Después en Outgoing Server o Servidor saliente (SMTP) debemos asegurarnos de lo mismo, que esté Connection Security en SSL/TLS:

Por último comprobamos que funciona correctamente, enviando un correo de un usuario al otro:

Para la configuración del DKIM usaremos otro script también:

Este script de PowerShell genera un par de claves criptográficas RSA de 2048 bits destinadas a implementar DKIM (DomainKeys Identified Mail) en hmailserver. A diferencia del script de generación de certificados SSL, este script no crea ningún certificado X.509 ni lo instala en el almacén de Windows. El único propósito es producir los dos archivos necesarios para configurar la firma DKIM.

Antes que nada, debemos entender lo que es DKIM. DKIM es un mecanismo de autenticación de correo electrónico definido en el RFC 6376. Permite que un servidor de correo firme digitalmente cada mensaje saliente, de forma que el servidor receptor pueda verificar que el mensaje fue enviado realmente por el dominio declarado y que su contenido no ha sido alterado en tránsito.

El funcionamiento se basa en criptografía asimétrica:

  1. Clave privada: Reside en el servidor de correo y se usa para firmar cada mensaje saliente, nunca debe salir del servidor.
  2. Clave pública: Se publica en el DNS del dominio como un registro TXT. Cualquier servidor receptor puede consultarla para verificar la firma del mensaje.

El selector es un identificador que permite tener múltiples claves DKIM activas para un mismo dominio, por ejemplo para distinguir entre distintos servidores o rotar claves sin interrumpir el servicio. El registro DNS resultante tendrá el nombre dkim._domainkey.josedaniel0375asixc.com

DKIM es uno de los tres pilares de la autenticación de correo moderno, junto con SPF (Sender Policy Framework) y DMARC (Domain-based Message Authentication, Reporting and conformance). Sin DKIM configurado correctamente, los mensajes del dominio tienen mayor probabilidad de ser clasificados como spam por los servidores receptores.

Empecemos explicando la fase 1 del script, la configuración inicial y preparación del entorno antes de ejecutar el script:

Terminal window
$selector = "dkim"
$dominio = "josedaniel0375asixc.com"
$carpeta = "C:\certificado\dkim"
  1. $selector = "dkim": Es el identificador de la clave DKIM. Se usará como prefijo del registro DNS.
  2. $dominio = "josedaniel0375asixc.com": Dominio para el que se genera la clave. Aparece en el nombre del registro DNS y en la salida por pantalla.
  3. $carpeta = "C:\certificado\dkim": Directorio donde se guardarán los dos archivos generados.

Al igual que en el otro script, creamos el directorio sino existe añadiendo los 2 parámetros -Force para que no falle en caso de que el directorio exista y el Out-Null para no mostrar nada por pantalla.

Terminal window
New-Item -ItemType Directory -Force -Path $carpeta | Out-Null

Ahora entramos en la fase 2, la generación del par de claves RSA. La generación del par de claves se realiza directamente en memoria, sin implicar al almacén de certificados de Windows:

Terminal window
$rsa = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048)
$p = $rsa.ExportParameters($true)

La diferencia entre RSACryptoServiceProvider y New-SelfSignedCertificate radica en que New-SelfSignedCertificate crea un certificado X.509 completo y almacenarlo en Windows, el script para DKIM usa RSACryptoServiceProvider del framework .NET. El resultado es un objeto RSA puro en memoria, sin ningún certificado asociado y sin instalación en el sistema.

Acto seguido, la exportación de los parámetros. La llamada ExportParameters($true) devuelve un objeto RSAParameters con todos los componentes matemáticos de la clave. El argumento $true indica que se deben incluir los parámetros privados (si fuese $false solo se exportaría la clave pública). El objeto resultante se almacena en la variable $p y contiene los nueve campos que definen completamente una clave RSA:

ParámetroDescripción
ModulusMódulo $n = p \times q$. Define el tamaño de la clave (256 bytes para RSA-2048).
ExponentExponente público $e$. Habitualmente el valor 65537 (0x10001).
DExponente privado $d$. Componente principal del descifrado.
PPrimer factor primo de $n$.
QSegundo factor primo de $n$.
DP$d \pmod{p-1}$. Optimización CRT para el descifrado.
DQ$d \pmod{q-1}$. Optimización CRT para el descifrado.
InverseQ$q^{-1} \pmod{p}$. Inverso modular necesario para el Teorema Chino del Resto.

Entramos en la fase 3 del script, las funciones auxiliares de codificación. En el script definimos cinco funciones que implementan las primitivas necesarias para construir estructuras binarias en formato ASN.1/DER y convertirlas al formato PEM. Estas funciones son la base sobre la que se construyen tanto la clave privada como la clave pública.

  1. Write-AsciiNoBom: Como ya hemos explicado anteriormente, esta función escribe un archivo de texto en UTF-8 sin BOM y con saltos de línea Unix (LF).
Terminal window
function Write-AsciiNoBom([string]$path, [string]$content) {
$content = $content -replace "`r`n", "`n"
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $content, $enc)
}
  1. Encode-Length: Como ya hemos explicado anteriormente, esta función codifica una longitud según las reglas DER. Las longitudes menores de 128 (0x80) se representan en un único byte. Para longitudes mayores se usa codificación multibyte, el primer byte tiene el bit 7 activado (0x80) y los bits 0-6 indican cuántos bytes de longitud siguen a continuación.
Terminal window
function Encode-Length([int]$len) {
if ($len -lt 0x80) { return ,([byte]$len) }
$tmp = @()
$v = $len
while ($v -gt 0) { $tmp = ,([byte]($v -band 0xFF)) + $tmp; $v = $v -shr 8 }
return ,([byte](0x80 -bor $tmp.Length)) + $tmp
}
  1. Encode-Integer: Codifica un número entero grande como tipo ASN.1 INTEGER (tag 0x02). Primero elimina los bytes cero iniciales innecesarios. Después comprueba si el byte más significativo tiene el bit 7 a 1: en DER, un byte con ese bit activo se interpretaría como número negativo (complemento a dos), por lo que en ese caso se antepone un byte 0x00 para preservar la interpretación positiva.
Terminal window
function Encode-Integer([byte[]]$data) {
$i = 0
while ($i -lt ($data.Length - 1) -and $data[$i] -eq 0) { $i++ }
$data = $data[$i..($data.Length - 1)]
if ($data[0] -band 0x80) { $data = ,([byte]0) + $data }
return ,([byte]0x02) + (Encode-Length $data.Length) + $data
}
  1. Encode-Sequence: Envuelve un bloque de bytes en una estructura ASN.1 SEQUENCE (tag 0x30). Es la envoltura estándar para agrupar una serie de campos en DER. Se utiliza tanto para construir la clave privada PKCS#1 como para la clave pública SPKI.
Terminal window
function Encode-Sequence([byte[]]$content) {
return ,([byte]0x30) + (Encode-Length $content.Length) + $content
}
  1. Encode-BitString: Codifica datos como tipo ASN.1 BIT STRING (tag 0x03). El primer byte del contenido debe ser siempre 0x00 para indicar que no hay bits de relleno al final. Se usa específicamente para envolver la clave pública dentro de la estructura SPKI, tal como exige el formato SubjectPublicKeyInfo definido en el RFC 5480.
Terminal window
function Encode-BitString([byte[]]$content) {
$data = ,([byte]0) + $content
return ,([byte]0x03) + (Encode-Length $data.Length) + $data
}
  1. To-Pem: Convierte un array de bytes DER al formato PEM: codifica en Base64, lo divide en líneas de 64 caracteres y lo rodea con las cabeceras estándar. Acepta un parámetro $header para poder usarse tanto con “RSA PRIVATE KEY” como con cualquier otro tipo. Esta función es una versión más limpia y reutilizable de la lógica que en el script SSL estaba repetida para cada archivo.
Terminal window
function To-Pem([string]$header, [byte[]]$der) {
$b64 = [Convert]::ToBase64String($der)
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("-----BEGIN $header-----")
for ($i = 0; $i -lt $b64.Length; $i += 64) {
$len = [Math]::Min(64, $b64.Length - $i)
[void]$sb.AppendLine($b64.Substring($i, $len))
}
[void]$sb.Append("-----END $header-----")
return $sb.ToString()
}

Ahora pasamos a la fase 4, la construcción y exportación de la clave privada (PKCS#1). La clave privada se construye ensamblando los nueve parámetros RSA en una estructura SEQUENCE de ASN.1, siguiendo el estándar PKCS#1 (RFC 8017):

Terminal window
$pkcs1 = @()
$pkcs1 += Encode-Integer ([byte[]]@(0))
$pkcs1 += Encode-Integer $p.Modulus
$pkcs1 += Encode-Integer $p.Exponent
$pkcs1 += Encode-Integer $p.D
$pkcs1 += Encode-Integer $p.P
$pkcs1 += Encode-Integer $p.Q
$pkcs1 += Encode-Integer $p.DP
$pkcs1 += Encode-Integer $p.DQ
$pkcs1 += Encode-Integer $p.InverseQ
$pkcs1Der = [byte[]](Encode-Sequence ([byte[]]$pkcs1))
$privPem = To-Pem "RSA PRIVATE KEY" $pkcs1Der
Write-AsciiNoBom "$carpeta\dkim_private.pem" $privPem

El primer campo codificado es el entero 0, que corresponde al campo “version” del formato PKCS#1 RSAPrivateKey. Siempre vale 0 para claves de dos primos (la variante estándar). Los ocho campos siguientes son los parámetros matemáticos exportados en la fase anterior.

Una vez ensamblados todos los campos con sus codificaciones INTEGER, se envuelven en una SEQUENCE con Encode-Sequence y se convierten al formato PEM con la cabecera “RSA PRIVATE KEY”, que identifica el archivo como PKCS#1. El archivo resultante es el que se carga en hMailServer u otro servidor de correo para firmar los mensajes salientes.

Ahora pasamos a la penúltima fase del script, la construcción de la clave pública en formato SPKI. La clave pública sigue un prroceso de construcción diferente al de la clave privada. El formato requerido por DKIM para el registro DNS es SubjectPublicKeyInfo (SPKI), definido en el RFC 5480, que es el mismo formato que usan los certificados X.509 para incluir la clave pública.

Primero se construye una SEQUENCE con solo los dos parámetros públicos: el módulo y el exponente público. Esta es la representación PKCS#1 de la clave pública:

$pubInner = @()
$pubInner += Encode-Integer $p.Modulus
$pubInner += Encode-Integer $p.Exponent
$pubKeyDer = [byte[]](Encode-Sequence ([byte[]]$pubInner))

A continuación se define el identificador del algoritmo RSA en formato DER. Esta secuencia de bytes es fija para RSA y corresponde al OID (Object Identifier) 1.2.840.113549.1.1.1 seguido de un parámetro NULL:

Terminal window
$algId = [byte[]](0x30,0x0d,0x06,0x09,0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x01,0x05,0x00)

Esta secuencia de 15 bytes tiene la siguiente estructura interna:

BytesValor hexSignificado
Tag SEQUENCE0x30Inicio de la estructura AlgorithmIdentifier
Length0x0d13 bytes de contenido
Tag OID0x06Tipo Object Identifier
Length OID0x099 bytes de OID
OID RSA2a 86 48 86 f7 0d 01 01 01Codificación DER del OID 1.2.840.113549.1.1.1 (rsaEncryption)
Tag NULL0x05Parámetros: NULL (sin parámetros adicionales)
Length NULL0x00Longitud cero del campo NULL

Finalmente se ensambla la estructura SPKI completa combinando el AlgorithmIdentifier con la clave pública envuelta en un BIT STRING:

Terminal window
$spki = @()
$spki += $algId
$spki += Encode-BitString $pubKeyDer
$spkiDer = [byte[]](Encode-Sequence ([byte[]]$spki))

El uso de Encode-BitString es obligatorio aquí: según el estándar X.509, el campo subjectPublicKey dentro de SubjectPublicKeyInfo debe ser un BIT STRING que contiene la representación DER de la clave pública. El byte 0x00 que Encode-BitString antepone indica que no hay bits de relleno.

La estructura resultante en DER tiene la siguiente jerarquía:

SEQUENCE {
SEQUENCE { (AlgorithmIdentifier)
OID 1.2.840.113549.1.1.1 (rsaEncryption)
NULL
}
BIT STRING { (subjectPublicKey)
SEQUENCE { (RSAPublicKey PKCS#1)
INTEGER (Modulus)
INTEGER (Exponent)
}
}
}

Ahora pasamos a la última fase, la generación del registro DNS TXT. Con la estructura SPKI construida, el script genera la cadena completa del registro DNS TXT:

Terminal window
$pubB64 = [Convert]::ToBase64String($spkiDer)
$txtValue = "v=DKIM1; k=rsa; p=$pubB64"
Write-AsciiNoBom "$carpeta\dkim_dns_txt.txt" $txtValue

La clave pública SPKI se convierte a Base64 (sin división en líneas, en una única cadena continua) y se incrusta en el valor del registro TXT con la sintaxis DKIM:

CampoValorSignificado
vDKIM1Versión del protocolo DKIM. Siempre DKIM1.
krsaTipo de algoritmo de la clave. Indica que es RSA.
p(Base64 de la clave pública SPKI)La clave pública que usarán los servidores receptores para verificar la firma.

La clave pública SPKI se convierte a Base64 (sin división en líneas, en una única cadena continua) y se incrusta en el valor del registro TXT con la sintaxis DKIM:

Campo DNSValor
Nombredkim._domainkey.josedaniel0375asixc.com
TipoTXT
Valorv=DKIM1; k=rsa; p=(contenido de dkim_dns_txt.txt)
TTL3600 (recomendado, ajustable según el proveedor DNS)

Finalmente el script muestra por pantalla un resumen con las rutas de los archivos generados y el nombre exacto del registro DNS que hay que crear, facilitando la configuración sin necesidad de abrir los archivos.

Ahora comprobamos que tengamos los archivos generados:

Una vez los tenemos, simplemente nos vamos a Domain -> josedaniel0375asixc.com -> DKIM Signing:

Una vez tenemos esto, ahora debemos añadir el registro en nuestro servidor DNS de la siguiente manera:

Ahora guardamos, reiniciamos y probamos que se haya añadido correctamente el registro:

Ahora enviamos un correo a un correo externo y verificamos los logs DEBUG del servidor hMailServer.

Como podemos ver, se firma con la DKIM pero no llegará al destino por qué google nos rechaza la conexión ya que no hemos realizado el paso del SMTP relayer.

Por último tendremos que habilitar el cifrado de extremo a extremo, para eso tendremos que generar 2 claves en las 2 cuentas:

Ahora una vez tenemos las 2 claves, tendremos que abrir OpenPGP Key Manager.

Ahora enviamos un correo y comprobamos dandole al botón de OpenPGP que realmente esté cifrado.