Las actualizaciones automáticas, y por qué hace falta una capa más de firma

En la Parte 1 terminamos la configuración del certificado Developer ID y de la notarización. Con eso ya tenemos listo lo necesario para entregar la app al usuario por primera vez. Pero una app no se distribuye una sola vez y se acabó: hay que seguir publicando nuevas versiones que corrigen errores y añaden funciones.

En el caso de una app de la Mac App Store, la App Store se encarga de las actualizaciones por ti. Las apps de distribución directa no funcionan así, de modo que tienes que incorporar tú mismo una función de actualización automática dentro de la app. En macOS, el estándar de facto para esta tarea es el framework de código abierto Sparkle. Si incorporas Sparkle, la app comprueba periódicamente un “feed de actualizaciones (appcast)” y, si hay una nueva versión, avisa al usuario, la descarga y la instala.

Aquí surge una duda. Si ya firmamos la app con el certificado Developer ID creado en la Parte 1, ¿por qué hace falta otra clave más?

La razón es que las dos firmas verifican cosas distintas.

  • Certificado Developer ID — lo usa Gatekeeper de macOS para decidir “si esta app se puede instalar
  • Clave EdDSA de Sparkle — la usa el Sparkle dentro de la app para decidir “si el archivo de actualización que se acaba de descargar lo creó realmente el desarrollador de esta app

La actualización automática es una operación muy delicada en términos de seguridad: descarga un archivo de internet y se sobrescribe a sí misma. Si alguien interceptara el servidor de actualizaciones o la ruta de comunicación e insertara un archivo falso, sería un problema grave. Para impedirlo, Sparkle solo acepta actualizaciones firmadas con la clave privada que solo tiene el desarrollador, y rechaza la instalación si la firma no coincide. Es, por tanto, una capa de verificación independiente del certificado.

En este artículo creamos el par de claves EdDSA (Ed25519) que se usará en esa verificación.

Igual que en la Parte 1, todos los nombres y rutas (FocusTimer, example.com, etc.) son valores de ejemplo. En la práctica, sustitúyelos por los datos de tu propia app.

Requisito previo — la app debe tener Sparkle añadido

Antes de crear la clave, el proyecto de la app debe tener el framework Sparkle como dependencia. Si todavía no lo tiene, añádelo en Xcode mediante el Swift Package Manager (SPM).

  1. Abre el proyecto en Xcode → File → Add Package Dependencies…
  2. Escribe la dirección del repositorio en la caja de búsqueda: https://github.com/sparkle-project/Sparkle
  3. Deja la regla de versión en 2.x (el último major) y añádelo

Después de esto, compila una vez el proyecto para que SPM descargue el paquete Sparkle. Las herramientas de línea de comandos que vienen incluidas con él son la clave del siguiente paso.

Paso 1 — Localizar las herramientas de línea de comandos de Sparkle

El paquete Sparkle incluye herramientas de línea de comandos que se usan para generar y firmar claves. Estas herramientas están dentro de la carpeta donde SPM ha descargado el paquete, pero esa ubicación varía según la versión de Xcode y la configuración de DerivedData. Por eso es más seguro localizarlas directamente.

SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \
  -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
echo "$SPARKLE_BIN"

Si se imprime una línea con una ruta, ha funcionado. Si no aparece nada, es que falta el “compila una vez el proyecto” del paso anterior; ejecuta una compilación en Xcode y vuelve a probar.

Dentro de esa carpeta están las siguientes herramientas.

  • generate_keys — generar, respaldar, restaurar y verificar la clave de firma (la que usamos en este artículo)
  • sign_update — firmar el archivo de actualización (se usa en la publicación real)
  • generate_appcast — generar el feed de actualizaciones (appcast.xml) (aparece en la Parte 3)

Paso 2 — Generar la clave de firma

Ahora creamos el par de claves.

"$SPARKLE_BIN/generate_keys"

Aparece una salida parecida a la siguiente.

Generating a new signing key...
A key has been generated and saved in your keychain. Add the SUPublicEDKey
key to the Info.plist of each app for which you intend to use Sparkle...

    <key>SUPublicEDKey</key>
    <string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>

Con este único comando se crean dos claves.

  • Clave pública (public key) — el valor SUPublicEDKey que se ve en la salida de arriba. No es secreta y es la clave que vas a incrustar dentro de la app.
  • Clave privada (private key) — no aparece en pantalla. Se guarda automáticamente en el Keychain de macOS como una entrada llamada “Private key for signing Sparkle updates”. Es el verdadero secreto, que nunca debe quedar en el disco como un archivo en texto plano.

Incrustar la clave pública en la app

Pon la cadena de la clave pública impresa en el Info.plist de la app. Tomando como referencia la app de ejemplo, añade las siguientes claves a FocusTimer-Info.plist.

<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
  • SUPublicEDKey — la clave pública que acabas de generar. La app verifica con esta clave la firma de las actualizaciones que descarga.
  • SUFeedURL — la dirección del feed de actualizaciones. Este dominio aún no existe; lo crearemos en la Parte 3. Por ahora es solo dejar el sitio reservado.

Como la clave pública está incrustada en la app y la clave privada solo la tiene el desarrollador, esa app solo aceptará actualizaciones firmadas con la clave privada. Esta es la estructura central de la verificación de actualizaciones de Sparkle.

Paso 3 — Copia de seguridad de la clave privada (¡imprescindible!)

Si te saltas este paso, puedes arrepentirte mucho más adelante.

La clave privada está guardada en el Keychain, así que en el ordenador que usas ahora no hay problema. Pero si pierdes el ordenador, se estropea el disco o reinstalas macOS, esta clave desaparecerá junto con él.

¿Qué pasa si desaparece la clave privada? Una actualización firmada con una clave nueva será rechazada en la verificación por la app de los usuarios existentes (la app que tiene incrustada la clave pública antigua). Es decir, no podrás volver a enviar nunca actualizaciones automáticas a los usuarios que ya están usando la app. No te quedará más remedio que avisar a cada usuario, uno por uno, de que “descargue e instale de nuevo la nueva versión por su cuenta”.

Por eso, justo después de crear la clave, haz la copia de seguridad de inmediato.

"$SPARKLE_BIN/generate_keys" -x ~/focustimer-sparkle-private.key
cat ~/focustimer-sparkle-private.key

La cadena base64 de una sola línea que imprime cat es la clave privada. Ejemplo:

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=

Guarda esta cadena como una nota segura (secure note) en un gestor de contraseñas como 1Password. Ponle a la nota un nombre que sea fácil de encontrar después, por ejemplo FocusTimer Sparkle EdDSA Private Key.

Justo después de confirmar que se ha guardado, elimina el archivo en texto plano que ha quedado en el disco.

rm ~/focustimer-sparkle-private.key

La regla es no dejar la clave privada abandonada en el disco como un archivo en texto plano. Guarda la copia de seguridad únicamente dentro de un gestor de contraseñas cifrado.

Un escollo clave — el símbolo % no forma parte de la clave

Al imprimir la clave con cat, la terminal (especialmente zsh) puede mostrar un símbolo % al final de la línea.

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%

Este % es solo una marca del shell que indica “la salida ha terminado sin un salto de línea”; no forma parte de la clave. Si copias también ese % en la copia de seguridad, la clave quedará corrupta cuando vayas a restaurarla más adelante. Las cadenas base64 suelen terminar en =, así que guarda la clave sin el % que va después del =.

Paso 4 — Restaurar en otro ordenador

Cuando tengas que compilar la app en un ordenador nuevo, vuelve a meter en el Keychain la clave privada que respaldaste.

echo "cadena_base64_respaldada" > ~/focustimer-sparkle-private.key
"$SPARKLE_BIN/generate_keys" -f ~/focustimer-sparkle-private.key
rm ~/focustimer-sparkle-private.key

La opción -f significa “importar al Keychain la clave contenida en el archivo”. Cuando termine la restauración, elimina aquí también de inmediato el archivo en texto plano.

Paso 5 — Comprobación

Comprobamos que la clave ha entrado correctamente.

"$SPARKLE_BIN/generate_keys" -p

Se imprime una línea con la clave pública. Este valor debe coincidir exactamente con el que pusiste en SUPublicEDKey del Info.plist en el Paso 2. Si es distinto, significa que la clave pública incrustada en la app y la clave de firma real no coinciden, con lo que la verificación de la actualización fallará.

Resumen de la Parte 2

Si has seguido hasta aquí, ahora ya tienes lo siguiente.

  • ✅ Par de claves EdDSA de Sparkle generado (clave pública + clave privada)
  • ✅ Clave pública incrustada en el Info.plist de la app (SUPublicEDKey)
  • ✅ Copia de seguridad de la clave privada guardada de forma segura en un gestor de contraseñas
  • ✅ Método de restauración en otro ordenador asimilado

Ahora la app ya tiene un medio para verificar “si la actualización descargada es auténtica”. Pero falta una cosa. En la Parte 2 escribimos https://updates.example.com/appcast.xml en SUFeedURL, pero en esa dirección todavía no hay nada.

En la siguiente parte crearemos un repositorio público donde subir el feed de actualizaciones (appcast.xml) y los archivos .dmg, lo conectaremos a un dominio que controlamos nosotros, y terminaremos los ajustes de compilación para concluir la configuración inicial.