Por qué ya no uso un certificado a la antigua en mi sitio HTTPS
(rachelbythebay.com)- La autora sentía rechazo durante años hacia el protocolo ACME por su complejidad y los riesgos de implementación
- Los clientes ACME existentes solían tener código riesgoso desde el punto de vista de la seguridad o difícil de entender, así que evitaba ejecutarlos por su cuenta
- Pero, debido al deterioro de la calidad y el aumento de precios de Gandi, su registrador de dominios, terminó implementando su propia herramienta de renovación de certificados
- Tras muchísimos intentos y errores, logró completar con éxito una herramienta para emitir certificados directamente mediante Let's Encrypt
- En la segunda mitad del texto explica en detalle el funcionamiento real del protocolo ACME y detalles de implementación de bajo nivel como JSON, base64 y firmas
Why I no longer have an old-school cert on my https site
Contexto y motivación
- A comienzos de 2023 explicó por qué seguía manteniendo un certificado a la antigua, pero ahora, en 2025, comparte por qué terminó abandonando ese enfoque
- Su rechazo hacia el protocolo ACME venía desde 2018, y las tecnologías web complejas y los métodos de codificación enrevesados eran una gran barrera
- Consideraba que la mayoría de los clientes ACME tenían código difícil de confiar y que era peligroso hacerlos correr con privilegios de root
- Después de que Gandi fuera adquirida por un fondo de capital privado, la calidad bajó y los precios subieron, así que ya no había razón para seguir manteniendo el certificado anterior
El inicio de la implementación propia
- En vez de usar herramientas existentes, fue implementando por su cuenta pequeñas funciones utilitarias, una por una
- Empezó por encapsular
jansson, una biblioteca JSON para C, para poder usarla desde C++ - Revisó varias bibliotecas para generar JWK (estructuras de clave), pero casi ninguna ayudó, así que decidió implementarlo por sí misma
- En el camino se detuvo y volvió a empezar varias veces, conectando poco a poco componentes pequeños de manera incremental
Entorno de pruebas y aplicación real
-
Para no tocar directamente los servidores reales de Let's Encrypt, usó en un entorno aislado un servidor ACME de pruebas llamado "pebble"
-
Después de numerosos fracasos, logró completar una herramienta inicial que recibe un CSR y emite un certificado, y
- la probó con éxito en el servidor de staging de Let's Encrypt
- también funcionó en producción
- y ya quedó aplicada al sitio web real
Explicación detallada del protocolo ACME
- Genera una clave RSA y crea un CSR (Certificate Signing Request) que incluye CN y SAN
- Analiza el JSON de la URL del directorio ACME para extraer endpoints como newNonce, newAccount y newOrder
- Extrae de la clave privada el módulo y el exponente público, y los convierte a codificación base64url apta para la web
- Tras crear el JWK, firma con RSA SHA256 junto con el payload JSON
- Obtiene un Nonce con una petición HTTP HEAD y luego envía la solicitud firmada por POST para crear la cuenta
- El encabezado
Locationde la respuesta no se usa como redirección real, sino como URL identificadora de la cuenta
La complejidad del protocolo ACME
- A pesar de tratarse solo de emitir un certificado, intervienen cosas como
- hash SHA256, base64web, JSON dentro de JSON y firmas RSA
- peticiones HEAD, identificación de cuenta mediante el encabezado Location, necesidad de un Nonce de un solo uso, etc.
- Menciona que todavía ni siquiera ha llegado a cubrir el pedido del certificado, la prueba de propiedad del dominio (como registros TXT) ni la finalización de la validación
- También señala cierta flexibilidad excesiva del estándar, ya que algunos clientes funcionan incluso con una implementación incorrecta de la codificación de
publicExponent
Conclusión
- ACME es extremadamente complejo y requiere una enorme cantidad de prueba, error y esfuerzo para implementarlo directamente
- Aun así, comparte que logró abandonar el certificado a la antigua y pasar con éxito a un esquema completamente automatizado
- También remata con la broma de si toda esta complejidad no será, quizá, una estructura diseñada para garantizarle trabajo a alguien
1 comentarios
Comentarios en Hacker News
Soy el líder técnico del equipo de SRE/infra de Let’s Encrypt, así que pienso mucho en este tipo de problemas
JSON Web Signature es un formato realmente complicado, y la API de ACME también está muy comprometida con ser RESTful
Si yo la hubiera diseñado, no la habría hecho así
Creo que el trasfondo de que esta estructura terminara así se debe en parte a la intención del IETF de reutilizar muchos estándares del IETF y en parte al diseño por comité
Tener aunque sea unas cuantas librerías de JSON, JWS y HTTP ya mejora mucho las cosas, pero el problema es que, sobre todo en C, ni siquiera esas librerías son fáciles de usar
El propio lenguaje de los RFC es complejo y además suele remitir a muchos otros documentos, así que estamos trabajando por separado en un cliente interactivo y en documentación para ayudar con eso
No termino de entender eso de que JSON Web Signature sea un formato complicado
Yo trato mucho con cosas complejas como ASN.1, Kerberos y PKI, y no me parece que JWS sea un formato especialmente difícil
Incluso si uno tuviera que implementarlo directamente en código, me sigue pareciendo mucho más fácil que S/MIME, CMS o Kerberos
Hace falta explicar mejor en qué parte JWS es “complicado”
Si el problema es JWT, me parece más central que no esté bien definido de forma estándar cómo un user agent HTTP debe recibirlo o solicitarlo
Vi que alguien decía que “si quieres emitir más de 3 certificados tienes que pagar”, pero yo lo he usado durante los últimos 5 años y nunca me ha llegado una factura; me parece un malentendido o información incorrecta
Al hablar de la parte donde se usa “e=AQAB” en lugar de “e=65537”, se explica que la causa es que JSON no maneja bien los números
Si pasas a un parser JSON un valor muy grande como 4723476276172647362476274672164762476438, la mayoría de los parsers JSON lo truncarán silenciosamente a un entero de 64 bits o a un float, o con suerte darán error
Un lenguaje como Common Lisp probablemente lo manejaría bien, pero en la práctica no mucha gente desarrolla en ese tipo de entorno
Por eso, si quieres transmitir números grandes de forma confiable en JSON, hasta parece mejor convertirlos a un arreglo de bytes en base64
Aunque parezca que no pasa nada, este tipo de comportamiento es la raíz de varios problemas de seguridad, así que me parece válido tratar así todos los números del protocolo
Eso sí, este enfoque tiene la desventaja de que se pierde la legibilidad humana de JSON, y personalmente creo que una S-Expression estandarizada habría sido una opción mucho mejor
Pero el mundo eligió JSON
Si no entiendes por qué el mundo eligió JSON, creo que lo estás ignorando a propósito
JSON se puede escribir, editar y leer manualmente con mucha facilidad para la mayoría de los datos
En cambio, Canonical S-Expression requiere anteponer información de longitud a cada elemento, así que editarlo a mano es demasiado tedioso
Para escribir una S-Expression tienes que contar caracteres uno por uno y corregir los prefijos, lo cual es muy molesto
Contrario a lo que se esperaría, creo que esa facilidad para escribir y modificar manualmente es la razón por la que JSON sobrevivió
Por cierto, el parser JSON de Ruby maneja bien los números grandes
En una app de C# sufrí un bug donde el serializer JSON emitía un BigInt como número, JS lo recibía y lo interpretaba mal en silencio
Todavía me sorprende que el comportamiento estándar sea desbordar en vez de dar error
Desde entonces, tengo la costumbre de tratar siempre como cadenas todos los números mayores a 32 bits
La comparación entre {"e":"AQAB"} y {"e":65537} tiene sentido, pero si lo comparas con {"e":"65537"}, también en ese caso todos los parsers JSON producirán el mismo resultado
Ya sea número o cadena, la conversión es clara
Claro, si el valor es tan grande que ni siquiera cabe en un double, entonces eso ya es un problema del lenguaje o del parser, pero me parece independiente de la forma de representación
Creo que el problema de JSON no está en el formato en sí, sino en que los parsers se hicieron originalmente para mapear a tipos de JS
Aunque algunos parsers puedan manejarlo bien, si haces eso se pierde la portabilidad de JSON
Si lo conviertes a Base64 pasa el mismo problema (porque ya no sigue el estándar)
Se puede hacer parsing personalizado con replacer y reviver, pero no está garantizado que esa función exista en todos los entornos
Al final, el origen del error es asumir que JSON se interpreta con un parser estándar
Si en vez de llamarlo JSON se llamara de otra manera, estos problemas disminuirían, pero la gente igual intentaría pasarlo al parser tal cual si se ve como JSON
Go tiene el tipo
json.Number, con el que puedes decodificar números como cadenas sin pérdidaComparto uno de mis tipos decimal arbitrarios favoritos https://github.com/ncruces/decimal?tab=readme-ov-file#decimal-arithmetic
Medio en broma, no me queda claro por qué S-Expression sería mejor en este caso
Incluso entre los LISP hay casos que no soportan aritmética de precisión arbitraria
Me resultó curioso por qué el autor adoptó una postura tan crítica hacia ACME y varios clientes
No parece simplemente un tema de falta de habilidad, así que supuse que había cierta antipatía hacia el propio concepto de ACME o hacia las herramientas de su ecosistema
Nosotros también lo aplicamos desde 2019 en algunos sitios basados en LE, y en ese tiempo probamos varios clientes ACME
Por ejemplo, Crypt-LE nos funcionó bien para nuestro caso, y al intentar integrarlo con Sectigo ACME, como le64 no bastaba, probamos varias cosas como certbot, lego y posh-acme
Al final usamos certbot corrigiendo un problema del entorno GHA, y posh-acme también nos pareció bueno
Releyéndolo, el tono filoso del autor no iba tanto contra ACME o los clientes, sino contra la propia especificación
La conclusión es que la idea de ACME es buena, pero su implementación y su aplicación en la práctica son decepcionantes
Creo que tengo una perspectiva parecida a la del autor
Cita la frase del autor: “muchos clientes existentes son código peligroso y no confío en ejecutarlos con privilegios de root en mi servidor”
Me parece una actitud razonable en tareas sensibles de seguridad
Para quien tuvo dificultad en entender el tono del texto original, se comparten enlaces a publicaciones anteriores que dan contexto
Hay mucha gente a la que no le gusta ejecutar en su servidor algo que no entiende, y yo también simpatizo con esa idea
Pero la seguridad es, por naturaleza, un juego del gato y el ratón, así que no queda otra que seguir cambiando
Por suerte, ACME te da la libertad de crear tu propio cliente
No estás obligado a usar certbot, ni es una estructura que te bloquee tus recursos como TPM
Si uno quiere implementar un cliente ACME desde cero, comparte la experiencia de que leer directamente los RFC (y los documentos relacionados de JOSE, etc.) es más fácil de lo que parece
Incluso lo implementó por su cuenta y escribió una guía para entender el flujo de ACME v2, que comparte aquí https://www.arnavion.dev/blog/2019-06-01-how-does-acme-v2-work/
No sustituye al RFC oficial, pero sirve bien como referencia tipo diagrama de flujo e índice por modalidad
Incluso hubo una tarea final de un curso de seguridad del MIT que consistía en implementar un cliente ACME https://css.csail.mit.edu/6.858/2023/labs/lab5.html
Se satiriza esa extraña realidad en la que, en vez de leer el manual paso por paso, puedes ganar más puntos de internet publicando en Hacker News un texto explicando todo el proceso en inglés
Agradecen al autor por señalar que la complejidad de los protocolos de infraestructura web sigue aumentando
Piensan que estos estándares no solo son una carga para desarrolladores que simplemente tienen que usar herramientas o clientes, sino que además actúan como una “barrera regulatoria”, estructurando las cosas de forma que al final solo las grandes empresas establecidas puedan cumplir los requisitos para operar en internet
Puede que ACME por sí solo no sea una barrera de entrada imposible de cruzar, pero al final todo se va acumulando hasta formar una pared
En OpenBSD hay un cliente ACME muy simple y liviano incluido en el sistema base
Entiendo que lo hicieron porque las alternativas existentes eran demasiado pesadas y no seguían la filosofía Unix
Da pena que el autor no haya considerado esa opción
Probablemente también podría portarse a otros sistemas operativos con un poco de esfuerzo
Creo que este cliente de OpenBSD es más bien un caso de la filosofía OpenBSD sin entender por qué la seguridad se ha vuelto tan compleja
Este cliente se instala y se usa solo en esa máquina, y mediante una estructura separada hace que cada componente no afecte a los demás
Pero el propio protocolo ACME permite una separación completa (
air-gapping), de modo que el servidor web, el solicitante del certificado y el servidor DNS pueden estar en entornos distintos sin problemaSi no usas el cliente integrado de OpenBSD, quizá sea más complejo, pero desde la perspectiva de principios de diseño de seguridad me parece superior
“Solo instala OpenBSD y listo” no deja de ser simplemente el camino fácil
También se menciona uacme (https://github.com/ndilieto/uacme)
Es código C liviano, y después de sufrir constantemente con el cliente Python de LE por problemas de energía, lo usaron de forma estable como reemplazo
Comparten la experiencia de que usan directamente el cliente ACME de OpenBSD y funciona muy bien
La recomendación de “generar una clave privada RSA de 4096 bits” en realidad solo empeora la velocidad para los visitantes, y la seguridad práctica sigue estando al nivel de 2048 bits
Se enfatiza que es mejor usar un certificado leaf de 2048 bits
Pregunta si 4096 bits no sería más resistente frente a captura pasiva y descifrado futuro
También le da curiosidad si la seguridad de los certificados intermedios afecta a los ataques asincrónicos
Como su webhost solo soporta claves RSA, usa RSA de 4096 bits a propósito para empujarlos a que den soporte a claves EC más rápido
Hacer este trabajo por cuenta propia sí te hace mejorar, pero el tono del texto del autor da la impresión de que está desahogando su frustración con el protocolo o con el proceso de despliegue de Let’s Encrypt
Incluso con una librería ACME liviana (https://github.com/jmccl/acme-lw, por ejemplo) se puede automatizar suficientemente; se preguntan por qué hacerlo de una forma tan sufrida
Los problemas de flags y bitfields vienen todos del legado histórico de ASN.1/X.509, la complejidad matemática es seria, y todas las librerías y el software siguen atados a las limitaciones tecnológicas de los años 80
Hubo una última oportunidad de ordenar este caos cuando llegó Let’s Encrypt o cuando apareció HTTP/2, pero en la práctica una ACME CA puede quedar armada con shell scripts, OpenSSL y alcohol, y además la compatibilidad con el software existente también era un problema, así que no hubo salto real
Comparte que la presión para migrar cada vez más a HTTPS sigue aumentando
Por ejemplo, en WhatsApp ya no se pueden abrir enlaces HTTP
Sugiere que usando proxy y caché se puede reducir la carga de tráfico, y que es un buen método para servidores pequeños
Enfatiza que, por muy complejo que sea ACME, sigue siendo muchísimo mejor que no tener soporte TLS
“Clave RSA, digest SHA256, firma RSA, base64 que en realidad no es base64, concatenación de cadenas, JSON dentro de JSON, usar el encabezado Location como identificador en vez de un redireccionamiento 301, una petición HEAD solo para obtener un valor de encabezado, una petición separada para el nonce en cada solicitud, etc.”
“Y todavía faltan pasos aún más complejos como crear la orden del certificado, manejar autorizaciones y challenges, key thumbprints, construir registros TXT, etc.”
Termina con un mensaje de apoyo diciendo que la complejidad es casi imposible de creer y agradeciendo que hayan compartido el resumen