wandres.dev
DISTRIBUCIÓN · TestFlight y App Store

Firma de código: certificados, identificadores, perfiles y capacidades

La firma de código no es un trámite de Xcode sino un sistema de autorización distribuida con cuatro piezas que casi nadie separa mentalmente: una identidad criptográfica que Apple avala, un identificador que nombra la app, un conjunto de permisos declarados y un documento firmado que ata las tres cosas y las limita en el tiempo. Esta lección desmonta el rompecabezas pieza a pieza, explica qué comprueba exactamente el dispositivo al instalar y al arrancar, y convierte los errores clásicos de firma en consecuencias predecibles de una regla concreta.

⏱ 20 min

El mensaje de error más odiado del desarrollo iOS dice que no hay un perfil de aprovisionamiento que coincida, y resulta odioso precisamente porque describe el síntoma de una comprobación cuyo contenido nunca se explicó. La firma de código no es burocracia: es un sistema de autorización distribuida que responde a tres preguntas independientes —quién publica este binario, qué binario es exactamente, y qué le está permitido hacer— y las responde con criptografía porque el dispositivo que ejecutará la app está fuera de tu control y no puede fiarse de nada que tú le digas sin prueba. Separar las cuatro piezas convierte un muro opaco en un sistema con reglas cortas y fallos predecibles.

🎯 Al terminar esta lección sabrás
  • Distinguir certificado, identificador de app, perfil de aprovisionamiento y capacidades, y saber qué garantiza cada uno.
  • Leer la verificación que hace el dispositivo como una cadena de comprobaciones concretas y no como magia.
  • Diagnosticar los fallos de firma habituales a partir de la regla que se está violando.
  • Decidir entre firma automática y manual según quién deba custodiar la clave privada.

Las cuatro piezas del rompecabezas

Casi todo el sufrimiento con la firma nace de tratar cuatro objetos distintos como si fueran uno solo. Tienen dueños distintos, caducidades distintas y modos de fallo distintos.

🔑

Certificado e identidad

El par de claves lo generas tú; Apple firma la pública y devuelve un certificado. La clave privada vive en tu llavero y es el único secreto real: el certificado es público y no sirve de nada sin ella.

🏷️

Identificador de app

El bundle identifier nombra la app de forma única en todo el ecosistema. Puede ser explícito o comodín, y en el portal lleva asociada la lista de capacidades que esa identidad tiene derecho a pedir.

🎛️

Capacidades y entitlements

Activar una capacidad en Xcode escribe una clave en el fichero de entitlements, la habilita en el identificador y obliga a regenerar el perfil. Son tres cambios, no uno.

📜

Perfil de aprovisionamiento

Un plist firmado por Apple que ata identificador, certificados autorizados, entitlements concedidos y, si es de desarrollo o ad hoc, la lista de dispositivos. Caduca.

El perfil es la pieza que la gente entiende peor porque no es una credencial: es un contrato firmado por Apple que dice qué combinaciones son legítimas. No te da poder por sí mismo, delimita el poder que ya tienes. De ahí se deduce por qué un perfil de App Store no contiene dispositivos —la distribución es universal y la restricción la impone la tienda— mientras que uno de desarrollo o de distribución interna sí los enumera uno a uno.

# Identidades de firma realmente disponibles en este llavero.
security find-identity -v -p codesigning

# Que declara el binario ya firmado: equipo, identificador y entitlements.
codesign -dvvv --entitlements :- MiApp.app

# El perfil es un plist firmado con CMS: se puede leer.
security cms -D -i perfil.mobileprovision | plutil -p -

Ese último comando es el que cierra la mayoría de las discusiones. Muestra la fecha de caducidad, los certificados aceptados, los entitlements concedidos y los dispositivos incluidos, es decir, todos los datos que la herramienta gráfica esconde detrás de un error genérico.

Los cuatro tipos de perfil y el identificador comodín

Los perfiles no son intercambiables: cada variedad codifica una política de distribución distinta y mezclarlas produce errores que parecen inexplicables hasta que se nombra la diferencia. El de desarrollo autoriza la ejecución en dispositivos registrados uno a uno y es el único que habilita el depurador. El de ad hoc reparte fuera de la tienda a una lista cerrada de dispositivos, sin depuración y con caducidad, que es el cauce para entregas manuales a un cliente. El de distribución interna de una organización renuncia a la lista de dispositivos a cambio de restringir la audiencia a los miembros de esa organización, y su uso indebido es la infracción más cara del ecosistema porque compromete la cuenta entera. El de App Store no contiene dispositivos en absoluto, porque ahí la restricción de quién instala la impone la tienda y no el documento.

De esa clasificación sale una regla de diagnóstico que ahorra tardes enteras: si el error menciona dispositivos, estás usando un perfil de desarrollo o ad hoc donde correspondía uno de distribución, y ninguna cantidad de dispositivos añadidos lo va a arreglar.

La segunda distinción que conviene fijar pronto es la del identificador comodín. Un identificador con comodín cubre una familia entera de apps y resulta cómodo mientras no hagan falta capacidades, porque las que necesitan asociar datos de forma estable a una app concreta —los grupos compartidos entre extensiones, la sincronización en la nube, las notificaciones remotas— exigen un identificador explícito. Descubrir esa restricción a mitad de proyecto obliga a rehacer identificador, perfiles y, si ya hubo instalaciones, la migración de los datos que colgaban del identificador anterior.

📝
Una app con extensiones no tiene un perfil: tiene varios

Widgets, extensiones de notificación, teclados y acciones comparten el empaquetado pero no la identidad: cada objetivo lleva su propio identificador anidado bajo el de la app contenedora y su propio conjunto de capacidades. Cuando un proyecto con extensiones falla al firmar, el problema está casi siempre en el objetivo que nadie revisó, no en el principal, y el mensaje de error rara vez dice cuál es.

Qué comprueba el dispositivo, en orden

La cadena de verificación ocurre en dos momentos distintos: al instalar y en cada arranque. Verla como una secuencia de comprobaciones independientes hace que cada fallo apunte a una pieza concreta.

flowchart TB
k[Clave privada en el llavero] --> c[Certificado avalado por Apple]
id[Identificador de la app] --> p[Perfil de aprovisionamiento]
c --> p
e[Capacidades y entitlements] --> p
d[Lista de dispositivos] --> p
p --> b[Binario firmado y empaquetado]
b --> v[El sistema verifica]
v --> v1[La cadena del certificado llega a la raiz de Apple]
v --> v2[El hash del codigo no ha cambiado desde la firma]
v --> v3[Los entitlements del binario caben dentro del perfil]
v --> v4[El perfil no ha caducado ni fue revocado]
v1 --> ok[Se permite ejecutar]
v2 --> ok
v3 --> ok
v4 --> ok

La tercera comprobación es la que genera los errores más desconcertantes, y su regla se enuncia en una frase: los entitlements del binario deben ser un subconjunto de los que concede el perfil. Pedir menos de lo permitido es legal; pedir uno solo de más invalida la instalación entera. Por eso activar una capacidad y compilar sin regenerar el perfil rompe la firma aunque el código no haya cambiado: el binario ahora pide algo que el contrato no autoriza.

⚠️
El error clásico casi siempre es uno de estos cinco

El identificador del proyecto no coincide con el del perfil, con frecuencia por una letra o por un sufijo de esquema. La clave privada no está en el llavero, porque el certificado se descargó en otra máquina y solo se copió la parte pública. El perfil caducó, cosa que ocurre cada doce meses sin avisar. Se activó una capacidad y no se regeneró el perfil. O el dispositivo de pruebas no está en la lista, algo que solo afecta a perfiles de desarrollo y ad hoc, nunca a los de App Store.

Merece la pena separar los dos momentos en que ocurren estas comprobaciones, porque explican comportamientos distintos. Al instalar se valida el paquete entero: firma, perfil, entitlements y compatibilidad. En cada arranque, en cambio, el sistema verifica páginas de código conforme las carga, lo que significa que una alteración del binario no produce un error de instalación sino una terminación abrupta en mitad de la ejecución. Ese es el motivo de que ciertos bloqueos sin traza útil, especialmente tras copiar o modificar un paquete a mano, sean en realidad fallos de firma disfrazados de fallos de código.

# Verificacion estricta, igual que la haria el sistema.
codesign --verify --deep --strict --verbose=2 MiApp.app

# Comprobar la aptitud del paquete completo para su destino.
spctl -a -t exec -vv MiApp.app

Conviene además desarmar un miedo muy extendido: que el certificado de distribución caduque no derriba las apps ya publicadas. Apple vuelve a firmar el binario con su propia identidad al distribuirlo desde la tienda, de modo que la caducidad de tu certificado afecta a tu capacidad de publicar una versión nueva, no a las copias instaladas. Lo que sí caduca en manos del usuario son las compilaciones repartidas fuera de la tienda, ad hoc y TestFlight incluidas.

Automático o manual: la pregunta es quién guarda la llave

La firma automática deja que Xcode cree certificados y perfiles y los mantenga al día. Es la elección correcta en una máquina de desarrollo y la equivocada en integración continua, porque delega en una herramienta interactiva una operación que debe ser reproducible y auditable. La firma manual invierte la carga: tú declaras qué identidad y qué perfil se usan, y cualquier desviación falla de inmediato en lugar de resolverse sola de forma silenciosa.

# Llavero efimero en el agente, para no contaminar el del sistema.
security create-keychain -p "$CLAVE" build.keychain
security import certificado.p12 -k build.keychain -P "$CLAVE_P12" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$CLAVE" build.keychain

# Archivar con firma explicita: sin sorpresas ni visitas al portal.
xcodebuild archive -scheme MiApp \
  -archivePath build/MiApp.xcarchive \
  CODE_SIGN_STYLE=Manual \
  PROVISIONING_PROFILE_SPECIFIER=MiApp_AppStore \
  CODE_SIGN_IDENTITY="Apple Distribution"

Hay una asimetría entre ambos modos que conviene tener presente antes de elegir. La firma automática es silenciosamente creativa: si falta algo, lo crea. Eso resuelve el problema en tu escritorio y lo agrava en un equipo, porque cada máquina que compila puede acabar generando certificados nuevos hasta agotar el cupo del equipo, momento en el que alguien revoca a ciegas y rompe la firma de otros. La firma manual es declarativa: si falta algo, falla. En una máquina de integración continua eso es exactamente lo que quieres, porque un fallo ruidoso al principio cuesta minutos y una credencial creada a escondidas cuesta una investigación.

El problema real de un equipo no es firmar sino compartir la identidad sin repartir el secreto. Las tres soluciones vigentes se ordenan por cuánta custodia ceden: un repositorio cifrado que guarda certificados y perfiles y los instala en cada máquina, una clave de la API de App Store Connect que sustituye a la sesión interactiva y permite generar credenciales sin contraseñas de persona, o dejar que la integración continua de Apple gestione la firma por completo. Las tres son legítimas; lo que no lo es, y sigue siendo sorprendentemente común, es que la clave privada de distribución exista en un solo portátil sin copia y sin control de quién la tiene.

💡
Revocar no es gratis, y por eso conviene inventariar

Antes de revocar un certificado porque estorba, comprueba qué perfiles lo incluyen: revocarlo invalida todos ellos y cualquier compilación que dependa de ellos deja de firmarse hasta que se regeneren. El inventario mínimo que evita el susto son tres datos por identidad —quién la posee, dónde está la copia de la clave privada y qué perfiles la referencian— y cabe en un documento de media página que casi nadie escribe hasta que ya es tarde.

Firmar no demuestra que el código sea bueno: demuestra a quién reclamar

La intuición equivocada consiste en leer la firma como un sello de calidad, cuando es un mecanismo de atribución e integridad, y esa diferencia explica el diseño entero. La firma no dice que la app sea segura; dice que este binario exacto procede de una entidad concreta que Apple identificó, que nadie lo ha alterado desde entonces y que sus permisos fueron concedidos de antemano por un tercero de confianza. Comprender esto reordena todo lo demás. Explica por qué el hash cubre cada recurso del paquete y no solo el ejecutable: si un atacante pudiera cambiar una imagen o un fichero de configuración sin invalidar la firma, la atribución dejaría de significar nada. Explica por qué los entitlements viajan dentro del binario firmado y además dentro del perfil firmado por Apple: uno declara lo que el código pide, el otro lo que se le autorizó, y la comprobación de subconjunto impide que quien controla el código amplíe por su cuenta lo que puede hacer. Y explica por qué los perfiles caducan aunque el certificado siga vigente: la caducidad no es un mecanismo de seguridad contra el robo, es un mecanismo de revocación perezosa que permite retirar autorizaciones sin comunicación en línea, en dispositivos que pueden pasar meses desconectados. Visto así, el rompecabezas deja de ser arbitrario: cada pieza existe porque el sistema tiene que funcionar en un dispositivo que no confía en ti, no puede preguntar a nadie y debe poder equivocarse hacia el lado seguro.

⚔️ Desmontar el rompecabezas en tu propio proyecto
  1. Ejecuta security find-identity -v -p codesigning y enumera cuántas identidades de distribución tienes; borra las caducadas y averigua dónde está la copia de seguridad de cada clave privada.
  2. Extrae el perfil de tu app con security cms -D y localiza en el plist la fecha de caducidad, los entitlements concedidos y los certificados aceptados.
  3. Ejecuta codesign -dvvv --entitlements :- sobre tu compilación y compara los entitlements del binario con los del perfil: comprueba a mano la relación de subconjunto.
  4. Activa una capacidad nueva sin regenerar el perfil, intenta instalar y lee el error a la luz de la regla que acabas de verificar.
  5. Pasa el proyecto a firma manual y consigue archivar sin que Xcode contacte con el portal; ese es el estado que necesitarás para automatizar.