Bitcoin Core actúa como el pilar de una red monetaria que asegura más de dos billones de dólares en valor. Los riesgos son inmensos, y grandes partes de la base de código pueden contener errores de alto impacto. El motor de consenso, el código de procesamiento de mensajes de pares (p2p) y las bibliotecas criptográficas son áreas donde las vulnerabilidades podrían permitir el robo, detener la red o socavar fundamentalmente la confianza en el sistema. A diferencia del software financiero tradicional respaldado por seguros y soluciones legales, la seguridad de Bitcoin depende totalmente de la calidad de su código y de los procesos que mantienen esa calidad.
El enfoque de seguridad en Bitcoin Core no está formalmente definido, sino que es un conjunto evolutivo de prácticas que ha mejorado con el tiempo. Los procesos de revisión se han vuelto más exhaustivos, la infraestructura de pruebas se ha expandido significativamente, y el proyecto en su conjunto se ha vuelto más conservador y deliberado con respecto a los cambios en el software. Este ritmo más lento es, en sí mismo, una medida de seguridad que reduce el riesgo de introducir nuevos errores a través de modificaciones apresuradas.
Este artículo examina varios aspectos clave de cómo Bitcoin Core aborda la seguridad:
- la política de divulgación para manejar las vulnerabilidades descubiertas
- la extensa infraestructura de pruebas que busca errores
- el conjunto más amplio de herramientas de prueba que detectan problemas antes de que lleguen a producción
Estas prácticas funcionan juntas, aunque no como una estrategia unificada, sino como capas complementarias de defensa que se han desarrollado a medida que el proyecto ha madurado.
Proceso de Divulgación de Vulnerabilidades
Bitcoin Core, como proyecto de software, no ofrece funcionalidad automatizada de actualización para el software que distribuye, como una medida de protección para sus usuarios frente a sus desarrolladores. Todos los binarios liberados pueden verificarse para coincidir con el código fuente publicado a través de compilaciones reproducibles. Los operadores de nodos son responsables de decidir qué versión del software ejecutar y cuándo actualizar. En el contexto de las vulnerabilidades de seguridad, esto presenta un grave dilema. Las correcciones deben ser de código abierto para el proceso de revisión antes de que se pueda realizar una publicación, sin embargo, la divulgación completa debe retrasarse para dar a los usuarios un tiempo razonable para actualizar, ya que una vez que se publican los detalles de una vulnerabilidad, los atacantes pueden explotarla.
Históricamente, la divulgación pública del proyecto sobre vulnerabilidades críticas de seguridad, ya sean informadas externamente o descubiertas por colaboradores, ha sido insuficiente. Esto llevó a una situación donde muchos usuarios percibían a Bitcoin Core como si nunca tuviera errores, una percepción peligrosa e inexacta. Aproximadamente un año y medio atrás, motivado por estos problemas, el proyecto revisó y formalizó su manejo de cuestiones de seguridad en una política de divulgación y proceso de asesoramiento integral. Los objetivos eran proporcionar más transparencia, establecer expectativas claras para los investigadores de seguridad (ofreciéndoles un incentivo para encontrar y divulgar responsablemente vulnerabilidades), comunicar mejor los riesgos de utilizar versiones obsoletas y hacer que los errores de seguridad estén disponibles para el grupo más amplio de colaboradores después de la divulgación, para ayudar a aprender y prevenir futuros problemas.
Política
Todas las vulnerabilidades deben ser reportadas a security@bitcoincore.org (ver SECURITY.md para más detalles). Al ser reportada una vulnerabilidad, se le asignará una categoría de severidad. Diferenciamos entre 4 clases de vulnerabilidades:
Crítica: Errores que amenazan la seguridad e integridad fundamentales de toda la red Bitcoin. Estos son errores que permiten el robo de monedas a nivel de protocolo, la creación de monedas fuera del calendario de emisión especificado, o divisiones permanentes en la cadena a nivel de red.
Alta: Errores con un impacto significativo en los nodos afectados o en la red. Por lo general, son explotables de forma remota bajo configuraciones predeterminadas y pueden causar una interrupción generalizada.
Media: Errores que pueden degradar notablemente el rendimiento o la funcionalidad de la red o de un nodo, pero que son limitados en su alcance o explotabilidad. Estos pueden requerir condiciones especiales para activarse, como configuraciones no predeterminadas, o resultar en una degradación del servicio en lugar de una falla completa del nodo.
Baja: Errores que son difíciles de explotar o que tienen un impacto menor en la operación de un nodo. Podrían activarse solo bajo configuraciones no predeterminadas o desde la red local, y no suponen una amenaza inmediata o generalizada.
Las vulnerabilidades de severidad Baja se divulgarán 2 semanas después del lanzamiento de una versión mayor que contenga la corrección. Las vulnerabilidades de severidad Media y Alta se divulgarán 2 semanas después de que la última versión afectada alcance su Fin de Vida (aproximadamente un año después de que se lanzara por primera vez una versión mayor que contenga la corrección).
Se realizará un pre-anuncio dos semanas antes de divulgar los detalles de una vulnerabilidad. Este pre-anuncio coincidirá con el lanzamiento de una nueva versión mayor y contendrá el número de vulnerabilidades corregidas y sus niveles de severidad.
Los errores Críticos no se consideran en la política estándar, ya que probablemente requerirían un procedimiento ad-hoc. Además, un error puede no considerarse una vulnerabilidad en absoluto. Cualquier problema reportado también puede considerarse grave, pero no requerir embargo.
Cuando se reporta una vulnerabilidad al proyecto, primero es verificada y evaluada por el “Equipo de Seguridad” de Bitcoin Core, un pequeño grupo de colaboradores de largo plazo con un historial de detección o corrección de errores de seguridad. El proyecto clasifica las vulnerabilidades en cuatro niveles de severidad: Crítico (amenazas a la integridad de la red como el robo de monedas o inflación), Alto (impacto significativo, explotable a distancia), Medio (degradación del rendimiento o alcance limitado) y Bajo (difícil de explotar con un impacto menor). Si se confirma como grave, se desarrolla una corrección que se prueba a fondo en privado. Luego, la corrección se presenta como una propuesta de extracción (pull request) al igual que cualquier otro cambio en el código, pero la descripción y discusión de la PR ocultan la verdadera naturaleza de la corrección. Puede enmarcarse como una reestructuración, mejora de rendimiento o endurecimiento contra problemas potenciales. Esto permite que la corrección pase por la revisión normal del código mientras se mantienen los detalles de la vulnerabilidad en privado.
Este enfoque implica verdaderos compromisos, y es un acto de equilibrio genuinamente difícil de mantener. Los críticos podrían argumentar que es paternalista o que concentra demasiado poder en manos de unos pocos desarrolladores que conocen vulnerabilidades antes que el público. Estas preocupaciones merecen una consideración seria, pero la alternativa de divulgación pública inmediata podría ser catastrófica. Publicar los detalles de vulnerabilidad antes de que la mayoría de los usuarios se hayan actualizado esencialmente proporciona a los atacantes tanto la lista de objetivos (nodos no actualizados) como el arma (código de explotación).
Infraestructura de Fuzzing
El fuzzing es una técnica de prueba que alimenta entradas aleatorias, malformadas o inesperadas a un software para encontrar errores. Básicamente, se generan y mutan continuamente casos de prueba de forma automática, se alimentan al programa y se observa el comportamiento inesperado, como bloqueos, errores lógicos, etc. Los fuzzers modernos utilizan algoritmos evolutivos para aprender qué entradas activan caminos de código interesantes, y luego mutan esas entradas para explorar más a fondo el programa. Es una forma efectiva de encontrar errores en casos límite que serían casi imposibles de descubrir a través de pruebas manuales o revisión de código al mismo ritmo.
Como el fuzzer proporciona las entradas para esta prueba, el desarrollador no puede afirmar directamente los resultados esperados (por ejemplo, la entrada A debe producir la salida B). En cambio, hacen afirmaciones sobre propiedades generales que el software debe mantener. Esto es extremadamente valioso, ya que permite construir una mayor confianza en el comportamiento deseado al probar propiedades como evitar que el nodo se bloquee o asegurarse de que la oferta de monedas nunca se infle más allá de lo esperado.
Debido a la necesidad crítica de corrección, robustez y seguridad, Bitcoin Core utiliza extensivamente el fuzzing con varios enfoques. A lo largo de la historia de Bitcoin Core, los esfuerzos de pruebas de fuzzing han ido en aumento. Las primeras menciones de fuzzing muy primitivo se remontan a 2012, y la integración de un marco de fuzzing simple ocurrió en 2016, que evolucionó hasta convertirse en el marco integral actual con más de 200 pruebas de fuzzing individuales, cubriendo componentes y funciones críticas de la base de código.
A diferencia de las pruebas unitarias estándar, las pruebas de fuzzing no tienen un punto de “aprobado” definido; es decir, no se ejecutan una vez y se obtiene un estado de “aprobado” o “fallido”. Dado que el fuzzing es un proceso aleatorio continuo, cualquier afirmación sobre los resultados (cuando no se encuentran fallas) solo puede ser probabilística. Una prueba de fuzzing puede ejecutarse durante 5000 horas sin encontrar un error, pero las siguientes 5000 horas podrían descubrir uno. En consecuencia, para ser efectivos, los tests de fuzzing deben ejecutarse continuamente. Mientras que Bitcoin Core se apoya en la infraestructura de oss-fuzz de Google para ejecutar sus pruebas de fuzzing, también invierte considerablemente en construir su propia infraestructura, con varios colaboradores realizando fuzzing de manera continua con sus propios setups. Por ejemplo, la infraestructura de Brink sola proporciona más de 1 millón de horas de CPU al año para fuzzing en Bitcoin Core.
Aunque el repositorio de Bitcoin Core tiene numerosas pruebas de fuzzing a nivel de componente/función, varios proyectos externos emplean estrategias de fuzzing distintas. Cryptofuzz, ahora retirado, se centró en el fuzzing diferencial de libsecp256k1 y otro código criptográfico. Para el código no criptográfico, como las primitivas de serialización, la lógica de consenso y el análisis de descriptores de billetera, el proyecto bitcoinfuzz utiliza un enfoque de fuzzing diferencial específico para Bitcoin. También se está desarrollando una metodología de fuzzing de sistema completo para descubrir errores a nivel de sistema con Fuzzamoto, principalmente orientada a encontrar errores que surgen de interacciones complicadas entre diferentes partes de la base de código que interactúan como un sistema completo.
Se han encontrado cientos, si no miles, de errores mediante fuzzing en versiones publicadas de Bitcoin Core o en solicitudes de extracción a lo largo de los años (obviamente no todos ellos son relevantes para la seguridad), resaltando la efectividad y la importancia del fuzzing. Un ejemplo recientemente publicado de alta severidad es CVE-2024-35202, un error de bloqueo accesible de forma remota encontrado a través del fuzzing que podría haber permitido a un atacante bloquear todos los nodos accesibles públicamente. El descubrimiento involucró una reestructuración de la lógica de retransmisión de bloques compactos, extrayéndola en un módulo aislado y testeable y escribiendo una prueba de fuzzing para ello.
Aseguramiento de la Calidad
Aunque se destaca el fuzzing, el proyecto emplea diversas metodologías de prueba adicionales a diario para minimizar aún más el riesgo de que los problemas lleguen al código de producción.
Bitcoin Core cuenta con cientos de pruebas unitarias. Estas pruebas están diseñadas para verificar el comportamiento esperado de pequeñas piezas de código aisladas, como funciones o clases individuales. Por ejemplo, se utilizan pruebas unitarias para verificar el comportamiento de la función de verificación de trabajo. Estas pruebas implican proporcionar entradas de casos límite a la función y comprobar si las salidas resultantes cumplen las expectativas.
Las pruebas funcionales, por otro lado, prueban una o más instancias de Bitcoin Core en su totalidad, verificando el comportamiento a un nivel de sistema más alto, mediante el uso de las interfaces externas del software (por ejemplo, RPCs, mensajes p2p) para simular escenarios potenciales del mundo real. Por ejemplo, una prueba podría poner en marcha una pequeña red de nodos, enviar una transacción a uno de ellos (por ejemplo, utilizando los RPC de billetera) y luego verificar si todos los nodos en la prueba eventualmente observan y aceptan la transacción. Históricamente, Bitcoin Core carecía de una modularidad significativa en el código, una característica que persiste en varias áreas. En consecuencia, el proyecto ha dependido más de un enfoque de pruebas funcionales que de un enfoque de pruebas unitarias, ya que a menudo requiere reestructuraciones previas del código para aislar el código objetivo para probarlo de manera independiente.
Cada metodología de prueba tiene sus fortalezas y debilidades. Las pruebas unitarias suelen ser rápidas de ejecutar y son buenas para identificar dónde se encuentra un error, dado que su alcance es pequeño y bien definido. Sin embargo, por definición, no detectarán errores que solo se manifiestan por la interacción de múltiples unidades. Aquí es donde las pruebas funcionales brillan, ya que ponen a prueba el sistema completo, lo que viene con un costo en velocidad de ejecución, ya que tienen que configurar y desmantelar instancias de nodos en cada ejecución de la prueba. También son mucho menos efectivas para indicar al desarrollador dónde se encuentra un error. Tomando el ejemplo anterior, si la prueba de propagación de transacciones falla (es decir, la transacción no se propagó a todos los nodos), es más difícil determinar qué componentes del sistema están fallando. Podría ser un error en la lógica de aceptación de mempool, en el código de red, en los RPC utilizados para crear la transacción o en cualquiera de los otros componentes involucrados. Ningún método es el mejor; es la combinación de todas las metodologías lo que forja un software con la mayor probabilidad de funcionar correctamente.
Todas las pruebas se ejecutan dentro de la CI en cada PR y en cada push a la rama maestra. Todas las pruebas unitarias, funcionales y de fuzzing (que ejecutan entradas previamente generadas) se ejecutan en una matriz de diferentes sistemas operativos, arquitecturas de CPU y varios mecanismos de detección de errores, como los sanitizers (Address, Thread, Undefined, Memory) y valgrind para detectar clases comunes de errores de C++ relacionados con la seguridad de la memoria y el comportamiento indefinido.
Bitcoin Core ha evolucionado de manera incremental desde el cliente original que Satoshi lanzó, con colaboradores que entran y salen con el tiempo, y por ello contiene mucho código heredado. La reestructuración del código existente para simplificarlo y aislarlo ha sido y sigue siendo una parte importante del trabajo realizado en el proyecto. Ya sea el Kernel, una nueva función p2p, mejoras de rendimiento o preparación para implementar más pruebas, todo requiere reestructuración. Sin embargo, las opiniones sobre cuándo y cómo reestructurar están divididas, ya que puede ser una espada de doble filo. Si bien la reestructuración refresca el contexto para quienes están involucrados, descubre errores y generalmente permite más pruebas, también puede ser aterrador modificar código que ya nadie entiende y puede llevar a la introducción de nuevos errores. Tanto las pruebas funcionales como otras estrategias de pruebas a nivel de sistema (como Fuzzamoto mencionado anteriormente en la sección de fuzzing) son formas de reducir el riesgo de los esfuerzos de reestructuración, ya que las pruebas en esa capa requieren poca o ninguna reestructuración previa.
Previo a lanzamientos importantes, como una estrategia de prueba adicional, el proyecto produce una guía de pruebas para usuarios, desarrolladores y la comunidad en general para probar manualmente características establecidas y nuevas. Se suele alentar a probar el software con un uso típico, como un llamado a la acción, para verificar que los flujos de trabajo normales de los usuarios individuales sigan funcionando.
No te pierdas la oportunidad de poseer The Core Issue — ¡presentando artículos escritos por muchos Desarrolladores Core que explican los proyectos en los que trabajan!
Este artículo es la Carta del Editor presentada en la última edición impresa de Bitcoin Magazine, The Core Issue. Lo compartimos aquí como un vistazo anticipado a las ideas exploradas a lo largo de todo el número.
Fuente: bitcoinmagazine.com