wandres.dev
INTENTS Y DEEP LINKS · la app en el sistema

App Links verificados: abrir sin preguntar

Un enlace HTTP declarado sin más convierte a tu aplicación en un candidato entre otros y condena al usuario a un diálogo de elección en cada apertura; un App Link verificado hace que el sistema abra tu aplicación directamente porque ha comprobado criptográficamente que el dominio y la aplicación pertenecen al mismo propietario. Esta lección explica el mecanismo completo de esa comprobación: el atributo de autoverificación en el filtro, el fichero `assetlinks.json` publicado bajo la ruta reservada del dominio, la huella de firma que debe coincidir con la que realmente firma el artefacto distribuido, y el momento exacto en que la plataforma ejecuta la verificación. Detalla además el diagnóstico con las herramientas de línea de órdenes, las causas de fallo más frecuentes y la estrategia cuando hay varios dominios, subdominios o entornos de preproducción.

⏱ 20 min

La lección anterior terminó con un enlace HTTP que abre la pantalla correcta, y con un detalle molesto que muchos equipos aceptan como inevitable: el sistema pregunta. Aparece un diálogo con el navegador y tu aplicación, el usuario elige, y cada punto de fricción de ese diálogo se paga en abandono medible. La causa de la pregunta es exactamente la que estudiamos al principio del nivel: cuando varios componentes declaran ser capaces de atender una dirección, el arbitraje corresponde al usuario, y la plataforma no tiene ninguna razón para creerse que esa dirección es tuya solo porque tú lo hayas escrito en tu manifiesto. Los App Links resuelven el problema por la única vía intelectualmente honesta que existe: dejan de creerte y lo comprueban, exigiendo una declaración firmada publicada en el propio dominio que solo su legítimo propietario puede colocar allí.

🎯 Al terminar esta lección sabrás
  • Entender por qué la verificación de dominio elimina el diálogo de elección y qué garantía aporta exactamente.
  • Publicar un assetlinks.json correcto con la huella de firma que realmente distribuye el artefacto.
  • Diagnosticar el estado de verificación desde la línea de órdenes y leer sus códigos de fallo.
  • Diseñar la estrategia de dominios, subdominios y entornos sin romper la verificación en producción.

Qué añade la verificación

Un filtro HTTP normal declara una capacidad; un filtro con android:autoVerify puesto a cierto declara además la pretensión de ser el destino legítimo de ese dominio, y pide a la plataforma que la compruebe. Si la comprobación tiene éxito, el sistema deja de considerar tu aplicación un candidato más y la convierte en el destino directo: el enlace abre la pantalla sin diálogo, sin navegador intermedio y sin que el usuario haya tenido que configurar nada.

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="ejemplo.com" />
    <data android:scheme="https" android:host="www.ejemplo.com" />
</intent-filter>

Conviene notar que la verificación automática no es la única vía para llegar a la apertura directa. El usuario puede conceder ese privilegio a mano desde los ajustes de la aplicación, en la sección de enlaces admitidos, marcando qué dominios quiere que se abran siempre contigo. Esa concesión manual es indistinguible de la verificada en cuanto al comportamiento, pero es radicalmente distinta en cuanto a la experiencia: exige que el usuario sepa que la opción existe, la encuentre y la active, cosa que hace una fracción despreciable de la gente. Ofrecer un atajo a esa pantalla desde tu propia interfaz es un buen recurso de último recurso, nunca un sustituto de hacer bien la verificación.

También conviene recordar quién manda al final. Aunque el dominio esté verificado, el usuario puede desactivar la apertura directa desde los mismos ajustes, y el sistema respetará esa decisión sin consultarte. La verificación no te concede el dominio: te concede la presunción de legitimidad, que es lo máximo que un sistema operativo puede otorgar sin quitarle al usuario el control sobre su propio dispositivo.

La garantía que aporta el mecanismo es concreta y conviene enunciarla con precisión, porque se sobreentiende mal en las dos direcciones. Lo que se demuestra es que quien controla el contenido servido bajo el dominio y quien posee la clave con la que se firmó la aplicación son la misma parte, o al menos partes que han acordado declararse mutuamente. No se demuestra que la aplicación sea buena, ni que el dominio sea seguro, ni que el enlace lleve a donde dice. Es una prueba de propiedad común, y es exactamente lo que hace falta para que el sistema pueda saltarse la pregunta sin exponer al usuario a un secuestro de enlaces.

El fichero y la huella

Esa comprobación descansa por completo en la infraestructura de certificados de la web, y por tanto hereda sus garantías y sus límites: es tan sólida como lo sea el control sobre el dominio y sobre la cadena de certificación, ni más ni menos. No hay ninguna autoridad específica de Android involucrada, lo que resulta elegante porque no introduce un tercero nuevo en el que confiar.

La declaración vive en una ruta reservada del dominio, bajo el directorio bien conocido, y debe servirse por HTTPS con tipo de contenido de JSON, sin redirecciones y con un certificado válido. Su contenido enlaza el dominio con un paquete y con las huellas de las claves que lo firman.

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.ejemplo.app",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
    ]
  }
}]

El fichero es una lista de declaraciones y no una sola, lo que permite que un mismo dominio autorice a varias aplicaciones —la principal, la variante para otro país, la versión ligera— y también que se declaren relaciones distintas de la apertura de enlaces, porque el mismo formato lo utilizan otros subsistemas, señaladamente el relleno automático de credenciales para compartir contraseñas entre el sitio web y la aplicación. Escribirlo pensando en esa pluralidad desde el principio evita tener que reorganizarlo cuando aparece la segunda necesidad.

[
  { "relation": ["delegate_permission/common.handle_all_urls"],
    "target": { "namespace": "android_app",
                "package_name": "com.ejemplo.app",
                "sha256_cert_fingerprints": ["AA:BB:..."] } },
  { "relation": ["delegate_permission/common.get_login_creds"],
    "target": { "namespace": "android_app",
                "package_name": "com.ejemplo.app",
                "sha256_cert_fingerprints": ["AA:BB:..."] } }
]

La huella es donde fracasa la mayoría de las integraciones, y siempre por la misma razón. En el flujo moderno de publicación, la clave con la que firmas localmente no es la clave con la que se firma lo que llega al dispositivo: la tienda vuelve a firmar el artefacto con la clave de firma de aplicación que custodia. Publicar la huella de la clave de subida produce un fichero perfectamente formado que verifica en tu compilación de depuración y falla en todas las instalaciones reales, que es el peor modo de fallo imaginable porque el equipo lo da por resuelto.

💡
Publica varias huellas a la vez y ahórrate el ciclo entero

El campo admite una lista, y no hay ninguna razón para no aprovecharlo. Incluye la huella de la clave de firma de aplicación que muestra la consola de publicación, la de la clave de subida y la de tu clave de depuración compartida si el equipo la usa. Con eso el mismo fichero verifica en el dispositivo del desarrollador, en la distribución interna de pruebas y en producción, y desaparece la clase entera de incidencias en las que algo funciona en un canal y no en otro sin que nadie sepa por qué.

flowchart TD
A[Instalacion o actualizacion de la app] --> B[El sistema lee los filtros con autoVerify]
B --> C[Descarga el fichero bajo la ruta bien conocida del dominio]
C --> D{HTTPS valido sin redirecciones}
D -->|no| E[Verificacion fallida]
D -->|si| F{El paquete declarado coincide}
F -->|no| E
F -->|si| G{Alguna huella coincide con la firma real}
G -->|no| E
G -->|si| H[Dominio verificado]
H --> I[El enlace abre la app sin dialogo]
E --> J[La app queda como candidato entre otros]

Cuándo verifica el sistema y cómo diagnosticarlo

La verificación no ocurre cuando llega el enlace sino en la instalación o la actualización de la aplicación, y con reintentos posteriores gobernados por la plataforma. Esa asincronía explica los dos desconciertos habituales: que un dominio recién publicado tarde en empezar a funcionar, y que un fichero corregido no surta efecto hasta reinstalar. Conviene añadir que el dispositivo puede no tener red en ese instante, lo que en la práctica significa que la ausencia de verificación nunca debe tratarse como un error del usuario ni bloquear ninguna funcionalidad.

Conviene tener presente que el estado no es global sino por usuario del dispositivo y por aplicación, y que se reevalúa al actualizar. Una aplicación que verificaba y deja de hacerlo tras una publicación suele haber cambiado de clave, de nombre de paquete o de conjunto de anfitriones declarados, y la comparación entre el estado antes y después de la actualización acota el problema en minutos.

El diagnóstico se hace desde la línea de órdenes y es la única fuente fiable. adb shell pm get-app-links com.ejemplo.app muestra cada anfitrión declarado con su estado, y adb shell pm verify-app-links --re-verify com.ejemplo.app fuerza un intento nuevo sin reinstalar. Los estados que interesan son verificado, no verificado y el conjunto de códigos de fallo, que distinguen con precisión entre no haber podido descargar el fichero, haberlo descargado mal formado y haberlo entendido pero no encontrar la huella. Esa distinción ahorra días.

Hay dos fuentes complementarias de información que conviene tener a mano. La consola de publicación muestra el estado de los enlaces tal como lo ve la distribución real, con la huella de firma que efectivamente se aplica, y es el sitio donde antes se detecta el error de huella. El asistente integrado en el entorno de desarrollo genera el fichero y comprueba los filtros declarados, lo que resulta útil para arrancar aunque no sustituye a la comprobación en un dispositivo real, porque no sabe nada de tu servidor ni de tu red de distribución de contenido.

⚠️
La red de distribución de contenido delante del dominio es la causa oculta favorita

Cuando el fichero se sirve correctamente desde el origen y aun así falla, el sospechoso habitual está en medio. Una capa de caché puede estar devolviendo una versión antigua durante horas, una regla de seguridad puede estar filtrando peticiones sin cabecera de navegador habitual, una configuración de dominio único puede estar redirigiendo el anfitrión sin prefijo, y una página de error personalizada puede estar devolviendo un código de éxito con contenido HTML que el verificador rechaza por no ser JSON. Antes de tocar el manifiesto, descarga el fichero desde una red externa sin credenciales y comprueba que lo que llega es exactamente lo que crees haber publicado.

Las causas de fallo se repiten con una uniformidad casi cómica en todos los equipos. Servir el fichero por HTTP o con un certificado incompleto en la cadena. Interponer una redirección desde el anfitrión sin prefijo al que lo lleva, cuando el proceso no sigue redirecciones. Devolver el JSON con un tipo de contenido de texto plano. Declarar el anfitrión sin prefijo en el manifiesto y publicar el fichero solo en el que sí lo tiene, o al revés. Y la ya citada huella equivocada. Ninguna de ellas produce un mensaje visible en la aplicación: el único síntoma es que vuelve a aparecer el diálogo.

🔐

Propiedad común

La verificación demuestra que dominio y clave de firma pertenecen a la misma parte, nada más y nada menos.

🧾

Ruta reservada

HTTPS, sin redirecciones, tipo de contenido de JSON y certificado con la cadena completa.

🖐️

Huella real

La que firma lo que llega al dispositivo, no la que usas al subir. Publica varias y termina con el problema.

🩺

Diagnóstico

pm get-app-links y pm verify-app-links son la única verdad; la interfaz nunca te dirá qué pasó.

Dominios, subdominios y entornos

Un último apunte de gobernanza que ahorra incidentes. El fichero vive en el repositorio del sitio web y no en el de la aplicación, de modo que su ciclo de vida lo controla un equipo distinto del que necesita que sea correcto. Ese desajuste organizativo, y no la tecnología, es la causa real de la mayoría de las regresiones: alguien migra el sitio, rehace la configuración del servidor y el fichero desaparece o deja de servirse con el tipo correcto, sin que nadie del lado móvil se entere hasta que el diálogo reaparece semanas después. La contramedida es una comprobación automática periódica que descargue el fichero y valide su contenido contra las huellas esperadas, ejecutada desde la integración continua del proyecto móvil.

Cada anfitrión se verifica por separado, así que una aplicación que atiende varios subdominios necesita el fichero publicado en cada uno de ellos, y el manifiesto puede declararlos todos en el mismo filtro. Los comodines de anfitrión existen en la sintaxis del filtro pero no eximen de publicar el fichero en cada anfitrión concreto que se quiera verificar, de modo que la práctica sana es enumerar en lugar de generalizar y saber exactamente qué está verificado.

La migración desde un esquema propio ya implantado merece una nota, porque casi ningún proyecto parte de cero. La estrategia que funciona es aditiva y no sustitutiva: se añaden los filtros HTTP verificados, se empieza a emitir el enlace nuevo en todos los canales bajo tu control, y el filtro del esquema antiguo se conserva indefinidamente porque hay mensajes ya enviados que lo usan. Retirarlo es una decisión de años, no de versiones, y conviene medir su tráfico real antes de tomarla en lugar de suponer que ya nadie lo usa.

Los entornos de preproducción merecen una decisión explícita. Si el paquete de la compilación de depuración lleva sufijo, no coincidirá con el nombre declarado en el fichero de producción, y por tanto no verificará por mucho que el dominio sea correcto. Las dos soluciones limpias son publicar un fichero propio en el dominio de pruebas que declare el paquete con sufijo, o aceptar deliberadamente que solo se verifica producción y usar el diálogo en los demás canales. Lo que no funciona es la vía intermedia habitual, que consiste en compartir el nombre de paquete entre entornos para que la verificación pase y descubrir después que las dos aplicaciones no pueden convivir en el mismo dispositivo.

Hay además una consecuencia sobre el diseño de las direcciones que conviene decidir pronto. Como la verificación otorga a tu aplicación todas las direcciones del anfitrión que casen con los filtros declarados, un filtro demasiado ancho captura páginas que no tienen equivalente en la aplicación —el blog, la ayuda, los términos legales— y el usuario acaba en una pantalla de error o de vuelta al navegador tras un parpadeo desagradable. Acotar con prefijos de ruta o con patrones explícitos, y reservar un camino de escape que abra el navegador cuando la dirección no tenga destino interno, es lo que separa una integración pulida de una que técnicamente funciona.

La verificación traslada la confianza del código al control del dominio

Merece la pena detenerse en lo que este mecanismo dice sobre cómo evoluciona la seguridad en las plataformas, porque el patrón se repite mucho más allá de Android. Durante los primeros años, cualquier aplicación podía declarar que atendía cualquier dirección, y la plataforma resolvía el conflicto con la única herramienta que tenía: preguntar. Preguntar es una respuesta honesta pero pésima, porque traslada al usuario una decisión que no está en condiciones de tomar —no puede saber cuál de dos aplicaciones es la legítima destinataria de un dominio— y porque el coste de preguntar mil veces al día es lo bastante alto como para que la gente conteste al azar con tal de que el diálogo desaparezca. La respuesta a ese problema no fue mejorar el diálogo sino eliminarlo, y para eliminarlo hubo que encontrar una autoridad externa a la aplicación en la que apoyarse. Esa autoridad es el control del dominio, verificado sobre la infraestructura criptográfica que ya sostiene la web entera. El movimiento es profundo: la plataforma dejó de tratar la declaración del manifiesto como una afirmación de hecho y pasó a tratarla como una pretensión que hay que probar contra una fuente independiente. Ahí hay una lección de arquitectura que trasciende con mucho a los enlaces. Cuando dos partes reclaman el mismo recurso y el sistema no puede decidir, hay exactamente tres salidas: inventarse una regla arbitraria, que siempre resulta injusta y explotable; preguntar al usuario, que traslada el problema a quien menos información tiene; o encontrar una prueba externa que dirima el conflicto sin intervención. Solo la tercera escala, y solo la tercera resiste a un adversario. Conviene además reconocer lo que el mecanismo deliberadamente no hace, porque las críticas mal dirigidas nacen de ahí. La verificación no juzga intenciones ni calidad, y no protege contra un dominio que efectivamente pertenece a alguien malicioso: protege contra la suplantación, que es un problema distinto y mucho más frecuente. Un sistema de confianza bien diseñado no promete confianza, promete un vínculo comprobable entre dos identidades, y deja el juicio sobre esas identidades a otras capas. Confundir ambas cosas lleva a exigirle al mecanismo garantías que nunca prometió y a descuidar las que sí hacen falta en otro sitio.

⚔️ Verifica un dominio de principio a fin y luego rómpelo
  1. Publica el fichero en un dominio propio, activa la autoverificación y confirma el estado con pm get-app-links antes y después de reinstalar.
  2. Sustituye la huella por la de la clave de subida cuando la distribución usa firma gestionada por la tienda, y observa que verifica en depuración y falla en producción.
  3. Introduce una redirección desde el anfitrión sin prefijo, comprueba el fallo y arréglalo sirviendo el fichero directamente en ambos.
  4. Cambia el tipo de contenido a texto plano y localiza el código de fallo exacto que devuelve la herramienta.
  5. Diseña la estrategia de tu propio proyecto para tres entornos, decide qué se verifica y qué no, y escribe la justificación en el repositorio junto al fichero.