diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96b2723..5228b9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,62 @@ jobs: - name: Probar el cliente run: npm test + # ── FIRMA DE macOS ────────────────────────────────────────────────────────────────── + # Hasta el 2026-09-19 este build NO firmaba la app de macOS: no se le pasaba ninguna + # identidad, así que el bundler de Tauri se saltaba `codesign` entero y el .dmg salía con + # la firma ad hoc que deja el enlazador. Se veía así en la app instalada: + # flags=0x20002(adhoc,linker-signed) · Info.plist=not bound · Sealed Resources=none + # Consecuencias: los entitlements (micrófono, cámara, JIT) NO se aplicaban —el + # Entitlements.plist del repo era decoración—, el Info.plist y los recursos no iban + # sellados, y la app no se podía notarizar de ninguna manera. + # + # `APPLE_SIGNING_IDENTITY` decide qué pasa, y por eso se calcula aquí y no en el `env:` + # del paso de abajo: un secreto que no existe llega como cadena VACÍA, y Tauri + # interpreta `APPLE_CERTIFICATE=""` como «hay certificado» y revienta al importarlo. + # Solo se exporta lo que de verdad tiene valor. + # + # · CON los secretos de Developer ID → firma real + notarizado + grapado. + # · SIN ellos → identidad "-", que es la firma AD HOC: no engaña a Gatekeeper (habrá + # que seguir abriendo con clic derecho), pero SÍ sella los recursos, ata el + # Info.plist y aplica los entitlements. Estrictamente mejor que no firmar. + # + # El hardened runtime no se configura: en Tauri v2 `bundle.macOS.hardenedRuntime` ya + # viene en true, y es lo que hace que los entitlements signifiquen algo. + - name: Preparar la firma de macOS + if: matrix.platform == 'macos-latest' + shell: bash + env: + CERT: ${{ secrets.APPLE_CERTIFICATE }} + CERT_PW: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + NOTARIZA_ID: ${{ secrets.APPLE_ID }} + NOTARIZA_PW: ${{ secrets.APPLE_PASSWORD }} + NOTARIZA_TEAM: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + # Los valores van por heredoc con delimitador: un base64 largo o con saltos rompería + # la forma `CLAVE=valor`. Nada se imprime: los secretos no pasan por el log. + exportar() { printf '%s<<__FIN__\n%s\n__FIN__\n' "$1" "$2" >> "$GITHUB_ENV"; } + if [ -n "${CERT:-}" ] && [ -n "${CERT_PW:-}" ]; then + exportar APPLE_CERTIFICATE "$CERT" + exportar APPLE_CERTIFICATE_PASSWORD "$CERT_PW" + exportar APPLE_SIGNING_IDENTITY "${IDENTITY:-Developer ID Application}" + echo "Firma: Developer ID (certificado del repo)." + else + exportar APPLE_SIGNING_IDENTITY "-" + echo "Firma: AD HOC. Sella recursos y aplica entitlements, pero Gatekeeper seguirá avisando." + echo "Para firma de verdad, añade los secrets APPLE_CERTIFICATE y APPLE_CERTIFICATE_PASSWORD." + fi + # Notarizar solo tiene sentido con Developer ID; las tres claves van juntas o ninguna. + if [ -n "${CERT:-}" ] && [ -n "${NOTARIZA_ID:-}" ] && [ -n "${NOTARIZA_PW:-}" ] && [ -n "${NOTARIZA_TEAM:-}" ]; then + exportar APPLE_ID "$NOTARIZA_ID" + exportar APPLE_PASSWORD "$NOTARIZA_PW" + exportar APPLE_TEAM_ID "$NOTARIZA_TEAM" + echo "Notarizado: activado." + else + echo "Notarizado: omitido (faltan APPLE_ID, APPLE_PASSWORD o APPLE_TEAM_ID, o no hay certificado)." + fi + - name: Construir, firmar y publicar uses: tauri-apps/tauri-action@v0 env: @@ -61,6 +117,7 @@ jobs: # Firma del updater: la clave privada y su contraseña van como secrets del repo. TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # Las APPLE_* las puso el paso de arriba en $GITHUB_ENV, solo si tenían valor. with: # En un tag usa su nombre (v1.0.0); en ejecución manual, deriva vX.Y.Z de la versión. tagName: ${{ github.ref_type == 'tag' && github.ref_name || 'v__VERSION__' }} @@ -70,6 +127,35 @@ jobs: prerelease: false args: ${{ matrix.args }} + # ── LA RED QUE FALTABA ────────────────────────────────────────────────────────────── + # Que el build no firmara la app de macOS no lo dijo nada durante meses: el .dmg salía, + # se instalaba y funcionaba. Se descubrió a mano, con `codesign -dv`, persiguiendo otra + # cosa. Este paso hace esa comprobación en cada publicación y TUMBA el build si los + # entitlements no llegaron: es barato y es el único aviso que habría existido. + - name: Comprobar la firma de macOS + if: matrix.platform == 'macos-latest' + shell: bash + run: | + set -euo pipefail + APP=$(find src-tauri/target/universal-apple-darwin/release/bundle/macos -maxdepth 1 -name "*.app" | head -1) + if [ -z "$APP" ]; then echo "::error::No apareció ningún .app donde se esperaba"; exit 1; fi + echo "== $APP ==" + codesign -dv --verbose=4 "$APP" 2>&1 || true + echo "== entitlements ==" + codesign -d --entitlements - --xml "$APP" 2>/dev/null | plutil -convert xml1 -o - - || true + echo "== integridad del bundle ==" + codesign --verify --strict --verbose=2 "$APP" + # Gatekeeper: con firma ad hoc esto SIEMPRE dice «rejected», y es esperado. Se + # informa sin tumbar nada; lo que sí es obligatorio es lo de abajo. + echo "== Gatekeeper (informativo) ==" + spctl -a -t exec -vv "$APP" 2>&1 || true + echo "== obligatorio: los entitlements tienen que estar ==" + if ! codesign -d --entitlements - --xml "$APP" 2>/dev/null | grep -q "com.apple.security.cs.allow-jit"; then + echo "::error::La app salió SIN entitlements. Revisa el paso «Preparar la firma de macOS» y bundle.macOS.entitlements en tauri.conf.json." + exit 1 + fi + echo "Entitlements aplicados." + activar: needs: publish runs-on: ubuntu-latest diff --git a/README.md b/README.md index aa1f555..f875906 100644 --- a/README.md +++ b/README.md @@ -229,20 +229,72 @@ Es la forma de mirar un cambio visual —la barra, los colores— sin sacárselo --- -## Avisos de seguridad del sistema (instalación interna sin firmar) +## Firma de macOS -Como los instaladores **no están firmados** con certificado de pago, la primera -vez cada sistema muestra una advertencia. Es normal para apps internas: +### Lo que pasaba hasta la 1.15.0 -- **Windows (SmartScreen):** pantalla azul *"Windows protegió su PC"* → - **Más información → Ejecutar de todos modos**. -- **macOS (Gatekeeper):** *"no se puede abrir porque proviene de un desarrollador - no identificado"* → clic derecho sobre la app → **Abrir** → **Abrir**. (Si - insiste, ejecutar una vez: `xattr -cr "/Applications/Labstream OS.app"`.) +El build **no firmaba** la app de macOS. No se le pasaba ninguna identidad, así que +el bundler de Tauri se saltaba `codesign` entero y el `.dmg` salía con la firma ad hoc +que deja el enlazador. En la app instalada se veía así: -Si en el futuro se distribuye a clientes externos, conviene firmar: -*code signing* en Windows (~150–400 USD/año) y un *Apple Developer ID* + notarizado -en Mac (99 USD/año) para quitar estos avisos. +``` +$ codesign -dv "/Applications/Labstream OS.app" +Identifier=labstream_desktop-0a5819ba37119701 +CodeDirectory ... flags=0x20002(adhoc,linker-signed) +Info.plist=not bound +Sealed Resources=none +TeamIdentifier=not set +``` + +Tres cosas mal, y ninguna avisaba: + +- **`Entitlements.plist` no se aplicaba.** Los permisos de micrófono, cámara y JIT + eran decoración: se escriben al firmar, y no se firmaba. +- **`Info.plist=not bound` y `Sealed Resources=none`.** Ni el Info.plist ni los + recursos iban dentro de la firma: cualquiera podía cambiarlos sin romperla. +- **Identificador de enlazador** (`labstream_desktop-`) en vez del de la app + (`co.labstream.os`), que es por lo que macOS recuerda los permisos concedidos. + +### Lo que hace ahora (desde la 1.16.0) + +`build.yml` decide según los secrets que haya en el repo: + +| Secrets | Resultado | +| --- | --- | +| ninguno | Firma **ad hoc** (`-s -`) con hardened runtime y los entitlements. Gatekeeper sigue avisando, pero el bundle va sellado y los permisos se aplican. | +| `APPLE_CERTIFICATE` + `APPLE_CERTIFICATE_PASSWORD` | Firma real con **Developer ID**. Gatekeeper deja de avisar en cualquier Mac. | +| …y además `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` | Firma real + **notarizado y grapado**. Ni el primer arranque avisa. | + +Y hay un paso que **tumba la publicación** si los entitlements no llegaron al `.app`. +Ese paso es la lección: que esto estuviera roto no lo dijo nada durante meses. + +### Pasar a Developer ID (99 USD/año) + +1. Alta en el Apple Developer Program. +2. En *Certificates* crear uno de tipo **Developer ID Application** y exportarlo del + Llavero como `.p12` con contraseña. +3. Convertirlo a texto: `base64 -i certificado.p12 | pbcopy`. +4. En GitHub → *Settings ▸ Secrets and variables ▸ Actions*, añadir: + - `APPLE_CERTIFICATE` — lo que quedó en el portapapeles. + - `APPLE_CERTIFICATE_PASSWORD` — la contraseña del `.p12`. + - `APPLE_SIGNING_IDENTITY` — opcional; por defecto `Developer ID Application`. +5. Para notarizar, además: + - `APPLE_ID` — el correo de la cuenta de desarrollador. + - `APPLE_PASSWORD` — una **contraseña específica de app** (appleid.apple.com, no la + del Apple ID). + - `APPLE_TEAM_ID` — el identificador de equipo de 10 caracteres. + +No hay que tocar ni un archivo: en cuanto los secrets existan, la siguiente etiqueta +sale firmada. + +### Mientras siga ad hoc + +- **macOS (Gatekeeper):** *"no se puede abrir porque proviene de un desarrollador no + identificado"* → clic derecho sobre la app → **Abrir** → **Abrir**. (Si insiste: + `xattr -cr "/Applications/Labstream OS.app"`.) +- **Windows (SmartScreen):** *"Windows protegió su PC"* → **Más información → + Ejecutar de todos modos**. Windows es aparte: su firma cuesta 150–400 USD/año y no + la toca nada de lo de arriba. --- @@ -257,10 +309,10 @@ labstream-desktop/ │ ├── tauri.conf.json nombre, versión, iconos, instaladores, entitlements │ ├── Cargo.toml dependencias Rust + versión │ ├── Info.plist textos de permiso (micrófono/cámara) en macOS -│ ├── Entitlements.plist entitlements de macOS (audio-input, cámara, red) +│ ├── Entitlements.plist entitlements de macOS (micrófono, cámara, JIT) │ ├── build.rs │ ├── capabilities/default.json │ ├── icons/ se generan desde app-icon.png │ └── src/{main.rs,lib.rs} lib.rs crea la ventana + SERVER_URL + tray -└── .github/workflows/build.yml build automático de .exe (Windows) y .dmg (macOS) +└── .github/workflows/build.yml build de .exe (Windows) y .dmg (macOS) + firma de macOS ``` diff --git a/package.json b/package.json index 57cbef3..55f4f58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "labstream-desktop", - "version": "1.15.0", + "version": "1.16.0", "private": true, "description": "App de escritorio (Windows) que envuelve Labstream OS — cliente del servidor del NAS.", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8582e7e..f5c5d3b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2366,7 +2366,7 @@ dependencies = [ [[package]] name = "labstream-desktop" -version = "1.15.0" +version = "1.16.0" dependencies = [ "active-win-pos-rs", "device_query", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 72f2455..fc38f41 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "labstream-desktop" -version = "1.15.0" +version = "1.16.0" description = "Cliente de escritorio de Labstream OS" authors = ["Labstream SAS"] edition = "2021" diff --git a/src-tauri/Entitlements.plist b/src-tauri/Entitlements.plist index f6ad93a..dd2b79d 100644 --- a/src-tauri/Entitlements.plist +++ b/src-tauri/Entitlements.plist @@ -1,17 +1,39 @@ + - + com.apple.security.device.audio-input - + com.apple.security.device.camera - - com.apple.security.network.client - - + com.apple.security.cs.allow-jit diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 82724a9..1e6217a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Labstream OS", - "version": "1.15.0", + "version": "1.16.0", "identifier": "co.labstream.os", "build": { "frontendDist": "../dist"