Son las dos de la madrugada. Tu app compila. La firmas. La empaquetas en un DMG. Ejecutas notarytool submit. Apple dice «In Progress». Esperas 5 minutos. 10. 20. Una hora. Dos horas. La submission sigue «In Progress». Te acuestas. A la mañana siguiente: Invalid.
Sin más explicación que «The signature of the binary is invalid». Para ambas arquitecturas. Gracias, Apple. Muy útil.
La notarización es uno de esos procesos que funciona a la perfección… hasta que no funciona. Y cuando falla, te deja con un .dmg que Gatekeeper no va a dejar abrir y un error que no te dice nada. Después de pelearme con esto durante un par de días con Tokamak (mi app de menu bar para monitorizar la cuota de Claude), decidí documentar todo lo aprendido y escribir un linter para no volver a pasar por esto.
Qué es la notarización (dicho en cristiano)
Imagina que la Mac App Store es un centro comercial con guardia de seguridad. Pero tú no quieres vender en el centro comercial — quieres distribuir tu app directamente, con tu propio DMG. Un puesto callejero.
Apple dice: «Vale, puedes. Pero antes pasa por el portero.»
Ese portero es la notarización. Es un servicio automatizado de Apple que escanea tu app firmada, verifica que no contiene malware conocido, y si todo está bien, te da un ticket. Ese ticket lo grapas a tu DMG (stapler staple) y a partir de entonces, cuando un usuario lo descargue e intente abrirlo, Gatekeeper ve el ticket y dice «adelante».
Sin ese ticket, el usuario ve esto:
«Tokamak.app» no se puede abrir porque Apple no puede comprobar que no contiene software malicioso.
Y tu app se queda en la cuneta.
Por qué Apple hace esto
Hay dos razones. Una legítima. Otra… bueno.
La legítima: proteger a los usuarios. Antes de la notarización (introducida en macOS 10.14.5, 2019), cualquiera podía distribuir un .app firmado con Developer ID y macOS lo abría sin rechistar. El code signing verificaba la identidad del desarrollador, pero no escaneaba el contenido. Si tu app firmada incluía un keylogger, firmado y todo se ejecutaba.
La notarización añade una capa: Apple escanea el binario en busca de malware conocido y comportamientos sospechosos antes de que llegue al usuario. No es una revisión humana como la App Store — es un sistema automatizado. Pero es algo.
La otra razón: control. Apple quiere que pases por App Store Connect para todo. La distribución directa con Developer ID siempre ha sido el ciudadano de segunda clase. La notarización es un paso más en ese camino de «puedes distribuir fuera de la App Store, pero te lo vamos a poner incómodo».
Dicho esto, la notarización es obligatoria desde macOS 10.15 para toda app distribuida fuera de la App Store. No es opcional. Tu app va notarizada o no se abre. Punto.
Los 7 errores que te van a hacer perder horas
Después de recopilar errores propios y de los foros de desarrolladores, estos son los que más duelen:
1. Falta el --timestamp
Este es el clásico. Tu codesign funciona perfecto en local, Gatekeeper no se queja, y la notarización devuelve «The signature of the binary is invalid.»
# MAL — firma válida localmente, Apple la rechaza
codesign --force --options runtime --sign "Developer ID Application: ..." MiApp.app
# BIEN — con timestamp del servidor de Apple
codesign --force --options runtime --timestamp --sign "Developer ID Application: ..." MiApp.app
El secure timestamp demuestra que la firma se hizo mientras el certificado era válido. Sin él, Apple no se fía. Es como firmar un contrato sin fecha — técnicamente es válido, pero nadie lo va a aceptar.
2. Certificado incorrecto
Hay tres certificados que suenan parecido y hacen cosas diferentes:
| Certificado | Para qué |
|---|---|
| Apple Development | Builds de debug (desarrollo local) |
| Apple Distribution | App Store y TestFlight |
| Developer ID Application | Distribución directa + notarización |
Si firmas con «Apple Development» e intentas notarizar, Apple te manda a paseo. Necesitas Developer ID Application. Parece obvio, pero cuando llevas tres horas intentándolo, no lo es.
3. Hardened Runtime desactivado
La notarización exige que tu app use el Hardened Runtime. Es una protección que impide que tu app haga cosas como inyectar código en otros procesos o deshabilitar protecciones de memoria.
# El flag --options runtime activa el Hardened Runtime
codesign --force --options runtime --timestamp --sign "Developer ID Application: ..." MiApp.app
Sin --options runtime, la firma es válida pero Apple la rechaza. Es como ir a una entrevista con el currículo perfecto pero sin pantalones.
4. xcodebuild -exportArchive se cuelga
Si usas xcodebuild -exportArchive con method: developer-id, prepárate: se cuelga. Indefinidamente. Sin output. Sin error. Solo… silencio.
El problema es que exportArchive necesita credenciales de Apple ID para contactar con los servidores de distribución, y si no las tiene cacheadas (o si hay cuentas viejas corruptas en el Keychain), se queda esperando una autenticación interactiva que nunca llega.
La solución: saltarse exportArchive y hacer la firma manualmente.
# 1. Copiar .app del archive
cp -R build/Tokamak.xcarchive/Products/Applications/Tokamak.app build/export/
# 2. Firmar con codesign
codesign --force --options runtime --timestamp \
--sign "Developer ID Application: Tu Nombre (TEAM_ID)" \
build/export/Tokamak.app
# 3. Crear DMG
hdiutil create -volname "MiApp" -srcfolder build/export/MiApp.app -format UDZO MiApp.dmg
Chapuza, sí. Pero funciona. Y no se cuelga.
5. codesign pide acceso al Keychain… en silencio
La primera vez que firmas con un certificado nuevo, codesign necesita acceso a la clave privada almacenada en el Keychain. macOS muestra un popup pidiendo permiso.
El problema: si ejecutas codesign desde un IDE, un script o una herramienta como Claude Code, el popup puede no aparecer. O aparecer detrás de todas las ventanas. Y codesign se queda colgado esperando tu respuesta a un popup que no ves.
Solución: la primera vez, ejecuta codesign directamente desde la terminal. Dale a «Permitir siempre» en el popup. A partir de entonces, los scripts funcionarán sin preguntar.
6. La notarización tarda… y tarda… y tarda
Apple dice que «most uploads are processed within minutes.» La realidad:
- Normal: 2-15 minutos
- Primera vez con Developer ID nuevo: hasta 1 hora
- Cuando Apple tiene problemas: horas. O días. Sin aviso.
En febrero de 2026, hay reportes en los foros de Apple de submissions atascadas en «In Progress» durante más de 16 horas. El servicio estaba técnicamente «operativo», pero muchas submissions simplemente no se procesaban.
No hay nada que puedas hacer excepto esperar y reintentar. Bienvenido a la developer experience de Apple.
7. Extended attributes que rompen la firma
macOS añade extended attributes a los ficheros descargados (el famoso com.apple.quarantine). Si tu pipeline de build copia ficheros que tienen estos atributos, la firma se invalida.
# Limpiar antes de firmar
xattr -cr MiApp.app
Es un detalle que nadie te cuenta hasta que te muerde.
El linter: 25 checks para no ir con el culo al aire
Después de acumular todos estos errores, escribí un script que verifica 25 condiciones de notarización — tanto estáticas (tu código y configuración) como dinámicas (los artefactos de build).
Se ejecuta así:
bash scripts/lint-notarize.sh
Y produce algo como:
── CRITICAL ──
✓ PASS C1: export-direct pasa --entitlements a codesign
✗ FAIL C2: Release CODE_SIGN_IDENTITY no es Developer ID
✓ PASS C3: ENABLE_HARDENED_RUNTIME = YES en Release
── HIGH ──
✗ FAIL H1: export-direct NO ejecuta xattr -cr
── MEDIUM ──
⚠ WARN M1: DMG no se firma
✓ PASS M2: LSUIElement declarado en project.yml
⚠ WARN M4: Solo se hace staple del DMG, no del .app
── CERTIFICADOS Y KEYCHAIN ──
✓ PASS P1: Certificado Developer ID Application en Keychain
✓ PASS P2: Perfil notarytool 'tokamak-notary' configurado
── ARTEFACTOS ──
✓ PASS P13: Binary universal (x86_64 + arm64)
✓ PASS C2-V: Export firmado con Developer ID Application
✓ PASS C3-V: Hardened runtime activo en firma del export
── RESUMEN ──
Total: 25 checks
✓ 18 passed
✗ 2 failed
⚠ 3 warnings
⊘ 2 skipped (build artifacts missing)
✗ NO NOTARIZAR — hay 2 fallos que corregir primero.
Los checks se dividen en cuatro niveles:
CRITICAL — Si falla alguno, la notarización va a fallar seguro:
– --entitlements en codesign (si no los pasas, pierdes sandbox y network)
– CODE_SIGN_IDENTITY = Developer ID en Release
– Hardened Runtime activado
HIGH — Probablemente falle o cause problemas:
– xattr -cr antes de firmar
– Sin dylibs de debug en Release
MEDIUM — Puede funcionar, pero te arriesgas:
– DMG firmado (no obligatorio, pero recomendado)
– LSApplicationCategoryType para App Store
– Consistencia de versiones entre AppInfo y project.yml
ARTEFACTOS — Verifica los builds generados:
– Firma válida con Developer ID
– Hardened Runtime activo en la firma
– Binary universal (x86_64 + arm64)
– Sin .DS_Store ni symlinks en el bundle
– Estructura correcta (Contents/{MacOS,Resources,Info.plist})
El script es estático para los checks de configuración (no necesita builds), y dinámico para los artefactos (necesitas haber hecho make archive && make export-direct). Así puedes ejecutarlo en cualquier momento como preflight check.
El flujo completo
Para que te hagas una idea del proceso de principio a fin:
flowchart TD
subgraph build[" 🔨 Build "]
direction TB
A["xcodegen generate"] --> B["xcodebuild archive<br/>(Release, firmado)"]
end
subgraph sign[" ✍️ Firma "]
direction TB
C["Copiar .app del archive"] --> D["xattr -cr<br/>(limpiar attributes)"]
D --> E["codesign --force<br/>--options runtime<br/>--timestamp<br/>--sign Developer ID"]
end
subgraph package[" 📦 Empaquetado "]
direction TB
F["hdiutil create<br/>(DMG formato UDZO)"]
end
subgraph notarize[" 🍎 Notarización "]
direction TB
G["notarytool submit<br/>--wait"] --> H{¿Accepted?}
H -->|Sí| I["stapler staple<br/>(grapar ticket)"]
H -->|No| J["notarytool log<br/>(ver errores)"]
J --> K["Corregir y<br/>volver a firmar"]
end
subgraph lint[" 🔍 Preflight "]
direction TB
L["lint-notarize.sh<br/>(25 checks)"]
end
build --> sign
sign --> lint
lint -->|✓ Todo OK| package
lint -->|✗ Fallos| K
package --> notarize
K --> sign
I --> M["✅ DMG listo<br/>para distribuir"]
Y en comandos:
# 1. Build + archive
make archive
# 2. Exportar + firmar (sin exportArchive, que se cuelga)
make export-direct
# 3. Lint (preflight)
bash scripts/lint-notarize.sh
# 4. DMG
make dmg
# 5. Notarizar
make notarize
# → xcrun notarytool submit ... --wait
# → xcrun stapler staple ...
# 6. Distribuir
# El DMG ya tiene el ticket grapado. Listo para subir.
Lecciones aprendidas a las 3AM
--timestampsiempre. Siempre. No hay excusa.- La primera firma, en terminal. Para que salga el popup del Keychain.
- No uses
exportArchivepara Developer ID. Copia +codesignmanual. - Ten paciencia con Apple. La notarización puede tardar minutos u horas. No hay SLA.
- Un linter vale más que cien intentos. Los 30 segundos que tarda el script te ahorran horas de submissions fallidas.
La notarización es como el examen de conducir: incómoda, burocrática, y probablemente necesaria. Pero una vez que tienes el proceso dominado, se convierte en un paso más del pipeline. Un paso que ya no te despierta a las 3AM preguntándote por qué Apple dice «Invalid» sin dignarse a explicar el motivo.
El script completo está en el repo de Tokamak.
Read this article in English.



