Diseño de seguridad de la actualización automática - Por qué HTTPS no es suficiente
· Actualizado el: · Go Komura · Desarrollo Windows, Seguridad, Actualizador, Actualización automática, Firma, MSIX, ClickOnce
Índice
- Ante todo, la conclusión
- Por qué la actualización automática es una zona de riesgo
- Patrones que no funcionan
- Buenas prácticas
- Configuración mínima segura
- Cómo pensarlo en proyectos Windows
- Lista de verificación mínima
- Resumen
- Referencias
1. Ante todo, la conclusión
A continuación, se enumeran primero los puntos de acuerdo prácticos.
- Si los requisitos lo permiten, priorizar primero una infraestructura de actualización ya existente, como MSIX App Installer o ClickOnce
- Si se necesita un actualizador propio, lo primero que hay que incorporar no es la interfaz de usuario, sino la verificación de firma y la recuperación ante fallos
- Tratar la información de actualización, como
latest.json, como metadata firmada y no como un archivo de configuración sin firmar - TLS es necesario, pero no es una condición suficiente
- La decisión de actualizar debe basarse en que «el cliente lo verificó y pudo determinar que es correcto», no en que «el servidor lo dice»
- Separar las claves de firma de desarrollo y de producción, y protegerlas con un HSM o un servicio de firma
- Ante un fallo de actualización, aplicar fail-closed en lugar de fail-open
- Es más seguro asumir que un actualizador sin protección contra rollback puede acabar retrocediendo a una versión vulnerable
- Si todavía no se puede incorporar la verificación de firma, es más seguro distribuir manualmente un instalador firmado que aplicar la actualización automática
Antes de continuar, se fijan solo los términos: en adelante se usan con este significado.
| Término | Significado |
|---|---|
| fail-closed | Diseño que, ante una anomalía, se inclina hacia el lado de «detenerse». Si falla la verificación de firma, no se avanza con la actualización |
| fail-open | Diseño que, incluso ante una anomalía, se inclina hacia el lado de «continuar». Solo se muestra una advertencia y la actualización sigue adelante |
| staging | Método en el que la nueva versión se despliega primero en otro lugar y se activa el cambio una vez terminada la verificación |
| kill switch | Mecanismo para detener de inmediato, mediante una operación en el servidor, una actualización que se está distribuyendo |
| trust anchor | El origen que el cliente confía desde el principio: una clave pública raíz o una cadena de certificados fijada |
En definitiva, el núcleo de la actualización automática no es «cómo descargar», sino «qué se confía, dónde se verifica y cómo se revierte cuando algo se rompe».
2. Por qué la actualización automática es una zona de riesgo
Una funcionalidad normal queda contenida dentro de la propia aplicación. El actualizador, en cambio, reúne de golpe estas tres cosas:
- Ir a buscar un archivo desde el exterior
- Confiar en ese archivo
- Reemplazar el ejecutable existente
Es decir, la vía para la ejecución de código arbitrario ya está integrada en el producto desde el principio.
Un malentendido habitual aquí es pensar que «es HTTPS, así que es seguro». Por supuesto que TLS es necesario. Pero lo que protege es, principalmente, la vía de comunicación y la legitimidad del destino. Frente a que el propio servidor de actualizaciones haya sido comprometido, que se haya colocado un artefacto incorrecto en la CDN oficial, o que se haya sustituido un manifest sin firmar, eso por sí solo no basta.
De hecho, basta con mirar las amenazas que ordena TUF (The Update Framework, la especificación que define el modelo de confianza para las actualizaciones de software) para ver que el sistema de actualización enfrenta, al menos, lo siguiente:
- Hacer instalar software malicioso arbitrario
- El rollback, que fuerza el retroceso a una versión antigua vulnerable
- El freeze, que oculta la existencia de una versión nueva
- El mix-and-match, que mezcla metadata y artefactos que no son coherentes entre sí
En otras palabras, la actualización automática no es «transferencia de archivos», sino «distribución de confianza». Solo cuando se diseña esta parte, la actualización automática empieza a funcionar de forma segura.
2.1 Tabla de correspondencia entre amenazas y medidas
En este artículo, las amenazas y las medidas se tratan en capítulos separados. Antes de eso, se resume la correspondencia entre ambas en una sola tabla. Con esto basta como esqueleto del diseño.
| Amenaza | Qué sucede | ¿Se puede evitar solo con TLS? | Medida principal | Capítulo detallado |
|---|---|---|---|---|
| Distribución de un artefacto ilegítimo | Hacer instalar como actualización oficial un archivo preparado por el atacante | No se puede evitar (no sirve de nada ante un compromiso del origen o una distribución errónea) | Metadata firmada y verificación en el cliente del hash / firma del artefacto | 4.2 / 4.3 / 4.4 |
| rollback | Aunque la firma sea correcta, se hace retroceder a una versión anterior con vulnerabilidades conocidas | No se puede evitar (tanto la firma como TLS son correctos) | Conservar la versión de release más alta conocida y rechazar las anteriores | 4.8 |
| freeze | Aunque haya una versión nueva, se sigue devolviendo la metadata antigua para impedir la actualización | No se puede evitar | Dar a la metadata un campo expires_at y rechazar la que sea demasiado antigua |
4.3 / 4.8 |
| mix-and-match | Se entrega una combinación de metadata y artefacto que no son coherentes entre sí | No se puede evitar | Fijar en el manifest el hash / tamaño / versión del artifact de destino | 4.3 / 4.8 |
| Compromiso de la clave de firma | Se distribuye una actualización ilegítima con firma legítima | No se puede evitar | Separación de claves de desarrollo / producción, HSM o servicio de firma, flujo de aprobación y registro de auditoría, separación de la clave raíz y de la clave de metadata | 4.5 |
| Fallo o interrupción de la actualización | Se cae a mitad del reemplazo y la aplicación deja de iniciarse | Fuera de alcance | staging + activación atómica + rollback | 4.6 |
| Elusión de la verificación | Queda en producción un atajo como skipVerify |
Fuera de alcance | Fijar fail-closed como requisito y no tener un indicador de elusión | 3.7 / 4.6 |
| Imposibilidad de detener en caso de incidente | Se sigue distribuyendo una versión con problemas | Fuera de alcance | blocklist, minimum allowed version, kill switch | 4.8 / 7 |
El sentido de este artículo es que la columna «¿Se puede evitar solo con TLS?» sea, en todos los casos, «no se puede evitar» o «fuera de alcance». TLS protege la vía, pero proteger el contenido de lo que se distribuye y la autoridad para distribuirlo es un mecanismo distinto.
3. Patrones que no funcionan
Antes que nada, se resumen las formas peligrosas que se ven a menudo en la práctica.
| Patrón incorrecto | Qué es peligroso | Corrección mínima |
|---|---|---|
Obtener version.json por HTTPS y ejecutar directamente el zip / exe de la URL |
Vulnerable a compromiso del origen, sustitución de configuración y distribución errónea | Pasar a metadata firmada y verificación en el cliente del artefacto |
| Firmar solo el binario y dejar el manifest sin firmar | Se pueden manipular la URL, la versión, el canal y el indicador de actualización obligatoria | Usar un manifest firmado que incluya versión / hash / tamaño / canal / expiración |
| Guardar la clave de firma en el PC de desarrollo o en archivos de CI | Si se compromete, se puede distribuir malware con firma legítima | HSM / servicio de firma + flujo de aprobación + registro de auditoría |
| «Ignorar el error de verificación y continuar» al fallar la actualización | Se abre la vía más débil justo en caso de incidente | Aplicar fail-closed |
| Actualizar sobrescribiendo sin conservar la versión anterior | Corte de energía, falta de espacio en disco o fallo a mitad de proceso dejan la app sin poder iniciarse | staging + activación atómica + rollback |
| Permitir una versión antigua basándose solo en la comparación de versiones | Se permite el rollback a una versión vulnerable | Versión de release monótonamente creciente y almacenamiento de la más alta conocida |
| Ejecutar todo el actualizador con permisos de administrador | El alcance del daño es amplio si se compromete | Descarga/verificación con permisos bajos; el reemplazo se aísla en un helper mínimo |
| Empezar por la actualización diferencial | La implementación es compleja y aumentan los fallos de verificación no detectados | Empezar primero por la actualización de paquete completo |
A continuación, se examina esto con algo más de detalle.
3.1 Detenerse en «es HTTPS, así que está bien»
Este es el caso más frecuente.
- Al iniciar, se lee
latest.json - Se extrae
downloadUrl - Se descarga el zip / exe
- Se descomprime y se reemplaza
- Fin
A simple vista parece razonable, pero la raíz de la confianza depende demasiado de la respuesta del servidor. Si se compromete el servidor de actualizaciones o la configuración de distribución, se puede distribuir una actualización ilegítima sobre un HTTPS perfectamente correcto.
TLS es necesario. Pero el diseño del actualizador no termina solo con TLS.
3.2 Se firma, pero no se verifica en el lado del cliente
Aunque el archivo se firme en el momento del release, si el cliente no la comprueba, no sirve de nada.
Un caso habitual es:
- Se firma en el CI
- pero el actualizador solo comprueba el hash
- y además ese hash proviene de un manifest sin firmar
Ese es el patrón.
En ese caso, en cuanto se sustituye el manifest, el hash se sustituye junto con él. La idea de que «comprobar el hash es seguro» solo se sostiene si también se protege el origen de ese hash.
3.3 El manifest no está firmado
En el sistema de actualización, lo que realmente hay que proteger no es solo el ejecutable en sí. Al menos la siguiente información es peligrosa si se manipula:
- version / release id
- La URL y el nombre de archivo del objeto de descarga
- hash / size
- El canal (stable / beta, etc.)
- Si la actualización es obligatoria
- El sistema operativo / arquitectura aplicable
- La fecha de expiración de la metadata
- La versión mínima requerida del actualizador
En resumen, conviene adoptar la actitud de incluir en la signed metadata toda la información usada para decidir la actualización.
3.4 La gestión de la clave de firma es descuidada
Buena parte de la seguridad de la funcionalidad de actualización es, en realidad, la seguridad de la gestión de claves.
Si la clave de firma de producción está guardada de las siguientes formas, es bastante peligroso:
- Dejada en el almacén de certificados del PC de desarrollo
- Subida como
.pfxen un secret del CI - Varias personas distribuyen localmente la misma clave privada
- La firma de desarrollo y la de producción comparten la misma cadena de confianza
En ese caso, aunque el propio actualizador sea correcto, no se puede detener una «actualización ilegítima con firma legítima».
3.5 Actualización por sobrescritura sin conservar la versión anterior
En la actualización, el diseño para el caso de fallo importa más que el del caso de éxito.
- Se cortó a mitad de la descarga
- Falló la descompresión
- Se cortó la energía a mitad del reemplazo
- La versión nueva se inició, pero falló en la migración inicial
En estos casos, si la versión anterior ya no existe, la recuperación se vuelve mucho más pesada. En la práctica, el problema no es tanto el hecho de que «la actualización falló», sino que «la aplicación dejó de iniciarse en el entorno del cliente».
3.6 No se ha considerado el rollback
Incluso una versión legítima y firmada puede resultar conveniente para un atacante si se trata de una versión antigua vulnerable.
Por ejemplo:
- La versión 1.8 tiene una vulnerabilidad conocida
- El entorno del cliente ya está actualizado a la 2.3
- El atacante vuelve a distribuir la 1.8
Si esto pasa, aunque la firma en sí sea correcta, la situación es peligrosa.
No basta con comprobar solo «si está firmado»; también hay que verificar «si está permitido instalar esa versión ahora».
3.7 Fail-open
Esto es lo que menos se debe hacer en producción.
- Si falla la verificación de firma, se muestra solo una advertencia y se continúa
- Existe un indicador oculto que permite ignorar el error de expiración del certificado
- El
skipVerify=truede depuración queda activo también en producción
Precisamente en un incidente o un ataque, este tipo de atajos se convierten en el vector principal.
4. Buenas prácticas
4.1 Primero, apoyarse en una infraestructura de actualización existente
Es más seguro cuestionar primero si realmente hace falta un actualizador propio.
En Windows, mientras los requisitos lo permitan, conviene priorizar la evaluación de estas opciones:
- MSIX + App Installer
- ClickOnce
- Store / MDM / infraestructura de distribución interna
- MSI + gestión de distribución por parte de la empresa
La razón es simple: se puede trasladar en buena medida a la plataforma la responsabilidad de la propia actualización. Por supuesto, se pierde libertad, pero resulta más fácil mantener la coherencia entre la interfaz de actualización, el manifest de distribución, la firma del paquete y la operación.
Un actualizador propio se vuelve necesario, por ejemplo, en estos casos:
- Se quiere controlar estrictamente varios canales: stable / beta / preview
- Se quiere disponer de distribución escalonada o una tasa de rollout
- Se quiere controlar con precisión el momento de la actualización por necesidades propias del negocio
- Hay una configuración que no encaja en MSIX / ClickOnce
Incluso en ese caso, conviene entenderlo no como «queremos libertad», sino como «asumimos nosotros mismos la responsabilidad de la actualización»; así el criterio no se desvía.
4.2 Mantener el origen de la confianza en el lado del cliente
Un actualizador seguro no confía tal cual en la respuesta del servidor. En el lado del cliente hacen falta, como mínimo, estas dos cosas:
- La clave pública o la cadena de certificados en la que se confía
- Un mecanismo para verificar la metadata firmada con esa clave
Dicho de otro modo, hay que construir un estado en el que el cliente pueda confirmar, no que «el servidor dice que es la última versión», sino que «esta metadata es la última versión emitida por un firmante en el que se confía».
A continuación se muestra en un diagrama cómo la confianza se encadena desde la raíz hasta el archivo.
flowchart TD
accTitle: Cadena de confianza de la actualización automática
accDescr: Diagrama que muestra cómo la confianza se encadena desde la clave raíz, pasando por la clave de firma de la metadata y la metadata firmada, hasta la verificación, la descarga en staging, la activación y la comprobación de salud, terminando en la finalización o en el rollback.
ROOT["Clave raíz<br/>trust anchor incrustado en el cliente"] --> SIGNKEY["Clave de firma de la metadata<br/>delegada desde la raíz y renovada con frecuencia"]
SIGNKEY --> META["Metadata de actualización firmada<br/>version / url / hash / size / expiry"]
META --> CHECK1{"Verificar firma, expiración y version"}
CHECK1 -- "NG" --> STOP["Cancelar la actualización<br/>fail-closed"]
CHECK1 -- "OK" --> DL["Descargar el artefacto al área de staging"]
DL --> CHECK2{"Verificar size / hash / firma del paquete"}
CHECK2 -- "NG" --> STOP
CHECK2 -- "OK" --> ACT["Activar conservando la versión anterior"]
ACT --> HEALTH{"Comprobación de salud en el primer arranque"}
HEALTH -- "NG" --> RB["Hacer rollback a la versión anterior"]
HEALTH -- "OK" --> DONE["Actualización completada"]
Hay dos puntos que conviene retener del diagrama. Primero, que la confianza se encadena de arriba abajo en una sola cadena. Segundo, que sin importar en qué punto de la cadena se rompa, el destino es siempre Cancelar la actualización o rollback, nunca avanzar de todos modos.
4.3 Diseñar en torno a signed metadata (metadata firmada)
Como mínimo, hay que incluir lo siguiente en la metadata de actualización y hacerlo objeto de firma.
| Elemento | Motivo para incluirlo |
|---|---|
| release version / release id | Prevención de rollback, auditoría |
| Nombre del artifact, URL, package type | Fijar qué archivo se debe obtener |
| hash, size | Detección de manipulación, detección de una distribución dañada |
| channel | No mezclar beta con stable |
| OS / arquitectura de destino | Prevención de distribución errónea |
| minimum updater version | Detener actualizadores antiguos cuando cambia el protocolo |
| expires_at | Medida contra freeze |
| published_at | Auditoría, análisis |
| mandatory / optional | Hacer que tampoco se pueda manipular la bifurcación de la UX de actualización |
Lo importante aquí es concentrar en la signed metadata todos los elementos que se usan para decidir la actualización. Adoptar la forma en que la lógica reside en el cliente y la autenticidad de la información se protege mediante la firma reduce los incidentes.
Como sin ver la forma concreta no se puede llevar esto a la implementación, se muestra a continuación un ejemplo mínimo. Primero, el contenido que es objeto de la firma.
{
"schema_version": 1,
"channel": "stable",
"release_version": "2.4.1",
"published_at": "2026-04-09T01:00:00Z",
"expires_at": "2026-04-16T01:00:00Z",
"minimum_updater_version": "2.0.0",
"minimum_allowed_version": "2.2.0",
"mandatory": false,
"artifacts": [
{
"os": "windows",
"arch": "x64",
"package_type": "msi",
"file_name": "MyApp-2.4.1-x64.msi",
"url": "https://updates.example.com/stable/MyApp-2.4.1-x64.msi",
"size": 48234496,
"sha256": "5f2c...64 dígitos hexadecimales..."
}
]
}
Esto se envuelve junto con la firma.
{
"signed": {
"schema_version": 1,
"channel": "stable",
"release_version": "2.4.1",
"_comment": "Se coloca tal cual el objeto anterior"
},
"signatures": [
{
"keyid": "3f9a...",
"sig": "MEUCIQ..."
}
]
}
El motivo de esta forma es que todos los valores usados para decidir la actualización están dentro de signed. Como la URL, la versión, el canal y el indicador de actualización obligatoria están todos dentro, sustituir solo la parte externa hace que la verificación falle. El orden de procesamiento en el cliente se fija como «verificar signed → si pasa, usar solo los valores de su interior». Una implementación que lea url y empiece a descargar antes de verificar anula el sentido de haber adoptado esta forma.
Hay un punto de atención en la implementación. En JSON, la secuencia de bytes cambia según el orden de las claves o cómo se insertan los espacios. Como la verificación de firma se hace sobre la secuencia de bytes, decida de antemano si el objeto de la firma será una «representación normalizada» o la «secuencia de bytes recibida tal cual». Si no se decide esto y el servidor firma el objeto que genera mientras el cliente firma el resultado de volver a serializarlo, la verificación fallará aunque la actualización sea legítima. Y si, al revés, se empieza a esquivar esta discrepancia pensando «bueno, dejémoslo pasar», eso es el primer paso hacia fail-open.
4.4 Verificar también el propio artefacto
Después de verificar la metadata, en el artefacto descargado también se comprueba lo siguiente:
- size
- hash
- Firma de paquete / firma de código
- El emisor y el identificador esperado
Si se trabaja con PE / MSI / MSIX de Windows, es más seguro dar por sentado que la verificación de AuthentiCode o de la firma del paquete se realiza en el lado del cliente. En macOS, dar por sentado también en la vía de actualización el Developer ID y la notarization mantiene el criterio sin desviaciones.
4.5 Proteger la clave mediante la operación, no solo mediante funciones
En la gestión de claves, la diferencia se nota más en la operación que en la implementación.
Como mínimo, es más seguro separar lo siguiente:
- Clave de firma de desarrollo
- Clave de firma de staging
- Clave de firma de producción
Y, además, para la de producción conviene incluir hasta:
- HSM
- Servicio de firma en la nube
- Un signing system con flujo de aprobación
- Registro de auditoría
- Un procedimiento de key rotation
- Firma con timestamp
Conviene diseñar todo esto hasta llegar a incluirlo.
Que «el CI firme automáticamente en cuanto pasa el build de producción» es cómodo, pero también amplía el radio de daño en caso de compromiso. Como mínimo, debería poder rastrearse quién firmó qué y cuándo.
Una vez que la operación esté rodando, resulta aún más seguro separar la clave del root trust, que apenas cambia, de la clave de la metadata de actualización, que se vuelve a firmar con frecuencia. Un diseño que mantiene el root más cerca de estar offline y usa una clave distinta para la metadata de actualización facilita reducir el radio de daño en caso de compromiso de una clave.
4.6 Fail-closed y staged update (actualización por etapas)
El flujo de actualización sigue, básicamente, este orden:
- Obtener la metadata
- Verificar la firma, la fecha de expiración y la versión
- Descargar el artefacto al área de staging
- Verificar hash / size / firma
- Preparar la activación conservando la versión anterior
- Cambiar de versión al reiniciar o mediante un helper dedicado
- Comprobación de salud en el primer arranque
- Si hay problemas, rollback
Lo importante aquí son estas dos cosas: No reemplazar antes de que termine la verificación No avanzar si falla
4.7 Restringir los permisos del actualizador
Conviene evitar ejecutar todo el actualizador con permisos de administrador.
Lo ideal es la siguiente separación:
- Descarga y verificación: permisos bajos
- Solo el reemplazo real del archivo: un helper con permisos mínimos
- El helper no hace más que «colocar un paquete ya verificado en el lugar establecido»
Cuanto más necesita el diseño una elevación de permisos, más peligroso resulta si no se separa con claridad qué está ya verificado antes de esa elevación.
4.8 Eliminar rollback, freeze y mix-and-match desde el principio
Esto es doloroso de añadir después, así que conviene incorporarlo desde el principio.
-
Medida contra rollback. El cliente conserva «la metadata version / release version más alta que ha visto hasta ahora» y rechaza cualquiera anterior a esa.
-
Medida contra freeze. Se da a la metadata un campo de expiración y se rechaza la metadata demasiado antigua.
-
Medida contra mix-and-match. Se mantiene la coherencia entre las distintas piezas de metadata. Como mínimo, se fija en el propio manifest el hash / size / version del artifact de destino.
Además, si se puede distribuir mediante signed metadata un blocklist que rechace un build específico, o un minimum allowed version, la contención en caso de incidente se acelera.
Aunque no se adopte TUF tal cual, estas tres propiedades son bastante importantes.
4.9 Empezar con actualización completa
La actualización diferencial ayuda con el ancho de banda, pero como primera implementación resulta compleja.
- Desde qué versión antigua hacia qué versión nueva se aplica el diferencial
- El hash previo que se asume antes de aplicar el diferencial
- El hash final tras aplicar el diferencial
- La recuperación en caso de fallo a mitad de proceso
- La aplicación parcial y la limpieza de diferenciales antiguos
Todo esto se multiplica de golpe. Para la versión inicial, basta con llegar hasta reemplazar de forma segura el paquete completo ya firmado.
5. Configuración mínima segura
Aunque no se llegue a algo tan elaborado como TUF completo, la configuración mínima segura de un actualizador propio suele adoptar, más o menos, esta forma.
5.1 Lo que tiene el cliente
Lo que tiene el cliente es el material para poner en duda la respuesta del servidor. Si esto está vacío, la decisión de si se debe actualizar o no queda solo en manos de lo que diga el servidor.
- La clave pública root en la que se confía, o una cadena de certificados fijada
- La versión actualmente en ejecución
- La metadata version / release version más alta vista hasta ahora
- Los canales permitidos
- La versión inmediatamente anterior, para el rollback
5.2 Lo que devuelve el servidor
Lo que devuelve el servidor no es la decisión en sí, sino el material para tomarla. Ninguno de estos elementos se confía por sí solo; solo adquieren sentido después de pasar por la verificación que usa el trust anchor de 5.1.
- La update metadata firmada
- El artefacto ya firmado, o firmado por la plataforma
- Si es necesario, la información de blocklist / minimum allowed version
5.3 Flujo típico
Obtener la metadata
↓
Verificar firma, expiry, version y channel
↓
Descargar el artefacto a staging
↓
Verificar size / hash / package signature
↓
Activación conservando la versión anterior
↓
Si falla el primer arranque, rollback
Lo importante aquí es que la sola respuesta del servidor de actualizaciones no basta para que nada se sostenga. Lo que lo hace posible es el trust anchor que tiene el cliente, junto con la lógica de verificación.
6. Cómo pensarlo en proyectos Windows
En las aplicaciones Windows, resulta más fácil ordenar las ideas partiendo primero del método de distribución.
- Si los requisitos lo permiten, MSIX App Installer
- Si se trata de una aplicación interna en .NET y per-user encaja, ClickOnce
- Si hace falta llegar a servicios, drivers, shell extensions o un control de canal propio, MSI + un actualizador propio también entra en la comparación
Sin embargo, aunque se elija un actualizador propio, lo que hay que hacer no disminuye. Más bien aumenta.
- Verificación de Authenticode / firma de paquete
- signed manifest
- Medidas contra rollback
- Separación de permisos del helper de actualización
- Estrategia de actualización del propio actualizador
6.1 Cómo verificar la firma Authenticode en el lado del cliente
Escribir solo «verificar Authenticode» no basta para llevarlo a la implementación, así que se enumeran a continuación los puntos de entrada en Windows.
| Qué se quiere hacer | Medio |
|---|---|
| Verificar dentro del propio código del actualizador | La API WinVerifyTrust (wintrust.dll). Al especificar WINTRUST_ACTION_GENERIC_VERIFY_V2 se aplica la política de verificación de Authenticode |
| Comprobar desde un procedimiento operativo o desde el CI | Get-AuthenticodeSignature de PowerShell |
| Firmar y comprobar durante el trabajo de release | signtool sign / signtool verify del Windows SDK |
En PowerShell, la comprobación mínima se reduce a esto.
$path = ".\MyApp-2.4.1-x64.msi"
$sig = Get-AuthenticodeSignature -FilePath $path
if ($sig.Status -ne 'Valid') {
throw "signature check failed: $($sig.Status) / $($sig.StatusMessage)"
}
# No basta con que "la firma sea válida"; hay que fijar también de quién es.
# Sin embargo, el Subject (nombre distintivo) no es único. Un certificado con
# el mismo CN/O/C puede emitirlo cualquier otra CA, y si este equipo confía
# en esa CA, el Status será Valid y también pasará la comparación de Subject.
# Lo que hay que fijar es la "cadena del emisor" o la clave pública;
# el Subject solo sirve como apoyo adicional.
$expectedIssuers = @( # huella (thumbprint) del emisor (CA intermedia/raíz)
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B' # <- sustituir por el valor real
)
$expectedSubject = 'CN=Example Software Inc., O=Example Software Inc., C=JP'
# La cadena que se reconstruye aquí sirve solo para "recorrer el emisor";
# la decisión de si es confiable ya se resolvió arriba con Status = Valid.
# De forma predeterminada, X509ChainPolicy.VerificationTime es la hora en
# que se llamó al constructor (= ahora), así que al hacer Build tal cual,
# en cuanto caduque el certificado de firma, fallará con NotTimeValid. Una
# firma con timestamp sigue siendo Valid tras caducar, así que dejar esto
# por defecto convierte al actualizador en uno que "rechaza de golpe, en
# cuanto se renueva el certificado, releases pasados que se verificaron
# correctamente". Si se conoce la hora de la firma, se pone en
# VerificationTime; si no se conoce, la validez ya se comprobó arriba,
# así que este Build no la evalúa
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.VerificationFlags = 'IgnoreNotTimeValid'
$chain.ChainPolicy.RevocationMode = 'NoCheck'
try {
$built = $chain.Build($sig.SignerCertificate)
# Comprobar que el emisor esperado está en algún punto entre el
# firmante y la raíz. Si Build falla, la cadena solo queda completada
# a medias, así que también en ese caso esta comprobación lo rechaza
# (fail-closed)
$chainThumbprints = @($chain.ChainElements | ForEach-Object { $_.Certificate.Thumbprint })
if (-not ($expectedIssuers | Where-Object { $chainThumbprints -contains $_ })) {
throw "unexpected issuing chain (built=$built): $($chainThumbprints -join ' / ')"
}
}
finally {
$chain.Dispose()
}
if ($sig.SignerCertificate.Subject -ne $expectedSubject) {
throw "unexpected signer: $($sig.SignerCertificate.Subject)"
}
Hay cinco restricciones que conviene tener presentes en este punto.
- Que
StatusseaValidsolo significa que “es correcto como firma”. De quién es la firma se comprueba aparte - Que
Subjectcoincida no demuestra “ser el titular real”. El nombre distintivo no es un identificador único: tanto una CA interna de empresa como una CA pública pueden emitir un certificado de firma de código con el mismo nombre distintivoCN=Example Software Inc., O=Example Software Inc., C=JP. Si el cliente confía en esa CA, una versión sustituida, firmada con otra clave y otro emisor, pasa igualmente conStatus = Validy coincidencia deSubject. Lo que hay que fijar es la cadena del emisor (que la huella de la CA intermedia / raíz esperada esté en la ruta que va del firmante a la raíz) o la clave pública, y usarSubjectsolo como filtro adicional sobre eso - Precisamente por fijar este valor, hay que decidir de antemano el procedimiento de cambio. De lo contrario, se rompe de la peor manera: el día en que se cambia el certificado o la CA, la actualización se detiene en todos los equipos. El valor esperado debe mantenerse siempre como un “array”, de modo que se pueda aceptar el antiguo y el nuevo en paralelo antes de hacer el cambio (en tres pasos: primero distribuir un actualizador que ya tenga el nuevo emisor añadido a la lista permitida, después cambiar la firma, y por último eliminar el valor antiguo). Como este “distribuir primero” solo es posible cuando el propio actualizador puede actualizarse, diséñelo junto con la “estrategia de actualización del propio actualizador” mencionada al inicio del capítulo 6
- Una firma sin timestamp deja de pasar la verificación en cuanto caduca el certificado. Hay que añadir siempre un timestamp en el momento del release. Esta es la razón por la que en 4.5 se menciona la firma con timestamp
- No deje que el
X509Chainusado para recorrer el emisor vuelva a juzgar la vigencia con la hora actual. De forma predeterminada,X509ChainPolicy.VerificationTimees la hora en que se llamó al constructor, es decir, ahora. La propia documentación indica que “al verificar un mensaje firmado, la firma debe ser válida en el momento de la firma, no en el momento de la verificación, por lo que esta propiedad es importante”. Si se deja este valor por defecto, se acaba rechazando, justo después, como “certificado caducado” un paquete que el timestamp había hecho válido (Status = Valid). Eso anula el sentido de haber añadido el timestamp y, además, hace que todos los releases pasados dejen de pasar en el instante en que se renueva el certificado. Si se conoce la hora de la firma, colóquela enVerificationTime; si no se conoce, useIgnoreNotTimeValid. Esto no es debilitar la verificación. La decisión sobre la vigencia y la confianza ya se resolvió un paso antes, conStatus = Valid; esteBuildsirve únicamente para averiguar “quién lo emitió”. Por la misma razón, tampoco se comprueba aquí la revocación (la CRL de un certificado ya caducado no siempre sigue publicándose). El medio para detener algo cuando se filtra una clave no es la CRL, sino el blocklist y el minimum allowed version (4.8)
Además, el resultado de la verificación depende del almacén de certificados y de la configuración de confianza de ese equipo. En un entorno donde la configuración de confianza del cliente es laxa, el significado de Status = Valid también se vuelve laxo. Si, como se mostró arriba, el propio actualizador mantiene el emisor que espera, no se ve afectado aunque la configuración de confianza del equipo sea más amplia.
Una forma peligrosa habitual en Windows es la secuencia directa DownloadFile -> unzip -> kill process -> overwrite -> restart.
Esto puede funcionar, pero es débil tanto en seguridad como en capacidad de recuperación.
Una operación en la que se hace pasar las advertencias de SmartScreen o UAC con «Más información → Ejecutar de todos modos» no es diseño de actualización, sino habituación a la advertencia. Si se quiere construir una vía de actualización correcta, en lugar de acostumbrar a la gente a la advertencia, hay que orientarse hacia una configuración de distribución y verificación en la que la advertencia rara vez aparezca.
La comparación de los métodos de distribución en sí se organiza también en este otro artículo. Cómo elegir el método de distribución de aplicaciones de Windows - MSI/MSIX/ClickOnce/xcopy/actualizador propio
7. Lista de verificación mínima
Antes de publicar un actualizador propio, conviene comprobar, como mínimo, lo siguiente.
- La metadata de actualización está firmada
- La metadata incluye version / hash / size / channel / expiry
- Se verifica la firma y la versión en el lado del cliente
- La fijación del firmante se hace mediante la cadena del emisor o la clave pública, no mediante el nombre distintivo (Subject)
- Está definido el procedimiento de cambio del valor fijado (un período en el que se aceptan en paralelo el antiguo y el nuevo)
- Se verifica el hash del artefacto y la firma de la plataforma
- La clave de firma de producción está separada del entorno de desarrollo
- Quedan el registro de uso de la clave y el registro de aprobación
- Se usa una firma con timestamp
- Con la actualización en staging, se cambia de versión conservando la anterior
- Existen las condiciones y el procedimiento de rollback
- Ante un fallo de verificación, se detiene con fail-closed
- Existe una política de actualización para el propio actualizador
- Se puede distribuir un blocklist / minimum allowed version
- Existe un kill switch para detener la distribución escalonada
- Se pueden observar la tasa de fallos, la tasa de rollback y los fallos de verificación de firma
Si esta lista de verificación tiene muchos huecos, resulta más efectivo terminar de definir el modelo de confianza de distribución antes que construir primero la interfaz del actualizador.
8. Resumen
Al final, lo que hay que pensar sobre la seguridad de la funcionalidad de actualización automática se reduce a esto.
No la comodidad de la actualización, sino diseñar a quién se confía y cómo verifica el cliente esa confianza.
Sobre esa base, dicho a grandes rasgos, el criterio práctico es este:
- Si una infraestructura ya existente basta, apoyarse primero en ella
- Si se construye un actualizador propio, incorporar la metadata firmada y la gestión de claves antes que HTTPS
- Un actualizador que no diseña la recuperación ante fallos ni el rollback resulta doloroso en producción
- El actualizador no es una función de distribución, sino el propio límite de seguridad del producto
Si la configuración actual se parece a latest.json + sustitución de zip, lo primero que hay que corregir no es el proceso de descarga, sino la forma en que se deposita la confianza.
Con solo corregir esto, el nivel de riesgo cambia considerablemente.
9. Referencias
- CISA Secure by Design Pledge
- NIST: Security Considerations for Code Signing
- NIST Secure Software Development Framework (SSDF)
- The Update Framework Specification
- TUF: Roles and metadata
- TUF: Security
- Microsoft Learn: Authenticode Digital Signatures
- Microsoft Learn: WinVerifyTrust function
- Microsoft Learn: Get-AuthenticodeSignature
- Microsoft Learn: Auto-update and repair apps - MSIX
- Microsoft Learn: ClickOnce Deployment and Security
- Apple Developer: Developer ID
- CA/Browser Forum: Baseline Requirements for the Issuance and Management of Publicly-Trusted Code Signing Certificates
Temas relacionados
Esta es una página de temas cercana a este artículo. A partir del artículo, se puede avanzar hacia servicios relacionados u otros artículos.
Temas técnicos de Windows
Es la puerta de entrada que reúne los temas técnicos de desarrollo Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
Desarrollo de aplicaciones Windows
La actualización automática no es solo cuestión de interfaz de usuario: es un diseño que incluye el método de distribución, los permisos, la recuperación y la operación. En el desarrollo nuevo de una aplicación Windows o en la revisión de un software existente, se puede empezar por ordenar el método de actualización.
Consultoría técnica y revisión de diseño
Se puede consultar desde la etapa de ordenar ideas, con preguntas como «¿hace falta un actualizador propio?», «¿basta con MSIX / ClickOnce?» o «¿qué parte del diseño de actualización actual es peligrosa?».
Perfil del autor
Go Komura
Representante de KomuraSoft LLC
Se especializa en desarrollo de software Windows, consultoría técnica e investigación de fallos, con fortaleza particular en proyectos con activos existentes y en la investigación de fallos de causa poco visible.
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Por qué aparece «Windows protegió su PC» en Windows
Analiza por qué aparece SmartScreen al distribuir apps de Windows: firma de código, certificados EV/OV, Azure Artifact Signing, MSIX, Sto...
Cómo elegir el método de distribución de aplicaciones de Windows - MSI/MSIX/ClickOnce/xcopy/actualizador propio
El método de distribución de una app de Windows no es una preferencia de formato de instalador, sino la elección del grado de integración...
Práctica de CI/CD para aplicaciones WinForms / WPF — automatizar desde la compilación hasta la firma y la distribución con GitHub Actions
CI/CD para WinForms/WPF con GitHub Actions: build y pruebas, versión por tags, firma con signtool y tabla de decisión por formato de dist...
Cuando su aplicación Windows de desarrollo propio es tratada como virus — cómo abordar los falsos positivos de Microsoft Defender y su impacto en el rendimiento
Procedimiento oficial para resolver falsos positivos de Microsoft Defender en apps Windows propias: por qué ocurren, cómo reportarlos, re...
¿Qué es ClickOnce? - Cómo funciona, actualizaciones y en qué casos conviene o no usarlo, desde la práctica
ClickOnce distribuye apps de escritorio Windows en .NET: manifiestos, actualizaciones, caché, firma y en qué casos conviene usarlo, con d...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
La distribución, la actualización, la firma, el rollback y la elección entre MSIX / ClickOnce de una aplicación Windows requieren pensar no solo en la implementación, sino también en el método de distribución y en el diseño operativo.
Consultoría técnica y revisión de diseño
El límite de confianza de la actualización automática, la metadata firmada, la operación de claves y el diseño fail-closed dependen más de ordenar la arquitectura general que de una implementación puntual.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿La actualización automática es segura solo con que se comunique por HTTPS?
- TLS es necesario, pero no es una condición suficiente. Lo que protege TLS es principalmente la vía de comunicación y la legitimidad del destino; no cubre los casos en que el propio servidor de actualizaciones ha sido comprometido, se ha colocado un artefacto incorrecto en la CDN oficial, o se ha sustituido un manifest sin firmar. La decisión de actualizar no debe basarse en «porque el servidor lo dice», sino en que «el cliente ha verificado la metadata firmada y ha podido determinar que es correcta».
- ¿Qué debe incluirse y firmarse en la metadata de actualización?
- Lo básico es concentrar en la metadata firmada todos los elementos que se usan para decidir la actualización. En concreto: la versión de release, la URL y el nombre de archivo del artefacto, el hash y el tamaño, el canal (stable / beta, etc.), el sistema operativo o arquitectura de destino, la versión mínima del actualizador, la fecha de expiración de la metadata (expires_at), y un indicador de si la actualización es obligatoria, entre otros. Si solo se firma el binario y el manifest queda sin firmar, sigue existiendo margen para manipular la URL, la versión o el indicador de actualización obligatoria.
- ¿Qué es un ataque de rollback? ¿Cómo se previene?
- Es un ataque en el que, aunque se trate de una versión legítima y firmada, un atacante vuelve a distribuir una versión antigua con vulnerabilidades conocidas para forzar el retroceso a esa versión vulnerable. Como la firma en sí es correcta, la sola verificación de la firma no lo evita. Como medida, el cliente debe conservar la versión de release más alta que haya visto hasta el momento y rechazar cualquiera anterior a esa. Además, conviene dar a la metadata una fecha de expiración para evitar ataques de freeze que oculten las versiones nuevas, y fijar en el manifest el hash, el tamaño y la versión del artefacto para neutralizar también los ataques de mix-and-match.
- ¿Conviene crear un actualizador propio o usar un mecanismo ya existente?
- Si los requisitos lo permiten, lo más seguro es priorizar primero una infraestructura de actualización ya existente, como MSIX App Installer o ClickOnce, porque así se puede trasladar buena parte de la responsabilidad de la actualización a la plataforma. Un actualizador propio se vuelve necesario cuando hay requisitos que no encajan en esas infraestructuras, como un control estricto de múltiples canales o una distribución escalonada. Incluso en ese caso, lo primero que hay que incorporar no es la interfaz de usuario, sino la verificación de firma y la recuperación ante fallos: hay que respetar el principio fail-closed (no avanzar si la verificación falla) y permitir el cambio de versión conservando la anterior, de modo que sea posible hacer rollback.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.