Las atestaciones de procedencia de GitHub no distinguen quién compila
Un usuario documenta en el foro de GitHub que una organización puede crear un runner con el nombre ubuntu-24.04, servir ahí los jobs y obtener una atestación que dice github-hosted.
GitHub no puede distinguir, mirando solo la atestación de procedencia de un artefacto, entre un runner oficial suyo y uno creado por la organización que se haya bautizado igual. Lo ha documentado el usuario amiller en el foro de la comunidad de GitHub, y el ejemplo que da no deja mucho margen: una atestación válida puede certificar un binario que no salió del código fuente que dice.
Las atestaciones de artefactos de GitHub existen precisamente para lo contrario. Atan el binario construido al código y al workflow que lo produjo, y gh attestation verify es la herramienta con la que un consumidor comprueba esa cadena antes de desplegar. El fallo está en el eslabón del medio: la máquina que compila.
Las organizaciones con plan de pago que usan larger runners alojados por GitHub eligen la imagen con la que arrancan. GitHub no valida el nombre del runner contra sus propias etiquetas. Así que alguien puede crear un runner con una imagen propia, llamarlo ubuntu-24.04 y colocar ahí los jobs que piden esa etiqueta. El campo runnerEnvironment de la atestación sigue diciendo github-hosted, que es justo el valor pensado para separar lo que controla GitHub de lo autoalojado.
El 4 que se convierte en 5
El ejemplo es un hello.c que imprime un 4 y un workflow con actions/attest-build-provenance@v2 y runs-on: ubuntu-24.04. En un runner compartido de GitHub, el binario imprime 4 y la atestación cuadra. El mismo workflow, desde el mismo commit, ejecutado en un runner creado por la organización: esa imagen reescribe el fuente para que ponga 5 antes de compilar, compila y restaura el fichero original para no dejar rastro en el workspace. Al verificar el artefacto con gh attestation verify, el comando responde que todo está bien. Incluso con --deny-self-hosted-runners.
La causa es lo que la atestación no firma. runner_environment es demasiado amplio; builder.id apunta al fichero de workflow, no a la máquina; no hay campo con el nombre del runner, su grupo, su tamaño ni la imagen desde la que arrancó. Tampoco sirve fijar el job al grupo GitHub Actions desde runs-on: ese grupo es reservado y no se puede direccionar por nombre.
La API todavía lo sabe
La información para distinguir los dos casos existe, solo que fuera de la atestación. La extensión de Fulcio y runDetails.metadata.invocationId llevan a la ejecución concreta en GitHub Actions, y desde ahí la API de Jobs expone runner_group_name (GitHub Actions frente a un grupo de la organización, normalmente default), runner_group_id (el reservado es 0) y runner_name. Los logs lo confirman: Runner Image: ubuntu-24.04 en el pool compartido, Source: Custom Name: shimcompile en el sustituido.
En la práctica, quien verifique hoy tiene que cruzar la atestación con la API de Jobs por su cuenta, y eso no es algo que haga una herramienta de verificación estándar. Mientras no haya un campo firmado que identifique el runner y su imagen, la atestación certifica el workflow, no la máquina, y esa diferencia es exactamente la que aprovecha quien tenga acceso a la configuración de runners de la organización.
