Material OSS público. Los repositorios citados (
yolo-labz/claude-mac-chrome,yolo-labz/wa,yolo-labz/kokoro-speakd,yolo-labz/claude-classroom-submit,yolo-labz/linkedin-chrome-copilot,yolo-labz/fand) están todos abiertos en GitHub. Los números que siguen salieron de los pipelines de release que corren hoy.
El artefacto es chico; la confianza que exige, no
Un plugin de Claude Code no pasa de ser un artefacto diminuto: un hook script, un slash command, un wrapper de servidor MCP. El repositorio acompaña ese tamaño — la mediana de los seis en cuestión queda en ~2 KLOC. Pero la superficie de confianza no entra en ese mismo molde. El plugin se carga dentro de la misma sesión de shell donde viven el código, los secretos y los tokens de quien desarrolla. Desde ahí, un tarball sin firmar deja de ser un detalle y se vuelve superficie de ataque legítima.
Y acá está el dato que lo cambia todo: hasta abril de 2026, el marketplace de plugins de Claude Code de Anthropic no exige absolutamente nada en este frente. Nada de firma, nada de SBOM, nada de SLSA, ninguna verificación de firma al instalar. La confianza se deposita en el marketplace, nunca en el plugin individual. O sea: blindar la cadena de suministro de estos repositorios es trabajo voluntario, hecho antes de que la política del marketplace lo pida. Vale la pena igual por dos razones. Primero, quien sabe qué está mirando puede verificar el plugin con un solo comando. Segundo, el día en que el marketplace decida apretar las reglas, la flota ya llega lista.
La decisión, en una frase
En el contexto de un dev solo publicando plugins OSS, frente a la presión por confianza en la cadena de suministro y sin ningún equipo de seguridad detrás, decidimos por attestations SLSA L2 + cosign keyless vía OIDC + SBOM doble, todo aplicado por un único canon de GitHub Actions, para lograr provenance de grado de auditoría sin costo por plugin — aceptando, a cambio, el costo único de armar el template.
Seis repositorios, tres formatos, un solo canon
El criterio para entrar en la lista fue simple: todo repositorio tiene que tener una superficie de release — un binario, una wheel o un tarball con tag. Los seis que encajan:
| Repositorio | Lenguaje | Lo que va al release |
|---|---|---|
claude-mac-chrome | Bash | tarball con tag + Homebrew tap |
wa | Go 1.24 | binarios darwin/linux vía GoReleaser + Homebrew tap |
kokoro-speakd | Python 3.12 | wheel en PyPI + pesos del modelo en GitHub Release |
claude-classroom-submit | Python 3.12 | wheel en PyPI |
linkedin-chrome-copilot | Bash | tarball con tag |
fand | Rust | binarios compilados con cargo + Homebrew tap |
Son tres formatos de release distintos — binario, wheel y tarball — conviviendo bajo el mismo canon. Y la tarea del canon es justamente esa: hacer que la superficie de firma quede idéntica en los tres, sin importar qué hay debajo.
El corazón: nueve líneas que entran en todos lados
El paso de firma que aterriza en cada workflow de release es tan corto que entra de una sola mirada:
- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: ${{ inputs.artifact-path }}
- name: Attest SBOM
uses: actions/attest-sbom@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: ${{ inputs.artifact-path }}
sbom-path: sbom.cdx.jsonDos actions nativas de GitHub, pineadas por el SHA completo de 40 caracteres con ese comentario # v4.1.0 al final — la parte de la que depende la regex de Dependabot para reconocer la entrada. Y no hay ninguna infraestructura de firma externa en el camino. El token OIDC emitido por https://token.actions.githubusercontent.com se cambia, en la CA Fulcio de Sigstore, por un certificado de firma de vida corta; la firma queda registrada en el log de transparencia público Rekor; y el bundle que sale de eso va adjunto al GitHub Release.
Vale detenerse en un tropiezo clásico. El issuer OIDC del CI es https://token.actions.githubusercontent.com, y punto. La URL https://github.com/login/oauth es otra cosa — es el flujo OAuth interactivo, hecho por una persona. Apuntar esa segunda en la flag --certificate-oidc-issuer de un workflow rinde un error nada obvio allá en el cosign verify-blob. Así que, si aparece cualquier queja de issuer-mismatch, empezá revisando esa string.
Dos SBOMs en una sola llamada a syft
Dos formatos importan acá, y por razones regulatorias bien concretas. El CycloneDX 1.7 es el estándar de OWASP y es lo que el EU Cyber Resilience Act les cobra a los productos en alcance. El SPDX 2.3 es el estándar ISO/IEC 5962:2021, exigido por la US Executive Order 14028 en las adquisiciones federales. Hubo un tiempo en que producir los dos significaba correr dos veces; el syft de hoy lo resuelve en una invocación única:
syft . \
-o [email protected]=sbom.cdx.json \
-o spdx-json=sbom.spdx.jsonEn el repositorio Go (wa), una corrida extra de cyclonedx-gomod app -licenses -std -json agrega un SBOM nativo de Go bastante más detallado — versiones de módulo, resúmenes de licencia, las fronteras de la std-lib — que se suma a lo que syft ya produjo. En cambio, los repositorios Python se apoyan en pypa/gh-action-pypi-publish@release/v1 (v1.11+), que genera las attestations PEP 740 por sí solo, a partir del flujo OIDC de Trusted Publishing, y se ahorra un paso Sigstore aparte.
En cuanto al tamaño, nada de esto pesa. El sbom.cdx.json queda en ~12 KB en los repositorios Bash y en ~84 KB en wa después del augment de gomod. El bundle de provenance (attestation.intoto.jsonl) se detiene en ~4 KB por artefacto. Sumando todo, ni siquiera da para llamarlo footprint relevante en los assets del release.
Cerrar los permisos: negar por defecto
La regla a nivel del workflow es negar todo de entrada y dejar que cada job vuelva a otorgar, a mano, solo aquello que necesita:
permissions: {}
jobs:
release:
permissions:
id-token: write # OIDC for keyless signing
attestations: write # write provenance + SBOM attestations
contents: write # cut the GitHub Release
runs-on: ubuntu-latest
steps:
- uses: step-security/harden-runner@<sha> # v2
with:
egress-policy: audit
- uses: actions/checkout@<sha>
with:
persist-credentials: false
# ... build + sign stepsEl step-security/harden-runner arranca en egress-policy: audit durante el primer ciclo de release — la idea es solo observar qué endpoints legítimos de Sigstore se activan — y pasa a block cuando la allowlist ya esté asentada. Y el persist-credentials: false en actions/checkout impide que el GITHUB_TOKEN por defecto quede olvidado dentro del .git/config después de que el workflow termina.
La fitness function: verificar en cada release
Un pipeline de firma que nunca se ejercita del lado de quien consume es un pipeline que se pudre en silencio. Por eso la fitness function vive dentro del propio CI: todo workflow de release termina ejecutando exactamente la verificación que espera que el usuario corra.
- name: Smoke-verify the just-signed artifact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
artifact="${{ inputs.artifact-path }}"
repo="${{ github.repository }}"
gh attestation verify "${artifact}" \
--repo "${repo}" \
--predicate-type "https://slsa.dev/provenance/v1" \
--format json \
| jq -e '.[] | select(.verificationResult.signature.certificate.sourceRepositoryURI == "https://github.com/${{ github.repository }}")'
echo "OK: SLSA L2 provenance verified against ${repo}"Si el artefacto que acaba de publicarse no coincide con su propia attestation, el release se rompe ahí mismo — un aviso más fuerte de lo que cualquier línea de documentación va a ser jamás. Y no es casualidad: ese gh attestation verify es el mismo comando que va en el README como smoke test del usuario.
La trampa: nunca vuelvas a re-taguear un release
Esta es la trampa que agarra al dev solo apenas empieza. El CI falló, la tag ya se había empujado demasiado temprano, y el reflejo es borrar la tag local, hacer force-push, corregir y taguear de nuevo encima.
No lo hagas. El slsa-verifier y el predicate de provenance que está por debajo validan contra el commit SHA del momento de la firma. Re-taguear genera una provenance desfasada: los commits nuevos existen, pero la afirmación firmada sigue apuntando al SHA viejo. En la práctica, el gh attestation verify sigue pasando contra el bundle firmado de la tag original, mientras que cualquiera que descargue el árbol de fuentes re-tagueado va a recibir artefactos cuya attestation describe un commit que ni siquiera tiene en mano.
La salida es aburrida de tan simple: corta la vX.Y.Z+1. Subí el número de patch, empujá la tag nueva, corré el workflow otra vez y archivá el release roto con una nota “superseded by” en el cuerpo. Es esa disciplina de tags monotónicas la que mantiene la provenance verificable.
Qué esperar del Scorecard
El OpenSSF Scorecard sirve como termómetro, no como meta. Para un repositorio solo-dev de yolo-labz, el techo realista es ~8.7/10, y quien tira ese número para abajo es, estructuralmente, el check de Contributors: se traba alrededor de 3/10 para quien trabaja solo y no se puede burlar con trailers Co-Authored-By: — el Scorecard descarta bots e ignora campos Company vacíos. Fuera de ese, todos los demás pasan de 8 apenas el canon entra:
- Pinned-Dependencies →
10/10después de pasar cada action por el secure-workflow rewriter de StepSecurity. - Token-Permissions →
10/10gracias al deny-all como default. - Signed-Releases →
10/10apenas las attestations de Sigstore están adjuntas. - Packaging →
10/10, ya que todo repositorio publica por una action reconocida (softprops/action-gh-release,pypa/gh-action-pypi-publish). - Maintained → se autorrecupera en el día 90, con que haya al menos un commit por semana.
El único check que cobra código de verdad es el de Fuzzing. Un workflow fuzz.yml es invisible para el Scorecard — quiere ver o bien fuzz tests en Go (func FuzzX(f *testing.F) en cualquier *_test.go), o bien un directorio .clusterfuzzlite/ cumpliendo el contrato de OSS-Fuzz. En wa, una sola función de fuzz en Go ya rinde diez puntos en ese rubro. Para los repositorios shell, como claude-mac-chrome, el camino es ClusterFuzzLite, vía .github/workflows/cflite_pr.yml más el .github/workflows/cflite_cron.yml para los batch runs nocturnos.
Del otro lado del mostrador: qué tiene que correr el usuario
En el README, el bloque de verificación se encoge a dos comandos:
gh release download v1.2.0 --repo yolo-labz/wa
gh attestation verify wa-darwin-arm64.tar.gz --repo yolo-labz/waEse es el ritual entero para que alguien establezca confianza y confirme que el artefacto salió de verdad del pipeline anunciado. El cosign verify-blob y el slsa-verifier existen, pero son los caminos offline y avanzados — su lugar es un apéndice Reproducing the verification offline, jamás el titular. Mezclar “avanzado” con “default” es justo por donde la UX de confianza empieza a pudrirse.
La cuenta, sin maquillaje
La contabilidad honesta del canon, después de desplegarlo en los seis repositorios:
- Escribir el template (una sola vez): unos dos días en el primer repositorio, contando ya el ida y vuelta de depurar la egress-allowlist para el modo
blockdeharden-runner. - Enchufar cada repositorio nuevo: ~30 minutos — copiar el
release.yml, ajustar elsubject-path, encajar el job de build del lenguaje y hacer push. - Overhead por release: ~25 segundos de más en el CI, sumando generación de SBOM + attestation + smoke verify.
- Secretos rotados: cero. El Sigstore keyless elimina de raíz la clave de firma de larga duración.
El setup es un costo fijo que se reparte en cada release futuro de la flota. El costo marginal de cada plugin es solo la copia del YAML — la criptografía es el mismo tramo de código corriendo sobre la misma imagen de runner.
Referencias
- SLSA Specification v1.0, Build Track Levels — slsa.dev/spec/v1.0/levels
- GitHub, Using artifact attestations to establish provenance for builds — docs.github.com/en/actions/security-guides/using-artifact-attestations
- Sigstore, Rekor Transparency Log Architecture — docs.sigstore.dev/rekor/overview
- Zahan, Lin et al. (2023) OpenSSF Scorecard: On the path toward ecosystem-wide automated security metrics
arXiv:2208.03922— arxiv.org/abs/2208.03922 - CycloneDX 1.7 spec — cyclonedx.org/specification/overview
- Linux Foundation / SPDX 2.3 — spdx.github.io/spdx-spec/v2.3
- PEP 740, Index support for digital attestations — peps.python.org/pep-0740